splitTab.component.ts 16.6 KB
Newer Older
E
Eugene Pankov 已提交
1 2
import { Observable, Subject, Subscription } from 'rxjs'
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, OnInit, OnDestroy } from '@angular/core'
E
Eugene Pankov 已提交
3 4
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
E
Eugene Pankov 已提交
5 6
import { TabsService } from '../services/tabs.service'
import { HotkeysService } from '../services/hotkeys.service'
E
Eugene Pankov 已提交
7
import { TabRecoveryService } from '../services/tabRecovery.service'
E
Eugene Pankov 已提交
8

E
Eugene Pankov 已提交
9 10
export type SplitOrientation = 'v' | 'h' // eslint-disable-line @typescript-eslint/no-type-alias
export type SplitDirection = 'r' | 't' | 'b' | 'l' // eslint-disable-line @typescript-eslint/no-type-alias
E
Eugene Pankov 已提交
11

12 13 14
/**
 * Describes a horizontal or vertical split row or column
 */
E
Eugene Pankov 已提交
15 16
export class SplitContainer {
    orientation: SplitOrientation = 'h'
17 18 19 20

    /**
     * Children could be tabs or other containers
     */
E
Eugene Pankov 已提交
21
    children: (BaseTabComponent | SplitContainer)[] = []
22 23 24 25

    /**
     * Relative sizes of children, between 0 and 1. Total sum is 1
     */
E
Eugene Pankov 已提交
26
    ratios: number[] = []
27

E
Eugene Pankov 已提交
28 29 30 31
    x: number
    y: number
    w: number
    h: number
E
Eugene Pankov 已提交
32

33 34 35
    /**
     * @return Flat list of all tabs inside this container
     */
E
Eugene Pankov 已提交
36 37
    getAllTabs (): BaseTabComponent[] {
        let r: BaseTabComponent[] = []
E
lint  
Eugene Pankov 已提交
38
        for (const child of this.children) {
E
Eugene Pankov 已提交
39
            if (child instanceof SplitContainer) {
E
Eugene Pankov 已提交
40
                r = r.concat(child.getAllTabs())
E
Eugene Pankov 已提交
41 42 43 44 45 46 47
            } else {
                r.push(child)
            }
        }
        return r
    }

48 49 50
    /**
     * Remove unnecessarily nested child containers and renormalizes [[ratios]]
     */
E
Eugene Pankov 已提交
51 52
    normalize () {
        for (let i = 0; i < this.children.length; i++) {
E
lint  
Eugene Pankov 已提交
53
            const child = this.children[i]
E
Eugene Pankov 已提交
54 55 56 57 58 59 60 61 62 63 64 65

            if (child instanceof SplitContainer) {
                child.normalize()

                if (child.children.length === 0) {
                    this.children.splice(i, 1)
                    this.ratios.splice(i, 1)
                    i--
                    continue
                } else if (child.children.length === 1) {
                    this.children[i] = child.children[0]
                } else if (child.orientation === this.orientation) {
E
lint  
Eugene Pankov 已提交
66
                    const ratio = this.ratios[i]
E
Eugene Pankov 已提交
67 68 69 70 71 72 73 74 75 76 77 78
                    this.children.splice(i, 1)
                    this.ratios.splice(i, 1)
                    for (let j = 0; j < child.children.length; j++) {
                        this.children.splice(i, 0, child.children[j])
                        this.ratios.splice(i, 0, child.ratios[j] * ratio)
                        i++
                    }
                }
            }
        }

        let s = 0
E
lint  
Eugene Pankov 已提交
79
        for (const x of this.ratios) {
E
Eugene Pankov 已提交
80 81 82 83
            s += x
        }
        this.ratios = this.ratios.map(x => x / s)
    }
E
Eugene Pankov 已提交
84

85 86 87
    /**
     * Gets the left/top side offset for the given element index (between 0 and 1)
     */
E
Eugene Pankov 已提交
88 89 90 91 92 93 94 95
    getOffsetRatio (index: number): number {
        let s = 0
        for (let i = 0; i < index; i++) {
            s += this.ratios[i]
        }
        return s
    }

E
Eugene Pankov 已提交
96
    async serialize () {
E
Eugene Pankov 已提交
97
        const children: any[] = []
E
lint  
Eugene Pankov 已提交
98
        for (const child of this.children) {
E
Eugene Pankov 已提交
99 100 101 102 103 104 105 106 107 108 109 110 111
            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,
        }
    }
E
Eugene Pankov 已提交
112 113
}

