Configurable shortcuts in the Debugger. Fixes #2901

pull/8/head
Murtuza Zabuawala 2018-02-09 12:43:27 +00:00 committed by Dave Page
parent 258b064963
commit 942ac733a4
8 changed files with 341 additions and 59 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

View File

@ -4,7 +4,7 @@ Keyboard Shortcuts
Keyboard shortcuts are provided in pgAdmin to allow easy access to specific Keyboard shortcuts are provided in pgAdmin to allow easy access to specific
functions. Alternate shortcuts can be configured through File > Preferences if functions. Alternate shortcuts can be configured through File > Preferences if
desired. desired.˝
**Main Browser Window** **Main Browser Window**
@ -130,7 +130,7 @@ When using the Debugger, the following shortcuts are available:
+--------------------------+--------------------+-----------------------------------+ +--------------------------+--------------------+-----------------------------------+
| <accesskey> + s | <accesskey> + s | Stop | | <accesskey> + s | <accesskey> + s | Stop |
+--------------------------+--------------------+-----------------------------------+ +--------------------------+--------------------+-----------------------------------+
| Alt + Shift + g | Alt + Shift + g | Enter or Edit values in Grid | | Alt + Shift + q | Alt + Shift + q | Enter or Edit values in Grid |
+--------------------------+--------------------+-----------------------------------+ +--------------------------+--------------------+-----------------------------------+

View File

@ -69,6 +69,11 @@ Expand the *Debugger* node to specify your debugger display preferences.
* When the *Open in new browser tab* switch is set to *True*, the Debugger will open in a new browser tab when invoked. * When the *Open in new browser tab* switch is set to *True*, the Debugger will open in a new browser tab when invoked.
Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the debugger window navigation:
.. image:: images/preferences_debugger_keyboard_shortcuts.png
:alt: Preferences dialog debugger keyboard shortcuts section
**The Miscellaneous Node** **The Miscellaneous Node**
Expand the *Miscellaneous* node to specify miscellaneous display preferences. Expand the *Miscellaneous* node to specify miscellaneous display preferences.

View File

