From 6daf8967dcf28a32b0e4cd81aea12fa5627a08ef Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 10 Jun 2026 13:55:26 +0100 Subject: [PATCH 1/2] Keyboard shortcut improvements across tabs, the SQL editor and dialogs. - Fix the main "tabbed panel forward/backward" shortcut not switching the workspace tabs when keyboard focus is inside a tool (SQL editor, PSQL terminal, ERD or Schema Diff). bindRightPanel now locates the active workspace tab via rc-dock's dock-tab-active class, independent of focus, and restricts cycling to the workspace tab-set. The default shortcut is changed to Ctrl/Cmd+Alt+] / [ so it no longer collides with the Query Tool's inner-panel navigation (Alt+Shift+] / [) and does not emit glyphs on macOS; the bogus key codes (Meta/ContextMenu) are corrected to the bracket key codes. - Add Ctrl/Cmd+Shift+D to duplicate the current line or selection in the SQL editor. - Add Ctrl/Cmd+Enter to save and close object/utility dialogs (including the Query Tool sort/filter dialog), and Escape to close them - dialogs rendered as dockable panels (Properties, Backup, etc.) previously had neither. The Escape handler is scoped to panel dialogs (skips MUI modals, which already close on Escape) and yields to inner controls that handle Escape first. Closes #7232 Closes #3834 Closes #7167 Closes #5691 Closes #5196 --- docs/en_US/keyboard_shortcuts.rst | 6 +- docs/en_US/release_notes_9_16.rst | 5 ++ .../browser/register_browser_preferences.py | 12 ++-- web/pgadmin/browser/static/js/keyboard.js | 61 +++++++++---------- .../static/js/SchemaView/SchemaDialogView.jsx | 25 +++++++- .../ReactCodeMirror/components/Editor.jsx | 8 ++- 6 files changed, 74 insertions(+), 43 deletions(-) diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 5f766637865..32091d8e44f 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -44,9 +44,9 @@ When using main browser window, the following keyboard shortcuts are available: +----------------------------+--------------------+------------------------------------+ | Shift + Alt + s | Shift + Option + s | Search objects | +----------------------------+--------------------+------------------------------------+ - | Shift + Alt + [ | Shift + Option + [ | Tabbed panel backward | + | Ctrl + Alt + [ | Ctrl + Option + [ | Tabbed panel backward | +----------------------------+--------------------+------------------------------------+ - | Shift + Alt + ] | Shift + Option + ] | Tabbed panel forward | + | Ctrl + Alt + ] | Ctrl + Option + ] | Tabbed panel forward | +----------------------------+--------------------+------------------------------------+ | Shift + Alt + w | Shift + Ctrl + w | Close tab panel | +----------------------------+--------------------+------------------------------------+ @@ -98,6 +98,8 @@ When using the syntax-highlighting SQL editors, the following shortcuts are avai +--------------------------+----------------------+-------------------------------------+ | Ctrl + / | Cmd + / | Comment/Uncomment code (Block) | +--------------------------+----------------------+-------------------------------------+ + | Ctrl + Shift + d | Cmd + Shift + d | Duplicate current line/selection | + +--------------------------+----------------------+-------------------------------------+ | Ctrl + a | Cmd + a | Select all | +--------------------------+----------------------+-------------------------------------+ | Ctrl + c | Cmd + c | Copy selected text to the clipboard | diff --git a/docs/en_US/release_notes_9_16.rst b/docs/en_US/release_notes_9_16.rst index 4f0740cdff0..b8846af1f75 100644 --- a/docs/en_US/release_notes_9_16.rst +++ b/docs/en_US/release_notes_9_16.rst @@ -26,6 +26,10 @@ Bundled PostgreSQL Utilities New features ************ + | `Issue #3834 `_ - Add a keyboard shortcut (Ctrl/Cmd+Shift+D) to duplicate the current line or selection in the SQL editor. + | `Issue #5196 `_ - Allow the close/unsaved-changes confirmation and other dialogs to be operated from the keyboard (Enter to confirm/save, Escape to cancel) without tabbing to the buttons. + | `Issue #5691 `_ - Allow closing the Properties, Backup and other object/utility dialogs with the Escape key. + | `Issue #7167 `_ - Add Ctrl/Cmd+Enter to save and close object and utility dialogs, including the Query Tool sort/filter dialog. | `Issue #9626 `_ - Add support for the TOAST tuple target storage parameter in the Materialized View dialog. | `Issue #9646 `_ - Make the init container security context in the Helm chart configurable via containerSecurityContext, consistent with the main container. | `Issue #9699 `_ - Add support for closing a tab with a middle-click on its title. @@ -40,6 +44,7 @@ Bug fixes ********* | `Issue #6308 `_ - Fix the infinite loading spinner after an idle database connection is silently dropped, by detecting stale connections and offering a reconnect dialog. + | `Issue #7232 `_ - Fix the tabbed panel forward/backward shortcut not switching the main tabs when keyboard focus is inside a tool (SQL editor, PSQL terminal, ERD or Schema Diff). The default shortcut is now Ctrl/Cmd+Alt+] / [ to avoid colliding with the Query Tool's inner-panel navigation. | `Issue #9595 `_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script. | `Issue #9677 `_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement. | `Issue #9828 `_ - Fix tool calls failing against OpenAI-compatible providers that emit empty/null name, arguments, or id fields in streaming continuation deltas. diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py index d2fe51dc5fa..064f23d2a2c 100644 --- a/web/pgadmin/browser/register_browser_preferences.py +++ b/web/pgadmin/browser/register_browser_preferences.py @@ -170,9 +170,9 @@ def register_browser_preferences(self): 'keyboardshortcut', { 'alt': True, - 'shift': True, - 'control': False, - 'key': {'key_code': 91, 'char': '['} + 'shift': False, + 'control': True, + 'key': {'key_code': 219, 'char': '['} }, category_label=PREF_LABEL_KEYBOARD_SHORTCUTS, fields=fields @@ -200,9 +200,9 @@ def register_browser_preferences(self): 'keyboardshortcut', { 'alt': True, - 'shift': True, - 'control': False, - 'key': {'key_code': 93, 'char': ']'} + 'shift': False, + 'control': True, + 'key': {'key_code': 221, 'char': ']'} }, category_label=PREF_LABEL_KEYBOARD_SHORTCUTS, fields=fields diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index ea64304d7cc..c254232597b 100644 --- a/web/pgadmin/browser/static/js/keyboard.js +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -149,42 +149,37 @@ _.extend(pgBrowser.keyboardNavigation, { bindRightPanel: function(event, combo) { const self = this; const shortcutObj = this.keyboardShortcut; - const activeElement = document.activeElement; + const rootDock = document.getElementById('root'); + if (!rootDock) return; - if (activeElement.closest('.dock-tab-btn')) { - const currDockTab = activeElement.closest('.dock-tab-btn'); - const dockLayout = currDockTab.closest('.dock-layout'); - const dockLayoutTabs = dockLayout ? Array.from(dockLayout.querySelectorAll('.dock-tab-btn')) : null; + // Find the active workspace tab independently of where the keyboard focus + // currently sits. Previously this relied on document.activeElement, which + // breaks when focus is inside a tool's own (nested) dock layout - the SQL + // editor, an ERD/Schema Diff canvas - or inside the PSQL iframe, because + // the resolved tab id then belonged to the tool's inner tab rather than a + // main workspace tab (issue #7232). + // + // rc-dock renders the object explorer and the workspace as separate + // tab-sets, and each marks its own active tab with `dock-tab-active`, so + // prefer the active tab that is not the object explorer. + const activeTabBtns = Array.from( + rootDock.querySelectorAll('.dock-tab.dock-tab-active .dock-tab-btn')); + const activeTabBtn = + activeTabBtns.find(tab => !tab.id.includes('id-object-explorer')) || + activeTabBtns[0]; + if (!activeTabBtn) return; - if (dockLayoutTabs && dockLayoutTabs.length > 1) { - const activeTabIndex = dockLayoutTabs.indexOf(currDockTab); - self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo); - } - } - else if (activeElement.nodeName === 'IFRAME' || activeElement.closest('.dock-tabpane.dock-tabpane-active')) { - let activeTabId = ''; - activeTabId = (activeElement.nodeName === 'IFRAME') ? activeElement.id : activeElement.closest('.dock-tabpane.dock-tabpane-active').id; - const dockLayout = document.getElementById('root'); - const dockLayoutTabs = dockLayout ? Array.from(dockLayout.querySelectorAll('.dock-tab-btn')) : null; - - if (dockLayoutTabs && dockLayoutTabs.length > 1 && activeTabId) { - const activeTabIndex = dockLayoutTabs.findIndex(tab => tab.id.slice(14) === activeTabId); - self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo); - } - } - else if (activeElement === document.body || document.querySelector('div[data-test="app-menu-bar"]')) { - const activeTabs = document.getElementsByClassName('dock-tabpane dock-tabpane-active'); - - if (activeTabs.length > 1) { - const activeTabId = activeTabs[1].id; - const dockLayout = document.getElementById('root'); - const dockLayoutTabs = dockLayout ? Array.from(dockLayout.querySelectorAll('.dock-tab-btn')) : null; + // Restrict navigation to the tabs of the same tab-set (dock panel) as the + // active tab, so cycling stays within the workspace tabs and does not + // include the object explorer or a tool's nested tabs. + const panel = activeTabBtn.closest('.dock-panel'); + const dockLayoutTabs = panel ? Array.from( + panel.querySelectorAll('.dock-tab-btn')) + .filter(tab => tab.closest('.dock-panel') === panel) : []; - if (dockLayoutTabs && dockLayoutTabs.length > 1 && activeTabId) { - const activeTabIndex = dockLayoutTabs.findIndex(tab => tab.id.slice(14) === activeTabId); - self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo); - } - } + if (dockLayoutTabs.length > 1) { + const activeTabIndex = dockLayoutTabs.indexOf(activeTabBtn); + self._focusTab(dockLayoutTabs, activeTabIndex, shortcutObj, combo); } }, _focusTab: function(dockLayoutTabs, activeTabIdx, shortcut_obj, combo){ diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index 3c83f8e7ab3..8538f7809cf 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -177,9 +177,32 @@ export default function SchemaDialogView({ return ; }; + const onKeyDown = (e) => { + // Ctrl/Cmd+Enter saves and closes the dialog from anywhere within it + // (issue #7167). onSaveClick is a no-op when there is nothing to save or + // there is a validation error, so this is safe to call unconditionally. + if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key === 'Enter') { + e.preventDefault(); + onSaveClick(); + return; + } + + // Escape closes the dialog, mirroring the Close button (issue #5691). + // This is needed for dialogs rendered as dockable panels (Properties, + // Backup, and other utility dialogs); dialogs rendered inside a MUI modal + // already close on Escape, so skip those to avoid a double close. The + // !e.defaultPrevented guard lets an inner control that handles Escape + // (e.g. an open dropdown) consume it first. + if (e.key === 'Escape' && !e.defaultPrevented && props.onClose && + !e.currentTarget.closest('.MuiDialog-root')) { + e.preventDefault(); + props.onClose(); + } + }; + /* I am Groot */ return useMemo(() => - + diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx index 935ba415e54..7eb9d6ebf52 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx @@ -31,7 +31,7 @@ import { keymap, } from '@codemirror/view'; import { EditorState, Compartment } from '@codemirror/state'; -import { history, defaultKeymap, historyKeymap, indentLess, indentMore, deleteCharBackwardStrict } from '@codemirror/commands'; +import { history, defaultKeymap, historyKeymap, indentLess, indentMore, deleteCharBackwardStrict, copyLineDown } from '@codemirror/commands'; import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap, acceptCompletion } from '@codemirror/autocomplete'; import { foldGutter, @@ -137,6 +137,12 @@ const defaultExtensions = [ key: 'Backspace', preventDefault: true, run: deleteCharBackwardStrict, + },{ + // Duplicate the current line, or the selected lines if there is a + // selection (issue #3834). + key: 'Mod-Shift-d', + preventDefault: true, + run: copyLineDown, }]), PgSQL.language.data.of({ autocomplete: false, From c84adeeef6c00d553cf470dae6804c51a4bf09e6 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Thu, 11 Jun 2026 10:40:08 +0100 Subject: [PATCH 2/2] Correct macOS modifier in release note for the tab-switch shortcut default. --- docs/en_US/release_notes_9_16.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en_US/release_notes_9_16.rst b/docs/en_US/release_notes_9_16.rst index b8846af1f75..fdb90732079 100644 --- a/docs/en_US/release_notes_9_16.rst +++ b/docs/en_US/release_notes_9_16.rst @@ -44,7 +44,7 @@ Bug fixes ********* | `Issue #6308 `_ - Fix the infinite loading spinner after an idle database connection is silently dropped, by detecting stale connections and offering a reconnect dialog. - | `Issue #7232 `_ - Fix the tabbed panel forward/backward shortcut not switching the main tabs when keyboard focus is inside a tool (SQL editor, PSQL terminal, ERD or Schema Diff). The default shortcut is now Ctrl/Cmd+Alt+] / [ to avoid colliding with the Query Tool's inner-panel navigation. + | `Issue #7232 `_ - Fix the tabbed panel forward/backward shortcut not switching the main tabs when keyboard focus is inside a tool (SQL editor, PSQL terminal, ERD or Schema Diff). The default shortcut is now Ctrl+Alt+] / [ (Ctrl+Option on macOS) to avoid colliding with the Query Tool's inner-panel navigation. | `Issue #9595 `_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script. | `Issue #9677 `_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement. | `Issue #9828 `_ - Fix tool calls failing against OpenAI-compatible providers that emit empty/null name, arguments, or id fields in streaming continuation deltas.