114 115 116
/**
 * Represents a spanner (draggable border between two split areas)
 */
E
Eugene Pankov 已提交
117
export interface SplitSpannerInfo {
E
Eugene Pankov 已提交
118
    container: SplitContainer
119 120 121 122

    /**
     * Number of the right/bottom split in the container
     */
E
Eugene Pankov 已提交
123 124 125
    index: number
}

126 127 128 129
/**
 * Split tab is a tab that contains other tabs and allows further splitting them
 * You'll mainly encounter it inside [[AppService]].tabs
 */
E
Eugene Pankov 已提交
130
@Component({
E
Eugene Pankov 已提交
131 132 133 134
    selector: 'split-tab',
    template: `
        <ng-container #vc></ng-container>
        <split-tab-spanner
E
Eugene Pankov 已提交
135
            *ngFor='let spanner of _spanners'
E
Eugene Pankov 已提交
136 137
            [container]='spanner.container'
            [index]='spanner.index'
E
Eugene Pankov 已提交
138
            (change)='onSpannerAdjusted(spanner)'
E
Eugene Pankov 已提交
139 140 141
        ></split-tab-spanner>
    `,
    styles: [require('./splitTab.component.scss')],
E
Eugene Pankov 已提交
142
})
E
Eugene Pankov 已提交
143
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
144
    /** @hidden */
E
Eugene Pankov 已提交
145
    @ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
146 147 148 149

    /**
     * Top-level split container
     */
E
Eugene Pankov 已提交
150
    root: SplitContainer
151 152

    /** @hidden */
E
Eugene Pankov 已提交
153
    _recoveredState: any
154 155

    /** @hidden */
E
Eugene Pankov 已提交
156
    _spanners: SplitSpannerInfo[] = []
157

E
Eugene Pankov 已提交
158 159 160 161
    private focusedTab: BaseTabComponent
    private hotkeysSubscription: Subscription
    private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()

162 163 164 165 166
    private tabAdded = new Subject<BaseTabComponent>()
    private tabRemoved = new Subject<BaseTabComponent>()
    private splitAdjusted = new Subject<SplitSpannerInfo>()
    private focusChanged = new Subject<BaseTabComponent>()

E
Eugene Pankov 已提交
167 168
    get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
    get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
169 170 171 172

    /**
     * Fired when split ratio is changed for a given spanner
     */
E
Eugene Pankov 已提交
173
    get splitAdjusted$ (): Observable<SplitSpannerInfo> { return this.splitAdjusted }
174 175 176 177

    /**
     * Fired when a different sub-tab gains focus
     */
E
Eugene Pankov 已提交
178
    get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged }
E
Eugene Pankov 已提交
179

180
    /** @hidden */
E
Eugene Pankov 已提交
181
    constructor (
E
Eugene Pankov 已提交
182
        private hotkeys: HotkeysService,
E
Eugene Pankov 已提交
183
        private tabsService: TabsService,
E
Eugene Pankov 已提交
184
        private tabRecovery: TabRecoveryService,
E
Eugene Pankov 已提交
185 186 187 188 189 190
    ) {
        super()
        this.root = new SplitContainer()
        this.setTitle('')

        this.focused$.subscribe(() => {
E
Eugene Pankov 已提交
191
            this.getAllTabs().forEach(x => x.emitFocused())
192 193 194 195 196
            if (this.focusedTab) {
                this.focus(this.focusedTab)
            } else {
                this.focusAnyIn(this.root)
            }
E
Eugene Pankov 已提交
197
        })
E
Eugene Pankov 已提交
198
        this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
E
Eugene Pankov 已提交
199 200 201 202 203 204

        this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
            if (!this.hasFocus) {
                return
            }
            switch (hotkey) {
E
Eugene Pankov 已提交
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
                case 'split-right':
                    this.splitTab(this.focusedTab, 'r')
                    break
                case 'split-bottom':
                    this.splitTab(this.focusedTab, 'b')
                    break
                case 'split-top':
                    this.splitTab(this.focusedTab, 't')
                    break
                case 'split-left':
                    this.splitTab(this.focusedTab, 'l')
                    break
                case 'pane-nav-left':
                    this.navigate('l')
                    break
                case 'pane-nav-right':
                    this.navigate('r')
                    break
                case 'pane-nav-up':
                    this.navigate('t')
                    break
                case 'pane-nav-down':
                    this.navigate('b')
                    break
                case 'close-pane':
                    this.removeTab(this.focusedTab)
                    break
E
Eugene Pankov 已提交
232 233 234 235
            }
        })
    }

