proper tab classes

This commit is contained in:
Eugene Pankov 2017-04-08 14:50:10 +02:00
parent 2cca57e0fb
commit 79cd2a3bbb
27 changed files with 208 additions and 227 deletions

View file

@ -1,3 +1,3 @@
export abstract class DefaultTabProvider {
abstract open (): void
abstract async openNewTab (): Promise<void>
}

View file

@ -1,4 +1,4 @@
export { Tab } from './tab'
export { BaseTabComponent } from '../components/baseTab'
export { TabRecoveryProvider } from './tabRecovery'
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider'

View file

@ -1,32 +0,0 @@
import { EventEmitter } from '@angular/core'
import { BaseTabComponent } from 'components/baseTab'
export declare type ComponentType<T extends Tab> = new (...args: any[]) => BaseTabComponent<T>
export abstract class Tab {
id: number
title: string
scrollable: boolean
hasActivity = false
focused = new EventEmitter<any>()
blurred = new EventEmitter<any>()
static lastTabID = 0
constructor () {
this.id = Tab.lastTabID++
}
displayActivity (): void {
this.hasActivity = true
}
abstract getComponentType (): ComponentType<Tab>
getRecoveryToken (): any {
return null
}
destroy (): void {
}
}

View file

@ -1,5 +1,3 @@
import { Tab } from './tab'
export abstract class TabRecoveryProvider {
abstract async recover (recoveryToken: any): Promise<Tab>
abstract async recover (recoveryToken: any): Promise<void>
}

View file

@ -17,6 +17,7 @@ import { NotifyService } from 'services/notify'
import { PluginsService } from 'services/plugins'
import { QuitterService } from 'services/quitter'
import { DockingService } from 'services/docking'
import { TabRecoveryService } from 'services/tabRecovery'
import { AppRootComponent } from 'components/appRoot'
import { CheckboxComponent } from 'components/checkbox'
@ -53,6 +54,7 @@ let plugins = [
ModalService,
NotifyService,
PluginsService,
TabRecoveryService,
QuitterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
],

View file

@ -17,7 +17,7 @@ title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appear
[class.pre-selected]='idx == app.tabs.indexOf(app.activeTab) - 1',
[class.post-selected]='idx == app.tabs.indexOf(app.activeTab) + 1',
[index]='idx',
[model]='tab',
[tab]='tab',
[active]='tab == app.activeTab',
[hasActivity]='tab.hasActivity',
@animateTab,
@ -36,7 +36,7 @@ title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appear
tab-body(
*ngFor='let tab of app.tabs; trackBy: tab?.id',
[active]='tab == app.activeTab',
[model]='tab',
[tab]='tab',
[class.scrollable]='tab.scrollable',
)

View file

