feat(desktop): add global UI zoom settings and shortcuts

This commit is contained in:
t8y2
2026-05-28 17:43:06 +08:00
parent d35aeaa08c
commit f7f5758c85
14 changed files with 255 additions and 28 deletions
+73
View File
@@ -43,8 +43,11 @@ import {
isModRShortcut,
isNewQueryShortcut,
isObjectSourceSaveShortcutTarget,
isResetZoomShortcut,
isRefreshDataShortcut,
isSaveShortcut,
isZoomInShortcut,
isZoomOutShortcut,
} from "@/lib/keyboardShortcuts";
import { isPreviewTab } from "@/lib/tabPresentation";
import { supportsSqlFileExecution } from "@/lib/databaseCapabilities";
@@ -224,6 +227,47 @@ const saveSqlFolders = computed(() => {
return tab ? savedSqlStore.listFolders(tab.connectionId) : [];
});
async function applyUiScale(scale: number) {
if (!isDesktop) return;
try {
const { getCurrentWebview } = await import("@tauri-apps/api/webview");
await getCurrentWebview().setZoom(scale);
} catch (error) {
console.warn("[DBX] Failed to apply UI scale", { scale, error });
}
}
function setGlobalUiScale(scale: number) {
settingsStore.updateEditorSettings({ uiScale: scale });
}
function zoomInUi() {
setGlobalUiScale(settingsStore.editorSettings.uiScale + 0.1);
}
function zoomOutUi() {
setGlobalUiScale(settingsStore.editorSettings.uiScale - 0.1);
}
function resetUiZoom() {
setGlobalUiScale(1);
}
function isGlobalUiZoomTarget(target: EventTarget | null): target is Element {
if (!(target instanceof Element)) return false;
if (target.closest("[data-query-editor-root], [data-cell-detail-editor-root], [data-object-source-editor]")) {
return true;
}
if (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
(target instanceof HTMLElement && target.isContentEditable)
) {
return false;
}
return !target.closest("[contenteditable='true']");
}
watch(
() => queryStore.activeTabId,
(id) => {
@@ -242,6 +286,14 @@ watch(
},
);
watch(
() => settingsStore.editorSettings.uiScale,
(scale) => {
void applyUiScale(scale);
},
{ immediate: true },
);
function toggleAiPanel() {
showAiPanel.value = !showAiPanel.value;
localStorage.setItem("dbx-ai-panel-open", String(showAiPanel.value));
@@ -667,6 +719,26 @@ function handleKeydown(e: KeyboardEvent) {
e.stopPropagation();
return;
}
if (isDesktop && isGlobalUiZoomTarget(e.target)) {
if (isZoomInShortcut(e, shortcuts)) {
e.preventDefault();
e.stopPropagation();
zoomInUi();
return;
}
if (isZoomOutShortcut(e, shortcuts)) {
e.preventDefault();
e.stopPropagation();
zoomOutUi();
return;
}
if (isResetZoomShortcut(e, shortcuts)) {
e.preventDefault();
e.stopPropagation();
resetUiZoom();
return;
}
}
if (isDesktop && isBrowserReloadShortcut(e)) {
e.preventDefault();
e.stopPropagation();
@@ -733,6 +805,7 @@ onMounted(async () => {
aiPanelReady.value = true;
});
applyTheme();
void applyUiScale(settingsStore.editorSettings.uiScale);
window.addEventListener("keydown", handleKeydown);
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
if (isDesktop) {
@@ -87,6 +87,7 @@ const emit = defineEmits<{
// Local edit state
const editFontFamily = ref(settingsStore.editorSettings.fontFamily);
const editFontSize = ref(settingsStore.editorSettings.fontSize);
const editUiScale = ref(settingsStore.editorSettings.uiScale);
const editTheme = ref(settingsStore.editorSettings.theme);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
@@ -105,6 +106,7 @@ const redisScanPageSizeOptions = [200, 1000, 5000, 10000];
const systemFonts = ref<string[]>([]);
const systemFontsLoading = ref(false);
const systemFontsLoaded = ref(false);
const uiScaleOptions = [0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
// --- Snippet state ---
const editSnippets = ref<SqlSnippet[]>(settingsStore.editorSettings.snippets.map((s) => ({ ...s })));
@@ -219,6 +221,7 @@ watch(
if (open) {
editFontFamily.value = settingsStore.editorSettings.fontFamily;
editFontSize.value = settingsStore.editorSettings.fontSize;
editUiScale.value = settingsStore.editorSettings.uiScale;
editTheme.value = settingsStore.editorSettings.theme;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editWordWrap.value = settingsStore.editorSettings.wordWrap;
@@ -256,6 +259,7 @@ function hasChanges(): boolean {
return (
editFontFamily.value !== settingsStore.editorSettings.fontFamily ||
editFontSize.value !== settingsStore.editorSettings.fontSize ||
editUiScale.value !== settingsStore.editorSettings.uiScale ||
editTheme.value !== settingsStore.editorSettings.theme ||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
@@ -280,6 +284,7 @@ async function applySettings() {
settingsStore.updateEditorSettings({
fontFamily: editFontFamily.value,
fontSize: editFontSize.value,
uiScale: editUiScale.value,
theme: editTheme.value,
executeMode: editExecuteMode.value,
wordWrap: editWordWrap.value,
@@ -304,6 +309,7 @@ async function applySettings() {
function resetDefaults() {
editFontFamily.value = DEFAULT_EDITOR_SETTINGS.fontFamily;
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editUiScale.value = DEFAULT_EDITOR_SETTINGS.uiScale;
editTheme.value = DEFAULT_EDITOR_SETTINGS.theme;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
@@ -1106,6 +1112,31 @@ watch(
</section>
<section v-else-if="activeSettingsTab === 'appearance'" class="flex flex-col gap-5 py-2">
<div class="space-y-2">
<Label>{{ t("settings.uiScale") }}</Label>
<Select
:model-value="String(editUiScale)"
@update:model-value="
(value: any) => {
const next = Number(value);
if (Number.isFinite(next)) editUiScale = next;
}
"
>
<SelectTrigger class="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="scale in uiScaleOptions" :key="scale" :value="String(scale)">
{{ Math.round(scale * 100) }}%
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">{{ t("settings.uiScaleDescription") }}</p>
</div>
<Separator />
<div class="space-y-2">
<Label>{{ t("settings.appLayout") }}</Label>
<div class="grid grid-cols-2 gap-2">
@@ -383,34 +383,6 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
]),
) ?? [],
codeMirrorKeymap.of([
{
key: "Mod-=",
run: () => {
zoomIn();
return true;
},
},
{
key: "Mod-+",
run: () => {
zoomIn();
return true;
},
},
{
key: "Mod--",
run: () => {
zoomOut();
return true;
},
},
{
key: "Mod-0",
run: () => {
resetZoom();
return true;
},
},
{
key: shortcutToCodeMirrorKey(shortcuts.acceptCompletion),
run: (view) => codeMirrorAcceptCompletion?.(view) ?? false,
+6
View File
@@ -1386,6 +1386,9 @@ export default {
noFontsFound: "No fonts found",
useCustomFont: "Use “{font}”",
fontSize: "Font Size",
uiScale: "UI Scale",
uiScaleDescription:
"Scale the entire desktop UI for high-DPI displays. Changes apply immediately and are restored on next launch.",
theme: "Theme",
selectTheme: "Select theme...",
followAppTheme: "Follow app theme",
@@ -1480,6 +1483,9 @@ export default {
shortcutNewQuery: "New query",
shortcutCloseTab: "Close tab",
shortcutFocusSearch: "Focus search",
shortcutZoomInUi: "Zoom in UI",
shortcutZoomOutUi: "Zoom out UI",
shortcutResetUiZoom: "Reset UI zoom",
shortcutRefreshData: "Refresh data",
shortcutToggleTranspose: "Toggle transpose view",
shortcutCancelSearch: "Cancel search",
+6
View File
@@ -1280,6 +1280,9 @@ export default {
noFontsFound: "No se encontraron fuentes",
useCustomFont: "Usar “{font}”",
fontSize: "Tamaño de fuente",
uiScale: "Escala de interfaz",
uiScaleDescription:
"Escala toda la interfaz de escritorio para pantallas de alta densidad. Los cambios se aplican al instante y se restauran al volver a abrir.",
theme: "Tema",
selectTheme: "Seleccionar tema...",
appLayout: "Diseño de la interfaz",
@@ -1373,6 +1376,9 @@ export default {
shortcutNewQuery: "Nueva consulta",
shortcutCloseTab: "Cerrar pestaña",
shortcutFocusSearch: "Enfocar búsqueda",
shortcutZoomInUi: "Ampliar interfaz",
shortcutZoomOutUi: "Reducir interfaz",
shortcutResetUiZoom: "Restablecer zoom de interfaz",
shortcutRefreshData: "Actualizar datos",
shortcutToggleTranspose: "Alternar vista transpuesta",
shortcutCancelSearch: "Cancelar búsqueda",
+5
View File
@@ -1361,6 +1361,8 @@ export default {
noFontsFound: "未找到字体",
useCustomFont: "使用“{font}”",
fontSize: "字号",
uiScale: "界面缩放",
uiScaleDescription: "按比例缩放整个桌面端界面,适合高清屏;修改后立即生效,并在下次启动时恢复。",
theme: "主题",
selectTheme: "选择主题...",
followAppTheme: "跟随应用主题",
@@ -1447,6 +1449,9 @@ export default {
shortcutNewQuery: "新建查询",
shortcutCloseTab: "关闭标签页",
shortcutFocusSearch: "聚焦搜索",
shortcutZoomInUi: "放大全局界面",
shortcutZoomOutUi: "缩小全局界面",
shortcutResetUiZoom: "重置全局界面缩放",
shortcutRefreshData: "刷新数据",
shortcutToggleTranspose: "切换转置视图",
shortcutCancelSearch: "取消搜索",
+21
View File
@@ -92,6 +92,27 @@ export function isModRShortcut(event: ShortcutLikeEvent): boolean {
return matchesShortcut(event, "Mod+R");
}
export function isZoomInShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
if (matchesShortcut(event, actionShortcut("zoomInUi", shortcuts))) return true;
if (event.isComposing || event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return normalizeKey(event.key) === "NumpadAdd" && !event.shiftKey;
}
export function isZoomOutShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
if (matchesShortcut(event, actionShortcut("zoomOutUi", shortcuts))) return true;
if (event.isComposing || event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return normalizeKey(event.key) === "NumpadSubtract" && !event.shiftKey;
}
export function isResetZoomShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
if (matchesShortcut(event, actionShortcut("resetUiZoom", shortcuts))) return true;
if (event.isComposing || event.altKey || event.shiftKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return normalizeKey(event.key) === "Numpad0";
}
export function isToggleTransposeShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("toggleTranspose", shortcuts));
}
+21
View File
@@ -7,6 +7,9 @@ export type ShortcutActionId =
| "newQuery"
| "closeTab"
| "focusSearch"
| "zoomInUi"
| "zoomOutUi"
| "resetUiZoom"
| "find"
| "replace"
| "refreshData"
@@ -73,6 +76,24 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "global",
defaultShortcut: "Mod+F",
},
{
id: "zoomInUi",
labelKey: "settings.shortcutZoomInUi",
scope: "global",
defaultShortcut: "Mod+=",
},
{
id: "zoomOutUi",
labelKey: "settings.shortcutZoomOutUi",
scope: "global",
defaultShortcut: "Mod+-",
},
{
id: "resetUiZoom",
labelKey: "settings.shortcutResetUiZoom",
scope: "global",
defaultShortcut: "Mod+0",
},
{
id: "find",
labelKey: "settings.shortcutFind",
+11
View File
@@ -172,6 +172,7 @@ export type EditorTheme =
export interface EditorSettings {
fontFamily: string;
fontSize: number;
uiScale: number;
theme: EditorTheme;
executeMode: "all" | "current";
wordWrap: boolean;
@@ -222,6 +223,7 @@ export const FONT_FAMILIES: { value: string; label: string }[] = [
export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: 13,
uiScale: 1,
theme: "app",
executeMode: "all",
wordWrap: false,
@@ -245,6 +247,13 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
export const STORAGE_KEY = "dbx-editor-settings";
const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size";
const MIN_UI_SCALE = 0.75;
const MAX_UI_SCALE = 2;
function normalizeUiScale(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.uiScale;
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value * 100) / 100));
}
function normalizeColumnFormatters(value: unknown): Record<string, ColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
@@ -301,6 +310,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
return {
fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily,
fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
uiScale: normalizeUiScale(settings.uiScale),
theme: settings.theme && EDITOR_THEME_VALUES.has(settings.theme) ? settings.theme : DEFAULT_EDITOR_SETTINGS.theme,
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
@@ -427,6 +437,7 @@ export const useSettingsStore = defineStore("settings", () => {
function updateEditorSettings(partial: Partial<EditorSettings>) {
if (partial.fontFamily !== undefined) editorSettings.value.fontFamily = partial.fontFamily;
if (partial.fontSize !== undefined) editorSettings.value.fontSize = partial.fontSize;
if (partial.uiScale !== undefined) editorSettings.value.uiScale = normalizeUiScale(partial.uiScale);
if (partial.theme !== undefined) editorSettings.value.theme = partial.theme;
if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode;
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;