236
    /** @hidden */
E
Eugene Pankov 已提交
237
    async ngOnInit () {
E
Eugene Pankov 已提交
238 239
        if (this._recoveredState) {
            await this.recoverContainer(this.root, this._recoveredState)
E
Eugene Pankov 已提交
240 241
            this.layout()
            setImmediate(() => {
242 243 244 245
                if (this.hasFocus) {
                    this.getAllTabs().forEach(x => x.emitFocused())
                    this.focusAnyIn(this.root)
                }
E
Eugene Pankov 已提交
246 247 248 249
            })
        }
    }

250
    /** @hidden */
E
Eugene Pankov 已提交
251 252 253 254
    ngOnDestroy () {
        this.hotkeysSubscription.unsubscribe()
    }

255
    /** @returns Flat list of all sub-tabs */
E
Eugene Pankov 已提交
256 257 258 259 260 261
    getAllTabs () {
        return this.root.getAllTabs()
    }

    getFocusedTab (): BaseTabComponent {
        return this.focusedTab
E
Eugene Pankov 已提交
262 263 264 265
    }

    focus (tab: BaseTabComponent) {
        this.focusedTab = tab
E
lint  
Eugene Pankov 已提交
266
        for (const x of this.getAllTabs()) {
E
Eugene Pankov 已提交
267 268 269 270 271 272
            if (x !== tab) {
                x.emitBlurred()
            }
        }
        if (tab) {
            tab.emitFocused()
E
Eugene Pankov 已提交
273
            this.focusChanged.next(tab)
E
Eugene Pankov 已提交
274 275 276 277
        }
        this.layout()
    }

278 279 280
    /**
     * Focuses the first available tab inside the given [[SplitContainer]]
     */
E
Eugene Pankov 已提交
281 282 283 284 285 286 287 288 289 290 291
    focusAnyIn (parent: BaseTabComponent | SplitContainer) {
        if (!parent) {
            return
        }
        if (parent instanceof SplitContainer) {
            this.focusAnyIn(parent.children[0])
        } else {
            this.focus(parent)
        }
    }

292 293 294
    /**
     * Inserts a new `tab` to the `side` of the `relative` tab
     */
E
Eugene Pankov 已提交
295 296 297
    addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection) {
        let target = (relative ? this.getParentOf(relative) : null) || this.root
        let insertIndex = relative ? target.children.indexOf(relative) : -1
E
Eugene Pankov 已提交
298 299

        if (
E
Eugene Pankov 已提交
300 301
            target.orientation === 'v' && ['l', 'r'].includes(side) ||
            target.orientation === 'h' && ['t', 'b'].includes(side)
E
Eugene Pankov 已提交
302
        ) {
E
lint  
Eugene Pankov 已提交
303
            const newContainer = new SplitContainer()
E
Eugene Pankov 已提交
304
            newContainer.orientation = target.orientation === 'v' ? 'h' : 'v'
E
Eugene Pankov 已提交
305
            newContainer.children = relative ? [relative] : []
E
Eugene Pankov 已提交
306 307 308 309 310 311 312 313 314
            newContainer.ratios = [1]
            target.children[insertIndex] = newContainer
            target = newContainer
            insertIndex = 0
        }

        if (insertIndex === -1) {
            insertIndex = 0
        } else {
E
Eugene Pankov 已提交
315
            insertIndex += side === 'l' || side === 't' ? 0 : 1
E
Eugene Pankov 已提交
316 317 318 319 320 321 322 323
        }

        for (let i = 0; i < target.children.length; i++) {
            target.ratios[i] *= target.children.length / (target.children.length + 1)
        }
        target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
        target.children.splice(insertIndex, 0, tab)

E
Eugene Pankov 已提交
324
        this.recoveryStateChangedHint.next()
E
Eugene Pankov 已提交
325
        this.attachTabView(tab)
E
Eugene Pankov 已提交
326 327 328

        setImmediate(() => {
            this.layout()
E
Eugene Pankov 已提交
329
            this.tabAdded.next(tab)
E
Eugene Pankov 已提交
330 331
            this.focus(tab)
        })
E
Eugene Pankov 已提交
332 333
    }

