-- Copyright 2007-2014 Mitchell mitchell.att.foicica.com. See LICENSE. -- Contributions from Robert Gieseke. local M = {} --[[ This comment is for LuaDoc. --- -- Defines the menus used by Textadept. -- Menus are simply tables and may be edited in place. Submenus have `title` -- keys with string text. Use the '#' operator (instead of `ipairs()`) for -- iteration. -- If applicable, load this module last in your *~/.textadept/init.lua*, after -- [`textadept.keys`]() since it looks up defined key commands to show them in -- menus. module('textadept.menu')]] local _L, buffer, view = _L, buffer, view local editing, utils = textadept.editing, textadept.keys.utils local SEPARATOR = {''} --- -- The default main menubar. -- @class table -- @name menubar local menubar = { { title = _L['_File'], {_L['_New'], buffer.new}, {_L['_Open'], io.open_file}, {_L['Open _Recent...'], io.open_recent_file}, {_L['Re_load'], io.reload_file}, {_L['_Save'], io.save_file}, {_L['Save _As'], io.save_file_as}, {_L['Save All'], io.save_all_files}, SEPARATOR, {_L['_Close'], io.close_buffer}, {_L['Close All'], io.close_all_buffers}, SEPARATOR, {_L['Loa_d Session...'], textadept.session.load}, {_L['Sav_e Session...'], textadept.session.save}, SEPARATOR, {_L['_Quit'], quit}, }, { title = _L['_Edit'], {_L['_Undo'], buffer.undo}, {_L['_Redo'], buffer.redo}, SEPARATOR, {_L['Cu_t'], buffer.cut}, {_L['_Copy'], buffer.copy}, {_L['_Paste'], buffer.paste}, {_L['Duplicate _Line'], buffer.line_duplicate}, {_L['_Delete'], buffer.clear}, {_L['D_elete Word'], utils.delete_word}, {_L['Select _All'], buffer.select_all}, SEPARATOR, {_L['_Match Brace'], editing.match_brace}, {_L['Complete _Word'], {editing.autocomplete, 'word'}}, {_L['_Highlight Word'], editing.highlight_word}, {_L['Toggle _Block Comment'], editing.block_comment}, {_L['T_ranspose Characters'], editing.transpose_chars}, {_L['_Join Lines'], editing.join_lines}, {_L['_Filter Through'], {ui.command_entry.enter_mode, 'filter_through'}}, { title = _L['_Select'], {_L['Select to _Matching Brace'], {editing.match_brace, 'select'}}, {_L['Select between _XML Tags'], {editing.select_enclosed, '>', '<'}}, {_L['Select in XML _Tag'], {editing.select_enclosed, '<', '>'}}, {_L['Select in _Single Quotes'], {editing.select_enclosed, "'", "'"}}, {_L['Select in _Double Quotes'], {editing.select_enclosed, '"', '"'}}, {_L['Select in _Parentheses'], {editing.select_enclosed, '(', ')'}}, {_L['Select in _Brackets'], {editing.select_enclosed, '[', ']'}}, {_L['Select in B_races'], {editing.select_enclosed, '{', '}'}}, {_L['Select _Word'], editing.select_word}, {_L['Select _Line'], editing.select_line}, {_L['Select Para_graph'], editing.select_paragraph}, }, { title = _L['Selectio_n'], {_L['_Upper Case Selection'], buffer.upper_case}, {_L['_Lower Case Selection'], buffer.lower_case}, SEPARATOR, {_L['Enclose as _XML Tags'], utils.enclose_as_xml_tags}, {_L['Enclose as Single XML _Tag'], {editing.enclose, '<', ' />'}}, {_L['Enclose in Single _Quotes'], {editing.enclose, "'", "'"}}, {_L['Enclose in _Double Quotes'], {editing.enclose, '"', '"'}}, {_L['Enclose in _Parentheses'], {editing.enclose, '(', ')'}}, {_L['Enclose in _Brackets'], {editing.enclose, '[', ']'}}, {_L['Enclose in B_races'], {editing.enclose, '{', '}'}}, SEPARATOR, {_L['_Move Selected Lines Up'], buffer.move_selected_lines_up}, {_L['Move Selected Lines Do_wn'], buffer.move_selected_lines_down}, }, }, { title = _L['_Search'], {_L['_Find'], ui.find.focus}, {_L['Find _Next'], ui.find.find_next}, {_L['Find _Previous'], ui.find.find_prev}, {_L['_Replace'], ui.find.replace}, {_L['Replace _All'], ui.find.replace_all}, {_L['Find _Incremental'], ui.find.find_incremental}, SEPARATOR, {_L['Find in Fi_les'], utils.find_in_files}, {_L['Goto Nex_t File Found'], {ui.find.goto_file_found, false, true}}, {_L['Goto Previou_s File Found'], {ui.find.goto_file_found, false, false}}, SEPARATOR, {_L['_Jump to'], editing.goto_line}, }, { title = _L['_Tools'], {_L['Command _Entry'], {ui.command_entry.enter_mode, 'lua_command', 'lua'}}, {_L['Select Co_mmand'], utils.select_command}, SEPARATOR, {_L['_Run'], textadept.run.run}, {_L['_Compile'], textadept.run.compile}, {_L['Buil_d'], textadept.run.build}, {_L['S_top'], textadept.run.stop}, {_L['_Next Error'], {textadept.run.goto_error, false, true}}, {_L['_Previous Error'], {textadept.run.goto_error, false, false}}, SEPARATOR, { title = _L['_Bookmark'], {_L['_Toggle Bookmark'], textadept.bookmarks.toggle}, {_L['_Clear Bookmarks'], textadept.bookmarks.clear}, {_L['_Next Bookmark'], {textadept.bookmarks.goto_mark, true}}, {_L['_Previous Bookmark'], {textadept.bookmarks.goto_mark, false}}, {_L['_Goto Bookmark...'], textadept.bookmarks.goto_mark}, }, { title = _L['Snap_open'], {_L['Snapopen _User Home'], {io.snapopen, _USERHOME}}, {_L['Snapopen _Textadept Home'], {io.snapopen, _HOME}}, {_L['Snapopen _Current Directory'], utils.snapopen_filedir}, {_L['Snapopen Current _Project'], io.snapopen}, }, { title = _L['_Snippets'], {_L['_Insert Snippet...'], textadept.snippets._select}, {_L['_Expand Snippet/Next Placeholder'], textadept.snippets._insert}, {_L['_Previous Snippet Placeholder'], textadept.snippets._previous}, {_L['_Cancel Snippet'], textadept.snippets._cancel_current}, }, SEPARATOR, {_L['_Complete Symbol'], utils.autocomplete_symbol}, {_L['Show _Documentation'], textadept.editing.show_documentation}, {_L['Show St_yle'], utils.show_style}, }, { title = _L['_Buffer'], {_L['_Next Buffer'], {view.goto_buffer, view, 1, true}}, {_L['_Previous Buffer'], {view.goto_buffer, view, -1, true}}, {_L['_Switch to Buffer...'], ui.switch_buffer}, SEPARATOR, { title = _L['_Indentation'], {_L['Tab width: _2'], {utils.set_indentation, 2}}, {_L['Tab width: _3'], {utils.set_indentation, 3}}, {_L['Tab width: _4'], {utils.set_indentation, 4}}, {_L['Tab width: _8'], {utils.set_indentation, 8}}, SEPARATOR, {_L['_Toggle Use Tabs'], {utils.toggle_property, 'use_tabs'}}, {_L['_Convert Indentation'], editing.convert_indentation}, }, { title = _L['_EOL Mode'], {_L['CRLF'], {utils.set_eol_mode, buffer.EOL_CRLF}}, {_L['CR'], {utils.set_eol_mode, buffer.EOL_CR}}, {_L['LF'], {utils.set_eol_mode, buffer.EOL_LF}}, }, { title = _L['E_ncoding'], {_L['_UTF-8 Encoding'], {utils.set_encoding, 'UTF-8'}}, {_L['_ASCII Encoding'], {utils.set_encoding, 'ASCII'}}, {_L['_ISO-8859-1 Encoding'], {utils.set_encoding, 'ISO-8859-1'}}, {_L['_MacRoman Encoding'], {utils.set_encoding, 'MacRoman'}}, {_L['UTF-1_6 Encoding'], {utils.set_encoding, 'UTF-16LE'}}, }, SEPARATOR, {_L['Select _Lexer...'], textadept.file_types.select_lexer}, {_L['_Refresh Syntax Highlighting'], {buffer.colourise, buffer, 0, -1}}, }, { title = _L['_View'], {_L['_Next View'], {ui.goto_view, 1, true}}, {_L['_Previous View'], {ui.goto_view, -1, true}}, SEPARATOR, {_L['Split View _Horizontal'], {view.split, view}}, {_L['Split View _Vertical'], {view.split, view, true}}, {_L['_Unsplit View'], {view.unsplit, view}}, {_L['Unsplit _All Views'], utils.unsplit_all}, {_L['_Grow View'], utils.grow}, {_L['Shrin_k View'], utils.shrink}, SEPARATOR, {_L['Toggle Current _Fold'], utils.toggle_current_fold}, SEPARATOR, {_L['Toggle View _EOL'], {utils.toggle_property, 'view_eol'}}, {_L['Toggle _Wrap Mode'], {utils.toggle_property, 'wrap_mode'}}, {_L['Toggle Show In_dent Guides'], {utils.toggle_property, 'indentation_guides'}}, {_L['Toggle View White_space'], {utils.toggle_property, 'view_ws'}}, {_L['Toggle _Virtual Space'], {utils.toggle_property, 'virtual_space_options', buffer.VS_USERACCESSIBLE}}, SEPARATOR, {_L['Zoom _In'], buffer.zoom_in}, {_L['Zoom _Out'], buffer.zoom_out}, {_L['_Reset Zoom'], utils.reset_zoom}, }, { title = _L['_Help'], {_L['Show _Manual'], {utils.open_webpage, _HOME..'/doc/manual.html'}}, {_L['Show _LuaDoc'], {utils.open_webpage, _HOME..'/doc/api.html'}}, SEPARATOR, {_L['_About'], {ui.dialogs.msgbox, {title = 'Textadept', text = _RELEASE, informative_text = 'Copyright © 2007-2014 Mitchell. See LICENSE\n'.. 'http://foicica.com/textadept', icon_file = _HOME..'/core/images/ta_64x64.png'}}}, }, } --- -- The default right-click context menu. -- @class table -- @name context_menu local context_menu = { {_L['_Undo'], buffer.undo}, {_L['_Redo'], buffer.redo}, SEPARATOR, {_L['Cu_t'], buffer.cut}, {_L['_Copy'], buffer.copy}, {_L['_Paste'], buffer.paste}, {_L['_Delete'], buffer.clear}, SEPARATOR, {_L['Select _All'], buffer.select_all} } --- -- The default tabbar context menu. -- @class table -- @name tab_context_menu local tab_context_menu = { {_L['_Close'], io.close_buffer}, SEPARATOR, {_L['_Save'], io.save_file}, {_L['Save _As'], io.save_file_as}, SEPARATOR, {_L['Re_load'], io.reload_file}, } -- Table of proxy tables for menus. local proxies = {} local key_shortcuts, menu_actions, contextmenu_actions, items, commands -- Returns the GDK integer keycode and modifier mask for a key sequence. -- This is used for creating menu accelerators. -- @param key_seq The string key sequence. -- @return keycode and modifier mask local function get_gdk_key(key_seq) if not key_seq then return nil end local mods, key = key_seq:match('^([cams]*)(.+)$') if not mods or not key then return nil end local modifiers = ((mods:find('s') or key:lower() ~= key) and 1 or 0) + (mods:find('c') and 4 or 0) + (mods:find('a') and 8 or 0) + (mods:find('m') and 268435456 or 0) local byte = string.byte(key) if #key > 1 or byte < 32 then for i, s in pairs(keys.KEYSYMS) do if s == key and i > 0xFE20 then byte = i break end end end return byte, modifiers end -- Get a string uniquely identifying a key binding. -- This is used to match menu items with key bindings to show the key shortcut. -- @param f A value in the `keys` table. local function get_id(f) local id = '' if type(f) == 'function' then id = tostring(f) elseif type(f) == 'table' then for i = 1, #f do id = id..tostring(f[i]) end end return id end -- Creates a menu suitable for `ui.menu()` from the menu table format. -- Also assigns key commands. -- @param menu The menu to create a GTK+ menu from. -- @param contextmenu Flag indicating whether or not the menu is a context menu. -- If so, menu_id offset is 1000. The default value is `false`. -- @return GTK+ menu that can be passed to `ui.menu()`. -- @see ui.menu local function read_menu_table(menu, contextmenu) local gtkmenu = {} gtkmenu.title = menu.title for _, menuitem in ipairs(menu) do if menuitem.title then gtkmenu[#gtkmenu + 1] = read_menu_table(menuitem, contextmenu) else local label, f = menuitem[1], menuitem[2] local menu_id = not contextmenu and #menu_actions + 1 or #contextmenu_actions + 1000 + 1 local key, mods = get_gdk_key(key_shortcuts[get_id(f)]) gtkmenu[#gtkmenu + 1] = {label, menu_id, key, mods} if f then local actions = not contextmenu and menu_actions or contextmenu_actions actions[menu_id < 1000 and menu_id or menu_id - 1000] = f end end end return gtkmenu end -- Builds the item and commands tables for the filtered list dialog. -- @param menu The menu to read from. -- @param title The title of the menu. -- @param items The current list of items. -- @param commands The current list of commands. local function build_command_tables(menu, title, items, commands) for _, menuitem in ipairs(menu) do if menuitem.title then build_command_tables(menuitem, menuitem.title, items, commands) elseif menuitem[1] ~= '' then local label, f = menuitem[1], menuitem[2] if title then label = title..': '..label end items[#items + 1] = label:gsub('_([^_])', '%1') items[#items + 1] = key_shortcuts[get_id(f)] or '' commands[#commands + 1] = f end end end -- Returns a proxy table for menu table *menu* such that when a menu item is -- changed or added, *update* is called to update the menu in the UI. -- @param menu The menu or table of menus to create a proxy for. -- @param update The function to call to update the menu in the UI when a menu -- item is changed or added. -- @param menubar Used internally to keep track of the top-level menu for -- calling *update* with. local function proxy_menu(menu, update, menubar) return setmetatable({}, { __index = function(t, k) local v = menu[k] return type(v) == 'table' and proxy_menu(v, update, menubar or menu) or v end, __newindex = function(t, k, v) menu[k] = getmetatable(v) and getmetatable(v).menu or v update(menubar or menu) end, __len = function(t) return #menu end, menu = menu -- store existing menu for copying (e.g. m[#m + 1] = m[#m]) }) end -- Sets `ui.menubar` from menu table *menubar*. -- Each menu is an ordered list of menu items and has a `title` key for the -- title text. Menu items are tables containing menu text and either a function -- to call or a table containing a function with its parameters to call when an -- item is clicked. Menu items may also be sub-menus, ordered lists of menu -- items with an additional `title` key for the sub-menu's title text. -- @param menubar The table of menu tables to create the menubar from. -- @see ui.menubar -- @see ui.menu local function set_menubar(menubar) key_shortcuts = {} for key, f in pairs(keys) do key_shortcuts[get_id(f)] = key end menu_actions = {} local _menubar = {} for i = 1, #menubar do _menubar[#_menubar + 1] = ui.menu(read_menu_table(menubar[i])) end ui.menubar = _menubar items, commands = {}, {} build_command_tables(menubar, nil, items, commands) proxies.menubar = proxy_menu(menubar, set_menubar) end set_menubar(menubar) -- Sets `ui.context_menu` and `ui.tab_context_menu` from menu item lists -- *buffer_menu* and *tab_menu*, respectively. -- Menu items are tables containing menu text and either a function to call or -- a table containing a function with its parameters to call when an item is -- clicked. Menu items may also be sub-menus, ordered lists of menu items with -- an additional `title` key for the sub-menu's title text. -- @param buffer_menu Optional menu table to create the buffer context menu -- from. If `nil`, uses the default context menu. -- @param tab_menu Optional menu table to create the tabbar context menu from. -- If `nil`, uses the default tab context menu. -- @see ui.context_menu -- @see ui.tab_context_menu -- @see ui.menu local function set_contextmenus(buffer_menu, tab_menu) contextmenu_actions = {} local menu = buffer_menu or context_menu ui.context_menu = ui.menu(read_menu_table(menu, true)) proxies.context_menu = proxy_menu(menu, set_contextmenus) menu = tab_menu or tab_context_menu ui.tab_context_menu = ui.menu(read_menu_table(menu, true)) proxies.tab_context_menu = proxy_menu(menu, function() set_contextmenus(nil, menu) end) end if not CURSES then set_contextmenus() end --- -- Prompts the user to select a menu command to run. -- @name select_command function M.select_command() local button, i = ui.dialogs.filteredlist{ title = _L['Run Command'], columns = {_L['Command'], _L['Key Command']}, items = items, width = CURSES and ui.size[1] - 2 or nil } if button ~= 1 or not i then return end keys.run_command(commands[i], type(commands[i])) end -- Performs the appropriate action when clicking a menu item. events.connect(events.MENU_CLICKED, function(menu_id) local actions = menu_id < 1000 and menu_actions or contextmenu_actions local action = actions[menu_id < 1000 and menu_id or menu_id - 1000] assert(type(action) == 'function' or type(action) == 'table', _L['Unknown command:']..' '..tostring(action)) keys.run_command(action, type(action)) end) if not CURSES then -- Set a language-specific context menu or the default one. local function set_language_contextmenu() local lang = _G.buffer:get_lexer() set_contextmenus(_M[lang] and _M[lang].context_menu, _M[lang] and _M[lang].tab_context_menu) end events.connect(events.LEXER_LOADED, set_language_contextmenu) events.connect(events.BUFFER_AFTER_SWITCH, set_language_contextmenu) events.connect(events.VIEW_AFTER_SWITCH, set_language_contextmenu) events.connect(events.BUFFER_NEW, set_language_contextmenu) end return setmetatable(M, { __index = function(t, k) return proxies[k] or M[k] end, __newindex = function(t, k, v) if k == 'menubar' then set_menubar(v) elseif k == 'context_menu' then set_contextmenus(v) elseif k == 'tab_context_menu' then set_contextmenus(nil, v) else M[k] = v end end })