@ -5,10 +5,11 @@ import { ToasterConfig } from 'angular2-toaster'
import { ElectronService } from 'services/electron'
import { HostAppService } from 'services/hostApp'
import { HotkeysService } from 'services/hotkeys'
import { LogService } from 'services/log'
import { Logger, LogService } from 'services/log'
import { QuitterService } from 'services/quitter'
import { ConfigService } from 'services/config'
import { DockingService } from 'services/docking'
import { TabRecoveryService } from 'services/tabRecovery'
import { AppService, IToolbarButton, ToolbarButtonProvider } from 'api'
@ -43,10 +44,12 @@ import 'theme.scss'
})
export class AppRootComponent {
toasterConfig: ToasterConfig
logger: Logger
constructor(
private docking: DockingService,
private electron: ElectronService,
private tabRecovery: TabRecoveryService,
public hostApp: HostAppService,
public hotkeys: HotkeysService,
public config: ConfigService,
@ -57,8 +60,8 @@ export class AppRootComponent {
) {
console.timeStamp('AppComponent ctor')
let logger = log.create('main')
logger.info('v', electron.app.getVersion())
this.logger = log.create('main')
this.logger.info('v', electron.app.getVersion())
this.toasterConfig = new ToasterConfig({
mouseoverTimerStop: true,
@ -131,7 +134,9 @@ export class AppRootComponent {
getRightToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(true) }
async ngOnInit () {
await this.app.restoreTabs()
await this.tabRecovery.recoverTabs()
this.tabRecovery.saveTabs(this.app.tabs)
if (this.app.tabs.length == 0) {
this.app.openDefaultTab()
}

View file

@ -1,12 +1,29 @@
import { Tab } from 'api/tab'
import { BehaviorSubject } from 'rxjs'
import { EventEmitter, ViewRef } from '@angular/core'
export class BaseTabComponent<T extends Tab> {
protected model: T
initModel (model: T) {
this.model = model
this.initTab()
export abstract class BaseTabComponent {
id: number
title$ = new BehaviorSubject<string>(null)
scrollable: boolean
hasActivity = false
focused = new EventEmitter<any>()
blurred = new EventEmitter<any>()
hostView: ViewRef
private static lastTabID = 0
constructor () {
this.id = BaseTabComponent.lastTabID++
}
initTab () { }
displayActivity (): void {
this.hasActivity = true
}
getRecoveryToken (): any {
return null
}
destroy (): void {
}
}

View file

@ -1,5 +1,4 @@
import { Component, Input, ViewContainerRef, ViewChild, HostBinding, ComponentFactoryResolver, ComponentRef } from '@angular/core'
import { Tab } from 'api/tab'
import { Component, Input, ViewChild, HostBinding, ViewContainerRef } from '@angular/core'
import { BaseTabComponent } from 'components/baseTab'
@Component({
@ -9,21 +8,12 @@ import { BaseTabComponent } from 'components/baseTab'
})
export class TabBodyComponent {
@Input() @HostBinding('class.active') active: boolean
@Input() model: Tab
@Input() tab: BaseTabComponent
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
private component: ComponentRef<BaseTabComponent<Tab>>
constructor (private componentFactoryResolver: ComponentFactoryResolver) {
}
ngAfterViewInit () {
// run after the change detection finishes
setImmediate(() => {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.model.getComponentType())
this.component = this.placeholder.createComponent(componentFactory)
setImmediate(() => {
this.component.instance.initModel(this.model)
})
this.placeholder.insert(this.tab.hostView)
})
}
}

View file

@ -1,4 +1,4 @@
.content-wrapper
.index {{index + 1}}
.name {{model.title || "Terminal"}}
.name {{(tab.title$ || "Terminal") | async}}
button((click)='closeClicked.emit()') &times;

View file

@ -1,5 +1,5 @@
import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'
import { Tab } from 'api/tab'
import { BaseTabComponent } from 'components/baseTab'
import './tabHeader.scss'
@ -12,6 +12,6 @@ export class TabHeaderComponent {
@Input() index: number
@Input() @HostBinding('class.active') active: boolean
@Input() @HostBinding('class.has-activity') hasActivity: boolean
@Input() model: Tab
@Input() tab: BaseTabComponent
@Output() closeClicked = new EventEmitter()
}

View file

@ -1,36 +1,49 @@
import { Inject, Injectable } from '@angular/core'
import { Subject } from 'rxjs'
import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angular/core'
import { Logger, LogService } from 'services/log'
import { Tab } from 'api/tab'
import { TabRecoveryProvider } from 'api/tabRecovery'
import { DefaultTabProvider } from 'api/defaultTabProvider'
import { BaseTabComponent } from 'components/baseTab'
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
@Injectable()
export class AppService {
tabs: Tab[] = []
activeTab: Tab
tabs: BaseTabComponent[] = []
activeTab: BaseTabComponent
lastTabIndex = 0
logger: Logger
tabsChanged$ = new Subject()
constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
private defaultTabProvider: DefaultTabProvider,
private componentFactoryResolver: ComponentFactoryResolver,
@Optional() private defaultTabProvider: DefaultTabProvider,
private injector: Injector,
log: LogService,
) {
this.logger = log.create('app')
}
openTab (tab: Tab): void {
this.tabs.push(tab)
this.selectTab(tab)
this.saveTabs()
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
let componentRef = componentFactory.create(this.injector)
componentRef.instance.hostView = componentRef.hostView
Object.assign(componentRef.instance, inputs || {})
this.tabs.push(componentRef.instance)
this.selectTab(componentRef.instance)
this.tabsChanged$.next()
return componentRef.instance
}
openDefaultTab (): void {
if (this.defaultTabProvider) {
this.defaultTabProvider.openNewTab()
}
}
selectTab (tab) {
selectTab (tab: BaseTabComponent) {
if (this.tabs.includes(this.activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this.activeTab)
} else {
@ -41,7 +54,9 @@ export class AppService {
this.activeTab.blurred.emit()
}
this.activeTab = tab
this.activeTab.focused.emit()
if (this.activeTab) {
this.activeTab.focused.emit()
}
}
toggleLastTab () {
@ -65,7 +80,7 @@ export class AppService {
}
}
closeTab (tab) {
closeTab (tab: BaseTabComponent) {
tab.destroy()
/* if (tab.session) {
this.sessions.destroySession(tab.session)
@ -75,38 +90,6 @@ export class AppService {
if (tab == this.activeTab) {
this.selectTab(this.tabs[newIndex])
}
this.saveTabs()
}
saveTabs () {
window.localStorage.tabsRecovery = JSON.stringify(
this.tabs
.map((tab) => tab.getRecoveryToken())
.filter((token) => !!token)
)
}
async restoreTabs (): Promise<void> {
if (window.localStorage.tabsRecovery) {
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
let tab: Tab
for (let provider of this.tabRecoveryProviders) {
try {
tab = await provider.recover(token)
if (tab) {
break
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
if (tab) {
this.openTab(tab)
} else {
this.logger.warn('Cannot restore tab from the token:', token)
}
}
this.saveTabs()
}
this.tabsChanged$.next()
}
}

View file

@ -0,0 +1,45 @@
import { Injectable, Inject } from '@angular/core'
import { Logger, LogService } from 'services/log'
import { BaseTabComponent } from 'components/baseTab'
import { TabRecoveryProvider } from 'api/tabRecovery'
import { AppService } from 'services/app'
@Injectable()
export class TabRecoveryService {
logger: Logger
constructor(
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
app: AppService,
log: LogService
) {
this.logger = log.create('tabRecovery')
app.tabsChanged$.subscribe(() => {
this.saveTabs(app.tabs)
})
}
saveTabs (tabs: BaseTabComponent[]) {
window.localStorage.tabsRecovery = JSON.stringify(
tabs
.map((tab) => tab.getRecoveryToken())
.filter((token) => !!token)
)
}
async recoverTabs (): Promise<void> {
if (window.localStorage.tabsRecovery) {
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.tabRecoveryProviders) {
try {
await provider.recover(token)
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
}
}
}
}

View file

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
import { SettingsTab } from './tab'
import { SettingsTabComponent } from './components/settingsTab'
@Injectable()
@ -17,11 +17,11 @@ export class ButtonProvider extends ToolbarButtonProvider {
title: 'Settings',
weight: 10,
click: () => {
let settingsTab = this.app.tabs.find((tab) => tab instanceof SettingsTab)
let settingsTab = this.app.tabs.find((tab) => tab instanceof SettingsTabComponent)
if (settingsTab) {
this.app.selectTab(settingsTab)
} else {
this.app.openTab(new SettingsTab())
this.app.openNewTab(SettingsTabComponent)
}
}
}]

View file

@ -2,22 +2,20 @@ import { Component, Inject } from '@angular/core'
import { ElectronService } from 'services/electron'
import { ConfigService } from 'services/config'
import { DockingService } from 'services/docking'
import { IHotkeyDescription, HotkeyProvider } from 'api/hotkeyProvider'
import { IHotkeyDescription, HotkeyProvider, BaseTabComponent } from 'api'
import { BaseTabComponent } from 'components/baseTab'
import { SettingsTab } from '../tab'
import { SettingsTabProvider } from '../api'
@Component({
selector: 'settings-pane',
template: require('./settingsPane.pug'),
selector: 'settings-tab',
template: require('./settingsTab.pug'),
styles: [
require('./settingsPane.scss'),
require('./settingsPane.deep.css'),
require('./settingsTab.scss'),
require('./settingsTab.deep.css'),
],
})
export class SettingsPaneComponent extends BaseTabComponent<SettingsTab> {
export class SettingsTabComponent extends BaseTabComponent {
globalHotkey = ['Ctrl+Shift+G']
private hotkeyDescriptions: IHotkeyDescription[]
@ -30,6 +28,12 @@ export class SettingsPaneComponent extends BaseTabComponent<SettingsTab> {
) {
super()
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.title$.next('Settings')
this.scrollable = true
}
getRecoveryToken (): any {
return { type: 'app:settings' }
}
ngOnDestroy () {

View file

@ -7,7 +7,7 @@ import { HotkeyInputComponent } from './components/hotkeyInput'
import { HotkeyDisplayComponent } from './components/hotkeyDisplay'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput'
import { SettingsPaneComponent } from './components/settingsPane'
import { SettingsTabComponent } from './components/settingsTab'
import { SettingsTabBodyComponent } from './components/settingsTabBody'
import { ToolbarButtonProvider, TabRecoveryProvider } from 'api'
@ -28,14 +28,14 @@ import { RecoveryProvider } from './recoveryProvider'
],
entryComponents: [
HotkeyInputModalComponent,
SettingsPaneComponent,
SettingsTabComponent,
],
declarations: [
HotkeyDisplayComponent,
HotkeyInputComponent,
HotkeyInputModalComponent,
MultiHotkeyInputComponent,
SettingsPaneComponent,
SettingsTabComponent,
SettingsTabBodyComponent,
],
})

View file

@ -1,14 +1,19 @@
import { Injectable } from '@angular/core'
import { Tab, TabRecoveryProvider } from 'api'
import { SettingsTab } from './tab'
import { TabRecoveryProvider, AppService } from 'api'
import { SettingsTabComponent } from './components/settingsTab'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<Tab> {
constructor(
private app: AppService
) {
super()
}
async recover (recoveryToken: any): Promise<void> {
if (recoveryToken.type == 'app:settings') {
return new SettingsTab()
this.app.openNewTab(SettingsTabComponent)
}
return null
}
}

View file

@ -1,20 +0,0 @@
import { Tab, ComponentType } from 'api/tab'
import { SettingsPaneComponent } from './components/settingsPane'
export class SettingsTab extends Tab {
constructor () {
super()
this.title = 'Settings'
this.scrollable = true
}
getComponentType (): ComponentType<SettingsTab> {
return SettingsPaneComponent
}
getRecoveryToken (): any {
return {
type: 'app:settings',
}
}
}

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
import { SessionsService } from './services/sessions'
import { TerminalTab } from './tab'
import { TerminalTabComponent } from './components/terminalTab'
@Injectable()
@ -14,17 +14,20 @@ export class ButtonProvider extends ToolbarButtonProvider {
super()
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
if (hotkey == 'new-tab') {
this.app.openTab(await this.getNewTab())
this.openNewTab()
}
})
}
async getNewTab (): Promise<TerminalTab> {
async openNewTab (): Promise<void> {
let cwd = null
if (this.app.activeTab instanceof TerminalTab) {
if (this.app.activeTab instanceof TerminalTabComponent) {
cwd = await this.app.activeTab.session.getWorkingDirectory()
}
return new TerminalTab(await this.sessions.createNewSession({ command: 'zsh', cwd }))
this.app.openNewTab(
TerminalTabComponent,
{ session: await this.sessions.createNewSession({ command: 'zsh', cwd }) }
)
}
provide (): IToolbarButton[] {
@ -32,7 +35,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
icon: 'plus',
title: 'New terminal',
click: async () => {
this.app.openTab(await this.getNewTab())
this.openNewTab()
}
}]
}

View file

@ -48,22 +48,21 @@
.col-lg-6
.form-group
label Font
input.form-control(
type='text',
[ngbTypeahead]='fontAutocomplete',
'[(ngModel)]'='config.store.terminal.font',
(ngModelChange)='config.save()',
)
.row
.col-8
input.form-control(
type='text',
[ngbTypeahead]='fontAutocomplete',
'[(ngModel)]'='config.store.terminal.font',
(ngModelChange)='config.save()',
)
.col-4
input.form-control(
type='number',
'[(ngModel)]'='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Font to be used in the terminal
.form-group
label Font size
input.form-control(
type='number',
'[(ngModel)]'='config.store.terminal.fontSize',
(ngModelChange)='config.save()',
)
small.form-text.text-muted Text size to be used in the terminal
.form-group
label Color scheme

View file

@ -1,11 +1,10 @@
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs'
import { Component, NgZone, Inject, ViewChild, HostBinding } from '@angular/core'
import { Component, NgZone, Inject, ViewChild, HostBinding, Input } from '@angular/core'
import { BaseTabComponent } from 'components/baseTab'
import { TerminalTab } from '../tab'
import { TerminalDecorator, ResizeEvent } from '../api'
import { AppService, ConfigService } from 'api'
import { Session } from '../services/sessions'
import { AppService, ConfigService, BaseTabComponent } from 'api'
import { hterm, preferenceManager } from '../hterm'
@ -14,17 +13,17 @@ import { hterm, preferenceManager } from '../hterm'
template: '<div #content class="content"></div>',
styles: [require('./terminalTab.scss')],
})
export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
export class TerminalTabComponent extends BaseTabComponent {
hterm: any
configSubscription: Subscription
focusedSubscription: Subscription
title$ = new BehaviorSubject('')
size$ = new ReplaySubject<ResizeEvent>(1)
input$ = new Subject<string>()
output$ = new Subject<string>()
contentUpdated$ = new Subject<void>()
alternateScreenActive$ = new BehaviorSubject(false)
mouseEvent$ = new Subject<Event>()
@Input() session: Session
@ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string
private io: any
@ -41,8 +40,15 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
})
}
initTab () {
this.focusedSubscription = this.model.focused.subscribe(() => {
getRecoveryToken (): any {
return {
type: 'app:terminal',
recoveryId: this.session.recoveryId,
}
}
ngOnInit () {
this.focusedSubscription = this.focused.subscribe(() => {
this.hterm.scrollPort_.focus()
})
@ -57,24 +63,24 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
this.hterm.installKeyboard()
this.io = this.hterm.io.push()
this.attachIOHandlers(this.io)
this.model.session.output$.subscribe((data) => {
this.session.output$.subscribe((data) => {
this.zone.run(() => {
this.output$.next(data)
})
this.write(data)
})
this.model.session.closed$.first().subscribe(() => {
this.app.closeTab(this.model)
this.session.closed$.first().subscribe(() => {
this.app.closeTab(this)
})
this.model.session.releaseInitialDataBuffer()
this.session.releaseInitialDataBuffer()
}
this.hterm.decorate(this.content.nativeElement)
this.configure()
setTimeout(() => {
this.output$.subscribe(() => {
this.model.displayActivity()
this.displayActivity()
})
}, 1000)
}
@ -82,7 +88,6 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
attachHTermHandlers (hterm: any) {
hterm.setWindowTitle = (title) => {
this.zone.run(() => {
this.model.title = title
this.title$.next(title)
})
}
@ -142,14 +147,14 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
io.onTerminalResize = (columns, rows) => {
// console.log(`Resizing to ${columns}x${rows}`)
this.zone.run(() => {
this.model.session.resize(columns, rows)
this.session.resize(columns, rows)
this.size$.next({ width: columns, height: rows })
})
}
}
sendInput (data: string) {
this.model.session.write(data)
this.session.write(data)
}
write (data: string) {
@ -198,5 +203,7 @@ export class TerminalTabComponent extends BaseTabComponent<TerminalTab> {
this.contentUpdated$.complete()
this.alternateScreenActive$.complete()
this.mouseEvent$.complete()
this.session.gracefullyDestroy()
}
}

View file

@ -1,23 +1,25 @@
import { Injectable } from '@angular/core'
import { Tab, TabRecoveryProvider } from 'api'
import { TerminalTab } from './tab'
import { TabRecoveryProvider, AppService } from 'api'
import { SessionsService } from './services/sessions'
import { TerminalTabComponent } from './components/terminalTab'
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
constructor (private sessions: SessionsService) {
constructor (
private sessions: SessionsService,
private app: AppService,
) {
super()
}
async recover (recoveryToken: any): Promise<Tab> {
async recover (recoveryToken: any): Promise<void> {
if (recoveryToken.type == 'app:terminal') {
let session = await this.sessions.recover(recoveryToken.recoveryId)
if (!session) {
return null
return
}
return new TerminalTab(session)
this.app.openNewTab(TerminalTabComponent, { session })
}
return null
}
}

View file

@ -1,27 +0,0 @@
import { Tab, ComponentType } from 'api/tab'
import { TerminalTabComponent } from './components/terminalTab'
import { Session } from './services/sessions'
export class TerminalTab extends Tab {
static recoveryId = 'app:terminal'
constructor (public session: Session) {
super()
}
getComponentType (): ComponentType<TerminalTab> {
return TerminalTabComponent
}
getRecoveryToken (): any {
return {
type: 'app:terminal',
recoveryId: this.session.recoveryId,
}
}
destroy (): void {
this.session.gracefullyDestroy()
}
}

View file

@ -135,7 +135,7 @@ app-root > .content {
margin-top: 3px;
tab-header {
&.pre-selected, &:nth-last-child(1) {
&.pre-selected {
.content-wrapper {
border-bottom-right-radius: $tab-border-radius;
}
@ -167,7 +167,7 @@ app-root > .content {
margin-bottom: 3px;
tab-header {
&.pre-selected, &:nth-last-child(1) {
&.pre-selected {
.content-wrapper {
border-top-right-radius: $tab-border-radius;
}
@ -200,7 +200,7 @@ tab-body {
background: $body-bg;
}
settings-pane > ngb-tabset {
settings-tab > ngb-tabset {
border-right: 1px solid $body-bg2;
& > .nav {