Improve: shortcuts

- Unassign default shortcuts from "Navigate to previous / next page" action to avoid confusion;
- Allow to unassign shortcuts;
- Allow to assign multiple shortcuts to one action;
- Add context menu to the shortcuts list;
- Improve design;
This commit is contained in:
aleksey-hoffman committed 2026-05-10 20:06:50 +03:00
1 parent 7214a940b3
commit e017cb141e
20 files changed
+1015 -328

No files matched your search

+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "即使应用程序未获得焦点,全局快捷键也会触发操作。",
"changeShortcut": "更改快捷键",
"resetShortcut": "重置快捷键",
"unassignBinding": "取消绑定",
"addAlternateBinding": "添加备用快捷键绑定",
"removeAlternateBinding": "移除备用快捷键绑定",
"localShortcutsAppScope": "本地快捷键(应用范围)",
"whenConditionTooltip": "必须满足的条件才能触发此快捷键",
"unknownMainKey": "未知按键",
@@ -788,8 +791,8 @@
"openGlobalSearch": "打开全局搜索",
"openTheAppWindowCreateANewNote": "打开应用程序窗口并创建一个新笔记",
"displayShortcutList": "显示快捷键列表",
"closeOpenedDialogOverlay": "关闭打开的对话框/覆盖;关闭准备复制或移动的项目,取消选择项目",
"openCloseSelectedFileInQuickView": "在快速查看窗口中打开/关闭所选文件。支持:图像、视频、音频、PDF、纯文本",
"closeOpenedDialogOverlay": "关闭对话框 / 浮层 / 菜单 / 选择",
"openCloseSelectedFileInQuickView": "在快速查看窗口中打开 / 关闭所选文件",
"toggleFullScreen": "切换全屏",
"uiZoomIncrease": "用户界面缩放增加",
"uiZoomDecrease": "用户界面缩放减少",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Globale Verknüpfungen lösen Aktionen aus, auch wenn die App nicht fokussiert ist.",
"changeShortcut": "Verknüpfung ändern",
"resetShortcut": "Verknüpfung zurücksetzen",
"unassignBinding": "Zuweisung aufheben",
"addAlternateBinding": "Alternative Tastenkombination hinzufügen",
"removeAlternateBinding": "Alternative Tastenkombination entfernen",
"localShortcutsAppScope": "Lokale Verknüpfungen (Anwendungsbereich)",
"whenConditionTooltip": "Bedingungen, die erfüllt sein müssen, damit diese Verknüpfung ausgelöst wird",
"unknownMainKey": "Unbekannte Taste",
@@ -788,8 +791,8 @@
"openGlobalSearch": "Globale Suche öffnen",
"openTheAppWindowCreateANewNote": "Öffne das App-Fenster und erstelle eine neue Notiz",
"displayShortcutList": "Kurzwahlliste anzeigen",
"closeOpenedDialogOverlay": "Geöffneten Dialog / Overlay schließen; zum Kopieren oder Verschieben vorbereitete Elemente verwerfen, Elemente abwählen",
"openCloseSelectedFileInQuickView": "Ausgewählte Datei im Schnellansichtsfenster öffnen / schließen. Unterstützt: Bilder, Videos, Audio, PDF, einfacher Text",
"closeOpenedDialogOverlay": "Dialog / Overlay / Menü / Auswahl schließen",
"openCloseSelectedFileInQuickView": "Ausgewählte Datei im Schnellansichtsfenster öffnen / schließen",
"toggleFullScreen": "Vollbild umschalten",
"uiZoomIncrease": "UI-Zoom erhöhen",
"uiZoomDecrease": "UI-Zoom verringern",
+5 -2
View File
@@ -808,6 +808,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Global shortcuts will trigger actions even when the app is not focused.",
"changeShortcut": "Change shortcut",
"resetShortcut": "Reset shortcut",
"unassignBinding": "Unassign binding",
"addAlternateBinding": "Add alternate binding",
"removeAlternateBinding": "Remove alternate binding",
"localShortcutsAppScope": "Local shortcuts (app scope)",
"whenConditionTooltip": "Conditions that must be met for this shortcut to trigger",
"unknownMainKey": "Unknown key",
@@ -827,8 +830,8 @@
"openGlobalSearch": "Open global search",
"openTheAppWindowCreateANewNote": "Open the app window & create a new note",
"displayShortcutList": "Display shortcut list",
"closeOpenedDialogOverlay": "Close opened dialog / overlay; dismiss items prepared for copying or moving, deselect items",
"openCloseSelectedFileInQuickView": "Open / close selected file in Quick View window. Supported: images, videos, audio, PDF, plain text",
"closeOpenedDialogOverlay": "Dismiss dialog / overlay / menu / selection",
"openCloseSelectedFileInQuickView": "Open / close selected file in Quick View window",
"toggleFullScreen": "Toggle full screen",
"uiZoomIncrease": "UI zoom increase",
"uiZoomDecrease": "UI zoom decrease",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Los accesos directos globales activarán acciones incluso cuando la aplicación no esté enfocada",
"changeShortcut": "Cambiar acceso directo",
"resetShortcut": "Restablecer acceso directo",
"unassignBinding": "Desasignar atajo",
"addAlternateBinding": "Añadir acceso directo alternativo",
"removeAlternateBinding": "Quitar acceso directo alternativo",
"localShortcutsAppScope": "Accesos directos locales (alcance de la aplicación)",
"whenConditionTooltip": "Condiciones que deben cumplirse para que este acceso directo se active",
"unknownMainKey": "Tecla desconocida",
@@ -788,8 +791,8 @@
"openGlobalSearch": "Abrir búsqueda global",
"openTheAppWindowCreateANewNote": "Abre la ventana de la aplicación y crea una nueva nota",
"displayShortcutList": "Mostrar lista de accesos directos",
"closeOpenedDialogOverlay": "Cerrar cuadro de diálogo abierto/superposición; descartar elementos preparados para copiar o mover, anular la selección de elementos",
"openCloseSelectedFileInQuickView": "Abrir/cerrar el archivo seleccionado en la ventana Vista rápida. Compatible: imágenes, videos, audio, PDF, texto sin formato",
"closeOpenedDialogOverlay": "Cerrar diálogo / superposición / menú / selección",
"openCloseSelectedFileInQuickView": "Abrir / cerrar archivo seleccionado en ventana Vista rápida",
"toggleFullScreen": "Alternar pantalla completa",
"uiZoomIncrease": "Aumento del zoom de la interfaz de usuario",
"uiZoomDecrease": "Disminución del zoom de la interfaz de usuario",
+5 -2
View File
@@ -768,6 +768,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "میانبرهای جهانی حتی زمانی که برنامه متمرکز نشده باشد، عملکردها را فعال می کند.",
"changeShortcut": "تغییر میانبر",
"resetShortcut": "بازنشانی میانبر",
"unassignBinding": "لغو انتساب",
"addAlternateBinding": "افزودن میانبر جایگزین",
"removeAlternateBinding": "حذف میانبر جایگزین",
"localShortcutsAppScope": "میانبرهای محلی (حوزه برنامه)",
"whenConditionTooltip": "شرایطی که باید برای فعال شدن این میانبر رعایت شوند",
"unknownMainKey": "کلید ناشناخته",
@@ -787,8 +790,8 @@
"openGlobalSearch": "باز کردن جستجوی جهانی",
"openTheAppWindowCreateANewNote": "پنجره برنامه را باز کنید و یک یادداشت جدید ایجاد کنید",
"displayShortcutList": "نمایش لیست میانبر",
"closeOpenedDialogOverlay": "بستن گفتگو / همپوشانی باز شده؛ موارد آماده شده برای کپی یا جابجایی را رد کنید، موارد را لغو انتخاب کنید",
"openCloseSelectedFileInQuickView": "باز کردن / بستن فایل انتخاب شده در پنجره نمایش سریع. پشتیبانی شده: تصاویر، فیلم ها، صدا، PDF، متن ساده",
"closeOpenedDialogOverlay": "بستن گفتگو / همپوشانی / منو / انتخاب",
"openCloseSelectedFileInQuickView": "باز کردن / بستن فایل انتخاب‌شده در پنجره نمایش سریع",
"toggleFullScreen": "تغییر تمام صفحه",
"uiZoomIncrease": "افزایش بزرگنمایی UI",
"uiZoomDecrease": "کاهش زوم UI",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Les raccourcis globaux déclencheront des actions même lorsque l'application n'est pas ciblée.",
"changeShortcut": "Changer de raccourci",
"resetShortcut": "Réinitialiser le raccourci",
"unassignBinding": "Annuler l'attribution",
"addAlternateBinding": "Ajouter un raccourci alternatif",
"removeAlternateBinding": "Supprimer le raccourci alternatif",
"localShortcutsAppScope": "Raccourcis locaux (portée de l'application)",
"whenConditionTooltip": "Conditions qui doivent être remplies pour que ce raccourci se déclenche",
"unknownMainKey": "Touche inconnue",
@@ -788,8 +791,8 @@
"openGlobalSearch": "Ouvrir la recherche globale",
"openTheAppWindowCreateANewNote": "Ouvrez la fenêtre de l'application et créez une nouvelle note",
"displayShortcutList": "Afficher la liste des raccourcis",
"closeOpenedDialogOverlay": "Fermer la boîte de dialogue / la superposition ouverte ; ignorer les éléments préparés pour la copie ou le déplacement, désélectionner les éléments",
"openCloseSelectedFileInQuickView": "Ouvrir/fermer le fichier sélectionné dans la fenêtre Quick View. Pris en charge : images, vidéos, audio, PDF, texte brut",
"closeOpenedDialogOverlay": "Fermer dialogue / superposition / menu / sélection",
"openCloseSelectedFileInQuickView": "Ouvrir / fermer le fichier sélectionné dans la fenêtre Quick View",
"toggleFullScreen": "Passer en plein écran",
"uiZoomIncrease": "Augmentation du zoom de l'interface utilisateur",
"uiZoomDecrease": "diminution du zoom de l'interface utilisateur",
+5 -2
View File
@@ -808,6 +808,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "ऐप फोकस न होने पर भी वैश्विक शॉर्टकट कार्रवाइयों को ट्रिगर करेगा।",
"changeShortcut": "शॉर्टकट बदलें",
"resetShortcut": "शॉर्टकट रीसेट करें",
"unassignBinding": "बाइंडिंग हटाएँ",
"addAlternateBinding": "वैकल्पिक शॉर्टकट जोड़ें",
"removeAlternateBinding": "वैकल्पिक शॉर्टकट हटाएँ",
"localShortcutsAppScope": "स्थानीय शॉर्टकट (ऐप स्कोप)",
"whenConditionTooltip": "इस शॉर्टकट को ट्रिगर करने के लिए शर्तें पूरी होनी चाहिए",
"unknownMainKey": "अज्ञात कुंजी",
@@ -827,8 +830,8 @@
"openGlobalSearch": "वैश्विक खोज खोलें",
"openTheAppWindowCreateANewNote": "ऐप विंडो खोलें और एक नया नोट बनाएं",
"displayShortcutList": "शॉर्टकट सूची प्रदर्शित करें",
"closeOpenedDialogOverlay": "खोले गए संवाद/ओवरले को बंद करें; कॉपी करने या स्थानांतरित करने के लिए तैयार किए गए आइटम को ख़ारिज करें, आइटम का चयन रद्द करें",
"openCloseSelectedFileInQuickView": "त्वरित दृश्य विंडो में चयनित फ़ाइल खोलें/बंद करें। समर्थित: चित्र, वीडियो, ऑडियो, पीडीएफ, सादा पाठ",
"closeOpenedDialogOverlay": "संवाद / ओवरले / मेनू / चयन बंद करें",
"openCloseSelectedFileInQuickView": "त्वरित दृश्य विंडो में चयनित फ़ाइल खोलें / बंद करें",
"toggleFullScreen": "संपूर्ण स्क्रीन टॉगल करें",
"uiZoomIncrease": "यूआई ज़ूम वृद्धि",
"uiZoomDecrease": "यूआई ज़ूम में कमी",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Le scorciatoie globali attivano azioni anche quando l'app non è focalizzata.",
"changeShortcut": "Cambia scorciatoia",
"resetShortcut": "Reimposta scorciatoia",
"unassignBinding": "Rimuovi assegnazione",
"addAlternateBinding": "Aggiungi scorciatoia alternativa",
"removeAlternateBinding": "Rimuovi scorciatoia alternativa",
"localShortcutsAppScope": "Scorciatoie locali (sistema)",
"whenConditionTooltip": "Condizioni che devono essere soddisfatte affinché questa scorciatoia si attivi",
"unknownMainKey": "Tasto sconosciuto",
@@ -788,8 +791,8 @@
"openGlobalSearch": "Apri la ricerca globale",
"openTheAppWindowCreateANewNote": "Apri la finestra dell'app e crea una nuova nota",
"displayShortcutList": "Mostra la lista delle scorciatoie",
"closeOpenedDialogOverlay": "Chiudi finestra di dialogo aperta / sovrimpressione; elimina gli elementi preparati per la copia o lo spostamento, deseleziona gli elementi",
"openCloseSelectedFileInQuickView": "Apri / chiudi il file selezionato nella finestra Visualizzazione Rapida. File supportati: immagini, video, audio, PDF, testo normale",
"closeOpenedDialogOverlay": "Chiudi dialogo / sovrapposizione / menu / selezione",
"openCloseSelectedFileInQuickView": "Apri / chiudi il file selezionato nella finestra Quick View",
"toggleFullScreen": "Attiva lo schermo intero",
"uiZoomIncrease": "Aumento zoom della UI",
"uiZoomDecrease": "Diminuzione zoom della UI",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "グローバル ショートカットは、アプリがフォーカスされていなくてもアクションをトリガーします。",
"changeShortcut": "ショートカットを変更",
"resetShortcut": "ショートカットをリセット",
"unassignBinding": "割り当てを解除",
"addAlternateBinding": "代替キーバインドを追加",
"removeAlternateBinding": "代替キーバインドを削除",
"localShortcutsAppScope": "ローカル ショートカット (アプリ スコープ)",
"whenConditionTooltip": "このショートカットをトリガーするために満たすべき条件",
"unknownMainKey": "不明なキー",
@@ -788,8 +791,8 @@
"openGlobalSearch": "グローバル検索を開く",
"openTheAppWindowCreateANewNote": "アプリ ウィンドウを開いて新しいメモを作成",
"displayShortcutList": "ショートカット一覧を表示",
"closeOpenedDialogOverlay": "開いているダイアログ/オーバーレイを閉じます。コピーまたは移動の準備ができているアイテムを閉じ、アイテムの選択を解除します",
"openCloseSelectedFileInQuickView": "クイック ビュー ウィンドウで選択したファイルを開く/閉じる。サポート対象: 画像、動画、音声、PDF、プレーン テキスト",
"closeOpenedDialogOverlay": "ダイアログ / オーバーレイ / メニュー / 選択を閉じる",
"openCloseSelectedFileInQuickView": "クイック ビュー ウィンドウで選択したファイルを開く / 閉じる",
"toggleFullScreen": "フルスクリーン切り替え",
"uiZoomIncrease": "UI ズーム増加",
"uiZoomDecrease": "UIズーム減少",
+5 -2
View File
@@ -771,6 +771,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Atalhos globais acionarão ações mesmo quando o aplicativo não estiver em foco",
"changeShortcut": "Alterar atalho",
"resetShortcut": "Redefinir atalho",
"unassignBinding": "Desatribuir atalho",
"addAlternateBinding": "Adicionar atalho alternativo",
"removeAlternateBinding": "Remover atalho alternativo",
"localShortcutsAppScope": "Atalhos locais (escopo do aplicativo)",
"whenConditionTooltip": "Condições que devem ser atendidas para que este atalho seja acionado",
"unknownMainKey": "Tecla desconhecida",
@@ -790,8 +793,8 @@
"openGlobalSearch": "Abrir pesquisa global",
"openTheAppWindowCreateANewNote": "Abra a janela do aplicativo e crie uma nova nota",
"displayShortcutList": "Exibir lista de atalhos",
"closeOpenedDialogOverlay": "Fechar caixa de diálogo/sobreposição aberta; descartar itens preparados para copiar ou mover, desmarcar itens",
"openCloseSelectedFileInQuickView": "Abrir/fechar o arquivo selecionado na janela Quick View. Suportado: imagens, vídeos, áudio, PDF, texto simples",
"closeOpenedDialogOverlay": "Fechar diálogo / sobreposição / menu / seleção",
"openCloseSelectedFileInQuickView": "Abrir / fechar arquivo selecionado na janela Quick View",
"toggleFullScreen": "Alternar tela inteira",
"uiZoomIncrease": "Aumento de zoom da interface do usuário",
"uiZoomDecrease": "Diminuir zoom da interface do usuário",
+5 -2
View File
@@ -808,6 +808,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Глобальные шорткаты будут запускать действия, даже если приложение не сфокусировано.",
"changeShortcut": "Изменить шорткат",
"resetShortcut": "Сбросить шорткат",
"unassignBinding": "Снять назначение",
"addAlternateBinding": "Добавить дополнительную привязку",
"removeAlternateBinding": "Удалить дополнительную привязку",
"localShortcutsAppScope": "Локальные шорткаты (в приложении)",
"whenConditionTooltip": "Условия, которые должны быть выполнены для срабатывания этого шортката",
"unknownMainKey": "Неизвестная клавиша",
@@ -827,8 +830,8 @@
"openGlobalSearch": "Открыть глобальный поиск",
"openTheAppWindowCreateANewNote": "Открыть окно приложения и создать новую заметку",
"displayShortcutList": "Показать список шорткатов",
"closeOpenedDialogOverlay": "Закрыть открытый диалог/оверлей; закрыть элементы, подготовленные для копирования или перемещения, отменить выбор элементов",
"openCloseSelectedFileInQuickView": "Открыть/закрыть выбранный файл в окне быстрого просмотра. Поддерживаются: изображения, видео, аудио, PDF, обычный текст",
"closeOpenedDialogOverlay": "Закрыть диалог / оверлей / меню / выделение",
"openCloseSelectedFileInQuickView": "Открыть / закрыть выбранный файл в окне быстрого просмотра",
"toggleFullScreen": "Включить полноэкранный режим",
"uiZoomIncrease": "Увеличение масштаба интерфейса",
"uiZoomDecrease": "уменьшение масштаба интерфейса",
+5 -2
View File
@@ -808,6 +808,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Globalne bližnjice bodo sprožile dejanja tudi, ko aplikacija ni v fokusu.",
"changeShortcut": "Spremeni bližnjico",
"resetShortcut": "Ponastavi bližnjico",
"unassignBinding": "Odstrani dodelitev",
"addAlternateBinding": "Dodaj nadomestno bližnjico",
"removeAlternateBinding": "Odstrani nadomestno bližnjico",
"localShortcutsAppScope": "Lokalne bližnjice (obseg aplikacije)",
"whenConditionTooltip": "Pogoji, ki morajo biti izpolnjeni, da se bližnjica sproži",
"unknownMainKey": "Neznana tipka",
@@ -827,8 +830,8 @@
"openGlobalSearch": "Odpri globalno iskanje",
"openTheAppWindowCreateANewNote": "Odpri okno aplikacije & ustvari novo opombo",
"displayShortcutList": "Prikaži seznam bližnjic",
"closeOpenedDialogOverlay": "Zapri odprt dialog / prekrivanje; odkloni predmete pripravljen za kopiranje ali premik, odznači predmete",
"openCloseSelectedFileInQuickView": "Odpri / zapri izbrano datoteko v oknu Quick View. Podprto: slike, videi, zvok, PDF, besedilo",
"closeOpenedDialogOverlay": "Zapri dialog / prekrivanje / meni / izbiro",
"openCloseSelectedFileInQuickView": "Odpri / zapri izbrano datoteko v oknu Quick View",
"toggleFullScreen": "Preklopi celozaslonski način",
"uiZoomIncrease": "Povečaj UI povečavo",
"uiZoomDecrease": "Zmanjšaj UI povečavo",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Genel kısayollar, uygulama odaklanmadığında bile eylemleri tetikler.",
"changeShortcut": "Kısayolu değiştir",
"resetShortcut": "Kısayolu sıfırla",
"unassignBinding": "Atamayı kaldır",
"addAlternateBinding": "Alternatif kısayol ekle",
"removeAlternateBinding": "Alternatif kısayolu kaldır",
"localShortcutsAppScope": "Yerel kısayollar (uygulama kapsamı)",
"whenConditionTooltip": "Bu kısayolun tetiklenmesi için karşılanması gereken koşullar",
"unknownMainKey": "Bilinmeyen tuş",
@@ -788,8 +791,8 @@
"openGlobalSearch": "Global aramayı aç",
"openTheAppWindowCreateANewNote": "Uygulama penceresini aç ve yeni bir not oluştur",
"displayShortcutList": "Kısayol listesini görüntüle",
"closeOpenedDialogOverlay": "Açılan iletişim kutusunu / yer paylaşımını kapat; kopyalamak veya taşımak için hazırlanan öğeleri kapat, öğelerin seçimini kaldır",
"openCloseSelectedFileInQuickView": "Hızlı Görünüm penceresinde seçilen dosyayı aç / kapat. Desteklenen: resimler, videolar, ses, PDF, düz metin",
"closeOpenedDialogOverlay": "İletişim kutusu / yer paylaşımı / menü / seçimi kapat",
"openCloseSelectedFileInQuickView": "Hızlı Görünüm penceresinde seçilen dosyayı aç / kapat",
"toggleFullScreen": "Tam ekrana geç",
"uiZoomIncrease": "UI yakınlaştırma artışı",
"uiZoomDecrease": "UI yakınlaştırma azaltma",
+5 -2
View File
@@ -808,6 +808,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "ایپ فوکس میں نہ ہو تو بھی عالمی شارٹ کٹس کام کرتے ہیں۔",
"changeShortcut": "شارٹ کٹ بدلیں",
"resetShortcut": "شارٹ کٹ ری سیٹ کریں",
"unassignBinding": "تفویض ہٹائیں",
"addAlternateBinding": "متبادل شارٹ کٹ شامل کریں",
"removeAlternateBinding": "متبادل شارٹ کٹ ہٹائیں",
"localShortcutsAppScope": "مقامی شارٹ کٹس (ایپ سطح)",
"whenConditionTooltip": "اس شارٹ کٹ کے لیے پوری ہونے والی شرائط",
"unknownMainKey": "نامعلوم کلید",
@@ -827,8 +830,8 @@
"openGlobalSearch": "عالمی تلاش کھولیں",
"openTheAppWindowCreateANewNote": "ایپ کھولیں اور نیا نوٹ بنائیں",
"displayShortcutList": "شارٹ کٹ فہرست دکھائیں",
"closeOpenedDialogOverlay": "ڈائیلاگ/اوورلے بند کریں؛ کاپی/منتقل کے لیے تیار اشیاء منسوخ کریں، انتخاب ہٹائیں",
"openCloseSelectedFileInQuickView": "فوری نظارے میں منتخب فائل کھولیں / بند کریں۔ معاون: تصویر، ویڈیو، آڈیو، PDF، سادہ متن",
"closeOpenedDialogOverlay": "ڈائیلاگ / اوورلے / مینو / انتخاب برخاست کریں",
"openCloseSelectedFileInQuickView": "فوری نظارے میں منتخب فائل کھولیں / بند کریں",
"toggleFullScreen": "مکمل اسکرین ٹوگل کریں",
"uiZoomIncrease": "UI زوم بڑھائیں",
"uiZoomDecrease": "UI زوم کم کریں",
+5 -2
View File
@@ -769,6 +769,9 @@
"globalShortcutsTriggerActionsWhenNotFocused": "Các phím tắt chung sẽ kích hoạt hành động ngay cả khi ứng dụng không được đặt tiêu điểm.",
"changeShortcut": "Thay đổi phím tắt",
"resetShortcut": "Đặt lại phím tắt",
"unassignBinding": "Hủy gán phím tắt",
"addAlternateBinding": "Thêm phím tắt thay thế",
"removeAlternateBinding": "Xóa phím tắt thay thế",
"localShortcutsAppScope": "Phím tắt cục bộ (phạm vi ứng dụng)",
"whenConditionTooltip": "Các điều kiện phải được đáp ứng để phím tắt này kích hoạt",
"unknownMainKey": "Phím không xác định",
@@ -788,8 +791,8 @@
"openGlobalSearch": "Mở tìm kiếm toàn cầu",
"openTheAppWindowCreateANewNote": "Mở cửa sổ ứng dụng và tạo ghi chú mới",
"displayShortcutList": "Hiển thị danh sách phím tắt",
"closeOpenedDialogOverlay": "Đóng hộp thoại/lớp phủ đã mở; loại bỏ các mục chuẩn bị sao chép hoặc di chuyển, bỏ chọn các mục",
"openCloseSelectedFileInQuickView": "Mở/đóng tệp đã chọn trong cửa sổ Xem nhanh. Được hỗ trợ: hình ảnh, video, âm thanh, PDF, văn bản thuần túy",
"closeOpenedDialogOverlay": "Đóng hộp thoại / lớp phủ / menu / lựa chọn",
"openCloseSelectedFileInQuickView": "Mở / đóng tệp đã chọn trong cửa sổ Xem nhanh",
"toggleFullScreen": "Bật chế độ toàn màn hình",
"uiZoomIncrease": "Tăng thu phóng giao diện người dùng",
"uiZoomDecrease": "Giảm thu phóng giao diện người dùng",
@@ -25,6 +25,12 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from '@/components/ui/context-menu';
import {
KeyboardIcon,
RotateCcwIcon,
@@ -65,6 +71,7 @@ import {
formatCaptureChordLabel,
isModifierPhysicalKeyCode,
resolveShortcutKeyFromKeyboardEvent,
type ShortcutDefinition,
type ShortcutId,
type ShortcutKeys,
} from '@/stores/runtime/shortcuts';
@@ -134,7 +141,7 @@ const globalShortcutIcons: Record<GlobalShortcutId, Component> = {
const extensionsStorageStore = useExtensionsStorageStore();
const isDialogOpen = ref(false);
const editingShortcutId = ref<ShortcutId | null>(null);
const editingAppShortcutDefinition = ref<ShortcutDefinition | null>(null);
const editingGlobalShortcutId = ref<GlobalShortcutId | null>(null);
const recordedKeys = ref<ShortcutKeys | null>(null);
const isRecording = ref(false);
@@ -408,8 +415,8 @@ const editingDefinition = computed(() => {
return null;
}
if (!editingShortcutId.value) return null;
return shortcutsStore.getShortcutDefinition(editingShortcutId.value);
if (!editingAppShortcutDefinition.value) return null;
return editingAppShortcutDefinition.value;
});
const currentEditingShortcutLabel = computed(() => {
@@ -417,8 +424,8 @@ const currentEditingShortcutLabel = computed(() => {
return globalShortcutsStore.getShortcutLabel(editingGlobalShortcutId.value);
}
if (!editingShortcutId.value) return '';
return shortcutsStore.getShortcutLabel(editingShortcutId.value);
if (!editingAppShortcutDefinition.value) return '';
return shortcutsStore.getShortcutBindingLabel(editingAppShortcutDefinition.value);
});
const recordedKeysLabel = computed(() => {
@@ -438,20 +445,57 @@ function createUnassignedShortcutKeys(): ShortcutKeys {
return { key: '' };
}
async function contextUnassignShortcutBinding(definition: ShortcutDefinition): Promise<void> {
await shortcutsStore.setShortcut(
definition.id,
createUnassignedShortcutKeys(),
definition.bindingSlot ?? 0,
);
}
async function contextAddAlternateShortcutBinding(definition: ShortcutDefinition): Promise<void> {
await shortcutsStore.addUserAlternateChordBinding(definition.id, definition);
}
async function contextRemoveUserAlternateBinding(definition: ShortcutDefinition): Promise<void> {
await shortcutsStore.removeUserAlternateChordBindingRow(definition.id, definition.bindingSlot ?? 0);
}
async function contextResetAppShortcutBinding(definition: ShortcutDefinition): Promise<void> {
await shortcutsStore.resetShortcut(definition.id, definition.bindingSlot ?? 0);
}
async function contextResetGlobalShortcut(globalShortcutId: GlobalShortcutId): Promise<void> {
await globalShortcutsStore.resetShortcut(globalShortcutId);
}
async function contextUnassignGlobalShortcut(globalShortcutId: GlobalShortcutId): Promise<void> {
globalRegisterFailed.value = false;
await globalShortcutsStore.unsetShortcut(globalShortcutId);
}
function shortcutKeysConflict(firstKeys: ShortcutKeys, secondKeys: ShortcutKeys): boolean {
return Boolean(firstKeys.key && secondKeys.key) && shortcutKeysMatch(firstKeys, secondKeys);
}
function shortcutDefinitionsMatchSlot(
firstDefinition: ShortcutDefinition,
secondDefinition: ShortcutDefinition,
): boolean {
return firstDefinition.id === secondDefinition.id
&& (firstDefinition.bindingSlot ?? 0) === (secondDefinition.bindingSlot ?? 0);
}
function findConflictingAppShortcuts(
keys: ShortcutKeys,
excludeShortcutId?: ShortcutId,
excludeDefinition?: ShortcutDefinition,
) {
return shortcutsStore.definitions.filter((definition) => {
if (definition.id === excludeShortcutId) {
if (excludeDefinition && shortcutDefinitionsMatchSlot(definition, excludeDefinition)) {
return false;
}
return shortcutKeysConflict(shortcutsStore.getShortcutKeys(definition.id), keys);
return shortcutKeysConflict(shortcutsStore.resolveShortcutBindingKeys(definition), keys);
});
}
@@ -551,7 +595,7 @@ const conflictLabel = computed(() => {
const conflictingAppShortcut = findConflictingAppShortcuts(
recordedKeys.value,
editingShortcutId.value ?? undefined,
editingAppShortcutDefinition.value ?? undefined,
)[0] ?? null;
if (conflictingAppShortcut) {
@@ -572,11 +616,10 @@ const conflictLabel = computed(() => {
const hasConflict = computed(() => conflictLabel.value !== null);
function openShortcutEditor(shortcutId: ShortcutId) {
const definition = shortcutsStore.getShortcutDefinition(shortcutId);
if (definition?.isReadOnly) return;
function openShortcutEditor(definition: ShortcutDefinition) {
if (definition.isReadOnly) return;
editingShortcutId.value = shortcutId;
editingAppShortcutDefinition.value = definition;
editingExtensionKeybinding.value = null;
editingGlobalShortcutId.value = null;
recordedKeys.value = null;
@@ -592,7 +635,7 @@ function openShortcutEditor(shortcutId: ShortcutId) {
function openGlobalShortcutEditor(globalShortcutId: GlobalShortcutId) {
editingGlobalShortcutId.value = globalShortcutId;
editingExtensionKeybinding.value = null;
editingShortcutId.value = null;
editingAppShortcutDefinition.value = null;
recordedKeys.value = null;
globalRegisterFailed.value = false;
isRecording.value = true;
@@ -688,53 +731,45 @@ async function saveShortcut(): Promise<boolean> {
return true;
}
if (!editingShortcutId.value) return false;
await shortcutsStore.setShortcut(editingShortcutId.value, recordedKeys.value);
if (!editingAppShortcutDefinition.value) return false;
await shortcutsStore.setShortcut(
editingAppShortcutDefinition.value.id,
recordedKeys.value,
editingAppShortcutDefinition.value.bindingSlot ?? 0,
);
isDialogOpen.value = false;
editingShortcutId.value = null;
editingAppShortcutDefinition.value = null;
recordedKeys.value = null;
return true;
}
async function resetShortcut() {
if (editingGlobalShortcutId.value) {
await globalShortcutsStore.resetShortcut(editingGlobalShortcutId.value);
isDialogOpen.value = false;
editingGlobalShortcutId.value = null;
recordedKeys.value = null;
return;
}
if (!editingShortcutId.value) return;
await shortcutsStore.resetShortcut(editingShortcutId.value);
isDialogOpen.value = false;
editingShortcutId.value = null;
recordedKeys.value = null;
}
async function resetAllShortcuts() {
await shortcutsStore.resetAllShortcuts();
}
async function clearConflictingAppShortcuts(
keys: ShortcutKeys,
excludeShortcutId?: ShortcutId,
excludeDefinition?: ShortcutDefinition,
): Promise<ShortcutConflictRestore[]> {
const conflictingShortcuts = findConflictingAppShortcuts(keys, excludeShortcutId);
const conflictingShortcuts = findConflictingAppShortcuts(keys, excludeDefinition);
const restoreActions: ShortcutConflictRestore[] = [];
for (const shortcut of conflictingShortcuts) {
const previousKeys = shortcutsStore.getShortcutKeys(shortcut.id);
const previousSource = shortcutsStore.getSource(shortcut.id);
const previousKeys = shortcutsStore.resolveShortcutBindingKeys(shortcut);
const previousSource = shortcutsStore.getShortcutBindingSource(shortcut);
restoreActions.push(async () => {
if (previousSource === 'system') {
await shortcutsStore.resetShortcut(shortcut.id);
await shortcutsStore.resetShortcut(shortcut.id, shortcut.bindingSlot ?? 0);
return;
}
await shortcutsStore.setShortcut(shortcut.id, previousKeys);
await shortcutsStore.setShortcut(shortcut.id, previousKeys, shortcut.bindingSlot ?? 0);
});
await shortcutsStore.setShortcut(shortcut.id, createUnassignedShortcutKeys());
await shortcutsStore.setShortcut(
shortcut.id,
createUnassignedShortcutKeys(),
shortcut.bindingSlot ?? 0,
);
}
return restoreActions;
@@ -861,7 +896,7 @@ async function clearConflictingShortcuts(keys: ShortcutKeys): Promise<ShortcutCo
return restoreActions;
}
restoreActions.push(...await clearConflictingAppShortcuts(keys, editingShortcutId.value ?? undefined));
restoreActions.push(...await clearConflictingAppShortcuts(keys, editingAppShortcutDefinition.value ?? undefined));
restoreActions.push(...await clearConflictingLocalExtensionKeybindings(keys, editingExtensionKeybinding.value?.commandId));
return restoreActions;
}
@@ -886,26 +921,26 @@ async function replaceShortcut() {
}
}
function getConditionsLabel(shortcutId: ShortcutId): string {
const definition = shortcutsStore.getShortcutDefinition(shortcutId);
if (!definition) return '';
function getConditionsLabelFromDefinition(definition: ShortcutDefinition): string {
return formatConditionsLabel(definition.conditions);
}
function getSourceLabel(shortcutId: ShortcutId): string {
return shortcutsStore.getSource(shortcutId) === 'user' ? t('shortcutsUI.sourceUser') : t('shortcutsUI.sourceSystem');
function getShortcutBindingSourceLabel(definition: ShortcutDefinition): string {
return shortcutsStore.getShortcutBindingSource(definition) === 'user'
? t('shortcutsUI.sourceUser')
: t('shortcutsUI.sourceSystem');
}
function getGlobalSourceLabel(globalShortcutId: GlobalShortcutId): string {
return globalShortcutsStore.getSource(globalShortcutId) === 'user' ? t('shortcutsUI.sourceUser') : t('shortcutsUI.sourceSystem');
}
function getSourceIcon(shortcutId: ShortcutId): Component {
return shortcutsStore.getSource(shortcutId) === 'user' ? UserIcon : SettingsIcon;
function getShortcutBindingSourceIcon(definition: ShortcutDefinition): Component {
return shortcutsStore.getShortcutBindingSource(definition) === 'user' ? UserIcon : SettingsIcon;
}
function getShortcutListLabel(label: string): string {
return label || unassignedShortcutLabel;
function isShortcutLabelAssigned(label: string): boolean {
return label.trim().length > 0;
}
function openExtensionKeybindingEditor(extensionId: string, commandId: string, commandTitle: string) {
@@ -915,7 +950,7 @@ function openExtensionKeybindingEditor(extensionId: string, commandId: string, c
commandTitle,
scope: 'local',
};
editingShortcutId.value = null;
editingAppShortcutDefinition.value = null;
editingGlobalShortcutId.value = null;
recordedKeys.value = null;
globalRegisterFailed.value = false;
@@ -934,7 +969,7 @@ function openExtensionGlobalShortcutEditor(extensionId: string, commandId: strin
commandTitle,
scope: 'global',
};
editingShortcutId.value = null;
editingAppShortcutDefinition.value = null;
editingGlobalShortcutId.value = null;
recordedKeys.value = null;
globalRegisterFailed.value = false;
@@ -985,31 +1020,11 @@ async function saveExtensionKeybinding(): Promise<boolean> {
return true;
}
async function resetExtensionKeybinding() {
if (!editingExtensionKeybinding.value) return;
await extensionsStorageStore.removeKeybindingOverride(
editingExtensionKeybinding.value.extensionId,
editingExtensionKeybinding.value.commandId,
editingExtensionKeybinding.value.scope,
);
if (editingExtensionKeybinding.value.scope === 'local') {
extensionsStore.resetKeybindingOverride(editingExtensionKeybinding.value.commandId);
}
await globalShortcutsStore.syncExtensionShortcuts();
isDialogOpen.value = false;
editingExtensionKeybinding.value = null;
recordedKeys.value = null;
}
watch(isDialogOpen, (open) => {
if (!open) {
isRecording.value = false;
recordedKeys.value = null;
editingShortcutId.value = null;
editingAppShortcutDefinition.value = null;
editingGlobalShortcutId.value = null;
editingExtensionKeybinding.value = null;
}
@@ -1025,6 +1040,39 @@ watch(isDialogOpen, (open) => {
const extensionsStore = useExtensionsStore();
async function resetExtensionKeybindingAt(
extensionId: string,
commandId: string,
scope: 'local' | 'global',
): Promise<void> {
await extensionsStorageStore.removeKeybindingOverride(extensionId, commandId, scope);
if (scope === 'local') {
extensionsStore.resetKeybindingOverride(commandId);
}
await globalShortcutsStore.syncExtensionShortcuts();
}
async function unassignExtensionKeybindingAt(
extensionId: string,
commandId: string,
scope: 'local' | 'global',
): Promise<void> {
const override: ExtensionKeybindingOverride = {
commandId,
scope,
keys: createUnassignedShortcutKeys(),
};
await extensionsStorageStore.setKeybindingOverride(extensionId, override);
if (scope === 'local') {
extensionsStore.applyKeybindingOverride(commandId, createUnassignedShortcutKeys());
}
await globalShortcutsStore.syncExtensionShortcuts();
}
const extensionKeybindings = computed(() => {
return extensionsStore.keybindings.map((keybinding) => {
const commandIdWithoutPrefix = keybinding.commandId.replace(`${keybinding.extensionId}.`, '');
@@ -1114,88 +1162,136 @@ const globalExtensionShortcuts = computed(() => {
</div>
<div class="shortcuts-table__body">
<div
<ContextMenu
v-for="definition in globalShortcutsStore.definitions"
:key="definition.id"
class="shortcuts-table__row"
@click="openGlobalShortcutEditor(definition.id)"
>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<component
:is="globalShortcutIcons[definition.id]"
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ t(definition.labelKey) }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd class="shortcuts-table__kbd">
{{ getShortcutListLabel(globalShortcutsStore.getShortcutLabel(definition.id)) }}
</kbd>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when" />
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span
class="shortcuts-table__source"
:class="{
'shortcuts-table__source--user': globalShortcutsStore.getSource(definition.id) === 'user',
'shortcuts-table__source--system': globalShortcutsStore.getSource(definition.id) === 'system',
}"
<ContextMenuTrigger as-child>
<div
class="shortcuts-table__row"
@click="openGlobalShortcutEditor(definition.id)"
>
<component
:is="globalShortcutsStore.getSource(definition.id) === 'user' ? UserIcon : SettingsIcon"
:size="12"
/>
{{ getGlobalSourceLabel(definition.id) }}
</span>
</div>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<component
:is="globalShortcutIcons[definition.id]"
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ t(definition.labelKey) }}
</span>
</div>
<div
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd
v-if="isShortcutLabelAssigned(globalShortcutsStore.getShortcutLabel(definition.id))"
class="shortcuts-table__kbd"
>
{{ globalShortcutsStore.getShortcutLabel(definition.id) }}
</kbd>
<span
v-else
class="shortcuts-keybinding-unassigned"
>
{{ unassignedShortcutLabel }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when" />
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span
class="shortcuts-table__source"
:class="{
'shortcuts-table__source--user': globalShortcutsStore.getSource(definition.id) === 'user',
'shortcuts-table__source--system': globalShortcutsStore.getSource(definition.id) === 'system',
}"
>
<component
:is="globalShortcutsStore.getSource(definition.id) === 'user' ? UserIcon : SettingsIcon"
:size="12"
/>
{{ getGlobalSourceLabel(definition.id) }}
</span>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent class="shortcuts-settings-context-menu">
<ContextMenuItem @select="contextResetGlobalShortcut(definition.id)">
{{ t('shortcutsUI.resetShortcut') }}
</ContextMenuItem>
<ContextMenuItem @select="contextUnassignGlobalShortcut(definition.id)">
{{ t('shortcutsUI.unassignBinding') }}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<ContextMenu
v-for="shortcut in globalExtensionShortcuts"
:key="shortcut.commandId"
class="shortcuts-table__row"
@click="openExtensionGlobalShortcutEditor(shortcut.extensionId, shortcut.commandId, shortcut.commandTitle)"
:key="`${shortcut.extensionId}-${shortcut.commandId}`"
>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<BlocksIcon
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ shortcut.commandTitle }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd class="shortcuts-table__kbd">
{{ getShortcutListLabel(shortcut.keybindingLabel) }}
</kbd>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when" />
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span
class="shortcuts-table__source"
:class="{
'shortcuts-table__source--user': shortcut.source === 'user',
'shortcuts-table__source--system': shortcut.source === 'system',
}"
<ContextMenuTrigger as-child>
<div
class="shortcuts-table__row"
@click="openExtensionGlobalShortcutEditor(shortcut.extensionId, shortcut.commandId, shortcut.commandTitle)"
>
<component
:is="shortcut.source === 'user' ? UserIcon : SettingsIcon"
:size="12"
/>
{{ shortcut.extensionName }}
</span>
</div>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<BlocksIcon
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ shortcut.commandTitle }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd
v-if="isShortcutLabelAssigned(shortcut.keybindingLabel)"
class="shortcuts-table__kbd"
>
{{ shortcut.keybindingLabel }}
</kbd>
<span
v-else
class="shortcuts-keybinding-unassigned"
>
{{ unassignedShortcutLabel }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when" />
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span
class="shortcuts-table__source"
:class="{
'shortcuts-table__source--user': shortcut.source === 'user',
'shortcuts-table__source--system': shortcut.source === 'system',
}"
>
<component
:is="shortcut.source === 'user' ? UserIcon : SettingsIcon"
:size="12"
/>
{{ shortcut.extensionName }}
</span>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent class="shortcuts-settings-context-menu">
<ContextMenuItem
@select="resetExtensionKeybindingAt(shortcut.extensionId, shortcut.commandId, 'global')"
>
{{ t('shortcutsUI.resetShortcut') }}
</ContextMenuItem>
<ContextMenuItem
@select="unassignExtensionKeybindingAt(shortcut.extensionId, shortcut.commandId, 'global')"
>
{{ t('shortcutsUI.unassignBinding') }}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
</div>
</div>
@@ -1226,66 +1322,105 @@ const globalExtensionShortcuts = computed(() => {
</div>
<div class="shortcuts-table__body">
<div
<ContextMenu
v-for="definition in shortcutsStore.definitions"
:key="definition.id"
class="shortcuts-table__row"
:class="{ 'shortcuts-table__row--readonly': definition.isReadOnly }"
@click="openShortcutEditor(definition.id)"
:key="`${definition.id}-${definition.bindingSlot ?? 0}`"
>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<component
:is="shortcutIcons[definition.id]"
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ t(definition.labelKey) }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd class="shortcuts-table__kbd">
{{ getShortcutListLabel(shortcutsStore.getShortcutLabel(definition.id)) }}
</kbd>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when">
<Tooltip v-if="getConditionsLabel(definition.id)">
<TooltipTrigger as-child>
<code class="shortcuts-table__when-code">
{{ getConditionsLabel(definition.id) }}
</code>
</TooltipTrigger>
<TooltipContent side="top">
<code class="shortcuts-table__when-tooltip-code">
{{ getConditionsLabel(definition.id) }}
</code>
</TooltipContent>
</Tooltip>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span
class="shortcuts-table__source"
<ContextMenuTrigger
as-child
:disabled="definition.isReadOnly"
>
<div
class="shortcuts-table__row"
:class="{
'shortcuts-table__source--user': shortcutsStore.getSource(definition.id) === 'user',
'shortcuts-table__source--system': shortcutsStore.getSource(definition.id) === 'system',
'shortcuts-table__row--readonly': definition.isReadOnly,
'shortcuts-table__row--alternate-chord': definition.alternateChordRow,
}"
@click="openShortcutEditor(definition)"
>
<component
:is="getSourceIcon(definition.id)"
:size="12"
/>
{{ getSourceLabel(definition.id) }}
</span>
<LockIcon
v-if="definition.isReadOnly"
:size="10"
class="shortcuts-table__lock-icon"
/>
</div>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<component
:is="shortcutIcons[definition.id]"
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ t(definition.labelKey) }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd
v-if="isShortcutLabelAssigned(shortcutsStore.getShortcutBindingLabel(definition))"
class="shortcuts-table__kbd"
>
{{ shortcutsStore.getShortcutBindingLabel(definition) }}
</kbd>
<span
v-else
class="shortcuts-keybinding-unassigned"
>
{{ unassignedShortcutLabel }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when">
<Tooltip v-if="getConditionsLabelFromDefinition(definition)">
<TooltipTrigger as-child>
<code class="shortcuts-table__when-code">
{{ getConditionsLabelFromDefinition(definition) }}
</code>
</TooltipTrigger>
<TooltipContent side="top">
<code class="shortcuts-table__when-tooltip-code">
{{ getConditionsLabelFromDefinition(definition) }}
</code>
</TooltipContent>
</Tooltip>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span
class="shortcuts-table__source"
:class="{
'shortcuts-table__source--user': shortcutsStore.getShortcutBindingSource(definition) === 'user',
'shortcuts-table__source--system': shortcutsStore.getShortcutBindingSource(definition) === 'system',
}"
>
<component
:is="getShortcutBindingSourceIcon(definition)"
:size="12"
/>
{{ getShortcutBindingSourceLabel(definition) }}
</span>
<LockIcon
v-if="definition.isReadOnly"
:size="10"
class="shortcuts-table__lock-icon"
/>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent
v-if="!definition.isReadOnly"
class="shortcuts-settings-context-menu"
>
<ContextMenuItem @select="contextResetAppShortcutBinding(definition)">
{{ t('shortcutsUI.resetShortcut') }}
</ContextMenuItem>
<ContextMenuItem @select="contextUnassignShortcutBinding(definition)">
{{ t('shortcutsUI.unassignBinding') }}
</ContextMenuItem>
<ContextMenuItem @select="contextAddAlternateShortcutBinding(definition)">
{{ t('shortcutsUI.addAlternateBinding') }}
</ContextMenuItem>
<template v-if="definition.userDefinedAlternateChord">
<ContextMenuItem @select="contextRemoveUserAlternateBinding(definition)">
{{ t('shortcutsUI.removeAlternateBinding') }}
</ContextMenuItem>
</template>
</ContextMenuContent>
</ContextMenu>
</div>
</div>
</div>
@@ -1312,41 +1447,67 @@ const globalExtensionShortcuts = computed(() => {
</div>
<div class="shortcuts-table__body">
<div
<ContextMenu
v-for="keybinding in extensionKeybindings"
:key="keybinding.commandId"
class="shortcuts-table__row"
@click="openExtensionKeybindingEditor(keybinding.extensionId, keybinding.commandId, keybinding.commandTitle)"
:key="`${keybinding.extensionId}-${keybinding.commandId}`"
>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<BlocksIcon
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ keybinding.commandTitle }}
</span>
</div>
<ContextMenuTrigger as-child>
<div
class="shortcuts-table__row"
@click="openExtensionKeybindingEditor(keybinding.extensionId, keybinding.commandId, keybinding.commandTitle)"
>
<div class="shortcuts-table__cell shortcuts-table__cell--command">
<BlocksIcon
:size="14"
class="shortcuts-table__cell-icon"
/>
<span class="shortcuts-table__cell-text">
{{ keybinding.commandTitle }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd class="shortcuts-table__kbd">
{{ getShortcutListLabel(keybinding.keybindingLabel) }}
</kbd>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--keybinding">
<kbd
v-if="isShortcutLabelAssigned(keybinding.keybindingLabel)"
class="shortcuts-table__kbd"
>
{{ keybinding.keybindingLabel }}
</kbd>
<span
v-else
class="shortcuts-keybinding-unassigned"
>
{{ unassignedShortcutLabel }}
</span>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when">
<code class="shortcuts-table__when-code">
{{ keybinding.whenLabel }}
</code>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--when">
<code class="shortcuts-table__when-code">
{{ keybinding.whenLabel }}
</code>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span class="shortcuts-table__source shortcuts-table__source--extension">
<BlocksIcon :size="12" />
{{ keybinding.extensionName }}
</span>
</div>
</div>
<div class="shortcuts-table__cell shortcuts-table__cell--source">
<span class="shortcuts-table__source shortcuts-table__source--extension">
<BlocksIcon :size="12" />
{{ keybinding.extensionName }}
</span>
</div>
</div>
</ContextMenuTrigger>
<ContextMenuContent class="shortcuts-settings-context-menu">
<ContextMenuItem
@select="resetExtensionKeybindingAt(keybinding.extensionId, keybinding.commandId, 'local')"
>
{{ t('shortcutsUI.resetShortcut') }}
</ContextMenuItem>
<ContextMenuItem
@select="unassignExtensionKeybindingAt(keybinding.extensionId, keybinding.commandId, 'local')"
>
{{ t('shortcutsUI.unassignBinding') }}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
</div>
</template>
@@ -1373,20 +1534,29 @@ const globalExtensionShortcuts = computed(() => {
<span class="shortcut-editor__label">
{{ t('dialogs.shortcutEditorDialog.shortcut') }}:
</span>
<kbd class="shortcut-editor__kbd">
<kbd
v-if="isShortcutLabelAssigned(currentEditingShortcutLabel)"
class="shortcut-editor__kbd"
>
{{ currentEditingShortcutLabel }}
</kbd>
<span
v-else
class="shortcuts-keybinding-unassigned"
>
{{ unassignedShortcutLabel }}
</span>
</div>
<div
v-if="!isEditingGlobalShortcut && editingShortcutId && getConditionsLabel(editingShortcutId)"
v-if="!isEditingGlobalShortcut && editingAppShortcutDefinition && getConditionsLabelFromDefinition(editingAppShortcutDefinition)"
class="shortcut-editor__info-row"
>
<span class="shortcut-editor__label">
{{ t('when') }}:
</span>
<code class="shortcut-editor__when-code">
{{ getConditionsLabel(editingShortcutId) }}
{{ getConditionsLabelFromDefinition(editingAppShortcutDefinition) }}
</code>
</div>
@@ -1461,30 +1631,22 @@ const globalExtensionShortcuts = computed(() => {
</div>
<div class="shortcut-editor-dialog__footer">
<ConfirmButton
<Button
v-if="hasConflict"
variant="outline"
class="shortcut-editor-dialog__footer-button"
@click="isEditingExtension ? resetExtensionKeybinding() : resetShortcut()"
class="shortcut-editor-dialog__replace-button"
:disabled="!recordedKeys"
@click="replaceShortcut"
>
<RotateCcwIcon :size="14" />
{{ t('shortcutsUI.resetShortcut') }}
</ConfirmButton>
<div class="shortcut-editor-dialog__footer-right">
<Button
v-if="hasConflict"
variant="secondary"
:disabled="!recordedKeys"
@click="replaceShortcut"
>
{{ t('conflictDialog.replace') }}
</Button>
<Button
:disabled="!recordedKeys || hasConflict"
@click="isEditingExtension ? saveExtensionKeybinding() : saveShortcut()"
>
{{ t('save') }}
</Button>
</div>
{{ t('conflictDialog.replace') }}
</Button>
<Button
v-if="!hasConflict"
:disabled="!recordedKeys"
@click="isEditingExtension ? saveExtensionKeybinding() : saveShortcut()"
>
{{ t('save') }}
</Button>
</div>
</DialogContent>
</Dialog>
@@ -1595,7 +1757,7 @@ const globalExtensionShortcuts = computed(() => {
background-color: hsl(var(--muted) / 30%);
font-size: 0.75rem;
font-weight: 600;
grid-template-columns: 1fr 140px 160px 100px;
grid-template-columns: minmax(140px, 1fr) 140px 160px 100px;
text-transform: uppercase;
}
@@ -1613,7 +1775,7 @@ const globalExtensionShortcuts = computed(() => {
display: grid;
border-bottom: 1px solid hsl(var(--border) / 50%);
cursor: pointer;
grid-template-columns: 1fr 140px 160px 100px;
grid-template-columns: minmax(140px, 1fr) 140px 160px 100px;
transition: background-color 0.1s ease;
}
@@ -1626,6 +1788,10 @@ const globalExtensionShortcuts = computed(() => {
opacity: 0.7;
}
.shortcuts-table__row--alternate-chord {
background-color: hsl(var(--muted) / 30%);
}
.shortcuts-table__row--readonly:hover {
background-color: transparent;
}
@@ -1639,19 +1805,32 @@ const globalExtensionShortcuts = computed(() => {
.shortcuts-table__cell--command {
overflow: hidden;
min-width: 0;
align-items: flex-start;
}
.shortcuts-table__cell-icon {
flex-shrink: 0;
margin-top: 2px;
color: hsl(var(--muted-foreground));
}
.shortcuts-table__cell-text {
display: -webkit-box;
overflow: hidden;
min-width: 0;
-webkit-box-orient: vertical;
color: hsl(var(--foreground));
font-size: 0.875rem;
text-overflow: ellipsis;
white-space: nowrap;
-webkit-line-clamp: 2;
line-clamp: 2;
line-height: 1.35;
overflow-wrap: anywhere;
}
.shortcuts-keybinding-unassigned {
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
}
.shortcuts-table__kbd {
@@ -1870,19 +2049,20 @@ const globalExtensionShortcuts = computed(() => {
.shortcut-editor-dialog__footer {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-end;
padding-top: 1rem;
border-top: 1px solid hsl(var(--border));
margin-top: 0.5rem;
gap: 0.5rem;
}
.shortcut-editor-dialog__footer-button {
gap: 8px;
.shortcut-editor-dialog__replace-button.sigma-ui-button {
border-color: hsl(var(--destructive));
color: hsl(var(--destructive));
}
.shortcut-editor-dialog__footer-right {
display: flex;
gap: 0.5rem;
.shortcut-editor-dialog__replace-button.sigma-ui-button:hover {
background-color: hsl(var(--destructive) / 12%);
color: hsl(var(--destructive));
}
</style>
+79 -3
View File
@@ -69,6 +69,7 @@ describe('shortcuts store', () => {
extensionKeybindings.splice(0, extensionKeybindings.length);
userSettingsStoreMock.userSettings = reactive({
shortcuts: {},
shortcutUserAlternateChordSlots: {},
});
userSettingsStoreMock.setUserSettingsStorage.mockReset();
setAppKeybindingConflictCheckerMock.mockReset();
@@ -154,20 +155,22 @@ describe('shortcuts store', () => {
it('defines customizable mouse page and Alt+arrow pane navigation shortcuts', () => {
const shortcutsStore = useShortcutsStore();
expect(shortcutsStore.getShortcutLabel('navigatePageBack')).toBe('Mouse Button 4');
expect(shortcutsStore.getShortcutLabel('navigatePageForward')).toBe('Mouse Button 5');
expect(shortcutsStore.getShortcutLabel('navigatePageBack')).toBe('');
expect(shortcutsStore.getShortcutLabel('navigatePageForward')).toBe('');
expect(shortcutsStore.getShortcutLabel('navigateHistoryBack')).toBe('Alt+←');
expect(shortcutsStore.getShortcutLabel('navigateHistoryForward')).toBe('Alt+→');
expect(shortcutsStore.getShortcutLabel('goUpDirectory')).toBe('Alt+↑');
});
it('matches mouse shortcuts for page history navigation', async () => {
it('matches mouse shortcuts for page history navigation when assigned', async () => {
const shortcutsStore = useShortcutsStore();
const navigatePageBackHandler = vi.fn();
const navigatePageForwardHandler = vi.fn();
shortcutsStore.registerHandler('navigatePageBack', navigatePageBackHandler);
shortcutsStore.registerHandler('navigatePageForward', navigatePageForwardHandler);
await shortcutsStore.setShortcut('navigatePageBack', { key: 'MouseButton4' });
await shortcutsStore.setShortcut('navigatePageForward', { key: 'MouseButton5' });
const backEvent = new MouseEvent('mousedown', {
button: 3,
@@ -251,6 +254,79 @@ describe('shortcuts store', () => {
expect(upEvent.defaultPrevented).toBe(true);
});
it('matches default mouse back and forward for directory history alongside Alt+arrow', async () => {
const shortcutsStore = useShortcutsStore();
const navigateHistoryBackHandler = vi.fn();
const navigateHistoryForwardHandler = vi.fn();
shortcutsStore.registerHandler('navigateHistoryBack', navigateHistoryBackHandler);
shortcutsStore.registerHandler('navigateHistoryForward', navigateHistoryForwardHandler);
const mouseBackEvent = new MouseEvent('mousedown', {
button: 3,
bubbles: true,
cancelable: true,
});
const mouseForwardEvent = new MouseEvent('mousedown', {
button: 4,
bubbles: true,
cancelable: true,
});
await expect(shortcutsStore.handleMouseDown(mouseBackEvent)).resolves.toBe(true);
await expect(shortcutsStore.handleMouseDown(mouseForwardEvent)).resolves.toBe(true);
expect(navigateHistoryBackHandler).toHaveBeenCalledTimes(1);
expect(navigateHistoryForwardHandler).toHaveBeenCalledTimes(1);
expect(mouseBackEvent.defaultPrevented).toBe(true);
expect(mouseForwardEvent.defaultPrevented).toBe(true);
});
it('applies setShortcut without binding slot only to binding slot 0 and leaves slot 1 defaults', async () => {
const shortcutsStore = useShortcutsStore();
await shortcutsStore.setShortcut('navigateHistoryBack', {
ctrl: true,
key: 'h',
});
expect(shortcutsStore.getShortcutLabel('navigateHistoryBack')).toBe('Ctrl+H');
const mouseBackDefinition = shortcutsStore.definitions.find(
definition =>
definition.id === 'navigateHistoryBack'
&& (definition.bindingSlot ?? 0) === 1,
);
expect(mouseBackDefinition).toBeDefined();
expect(shortcutsStore.resolveShortcutBindingKeys(mouseBackDefinition!)).toEqual({
key: 'MouseButton4',
});
});
it('applies setShortcut with binding slot 1 without changing slot 0 keys', async () => {
const shortcutsStore = useShortcutsStore();
await shortcutsStore.setShortcut(
'navigateHistoryBack',
{ alt: true, key: 'h' },
1,
);
expect(shortcutsStore.getShortcutLabel('navigateHistoryBack')).toBe('Alt+←');
const mouseDefinition = shortcutsStore.definitions.find(
definition =>
definition.id === 'navigateHistoryBack'
&& (definition.bindingSlot ?? 0) === 1,
);
expect(mouseDefinition).toBeDefined();
expect(shortcutsStore.resolveShortcutBindingKeys(mouseDefinition!)).toEqual({
alt: true,
key: 'h',
});
});
it('keeps zoom and fullscreen shortcuts active while a dialog is open', async () => {
const shortcutsStore = useShortcutsStore();
const zoomInHandler = vi.fn();
+407 -27
View File
@@ -5,7 +5,12 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useUserSettingsStore } from '@/stores/storage/user-settings';
import type { ShortcutId, ShortcutKeys, UserShortcuts } from '@/types/user-settings';
import type {
ShortcutId,
ShortcutKeys,
ShortcutUserAlternateChordSlots,
UserShortcuts,
} from '@/types/user-settings';
import { setAppKeybindingConflictChecker } from '@/modules/extensions/api';
import { getSelectedEntries } from '@/modules/extensions/context';
import { useExtensionsStore } from '@/stores/runtime/extensions';
@@ -16,6 +21,7 @@ import {
import { isDialogOpened, isInputFieldActive } from '@/utils/dom-interaction-state';
export type { ShortcutId, ShortcutKeys, UserShortcuts };
export type { UserShortcutStoredValue, ShortcutUserAlternateChordSlots } from '@/types/user-settings';
export type ShortcutConditions = {
inputFieldIsActive?: boolean;
@@ -25,6 +31,9 @@ export type ShortcutConditions = {
export type ShortcutDefinition = {
id: ShortcutId;
bindingSlot?: number;
alternateChordRow?: boolean;
userDefinedAlternateChord?: boolean;
labelKey: string;
defaultKeys: ShortcutKeys;
scope: 'global' | 'navigator' | 'settings';
@@ -169,7 +178,7 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
id: 'navigatePageBack',
labelKey: 'shortcuts.navigatePageBack',
defaultKeys: {
key: 'MouseButton4',
key: '',
},
scope: 'global',
conditions: {
@@ -181,7 +190,7 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
id: 'navigatePageForward',
labelKey: 'shortcuts.navigatePageForward',
defaultKeys: {
key: 'MouseButton5',
key: '',
},
scope: 'global',
conditions: {
@@ -479,6 +488,7 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
},
{
id: 'navigateHistoryBack',
bindingSlot: 0,
labelKey: 'shortcuts.navigateHistoryBack',
defaultKeys: {
alt: true,
@@ -491,8 +501,24 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
},
isReadOnly: false,
},
{
id: 'navigateHistoryBack',
bindingSlot: 1,
alternateChordRow: true,
labelKey: 'shortcuts.navigateHistoryBack',
defaultKeys: {
key: 'MouseButton4',
},
scope: 'navigator',
conditions: {
inputFieldIsActive: false,
dialogIsOpened: false,
},
isReadOnly: false,
},
{
id: 'navigateHistoryForward',
bindingSlot: 0,
labelKey: 'shortcuts.navigateHistoryForward',
defaultKeys: {
alt: true,
@@ -505,6 +531,21 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
},
isReadOnly: false,
},
{
id: 'navigateHistoryForward',
bindingSlot: 1,
alternateChordRow: true,
labelKey: 'shortcuts.navigateHistoryForward',
defaultKeys: {
key: 'MouseButton5',
},
scope: 'navigator',
conditions: {
inputFieldIsActive: false,
dialogIsOpened: false,
},
isReadOnly: false,
},
{
id: 'goUpDirectory',
labelKey: 'shortcuts.goUpDirectory',
@@ -581,6 +622,53 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
},
];
function mergeShortcutDefinitionsWithUserAlternateRows(
baseDefinitions: ShortcutDefinition[],
userAlternateChordSlots: ShortcutUserAlternateChordSlots,
): ShortcutDefinition[] {
const merged: ShortcutDefinition[] = [];
let baseWalkIndex = 0;
while (baseWalkIndex < baseDefinitions.length) {
const currentShortcutId = baseDefinitions[baseWalkIndex].id;
while (
baseWalkIndex < baseDefinitions.length
&& baseDefinitions[baseWalkIndex].id === currentShortcutId
) {
merged.push(baseDefinitions[baseWalkIndex]);
baseWalkIndex += 1;
}
const extraSlots = userAlternateChordSlots[currentShortcutId];
if (!extraSlots?.length) continue;
const primaryTemplate = baseDefinitions.find(
definition =>
definition.id === currentShortcutId
&& (definition.bindingSlot ?? 0) === 0,
);
if (!primaryTemplate) continue;
for (const bindingSlot of extraSlots) {
merged.push({
id: primaryTemplate.id,
bindingSlot,
labelKey: primaryTemplate.labelKey,
defaultKeys: { key: '' },
scope: primaryTemplate.scope,
conditions: { ...primaryTemplate.conditions },
isReadOnly: primaryTemplate.isReadOnly,
alternateChordRow: true,
userDefinedAlternateChord: true,
});
}
}
return merged;
}
const MODIFIER_ONLY_KEYBOARD_CODES = new Set([
'MetaLeft',
'MetaRight',
@@ -1014,7 +1102,12 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
const userSettingsStore = useUserSettingsStore();
const extensionsStore = useExtensionsStore();
const definitions = ref<ShortcutDefinition[]>(DEFAULT_SHORTCUTS);
const definitions = computed(() =>
mergeShortcutDefinitionsWithUserAlternateRows(
DEFAULT_SHORTCUTS,
userSettingsStore.userSettings.shortcutUserAlternateChordSlots ?? {},
),
);
const handlers = ref<Map<ShortcutId, HandlerRegistration>>(new Map());
const isInitialized = ref(false);
const isListenerActive = ref(false);
@@ -1028,13 +1121,94 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
},
});
async function persistShortcutUserAlternateChordSlots(next: ShortcutUserAlternateChordSlots): Promise<void> {
const cleaned: ShortcutUserAlternateChordSlots = {};
for (const shortcutId of Object.keys(next) as ShortcutId[]) {
const slots = next[shortcutId];
if (slots?.length) {
cleaned[shortcutId] = slots;
}
}
userSettingsStore.userSettings.shortcutUserAlternateChordSlots = cleaned;
await userSettingsStore.setUserSettingsStorage('shortcutUserAlternateChordSlots', cleaned);
}
function getMaxBindingSlotIndex(shortcutId: ShortcutId): number {
let maxSlot = 0;
for (const definition of definitions.value) {
if (definition.id !== shortcutId) continue;
const slotIndex = definition.bindingSlot ?? 0;
if (slotIndex > maxSlot) {
maxSlot = slotIndex;
}
}
return maxSlot;
}
function resolveShortcutBindingKeys(definition: ShortcutDefinition): ShortcutKeys {
const bindingSlot = definition.bindingSlot ?? 0;
const shortcutId = definition.id;
const entry = userShortcuts.value[shortcutId];
const defaultKeys = definition.defaultKeys;
if (entry === undefined) {
return defaultKeys;
}
if (!Array.isArray(entry)) {
return bindingSlot === 0 ? entry : defaultKeys;
}
const slotValue = entry[bindingSlot];
return slotValue != null ? slotValue : defaultKeys;
}
function getShortcutBindingLabel(definition: ShortcutDefinition): string {
return formatShortcutKeys(resolveShortcutBindingKeys(definition));
}
function isShortcutBindingCustomized(definition: ShortcutDefinition): boolean {
const bindingSlot = definition.bindingSlot ?? 0;
const entry = userShortcuts.value[definition.id];
if (entry === undefined) {
return false;
}
if (!Array.isArray(entry)) {
return bindingSlot === 0;
}
return entry[bindingSlot] != null;
}
function getShortcutBindingSource(definition: ShortcutDefinition): 'system' | 'user' {
return isShortcutBindingCustomized(definition) ? 'user' : 'system';
}
function getPrimaryShortcutDefinition(shortcutId: ShortcutId): ShortcutDefinition | undefined {
return definitions.value.find(
definitionItem =>
definitionItem.id === shortcutId
&& (definitionItem.bindingSlot ?? 0) === 0,
);
}
function getShortcutKeys(shortcutId: ShortcutId): ShortcutKeys {
const userKeys = userShortcuts.value[shortcutId];
if (userKeys) return userKeys;
const definition = getPrimaryShortcutDefinition(shortcutId);
const definition = definitions.value.find(definitionItem => definitionItem.id === shortcutId);
if (!definition) {
return { key: '' };
}
return definition?.defaultKeys ?? { key: '' };
return resolveShortcutBindingKeys(definition);
}
function getShortcutLabel(shortcutId: ShortcutId): string {
@@ -1042,7 +1216,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
}
function getShortcutDefinition(shortcutId: ShortcutId): ShortcutDefinition | undefined {
return definitions.value.find(definitionItem => definitionItem.id === shortcutId);
return getPrimaryShortcutDefinition(shortcutId);
}
function isCustomized(shortcutId: ShortcutId): boolean {
@@ -1053,22 +1227,211 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
return isCustomized(shortcutId) ? 'user' : 'system';
}
async function setShortcut(shortcutId: ShortcutId, keys: ShortcutKeys): Promise<void> {
const newShortcuts = {
async function setShortcut(
shortcutId: ShortcutId,
keys: ShortcutKeys,
bindingSlot = 0,
): Promise<void> {
const arrayLength = getMaxBindingSlotIndex(shortcutId) + 1;
if (arrayLength <= 1) {
const newShortcuts = {
...userShortcuts.value,
[shortcutId]: keys,
};
userShortcuts.value = newShortcuts;
return;
}
const current = userShortcuts.value[shortcutId];
const nextArray: Array<ShortcutKeys | null> = Array.from({ length: arrayLength }, () => null);
if (Array.isArray(current)) {
for (let slotIndex = 0; slotIndex < arrayLength; slotIndex += 1) {
nextArray[slotIndex] = current[slotIndex] ?? null;
}
}
else if (current !== undefined) {
nextArray[0] = current;
}
nextArray[bindingSlot] = keys;
if (nextArray.every(slotValue => slotValue == null)) {
const newShortcuts = { ...userShortcuts.value };
delete newShortcuts[shortcutId];
userShortcuts.value = newShortcuts;
return;
}
userShortcuts.value = {
...userShortcuts.value,
[shortcutId]: keys,
[shortcutId]: nextArray,
};
userShortcuts.value = newShortcuts;
}
async function resetShortcut(shortcutId: ShortcutId): Promise<void> {
const newShortcuts = { ...userShortcuts.value };
delete newShortcuts[shortcutId];
userShortcuts.value = newShortcuts;
async function resetShortcut(shortcutId: ShortcutId, bindingSlot?: number): Promise<void> {
const arrayLength = getMaxBindingSlotIndex(shortcutId) + 1;
if (bindingSlot === undefined) {
const newShortcuts = { ...userShortcuts.value };
delete newShortcuts[shortcutId];
userShortcuts.value = newShortcuts;
const clearedExtras: ShortcutUserAlternateChordSlots = {
...(userSettingsStore.userSettings.shortcutUserAlternateChordSlots ?? {}),
};
delete clearedExtras[shortcutId];
await persistShortcutUserAlternateChordSlots(clearedExtras);
return;
}
if (arrayLength <= 1) {
const newShortcuts = { ...userShortcuts.value };
delete newShortcuts[shortcutId];
userShortcuts.value = newShortcuts;
return;
}
const current = userShortcuts.value[shortcutId];
const nextArray: Array<ShortcutKeys | null> = Array.from({ length: arrayLength }, () => null);
if (Array.isArray(current)) {
for (let slotIndex = 0; slotIndex < arrayLength; slotIndex += 1) {
nextArray[slotIndex] = current[slotIndex] ?? null;
}
}
else if (current !== undefined) {
nextArray[0] = current;
}
nextArray[bindingSlot] = null;
if (nextArray.every(slotValue => slotValue == null)) {
const newShortcuts = { ...userShortcuts.value };
delete newShortcuts[shortcutId];
userShortcuts.value = newShortcuts;
return;
}
userShortcuts.value = {
...userShortcuts.value,
[shortcutId]: nextArray,
};
}
function trimUserShortcutEntryToMaxSlot(shortcutId: ShortcutId): void {
const maxSlotIndex = getMaxBindingSlotIndex(shortcutId);
const arrayLength = maxSlotIndex + 1;
const currentEntry = userShortcuts.value[shortcutId];
if (!Array.isArray(currentEntry)) return;
const trimmed = currentEntry.slice(0, arrayLength);
if (trimmed.every(slotValue => slotValue == null)) {
const newShortcuts = { ...userShortcuts.value };
delete newShortcuts[shortcutId];
userShortcuts.value = newShortcuts;
return;
}
userShortcuts.value = {
...userShortcuts.value,
[shortcutId]: trimmed,
};
}
async function addUserAlternateChordBinding(
shortcutId: ShortcutId,
originDefinition: ShortcutDefinition,
): Promise<void> {
const primaryTemplate = DEFAULT_SHORTCUTS.find(
definition =>
definition.id === shortcutId
&& (definition.bindingSlot ?? 0) === 0,
);
if (!primaryTemplate || primaryTemplate.isReadOnly) return;
const slotsBefore = userSettingsStore.userSettings.shortcutUserAlternateChordSlots ?? {};
const mergedBefore = mergeShortcutDefinitionsWithUserAlternateRows(DEFAULT_SHORTCUTS, slotsBefore);
const originBindingSlot = originDefinition.bindingSlot ?? 0;
const originIdx = mergedBefore.findIndex(
definition =>
definition.id === shortcutId
&& (definition.bindingSlot ?? 0) === originBindingSlot,
);
if (originIdx < 0) return;
let endIdx = originIdx;
while (
endIdx + 1 < mergedBefore.length
&& mergedBefore[endIdx + 1].id === shortcutId
) {
endIdx += 1;
}
const insertMergedIndex = endIdx + 1;
const builtinCountForId = DEFAULT_SHORTCUTS.filter(definition => definition.id === shortcutId).length;
const groupStart = mergedBefore.findIndex(definition => definition.id === shortcutId);
if (groupStart < 0) return;
const userVisualStart = groupStart + builtinCountForId;
const rawInsertPosition = insertMergedIndex - userVisualStart;
const current: ShortcutUserAlternateChordSlots = { ...slotsBefore };
const userSlots = [...(current[shortcutId] ?? [])];
const insertIntoUserArray = Math.max(0, Math.min(rawInsertPosition, userSlots.length));
const maxBuiltinSlot = Math.max(
0,
...DEFAULT_SHORTCUTS
.filter(definition => definition.id === shortcutId)
.map(definition => definition.bindingSlot ?? 0),
);
const maxFromUserSlots = userSlots.length ? Math.max(...userSlots) : -1;
const maxSlotOverall = Math.max(maxBuiltinSlot, maxFromUserSlots);
const newSlot = maxSlotOverall + 1;
userSlots.splice(insertIntoUserArray, 0, newSlot);
current[shortcutId] = userSlots;
await persistShortcutUserAlternateChordSlots(current);
}
async function removeUserAlternateChordBindingRow(shortcutId: ShortcutId, bindingSlot: number): Promise<void> {
await resetShortcut(shortcutId, bindingSlot);
const current: ShortcutUserAlternateChordSlots = {
...(userSettingsStore.userSettings.shortcutUserAlternateChordSlots ?? {}),
};
const userSlots = [...(current[shortcutId] ?? [])];
const filtered = userSlots.filter(slot => slot !== bindingSlot);
if (filtered.length === userSlots.length) return;
if (filtered.length) {
current[shortcutId] = filtered;
}
else {
delete current[shortcutId];
}
await persistShortcutUserAlternateChordSlots(current);
trimUserShortcutEntryToMaxSlot(shortcutId);
}
async function resetAllShortcuts(): Promise<void> {
userShortcuts.value = {};
await persistShortcutUserAlternateChordSlots({});
}
function checkConditions(definition: ShortcutDefinition, registration?: HandlerRegistration): boolean {
@@ -1142,7 +1505,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
function findMatchingShortcut(event: KeyboardEvent): ShortcutId | null {
for (const definition of definitions.value) {
const keys = getShortcutKeys(definition.id);
const keys = resolveShortcutBindingKeys(definition);
if (matchesShortcut(event, keys)) {
return definition.id;
@@ -1156,7 +1519,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
const matchingShortcuts: ShortcutId[] = [];
for (const definition of definitions.value) {
const keys = getShortcutKeys(definition.id);
const keys = resolveShortcutBindingKeys(definition);
if (matchesShortcut(event, keys)) {
matchingShortcuts.push(definition.id);
@@ -1170,7 +1533,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
const matchingShortcuts: ShortcutId[] = [];
for (const definition of definitions.value) {
const keys = getShortcutKeys(definition.id);
const keys = resolveShortcutBindingKeys(definition);
if (matchesMouseShortcut(event, keys)) {
matchingShortcuts.push(definition.id);
@@ -1180,13 +1543,22 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
return matchingShortcuts;
}
function findConflictingShortcut(keys: ShortcutKeys, excludeShortcutId?: ShortcutId): ShortcutDefinition | null {
function findConflictingShortcut(
keys: ShortcutKeys,
excludeDefinition?: ShortcutDefinition,
): ShortcutDefinition | null {
const keysLabel = formatShortcutKeys(keys);
for (const definition of definitions.value) {
if (definition.id === excludeShortcutId) continue;
if (
excludeDefinition
&& definition.id === excludeDefinition.id
&& (definition.bindingSlot ?? 0) === (excludeDefinition.bindingSlot ?? 0)
) {
continue;
}
const existingLabel = getShortcutLabel(definition.id);
const existingLabel = formatShortcutKeys(resolveShortcutBindingKeys(definition));
if (existingLabel === keysLabel) {
return definition;
@@ -1311,9 +1683,6 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
const matchingShortcutIds = findAllMatchingMouseShortcuts(event);
if (matchingShortcutIds.length === 0) return false;
event.preventDefault();
event.stopPropagation();
for (const shortcutId of matchingShortcutIds) {
const result = callShortcutHandler(shortcutId);
if (result === null) continue;
@@ -1322,6 +1691,11 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
? (await result) !== false
: result !== false;
if (isHandled) {
event.preventDefault();
event.stopPropagation();
}
return isHandled;
}
@@ -1352,7 +1726,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
function hasConflictWithAppShortcut(keys: ShortcutKeys): boolean {
for (const definition of definitions.value) {
const shortcutKeys = getShortcutKeys(definition.id);
const shortcutKeys = resolveShortcutBindingKeys(definition);
if (
shortcutKeys.key.toLowerCase() === keys.key.toLowerCase()
@@ -1389,11 +1763,17 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
getShortcutKeys,
getShortcutLabel,
getShortcutDefinition,
resolveShortcutBindingKeys,
getShortcutBindingLabel,
getShortcutBindingSource,
isShortcutBindingCustomized,
isCustomized,
getSource,
setShortcut,
resetShortcut,
resetAllShortcuts,
addUserAlternateChordBinding,
removeUserAlternateChordBindingRow,
registerHandler,
unregisterHandler,
setShortcutCaptureActive,
+1
View File
@@ -151,6 +151,7 @@ export const useUserSettingsStore = defineStore('userSettings', () => {
visualFilters: createDefaultVisualFiltersSettings(),
settingsCurrentTab: 'general',
shortcuts: {},
shortcutUserAlternateChordSlots: {},
globalShortcuts: {},
focusWindowOnDriveConnected: true,
preventDropdownCloseFocusReturn: false,
+6 -1
View File
@@ -88,6 +88,8 @@ export type ShortcutKeys = {
key: string;
};
export type UserShortcutStoredValue = ShortcutKeys | Array<ShortcutKeys | null>;
export type ShortcutId
= 'toggleGlobalSearch'
| 'switchToHomePage'
@@ -131,7 +133,9 @@ export type ShortcutId
| 'uiZoomDecrease'
| 'toggleFullscreen';
export type UserShortcuts = Partial<Record<ShortcutId, ShortcutKeys>>;
export type UserShortcuts = Partial<Record<ShortcutId, UserShortcutStoredValue>>;
export type ShortcutUserAlternateChordSlots = Partial<Record<ShortcutId, number[]>>;
export type GlobalShortcutId = 'launchApp';
@@ -172,6 +176,7 @@ export type UserSettings = {
visualFilters: VisualFiltersSettings;
settingsCurrentTab: string;
shortcuts?: UserShortcuts;
shortcutUserAlternateChordSlots?: ShortcutUserAlternateChordSlots;
globalShortcuts?: UserGlobalShortcuts;
focusWindowOnDriveConnected: boolean;
preventDropdownCloseFocusReturn: boolean;