Remap any shortcut in Settings → Keyboard; the macOS menu bar follows
A Keyboard section in the Settings window edits the shared keymap: rows by group with a search field, a chip per key (x removes), + to record, reset per row and Reset all. Recording takes the keydown before every other key in the window (Cmd+W records instead of closing), refuses reserved combos without preventDefault (Cmd+Q still quits), requires a chord or F-key for editor commands, and offers Replace when another command in an overlapping scope owns the key. Rows flag conflicts and plain character keys that would stop starting a type-to-filter query. Fixed keys are listed read-only, in the platform's spelling. Remaps are ordinary settings (only rows that differ from the defaults), so every window, the menu hints and the tooltips follow live — and the macOS menu bar: its accelerators are built from the keymap (only the combos Electrobun's parser expresses faithfully) and rebuilt on a remap. A main-window menu action fired from the editor or Settings window brings the main window forward first. Also: the "Enter / double-click on a file" setting under File list (do nothing / open local files in their default app), arrow keys in the Settings sidebar with focus on open and a hand-walked Tab, translations for all seven languages (the spec now checks every language labels every command), and a README shortcut table generated from the keymap. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,10 +15,16 @@
|
||||
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<!-- section list -->
|
||||
<nav class="flex w-44 shrink-0 flex-col gap-0.5 border-r border-border bg-chrome p-2">
|
||||
<nav
|
||||
#nav
|
||||
(keydown)="onNavKeydown($event)"
|
||||
class="flex w-44 shrink-0 flex-col gap-0.5 border-r border-border bg-chrome p-2"
|
||||
>
|
||||
@for (s of sections; track s) {
|
||||
<button
|
||||
(click)="section.set(s)"
|
||||
[attr.data-section]="s"
|
||||
[attr.aria-current]="section() === s ? 'page' : null"
|
||||
class="px-2.5 py-1.5 text-left text-xs hover:bg-accent hover:text-foreground"
|
||||
[class.bg-accent]="section() === s"
|
||||
[class.font-medium]="section() === s"
|
||||
@@ -266,6 +272,171 @@
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Enter / double-click on a file: nothing (folders only) or open it -->
|
||||
<div class="mt-2 flex items-center justify-between gap-2">
|
||||
<span class="text-xs">{{ i18n.t('options.openFiles.label') }}</span>
|
||||
<div class="flex border border-border p-0.5">
|
||||
<button
|
||||
(click)="settings.setOpenFilesOnEnter(false)"
|
||||
class="px-2 py-0.5 text-[0.6875rem]"
|
||||
[class.bg-accent]="!settings.openFilesOnEnter()"
|
||||
[class.text-foreground]="!settings.openFilesOnEnter()"
|
||||
[class.text-muted-foreground]="settings.openFilesOnEnter()"
|
||||
>
|
||||
{{ i18n.t('options.openFiles.nothing') }}
|
||||
</button>
|
||||
<button
|
||||
(click)="settings.setOpenFilesOnEnter(true)"
|
||||
class="px-2 py-0.5 text-[0.6875rem]"
|
||||
[class.bg-accent]="settings.openFilesOnEnter()"
|
||||
[class.text-foreground]="settings.openFilesOnEnter()"
|
||||
[class.text-muted-foreground]="!settings.openFilesOnEnter()"
|
||||
>
|
||||
{{ i18n.t('options.openFiles.open') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="-mt-1 pl-3 text-[0.6875rem] text-muted-foreground">{{ i18n.t('options.openFiles.hint') }}</p>
|
||||
}
|
||||
|
||||
@case ('keyboard') {
|
||||
<!-- search + reset all -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<ng-icon
|
||||
name="tdesignSearch"
|
||||
size="0.8rem"
|
||||
class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
[value]="keyFilter()"
|
||||
(input)="keyFilter.set($any($event.target).value)"
|
||||
[placeholder]="i18n.t('keys.ui.filter')"
|
||||
spellcheck="false"
|
||||
class="h-7 w-full border border-input bg-background pr-2 pl-7 text-xs outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
@if (confirmResetAll()) {
|
||||
<button
|
||||
(click)="resetAll()"
|
||||
class="shrink-0 border border-destructive px-2 py-0.5 text-[0.6875rem] text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
{{ i18n.t('keys.ui.resetAllConfirm') }}
|
||||
</button>
|
||||
<button
|
||||
(click)="confirmResetAll.set(false)"
|
||||
class="shrink-0 px-2 py-0.5 text-[0.6875rem] text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
(click)="confirmResetAll.set(true)"
|
||||
[disabled]="!anyCustom()"
|
||||
class="shrink-0 border border-border px-2 py-0.5 text-[0.6875rem] hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
>
|
||||
{{ i18n.t('keys.ui.resetAll') }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<p class="text-[0.6875rem] text-muted-foreground">{{ i18n.t('keys.ui.intro') }}</p>
|
||||
|
||||
@for (group of keyGroups(); track group.id) {
|
||||
<h3 class="mt-1 text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{{ i18n.t('keys.groups.' + group.id) }}
|
||||
</h3>
|
||||
<div class="-mt-2 flex flex-col">
|
||||
@for (binding of group.rows; track binding.id) {
|
||||
<div class="flex min-h-7 items-center gap-1.5 border-b border-border/50 py-0.5 text-xs">
|
||||
<span class="min-w-0 flex-1 truncate" [title]="rowLabel(binding)">{{ rowLabel(binding) }}</span>
|
||||
@if (conflictWith(binding); as others) {
|
||||
<span
|
||||
class="grid size-4 shrink-0 place-items-center bg-destructive/15 text-[0.625rem] font-bold text-destructive"
|
||||
[title]="i18n.t('keys.ui.conflict', { action: others })"
|
||||
>!</span
|
||||
>
|
||||
}
|
||||
<div class="flex shrink-0 flex-wrap justify-end gap-1">
|
||||
@for (combo of binding.combos; track $index) {
|
||||
<span class="flex items-center border border-border bg-muted pl-1.5 text-[0.6875rem] tabular-nums">
|
||||
{{ shortcuts.format(combo) }}
|
||||
<button
|
||||
(click)="removeCombo(binding, $index)"
|
||||
[attr.aria-label]="i18n.t('keys.ui.remove', { key: shortcuts.format(combo) })"
|
||||
[title]="i18n.t('keys.ui.remove', { key: shortcuts.format(combo) })"
|
||||
class="grid size-4 place-items-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ng-icon name="tdesignClose" size="0.6rem" />
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
@if (capturing() === binding.id) {
|
||||
<span class="border border-primary px-1.5 text-[0.6875rem] text-primary">
|
||||
{{ i18n.t('keys.ui.press', { esc: fixedKeys().esc }) }}
|
||||
</span>
|
||||
} @else if (!binding.combos.length) {
|
||||
<span class="px-1 text-[0.6875rem] text-muted-foreground">{{ i18n.t('keys.ui.unbound') }}</span>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
data-record
|
||||
(click)="startCapture(binding.id)"
|
||||
[attr.aria-label]="i18n.t('keys.ui.add')"
|
||||
[title]="i18n.t('keys.ui.add')"
|
||||
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
[class.text-primary]="capturing() === binding.id"
|
||||
>
|
||||
<ng-icon name="tdesignAdd" size="0.75rem" />
|
||||
</button>
|
||||
<button
|
||||
(click)="resetRow(binding)"
|
||||
[disabled]="!binding.custom"
|
||||
[attr.aria-label]="i18n.t('keys.ui.reset')"
|
||||
[title]="i18n.t('keys.ui.reset')"
|
||||
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:invisible"
|
||||
>
|
||||
<ng-icon name="tdesignRefresh" size="0.7rem" />
|
||||
</button>
|
||||
</div>
|
||||
@if (note()?.id === binding.id) {
|
||||
<div class="flex items-center gap-2 py-1 pl-3 text-[0.6875rem] text-destructive">
|
||||
<span class="min-w-0 flex-1">{{ noteText(note()!) }}</span>
|
||||
@if (note()!.kind === 'conflict') {
|
||||
<button
|
||||
(click)="replaceConflict()"
|
||||
class="shrink-0 border border-border px-2 py-0.5 text-foreground hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('keys.ui.replace') }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
(click)="note.set(null)"
|
||||
class="shrink-0 px-2 py-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (filterKey(binding); as key) {
|
||||
<p class="py-0.5 pl-3 text-[0.6875rem] text-muted-foreground">
|
||||
{{ i18n.t('keys.ui.plainLetter', { key }) }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="text-xs text-muted-foreground">{{ i18n.t('keys.ui.noResults') }}</p>
|
||||
}
|
||||
|
||||
<h3 class="mt-1 text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{{ i18n.t('keys.ui.fixed') }}
|
||||
</h3>
|
||||
<ul class="-mt-1 flex flex-col gap-1 text-[0.6875rem] text-muted-foreground">
|
||||
@for (line of fixedLines; track line) {
|
||||
<li>{{ i18n.t('keys.fixed.' + line, fixedKeys()) }}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@case ('about') {
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Component, ElementRef, afterNextRender, computed, inject, signal, viewChild } from '@angular/core';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { BRANDING } from '@shared/branding';
|
||||
import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
|
||||
import {
|
||||
type Combo,
|
||||
comboFromEvent,
|
||||
conflicts,
|
||||
firesInTextField,
|
||||
GROUPS,
|
||||
isReserved,
|
||||
ownerOf,
|
||||
type ResolvedBinding,
|
||||
sameCombo,
|
||||
type ShortcutId,
|
||||
typesCharacter,
|
||||
} from '@shared/keymap';
|
||||
import { COLUMN_KEYS } from '@core/context-menu.service';
|
||||
import { cycleFocus } from '@core/focus-cycle';
|
||||
import { I18nService } from '@core/i18n.service';
|
||||
import { blockNativeContextMenu, blockReloadKeys, IS_MAC_CHROME } from '@core/platform';
|
||||
import { blockNativeContextMenu, blockReloadKeys, IS_MAC, IS_MAC_CHROME } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
|
||||
const SECTIONS = ['appearance', 'transfers', 'editor', 'fileList', 'about'] as const;
|
||||
const SECTIONS = ['appearance', 'transfers', 'editor', 'fileList', 'keyboard', 'about'] as const;
|
||||
type Section = (typeof SECTIONS)[number];
|
||||
|
||||
const THEMES = ['system', 'light', 'dark'] as const;
|
||||
@@ -21,10 +35,23 @@ const SPEED_UNITS = [
|
||||
const TRANSFER_MODES = ['single', 'multi'] as const;
|
||||
const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
|
||||
/** The keys no remap moves, listed read-only under the editable ones. */
|
||||
const FIXED_LINES = ['escape', 'escList', 'dialogs', 'menus', 'filter', 'shift', 'find', 'sidebar'] as const;
|
||||
|
||||
/** A recording that could not simply be added: explained under its row. */
|
||||
interface CaptureNote {
|
||||
id: ShortcutId;
|
||||
kind: 'reserved' | 'needsModifier' | 'conflict';
|
||||
combo: Combo;
|
||||
/** conflict: the command that already owns the combo. */
|
||||
owner?: ShortcutId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Root of the Settings window (APP_MODE 'settings'). Every control is a plain
|
||||
* SettingsService setter: the save goes to bun, which rebroadcasts it, so the
|
||||
* other windows follow live while this one stays open.
|
||||
* other windows follow live while this one stays open — keyboard remaps
|
||||
* included (menu hints, tooltips and the macOS menu bar follow at once).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -33,13 +60,15 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
host: {
|
||||
'(window:keydown)': 'onWindowKeydown($event)',
|
||||
'(window:contextmenu)': 'blockContextMenu($event)',
|
||||
'(window:mousedown)': 'onWindowMousedown($event)',
|
||||
'(window:blur)': 'capturing.set(null)',
|
||||
},
|
||||
})
|
||||
export class SettingsWindow {
|
||||
readonly settings = inject(SettingsService);
|
||||
readonly i18n = inject(I18nService);
|
||||
readonly shortcuts = inject(ShortcutsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
|
||||
readonly isMacChrome = IS_MAC_CHROME;
|
||||
readonly sections = SECTIONS;
|
||||
@@ -52,11 +81,50 @@ export class SettingsWindow {
|
||||
readonly transferModes = TRANSFER_MODES;
|
||||
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
||||
readonly columnKeys = COLUMN_KEYS;
|
||||
readonly fixedLines = FIXED_LINES;
|
||||
readonly appName = BRANDING.appName;
|
||||
readonly buildLabel = BRANDING.buildLabel;
|
||||
/** "100%" for the stepper readout; the value itself lives in settings. */
|
||||
readonly scalePercent = computed(() => `${Math.round(this.settings.uiScale() * 100)}%`);
|
||||
|
||||
private readonly nav = viewChild.required<ElementRef<HTMLElement>>('nav');
|
||||
|
||||
// ---- Settings → Keyboard ----
|
||||
readonly keyFilter = signal('');
|
||||
/** The command being recorded, if any. */
|
||||
readonly capturing = signal<ShortcutId | null>(null);
|
||||
readonly note = signal<CaptureNote | null>(null);
|
||||
readonly confirmResetAll = signal(false);
|
||||
readonly anyCustom = computed(() => this.shortcuts.keymap().some((b) => b.custom));
|
||||
private readonly conflictMap = computed(() => conflicts(this.shortcuts.keymap()));
|
||||
|
||||
/** The editable rows by group, narrowed by the search field (label or keys). */
|
||||
readonly keyGroups = computed(() => {
|
||||
const query = this.keyFilter().trim().toLowerCase();
|
||||
const keymap = this.shortcuts.keymap();
|
||||
return GROUPS.map((group) => ({
|
||||
id: group,
|
||||
rows: keymap.filter((b) => b.group === group && (!query || this.rowText(b).includes(query))),
|
||||
})).filter((group) => group.rows.length);
|
||||
});
|
||||
|
||||
/** The fixed keys, in this platform's spelling, for the read-only lines. */
|
||||
readonly fixedKeys = computed(() => ({
|
||||
esc: this.shortcuts.fixedKey('Escape'),
|
||||
enter: this.shortcuts.fixedKey('Enter'),
|
||||
shiftEnter: this.shortcuts.fixedKey('Shift+Enter'),
|
||||
tab: this.shortcuts.fixedKey('Tab'),
|
||||
shiftTab: this.shortcuts.fixedKey('Shift+Tab'),
|
||||
backspace: this.shortcuts.fixedKey('Backspace'),
|
||||
home: this.shortcuts.fixedKey('Home'),
|
||||
end: this.shortcuts.fixedKey('End'),
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
// the keyboard starts on the current section
|
||||
afterNextRender(() => this.sectionButton(this.section())?.focus());
|
||||
}
|
||||
|
||||
/** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */
|
||||
editorAppName(): string {
|
||||
const path = this.settings.editorApp();
|
||||
@@ -74,16 +142,153 @@ export class SettingsWindow {
|
||||
void this.rpc.request.showAbout({});
|
||||
}
|
||||
|
||||
/** This window's keys: close-window, and the any-window interface scale. */
|
||||
// ---- sections ----
|
||||
|
||||
/** ↑ / ↓ in the sidebar move between sections (and focus follows). */
|
||||
onNavKeydown(event: KeyboardEvent): void {
|
||||
const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
|
||||
if (!step) return;
|
||||
event.preventDefault();
|
||||
const next = SECTIONS[(SECTIONS.indexOf(this.section()) + step + SECTIONS.length) % SECTIONS.length];
|
||||
this.section.set(next);
|
||||
this.sectionButton(next)?.focus();
|
||||
}
|
||||
|
||||
private sectionButton(section: Section): HTMLElement | null {
|
||||
return this.nav().nativeElement.querySelector<HTMLElement>(`[data-section="${section}"]`);
|
||||
}
|
||||
|
||||
// ---- keyboard remaps ----
|
||||
|
||||
rowLabel(binding: ResolvedBinding): string {
|
||||
return binding.n
|
||||
? this.i18n.t('keys.go-to-tab', { n: binding.n })
|
||||
: this.i18n.t(`keys.${binding.id}`);
|
||||
}
|
||||
|
||||
private labelOf(id: ShortcutId): string {
|
||||
const binding = this.shortcuts.binding(id);
|
||||
return binding ? this.rowLabel(binding) : id;
|
||||
}
|
||||
|
||||
private rowText(binding: ResolvedBinding): string {
|
||||
return [this.rowLabel(binding), ...binding.combos.map((c) => this.shortcuts.format(c))]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/** "Rename, Edit" when this row shares a key with others that can collide. */
|
||||
conflictWith(binding: ResolvedBinding): string {
|
||||
return (this.conflictMap().get(binding.id) ?? []).map((id) => this.labelOf(id)).join(', ');
|
||||
}
|
||||
|
||||
/** A plain character key on a file-list row: typing it no longer filters. */
|
||||
filterKey(binding: ResolvedBinding): string {
|
||||
if (!binding.scopes.some((s) => s === 'panel' || s === 'main')) return '';
|
||||
const plain = binding.combos.find(typesCharacter);
|
||||
return plain ? this.shortcuts.format(plain) : '';
|
||||
}
|
||||
|
||||
noteText(note: CaptureNote): string {
|
||||
const key = this.shortcuts.format(note.combo);
|
||||
if (note.kind === 'reserved') return this.i18n.t('keys.ui.reserved', { key });
|
||||
if (note.kind === 'needsModifier') return this.i18n.t('keys.ui.needsModifier', { key });
|
||||
return this.i18n.t('keys.ui.usedBy', { key, action: note.owner ? this.labelOf(note.owner) : '' });
|
||||
}
|
||||
|
||||
startCapture(id: ShortcutId): void {
|
||||
this.note.set(null);
|
||||
this.capturing.set(this.capturing() === id ? null : id);
|
||||
}
|
||||
|
||||
/**
|
||||
* One keydown while recording, taken before every other key in this window
|
||||
* (⌘W records instead of closing). A bare modifier keeps waiting; Esc on its
|
||||
* own cancels. Reserved combos are refused WITHOUT preventDefault, so ⌘Q
|
||||
* still quits and ⌘C still copies.
|
||||
*/
|
||||
private record(event: KeyboardEvent, id: ShortcutId): void {
|
||||
const combo = comboFromEvent(event, IS_MAC);
|
||||
if (combo && isReserved(combo, IS_MAC)) {
|
||||
this.capturing.set(null);
|
||||
this.note.set({ id, kind: 'reserved', combo });
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.repeat || !combo) return;
|
||||
const bare = !combo.mod && !combo.ctrl && !combo.alt && !combo.shift;
|
||||
this.capturing.set(null);
|
||||
if (bare && combo.key === 'Escape') return;
|
||||
const binding = this.shortcuts.binding(id);
|
||||
if (!binding || binding.combos.some((c) => sameCombo(c, combo))) return;
|
||||
// the editor's text area always has focus: a plain key could never fire there
|
||||
if (binding.scopes.includes('editor') && !firesInTextField(combo, IS_MAC)) {
|
||||
this.note.set({ id, kind: 'needsModifier', combo });
|
||||
return;
|
||||
}
|
||||
const owner = ownerOf(this.shortcuts.keymap(), combo, binding.scopes, id);
|
||||
if (owner) {
|
||||
this.note.set({ id, kind: 'conflict', combo, owner: owner.id });
|
||||
return;
|
||||
}
|
||||
this.shortcuts.rebind({ [id]: [...binding.combos, combo] });
|
||||
}
|
||||
|
||||
/** "Replace": the key moves from the command that had it to this one. */
|
||||
replaceConflict(): void {
|
||||
const note = this.note();
|
||||
const binding = note && this.shortcuts.binding(note.id);
|
||||
const owner = note?.owner && this.shortcuts.binding(note.owner);
|
||||
if (!note || !binding || !owner) return;
|
||||
this.shortcuts.rebind({
|
||||
[note.id]: [...binding.combos, note.combo],
|
||||
[owner.id]: owner.combos.filter((c) => !sameCombo(c, note.combo)),
|
||||
});
|
||||
this.note.set(null);
|
||||
}
|
||||
|
||||
removeCombo(binding: ResolvedBinding, index: number): void {
|
||||
this.shortcuts.rebind({ [binding.id]: binding.combos.filter((_, i) => i !== index) });
|
||||
}
|
||||
|
||||
resetRow(binding: ResolvedBinding): void {
|
||||
this.shortcuts.rebind({ [binding.id]: binding.defaults });
|
||||
if (this.note()?.id === binding.id) this.note.set(null);
|
||||
}
|
||||
|
||||
resetAll(): void {
|
||||
this.shortcuts.resetAll();
|
||||
this.confirmResetAll.set(false);
|
||||
this.note.set(null);
|
||||
}
|
||||
|
||||
// ---- window ----
|
||||
|
||||
/** Recording first; then this window's keys (close-window, the scale) and
|
||||
* Tab, walked by hand — WKWebView skips buttons on Tab otherwise. */
|
||||
onWindowKeydown(event: KeyboardEvent): void {
|
||||
const recording = this.capturing();
|
||||
if (recording) {
|
||||
this.record(event, recording);
|
||||
return;
|
||||
}
|
||||
const id = this.shortcuts.claim(event, 'settings');
|
||||
if (id === 'close-window') void this.rpc.request.settingsClose({});
|
||||
else if (id === 'scale-up') this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
else if (id === 'scale-down') this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
else if (id === 'scale-reset') this.settings.setUiScale(1);
|
||||
else if (!id) cycleFocus(document.body, event);
|
||||
blockReloadKeys(event);
|
||||
}
|
||||
|
||||
/** A click anywhere but a record button ends a recording. */
|
||||
onWindowMousedown(event: MouseEvent): void {
|
||||
if (this.capturing() && !(event.target as Element).closest('[data-record]')) {
|
||||
this.capturing.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
blockContextMenu(event: Event): void {
|
||||
blockNativeContextMenu(event);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user