split tab recovery (#49)

This commit is contained in:
Eugene Pankov 2019-03-03 23:51:25 +01:00
parent 70b463b086
commit 7279ba13ac
5 changed files with 103 additions and 17 deletions

View File

@ -18,6 +18,7 @@ export abstract class BaseTabComponent {
protected progress = new Subject<number>() protected progress = new Subject<number>()
protected activity = new Subject<boolean>() protected activity = new Subject<boolean>()
protected destroyed = new Subject<void>() protected destroyed = new Subject<void>()
protected recoveryStateChangedHint = new Subject<void>()
private progressClearTimeout: number private progressClearTimeout: number
@ -27,6 +28,7 @@ export abstract class BaseTabComponent {
get progress$ (): Observable<number> { return this.progress } get progress$ (): Observable<number> { return this.progress }
get activity$ (): Observable<boolean> { return this.activity } get activity$ (): Observable<boolean> { return this.activity }
get destroyed$ (): Observable<void> { return this.destroyed } get destroyed$ (): Observable<void> { return this.destroyed }
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
constructor () { constructor () {
this.focused$.subscribe(() => { this.focused$.subscribe(() => {
@ -91,6 +93,7 @@ export abstract class BaseTabComponent {
this.blurred.complete() this.blurred.complete()
this.titleChange.complete() this.titleChange.complete()
this.progress.complete() this.progress.complete()
this.recoveryStateChangedHint.complete()
this.destroyed.next() this.destroyed.next()
this.destroyed.complete() this.destroyed.complete()
} }

View File

@ -1,8 +1,10 @@
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { Component, ViewChild, ViewContainerRef, EmbeddedViewRef } from '@angular/core' import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef } from '@angular/core'
import { BaseTabComponent } from './baseTab.component' import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { TabsService } from '../services/tabs.service' import { TabsService } from '../services/tabs.service'
import { HotkeysService } from '../services/hotkeys.service' import { HotkeysService } from '../services/hotkeys.service'
import { TabRecoveryService } from '../services/tabRecovery.service'
export declare type SplitOrientation = 'v' | 'h' export declare type SplitOrientation = 'v' | 'h'
export declare type SplitDirection = 'r' | 't' | 'b' | 'l' export declare type SplitDirection = 'r' | 't' | 'b' | 'l'
@ -57,6 +59,23 @@ export class SplitContainer {
} }
this.ratios = this.ratios.map(x => x / s) 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({ @Component({
@ -72,10 +91,12 @@ export class SplitTabComponent extends BaseTabComponent {
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef @ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
hotkeysSubscription: Subscription hotkeysSubscription: Subscription
focusedTab: BaseTabComponent focusedTab: BaseTabComponent
recoveredState: any
constructor ( constructor (
protected hotkeys: HotkeysService, private hotkeys: HotkeysService,
private tabsService: TabsService, private tabsService: TabsService,
private tabRecovery: TabRecoveryService,
) { ) {
super() super()
this.root = new SplitContainer() 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 () { ngOnDestroy () {
this.hotkeysSubscription.unsubscribe() this.hotkeysSubscription.unsubscribe()
} }
@ -181,15 +213,20 @@ export class SplitTabComponent extends BaseTabComponent {
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1)) target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
target.children.splice(insertIndex, 0, tab) target.children.splice(insertIndex, 0, tab)
let ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> this.recoveryStateChangedHint.next()
this.viewRefs.set(tab, ref) this.addTab(tab)
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
setImmediate(() => { setImmediate(() => {
this.layout() this.layout()
this.focus(tab) this.focus(tab)
}) })
}
addTab (tab: BaseTabComponent) {
let ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any>
this.viewRefs.set(tab, ref)
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
tab.titleChange$.subscribe(t => this.setTitle(t)) tab.titleChange$.subscribe(t => this.setTitle(t))
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity()) 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) return !(await Promise.all(this.allTabs().map(x => x.canClose()))).some(x => !x)
} }
async getRecoveryToken (): Promise<any> {
return this.root.serialize()
}
async getCurrentProcess (): Promise<BaseTabProcess> {
return (await Promise.all(this.allTabs().map(x => x.getCurrentProcess()))).find(x => !!x)
}
private layout () { private layout () {
this.root.normalize() this.root.normalize()
this.layoutInternal(this.root, 0, 0, 100, 100) this.layoutInternal(this.root, 0, 0, 100, 100)
@ -280,4 +325,40 @@ export class SplitTabComponent extends BaseTabComponent {
offset += sizes[i] 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<RecoveredTab> {
if (recoveryToken && recoveryToken.type === 'app:split-tab') {
return {
type: SplitTabComponent,
options: { recoveredState: recoveryToken },
}
}
return null
}
} }

View File

@ -18,7 +18,7 @@ import { TitleBarComponent } from './components/titleBar.component'
import { ToggleComponent } from './components/toggle.component' import { ToggleComponent } from './components/toggle.component'
import { WindowControlsComponent } from './components/windowControls.component' import { WindowControlsComponent } from './components/windowControls.component'
import { RenameTabModalComponent } from './components/renameTabModal.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' import { AutofocusDirective } from './directives/autofocus.directive'
@ -26,6 +26,7 @@ import { HotkeyProvider } from './api/hotkeyProvider'
import { ConfigProvider } from './api/configProvider' import { ConfigProvider } from './api/configProvider'
import { Theme } from './api/theme' import { Theme } from './api/theme'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
import { TabRecoveryProvider } from './api/tabRecovery'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config' import { CoreConfigProvider } from './config'
@ -43,6 +44,7 @@ const PROVIDERS = [
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } } { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
] ]

View File

@ -68,11 +68,9 @@ export class AppService {
this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow()) this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow())
/*this.tabRecovery.recoverTabs().then(tabs => { this.tabRecovery.recoverTabs().then(tabs => {
for (let for (let tab of tabs) {
this.openNewTab(tab.type, tab.options) this.openNewTabRaw(tab.type, tab.options)
tab of tabs) {
this.openNewTab(tab.type, tab.options)
} }
this.tabsChanged$.subscribe(() => { this.tabsChanged$.subscribe(() => {
@ -81,7 +79,7 @@ export class AppService {
setInterval(() => { setInterval(() => {
tabRecovery.saveTabs(this.tabs) tabRecovery.saveTabs(this.tabs)
}, 30000) }, 30000)
})*/ })
} }
addTabRaw (tab: BaseTabComponent) { addTabRaw (tab: BaseTabComponent) {
@ -90,6 +88,10 @@ export class AppService {
this.tabsChanged.next() this.tabsChanged.next()
this.tabOpened.next(tab) this.tabOpened.next(tab)
tab.recoveryStateChangedHint$.subscribe(() => {
this.tabRecovery.saveTabs(this.tabs)
})
tab.titleChange$.subscribe(title => { tab.titleChange$.subscribe(title => {
if (tab === this.activeTab) { if (tab === this.activeTab) {
this.hostApp.setTitle(title) this.hostApp.setTitle(title)

View File

@ -10,9 +10,7 @@ export class TabsService {
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector, private injector: Injector,
private tabRecovery: TabRecoveryService, private tabRecovery: TabRecoveryService,
) { ) { }
}
create (type: TabComponentType, inputs?: any): BaseTabComponent { create (type: TabComponentType, inputs?: any): BaseTabComponent {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type) let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)