@ -1,8 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
// Debugger const LEFT_ARROW_KEY = 37,
const EDIT_KEY = 71, // Key: G -> Grid values
LEFT_ARROW_KEY = 37,
RIGHT_ARROW_KEY = 39, RIGHT_ARROW_KEY = 39,
F5_KEY = 116, F5_KEY = 116,
F7_KEY = 118, F7_KEY = 118,
@ -50,24 +48,27 @@ function _stopEventPropagation(event) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} }
/* Debugger: Keyboard Shortcuts handling */ /* Function use to validate shortcut keys */
function keyboardShortcutsDebugger($el, event) { function validateShortcutKeys(user_defined_shortcut, event) {
let keyCode = event.which || event.keyCode; if(!user_defined_shortcut) {
return false;
}
// To handle debugger's internal tab navigation like Parameters/Messages... let keyCode = event.which || event.keyCode;
if (this.isAltShiftBoth(event)) { return user_defined_shortcut.alt == event.altKey &&
// Get the active wcDocker panel from DOM element user_defined_shortcut.shift == event.shiftKey &&
user_defined_shortcut.control == event.ctrlKey &&
user_defined_shortcut.key.key_code == keyCode;
}
/* Debugger: Keyboard Shortcuts handling */
function keyboardShortcutsDebugger($el, event, user_defined_shortcuts) {
let panel_id, panel_content, $input; let panel_id, panel_content, $input;
switch(keyCode) { let edit_grid_keys = user_defined_shortcuts.edit_grid_keys,
case LEFT_ARROW_KEY: next_panel_keys = user_defined_shortcuts.next_panel_keys,
this._stopEventPropagation(event); previous_panel_keys = user_defined_shortcuts.previous_panel_keys;
panel_id = this.getInnerPanel($el, 'left');
break; if(this.validateShortcutKeys(edit_grid_keys, event)) {
case RIGHT_ARROW_KEY:
this._stopEventPropagation(event);
panel_id = this.getInnerPanel($el, 'right');
break;
case EDIT_KEY:
this._stopEventPropagation(event); this._stopEventPropagation(event);
panel_content = $el.find( panel_content = $el.find(
'div.wcPanelTabContent:not(".wcPanelTabContentHidden")' 'div.wcPanelTabContent:not(".wcPanelTabContentHidden")'
@ -77,11 +78,14 @@ function keyboardShortcutsDebugger($el, event) {
if($input.length) if($input.length)
$input.click(); $input.click();
} }
break; } else if(this.validateShortcutKeys(next_panel_keys, event)) {
this._stopEventPropagation(event);
panel_id = this.getInnerPanel($el, 'right');
} else if(this.validateShortcutKeys(previous_panel_keys, event)) {
this._stopEventPropagation(event);
panel_id = this.getInnerPanel($el, 'left');
} }
// Actual panel starts with 1 in wcDocker
return panel_id; return panel_id;
}
} }
// Finds the desired panel on which user wants to navigate to // Finds the desired panel on which user wants to navigate to
@ -169,6 +173,7 @@ module.exports = {
processEventDebugger: keyboardShortcutsDebugger, processEventDebugger: keyboardShortcutsDebugger,
processEventQueryTool: keyboardShortcutsQueryTool, processEventQueryTool: keyboardShortcutsQueryTool,
getInnerPanel: getInnerPanel, getInnerPanel: getInnerPanel,
validateShortcutKeys: validateShortcutKeys,
// misc functions // misc functions
_stopEventPropagation: _stopEventPropagation, _stopEventPropagation: _stopEventPropagation,
isMac: isMac, isMac: isMac,

View File

@ -73,6 +73,172 @@ class DebuggerModule(PgAdminModule):
'will be opened in a new browser tab.') 'will be opened in a new browser tab.')
) )
# Shortcut configuration for Accesskey
accesskey_fields = [
{
'name': 'key',
'type': 'keyCode',
'label': gettext('Key')
}
]
shortcut_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', 'btn_start',
gettext('Accesskey (Continue/Start)'), 'keyboardshortcut',
{
'key': {
'key_code': 67,
'char': 'c'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_stop',
gettext('Accesskey (Stop)'), 'keyboardshortcut',
{
'key': {
'key_code': 83,
'char': 's'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_step_into',
gettext('Accesskey (Step into)'), 'keyboardshortcut',
{
'key': {
'key_code': 73,
'char': 'i'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_step_over',
gettext('Accesskey (Step over)'), 'keyboardshortcut',
{
'key': {
'key_code': 79,
'char': 'o'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_toggle_breakpoint',
gettext('Accesskey (Toggle breakpoint)'), 'keyboardshortcut',
{
'key': {
'key_code': 84,
'char': 't'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts', 'btn_clear_breakpoints',
gettext('Accesskey (Clear all breakpoints)'), 'keyboardshortcut',
{
'key': {
'key_code': 88,
'char': 'x'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=accesskey_fields
)
self.preference.register(
'keyboard_shortcuts',
'edit_grid_values',
gettext('Edit grid values'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {
'key_code': 81,
'char': 'q'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'move_previous',
gettext('Previous tab'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {
'key_code': 37,
'char': 'ArrowLeft'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'move_next',
gettext('Next tab'),
'keyboardshortcut',
{
'alt': True,
'shift': True,
'control': False,
'key': {
'key_code': 39,
'char': 'ArrowRight'
}
},
category_label=gettext('Keyboard shortcuts'),
fields=shortcut_fields
)
def get_exposed_url_endpoints(self): def get_exposed_url_endpoints(self):
""" """
Returns the list of URLs exposed to the client. Returns the list of URLs exposed to the client.
@ -159,6 +325,33 @@ def update_session_function_transaction(trans_id, data):
session['functionData'] = function_data session['functionData'] = function_data
def get_shortcuts_for_accesskey():
"""
This function will fetch and return accesskey shortcuts for debugger
toolbar buttons
Returns:
Dict of shortcut keys
"""
# Fetch debugger preferences
dp = Preferences.module('debugger')
btn_start = dp.preference('btn_start').get()
btn_stop = dp.preference('btn_stop').get()
btn_step_into = dp.preference('btn_step_into').get()
btn_step_over = dp.preference('btn_step_over').get()
btn_toggle_breakpoint = dp.preference('btn_toggle_breakpoint').get()
btn_clear_breakpoints = dp.preference('btn_clear_breakpoints').get()
return {
'start': btn_start.get('key').get('char'),
'stop': btn_stop.get('key').get('char'),
'step_into': btn_step_into.get('key').get('char'),
'step_over': btn_step_over.get('key').get('char'),
'toggle_breakpoint': btn_toggle_breakpoint.get('key').get('char'),
'clear_breakpoints': btn_clear_breakpoints.get('key').get('char')
}
@blueprint.route( @blueprint.route(
'/init/<node_type>/<int:sid>/<int:did>/<int:scid>/<int:fid>', '/init/<node_type>/<int:sid>/<int:did>/<int:scid>/<int:fid>',
methods=['GET'], endpoint='init_for_function' methods=['GET'], endpoint='init_for_function'
@ -411,6 +604,8 @@ def direct_new(trans_id):
# We need client OS information to render correct Keyboard shortcuts # We need client OS information to render correct Keyboard shortcuts
user_agent = UserAgent(request.headers.get('User-Agent')) user_agent = UserAgent(request.headers.get('User-Agent'))
return render_template( return render_template(
"debugger/direct.html", "debugger/direct.html",
_=gettext, _=gettext,
@ -420,7 +615,8 @@ def direct_new(trans_id):
is_desktop_mode=current_app.PGADMIN_RUNTIME, is_desktop_mode=current_app.PGADMIN_RUNTIME,
is_linux=is_linux_platform, is_linux=is_linux_platform,
client_platform=user_agent.platform, client_platform=user_agent.platform,
stylesheets=[url_for('debugger.static', filename='css/debugger.css')] stylesheets=[url_for('debugger.static', filename='css/debugger.css')],
accesskey=get_shortcuts_for_accesskey()
) )

View File

@ -625,9 +625,8 @@ define([
Restart: function(trans_id) { Restart: function(trans_id) {
var self = this, var self = this,
baseUrl = url_for('debugger.restart', { baseUrl = url_for('debugger.restart', {'trans_id': trans_id});
'trans_id': trans_id,
});
self.enable('stop', false); self.enable('stop', false);
self.enable('step_over', false); self.enable('step_over', false);
self.enable('step_into', false); self.enable('step_into', false);
@ -636,7 +635,11 @@ define([
self.enable('continue', false); self.enable('continue', false);
// Clear msg tab // Clear msg tab
pgTools.DirectDebug.messages_panel.$container.find('.messages').html(''); pgTools.DirectDebug
.messages_panel
.$container
.find('.messages')
.html('');
$.ajax({ $.ajax({
url: baseUrl, url: baseUrl,
@ -1161,7 +1164,9 @@ define([
model: DebuggerVariablesModel, model: DebuggerVariablesModel,
}); });
VariablesCollection.prototype.on('change', self.deposit_parameter_value, self); VariablesCollection.prototype.on(
'change', self.deposit_parameter_value, self
);
var gridCols = [{ var gridCols = [{
name: 'name', name: 'name',
@ -1205,6 +1210,12 @@ define([
className: 'backgrid table-bordered', className: 'backgrid table-bordered',
}); });
variable_grid.collection.on(
'backgrid:edited', () => {
pgTools.DirectDebug.editor.focus();
}
);
variable_grid.render(); variable_grid.render();
// Render the variables grid into local variables panel // Render the variables grid into local variables panel
@ -1212,7 +1223,6 @@ define([
.$container .$container
.find('.local_variables') .find('.local_variables')
.append(variable_grid.el); .append(variable_grid.el);
}, },
AddParameters: function(result) { AddParameters: function(result) {
@ -1237,7 +1247,9 @@ define([
model: DebuggerParametersModel, model: DebuggerParametersModel,
}); });
self.ParametersCollection.prototype.on('change', self.deposit_parameter_value, self); ParametersCollection.prototype.on(
'change', self.deposit_parameter_value, self
);
var paramGridCols = [{ var paramGridCols = [{
name: 'name', name: 'name',
@ -1281,12 +1293,20 @@ define([
className: 'backgrid table-bordered', className: 'backgrid table-bordered',
}); });
param_grid.collection.on(
'backgrid:edited', () => {
pgTools.DirectDebug.editor.focus();
}
);
param_grid.render(); param_grid.render();
// Render the parameters grid into parameter panel // Render the parameters grid into parameter panel
pgTools.DirectDebug.parameters_panel.$container.find('.parameters').append(param_grid.el); pgTools.DirectDebug.parameters_panel
.$container
.find('.parameters')
.append(param_grid.el);
}, },
deposit_parameter_value: function(model) { deposit_parameter_value: function(model) {
var self = this; var self = this;
@ -1476,7 +1496,45 @@ define([
}, },
keyAction: function (event) { keyAction: function (event) {
var $el = this.$el, panel_id, actual_panel; var $el = this.$el, panel_id, actual_panel;
panel_id = keyboardShortcuts.processEventDebugger($el, event);
// If already fetched earlier then don't do it again
if(_.size(pgTools.DirectDebug.debugger_keyboard_shortcuts) == 0) {
// Fetch keyboard shortcut keys
var edit_grid_shortcut_perf, next_panel_perf, previous_panel_perf;
edit_grid_shortcut_perf = window.top.pgAdmin.Browser.get_preference(
'debugger', 'edit_grid_values'
);
next_panel_perf = window.top.pgAdmin.Browser.get_preference(
'debugger', 'move_next'
);
previous_panel_perf = window.top.pgAdmin.Browser.get_preference(
'debugger', 'move_previous'
);
// If debugger opened in new Tab then window.top won't be available
if(!edit_grid_shortcut_perf || !next_panel_perf || !previous_panel_perf) {
edit_grid_shortcut_perf = window.opener.pgAdmin.Browser.get_preference(
'debugger', 'edit_grid_values'
);
next_panel_perf = window.opener.pgAdmin.Browser.get_preference(
'debugger', 'move_next'
);
previous_panel_perf = window.opener.pgAdmin.Browser.get_preference(
'debugger', 'move_previous'
);
}
pgTools.DirectDebug.debugger_keyboard_shortcuts = {
'edit_grid_keys': edit_grid_shortcut_perf.value,
'next_panel_keys': next_panel_perf.value,
'previous_panel_keys': previous_panel_perf.value,
};
}
panel_id = keyboardShortcuts.processEventDebugger(
$el, event, pgTools.DirectDebug.debugger_keyboard_shortcuts
);
// Panel navigation // Panel navigation
if(!_.isUndefined(panel_id) && !_.isNull(panel_id)) { if(!_.isUndefined(panel_id) && !_.isNull(panel_id)) {
actual_panel = panel_id + 1; actual_panel = panel_id + 1;
@ -1513,6 +1571,7 @@ define([
this.debug_restarted = false; this.debug_restarted = false;
this.is_user_aborted_debugging = false; this.is_user_aborted_debugging = false;
this.is_polling_required = true; // Flag to stop unwanted ajax calls this.is_polling_required = true; // Flag to stop unwanted ajax calls
this.debugger_keyboard_shortcuts = {};
this.docker = new wcDocker( this.docker = new wcDocker(
'#container', { '#container', {
@ -1748,7 +1807,7 @@ define([
// To show the line-number and set breakpoint marker details by user. // To show the line-number and set breakpoint marker details by user.
self.editor = CodeMirror.fromTextArea( self.editor = CodeMirror.fromTextArea(
code_editor_area.get(0), { code_editor_area.get(0), {
tabindex: 0, tabindex: -1,
lineNumbers: true, lineNumbers: true,
foldOptions: { foldOptions: {
widget: '\u2026', widget: '\u2026',
@ -1775,6 +1834,14 @@ define([
matchBrackets: pgAdmin.Browser.editor_options.brace_matching, matchBrackets: pgAdmin.Browser.editor_options.brace_matching,
}); });
// Useful for keyboard navigation, when user presses escape key we will
// defocus from the codemirror editor allow user to navigate further
CodeMirror.on(self.editor, 'keydown', function(cm,event) {
if(event.keyCode==27){
document.activeElement.blur();
}
});
// On loading the docker, register the callbacks // On loading the docker, register the callbacks
var onLoad = function() { var onLoad = function() {
self.docker.finishLoading(100); self.docker.finishLoading(100);

View File

@ -41,19 +41,19 @@ try {
<div class="btn-group" role="group" aria-label=""> <div class="btn-group" role="group" aria-label="">
<button type="button" class="btn btn-default btn-step-into" <button type="button" class="btn btn-default btn-step-into"
title="{{ _('Step into') }}" title="{{ _('Step into') }}"
accesskey="i" accesskey="{{ accesskey.step_into }}"
tabindex="0" autofocus="autofocus"> tabindex="0" autofocus="autofocus">
<i class="fa fa-indent"></i> <i class="fa fa-indent"></i>
</button> </button>
<button type="button" class="btn btn-default btn-step-over" <button type="button" class="btn btn-default btn-step-over"
title="{{ _('Step over') }}" title="{{ _('Step over') }}"
accesskey="o" accesskey="{{ accesskey.step_over }}"
tabindex="0"> tabindex="0">
<i class="fa fa-outdent"></i> <i class="fa fa-outdent"></i>
</button> </button>
<button type="button" class="btn btn-default btn-continue" <button type="button" class="btn btn-default btn-continue"
title="{{ _('Continue/Start') }}" title="{{ _('Continue/Start') }}"
accesskey="c" accesskey="{{ accesskey.toggle_breakpoint }}"
tabindex="0"> tabindex="0">
<i class="fa fa-play-circle"></i> <i class="fa fa-play-circle"></i>
</button> </button>
@ -61,20 +61,20 @@ try {
<div class="btn-group" role="group" aria-label=""> <div class="btn-group" role="group" aria-label="">
<button type="button" class="btn btn-default btn-toggle-breakpoint" <button type="button" class="btn btn-default btn-toggle-breakpoint"
title="{{ _('Toggle breakpoint') }}" title="{{ _('Toggle breakpoint') }}"
accesskey="t" accesskey="{{ accesskey.toggle_breakpoint }}"
tabindex="0"> tabindex="0">
<i class="fa fa-circle"></i> <i class="fa fa-circle"></i>
</button> </button>
<button type="button" class="btn btn-default btn-clear-breakpoint" <button type="button" class="btn btn-default btn-clear-breakpoint"
title="{{ _('Clear all breakpoints') }}" title="{{ _('Clear all breakpoints') }}"
accesskey="x" accesskey="{{ accesskey.clear_breakpoints }}"
tabindex="0"> tabindex="0">
<i class="fa fa-ban"></i> <i class="fa fa-ban"></i>
</button> </button>
</div> </div>
<div class="btn-group" role="group" aria-label=""> <div class="btn-group" role="group" aria-label="">
<button type="button" class="btn btn-default btn-stop" <button type="button" class="btn btn-default btn-stop"
accesskey="s" accesskey="{{ accesskey.stop }}"
title="{{ _('Stop') }}" title="{{ _('Stop') }}"
tabindex="0"> tabindex="0">
<i class="fa fa-stop-circle"></i> <i class="fa fa-stop-circle"></i>

View File

@ -16,7 +16,14 @@ describe('the keyboard shortcuts', () => {
RIGHT_ARROW_KEY = 39, RIGHT_ARROW_KEY = 39,
MOVE_NEXT = 'right'; MOVE_NEXT = 'right';
let debuggerElementSpy, event; let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
'userDefinedShortcuts', [
{ 'edit_grid_keys': null },
{ 'next_panel_keys': null },
{ 'previous_panel_keys': null }
]
);
beforeEach(() => { beforeEach(() => {
event = { event = {
shift: false, shift: false,
@ -31,7 +38,9 @@ describe('the keyboard shortcuts', () => {
describe('when the key is not handled by the function', function () { describe('when the key is not handled by the function', function () {
beforeEach(() => { beforeEach(() => {
event.which = F1_KEY; event.which = F1_KEY;
keyboardShortcuts.processEventDebugger(debuggerElementSpy, event); keyboardShortcuts.processEventDebugger(
debuggerElementSpy, event, debuggerUserShortcutSpy
);
}); });
it('should allow event to propagate', () => { it('should allow event to propagate', () => {