From 7279ba13ac075c674eabfded3a39951acaa86f28 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 3 Mar 2019 23:51:25 +0100 Subject: [PATCH] split tab recovery (#49) --- .../src/components/baseTab.component.ts | 3 + .../src/components/splitTab.component.ts | 95 +++++++++++++++++-- terminus-core/src/index.ts | 4 +- terminus-core/src/services/app.service.ts | 14 +-- terminus-core/src/services/tabs.service.ts | 4 +- 5 files changed, 103 insertions(+), 17 deletions(-) diff --git a/terminus-core/src/components/baseTab.component.ts b/terminus-core/src/components/baseTab.component.ts index 808c6481..f3aca3f5 100644 --- a/terminus-core/src/components/baseTab.component.ts +++ b/terminus-core/src/components/baseTab.component.ts @@ -18,6 +18,7 @@ export abstract class BaseTabComponent { protected progress = new Subject() protected activity = new Subject() protected destroyed = new Subject() + protected recoveryStateChangedHint = new Subject() private progressClearTimeout: number @@ -27,6 +28,7 @@ export abstract class BaseTabComponent { get progress$ (): Observable { return this.progress } get activity$ (): Observable { return this.activity } get destroyed$ (): Observable { return this.destroyed } + get recoveryStateChangedHint$ (): Observable { return this.recoveryStateChangedHint } constructor () { this.focused$.subscribe(() => { @@ -91,6 +93,7 @@ export abstract class BaseTabComponent { this.blurred.complete() this.titleChange.complete() this.progress.complete() + this.recoveryStateChangedHint.complete() this.destroyed.next() this.destroyed.complete() } diff --git a/terminus-core/src/components/splitTab.component.ts b/terminus-core/src/components/splitTab.component.ts index 7cbd1d3e..88ccc0b3 100644 --- a/terminus-core/src/components/splitTab.component.ts +++ b/terminus-core/src/components/splitTab.component.ts @@ -1,8 +1,10 @@ import { Subscription } from 'rxjs' -import { Component, ViewChild, ViewContainerRef, EmbeddedViewRef } from '@angular/core' -import { BaseTabComponent } from './baseTab.component' +import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef } from '@angular/core' +import { BaseTabComponent, BaseTabProcess } from './baseTab.component' +import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery' import { TabsService } from '../services/tabs.service' import { HotkeysService } from '../services/hotkeys.service' +import { TabRecoveryService } from '../services/tabRecovery.service' export declare type SplitOrientation = 'v' | 'h' export declare type SplitDirection = 'r' | 't' | 'b' | 'l' @@ -57,6 +59,23 @@ export class SplitContainer { } this.ratios = this.ratios.map(x => x / s) } + + async serialize () { + let children = [] + for (let child of this.children) { + if (child instanceof SplitContainer) { + children.push(await child.serialize()) + } else { + children.push(await child.getRecoveryToken()) + } + } + return { + type: 'app:split-tab', + ratios: this.ratios, + orientation: this.orientation, + children, + } + } } @Component({ @@ -72,10 +91,12 @@ export class SplitTabComponent extends BaseTabComponent { @ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef hotkeysSubscription: Subscription focusedTab: BaseTabComponent + recoveredState: any constructor ( - protected hotkeys: HotkeysService, + private hotkeys: HotkeysService, private tabsService: TabsService, + private tabRecovery: TabRecoveryService, ) { super() this.root = new SplitContainer() @@ -120,6 +141,17 @@ export class SplitTabComponent extends BaseTabComponent { }) } + async ngOnInit () { + if (this.recoveredState) { + await this.recoverContainer(this.root, this.recoveredState) + this.layout() + setImmediate(() => { + this.allTabs().forEach(x => x.emitFocused()) + this.focusAnyIn(this.root) + }) + } + } + ngOnDestroy () { this.hotkeysSubscription.unsubscribe() } @@ -181,15 +213,20 @@ export class SplitTabComponent extends BaseTabComponent { target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1)) target.children.splice(insertIndex, 0, tab) - let ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef - this.viewRefs.set(tab, ref) - - ref.rootNodes[0].addEventListener('click', () => this.focus(tab)) + this.recoveryStateChangedHint.next() + this.addTab(tab) setImmediate(() => { this.layout() this.focus(tab) }) + } + + addTab (tab: BaseTabComponent) { + let ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef + this.viewRefs.set(tab, ref) + + ref.rootNodes[0].addEventListener('click', () => this.focus(tab)) tab.titleChange$.subscribe(t => this.setTitle(t)) tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity()) @@ -250,6 +287,14 @@ export class SplitTabComponent extends BaseTabComponent { return !(await Promise.all(this.allTabs().map(x => x.canClose()))).some(x => !x) } + async getRecoveryToken (): Promise { + return this.root.serialize() + } + + async getCurrentProcess (): Promise { + return (await Promise.all(this.allTabs().map(x => x.getCurrentProcess()))).find(x => !!x) + } + private layout () { this.root.normalize() this.layoutInternal(this.root, 0, 0, 100, 100) @@ -280,4 +325,40 @@ export class SplitTabComponent extends BaseTabComponent { offset += sizes[i] }) } + + private async recoverContainer (root: SplitContainer, state: any) { + let children: (SplitContainer | BaseTabComponent)[] = [] + root.orientation = state.orientation + root.ratios = state.ratios + root.children = children + for (let childState of state.children) { + if (childState.type === 'app:split-tab') { + let child = new SplitContainer() + await this.recoverContainer(child, childState) + children.push(child) + } else { + let recovered = await this.tabRecovery.recoverTab(childState) + if (recovered) { + let tab = this.tabsService.create(recovered.type, recovered.options) + children.push(tab) + this.addTab(tab) + } else { + state.ratios.splice(state.children.indexOf(childState), 0) + } + } + } + } +} + +@Injectable() +export class SplitTabRecoveryProvider extends TabRecoveryProvider { + async recover (recoveryToken: any): Promise { + if (recoveryToken && recoveryToken.type === 'app:split-tab') { + return { + type: SplitTabComponent, + options: { recoveredState: recoveryToken }, + } + } + return null + } } diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 08ac7541..8c6bb718 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -18,7 +18,7 @@ import { TitleBarComponent } from './components/titleBar.component' import { ToggleComponent } from './components/toggle.component' import { WindowControlsComponent } from './components/windowControls.component' import { RenameTabModalComponent } from './components/renameTabModal.component' -import { SplitTabComponent } from './components/splitTab.component' +import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component' import { AutofocusDirective } from './directives/autofocus.directive' @@ -26,6 +26,7 @@ import { HotkeyProvider } from './api/hotkeyProvider' import { ConfigProvider } from './api/configProvider' import { Theme } from './api/theme' import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' +import { TabRecoveryProvider } from './api/tabRecovery' import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { CoreConfigProvider } from './config' @@ -43,6 +44,7 @@ const PROVIDERS = [ { provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, + { provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true }, { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } } ] diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts index a05f4196..667eff06 100644 --- a/terminus-core/src/services/app.service.ts +++ b/terminus-core/src/services/app.service.ts @@ -68,11 +68,9 @@ export class AppService { this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow()) - /*this.tabRecovery.recoverTabs().then(tabs => { - for (let - this.openNewTab(tab.type, tab.options) - tab of tabs) { - this.openNewTab(tab.type, tab.options) + this.tabRecovery.recoverTabs().then(tabs => { + for (let tab of tabs) { + this.openNewTabRaw(tab.type, tab.options) } this.tabsChanged$.subscribe(() => { @@ -81,7 +79,7 @@ export class AppService { setInterval(() => { tabRecovery.saveTabs(this.tabs) }, 30000) - })*/ + }) } addTabRaw (tab: BaseTabComponent) { @@ -90,6 +88,10 @@ export class AppService { this.tabsChanged.next() this.tabOpened.next(tab) + tab.recoveryStateChangedHint$.subscribe(() => { + this.tabRecovery.saveTabs(this.tabs) + }) + tab.titleChange$.subscribe(title => { if (tab === this.activeTab) { this.hostApp.setTitle(title) diff --git a/terminus-core/src/services/tabs.service.ts b/terminus-core/src/services/tabs.service.ts index 5389c0f4..c2345744 100644 --- a/terminus-core/src/services/tabs.service.ts +++ b/terminus-core/src/services/tabs.service.ts @@ -10,9 +10,7 @@ export class TabsService { private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector, private tabRecovery: TabRecoveryService, - ) { - - } + ) { } create (type: TabComponentType, inputs?: any): BaseTabComponent { let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)