E
Eugene Pankov 已提交
334
    removeTab (tab: BaseTabComponent) {
E
lint  
Eugene Pankov 已提交
335
        const parent = this.getParentOf(tab)
E
Eugene Pankov 已提交
336 337 338
        if (!parent) {
            return
        }
E
lint  
Eugene Pankov 已提交
339
        const index = parent.children.indexOf(tab)
E
Eugene Pankov 已提交
340 341 342
        parent.ratios.splice(index, 1)
        parent.children.splice(index, 1)

E
Eugene Pankov 已提交
343
        this.detachTabView(tab)
E
Eugene Pankov 已提交
344 345 346

        this.layout()

E
Eugene Pankov 已提交
347 348
        this.tabRemoved.next(tab)

E
Eugene Pankov 已提交
349 350
        if (this.root.children.length === 0) {
            this.destroy()
R
Russell Myers 已提交
351 352
        } else {
            this.focusAnyIn(parent)
E
Eugene Pankov 已提交
353 354 355
        }
    }

356 357 358
    /**
     * Moves focus in the given direction
     */
E
Eugene Pankov 已提交
359 360
    navigate (dir: SplitDirection) {
        let rel: BaseTabComponent | SplitContainer = this.focusedTab
E
Eugene Pankov 已提交
361
        let parent = this.getParentOf(rel)
E
Eugene Pankov 已提交
362 363 364 365
        if (!parent) {
            return
        }

E
lint  
Eugene Pankov 已提交
366
        const orientation = ['l', 'r'].includes(dir) ? 'h' : 'v'
E
Eugene Pankov 已提交
367 368 369

        while (parent !== this.root && parent.orientation !== orientation) {
            rel = parent
E
Eugene Pankov 已提交
370
            parent = this.getParentOf(rel)
E
Eugene Pankov 已提交
371 372 373
            if (!parent) {
                return
            }
E
Eugene Pankov 已提交
374 375 376 377 378 379
        }

        if (parent.orientation !== orientation) {
            return
        }

E
lint  
Eugene Pankov 已提交
380
        const index = parent.children.indexOf(rel)
E
Eugene Pankov 已提交
381 382 383 384 385 386 387 388 389 390 391 392
        if (['l', 't'].includes(dir)) {
            if (index > 0) {
                this.focusAnyIn(parent.children[index - 1])
            }
        } else {
            if (index < parent.children.length - 1) {
                this.focusAnyIn(parent.children[index + 1])
            }
        }
    }

    async splitTab (tab: BaseTabComponent, dir: SplitDirection) {
E
lint  
Eugene Pankov 已提交
393
        const newTab = await this.tabsService.duplicate(tab)
E
Eugene Pankov 已提交
394 395 396
        if (newTab) {
            this.addTab(newTab, tab, dir)
        }
E
Eugene Pankov 已提交
397 398
    }

399 400 401
    /**
     * @returns the immediate parent of `tab`
     */
E
Eugene Pankov 已提交
402
    getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer|null {
E
Eugene Pankov 已提交
403
        root = root || this.root
E
lint  
Eugene Pankov 已提交
404
        for (const child of root.children) {
E
Eugene Pankov 已提交
405
            if (child instanceof SplitContainer) {
E
lint  
Eugene Pankov 已提交
406
                const r = this.getParentOf(tab, child)
E
Eugene Pankov 已提交
407 408 409 410 411 412 413 414 415 416 417
                if (r) {
                    return r
                }
            }
            if (child === tab) {
                return root
            }
        }
        return null
    }

418
    /** @hidden */
E
Eugene Pankov 已提交
419
    async canClose (): Promise<boolean> {
E
Eugene Pankov 已提交
420
        return !(await Promise.all(this.getAllTabs().map(x => x.canClose()))).some(x => !x)
E
Eugene Pankov 已提交
421 422
    }

423
    /** @hidden */
E
Eugene Pankov 已提交
424 425 426 427
    async getRecoveryToken (): Promise<any> {
        return this.root.serialize()
    }

428
    /** @hidden */
E
Eugene Pankov 已提交
429 430
    async getCurrentProcess (): Promise<BaseTabProcess|null> {
        return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x) || null
E
Eugene Pankov 已提交
431 432
    }

433
    /** @hidden */
E
Eugene Pankov 已提交
434 435 436 437 438 439
    onSpannerAdjusted (spanner: SplitSpannerInfo) {
        this.layout()
        this.splitAdjusted.next(spanner)
    }

    private attachTabView (tab: BaseTabComponent) {
E
Eugene Pankov 已提交
440
        const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
E
Eugene Pankov 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
        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())
        tab.progress$.subscribe(p => this.setProgress(p))
        if (tab.title) {
            this.setTitle(tab.title)
        }
        tab.destroyed$.subscribe(() => {
            this.removeTab(tab)
        })
    }

    private detachTabView (tab: BaseTabComponent) {
E
lint  
Eugene Pankov 已提交
457
        const ref = this.viewRefs.get(tab)
E
Eugene Pankov 已提交
458 459 460 461
        if (ref) {
            this.viewRefs.delete(tab)
            this.viewContainer.remove(this.viewContainer.indexOf(ref))
        }
E
Eugene Pankov 已提交
462 463
    }

