From 13ec887d6630bee5306ac3d7240c9aa63e9f3f09 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Wed, 28 Dec 2016 17:36:07 +0100 Subject: [PATCH] . --- app/src/app.module.ts | 5 ++ app/src/components/app.less | 4 +- app/src/components/app.pug | 2 + app/src/components/hotkeyInput.less | 39 ++++++++++ app/src/components/hotkeyInput.pug | 4 ++ app/src/components/hotkeyInput.ts | 30 ++++++++ app/src/components/hotkeyInputModal.less | 45 ++++++++++++ app/src/components/hotkeyInputModal.pug | 11 +++ app/src/components/hotkeyInputModal.ts | 55 ++++++++++++++ app/src/components/settingsModal.less | 64 ----------------- app/src/components/settingsModal.pug | 91 ++---------------------- app/src/components/settingsModal.ts | 2 + app/src/services/hotkeys.ts | 38 +++++----- app/src/services/hotkeys.util.ts | 55 ++++++++++++++ 14 files changed, 275 insertions(+), 170 deletions(-) create mode 100644 app/src/components/hotkeyInput.less create mode 100644 app/src/components/hotkeyInput.pug create mode 100644 app/src/components/hotkeyInput.ts create mode 100644 app/src/components/hotkeyInputModal.less create mode 100644 app/src/components/hotkeyInputModal.pug create mode 100644 app/src/components/hotkeyInputModal.ts create mode 100644 app/src/services/hotkeys.util.ts diff --git a/app/src/app.module.ts b/app/src/app.module.ts index 88a51dbf..68cb51f0 100644 --- a/app/src/app.module.ts +++ b/app/src/app.module.ts @@ -18,6 +18,8 @@ import { LocalStorageService } from 'angular2-localstorage/LocalStorageEmitter' import { AppComponent } from 'components/app' import { CheckboxComponent } from 'components/checkbox' +import { HotkeyInputComponent } from 'components/hotkeyInput' +import { HotkeyInputModalComponent } from 'components/hotkeyInputModal' import { SettingsModalComponent } from 'components/settingsModal' import { TerminalComponent } from 'components/terminal' @@ -43,11 +45,14 @@ import { TerminalComponent } from 'components/terminal' LocalStorageService, ], entryComponents: [ + HotkeyInputModalComponent, SettingsModalComponent, ], declarations: [ AppComponent, CheckboxComponent, + HotkeyInputComponent, + HotkeyInputModalComponent, SettingsModalComponent, TerminalComponent, ], diff --git a/app/src/components/app.less b/app/src/components/app.less index 28f7a665..015a70da 100644 --- a/app/src/components/app.less +++ b/app/src/components/app.less @@ -66,12 +66,12 @@ display: flex; flex-direction: row; - .btn-new-tab, .tab { + .btn-settings, .btn-new-tab, .tab { line-height: @tabs-height - 2px; cursor: pointer; } - .btn-new-tab { + .btn-new-tab, .btn-settings { padding: 0 15px; flex: none; flex-grow: 0; diff --git a/app/src/components/app.pug b/app/src/components/app.pug index c05e3710..978c6b76 100644 --- a/app/src/components/app.pug +++ b/app/src/components/app.pug @@ -22,6 +22,8 @@ .btn-new-tab((click)='newTab()') i.fa.fa-plus span Tab + .btn-settings((click)='showSettings()') + i.fa.fa-cog .tabs-content .tab(*ngFor='let tab of tabs; trackBy: tab?.id', [class.active]='tab == activeTab') diff --git a/app/src/components/hotkeyInput.less b/app/src/components/hotkeyInput.less new file mode 100644 index 00000000..ce5177d1 --- /dev/null +++ b/app/src/components/hotkeyInput.less @@ -0,0 +1,39 @@ +.button-states() { + transition: 0.125s all; + + &:hover:not(.active) { + background: rgba(255, 255, 255, .033); + } + + &:active:not(.active) { + 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 new file mode 100644 index 00000000..b0549382 --- /dev/null +++ b/app/src/components/hotkeyInput.pug @@ -0,0 +1,4 @@ +.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 new file mode 100644 index 00000000..1953d0de --- /dev/null +++ b/app/src/components/hotkeyInput.ts @@ -0,0 +1,30 @@ +import { Component, Input, Output, EventEmitter, HostListener, ChangeDetectionStrategy } from '@angular/core' +import { ModalService } from 'services/modal' +import { HotkeyInputModalComponent } from './hotkeyInputModal' + + +@Component({ + selector: 'hotkey-input', + template: require('./hotkeyInput.pug'), + styles: [require('./hotkeyInput.less')], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HotkeyInputComponent { + constructor( + private modal: ModalService, + ) { } + + @HostListener('click') public click() { + this.modal.open(HotkeyInputModalComponent).result.then((value: string[]) => { + this.model = value + this.modelChange.emit(this.model) + }) + } + + splitKeys(keys: string): string[] { + return keys.split('+').map((x) => x.trim()) + } + + @Input() model: string[] + @Output() modelChange = new EventEmitter() +} diff --git a/app/src/components/hotkeyInputModal.less b/app/src/components/hotkeyInputModal.less new file mode 100644 index 00000000..833d869c --- /dev/null +++ b/app/src/components/hotkeyInputModal.less @@ -0,0 +1,45 @@ +:host { + >.modal-body { + 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; + + div { + height: 10px; + background: #666; + } + } +} diff --git a/app/src/components/hotkeyInputModal.pug b/app/src/components/hotkeyInputModal.pug new file mode 100644 index 00000000..bb8411d4 --- /dev/null +++ b/app/src/components/hotkeyInputModal.pug @@ -0,0 +1,11 @@ +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') + + + .timeout + div([style.width]='timeoutProgress + "%"') + a.btn.btn-default((click)='close()') Cancel diff --git a/app/src/components/hotkeyInputModal.ts b/app/src/components/hotkeyInputModal.ts new file mode 100644 index 00000000..7cc3b8f2 --- /dev/null +++ b/app/src/components/hotkeyInputModal.ts @@ -0,0 +1,55 @@ +import { Component, Input } from '@angular/core' +import { HotkeysService } from 'services/hotkeys' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { Subscription } from 'rxjs' + +const INPUT_TIMEOUT = 2000 + + +@Component({ + selector: 'hotkey-input-modal', + template: require('./hotkeyInputModal.pug'), + styles: [require('./hotkeyInputModal.less')], +}) +export class HotkeyInputModalComponent { + private keySubscription: Subscription + private lastKeyEvent: number + private keyTimeoutInterval: NodeJS.Timer + + @Input() value: string[] = [] + @Input() timeoutProgress = 0 + + constructor( + private modalInstance: NgbActiveModal, + public hotkeys: HotkeysService, + ) { + this.keySubscription = hotkeys.key.subscribe(() => { + this.lastKeyEvent = performance.now() + this.value = this.hotkeys.getCurrentKeystrokes() + }) + } + + splitKeys (keys: string): string[] { + return keys.split('+').map((x) => x.trim()) + } + + ngOnInit () { + this.keyTimeoutInterval = setInterval(() => { + if (!this.lastKeyEvent) { + return + } + this.timeoutProgress = (performance.now() - this.lastKeyEvent) * 100 / INPUT_TIMEOUT + if (this.timeoutProgress >= 100) { + this.modalInstance.close(this.value) + } + }, 25) + } + + ngOnDestroy () { + clearInterval(this.keyTimeoutInterval) + } + + close() { + this.modalInstance.dismiss() + } +} diff --git a/app/src/components/settingsModal.less b/app/src/components/settingsModal.less index 6954f792..9b22a4db 100644 --- a/app/src/components/settingsModal.less +++ b/app/src/components/settingsModal.less @@ -2,68 +2,4 @@ >.modal-body { padding: 0 0 20px !important; } - - .form-group { - margin-left: 15px; - } - - .version-info { - text-align: center; - font-size: 12px; - color: #aaa; - cursor: pointer; - padding: 10px 0; - -webkit-user-select: text; - transition: .25s all; - - &:hover { - color: white; - } - } - - .status-line { - display: flex; - padding: 5px 10px; - - &.clickable { - &:hover { - background: rgba(0,0,0,.5); - cursor: pointer; - } - } - - .icon { - flex: none; - padding: 7px 10px 0 0px; - - img { - width: 32px; - height: 32px; - border-radius: 16px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); - } - - i { - width: 32px; - text-align: center; - } - } - - .main { - flex: auto; - flex-direction: column; - display: flex; - - .title { - flex: none; - font-size: 12px; - } - - .value { - flex: auto; - font-size: 16px; - color: #ddd; - } - } - } } diff --git a/app/src/components/settingsModal.pug b/app/src/components/settingsModal.pug index 5dff65e2..78020461 100644 --- a/app/src/components/settingsModal.pug +++ b/app/src/components/settingsModal.pug @@ -2,95 +2,18 @@ div.modal-body ngb-tabset(type='tabs nav-justified') ngb-tab template(ngbTabTitle) - i.fa.fa-cog - | General - template(ngbTabContent) - .status-line.clickable(*ngIf='connectionHost', (click)='openWeb()') - .icon - i.fa.fa-rss.fa-2x.fa-live - .main - .title Server - .value {{connectionHost}} - - .status-line(*ngIf='!connectionHost') - .icon - i.fa.fa-rss.fa-2x - .main - .title Server - .value Not connected - - - div.form-group - checkbox(text='Remember connected workspaces', '[(model)]'='config.store.rememberWorkspaces') - ngb-tab - template(ngbTabTitle) - i.fa.fa-wrench - | Advanced - template(ngbTabContent) - div.form-group(*ngIf='isWindows || isLinux') - div.input-group - input.form-control(type='text', placeholder='SNFS projects folder', '[(ngModel)]'='config.store.snfsPath') - div.input-group-btn - button.btn.btn-default((click)='selectSNFSPath()') - i.fa.fa-folder-open - div.form-group(*ngIf='isWindows') - label First drive letter to use - select.form-control('[(ngModel)]'='config.store.firstDrive') - option(*ngFor='let x of drives', value='{{x}}') {{x}}: - div.form-group(*ngIf='isMac') - label Extra NFS options - input.form-control(type='text', '[(ngModel)]'='config.store.extraNFSOptions') - div.form-group(*ngIf='isMac') - label Extra AFP options - input.form-control(type='text', '[(ngModel)]'='config.store.extraAFPOptions') - div.form-group(*ngIf='isMac') - label Extra SMB options - input.form-control(type='text', '[(ngModel)]'='config.store.extraSMBOptions') - div.form-group(*ngIf='isLinux') - label Extra NFS options - input.form-control(type='text', '[(ngModel)]'='config.store.extraLinuxNFSOptions') - div.form-group(*ngIf='isLinux') - label Extra SMB options - input.form-control(type='text', '[(ngModel)]'='config.store.extraLinuxSMBOptions') - - ngb-tab(*ngIf="apiServer.authorizedKeysStore.length > 0") - template(ngbTabTitle) - i.fa.fa-plug - | Apps - template(ngbTabContent) - .list-group - .list-group-item(*ngFor="let key of apiServer.authorizedKeysStore") - button.btn.btn-default((click)='apiServer.deauthorizeKey(key)') - i.fa.fa-times - span Disconnect this app - div {{key.name}} - - ngb-tab - template(ngbTabTitle) - i.fa.fa-info-circle - | About + i.fa.fa-keyboard-o + | Hotkeys template(ngbTabContent) .form-group - h1 ELEMENTS Client - div syslink GmbH © {{year}} - - .form-group - label Version - div {{version}} - - .form-group - button.btn.btn-default((click)='copyDiagnostics()') Copy diagnostic info + table.table + tr + th Toggle terminal window + td + hotkey-input('[(model)]'='globalHotkey') div.modal-footer div.btn-group.btn-group-justified - a.btn.btn-default((click)='logout()', *ngIf='elementsClient.userInfo') - i.fa.fa-fw.fa-arrow-left - br - | Log out - a.btn.btn-default((click)='quit()') - i.fa.fa-fw.fa-power-off - br - | Quit a.btn.btn-default((click)='close()') i.fa.fa-fw.fa-check br diff --git a/app/src/components/settingsModal.ts b/app/src/components/settingsModal.ts index 2905bd96..46065cf5 100644 --- a/app/src/components/settingsModal.ts +++ b/app/src/components/settingsModal.ts @@ -30,6 +30,8 @@ export class SettingsModalComponent { year: number version: string + globalHotkey = ['Ctrl+Shift+G'] + ngOnDestroy() { this.config.save() } diff --git a/app/src/services/hotkeys.ts b/app/src/services/hotkeys.ts index 3c1ebbc9..48038172 100644 --- a/app/src/services/hotkeys.ts +++ b/app/src/services/hotkeys.ts @@ -1,21 +1,20 @@ import { Injectable, NgZone, EventEmitter } from '@angular/core' import { ElectronService } from 'services/electron' +import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util' const hterm = require('hterm-commonjs') +const KEY_TIMEOUT = 2000 -export interface Key { - event: string, - alt: boolean, - ctrl: boolean, - cmd: boolean, - shift: boolean, - key: string +interface EventBufferEntry { + event: NativeKeyEvent, + time: number, } @Injectable() export class HotkeysService { - key = new EventEmitter() + key = new EventEmitter() globalHotkey = new EventEmitter() + private currentKeystrokes: EventBufferEntry[] = [] constructor( private zone: NgZone, @@ -26,10 +25,6 @@ export class HotkeysService { name: 'keydown', htermHandler: 'onKeyDown_', }, - { - name: 'keypress', - htermHandler: 'onKeyPress_', - }, { name: 'keyup', htermHandler: 'onKeyUp_', @@ -50,18 +45,21 @@ export class HotkeysService { } emitNativeEvent (name, nativeEvent) { + nativeEvent.event = name + + console.log(nativeEvent) + this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() }) + this.zone.run(() => { - this.key.emit({ - event: name, - alt: nativeEvent.altKey, - shift: nativeEvent.shiftKey, - cmd: nativeEvent.metaKey, - ctrl: nativeEvent.ctrlKey, - key: nativeEvent.key, - }) + this.key.emit(nativeEvent) }) } + getCurrentKeystrokes () : string[] { + this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT ) + return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event)) + } + registerHotkeys () { this.electron.globalShortcut.unregisterAll() this.electron.globalShortcut.register('`', () => { diff --git a/app/src/services/hotkeys.util.ts b/app/src/services/hotkeys.util.ts new file mode 100644 index 00000000..5134cc02 --- /dev/null +++ b/app/src/services/hotkeys.util.ts @@ -0,0 +1,55 @@ +import * as os from 'os' + + +export const metaKeyName = { + darwin: '⌘', + win32: 'Win', + linux: 'Super', +}[os.platform()] + +export const altKeyName = { + darwin: 'Option', + win32: 'Alt', + linux: 'Alt', +}[os.platform()] + + +export interface NativeKeyEvent { + event?: string, + altKey: boolean, + ctrlKey: boolean, + metaKey: boolean, + shiftKey: boolean, + key: string, + keyCode: string, +} + + +export function stringifyKeySequence(events: NativeKeyEvent[]): string[] { + let items: string[] = [] + let lastEvent: NativeKeyEvent + events = events.slice() + + while (events.length > 0) { + let event = events.shift() + if (event.event == 'keyup' && (lastEvent && lastEvent.event == 'keydown')) { + let itemKeys: string[] = [] + if (lastEvent.ctrlKey) { + itemKeys.push('Ctrl') + } + if (lastEvent.metaKey) { + itemKeys.push(metaKeyName) + } + if (lastEvent.altKey) { + itemKeys.push(altKeyName) + } + if (lastEvent.shiftKey) { + itemKeys.push('Shift') + } + itemKeys.push(lastEvent.key) + items.push(itemKeys.join('+')) + } + lastEvent = event + } + return items +}