Fix keyboard navigation in "inner" tabsets such as the Query Tool and Debugger. Fixes #4195

This commit is contained in:
Aditya Toshniwal 2019-06-10 11:10:49 +01:00 committed by Dave Page
parent 321b445a7e
commit f561c0cee6
11 changed files with 227 additions and 95 deletions

View File

@ -193,8 +193,8 @@ When using the Debugger, the following shortcuts are available:
| Alt + Shift + q | Option + Shift + q | Enter or Edit values in Grid |
+--------------------------+--------------------+-----------------------------------+
Inner Panel Navigation
**********************
Inner Tab and Panel Navigation
******************************
When using the Query Tool and Debugger, the following shortcuts are available
for inner panel navigation:
@ -203,13 +203,15 @@ for inner panel navigation:
:class: longtable
:widths: 2 2 3
+--------------------------+---------------------+------------------------------+
| Shortcut (Windows/Linux) | Shortcut (Mac) | Function |
+==========================+=====================+==============================+
| Alt + Shift + Right | Alt + Shift + Right | Move to next inner panel |
+--------------------------+---------------------+------------------------------+
| Alt + Shift + Left | Alt + Shift + Left | Move to previous inner panel |
+--------------------------+---------------------+------------------------------+
+--------------------------+---------------------------+------------------------------------+
| Shortcut (Windows/Linux) | Shortcut (Mac) | Function |
+==========================+===========================+====================================+
| Alt + Shift + ] | Alt + Shift + ] | Move to next tab within a panel |
+--------------------------+---------------------------+------------------------------------+
| Alt + Shift + [ | Alt + Shift + [ | Move to previous tab within a panel|
+--------------------------+---------------------------+------------------------------------+
| Alt + Shift + Tab | Alt + Shift + Tab | Move between inner panels |
+--------------------------+---------------------------+------------------------------------+
Access Key
**********

View File

@ -14,6 +14,7 @@ Bug fixes
*********
| `Bug #4171 <https://redmine.postgresql.org/issues/4171>`_ - Fix issue where reverse engineered SQL was failing for foreign tables, if it had "=" in the options.
| `Bug #4195 <https://redmine.postgresql.org/issues/4195>`_ - Fix keyboard navigation in "inner" tabsets such as the Query Tool and Debugger.
| `Bug #4253 <https://redmine.postgresql.org/issues/4253>`_ - Fix issue where new column should be created with Default value.
| `Bug #4255 <https://redmine.postgresql.org/issues/4255>`_ - Prevent the geometry viewer grabbing key presses when not in focus under Firefox, IE and Edge.
| `Bug #4320 <https://redmine.postgresql.org/issues/4320>`_ - Fix issue where SSH tunnel connection using password is failing, it's regression of Master Password.

View File

@ -9,6 +9,7 @@
import $ from 'jquery';
import gettext from 'sources/gettext';
import { getMod } from 'sources/utils';
const PERIOD_KEY = 190,
FWD_SLASH_KEY = 191,
@ -116,9 +117,52 @@ function validateShortcutKeys(user_defined_shortcut, event) {
user_defined_shortcut.key.key_code == keyCode;
}
// Finds the desired panel on which user wants to navigate to
function focusDockerPanel(docker, op) {
if(!docker) {
return;
}
// If no frame in focus, focus the first one
if(!docker._focusFrame) {
if(docker._frameList.length == 0 && docker._frameList[0]._panelList.length == 0) {
return;
}
docker._frameList[0]._panelList[docker._frameList[0]._curTab].focus();
}
let focus_frame = docker._focusFrame,
focus_id = 0,
flash = false;
// Mod is used to cycle the op
if (op == 'switch') {
let i = 0, total_frames = docker._frameList.length;
for(i = 0; i < total_frames; i++) {
if(focus_frame === docker._frameList[i]) break;
}
focus_frame = docker._frameList[getMod(i+1,total_frames)];
focus_id = focus_frame._curTab;
flash = true;
} else if (op == 'left') {
focus_id = getMod(focus_frame._curTab-1, focus_frame._panelList.length);
flash = false;
} else if (op == 'right') {
focus_id = getMod(focus_frame._curTab+1, focus_frame._panelList.length);
flash = false;
}
let focus_panel = focus_frame._panelList[focus_id];
focus_panel.$container.find('*[tabindex]:not([tabindex="-1"])').trigger('focus');
focus_panel.focus(flash);
return focus_panel._type;
}
/* Debugger: Keyboard Shortcuts handling */
function keyboardShortcutsDebugger($el, event, preferences) {
let panel_id, panel_content, $input;
function keyboardShortcutsDebugger($el, event, preferences, docker) {
let panel_type = '', panel_content, $input;
if(this.validateShortcutKeys(preferences.edit_grid_values, event)) {
this._stopEventPropagation(event);
@ -132,54 +176,32 @@ function keyboardShortcutsDebugger($el, event, preferences) {
}
} else if(this.validateShortcutKeys(preferences.move_next, event)) {
this._stopEventPropagation(event);
panel_id = this.getInnerPanel($el, 'right');
panel_type = focusDockerPanel(docker, 'right');
} else if(this.validateShortcutKeys(preferences.move_previous, event)) {
this._stopEventPropagation(event);
panel_id = this.getInnerPanel($el, 'left');
panel_type = focusDockerPanel(docker, 'left');
} else if(this.validateShortcutKeys(preferences.switch_panel, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'switch');
}
return panel_id;
}
// Finds the desired panel on which user wants to navigate to
function getInnerPanel($el, direction) {
if(!$el || !$el.length)
return false;
let total_panels = $el.find('.wcPanelTab');
// If no panels found OR if single panel
if (!total_panels.length || total_panels.length == 1)
return false;
let active_panel = $(total_panels).filter('.wcPanelTabActive'),
id = parseInt($(active_panel).attr('id')),
fist_panel = 0,
last_panel = total_panels.length - 1;
// Find desired panel
if (direction == 'left') {
if(id > fist_panel)
id--;
} else {
if (id < last_panel)
id++;
}
return id;
return panel_type;
}
/* Query tool: Keyboard Shortcuts handling */
function keyboardShortcutsQueryTool(
sqlEditorController, queryToolActions, event
sqlEditorController, queryToolActions, event, docker
) {
if (sqlEditorController.isQueryRunning()) {
return;
}
let keyCode = event.which || event.keyCode, panel_id;
let keyCode = event.which || event.keyCode, panel_type = '';
let executeKeys = sqlEditorController.preferences.execute_query;
let explainKeys = sqlEditorController.preferences.explain_query;
let explainAnalyzeKeys = sqlEditorController.preferences.explain_analyze_query;
let downloadCsvKeys = sqlEditorController.preferences.download_csv;
let nextPanelKeys = sqlEditorController.preferences.move_next;
let previousPanelKeys = sqlEditorController.preferences.move_previous;
let nextTabKeys = sqlEditorController.preferences.move_next;
let previousTabKeys = sqlEditorController.preferences.move_previous;
let switchPanelKeys = sqlEditorController.preferences.switch_panel;
let toggleCaseKeys = sqlEditorController.preferences.toggle_case;
let commitKeys = sqlEditorController.preferences.commit_transaction;
let rollbackKeys = sqlEditorController.preferences.rollback_transaction;
@ -236,12 +258,15 @@ function keyboardShortcutsQueryTool(
&& $(event.target).closest('.dropdown-submenu').length > 0) {
$(event.target).closest('.dropdown-submenu').find('.dropdown-menu').removeClass('show');
}
} else if(this.validateShortcutKeys(nextPanelKeys, event)) {
} else if(this.validateShortcutKeys(nextTabKeys, event)) {
this._stopEventPropagation(event);
panel_id = this.getInnerPanel(sqlEditorController.container, 'right');
} else if(this.validateShortcutKeys(previousPanelKeys, event)) {
panel_type = focusDockerPanel(docker, 'right');
} else if(this.validateShortcutKeys(previousTabKeys, event)) {
this._stopEventPropagation(event);
panel_id = this.getInnerPanel(sqlEditorController.container, 'left');
panel_type = focusDockerPanel(docker, 'left');
} else if(this.validateShortcutKeys(switchPanelKeys, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'switch');
} else if(keyCode === UP_KEY || keyCode === DOWN_KEY) {
/*Apply only for dropdown*/
if($(event.target).closest('.dropdown-menu').length > 0) {
@ -293,13 +318,13 @@ function keyboardShortcutsQueryTool(
}
}
return panel_id;
return panel_type;
}
export {
keyboardShortcutsDebugger as processEventDebugger,
keyboardShortcutsQueryTool as processEventQueryTool,
getInnerPanel, validateShortcutKeys,
focusDockerPanel, validateShortcutKeys,
_stopEventPropagation, isMac, isKeyCtrlAlt, isKeyAltShift, isKeyCtrlShift,
isKeyCtrlAltShift, isAltShiftBoth, isCtrlShiftBoth, isCtrlAltBoth,
shortcut_key, shortcut_title, shortcut_accesskey_title,

View File

@ -79,3 +79,7 @@ export function getGCD(inp_arr) {
return result;
}
export function getMod(no, divisor) {
return ((no % divisor) + divisor) % divisor;
}

View File

@ -184,8 +184,8 @@ class DebuggerModule(PgAdminModule):
'shift': True,
'control': False,
'key': {
'key_code': 37,
'char': 'ArrowLeft'
'key_code': 219,
'char': '['
}
},
category_label=gettext('Keyboard shortcuts'),
@ -202,8 +202,26 @@ class DebuggerModule(PgAdminModule):
'shift': True,
'control': False,
'key': {
'key_code': 39,
'char': 'ArrowRight'
'key_code': 221,
'char': ']'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'switch_panel',
gettext('Switch Panel'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {
'key_code': 9,
'char': 'Tab'
}
},
category_label=gettext('Keyboard shortcuts'),

View File

@ -1513,17 +1513,17 @@ define([
controller.Step_into(pgTools.DirectDebug.trans_id);
},
keyAction: function (event) {
var $el = this.$el, panel_id, actual_panel,
self = this;
let panel_type='';
panel_id = keyboardShortcuts.processEventDebugger(
$el, event, self.preferences
panel_type = keyboardShortcuts.processEventDebugger(
this.$el, event, this.preferences, pgTools.DirectDebug.docker
);
// Panel navigation
if(!_.isUndefined(panel_id) && !_.isNull(panel_id)) {
actual_panel = panel_id + 1;
pgTools.DirectDebug.docker.findPanels()[actual_panel].focus();
if(!_.isNull(panel_type) && !_.isUndefined(panel_type) && panel_type != '') {
setTimeout(function() {
pgBrowser.Events.trigger(`pgadmin:debugger:${panel_type}:focus`);
}, 100);
}
},
});
@ -1856,6 +1856,10 @@ define([
}
});
pgBrowser.Events.on('pgadmin:debugger:code:focus', ()=>{
self.editor.focus();
});
// On loading the docker, register the callbacks
var onLoad = function() {
self.docker.finishLoading(100);
@ -1906,6 +1910,16 @@ define([
// Create the toolbar view for debugging the function
this.toolbarView = new DebuggerToolbarView();
/* wcDocker focuses on window always, and all our shortcuts are
* bind to editor-panel. So when we use wcDocker focus, editor-panel
* loses focus and events don't work.
*/
$(window).on('keydown', (e)=>{
if(self.toolbarView.keyAction) {
self.toolbarView.keyAction(e);
}
});
/* Cache may take time to load for the first time
* Keep trying till available
*/

View File

@ -195,8 +195,8 @@ define('tools.querytool', [
'filename': 'css',
}),
theme: 'webcabin.overrides.css',
});
}
);
// Create the panels
var sql_panel = new pgAdmin.Browser.Panel({
@ -216,7 +216,7 @@ define('tools.querytool', [
isCloseable: false,
isPrivate: true,
extraClasses: 'hide-vertical-scrollbar',
content: '<div id ="datagrid" class="sql-editor-grid-container text-12" tabindex: "0"></div>',
content: '<div id ="datagrid" class="sql-editor-grid-container text-12" tabindex="0"></div>',
});
var explain = new pgAdmin.Browser.Panel({
@ -226,7 +226,7 @@ define('tools.querytool', [
height: '100%',
isCloseable: false,
isPrivate: true,
content: '<div class="sql-editor-explain" tabindex: "0"></div>',
content: '<div class="sql-editor-explain" tabindex="0"></div>',
});
var messages = new pgAdmin.Browser.Panel({
@ -236,7 +236,7 @@ define('tools.querytool', [
height: '100%',
isCloseable: false,
isPrivate: true,
content: '<div class="sql-editor-message" tabindex: "0"></div>',
content: '<div class="sql-editor-message" tabindex="0"></div>',
});
var history = new pgAdmin.Browser.Panel({
@ -246,7 +246,7 @@ define('tools.querytool', [
height: '33%',
isCloseable: false,
isPrivate: true,
content: '<div id ="history_grid" class="sql-editor-history-container" tabindex: "0"></div>',
content: '<div id ="history_grid" class="sql-editor-history-container" tabindex="0"></div>',
});
var scratch = new pgAdmin.Browser.Panel({
@ -256,7 +256,7 @@ define('tools.querytool', [
height: '33%',
isCloseable: true,
isPrivate: false,
content: '<div class="sql-scratch" tabindex: "0"><textarea wrap="off"></textarea></div>',
content: '<div class="sql-scratch"><textarea wrap="off" tabindex="0"></textarea></div>',
});
var notifications = new pgAdmin.Browser.Panel({
@ -266,7 +266,7 @@ define('tools.querytool', [
height: '100%',
isCloseable: false,
isPrivate: true,
content: '<div id ="notification_grid" class="sql-editor-notifications" tabindex: "0"></div>',
content: '<div id ="notification_grid" class="sql-editor-notifications" tabindex="0"></div>',
});
var geometry_viewer = new pgAdmin.Browser.Panel({
@ -277,7 +277,7 @@ define('tools.querytool', [
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
content: '<div id ="geometry_viewer_panel" class="sql-editor-geometry-viewer" tabindex: "0"></div>',
content: '<div id ="geometry_viewer_panel" class="sql-editor-geometry-viewer" tabindex="0"></div>',
});
// Load all the created panels
@ -317,8 +317,8 @@ define('tools.querytool', [
self.render_history_grid();
queryToolNotifications.renderNotificationsGrid(self.notifications_panel);
var text_container = $('<textarea id="sql_query_tool" tabindex: "-1"></textarea>');
var output_container = $('<div id="output-panel" tabindex: "0"></div>').append(text_container);
var text_container = $('<textarea id="sql_query_tool" tabindex="-1"></textarea>');
var output_container = $('<div id="output-panel" tabindex="0"></div>').append(text_container);
self.sql_panel_obj.$container.find('.pg-panel-content').append(output_container);
self.query_tool_obj = CodeMirror.fromTextArea(text_container.get(0), {
@ -342,6 +342,10 @@ define('tools.querytool', [
scrollbarStyle: 'simple',
});
pgBrowser.Events.on('pgadmin:query_tool:sql_panel:focus', ()=>{
self.query_tool_obj.focus();
});
if (!self.preferences.new_browser_tab) {
// Listen on the panel closed event and notify user to save modifications.
_.each(window.top.pgAdmin.Browser.docker.findPanels('frm_datagrid'), function(p) {
@ -1852,23 +1856,19 @@ define('tools.querytool', [
},
keyAction: function(event) {
var panel_id, self = this;
panel_id = keyboardShortcuts.processEventQueryTool(
this.handler, queryToolActions, event
var panel_type='';
panel_type = keyboardShortcuts.processEventQueryTool(
this.handler, queryToolActions, event, this.docker
);
// If it return panel id then focus it
if(!_.isNull(panel_id) && !_.isUndefined(panel_id)) {
// Returned panel index, by incrementing it by 1 we will get actual panel
panel_id++;
this.docker.findPanels()[panel_id].focus();
// We set focus on history tab so we need to set the focus on
// editor explicitly
if(panel_id == 3) {
setTimeout(function() { self.query_tool_obj.focus(); }, 100);
}
if(!_.isNull(panel_type) && !_.isUndefined(panel_type) && panel_type != '') {
setTimeout(function() {
pgBrowser.Events.trigger(`pgadmin:query_tool:${panel_type}:focus`);
}, 100);
}
},
// Callback function for the commit button click.
on_commit_transaction: function() {
queryToolActions.executeCommit(this.handler);
@ -2126,6 +2126,16 @@ define('tools.querytool', [
// Render the header
self.gridView.render();
/* wcDocker focuses on window always, and all our shortcuts are
* bind to editor-panel. So when we use wcDocker focus, editor-panel
* loses focus and events don't work.
*/
$(window).on('keydown', (e)=>{
if(self.gridView.keyAction) {
self.gridView.keyAction(e);
}
});
if (self.is_query_tool) {
// Fetch the SQL for Scripts (eg: CREATE/UPDATE/DELETE/SELECT)
// Call AJAX only if script type url is present

View File

@ -354,8 +354,8 @@ def RegisterQueryToolPreferences(self):
'shift': True,
'control': False,
'key': {
'key_code': 37,
'char': 'ArrowLeft'
'key_code': 219,
'char': '['
}
},
category_label=gettext('Keyboard shortcuts'),
@ -372,8 +372,26 @@ def RegisterQueryToolPreferences(self):
'shift': True,
'control': False,
'key': {
'key_code': 39,
'char': 'ArrowRight'
'key_code': 221,
'char': ']'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'switch_panel',
gettext('Switch Panel'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {
'key_code': 9,
'char': 'Tab'
}
},
category_label=gettext('Keyboard shortcuts'),

View File

@ -78,7 +78,7 @@ class _Preference(object):
# Look into the configuration table to find out the id of the specific
# preference.
res = PrefTable.query.filter_by(
name=name
name=name, cid=cid
).first()
if res is None:

View File

@ -8,6 +8,7 @@
//////////////////////////////////////////////////////////////////////////
import * as keyboardShortcuts from 'sources/keyboard_shortcuts';
import $ from 'jquery';
describe('the keyboard shortcuts', () => {
const F1_KEY = 112;
@ -45,9 +46,30 @@ describe('the keyboard shortcuts', () => {
});
describe('when user wants to goto next panel', function () {
it('returns panel id', function () {
expect(keyboardShortcuts.getInnerPanel(debuggerElementSpy, 'right')).toEqual(false);
let dockerSpy = {
'_focusFrame': {
'_curTab': 0,
'_panelList': [
{$container: $('<b/>'), '_type': 'type1', 'focus': function() {return true;}},
{$container: $('<b/>'), '_type': 'type2', 'focus': function() {return true;}},
],
},
};
it('right key', function () {
dockerSpy._focusFrame._curTab = 0;
expect(keyboardShortcuts.focusDockerPanel(dockerSpy, 'right')).toEqual('type2');
});
it('left key', function () {
dockerSpy._focusFrame._curTab = 1;
expect(keyboardShortcuts.focusDockerPanel(dockerSpy, 'left')).toEqual('type1');
});
it('left key cycle', function () {
dockerSpy._focusFrame._curTab = 0;
expect(keyboardShortcuts.focusDockerPanel(dockerSpy, 'left')).toEqual('type2');
});
it('right key cycle', function () {
dockerSpy._focusFrame._curTab = 1;
expect(keyboardShortcuts.focusDockerPanel(dockerSpy, 'left')).toEqual('type1');
});
});

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import { getEpoch, getGCD } from 'sources/utils';
import { getEpoch, getGCD, getMod } from 'sources/utils';
describe('getEpoch', function () {
it('should return non zero', function () {
@ -33,3 +33,21 @@ describe('getGCD', function () {
expect(getGCD(nos)).toEqual(3);
});
});
describe('getMod', function () {
it('complete divisible', function () {
expect(getMod(5,5)).toEqual(0);
});
it('incomplete divisible less divisor', function () {
expect(getMod(7,5)).toEqual(2);
});
it('incomplete divisible greater divisor', function () {
expect(getMod(5,7)).toEqual(5);
});
it('negative number', function () {
expect(getMod(-7,5)).toEqual(3);
});
});