mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-22 12:29:53 +00:00
.
This commit is contained in:
parent
f7af82902f
commit
13ec887d66
@ -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,
|
||||
],
|
||||
|
@ -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;
|
||||
|
@ -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')
|
||||
|
39
app/src/components/hotkeyInput.less
Normal file
39
app/src/components/hotkeyInput.less
Normal file
@ -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();
|
||||
}
|
4
app/src/components/hotkeyInput.pug
Normal file
4
app/src/components/hotkeyInput.pug
Normal file
@ -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') +
|
30
app/src/components/hotkeyInput.ts
Normal file
30
app/src/components/hotkeyInput.ts
Normal file
@ -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()
|
||||
}
|
45
app/src/components/hotkeyInputModal.less
Normal file
45
app/src/components/hotkeyInputModal.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
11
app/src/components/hotkeyInputModal.pug
Normal file
11
app/src/components/hotkeyInputModal.pug
Normal file
@ -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
|
55
app/src/components/hotkeyInputModal.ts
Normal file
55
app/src/components/hotkeyInputModal.ts
Normal file
@ -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()
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -30,6 +30,8 @@ export class SettingsModalComponent {
|
||||
year: number
|
||||
version: string
|
||||
|
||||
globalHotkey = ['Ctrl+Shift+G']
|
||||
|
||||
ngOnDestroy() {
|
||||
this.config.save()
|
||||
}
|
||||
|
@ -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>()
|
||||
key = new EventEmitter<NativeKeyEvent>()
|
||||
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('`', () => {
|
||||
|
55
app/src/services/hotkeys.util.ts
Normal file
55
app/src/services/hotkeys.util.ts
Normal file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user