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,