From 2acc4f77d4208c3dfdadffb7133b59e1c5c071fb Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Fri, 30 Dec 2016 23:37:41 +0300 Subject: [PATCH] . --- app/src/app.module.ts | 4 + app/src/components/app.less | 7 ++ app/src/components/app.pug | 2 + app/src/components/app.ts | 25 +++--- app/src/components/hotkeyDisplay.less | 24 ++++++ app/src/components/hotkeyDisplay.pug | 7 ++ app/src/components/hotkeyDisplay.ts | 37 +++++++++ app/src/components/hotkeyHint.less | 8 ++ app/src/components/hotkeyHint.pug | 4 + app/src/components/hotkeyHint.ts | 59 ++++++++++++++ app/src/components/hotkeyInput.less | 33 +------- app/src/components/hotkeyInput.pug | 4 - app/src/components/hotkeyInput.ts | 4 +- app/src/components/hotkeyInputModal.less | 30 +------- app/src/components/hotkeyInputModal.pug | 7 +- app/src/components/hotkeyInputModal.ts | 1 + app/src/services/config.ts | 58 ++------------ app/src/services/hotkeys.ts | 98 ++++++++++++++++++++++++ 18 files changed, 284 insertions(+), 128 deletions(-) create mode 100644 app/src/components/hotkeyDisplay.less create mode 100644 app/src/components/hotkeyDisplay.pug create mode 100644 app/src/components/hotkeyDisplay.ts create mode 100644 app/src/components/hotkeyHint.less create mode 100644 app/src/components/hotkeyHint.pug create mode 100644 app/src/components/hotkeyHint.ts delete mode 100644 app/src/components/hotkeyInput.pug diff --git a/app/src/app.module.ts b/app/src/app.module.ts index 68cb51f0..de921018 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -19,6 +19,8 @@ import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter' import { AppComponent } from 'components/app' import { CheckboxComponent } from 'components/checkbox' import { HotkeyInputComponent } from 'components/hotkeyInput' +import { HotkeyDisplayComponent } from 'components/hotkeyDisplay' +import { HotkeyHintComponent } from 'components/hotkeyHint' import { HotkeyInputModalComponent } from 'components/hotkeyInputModal' import { SettingsModalComponent } from 'components/settingsModal' import { TerminalComponent } from 'components/terminal' @@ -51,6 +53,8 @@ import { TerminalComponent } from 'components/terminal' declarations: [ AppComponent, CheckboxComponent, + HotkeyDisplayComponent, + HotkeyHintComponent, HotkeyInputComponent, HotkeyInputModalComponent, SettingsModalComponent, diff --git a/app/src/components/app.less b/app/src/components/app.less index 015a70da..fefacf17 100644 --- a/app/src/components/app.less +++ b/app/src/components/app.less @@ -189,3 +189,10 @@ } } } + +hotkey-hint { + position: absolute; + bottom: 0; + right: 0; + max-width: 300px; +} diff --git a/app/src/components/app.pug b/app/src/components/app.pug index 978c6b76..0f3735e2 100644 --- a/app/src/components/app.pug +++ b/app/src/components/app.pug @@ -29,6 +29,8 @@ .tab(*ngFor='let tab of tabs; trackBy: tab?.id', [class.active]='tab == activeTab') terminal([session]='tab.session', '[(title)]'='tab.name') +hotkey-hint + toaster-container([toasterconfig]="toasterconfig") template(ngbModalContainer) diff --git a/app/src/components/app.ts b/app/src/components/app.ts index dd1b4290..8022c3f3 100644 --- a/app/src/components/app.ts +++ b/app/src/components/app.ts @@ -49,6 +49,10 @@ class Tab { ] }) export class AppComponent { + toasterConfig: ToasterConfig + tabs: Tab[] = [] + activeTab: Tab + constructor( private modal: ModalService, private elementRef: ElementRef, @@ -70,6 +74,17 @@ export class AppComponent { timeout: 4000, }) + this.hotkeys.matchedHotkey.subscribe((hotkey) => { + if (hotkey == 'new-tab') { + this.newTab() + } + if (hotkey == 'close-tab') { + if (this.activeTab) { + this.closeTab(this.activeTab) + } + } + }) + this.hotkeys.key.subscribe((key) => { if (key.event == 'keydown') { if (key.alt && key.key >= '1' && key.key <= '9') { @@ -83,12 +98,6 @@ export class AppComponent { this.selectTab(this.tabs[9]) } } - if (key.ctrl && key.shift && key.key == 'W' && this.activeTab) { - this.closeTab(this.activeTab) - } - if (key.ctrl && key.shift && key.key == 'T' && this.activeTab) { - this.newTab() - } } }) @@ -98,10 +107,6 @@ export class AppComponent { }) } - toasterConfig: ToasterConfig - tabs: Tab[] = [] - activeTab: Tab - newTab () { this.addSessionTab(this.sessions.createNewSession({command: 'bash'})) } diff --git a/app/src/components/hotkeyDisplay.less b/app/src/components/hotkeyDisplay.less new file mode 100644 index 00000000..ff8563ac --- /dev/null +++ b/app/src/components/hotkeyDisplay.less @@ -0,0 +1,24 @@ +:host { + display: inline-block; + + .stroke { + display: inline-block; + margin: 0 5px; + + .key-container { + display: inline-block; + background: #222; + text-shadow: 0 1px 0 rgba(0,0,0,.5); + + .key { + display: inline-block; + padding: 4px 5px; + } + + .plus { + display: inline-block; + padding: 4px 2px; + } + } + } +} diff --git a/app/src/components/hotkeyDisplay.pug b/app/src/components/hotkeyDisplay.pug new file mode 100644 index 00000000..ac511e99 --- /dev/null +++ b/app/src/components/hotkeyDisplay.pug @@ -0,0 +1,7 @@ +.stroke(*ngFor='let stroke of model') + .key-container( + *ngFor='let key of splitKeys(stroke); let isLast = last; trackBy: key', + @animateKey + ) + .key {{key}} + .plus(*ngIf='!isLast') + diff --git a/app/src/components/hotkeyDisplay.ts b/app/src/components/hotkeyDisplay.ts new file mode 100644 index 00000000..aadc6c4e --- /dev/null +++ b/app/src/components/hotkeyDisplay.ts @@ -0,0 +1,37 @@ +import { Component, Input, ChangeDetectionStrategy, trigger, style, animate, transition, state } from '@angular/core' + + +@Component({ + selector: 'hotkey-display', + template: require('./hotkeyDisplay.pug'), + styles: [require('./hotkeyDisplay.less')], + //changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('animateKey', [ + state('in', style({ + 'transform': 'translateX(0)', + 'opacity': '1', + })), + transition(':enter', [ + style({ + 'transform': 'translateX(25px)', + 'opacity': '0', + }), + animate('250ms ease-out') + ]), + transition(':leave', [ + animate('250ms ease-in', style({ + 'transform': 'translateX(25px)', + 'opacity': '0', + })) + ]) + ]) + ] +}) +export class HotkeyDisplayComponent { + splitKeys(keys: string): string[] { + return keys.split('+').map((x) => x.trim()) + } + + @Input() model: string[] +} diff --git a/app/src/components/hotkeyHint.less b/app/src/components/hotkeyHint.less new file mode 100644 index 00000000..67bdc457 --- /dev/null +++ b/app/src/components/hotkeyHint.less @@ -0,0 +1,8 @@ +:host { + display: block; + + .line { + background: #333; + padding: 3px 10px; + } +} diff --git a/app/src/components/hotkeyHint.pug b/app/src/components/hotkeyHint.pug new file mode 100644 index 00000000..a7c8908d --- /dev/null +++ b/app/src/components/hotkeyHint.pug @@ -0,0 +1,4 @@ +.body(*ngIf='partialHotkeyMatches?.length > 0') + .line(*ngFor='let match of partialHotkeyMatches; trackBy: match?.id', @animateLine) + hotkey-display([model]='match.strokes') + span {{ hotkeys.getHotkeyDescription(match.id).name }} diff --git a/app/src/components/hotkeyHint.ts b/app/src/components/hotkeyHint.ts new file mode 100644 index 00000000..3e1797f0 --- /dev/null +++ b/app/src/components/hotkeyHint.ts @@ -0,0 +1,59 @@ +import { Component, ChangeDetectionStrategy, trigger, style, animate, transition, state } from '@angular/core' +import { HotkeysService, PartialHotkeyMatch } from 'services/hotkeys' + + +@Component({ + selector: 'hotkey-hint', + template: require('./hotkeyHint.pug'), + styles: [require('./hotkeyHint.less')], + //changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('animateLine', [ + state('in', style({ + 'transform': 'translateX(0)', + 'opacity': '1', + })), + transition(':enter', [ + style({ + 'transform': 'translateX(25px)', + 'opacity': '0', + }), + animate('250ms ease-out') + ]), + transition(':leave', [ + style({'height': '*'}), + animate('250ms ease-in', style({ + 'transform': 'translateX(25px)', + 'opacity': '0', + 'height': '0', + })) + ]) + ]) + ] +}) +export class HotkeyHintComponent { + partialHotkeyMatches: PartialHotkeyMatch[] + private keyTimeoutInterval: NodeJS.Timer = null + + constructor ( + public hotkeys: HotkeysService, + ) { + this.hotkeys.key.subscribe(() => { + let partialMatches = this.hotkeys.getCurrentPartiallyMatchedHotkeys() + if (partialMatches.length > 0) { + console.log('Partial matches:', partialMatches) + this.partialHotkeyMatches = partialMatches + + if (this.keyTimeoutInterval == null) { + this.keyTimeoutInterval = setInterval(() => { + if (this.hotkeys.getCurrentPartiallyMatchedHotkeys().length == 0) { + clearInterval(this.keyTimeoutInterval) + this.keyTimeoutInterval = null + this.partialHotkeyMatches = null + } + }, 500) + } + } + }) + } +} diff --git a/app/src/components/hotkeyInput.less b/app/src/components/hotkeyInput.less index ce5177d1..5009adde 100644 --- a/app/src/components/hotkeyInput.less +++ b/app/src/components/hotkeyInput.less @@ -1,4 +1,7 @@ -.button-states() { +:host { + display: inline-block; + padding: 5px 10px; + transition: 0.125s all; &:hover:not(.active) { @@ -9,31 +12,3 @@ background: rgba(0, 0, 0, .1); } } - -:host { - display: inline-block; - padding: 5px 10px; - - .stroke { - display: inline-block; - margin-right: 5px; - - .key-container { - display: inline-block; - - .key { - display: inline-block; - padding: 4px 5px; - background: #333; - text-shadow: 0 1px 0 rgba(0,0,0,.5); - } - - .plus { - display: inline-block; - margin: 0 5px; - } - } - } - - .button-states(); -} diff --git a/app/src/components/hotkeyInput.pug b/app/src/components/hotkeyInput.pug deleted file mode 100644 index b0549382..00000000 --- a/app/src/components/hotkeyInput.pug +++ /dev/null @@ -1,4 +0,0 @@ -.stroke(*ngFor='let stroke of model') - .key-container(*ngFor='let key of splitKeys(stroke); let isLast = last') - .key {{key}} - .plus(*ngIf='!isLast') + diff --git a/app/src/components/hotkeyInput.ts b/app/src/components/hotkeyInput.ts index 1953d0de..6c366b9f 100644 --- a/app/src/components/hotkeyInput.ts +++ b/app/src/components/hotkeyInput.ts @@ -5,7 +5,9 @@ import { HotkeyInputModalComponent } from './hotkeyInputModal' @Component({ selector: 'hotkey-input', - template: require('./hotkeyInput.pug'), + template: ` + + `, styles: [require('./hotkeyInput.less')], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/app/src/components/hotkeyInputModal.less b/app/src/components/hotkeyInputModal.less index 833d869c..85c3710b 100644 --- a/app/src/components/hotkeyInputModal.less +++ b/app/src/components/hotkeyInputModal.less @@ -3,42 +3,20 @@ padding: 30px 20px !important; } - .stroke { - display: inline-block; - margin: 8px 5px 0 0; - - .key-container { - display: inline-block; - - .key { - display: inline-block; - padding: 4px 5px; - background: #333; - text-shadow: 0 1px 0 rgba(0,0,0,.5); - } - - .plus { - display: inline-block; - margin: 0 5px; - } - } - } - .input { background: #111; - text-align: center; font-size: 24px; line-height: 24px; height: 50px; } .timeout { - background: #333; - height: 10px; - margin: 15px 0; + background: #111; + height: 5px; + margin: 0 0 15px; div { - height: 10px; + height: 5px; background: #666; } } diff --git a/app/src/components/hotkeyInputModal.pug b/app/src/components/hotkeyInputModal.pug index bb8411d4..88c0ef1a 100644 --- a/app/src/components/hotkeyInputModal.pug +++ b/app/src/components/hotkeyInputModal.pug @@ -1,11 +1,8 @@ div.modal-body label Press the key now .input - .stroke(*ngFor='let stroke of value') - .key-container(*ngFor='let key of splitKeys(stroke); let isLast = last') - .key {{key}} - .plus(*ngIf='!isLast') + + hotkey-display([model]='value') .timeout div([style.width]='timeoutProgress + "%"') - a.btn.btn-default((click)='close()') Cancel + a.btn.btn-default.pull-right((click)='close()') Cancel diff --git a/app/src/components/hotkeyInputModal.ts b/app/src/components/hotkeyInputModal.ts index 7cc3b8f2..21e75081 100644 --- a/app/src/components/hotkeyInputModal.ts +++ b/app/src/components/hotkeyInputModal.ts @@ -23,6 +23,7 @@ export class HotkeyInputModalComponent { private modalInstance: NgbActiveModal, public hotkeys: HotkeysService, ) { + this.hotkeys.clearCurrentKeystrokes() this.keySubscription = hotkeys.key.subscribe(() => { this.lastKeyEvent = performance.now() this.value = this.hotkeys.getCurrentKeystrokes() diff --git a/app/src/services/config.ts b/app/src/services/config.ts index b5a602f6..64e0fdaf 100644 --- a/app/src/services/config.ts +++ b/app/src/services/config.ts @@ -1,15 +1,10 @@ import { Injectable } from '@angular/core' -import { HostAppService, PLATFORM_MAC, PLATFORM_WINDOWS } from 'services/hostApp' const Config = nodeRequire('electron-config') -const exec = nodeRequire('child-process-promise').exec -import * as fs from 'fs' @Injectable() export class ConfigService { - constructor( - private hostApp: HostAppService, - ) { + constructor() { this.config = new Config({name: 'config'}) this.load() } @@ -17,65 +12,22 @@ export class ConfigService { private config: any private store: any - migrate() { - if (!this.has('migrated')) { - if (this.hostApp.platform == PLATFORM_WINDOWS) { - let configPath = `${this.hostApp.getPath('documents')}\\.elements.conf` - let config = null - try { - config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) - console.log('Migrating configuration:', config) - this.set('host', config.Hostname) - this.set('username', config.Username) - this.set('firstDrive', config.FirstDrive) - } catch (err) { - console.error('Could not migrate the config:', err) - } - this.set('migrated', 1) - this.save() - return Promise.resolve() - } - if (this.hostApp.platform == PLATFORM_MAC) { - return Promise.all([ - exec('defaults read ~/Library/Preferences/com.syslink.Elements.plist connection_host').then((result) => { - this.set('host', result.stdout.trim()) - }), - exec('defaults read ~/Library/Preferences/com.syslink.Elements.plist connection_username').then((result) => { - this.set('username', result.stdout.trim()) - }), - ]).then(() => { - this.set('migrated', 1) - this.save() - }).catch((err) => { - console.error('Could not migrate the config:', err) - this.set('migrated', 1) - this.save() - }) - } - } - return Promise.resolve() - } - set(key: string, value: any) { + this.store.set(key, value) this.save() - this.config.set(key, value) - this.load() } get(key: string): any { - this.save() - return this.config.get(key) + return this.store[key] } has(key: string): boolean { - this.save() - return this.config.has(key) + return this.store[key] != undefined } delete(key: string) { + delete this.store[key] this.save() - this.config.delete(key) - this.load() } load() { diff --git a/app/src/services/hotkeys.ts b/app/src/services/hotkeys.ts index 48038172..7034c46d 100644 --- a/app/src/services/hotkeys.ts +++ b/app/src/services/hotkeys.ts @@ -1,9 +1,35 @@ import { Injectable, NgZone, EventEmitter } from '@angular/core' import { ElectronService } from 'services/electron' +import { ConfigService } from 'services/config' import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util' const hterm = require('hterm-commonjs') +export interface HotkeyDescription { + id: string, + name: string, + defaults: string[][], +} + +export interface PartialHotkeyMatch { + id: string, + strokes: string[], + matchedLength: number, +} + const KEY_TIMEOUT = 2000 +const HOTKEYS: HotkeyDescription[] = [ + { + id: 'new-tab', + name: 'New tab', + defaults: [['Ctrl+Shift+T'], ['Ctrl+A', 'C']], + }, + { + id: 'close-tab', + name: 'Close tab', + defaults: [['Ctrl+Shift+W'], ['Ctrl+A', 'K']], + }, +] + interface EventBufferEntry { event: NativeKeyEvent, @@ -13,12 +39,14 @@ interface EventBufferEntry { @Injectable() export class HotkeysService { key = new EventEmitter() + matchedHotkey = new EventEmitter() globalHotkey = new EventEmitter() private currentKeystrokes: EventBufferEntry[] = [] constructor( private zone: NgZone, private electron: ElectronService, + private config: ConfigService, ) { let events = [ { @@ -42,6 +70,10 @@ export class HotkeysService { oldHandler.bind(this)(nativeEvent) } }) + + if (!config.get('hotkeys')) { + config.set('hotkeys', {}) + } } emitNativeEvent (name, nativeEvent) { @@ -51,10 +83,20 @@ export class HotkeysService { this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() }) this.zone.run(() => { + let matched = this.getCurrentFullyMatchedHotkey() + if (matched) { + console.log('Matched hotkey', matched) + this.matchedHotkey.emit(matched) + this.clearCurrentKeystrokes() + } this.key.emit(nativeEvent) }) } + clearCurrentKeystrokes () { + this.currentKeystrokes = [] + } + getCurrentKeystrokes () : string[] { this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT ) return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event)) @@ -66,4 +108,60 @@ export class HotkeysService { this.globalHotkey.emit() }) } + + getHotkeysConfig () { + let keys = {} + for (let key of HOTKEYS) { + keys[key.id] = key.defaults + } + for (let key in this.config.get('hotkeys')) { + keys[key] = this.config.get('hotkeys')[key] + } + return keys + } + + getCurrentFullyMatchedHotkey () : string { + for (let id in this.getHotkeysConfig()) { + for (let sequence of this.getHotkeysConfig()[id]) { + let currentStrokes = this.getCurrentKeystrokes() + if (currentStrokes.length < sequence.length) { + break + } + if (sequence.every((x, index) => { + return x.toLowerCase() == currentStrokes[currentStrokes.length - sequence.length + index].toLowerCase() + })) { + return id + } + } + } + return null + } + + getCurrentPartiallyMatchedHotkeys () : PartialHotkeyMatch[] { + let result = [] + for (let id in this.getHotkeysConfig()) { + for (let sequence of this.getHotkeysConfig()[id]) { + let currentStrokes = this.getCurrentKeystrokes() + + for (let matchLength = Math.min(currentStrokes.length, sequence.length); matchLength > 0; matchLength--) { + console.log(sequence, currentStrokes.slice(currentStrokes.length - sequence.length)) + if (sequence.slice(0, matchLength).every((x, index) => { + return x.toLowerCase() == currentStrokes[currentStrokes.length - matchLength + index].toLowerCase() + })) { + result.push({ + matchedLength: matchLength, + id, + strokes: sequence + }) + break + } + } + } + } + return result + } + + getHotkeyDescription (id: string) : HotkeyDescription { + return HOTKEYS.filter((x) => x.id == id)[0] + } }