mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-06 19:39:53 +00:00
split tab recovery (#49)
This commit is contained in:
parent
70b463b086
commit
7279ba13ac
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 } }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user