Scale stage 3: shortcuts service + Ctrl/Cmd +/-/0 for the scale
Groundwork for the vim-style bindings planned next: the app previously had NO
keyboard abstraction — `event.metaKey || event.ctrlKey` copy-pasted inline in
six places, no typing-target guard anywhere, every window root matching keys
by hand.
New @core/shortcuts.service.ts owns:
- isMod() (also accepts MouseEvent — Mod+click toggle-select uses the same
rule) and isTypingTarget();
- ONE declarative keymap: resolve(event, scope) -> ShortcutId | null, scoped
main / editor / manager. Layout rules are encoded once: digits, +/- and
Alt-letter chords match on event.code (AZERTY digit rows, layouts where '+'
needs Shift, macOS Option-Z producing Ω); plain letter chords match on
event.key so they follow the user's layout.
Both app.ts, both editor-window.ts and both connection-manager.ts now resolve
through the service and switch on ids — behaviour-preserving for every
existing shortcut, including the deliberate quirks (Ctrl+Tab never meta
because Cmd-Tab is the macOS app switcher; Cmd+9 = last tab; editor Mod+W is
the Windows/Linux arm, macOS routes via the app menu). Escape and Tab stay
component-local on purpose: their meaning is per-window state (find bar open?
close prompt up? caret in the textarea?), not a global binding. The file
panels keep their contextual key state machine but delegate modifier tests.
On top of the table: Ctrl/Cmd +/-/0 steps/resets the interface scale in every
window kind, all three previously unclaimed. isTypingTarget is applied
per-shortcut, not globally in resolve(): the scale keys must not fire while
typing ('-' in a rename prompt must type '-'), but the editor WANTS Mod+S
mid-typing and the tab shortcuts keep their existing reach.
Docs: architecture.md's density prose now describes the uiScale lever;
gotchas.md gains the scale section — the disassembly evidence that native
page zoom is an empty stub on Linux (so nobody tries it again), the
everything-must-be-rem rule, the skinfix-outranks-variants trap the harness
caught (48px close button silently 29px), and the px-mirrors-in-TS class of
bug with the remPx() remedy.
Verified: tsc + build clean; live boot with the user's real legacy settings
file migrates textSize:large -> uiScale 1.2 and renders scaled. Not verified
here (needs interaction): the stepper clicks and the shortcut keys themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import { PanelsService } from '@core/panels.service';
|
||||
import { blockNativeContextMenu } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { StatusBar } from './status-bar/status-bar';
|
||||
import { TabBar } from './tab-bar/tab-bar';
|
||||
import { TitleBar } from './title-bar/title-bar';
|
||||
@@ -62,6 +63,7 @@ export class App {
|
||||
readonly connections = inject(ConnectionsService);
|
||||
private readonly transfers = inject(TransfersService);
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
private readonly updater = inject(UpdaterService);
|
||||
|
||||
/** The connection-manager window is open — this window is modal-blocked. */
|
||||
@@ -176,26 +178,43 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
/** Tab (workspace) shortcuts — panel-level keys live on the panels themselves. */
|
||||
/** Window shortcuts — matching lives in ShortcutsService (one keymap for the
|
||||
* app); this switch only maps ids onto actions. Panel-level keys stay on
|
||||
* the panels themselves. */
|
||||
onWindowKeydown(event: KeyboardEvent): void {
|
||||
if (this.managerOpen()) return; // modal-blocked behind the manager
|
||||
const mod = event.metaKey || event.ctrlKey;
|
||||
if (mod && event.key.toLowerCase() === 't') {
|
||||
event.preventDefault();
|
||||
this.panels.newWorkspace();
|
||||
} else if (mod && event.key.toLowerCase() === 'w') {
|
||||
event.preventDefault();
|
||||
this.panels.closeWorkspace(this.panels.activeWorkspaceId());
|
||||
} else if (event.ctrlKey && event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
this.panels.cycleWorkspace(event.shiftKey ? -1 : 1);
|
||||
} else if (mod && !event.shiftKey && !event.altKey) {
|
||||
// ⌘1–⌘8 jump to that tab, ⌘9 to the last (Ctrl+digit elsewhere), like
|
||||
// Firefox. Matched on event.code so the digit row works on any layout.
|
||||
const digit = /^(?:Digit|Numpad)([1-9])$/.exec(event.code)?.[1];
|
||||
if (!digit) return;
|
||||
event.preventDefault();
|
||||
this.panels.goToWorkspace(digit === '9' ? -1 : Number(digit) - 1);
|
||||
const match = this.shortcuts.resolve(event, 'main');
|
||||
if (!match) return;
|
||||
// scale keys are hostile mid-typing ('-' in a rename prompt must type '-');
|
||||
// the tab shortcuts keep firing there, as they always did
|
||||
if (match.id.startsWith('scale-') && this.shortcuts.isTypingTarget(event)) return;
|
||||
event.preventDefault();
|
||||
switch (match.id) {
|
||||
case 'new-tab':
|
||||
this.panels.newWorkspace();
|
||||
break;
|
||||
case 'close-tab':
|
||||
this.panels.closeWorkspace(this.panels.activeWorkspaceId());
|
||||
break;
|
||||
case 'cycle-tab':
|
||||
this.panels.cycleWorkspace(1);
|
||||
break;
|
||||
case 'cycle-tab-back':
|
||||
this.panels.cycleWorkspace(-1);
|
||||
break;
|
||||
case 'go-to-tab':
|
||||
// ⌘1–⌘8 jump to that tab, ⌘9 to the last, like Firefox
|
||||
this.panels.goToWorkspace(match.digit === 9 ? -1 : match.digit! - 1);
|
||||
break;
|
||||
case 'scale-up':
|
||||
this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
break;
|
||||
case 'scale-down':
|
||||
this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
break;
|
||||
case 'scale-reset':
|
||||
this.settings.setUiScale(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DialogsService } from '@core/dialogs.service';
|
||||
import { blockNativeContextMenu } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { TitleBar } from '../title-bar/title-bar';
|
||||
import { Toasts } from '../toasts/toasts';
|
||||
|
||||
@@ -77,6 +78,7 @@ function fromDraft(d: Draft): SavedConnection {
|
||||
templateUrl: './connection-manager.html',
|
||||
host: {
|
||||
'(window:keydown.escape)': 'onEscape()',
|
||||
'(window:keydown)': 'onWindowKeydown($event)',
|
||||
'(window:contextmenu)': 'blockContextMenu($event)',
|
||||
// Aero desaturates an unfocused window's chrome — see .window.active.
|
||||
'(window:focus)': 'windowFocused.set(true)',
|
||||
@@ -94,6 +96,7 @@ export class ConnectionManager {
|
||||
// injected for its side effects: fetches settings and applies theme/accent/
|
||||
// text-size data attributes to this window's document root
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
|
||||
readonly draft = signal<Draft | null>(null);
|
||||
readonly busy = signal(false);
|
||||
@@ -149,6 +152,16 @@ export class ConnectionManager {
|
||||
if (!this.dialogs.anyOpen) this.close();
|
||||
}
|
||||
|
||||
/** Any-window shortcuts (interface scale). Escape has its own binding above. */
|
||||
onWindowKeydown(event: KeyboardEvent): void {
|
||||
const match = this.shortcuts.resolve(event, 'manager');
|
||||
if (!match || this.shortcuts.isTypingTarget(event)) return;
|
||||
event.preventDefault();
|
||||
if (match.id === 'scale-up') this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
else if (match.id === 'scale-down') this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
else if (match.id === 'scale-reset') this.settings.setUiScale(1);
|
||||
}
|
||||
|
||||
blockContextMenu(event: Event): void {
|
||||
blockNativeContextMenu(event);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { I18nService } from '@core/i18n.service';
|
||||
import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, MOD_LABEL } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { ToastService } from '@core/toast.service';
|
||||
import { TitleBar } from '../title-bar/title-bar';
|
||||
import { Toasts } from '../toasts/toasts';
|
||||
@@ -108,6 +109,7 @@ export class EditorWindow {
|
||||
// injected for its side effects: fetches settings and applies theme/accent/
|
||||
// text-size data attributes to this window's document root
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
|
||||
private draftTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private lastSentDirty = false;
|
||||
@@ -324,22 +326,43 @@ export class EditorWindow {
|
||||
// ---- keyboard ----
|
||||
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
const mod = event.metaKey || event.ctrlKey;
|
||||
if (mod && event.key.toLowerCase() === 's') {
|
||||
// Chord shortcuts resolve through the shared keymap (ShortcutsService);
|
||||
// Escape and Tab stay below because their meaning is THIS window's state
|
||||
// (close prompt up? find bar open? caret in the textarea?), not a binding.
|
||||
const match = this.shortcuts.resolve(event, 'editor');
|
||||
if (match) {
|
||||
// scale keys must not fire while typing — but the editor's own chords
|
||||
// (save/find/wrap) are WANTED mid-typing, so the guard is per-shortcut
|
||||
if (match.id.startsWith('scale-') && this.shortcuts.isTypingTarget(event)) return;
|
||||
event.preventDefault();
|
||||
void this.save();
|
||||
} else if (mod && event.key.toLowerCase() === 'f') {
|
||||
event.preventDefault();
|
||||
this.openFind();
|
||||
} else if (mod && event.key.toLowerCase() === 'w') {
|
||||
// reaches us on Windows/Linux; on macOS the app menu intercepts ⌘W and
|
||||
// bun routes it back as editorCloseRequest
|
||||
event.preventDefault();
|
||||
this.requestClose();
|
||||
} else if (event.altKey && event.code === 'KeyZ') {
|
||||
event.preventDefault();
|
||||
this.toggleWrap();
|
||||
} else if (event.key === 'Escape' && this.closePrompt()) {
|
||||
switch (match.id) {
|
||||
case 'save':
|
||||
void this.save();
|
||||
break;
|
||||
case 'find':
|
||||
this.openFind();
|
||||
break;
|
||||
case 'close-window':
|
||||
// reaches us on Windows/Linux; on macOS the app menu intercepts ⌘W
|
||||
// and bun routes it back as editorCloseRequest
|
||||
this.requestClose();
|
||||
break;
|
||||
case 'toggle-wrap':
|
||||
this.toggleWrap();
|
||||
break;
|
||||
case 'scale-up':
|
||||
this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
break;
|
||||
case 'scale-down':
|
||||
this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
break;
|
||||
case 'scale-reset':
|
||||
this.settings.setUiScale(1);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape' && this.closePrompt()) {
|
||||
event.preventDefault();
|
||||
this.cancelClose();
|
||||
} else if (event.key === 'Escape' && this.findOpen()) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PanelsService, type Side, type SortKey } from '@core/panels.service';
|
||||
import { IS_WINDOWS } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { TransfersService } from '@core/transfers.service';
|
||||
|
||||
/** Optional columns in fixed display order, with their track widths. */
|
||||
@@ -50,6 +51,7 @@ export class FilePanel {
|
||||
readonly dialogs = inject(DialogsService);
|
||||
private readonly menu = inject(ContextMenuService);
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
readonly panel = computed(() => this.panels.get(this.side()));
|
||||
readonly isActive = computed(() => this.panels.activeSide() === this.side());
|
||||
@@ -212,7 +214,7 @@ export class FilePanel {
|
||||
const panel = this.panel();
|
||||
if (event.shiftKey) {
|
||||
panel.selectRange(this.lastClickedIndex, index);
|
||||
} else if (event.metaKey || event.ctrlKey) {
|
||||
} else if (this.shortcuts.isMod(event)) {
|
||||
const entry = panel.sorted()[index];
|
||||
if (entry) panel.toggleSelect(entry.name);
|
||||
} else {
|
||||
@@ -258,7 +260,7 @@ export class FilePanel {
|
||||
this.panels.setActiveSide(this.panels.otherSide(this.side()));
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === 'a') {
|
||||
if (this.shortcuts.isMod(event) && key.toLowerCase() === 'a') {
|
||||
event.preventDefault();
|
||||
panel.selectAll();
|
||||
return;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PanelsService } from '@core/panels.service';
|
||||
import { blockNativeContextMenu } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { StatusBar } from './status-bar/status-bar';
|
||||
import { TabBar } from './tab-bar/tab-bar';
|
||||
import { ToastService } from '@core/toast.service';
|
||||
@@ -55,6 +56,7 @@ export class App {
|
||||
readonly connections = inject(ConnectionsService);
|
||||
private readonly transfers = inject(TransfersService);
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
private readonly updater = inject(UpdaterService);
|
||||
|
||||
/** The connection-manager window is open — this window is modal-blocked. */
|
||||
@@ -154,26 +156,43 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
/** Tab (workspace) shortcuts — panel-level keys live on the panels themselves. */
|
||||
/** Window shortcuts — matching lives in ShortcutsService (one keymap for the
|
||||
* app); this switch only maps ids onto actions. Panel-level keys stay on
|
||||
* the panels themselves. */
|
||||
onWindowKeydown(event: KeyboardEvent): void {
|
||||
if (this.managerOpen()) return; // modal-blocked behind the manager
|
||||
const mod = event.metaKey || event.ctrlKey;
|
||||
if (mod && event.key.toLowerCase() === 't') {
|
||||
event.preventDefault();
|
||||
this.panels.newWorkspace();
|
||||
} else if (mod && event.key.toLowerCase() === 'w') {
|
||||
event.preventDefault();
|
||||
this.panels.closeWorkspace(this.panels.activeWorkspaceId());
|
||||
} else if (event.ctrlKey && event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
this.panels.cycleWorkspace(event.shiftKey ? -1 : 1);
|
||||
} else if (mod && !event.shiftKey && !event.altKey) {
|
||||
// ⌘1–⌘8 jump to that tab, ⌘9 to the last (Ctrl+digit elsewhere), like
|
||||
// Firefox. Matched on event.code so the digit row works on any layout.
|
||||
const digit = /^(?:Digit|Numpad)([1-9])$/.exec(event.code)?.[1];
|
||||
if (!digit) return;
|
||||
event.preventDefault();
|
||||
this.panels.goToWorkspace(digit === '9' ? -1 : Number(digit) - 1);
|
||||
const match = this.shortcuts.resolve(event, 'main');
|
||||
if (!match) return;
|
||||
// scale keys are hostile mid-typing ('-' in a rename prompt must type '-');
|
||||
// the tab shortcuts keep firing there, as they always did
|
||||
if (match.id.startsWith('scale-') && this.shortcuts.isTypingTarget(event)) return;
|
||||
event.preventDefault();
|
||||
switch (match.id) {
|
||||
case 'new-tab':
|
||||
this.panels.newWorkspace();
|
||||
break;
|
||||
case 'close-tab':
|
||||
this.panels.closeWorkspace(this.panels.activeWorkspaceId());
|
||||
break;
|
||||
case 'cycle-tab':
|
||||
this.panels.cycleWorkspace(1);
|
||||
break;
|
||||
case 'cycle-tab-back':
|
||||
this.panels.cycleWorkspace(-1);
|
||||
break;
|
||||
case 'go-to-tab':
|
||||
// ⌘1–⌘8 jump to that tab, ⌘9 to the last, like Firefox
|
||||
this.panels.goToWorkspace(match.digit === 9 ? -1 : match.digit! - 1);
|
||||
break;
|
||||
case 'scale-up':
|
||||
this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
break;
|
||||
case 'scale-down':
|
||||
this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
break;
|
||||
case 'scale-reset':
|
||||
this.settings.setUiScale(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DialogsService } from '@core/dialogs.service';
|
||||
import { blockNativeContextMenu, IS_MAC_CHROME } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { Toasts } from '../toasts/toasts';
|
||||
|
||||
interface Draft {
|
||||
@@ -76,6 +77,7 @@ function fromDraft(d: Draft): SavedConnection {
|
||||
templateUrl: './connection-manager.html',
|
||||
host: {
|
||||
'(window:keydown.escape)': 'onEscape()',
|
||||
'(window:keydown)': 'onWindowKeydown($event)',
|
||||
'(window:contextmenu)': 'blockContextMenu($event)',
|
||||
},
|
||||
})
|
||||
@@ -88,6 +90,7 @@ export class ConnectionManager {
|
||||
// injected for its side effects: fetches settings and applies theme/accent/
|
||||
// text-size data attributes to this window's document root
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
|
||||
readonly draft = signal<Draft | null>(null);
|
||||
readonly busy = signal(false);
|
||||
@@ -143,6 +146,16 @@ export class ConnectionManager {
|
||||
if (!this.dialogs.anyOpen) this.close();
|
||||
}
|
||||
|
||||
/** Any-window shortcuts (interface scale). Escape has its own binding above. */
|
||||
onWindowKeydown(event: KeyboardEvent): void {
|
||||
const match = this.shortcuts.resolve(event, 'manager');
|
||||
if (!match || this.shortcuts.isTypingTarget(event)) return;
|
||||
event.preventDefault();
|
||||
if (match.id === 'scale-up') this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
else if (match.id === 'scale-down') this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
else if (match.id === 'scale-reset') this.settings.setUiScale(1);
|
||||
}
|
||||
|
||||
blockContextMenu(event: Event): void {
|
||||
blockNativeContextMenu(event);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { I18nService } from '@core/i18n.service';
|
||||
import { ALT_LABEL, blockNativeContextMenu, EDITOR_CONTEXT, IS_MAC_CHROME, MOD_LABEL } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { ToastService } from '@core/toast.service';
|
||||
import { Toasts } from '../toasts/toasts';
|
||||
|
||||
@@ -101,6 +102,7 @@ export class EditorWindow {
|
||||
// injected for its side effects: fetches settings and applies theme/accent/
|
||||
// text-size data attributes to this window's document root
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
|
||||
private draftTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private lastSentDirty = false;
|
||||
@@ -317,22 +319,43 @@ export class EditorWindow {
|
||||
// ---- keyboard ----
|
||||
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
const mod = event.metaKey || event.ctrlKey;
|
||||
if (mod && event.key.toLowerCase() === 's') {
|
||||
// Chord shortcuts resolve through the shared keymap (ShortcutsService);
|
||||
// Escape and Tab stay below because their meaning is THIS window's state
|
||||
// (close prompt up? find bar open? caret in the textarea?), not a binding.
|
||||
const match = this.shortcuts.resolve(event, 'editor');
|
||||
if (match) {
|
||||
// scale keys must not fire while typing — but the editor's own chords
|
||||
// (save/find/wrap) are WANTED mid-typing, so the guard is per-shortcut
|
||||
if (match.id.startsWith('scale-') && this.shortcuts.isTypingTarget(event)) return;
|
||||
event.preventDefault();
|
||||
void this.save();
|
||||
} else if (mod && event.key.toLowerCase() === 'f') {
|
||||
event.preventDefault();
|
||||
this.openFind();
|
||||
} else if (mod && event.key.toLowerCase() === 'w') {
|
||||
// reaches us on Windows/Linux; on macOS the app menu intercepts ⌘W and
|
||||
// bun routes it back as editorCloseRequest
|
||||
event.preventDefault();
|
||||
this.requestClose();
|
||||
} else if (event.altKey && event.code === 'KeyZ') {
|
||||
event.preventDefault();
|
||||
this.toggleWrap();
|
||||
} else if (event.key === 'Escape' && this.closePrompt()) {
|
||||
switch (match.id) {
|
||||
case 'save':
|
||||
void this.save();
|
||||
break;
|
||||
case 'find':
|
||||
this.openFind();
|
||||
break;
|
||||
case 'close-window':
|
||||
// reaches us on Windows/Linux; on macOS the app menu intercepts ⌘W
|
||||
// and bun routes it back as editorCloseRequest
|
||||
this.requestClose();
|
||||
break;
|
||||
case 'toggle-wrap':
|
||||
this.toggleWrap();
|
||||
break;
|
||||
case 'scale-up':
|
||||
this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
break;
|
||||
case 'scale-down':
|
||||
this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
break;
|
||||
case 'scale-reset':
|
||||
this.settings.setUiScale(1);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape' && this.closePrompt()) {
|
||||
event.preventDefault();
|
||||
this.cancelClose();
|
||||
} else if (event.key === 'Escape' && this.findOpen()) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PanelsService, type Side, type SortKey } from '@core/panels.service';
|
||||
import { IS_WINDOWS } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
import { TransfersService } from '@core/transfers.service';
|
||||
|
||||
/** Optional columns in fixed display order, with their track widths. */
|
||||
@@ -50,6 +51,7 @@ export class FilePanel {
|
||||
readonly dialogs = inject(DialogsService);
|
||||
private readonly menu = inject(ContextMenuService);
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
readonly panel = computed(() => this.panels.get(this.side()));
|
||||
readonly isActive = computed(() => this.panels.activeSide() === this.side());
|
||||
@@ -220,7 +222,7 @@ export class FilePanel {
|
||||
const panel = this.panel();
|
||||
if (event.shiftKey) {
|
||||
panel.selectRange(this.lastClickedIndex, index);
|
||||
} else if (event.metaKey || event.ctrlKey) {
|
||||
} else if (this.shortcuts.isMod(event)) {
|
||||
const entry = panel.sorted()[index];
|
||||
if (entry) panel.toggleSelect(entry.name);
|
||||
} else {
|
||||
@@ -266,7 +268,7 @@ export class FilePanel {
|
||||
this.panels.setActiveSide(this.panels.otherSide(this.side()));
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === 'a') {
|
||||
if (this.shortcuts.isMod(event) && key.toLowerCase() === 'a') {
|
||||
event.preventDefault();
|
||||
panel.selectAll();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user