diff --git a/docs/en_US/images/preferences_browser_keyboard_shortcuts.png b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png new file mode 100644 index 000000000..36c31d39f Binary files /dev/null and b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png differ diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 8697e99ce..fe0737c26 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -3,6 +3,7 @@ Keyboard Shortcuts ****************** Keyboard shortcuts are provided in pgAdmin to allow easy access to specific functions. +The shortcuts can be configured through File > Preferences dialogue as per the need. **Desktop Runtime** @@ -27,6 +28,27 @@ When running in the Desktop Runtime, the following keyboard shortcuts are availa | Ctrl+0 | Cmd+0 | Reset the zoom level | +--------------------------+----------------+---------------------------------------+ +**Main Browser Window** + +When using main browser window, the following keyboard shortcuts are available: + ++---------------------------+--------------------------------------------------------+ +| Shortcut for all platform | Function | ++===========================+========================================================+ +| Alt+Shift+F | Open the File menu | ++---------------------------+--------------------------------------------------------+ +| Alt+Shift+O | Open the Object menu | ++---------------------------+--------------------------------------------------------+ +| Alt+Shift+L | Open the Tools menu | ++---------------------------+--------------------------------------------------------+ +| Alt+Shift+H | Open the Help menu | ++---------------------------+--------------------------------------------------------+ +| Alt+Shift+B | Focus the browser tree | ++---------------------------+--------------------------------------------------------+ +| Alt+Shift+[ | Move tabbed panel backward/forward | +| Alt+Shift+] | | ++---------------------------+--------------------------------------------------------+ + **SQL Editors** diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index ce3de7246..6211edc5b 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -20,6 +20,13 @@ Use the fields on the *Display* panel to specify general display preferences: * When the *Show system objects* switch is set to *True*, the client will display system objects such as system schemas (for example, *pg_temp*) or system columns (for example, *xmin* or *ctid*) in the tree control. +Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the main window navigation: + +.. image:: images/preferences_browser_keyboard_shortcuts.png + :alt: Preferences dialog browser keyboard shortcuts section + +* The panel displays a list of keyboard shortcuts available for the main window; select the combination of the modifier keys along with the key to configure each shortcut. + Use the fields on the *Nodes* panel to select the object types that will be displayed in the *Browser* tree control: .. image:: images/preferences_browser_nodes.png diff --git a/libraries.txt b/libraries.txt index 5d10cc00f..17f85cfc5 100644 --- a/libraries.txt +++ b/libraries.txt @@ -37,3 +37,4 @@ BigNumber 3.0.1 MIT http://mikemcl.github.io/bignumb Source Code Pro 1.1 SIL OFL https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700 Open Sans 2.0 AL https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,700 Spectrum 1.8 MIT https://bgrins.github.io/spectrum/ +Mousetrap 1.6.1 AL https://github.com/ccampbell/mousetrap diff --git a/web/package.json b/web/package.json index 85427972e..01a55ec4b 100644 --- a/web/package.json +++ b/web/package.json @@ -70,6 +70,7 @@ "jquery-contextmenu": "^2.5.0", "jquery-ui": "^1.12.1", "moment": "^2.18.1", + "mousetrap": "^1.6.1", "prop-types": "^15.5.10", "react": "file:../web/pgadmin/static/vendor/react", "react-dom": "file:../web/pgadmin/static/vendor/react-dom", diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 038ef4fde..d3df08453 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -213,7 +213,117 @@ class BrowserModule(PgAdminModule): gettext("Count rows if estimated less than"), 'integer', 2000, category_label=gettext('Properties') ) + fields = [ + {'name': 'alt', 'type': 'checkbox', 'label': gettext('Alt / Option')}, + {'name': 'shift', 'type': 'checkbox', 'label': gettext('Shift')}, + {'name': 'control', 'type': 'checkbox', 'label': gettext('Ctrl')}, + {'name': 'key', 'type': 'keyCode', 'label': gettext('Key')} + ] + self.preference.register( + 'keyboard_shortcuts', + 'browser_tree', + gettext('Browser tree'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 66, 'char': 'b'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'tabbed_panel_backward', + gettext('Tabbed panel backward'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 91, 'char': '['} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'tabbed_panel_forward', + gettext('Tabbed panel forward'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 93, 'char': ']'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'main_menu_file', + gettext('File main menu'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 70, 'char': 'f'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'main_menu_object', + gettext('Object main menu'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 79, 'char': 'o'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'main_menu_tools', + gettext('Tools main menu'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 76, 'char': 'l'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'main_menu_help', + gettext('Help main menu'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': True, + 'control': False, + 'key': {'key_code': 72, 'char': 'h'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) def get_exposed_url_endpoints(self): """ Returns: diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 061bd4e0a..a6213902e 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -8,6 +8,7 @@ define('pgadmin.browser', [ 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', + 'pgadmin.browser.keyboard', ], function( gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify, codemirror, checkNodeVisibility @@ -544,7 +545,7 @@ define('pgadmin.browser', [ menus[m.name] = new MenuItem({ name: m.name, label: m.label, module: m.module, category: m.category, callback: m.callback, - priority: m.priority, data: m.data, url: m.url, + priority: m.priority, data: m.data, url: m.url || '#', target: m.target, icon: m.icon, enable: (m.enable == '' ? true : (_.isString(m.enable) && m.enable.toLowerCase() == 'false') ? @@ -678,6 +679,7 @@ define('pgadmin.browser', [ url: url_for('preferences.get_all'), success: function(res) { self.preferences_cache = res; + pgBrowser.keyboardNavigation.init(); }, error: function(xhr) { try { @@ -1958,8 +1960,8 @@ define('pgadmin.browser', [ pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab'; } - window.onbeforeunload = function(ev) { - var e = ev || window.event, + window.onbeforeunload = function() { + var e = window.event, msg = S(gettext('Are you sure you wish to close the %s browser?')).sprintf(pgBrowser.utils.app_name).value(); // For IE and Firefox prior to version 4 @@ -1971,5 +1973,6 @@ define('pgadmin.browser', [ return msg; }; + return pgAdmin.Browser; }); diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js new file mode 100644 index 000000000..250d0d348 --- /dev/null +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -0,0 +1,159 @@ +/* eslint-disable */ +define( + ['underscore', 'underscore.string', 'sources/pgadmin', 'jquery', 'mousetrap'], +function(_, S, pgAdmin, $, Mousetrap) { + 'use strict'; + + var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {}; + + pgBrowser.keyboardNavigation = pgBrowser.keyboardNavigation || {}; + + _.extend(pgBrowser.keyboardNavigation, { + init: function() { + Mousetrap.reset(); + if (pgBrowser.preferences_cache.length > 0) { + this.keyboardShortcut = { + 'file_shortcut': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_file').value), + 'object_shortcut': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_object').value), + 'tools_shortcut': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_tools').value), + 'help_shortcut': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_help').value), + 'left_tree_shortcut': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'browser_tree').value), + 'tabbed_panel_backward': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'tabbed_panel_backward').value), + 'tabbed_panel_forward': pgBrowser.keyboardNavigation.parseShortcutValue(pgBrowser.get_preference('browser', 'tabbed_panel_forward').value) + }; + this.shortcutMethods = { + 'bindMainMenu': {'shortcuts': [this.keyboardShortcut.file_shortcut, + this.keyboardShortcut.object_shortcut, this.keyboardShortcut.tools_shortcut, + this.keyboardShortcut.help_shortcut]}, // Main menu + 'bindRightPanel': {'shortcuts': [this.keyboardShortcut.tabbed_panel_backward, this.keyboardShortcut.tabbed_panel_forward]}, // Main window panels + 'bindMainMenuLeft': {'shortcuts': 'left', 'bindElem': '.pg-navbar'}, // Main menu + 'bindMainMenuRight': {'shortcuts': 'right', 'bindElem': '.pg-navbar'}, // Main menu + 'bindMainMenuUpDown': {'shortcuts': ['up', 'down']}, // Main menu + 'bindLeftTree': {'shortcuts': this.keyboardShortcut.left_tree_shortcut}, // Main menu + }; + this.bindShortcuts(); + } + }, + bindShortcuts: function() { + var self = this; + _.each(self.shortcutMethods, function(keyCombo, callback) { + self._bindWithMousetrap(keyCombo.shortcuts, self[callback], keyCombo.bindElem); + }); + }, + _bindWithMousetrap: function(shortcuts, callback, bindElem) { + if (bindElem) { + var elem = document.querySelector(bindElem); + Mousetrap(elem).bind(shortcuts, function() { + callback.apply(this, arguments); + }.bind(elem)); + } else { + Mousetrap.bind(shortcuts, function() { + callback.apply(this, arguments); + }); + } + }, + attachShortcut: function(shortcut, callback, bindElem) { + this._bindWithMousetrap(shortcut, callback, bindElem); + }, + detachShortcut: function(shortcut, bindElem) { + if (bindElem) Mousetrap(bindElem).unbind(shortcut); + else Mousetrap.unbind(shortcut); + }, + bindMainMenu: function(e, combo) { + var shortcut_obj = pgAdmin.Browser.keyboardNavigation.keyboardShortcut; + if (combo == shortcut_obj.file_shortcut) $('#mnu_file a.dropdown-toggle').dropdown('toggle'); + if (combo == shortcut_obj.object_shortcut) $('#mnu_obj a.dropdown-toggle').first().dropdown('toggle'); + if (combo == shortcut_obj.tools_shortcut) $('#mnu_tools a.dropdown-toggle').dropdown('toggle'); + if (combo == shortcut_obj.help_shortcut) $('#mnu_help a.dropdown-toggle').dropdown('toggle'); + }, + bindRightPanel: function(e, combo) { + var allPanels = pgAdmin.Browser.docker.findPanels(), + activePanel = 0, + nextPanel = allPanels.length, + prevPanel = 1, + activePanelId = 0, + activePanelFlag = false, + shortcut_obj = pgAdmin.Browser.keyboardNavigation.keyboardShortcut; + + _.each(pgAdmin.Browser.docker.findPanels(), function(panel, index){ + if (panel.isVisible() && !activePanelFlag && panel._type != 'browser'){ + activePanelId = index; + activePanelFlag = true; + } + }); + + if (combo == shortcut_obj.tabbed_panel_backward) activePanel = (activePanelId > 0) ? activePanelId - 1 : prevPanel; + else if (combo == shortcut_obj.tabbed_panel_forward) activePanel = (activePanelId < nextPanel) ? activePanelId + 1 : nextPanel; + + pgAdmin.Browser.docker.findPanels()[activePanel].focus(); + setTimeout(function() { + if (document.activeElement instanceof HTMLIFrameElement) { + document.activeElement.blur(); + } + }, 1000); + }, + bindMainMenuLeft: function(e) { + var prevMenu; + if ($(e.target).hasClass('menu-link')) { // Menu items + prevMenu = $(e.target).parent().parent().parent().prev('.dropdown'); + } + else if ($(e.target).parent().hasClass('dropdown-submenu')) { // Sub menu + $(e.target).parent().toggleClass('open'); + return; + } + else { //Menu headers + prevMenu = $(e.target).parent().prev('.dropdown'); + } + + if (prevMenu.hasClass('hide')) prevMenu = prevMenu.prev('.dropdown'); // Skip hidden menus + + prevMenu.find('a:first').dropdown('toggle'); + }, + bindMainMenuRight: function(e) { + var nextMenu; + if ($(e.target).hasClass('menu-link')) { // Menu items + nextMenu = $(e.target).parent().parent().parent().next('.dropdown'); + } + else if ($(e.target).parent().hasClass('dropdown-submenu')) { // Sub menu + $(e.target).parent().toggleClass('open'); + return; + } + else { //Menu headers + nextMenu = $(e.target).parent().next('.dropdown'); + } + + if (nextMenu.hasClass('hide')) nextMenu = nextMenu.next('.dropdown'); // Skip hidden menus + + nextMenu.find('a:first').dropdown('toggle'); + }, + bindMainMenuUpDown: function(e, combo) { + // Handle Sub-menus + if (combo == 'up' && $(e.target).parent().prev().prev('.dropdown-submenu').length > 0) { + $(e.target).parent().prev().prev('.dropdown-submenu').find('a:first').focus(); + } else { + if ($(e.target).parent().hasClass('dropdown-submenu')) { + $(e.target).parent().parent().parent().find('a:first').dropdown('toggle'); + $(e.target).parent().parent().children().eq(2).find('a:first').focus(); + } + } + }, + bindLeftTree: function() { + var t = pgAdmin.Browser.tree, + item = t.selected().length > 0 ? t.selected() : t.first(); + $('#tree').focus(); + t.focus(item); + t.select(item); + }, + parseShortcutValue: function(obj) { + var shortcut = ""; + if (obj.alt) { shortcut += 'alt+'; } + if (obj.shift) { shortcut += 'shift+'; } + if (obj.control) { shortcut += 'ctrl+'; } + shortcut += String.fromCharCode(obj.key.key_code).toLowerCase(); + return shortcut; + } + + }); + + return pgAdmin.keyboardNavigation; +}); diff --git a/web/pgadmin/browser/static/js/panel.js b/web/pgadmin/browser/static/js/panel.js index f08506769..ab0681a96 100644 --- a/web/pgadmin/browser/static/js/panel.js +++ b/web/pgadmin/browser/static/js/panel.js @@ -42,7 +42,8 @@ define( if (!that.showTitle) myPanel.title(false); else { - myPanel.title(title || that.title); + var title_elem = '' + (title || that.title) + ''; + myPanel.title(title_elem); if (that.icon != '') myPanel.icon(that.icon); } diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index f2ff74465..58ff43f8d 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -131,37 +131,37 @@ window.onload = function(e){ -