From a1d8594dca3a512e1d9192ff1f28fcd54e188840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 29 Jun 2026 19:53:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(tabs):=20=E6=B7=BB=E5=8A=A0=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E9=A1=B5=E5=88=87=E6=8D=A2=E5=BF=AB=E6=8D=B7=E9=94=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/App.vue | 51 +++++++++++++ apps/desktop/src/i18n/locales/en.ts | 11 +++ apps/desktop/src/i18n/locales/es.ts | 11 +++ apps/desktop/src/i18n/locales/it.ts | 11 +++ apps/desktop/src/i18n/locales/ja.ts | 11 +++ apps/desktop/src/i18n/locales/pt-BR.ts | 11 +++ apps/desktop/src/i18n/locales/zh-CN.ts | 11 +++ apps/desktop/src/i18n/locales/zh-TW.ts | 11 +++ apps/desktop/src/lib/keyboardShortcuts.ts | 16 ++++ apps/desktop/src/lib/shortcutRegistry.ts | 77 ++++++++++++++++++++ packages/app-tests/keyboardShortcuts.test.ts | 34 +++++++++ 11 files changed, 255 insertions(+) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 2e511e797..46fc5da13 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -58,9 +58,12 @@ import { isResetZoomShortcut, isRefreshDataShortcut, isSaveShortcut, + isSwitchToNextTabShortcut, + isSwitchToPreviousTabShortcut, isToggleSidebarShortcut, isZoomInShortcut, isZoomOutShortcut, + switchToTabIndexFromShortcut, } from "@/lib/keyboardShortcuts"; import { isPreviewTab } from "@/lib/tabPresentation"; import { supportsSqlFileExecution } from "@/lib/databaseCapabilities"; @@ -1161,10 +1164,37 @@ async function handleQuickOpenSelect(item: any) { } } +function dispatchBeforeTabSwitch(tabId: string) { + if (tabId === queryStore.activeTabId) return; + window.dispatchEvent(new CustomEvent("dbx:before-tab-switch", { detail: { tabId, fromTabId: queryStore.activeTabId } })); +} + +function activateQueryTab(tabId: string): boolean { + if (!queryStore.tabs.some((tab) => tab.id === tabId)) return false; + dispatchBeforeTabSwitch(tabId); + queryStore.activeTabId = tabId; + driverStoreActive.value = false; + return true; +} + +function activateTabByIndex(index: number): boolean { + const tab = queryStore.tabs[index]; + return tab ? activateQueryTab(tab.id) : false; +} + +function activateAdjacentTab(direction: -1 | 1): boolean { + const count = queryStore.tabs.length; + if (count < 2) return false; + const currentIndex = queryStore.tabs.findIndex((tab) => tab.id === queryStore.activeTabId); + const nextIndex = currentIndex < 0 ? (direction > 0 ? 0 : count - 1) : (currentIndex + direction + count) % count; + return activateTabByIndex(nextIndex); +} + function handleKeydown(e: KeyboardEvent) { if (e.defaultPrevented) return; const shortcuts = settingsStore.editorSettings.shortcuts; + const switchTabIndex = switchToTabIndexFromShortcut(e, shortcuts); if (isOpenSettingsShortcut(e, shortcuts)) { e.preventDefault(); @@ -1204,6 +1234,27 @@ function handleKeydown(e: KeyboardEvent) { setSidebarOpen(!sidebarOpen.value); return; } + if (switchTabIndex != null) { + if (activateTabByIndex(switchTabIndex)) { + e.preventDefault(); + e.stopPropagation(); + } + return; + } + if (isSwitchToPreviousTabShortcut(e, shortcuts)) { + if (activateAdjacentTab(-1)) { + e.preventDefault(); + e.stopPropagation(); + } + return; + } + if (isSwitchToNextTabShortcut(e, shortcuts)) { + if (activateAdjacentTab(1)) { + e.preventDefault(); + e.stopPropagation(); + } + return; + } if (isCloseTabShortcut(e, shortcuts)) { e.preventDefault(); if (showDriverStore.value) { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index a9ad5d1ae..e12d5e855 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2694,6 +2694,17 @@ export default { shortcutCloseTab: "Close tab", shortcutFocusSearch: "Focus search", shortcutQuickOpen: "Quick open (search all database objects)", + shortcutSwitchToPreviousTab: "Switch to previous tab", + shortcutSwitchToNextTab: "Switch to next tab", + shortcutSwitchToTab1: "Switch to tab 1", + shortcutSwitchToTab2: "Switch to tab 2", + shortcutSwitchToTab3: "Switch to tab 3", + shortcutSwitchToTab4: "Switch to tab 4", + shortcutSwitchToTab5: "Switch to tab 5", + shortcutSwitchToTab6: "Switch to tab 6", + shortcutSwitchToTab7: "Switch to tab 7", + shortcutSwitchToTab8: "Switch to tab 8", + shortcutSwitchToTab9: "Switch to tab 9", shortcutZoomInUi: "Zoom in UI", shortcutZoomOutUi: "Zoom out UI", shortcutResetUiZoom: "Reset UI zoom", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index b28c06f46..ce07b2197 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2689,6 +2689,17 @@ export default withEnglishFallback({ shortcutToggleSidebar: "Alternar barra lateral", shortcutFocusSearch: "Enfocar búsqueda", shortcutQuickOpen: "Abrir rápido (buscar todos los objetos de base de datos)", + shortcutSwitchToPreviousTab: "Cambiar a la pestaña anterior", + shortcutSwitchToNextTab: "Cambiar a la pestaña siguiente", + shortcutSwitchToTab1: "Cambiar a la pestaña 1", + shortcutSwitchToTab2: "Cambiar a la pestaña 2", + shortcutSwitchToTab3: "Cambiar a la pestaña 3", + shortcutSwitchToTab4: "Cambiar a la pestaña 4", + shortcutSwitchToTab5: "Cambiar a la pestaña 5", + shortcutSwitchToTab6: "Cambiar a la pestaña 6", + shortcutSwitchToTab7: "Cambiar a la pestaña 7", + shortcutSwitchToTab8: "Cambiar a la pestaña 8", + shortcutSwitchToTab9: "Cambiar a la pestaña 9", shortcutZoomInUi: "Ampliar interfaz", shortcutZoomOutUi: "Reducir interfaz", shortcutResetUiZoom: "Restablecer zoom de interfaz", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index b6d3f76ba..0f76a8af0 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2692,6 +2692,17 @@ export default withEnglishFallback({ shortcutToggleSidebar: "Attiva/disattiva barra laterale", shortcutFocusSearch: "Focalizza ricerca", shortcutQuickOpen: "Apertura rapida (ricerca tutti gli oggetti del database)", + shortcutSwitchToPreviousTab: "Passa alla scheda precedente", + shortcutSwitchToNextTab: "Passa alla scheda successiva", + shortcutSwitchToTab1: "Passa alla scheda 1", + shortcutSwitchToTab2: "Passa alla scheda 2", + shortcutSwitchToTab3: "Passa alla scheda 3", + shortcutSwitchToTab4: "Passa alla scheda 4", + shortcutSwitchToTab5: "Passa alla scheda 5", + shortcutSwitchToTab6: "Passa alla scheda 6", + shortcutSwitchToTab7: "Passa alla scheda 7", + shortcutSwitchToTab8: "Passa alla scheda 8", + shortcutSwitchToTab9: "Passa alla scheda 9", shortcutZoomInUi: "Ingrandisci UI", shortcutZoomOutUi: "Rimpicciolisci UI", shortcutResetUiZoom: "Reimposta zoom UI", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 7397ec95b..8d4ec97fe 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2674,6 +2674,17 @@ export default withEnglishFallback({ shortcutCloseTab: "タブを閉じる", shortcutFocusSearch: "検索にフォーカス", shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)", + shortcutSwitchToPreviousTab: "前のタブに切り替え", + shortcutSwitchToNextTab: "次のタブに切り替え", + shortcutSwitchToTab1: "タブ 1 に切り替え", + shortcutSwitchToTab2: "タブ 2 に切り替え", + shortcutSwitchToTab3: "タブ 3 に切り替え", + shortcutSwitchToTab4: "タブ 4 に切り替え", + shortcutSwitchToTab5: "タブ 5 に切り替え", + shortcutSwitchToTab6: "タブ 6 に切り替え", + shortcutSwitchToTab7: "タブ 7 に切り替え", + shortcutSwitchToTab8: "タブ 8 に切り替え", + shortcutSwitchToTab9: "タブ 9 に切り替え", shortcutZoomInUi: "UIを拡大", shortcutZoomOutUi: "UIを縮小", shortcutResetUiZoom: "UIズームをリセット", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index d2b37cf62..6f36e7227 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2701,6 +2701,17 @@ export default withEnglishFallback({ shortcutToggleSidebar: "Alternar barra lateral", shortcutFocusSearch: "Focar na pesquisa", shortcutQuickOpen: "Abrir rapidamente (pesquisar todos os objetos do banco de dados)", + shortcutSwitchToPreviousTab: "Alternar para a aba anterior", + shortcutSwitchToNextTab: "Alternar para a próxima aba", + shortcutSwitchToTab1: "Alternar para a aba 1", + shortcutSwitchToTab2: "Alternar para a aba 2", + shortcutSwitchToTab3: "Alternar para a aba 3", + shortcutSwitchToTab4: "Alternar para a aba 4", + shortcutSwitchToTab5: "Alternar para a aba 5", + shortcutSwitchToTab6: "Alternar para a aba 6", + shortcutSwitchToTab7: "Alternar para a aba 7", + shortcutSwitchToTab8: "Alternar para a aba 8", + shortcutSwitchToTab9: "Alternar para a aba 9", shortcutZoomInUi: "Aumentar zoom da UI", shortcutZoomOutUi: "Diminuir zoom da UI", shortcutResetUiZoom: "Redefinir zoom da UI", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index b8a37e665..5dcec3b01 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2702,6 +2702,17 @@ export default withEnglishFallback({ shortcutToggleSidebar: "切换侧边栏", shortcutFocusSearch: "聚焦搜索", shortcutQuickOpen: "快速打开 (搜索所有数据库对象)", + shortcutSwitchToPreviousTab: "切换到左侧标签页", + shortcutSwitchToNextTab: "切换到右侧标签页", + shortcutSwitchToTab1: "切换到第 1 个标签页", + shortcutSwitchToTab2: "切换到第 2 个标签页", + shortcutSwitchToTab3: "切换到第 3 个标签页", + shortcutSwitchToTab4: "切换到第 4 个标签页", + shortcutSwitchToTab5: "切换到第 5 个标签页", + shortcutSwitchToTab6: "切换到第 6 个标签页", + shortcutSwitchToTab7: "切换到第 7 个标签页", + shortcutSwitchToTab8: "切换到第 8 个标签页", + shortcutSwitchToTab9: "切换到第 9 个标签页", shortcutZoomInUi: "放大全局界面", shortcutZoomOutUi: "缩小全局界面", shortcutResetUiZoom: "重置全局界面缩放", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index b51dd51ce..9cf4e5f9b 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2579,6 +2579,17 @@ export default withEnglishFallback({ shortcutToggleSidebar: "切換側邊欄", shortcutFocusSearch: "聚焦搜尋", shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)", + shortcutSwitchToPreviousTab: "切換到左側分頁", + shortcutSwitchToNextTab: "切換到右側分頁", + shortcutSwitchToTab1: "切換到第 1 個分頁", + shortcutSwitchToTab2: "切換到第 2 個分頁", + shortcutSwitchToTab3: "切換到第 3 個分頁", + shortcutSwitchToTab4: "切換到第 4 個分頁", + shortcutSwitchToTab5: "切換到第 5 個分頁", + shortcutSwitchToTab6: "切換到第 6 個分頁", + shortcutSwitchToTab7: "切換到第 7 個分頁", + shortcutSwitchToTab8: "切換到第 8 個分頁", + shortcutSwitchToTab9: "切換到第 9 個分頁", shortcutZoomInUi: "放大全域介面", shortcutZoomOutUi: "縮小全域介面", shortcutResetUiZoom: "重設全域介面縮放", diff --git a/apps/desktop/src/lib/keyboardShortcuts.ts b/apps/desktop/src/lib/keyboardShortcuts.ts index 5c759aab6..ed16a36a3 100644 --- a/apps/desktop/src/lib/keyboardShortcuts.ts +++ b/apps/desktop/src/lib/keyboardShortcuts.ts @@ -63,6 +63,8 @@ function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial): boolean { return matchesShortcut(event, actionShortcut("executeSql", shortcuts)); } @@ -148,6 +150,20 @@ export function isQuickOpenShortcut(event: ShortcutLikeEvent, shortcuts?: Partia return matchesShortcut(event, actionShortcut("quickOpen", shortcuts)); } +export function isSwitchToPreviousTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): boolean { + return matchesShortcut(event, actionShortcut("switchToPreviousTab", shortcuts)); +} + +export function isSwitchToNextTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): boolean { + return matchesShortcut(event, actionShortcut("switchToNextTab", shortcuts)); +} + +export function switchToTabIndexFromShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): number | null { + const normalized = normalizeShortcutSettings(shortcuts); + const index = SWITCH_TO_TAB_ACTIONS.findIndex((actionId) => matchesShortcut(event, normalized[actionId])); + return index >= 0 ? index : null; +} + export function isBrowserReloadShortcut(event: ShortcutLikeEvent): boolean { if (event.isComposing || event.altKey) return false; const key = normalizeKey(event.key); diff --git a/apps/desktop/src/lib/shortcutRegistry.ts b/apps/desktop/src/lib/shortcutRegistry.ts index 91a0b6947..46a929b75 100644 --- a/apps/desktop/src/lib/shortcutRegistry.ts +++ b/apps/desktop/src/lib/shortcutRegistry.ts @@ -21,6 +21,17 @@ export type ShortcutActionId = | "closeTab" | "focusSearch" | "quickOpen" + | "switchToPreviousTab" + | "switchToNextTab" + | "switchToTab1" + | "switchToTab2" + | "switchToTab3" + | "switchToTab4" + | "switchToTab5" + | "switchToTab6" + | "switchToTab7" + | "switchToTab8" + | "switchToTab9" | "zoomInUi" | "zoomOutUi" | "resetUiZoom" @@ -175,6 +186,72 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "global", defaultShortcut: "Mod+P", }, + { + id: "switchToPreviousTab", + labelKey: "settings.shortcutSwitchToPreviousTab", + scope: "global", + defaultShortcut: "Shift+Mod+[", + }, + { + id: "switchToNextTab", + labelKey: "settings.shortcutSwitchToNextTab", + scope: "global", + defaultShortcut: "Shift+Mod+]", + }, + { + id: "switchToTab1", + labelKey: "settings.shortcutSwitchToTab1", + scope: "global", + defaultShortcut: "Mod+1", + }, + { + id: "switchToTab2", + labelKey: "settings.shortcutSwitchToTab2", + scope: "global", + defaultShortcut: "Mod+2", + }, + { + id: "switchToTab3", + labelKey: "settings.shortcutSwitchToTab3", + scope: "global", + defaultShortcut: "Mod+3", + }, + { + id: "switchToTab4", + labelKey: "settings.shortcutSwitchToTab4", + scope: "global", + defaultShortcut: "Mod+4", + }, + { + id: "switchToTab5", + labelKey: "settings.shortcutSwitchToTab5", + scope: "global", + defaultShortcut: "Mod+5", + }, + { + id: "switchToTab6", + labelKey: "settings.shortcutSwitchToTab6", + scope: "global", + defaultShortcut: "Mod+6", + }, + { + id: "switchToTab7", + labelKey: "settings.shortcutSwitchToTab7", + scope: "global", + defaultShortcut: "Mod+7", + }, + { + id: "switchToTab8", + labelKey: "settings.shortcutSwitchToTab8", + scope: "global", + defaultShortcut: "Mod+8", + }, + { + id: "switchToTab9", + labelKey: "settings.shortcutSwitchToTab9", + scope: "global", + defaultShortcut: "Mod+9", + }, { id: "zoomInUi", labelKey: "settings.shortcutZoomInUi", diff --git a/packages/app-tests/keyboardShortcuts.test.ts b/packages/app-tests/keyboardShortcuts.test.ts index 60f85aa08..9e6103a9b 100644 --- a/packages/app-tests/keyboardShortcuts.test.ts +++ b/packages/app-tests/keyboardShortcuts.test.ts @@ -14,11 +14,14 @@ import { isResetZoomShortcut, isRefreshDataShortcut, isSaveShortcut, + isSwitchToNextTabShortcut, + isSwitchToPreviousTabShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isToggleTransposeShortcut, isZoomInShortcut, isZoomOutShortcut, + switchToTabIndexFromShortcut, } from "../../apps/desktop/src/lib/keyboardShortcuts.ts"; import { shortcutToCodeMirrorKey } from "../../apps/desktop/src/lib/shortcutRegistry.ts"; @@ -58,6 +61,37 @@ test("matches Cmd+T for opening a new query", () => { assert.equal(isNewQueryShortcut({ key: "t", metaKey: true }), true); }); +test("matches Shift+Mod+brackets for switching adjacent tabs", () => { + assert.equal(isSwitchToPreviousTabShortcut({ key: "[", metaKey: true, shiftKey: true }), true); + assert.equal(isSwitchToPreviousTabShortcut({ key: "[", ctrlKey: true, shiftKey: true }), true); + assert.equal(isSwitchToNextTabShortcut({ key: "]", metaKey: true, shiftKey: true }), true); + assert.equal(isSwitchToNextTabShortcut({ key: "]", ctrlKey: true, shiftKey: true }), true); + assert.equal(isSwitchToPreviousTabShortcut({ key: "[", metaKey: true }), false); + assert.equal(isSwitchToNextTabShortcut({ key: "]", metaKey: true }), false); +}); + +test("matches Mod+number for switching to numbered tabs", () => { + assert.equal(switchToTabIndexFromShortcut({ key: "1", metaKey: true }), 0); + assert.equal(switchToTabIndexFromShortcut({ key: "5", ctrlKey: true }), 4); + assert.equal(switchToTabIndexFromShortcut({ key: "9", metaKey: true }), 8); + assert.equal(switchToTabIndexFromShortcut({ key: "0", metaKey: true }), null); + assert.equal(switchToTabIndexFromShortcut({ key: "1", metaKey: true, altKey: true }), null); +}); + +test("matches custom shortcut settings for tab switching", () => { + const shortcuts = { + switchToPreviousTab: "Alt+ArrowLeft", + switchToNextTab: "Alt+ArrowRight", + switchToTab3: "Shift+Mod+3", + } as any; + + assert.equal(isSwitchToPreviousTabShortcut({ key: "[", metaKey: true }, shortcuts), false); + assert.equal(isSwitchToPreviousTabShortcut({ key: "ArrowLeft", altKey: true }, shortcuts), true); + assert.equal(isSwitchToNextTabShortcut({ key: "ArrowRight", altKey: true }, shortcuts), true); + assert.equal(switchToTabIndexFromShortcut({ key: "3", metaKey: true }, shortcuts), null); + assert.equal(switchToTabIndexFromShortcut({ key: "3", metaKey: true, shiftKey: true }, shortcuts), 2); +}); + test("matches custom shortcut settings for opening a new query", () => { assert.equal(isNewQueryShortcut({ key: "t", metaKey: true }, { newQuery: "Shift+Mod+N" } as any), false); assert.equal(isNewQueryShortcut({ key: "n", ctrlKey: true, shiftKey: true } as any, { newQuery: "Shift+Mod+N" } as any), true);