diff --git a/tabby-core/src/api/hotkeyProvider.ts b/tabby-core/src/api/hotkeyProvider.ts index 2f6e2ecf..20f04d12 100644 --- a/tabby-core/src/api/hotkeyProvider.ts +++ b/tabby-core/src/api/hotkeyProvider.ts @@ -3,6 +3,11 @@ export interface HotkeyDescription { name: string } +export interface Hotkey { + strokes: string[] | string; // may be a sequence of strokes + isDuplicate: boolean; +} + /** * Extend to provide your own hotkeys. A corresponding [[ConfigProvider]] * must also provide the `hotkeys.foo` config options with the default values diff --git a/tabby-core/src/theme.paper.scss b/tabby-core/src/theme.paper.scss index f83f23da..0b088922 100644 --- a/tabby-core/src/theme.paper.scss +++ b/tabby-core/src/theme.paper.scss @@ -256,6 +256,11 @@ multi-hotkey-input { } } + .item:has(.duplicate) { + background-color: theme-color('danger'); + border: 1px solid theme-color('danger'); + } + .add { color: #777; padding: 4px 10px 0; @@ -265,6 +270,11 @@ multi-hotkey-input { &:hover { background: darken($body-bg2, 5%); } &:active { background: darken($body-bg2, 15%); } } + + .add:has(.duplicate), .item:has(.duplicate) .body, .item:has(.duplicate) .remove { + &:hover { background: darken(theme-color('danger'), 5%); } + &:active { background: darken(theme-color('danger'), 15%); } + } } hotkey-input-modal { diff --git a/tabby-core/src/theme.scss b/tabby-core/src/theme.scss index 38b96259..320eb3bc 100644 --- a/tabby-core/src/theme.scss +++ b/tabby-core/src/theme.scss @@ -162,6 +162,11 @@ multi-hotkey-input { } } + .item:has(.duplicate) { + background-color: theme-color('danger'); + border: 1px solid theme-color('danger'); + } + .add { color: #777; padding: 4px 10px 0; @@ -171,6 +176,11 @@ multi-hotkey-input { &:hover { background: darken($body-bg2, 5%); } &:active { background: darken($body-bg2, 15%); } } + + .add:has(.duplicate), .item:has(.duplicate) .body, .item:has(.duplicate) .remove { + &:hover { background: darken(theme-color('danger'), 5%); } + &:active { background: darken(theme-color('danger'), 15%); } + } } hotkey-input-modal { diff --git a/tabby-settings/src/components/hotkeySettingsTab.component.pug b/tabby-settings/src/components/hotkeySettingsTab.component.pug index 86dfb675..20826acc 100644 --- a/tabby-settings/src/components/hotkeySettingsTab.component.pug +++ b/tabby-settings/src/components/hotkeySettingsTab.component.pug @@ -14,6 +14,6 @@ h3.mb-3(translate) Hotkeys span.ml-2.text-muted ({{hotkey.id}}) .col-4.pr-5 multi-hotkey-input( - [model]='getHotkey(hotkey.id) || []', - (modelChange)='setHotkey(hotkey.id, $event)' + [hotkeys]='getHotkeys(hotkey.id) || []', + (hotkeysChange)='setHotkeys(hotkey.id, $event)' ) diff --git a/tabby-settings/src/components/hotkeySettingsTab.component.ts b/tabby-settings/src/components/hotkeySettingsTab.component.ts index 74b04e98..307c37f1 100644 --- a/tabby-settings/src/components/hotkeySettingsTab.component.ts +++ b/tabby-settings/src/components/hotkeySettingsTab.component.ts @@ -7,6 +7,7 @@ import { HotkeysService, HostAppService, } from 'tabby-core' +import { Hotkey } from 'tabby-core/src/api/hotkeyProvider' _('Search hotkeys') @@ -30,28 +31,44 @@ export class HotkeySettingsTabComponent { }) } - getHotkey (id: string) { + getHotkeys (id: string): Hotkey[] { let ptr = this.config.store.hotkeys for (const token of id.split(/\./g)) { ptr = ptr[token] } - return ptr + return (ptr || []).map(hotkey => this.detectDuplicates(hotkey)) } - setHotkey (id: string, value) { + setHotkeys (id: string, hotkeys: Hotkey[]) { let ptr = this.config.store let prop = 'hotkeys' for (const token of id.split(/\./g)) { ptr = ptr[prop] prop = token } - ptr[prop] = value + ptr[prop] = hotkeys.map(hotkey => + hotkey.strokes.length === 1 && Array.isArray(hotkey.strokes) + ? hotkey.strokes[0] + : hotkey.strokes, + ) this.config.save() } hotkeyFilterFn (hotkey: HotkeyDescription, query: string): boolean { - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - const s = hotkey.name + hotkey.id + (this.getHotkey(hotkey.id) || []).toString() + const s = hotkey.name + hotkey.id + this.getHotkeys(hotkey.id).map(h => h.strokes).toString() return s.toLowerCase().includes(query.toLowerCase()) } + + private detectDuplicates (strokes: string[] | string): Hotkey { + const allHotkeys = Object + .values(this.config.store.hotkeys) + .filter((value: unknown) => Array.isArray(value)) + .flat() + + const isDuplicate = allHotkeys + .filter(hotkey => JSON.stringify(hotkey) === JSON.stringify(strokes)) + .length > 1 + + return { strokes, isDuplicate } + } } diff --git a/tabby-settings/src/components/multiHotkeyInput.component.pug b/tabby-settings/src/components/multiHotkeyInput.component.pug index 9591f207..2d8b2e36 100644 --- a/tabby-settings/src/components/multiHotkeyInput.component.pug +++ b/tabby-settings/src/components/multiHotkeyInput.component.pug @@ -1,6 +1,8 @@ -.item(*ngFor='let item of model') - .body((click)='editItem(item)') - .stroke(*ngFor='let stroke of item') {{stroke}} - .remove((click)='removeItem(item)') × +.item(*ngFor='let hotkey of hotkeys') + .body((click)='editItem(hotkey)') + .stroke(*ngFor='let stroke of hotkey.strokes') + span(*ngIf='!hotkey.isDuplicate', translate) {{stroke}} + span.duplicate(*ngIf='hotkey.isDuplicate', translate) {{stroke}} + .remove((click)='removeItem(hotkey)') × .add((click)='addItem()', translate) Add... diff --git a/tabby-settings/src/components/multiHotkeyInput.component.ts b/tabby-settings/src/components/multiHotkeyInput.component.ts index ece80e6c..4bd3af12 100644 --- a/tabby-settings/src/components/multiHotkeyInput.component.ts +++ b/tabby-settings/src/components/multiHotkeyInput.component.ts @@ -1,6 +1,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { HotkeyInputModalComponent } from './hotkeyInputModal.component' +import { Hotkey } from 'tabby-core/src/api/hotkeyProvider' /** @hidden */ @Component({ @@ -10,37 +11,41 @@ import { HotkeyInputModalComponent } from './hotkeyInputModal.component' changeDetection: ChangeDetectionStrategy.OnPush, }) export class MultiHotkeyInputComponent { - @Input() model: string[][] = [] - @Output() modelChange = new EventEmitter() + @Input() hotkeys: Hotkey[] = [] + @Output() hotkeysChange = new EventEmitter() constructor ( private ngbModal: NgbModal, ) { } ngOnChanges (): void { - if (typeof this.model === 'string') { - this.model = [this.model] - } - this.model = this.model.map(item => typeof item === 'string' ? [item] : item) + this.hotkeys = this.hotkeys.map(hotkey => typeof hotkey.strokes === 'string' ? { ...hotkey, strokes: [hotkey.strokes] } : hotkey) } - editItem (item: string[]): void { - this.ngbModal.open(HotkeyInputModalComponent).result.then((value: string[]) => { - this.model[this.model.findIndex(x => x === item)] = value - this.model = this.model.slice() - this.modelChange.emit(this.model) + editItem (item: Hotkey): void { + this.ngbModal.open(HotkeyInputModalComponent).result.then((newStrokes: string[]) => { + this.hotkeys.find(hotkey => this.isEqual(hotkey, item))!.strokes = newStrokes + this.storeUpdatedHotkeys() }) } addItem (): void { this.ngbModal.open(HotkeyInputModalComponent).result.then((value: string[]) => { - this.model = this.model.concat([value]) - this.modelChange.emit(this.model) + this.hotkeys.push({ strokes: value, isDuplicate: false }) + this.storeUpdatedHotkeys() }) } - removeItem (item: string[]): void { - this.model = this.model.filter(x => x !== item) - this.modelChange.emit(this.model) + removeItem (item: Hotkey): void { + this.hotkeys = this.hotkeys.filter(x => x !== item) + this.storeUpdatedHotkeys() + } + + private storeUpdatedHotkeys () { + this.hotkeysChange.emit(this.hotkeys) + } + + private isEqual (h: Hotkey, item: Hotkey) { + return JSON.stringify(h.strokes) === JSON.stringify(item.strokes) } }