E
Eugene Pankov 已提交
464 465
    private layout () {
        this.root.normalize()
E
Eugene Pankov 已提交
466
        this._spanners = []
E
Eugene Pankov 已提交
467 468 469 470
        this.layoutInternal(this.root, 0, 0, 100, 100)
    }

    private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
E
Eugene Pankov 已提交
471
        const size = root.orientation === 'v' ? h : w
E
lint  
Eugene Pankov 已提交
472
        const sizes = root.ratios.map(x => x * size)
E
Eugene Pankov 已提交
473

E
Eugene Pankov 已提交
474 475 476 477 478
        root.x = x
        root.y = y
        root.w = w
        root.h = h

E
Eugene Pankov 已提交
479 480
        let offset = 0
        root.children.forEach((child, i) => {
E
Eugene Pankov 已提交
481 482 483 484
            const childX = root.orientation === 'v' ? x : x + offset
            const childY = root.orientation === 'v' ? y + offset : y
            const childW = root.orientation === 'v' ? w : sizes[i]
            const childH = root.orientation === 'v' ? sizes[i] : h
E
Eugene Pankov 已提交
485 486 487
            if (child instanceof SplitContainer) {
                this.layoutInternal(child, childX, childY, childW, childH)
            } else {
E
Eugene Pankov 已提交
488
                const element = this.viewRefs.get(child)!.rootNodes[0]
E
Eugene Pankov 已提交
489 490 491 492 493 494
                element.style.position = 'absolute'
                element.style.left = `${childX}%`
                element.style.top = `${childY}%`
                element.style.width = `${childW}%`
                element.style.height = `${childH}%`

E
Eugene Pankov 已提交
495
                element.style.opacity = child === this.focusedTab ? 1 : 0.75
E
Eugene Pankov 已提交
496 497
            }
            offset += sizes[i]
E
Eugene Pankov 已提交
498 499

            if (i !== 0) {
E
Eugene Pankov 已提交
500
                this._spanners.push({
E
Eugene Pankov 已提交
501 502 503 504
                    container: root,
                    index: i,
                })
            }
E
Eugene Pankov 已提交
505 506
        })
    }
E
Eugene Pankov 已提交
507 508

    private async recoverContainer (root: SplitContainer, state: any) {
E
lint  
Eugene Pankov 已提交
509
        const children: (SplitContainer | BaseTabComponent)[] = []
E
Eugene Pankov 已提交
510 511 512
        root.orientation = state.orientation
        root.ratios = state.ratios
        root.children = children
E
lint  
Eugene Pankov 已提交
513
        for (const childState of state.children) {
E
Eugene Pankov 已提交
514
            if (childState.type === 'app:split-tab') {
E
lint  
Eugene Pankov 已提交
515
                const child = new SplitContainer()
E
Eugene Pankov 已提交
516 517 518
                await this.recoverContainer(child, childState)
                children.push(child)
            } else {
E
lint  
Eugene Pankov 已提交
519
                const recovered = await this.tabRecovery.recoverTab(childState)
E
Eugene Pankov 已提交
520
                if (recovered) {
E
lint  
Eugene Pankov 已提交
521
                    const tab = this.tabsService.create(recovered.type, recovered.options)
E
Eugene Pankov 已提交
522
                    children.push(tab)
E
Eugene Pankov 已提交
523
                    this.attachTabView(tab)
E
Eugene Pankov 已提交
524 525 526 527 528 529 530 531
                } else {
                    state.ratios.splice(state.children.indexOf(childState), 0)
                }
            }
        }
    }
}

532
/** @hidden */
E
Eugene Pankov 已提交
533 534
@Injectable()
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
E
Eugene Pankov 已提交
535
    async recover (recoveryToken: any): Promise<RecoveredTab|null> {
E
Eugene Pankov 已提交
536 537 538
        if (recoveryToken && recoveryToken.type === 'app:split-tab') {
            return {
                type: SplitTabComponent,
E
Eugene Pankov 已提交
539
                options: { _recoveredState: recoveryToken },
E
Eugene Pankov 已提交
540 541 542 543
            }
        }
        return null
    }
E
Eugene Pankov 已提交
544
}