feat(tabs): 添加标签页切换快捷键支持

This commit is contained in:
二丫讲梵 2026-06-29 19:53:28 +08:00 committed by GitHub
parent c8a2292fe9
commit a1d8594dca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 255 additions and 0 deletions

View File

@ -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) {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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ズームをリセット",

View File

@ -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",

View File

@ -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: "重置全局界面缩放",

View File

@ -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: "重設全域介面縮放",

View File

@ -63,6 +63,8 @@ function actionShortcut(actionId: ShortcutActionId, shortcuts?: Partial<Shortcut
return normalizeShortcutSettings(shortcuts)[actionId];
}
const SWITCH_TO_TAB_ACTIONS: ShortcutActionId[] = ["switchToTab1", "switchToTab2", "switchToTab3", "switchToTab4", "switchToTab5", "switchToTab6", "switchToTab7", "switchToTab8", "switchToTab9"];
export function isExecuteSqlShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): 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<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("switchToPreviousTab", shortcuts));
}
export function isSwitchToNextTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("switchToNextTab", shortcuts));
}
export function switchToTabIndexFromShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): 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);

View File

@ -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",

View File

@ -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);