mirror of
https://github.com/Eugeny/tabby
synced 2025-01-07 10:49:10 +00:00
allow dragging other tabs into existing split tabs - fixes #1347
This commit is contained in:
parent
ff49b9e38a
commit
be4cc804a2
18 changed files with 357 additions and 66 deletions
|
@ -14,6 +14,7 @@
|
||||||
"watch": "webpack --progress --color --watch"
|
"watch": "webpack --progress --color --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^12.1.2",
|
||||||
"@electron/remote": "1.2.0",
|
"@electron/remote": "1.2.0",
|
||||||
"any-promise": "^1.3.0",
|
"any-promise": "^1.3.0",
|
||||||
"electron-config": "2.0.0",
|
"electron-config": "2.0.0",
|
||||||
|
|
|
@ -2,6 +2,15 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@angular/cdk@^12.1.2":
|
||||||
|
version "12.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.1.2.tgz#5c2407324d860737374d873bd4381bf7f90f8a61"
|
||||||
|
integrity sha512-ALupZejZDsVYcbNZcEH1cV8SDgVBL40FAwDnlSZxCgd0HOBHH0ZqQV+8z0uCQeMatoNM+SwmJ8Y1JXYh9Bqfiw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.2.0"
|
||||||
|
optionalDependencies:
|
||||||
|
parse5 "^5.0.0"
|
||||||
|
|
||||||
"@electron/remote@1.2.0":
|
"@electron/remote@1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f"
|
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f"
|
||||||
|
@ -2558,6 +2567,11 @@ parse-json@^2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
error-ex "^1.2.0"
|
error-ex "^1.2.0"
|
||||||
|
|
||||||
|
parse5@^5.0.0:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
|
||||||
|
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
|
||||||
|
|
||||||
path-exists@^3.0.0:
|
path-exists@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
|
||||||
|
@ -3399,6 +3413,11 @@ tslib@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||||
|
|
||||||
|
tslib@^2.2.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||||
|
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||||
|
|
||||||
tslib@~2.1.0:
|
tslib@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||||
|
|
|
@ -14,15 +14,17 @@ title-bar(
|
||||||
&& config.store.appearance.frame == "thin" \
|
&& config.store.appearance.frame == "thin" \
|
||||||
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
||||||
.tabs(
|
.tabs(
|
||||||
dnd-sortable-container,
|
cdkDropList,
|
||||||
[sortableData]='app.tabs',
|
[cdkDropListOrientation]='(config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "bottom") ? "horizontal" : "vertical"',
|
||||||
|
(cdkDropListDropped)='onTabsReordered($event)',
|
||||||
|
cdkAutoDropGroup='app-tabs'
|
||||||
)
|
)
|
||||||
tab-header(
|
tab-header(
|
||||||
*ngFor='let tab of app.tabs; let idx = index',
|
*ngFor='let tab of app.tabs; let idx = index',
|
||||||
dnd-sortable,
|
cdkDrag,
|
||||||
[sortableIndex]='idx',
|
[cdkDragData]='tab',
|
||||||
(onDragStart)='onTabDragStart()',
|
(cdkDragStarted)='onTabDragStart()',
|
||||||
(onDragEnd)='onTabDragEnd()',
|
(cdkDragEnded)='onTabDragEnd()',
|
||||||
[index]='idx',
|
[index]='idx',
|
||||||
[tab]='tab',
|
[tab]='tab',
|
||||||
[active]='tab == app.activeTab',
|
[active]='tab == app.activeTab',
|
||||||
|
@ -30,7 +32,7 @@ title-bar(
|
||||||
[@.disabled]='hasVerticalTabs()',
|
[@.disabled]='hasVerticalTabs()',
|
||||||
(click)='app.selectTab(tab)',
|
(click)='app.selectTab(tab)',
|
||||||
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
||||||
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
|
[class.drag-region]='hostApp.platform == Platform.macOS && !(app.tabDragActive$|async)',
|
||||||
)
|
)
|
||||||
|
|
||||||
.btn-group.background
|
.btn-group.background
|
||||||
|
@ -109,6 +111,7 @@ title-bar(
|
||||||
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
||||||
|
|
||||||
tab-body.content-tab(
|
tab-body.content-tab(
|
||||||
|
#tabBodies,
|
||||||
*ngFor='let tab of unsortedTabs',
|
*ngFor='let tab of unsortedTabs',
|
||||||
[class.content-tab-active]='tab == app.activeTab',
|
[class.content-tab-active]='tab == app.activeTab',
|
||||||
[active]='tab == app.activeTab',
|
[active]='tab == app.activeTab',
|
||||||
|
|
|
@ -132,6 +132,14 @@ $side-tab-width: 200px;
|
||||||
window-controls {
|
window-controls {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cdk-drag-animating {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drop-list-dragging tab-header:not(.cdk-drag-placeholder) {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
|
import { Component, Inject, Input, HostListener, HostBinding, ViewChildren } from '@angular/core'
|
||||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
import { HostAppService, Platform } from '../api/hostApp'
|
import { HostAppService, Platform } from '../api/hostApp'
|
||||||
import { HotkeysService } from '../services/hotkeys.service'
|
import { HotkeysService } from '../services/hotkeys.service'
|
||||||
|
@ -12,6 +13,7 @@ import { UpdaterService } from '../services/updater.service'
|
||||||
|
|
||||||
import { BaseTabComponent } from './baseTab.component'
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||||
|
import { TabBodyComponent } from './tabBody.component'
|
||||||
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
|
@ -57,7 +59,7 @@ export class AppRootComponent {
|
||||||
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
||||||
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
||||||
@HostBinding('class.no-tabs') noTabs = true
|
@HostBinding('class.no-tabs') noTabs = true
|
||||||
tabsDragging = false
|
@ViewChildren(TabBodyComponent) tabBodies: TabBodyComponent[]
|
||||||
unsortedTabs: BaseTabComponent[] = []
|
unsortedTabs: BaseTabComponent[] = []
|
||||||
updatesAvailable = false
|
updatesAvailable = false
|
||||||
activeTransfers: FileTransfer[] = []
|
activeTransfers: FileTransfer[] = []
|
||||||
|
@ -126,11 +128,18 @@ export class AppRootComponent {
|
||||||
this.app.tabOpened$.subscribe(tab => {
|
this.app.tabOpened$.subscribe(tab => {
|
||||||
this.unsortedTabs.push(tab)
|
this.unsortedTabs.push(tab)
|
||||||
this.noTabs = false
|
this.noTabs = false
|
||||||
|
this.app.emitTabDragEnded()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.tabClosed$.subscribe(tab => {
|
this.app.tabRemoved$.subscribe(tab => {
|
||||||
|
for (const tabBody of this.tabBodies) {
|
||||||
|
if (tabBody.tab === tab) {
|
||||||
|
tabBody.detach()
|
||||||
|
}
|
||||||
|
}
|
||||||
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
||||||
this.noTabs = app.tabs.length === 0
|
this.noTabs = app.tabs.length === 0
|
||||||
|
this.app.emitTabDragEnded()
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.fileTransferStarted$.subscribe(transfer => {
|
platform.fileTransferStarted$.subscribe(transfer => {
|
||||||
|
@ -174,12 +183,12 @@ export class AppRootComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabDragStart () {
|
onTabDragStart () {
|
||||||
this.tabsDragging = true
|
this.app.emitTabDragStarted()
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabDragEnd () {
|
onTabDragEnd () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.tabsDragging = false
|
this.app.emitTabDragEnded()
|
||||||
this.app.emitTabsChanged()
|
this.app.emitTabsChanged()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -194,6 +203,11 @@ export class AppRootComponent {
|
||||||
return submenuItems.some(x => !!x.icon)
|
return submenuItems.some(x => !!x.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTabsReordered (event: CdkDragDrop<BaseTabComponent[]>) {
|
||||||
|
moveItemInArray(this.app.tabs, event.previousIndex, event.currentIndex)
|
||||||
|
this.app.emitTabsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
||||||
let buttons: ToolbarButton[] = []
|
let buttons: ToolbarButton[] = []
|
||||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { ViewRef } from '@angular/core'
|
import { EmbeddedViewRef, ViewContainerRef, ViewRef } from '@angular/core'
|
||||||
import { RecoveryToken } from '../api/tabRecovery'
|
import { RecoveryToken } from '../api/tabRecovery'
|
||||||
import { BaseComponent } from './base.component'
|
import { BaseComponent } from './base.component'
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||||
* your tab state to be saved sooner
|
* your tab state to be saved sooner
|
||||||
*/
|
*/
|
||||||
protected recoveryStateChangedHint = new Subject<void>()
|
protected recoveryStateChangedHint = new Subject<void>()
|
||||||
|
protected viewContainer?: ViewContainerRef
|
||||||
|
|
||||||
|
/* @hidden */
|
||||||
|
viewContainerEmbeddedRef?: EmbeddedViewRef<any>
|
||||||
|
|
||||||
private progressClearTimeout: number
|
private progressClearTimeout: number
|
||||||
private titleChange = new Subject<string>()
|
private titleChange = new Subject<string>()
|
||||||
|
@ -61,6 +65,8 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||||
private activity = new Subject<boolean>()
|
private activity = new Subject<boolean>()
|
||||||
private destroyed = new Subject<void>()
|
private destroyed = new Subject<void>()
|
||||||
|
|
||||||
|
private _destroyCalled = false
|
||||||
|
|
||||||
get focused$ (): Observable<void> { return this.focused }
|
get focused$ (): Observable<void> { return this.focused }
|
||||||
get blurred$ (): Observable<void> { return this.blurred }
|
get blurred$ (): Observable<void> { return this.blurred }
|
||||||
get titleChange$ (): Observable<string> { return this.titleChange }
|
get titleChange$ (): Observable<string> { return this.titleChange }
|
||||||
|
@ -152,10 +158,29 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||||
this.blurred.next()
|
this.blurred.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertIntoContainer (container: ViewContainerRef): EmbeddedViewRef<any> {
|
||||||
|
this.viewContainerEmbeddedRef = container.insert(this.hostView) as EmbeddedViewRef<any>
|
||||||
|
this.viewContainer = container
|
||||||
|
return this.viewContainerEmbeddedRef
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromContainer (): void {
|
||||||
|
if (!this.viewContainer || !this.viewContainerEmbeddedRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.viewContainer.detach(this.viewContainer.indexOf(this.viewContainerEmbeddedRef))
|
||||||
|
this.viewContainerEmbeddedRef = undefined
|
||||||
|
this.viewContainer = undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before the tab is closed
|
* Called before the tab is closed
|
||||||
*/
|
*/
|
||||||
destroy (skipDestroyedEvent = false): void {
|
destroy (skipDestroyedEvent = false): void {
|
||||||
|
if (this._destroyCalled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._destroyCalled = true
|
||||||
this.focused.complete()
|
this.focused.complete()
|
||||||
this.blurred.complete()
|
this.blurred.complete()
|
||||||
this.titleChange.complete()
|
this.titleChange.complete()
|
||||||
|
@ -166,6 +191,7 @@ export abstract class BaseTabComponent extends BaseComponent {
|
||||||
this.destroyed.next()
|
this.destroyed.next()
|
||||||
}
|
}
|
||||||
this.destroyed.complete()
|
this.destroyed.complete()
|
||||||
|
this.hostView.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
|
|
18
tabby-core/src/components/selfPositioning.component.ts
Normal file
18
tabby-core/src/components/selfPositioning.component.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { HostBinding, ElementRef } from '@angular/core'
|
||||||
|
import { BaseComponent } from './base.component'
|
||||||
|
|
||||||
|
export abstract class SelfPositioningComponent extends BaseComponent {
|
||||||
|
@HostBinding('style.left') cssLeft: string
|
||||||
|
@HostBinding('style.top') cssTop: string
|
||||||
|
@HostBinding('style.width') cssWidth: string | null
|
||||||
|
@HostBinding('style.height') cssHeight: string | null
|
||||||
|
|
||||||
|
constructor (protected element: ElementRef) { super() }
|
||||||
|
|
||||||
|
protected setDimensions (x: number, y: number, w: number, h: number, unit = '%'): void {
|
||||||
|
this.cssLeft = `${x}${unit}`
|
||||||
|
this.cssTop = `${y}${unit}`
|
||||||
|
this.cssWidth = w ? `${w}${unit}` : null
|
||||||
|
this.cssHeight = h ? `${h}${unit}` : null
|
||||||
|
}
|
||||||
|
}
|
|
@ -123,6 +123,14 @@ export interface SplitSpannerInfo {
|
||||||
index: number
|
index: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a tab drop zone
|
||||||
|
*/
|
||||||
|
export interface SplitDropZoneInfo {
|
||||||
|
relativeToTab: BaseTabComponent
|
||||||
|
side: SplitDirection
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split tab is a tab that contains other tabs and allows further splitting them
|
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||||
* You'll mainly encounter it inside [[AppService]].tabs
|
* You'll mainly encounter it inside [[AppService]].tabs
|
||||||
|
@ -137,6 +145,12 @@ export interface SplitSpannerInfo {
|
||||||
[index]='spanner.index'
|
[index]='spanner.index'
|
||||||
(change)='onSpannerAdjusted(spanner)'
|
(change)='onSpannerAdjusted(spanner)'
|
||||||
></split-tab-spanner>
|
></split-tab-spanner>
|
||||||
|
<split-tab-drop-zone
|
||||||
|
*ngFor='let dropZone of _dropZones'
|
||||||
|
[dropZone]='dropZone'
|
||||||
|
(tabDropped)='onTabDropped($event, dropZone)'
|
||||||
|
>
|
||||||
|
</split-tab-drop-zone>
|
||||||
`,
|
`,
|
||||||
styles: [require('./splitTab.component.scss')],
|
styles: [require('./splitTab.component.scss')],
|
||||||
})
|
})
|
||||||
|
@ -157,6 +171,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
_spanners: SplitSpannerInfo[] = []
|
_spanners: SplitSpannerInfo[] = []
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
_dropZones: SplitDropZoneInfo[] = []
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
_allFocusMode = false
|
_allFocusMode = false
|
||||||
|
|
||||||
|
@ -166,12 +183,19 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||||
|
|
||||||
private tabAdded = new Subject<BaseTabComponent>()
|
private tabAdded = new Subject<BaseTabComponent>()
|
||||||
|
private tabAdopted = new Subject<BaseTabComponent>()
|
||||||
private tabRemoved = new Subject<BaseTabComponent>()
|
private tabRemoved = new Subject<BaseTabComponent>()
|
||||||
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
||||||
private focusChanged = new Subject<BaseTabComponent>()
|
private focusChanged = new Subject<BaseTabComponent>()
|
||||||
private initialized = new Subject<void>()
|
private initialized = new Subject<void>()
|
||||||
|
|
||||||
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when an existing top-level tab is dragged into this tab
|
||||||
|
*/
|
||||||
|
get tabAdopted$ (): Observable<BaseTabComponent> { return this.tabAdopted }
|
||||||
|
|
||||||
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -330,11 +354,27 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||||
|
return this.add(tab, relative, side)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a new `tab` to the `side` of the `relative` tab
|
* Inserts a new `tab` to the `side` of the `relative` tab
|
||||||
*/
|
*/
|
||||||
async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||||
tab.parent = this
|
if (thing instanceof SplitTabComponent) {
|
||||||
|
const tab = thing
|
||||||
|
thing = tab.root
|
||||||
|
tab.root = new SplitContainer()
|
||||||
|
for (const child of thing.getAllTabs()) {
|
||||||
|
child.removeFromContainer()
|
||||||
|
}
|
||||||
|
tab.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thing instanceof BaseTabComponent) {
|
||||||
|
thing.parent = this
|
||||||
|
}
|
||||||
|
|
||||||
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
|
let target = (relative ? this.getParentOf(relative) : null) ?? this.root
|
||||||
let insertIndex = relative ? target.children.indexOf(relative) : -1
|
let insertIndex = relative ? target.children.indexOf(relative) : -1
|
||||||
|
@ -362,15 +402,17 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
||||||
}
|
}
|
||||||
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, thing)
|
||||||
|
|
||||||
this.recoveryStateChangedHint.next()
|
this.recoveryStateChangedHint.next()
|
||||||
|
|
||||||
await this.initialized$.toPromise()
|
await this.initialized$.toPromise()
|
||||||
|
|
||||||
|
for (const tab of thing instanceof SplitContainer ? thing.getAllTabs() : [thing]) {
|
||||||
this.attachTabView(tab)
|
this.attachTabView(tab)
|
||||||
this.onAfterTabAdded(tab)
|
this.onAfterTabAdded(tab)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removeTab (tab: BaseTabComponent): void {
|
removeTab (tab: BaseTabComponent): void {
|
||||||
const parent = this.getParentOf(tab)
|
const parent = this.getParentOf(tab)
|
||||||
|
@ -381,8 +423,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
parent.ratios.splice(index, 1)
|
parent.ratios.splice(index, 1)
|
||||||
parent.children.splice(index, 1)
|
parent.children.splice(index, 1)
|
||||||
|
|
||||||
this.detachTabView(tab)
|
tab.removeFromContainer()
|
||||||
tab.parent = null
|
tab.parent = null
|
||||||
|
this.viewRefs.delete(tab)
|
||||||
|
|
||||||
this.layout()
|
this.layout()
|
||||||
|
|
||||||
|
@ -401,7 +444,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
}
|
}
|
||||||
const position = parent.children.indexOf(tab)
|
const position = parent.children.indexOf(tab)
|
||||||
parent.children[position] = newTab
|
parent.children[position] = newTab
|
||||||
this.detachTabView(tab)
|
tab.removeFromContainer()
|
||||||
this.attachTabView(newTab)
|
this.attachTabView(newTab)
|
||||||
tab.parent = null
|
tab.parent = null
|
||||||
newTab.parent = this
|
newTab.parent = this
|
||||||
|
@ -508,6 +551,16 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
this.splitAdjusted.next(spanner)
|
this.splitAdjusted.next(spanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
onTabDropped (tab: BaseTabComponent, zone: SplitDropZoneInfo) { // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
if (tab === this) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.add(tab, zone.relativeToTab, zone.side)
|
||||||
|
this.tabAdopted.next(tab)
|
||||||
|
}
|
||||||
|
|
||||||
destroy (): void {
|
destroy (): void {
|
||||||
super.destroy()
|
super.destroy()
|
||||||
for (const x of this.getAllTabs()) {
|
for (const x of this.getAllTabs()) {
|
||||||
|
@ -518,13 +571,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
layout (): void {
|
layout (): void {
|
||||||
this.root.normalize()
|
this.root.normalize()
|
||||||
this._spanners = []
|
this._spanners = []
|
||||||
|
this._dropZones = []
|
||||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachTabView (tab: BaseTabComponent) {
|
private attachTabView (tab: BaseTabComponent) {
|
||||||
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
const ref = tab.insertIntoContainer(this.viewContainer)
|
||||||
this.viewRefs.set(tab, ref)
|
this.viewRefs.set(tab, ref)
|
||||||
|
|
||||||
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
||||||
|
|
||||||
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
|
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
|
||||||
|
@ -541,14 +594,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private detachTabView (tab: BaseTabComponent) {
|
|
||||||
const ref = this.viewRefs.get(tab)
|
|
||||||
if (ref) {
|
|
||||||
this.viewRefs.delete(tab)
|
|
||||||
this.viewContainer.remove(this.viewContainer.indexOf(ref))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAfterTabAdded (tab: BaseTabComponent) {
|
private onAfterTabAdded (tab: BaseTabComponent) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.layout()
|
this.layout()
|
||||||
|
@ -593,6 +638,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
element.style.width = '90%'
|
element.style.width = '90%'
|
||||||
element.style.height = '90%'
|
element.style.height = '90%'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const side of ['t', 'r', 'b', 'l']) {
|
||||||
|
this._dropZones.push({
|
||||||
|
relativeToTab: child,
|
||||||
|
side: side as SplitDirection,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
offset += sizes[i]
|
offset += sizes[i]
|
||||||
|
@ -612,6 +664,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
||||||
root.ratios = state.ratios
|
root.ratios = state.ratios
|
||||||
root.children = children
|
root.children = children
|
||||||
for (const childState of state.children) {
|
for (const childState of state.children) {
|
||||||
|
if (!childState) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (childState.type === 'app:split-tab') {
|
if (childState.type === 'app:split-tab') {
|
||||||
const child = new SplitContainer()
|
const child = new SplitContainer()
|
||||||
await this.recoverContainer(child, childState, duplicate)
|
await this.recoverContainer(child, childState, duplicate)
|
||||||
|
|
37
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
37
tabby-core/src/components/splitTabDropZone.component.scss
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 15px;
|
||||||
|
transition: all 125ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background: rgba(255, 255, 255, .125);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .25);
|
||||||
|
transition: all 125ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlighted {
|
||||||
|
padding: 0px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
background: rgba(255, 255, 255, .5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep tab-header {
|
||||||
|
// placeholders
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
63
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
63
tabby-core/src/components/splitTabDropZone.component.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { AppService } from '../services/app.service'
|
||||||
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
|
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||||
|
import { SplitDropZoneInfo } from './splitTab.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
selector: 'split-tab-drop-zone',
|
||||||
|
template: `
|
||||||
|
<div
|
||||||
|
cdkDropList
|
||||||
|
(cdkDropListDropped)="tabDropped.emit($event.item.data); isHighlighted = false"
|
||||||
|
(cdkDropListEntered)="isHighlighted = true"
|
||||||
|
(cdkDropListExited)="isHighlighted = false"
|
||||||
|
cdkAutoDropGroup='app-tabs'
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [require('./splitTabDropZone.component.scss')],
|
||||||
|
})
|
||||||
|
export class SplitTabDropZoneComponent extends SelfPositioningComponent {
|
||||||
|
@Input() dropZone: SplitDropZoneInfo
|
||||||
|
@Output() tabDropped = new EventEmitter<BaseTabComponent>()
|
||||||
|
@HostBinding('class.active') isActive = false
|
||||||
|
@HostBinding('class.highlighted') isHighlighted = false
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
|
constructor (
|
||||||
|
element: ElementRef,
|
||||||
|
app: AppService,
|
||||||
|
) {
|
||||||
|
super(element)
|
||||||
|
this.subscribeUntilDestroyed(app.tabDragActive$, active => {
|
||||||
|
this.isActive = active
|
||||||
|
this.layout()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
layout () {
|
||||||
|
const tabElement: HTMLElement = this.dropZone.relativeToTab.viewContainerEmbeddedRef?.rootNodes[0]
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
t: [0, 0, tabElement.clientWidth, tabElement.clientHeight / 5],
|
||||||
|
l: [0, tabElement.clientHeight / 5, tabElement.clientWidth / 3, tabElement.clientHeight * 3 / 5],
|
||||||
|
r: [tabElement.clientWidth * 2 / 3, tabElement.clientHeight / 5, tabElement.clientWidth / 3, tabElement.clientHeight * 3 / 5],
|
||||||
|
b: [0, tabElement.clientHeight * 4 / 5, tabElement.clientWidth, tabElement.clientHeight / 5],
|
||||||
|
}[this.dropZone.side]
|
||||||
|
|
||||||
|
this.setDimensions(
|
||||||
|
args[0] + tabElement.offsetLeft,
|
||||||
|
args[1] + tabElement.offsetTop,
|
||||||
|
args[2],
|
||||||
|
args[3],
|
||||||
|
'px'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||||
import { SplitContainer } from './splitTab.component'
|
import { SplitContainer } from './splitTab.component'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
|
@ -8,20 +9,19 @@ import { SplitContainer } from './splitTab.component'
|
||||||
template: '',
|
template: '',
|
||||||
styles: [require('./splitTabSpanner.component.scss')],
|
styles: [require('./splitTabSpanner.component.scss')],
|
||||||
})
|
})
|
||||||
export class SplitTabSpannerComponent {
|
export class SplitTabSpannerComponent extends SelfPositioningComponent {
|
||||||
@Input() container: SplitContainer
|
@Input() container: SplitContainer
|
||||||
@Input() index: number
|
@Input() index: number
|
||||||
@Output() change = new EventEmitter<void>()
|
@Output() change = new EventEmitter<void>()
|
||||||
@HostBinding('class.active') isActive = false
|
@HostBinding('class.active') isActive = false
|
||||||
@HostBinding('class.h') isHorizontal = false
|
@HostBinding('class.h') isHorizontal = false
|
||||||
@HostBinding('class.v') isVertical = true
|
@HostBinding('class.v') isVertical = true
|
||||||
@HostBinding('style.left') cssLeft: string
|
|
||||||
@HostBinding('style.top') cssTop: string
|
|
||||||
@HostBinding('style.width') cssWidth: string | null
|
|
||||||
@HostBinding('style.height') cssHeight: string | null
|
|
||||||
private marginOffset = -5
|
private marginOffset = -5
|
||||||
|
|
||||||
constructor (private element: ElementRef) { }
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||||
|
constructor (element: ElementRef) {
|
||||||
|
super(element)
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
this.element.nativeElement.addEventListener('dblclick', () => {
|
this.element.nativeElement.addEventListener('dblclick', () => {
|
||||||
|
@ -92,11 +92,4 @@ export class SplitTabSpannerComponent {
|
||||||
this.container.ratios[this.index] = ratio
|
this.container.ratios[this.index] = ratio
|
||||||
this.change.emit()
|
this.change.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDimensions (x: number, y: number, w: number, h: number) {
|
|
||||||
this.cssLeft = `${x}%`
|
|
||||||
this.cssTop = `${y}%`
|
|
||||||
this.cssWidth = w ? `${w}%` : null
|
|
||||||
this.cssHeight = h ? `${h}%` : null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ export class TabBodyComponent implements OnChanges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detach () {
|
||||||
|
this.placeholder?.detach()
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
this.placeholder?.detach()
|
this.placeholder?.detach()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,14 @@
|
||||||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||||
.activity-indicator(*ngIf='tab.activity$|async')
|
.activity-indicator(*ngIf='tab.activity$|async')
|
||||||
|
|
||||||
.index(*ngIf='!config.store.terminal.hideTabIndex', #handle) {{index + 1}}
|
ng-container(*ngIf='!config.store.terminal.hideTabIndex')
|
||||||
|
.index(*ngIf='hostApp.platform === Platform.macOS', cdkDragHandle) {{index + 1}}
|
||||||
|
.index(*ngIf='hostApp.platform !== Platform.macOS') {{index + 1}}
|
||||||
|
|
||||||
.name(
|
.name(
|
||||||
[title]='tab.customTitle || tab.title',
|
[title]='tab.customTitle || tab.title',
|
||||||
[class.no-hover]='config.store.terminal.hideCloseButton'
|
[class.no-hover]='config.store.terminal.hideCloseButton'
|
||||||
) {{tab.customTitle || tab.title}}
|
) {{tab.customTitle || tab.title}}
|
||||||
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') ×
|
||||||
|
|
||||||
|
ng-content
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef, NgZone } from '@angular/core'
|
import { Component, Input, Optional, Inject, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||||
import { SortableComponent } from 'ng2-dnd'
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||||
import { BaseTabComponent } from './baseTab.component'
|
import { BaseTabComponent } from './baseTab.component'
|
||||||
|
@ -13,11 +12,6 @@ import { BaseComponent } from './base.component'
|
||||||
import { MenuItemOptions } from '../api/menu'
|
import { MenuItemOptions } from '../api/menu'
|
||||||
import { PlatformService } from '../api/platform'
|
import { PlatformService } from '../api/platform'
|
||||||
|
|
||||||
/** @hidden */
|
|
||||||
export interface SortableComponentProxy {
|
|
||||||
setDragHandle: (_: HTMLElement) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tab-header',
|
selector: 'tab-header',
|
||||||
|
@ -29,17 +23,16 @@ export class TabHeaderComponent extends BaseComponent {
|
||||||
@Input() @HostBinding('class.active') active: boolean
|
@Input() @HostBinding('class.active') active: boolean
|
||||||
@Input() tab: BaseTabComponent
|
@Input() tab: BaseTabComponent
|
||||||
@Input() progress: number|null
|
@Input() progress: number|null
|
||||||
@ViewChild('handle') handle?: ElementRef
|
Platform = Platform
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public app: AppService,
|
public app: AppService,
|
||||||
public config: ConfigService,
|
public config: ConfigService,
|
||||||
private hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
private hotkeys: HotkeysService,
|
private hotkeys: HotkeysService,
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
|
|
||||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -61,12 +54,6 @@ export class TabHeaderComponent extends BaseComponent {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
|
||||||
if (this.handle && this.hostApp.platform === Platform.macOS) {
|
|
||||||
this.parentDraggable.setDragHandle(this.handle.nativeElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showRenameTabModal (): void {
|
showRenameTabModal (): void {
|
||||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||||
|
|
26
tabby-core/src/directives/cdkAutoDropGroup.directive.ts
Normal file
26
tabby-core/src/directives/cdkAutoDropGroup.directive.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Directive, Input, OnInit } from '@angular/core'
|
||||||
|
import { CdkDropList } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
|
class FakeDropGroup {
|
||||||
|
_items: Set<CdkDropList> = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Directive({
|
||||||
|
selector: '[cdkAutoDropGroup]',
|
||||||
|
})
|
||||||
|
export class CdkAutoDropGroup implements OnInit {
|
||||||
|
static groups: Record<string, FakeDropGroup> = {}
|
||||||
|
|
||||||
|
@Input('cdkAutoDropGroup') groupName: string
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private cdkDropList: CdkDropList,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit (): void {
|
||||||
|
CdkAutoDropGroup.groups[this.groupName] ??= new FakeDropGroup()
|
||||||
|
CdkAutoDropGroup.groups[this.groupName]._items.add(this.cdkDropList)
|
||||||
|
this.cdkDropList['_group'] = CdkAutoDropGroup.groups[this.groupName]
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-sc
|
||||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
import { DndModule } from 'ng2-dnd'
|
import { DndModule } from 'ng2-dnd'
|
||||||
import { SortablejsModule } from 'ngx-sortablejs'
|
import { SortablejsModule } from 'ngx-sortablejs'
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
import { AppRootComponent } from './components/appRoot.component'
|
import { AppRootComponent } from './components/appRoot.component'
|
||||||
import { CheckboxComponent } from './components/checkbox.component'
|
import { CheckboxComponent } from './components/checkbox.component'
|
||||||
|
@ -22,6 +23,7 @@ import { RenameTabModalComponent } from './components/renameTabModal.component'
|
||||||
import { SelectorModalComponent } from './components/selectorModal.component'
|
import { SelectorModalComponent } from './components/selectorModal.component'
|
||||||
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||||
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||||
|
import { SplitTabDropZoneComponent } from './components/splitTabDropZone.component'
|
||||||
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
||||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||||
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
||||||
|
@ -30,6 +32,7 @@ import { AutofocusDirective } from './directives/autofocus.directive'
|
||||||
import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypeahead.directive'
|
import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypeahead.directive'
|
||||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||||
import { DropZoneDirective } from './directives/dropZone.directive'
|
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||||
|
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
|
||||||
|
|
||||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider } from './api'
|
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ToolbarButtonProvider, ProfilesService, ProfileProvider } from './api'
|
||||||
|
|
||||||
|
@ -78,6 +81,7 @@ const PROVIDERS = [
|
||||||
NgxFilesizeModule,
|
NgxFilesizeModule,
|
||||||
PerfectScrollbarModule,
|
PerfectScrollbarModule,
|
||||||
DndModule.forRoot(),
|
DndModule.forRoot(),
|
||||||
|
DragDropModule,
|
||||||
SortablejsModule.forRoot({ animation: 150 }),
|
SortablejsModule.forRoot({ animation: 150 }),
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -98,10 +102,12 @@ const PROVIDERS = [
|
||||||
SelectorModalComponent,
|
SelectorModalComponent,
|
||||||
SplitTabComponent,
|
SplitTabComponent,
|
||||||
SplitTabSpannerComponent,
|
SplitTabSpannerComponent,
|
||||||
|
SplitTabDropZoneComponent,
|
||||||
UnlockVaultModalComponent,
|
UnlockVaultModalComponent,
|
||||||
WelcomeTabComponent,
|
WelcomeTabComponent,
|
||||||
TransfersMenuComponent,
|
TransfersMenuComponent,
|
||||||
DropZoneDirective,
|
DropZoneDirective,
|
||||||
|
CdkAutoDropGroup,
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
PromptModalComponent,
|
PromptModalComponent,
|
||||||
|
@ -121,6 +127,7 @@ const PROVIDERS = [
|
||||||
FastHtmlBindDirective,
|
FastHtmlBindDirective,
|
||||||
AlwaysVisibleTypeaheadDirective,
|
AlwaysVisibleTypeaheadDirective,
|
||||||
SortablejsModule,
|
SortablejsModule,
|
||||||
|
DragDropModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||||
|
|
|
@ -54,7 +54,9 @@ export class AppService {
|
||||||
private activeTabChange = new Subject<BaseTabComponent|null>()
|
private activeTabChange = new Subject<BaseTabComponent|null>()
|
||||||
private tabsChanged = new Subject<void>()
|
private tabsChanged = new Subject<void>()
|
||||||
private tabOpened = new Subject<BaseTabComponent>()
|
private tabOpened = new Subject<BaseTabComponent>()
|
||||||
|
private tabRemoved = new Subject<BaseTabComponent>()
|
||||||
private tabClosed = new Subject<BaseTabComponent>()
|
private tabClosed = new Subject<BaseTabComponent>()
|
||||||
|
private tabDragActive = new Subject<boolean>()
|
||||||
private ready = new AsyncSubject<void>()
|
private ready = new AsyncSubject<void>()
|
||||||
|
|
||||||
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
||||||
|
@ -62,7 +64,9 @@ export class AppService {
|
||||||
get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
|
get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
|
||||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||||
|
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||||
|
get tabDragActive$ (): Observable<boolean> { return this.tabDragActive }
|
||||||
|
|
||||||
/** Fires once when the app is ready */
|
/** Fires once when the app is ready */
|
||||||
get ready$ (): Observable<void> { return this.ready }
|
get ready$ (): Observable<void> { return this.ready }
|
||||||
|
@ -131,21 +135,30 @@ export class AppService {
|
||||||
})
|
})
|
||||||
|
|
||||||
tab.destroyed$.subscribe(() => {
|
tab.destroyed$.subscribe(() => {
|
||||||
const newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
this.removeTab(tab)
|
||||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
this.tabRemoved.next(tab)
|
||||||
if (tab === this._activeTab) {
|
|
||||||
this.selectTab(this.tabs[newIndex])
|
|
||||||
}
|
|
||||||
this.tabsChanged.next()
|
|
||||||
this.tabClosed.next(tab)
|
this.tabClosed.next(tab)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab instanceof SplitTabComponent) {
|
if (tab instanceof SplitTabComponent) {
|
||||||
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
|
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
|
||||||
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
|
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
|
||||||
|
tab.tabAdopted$.subscribe(t => {
|
||||||
|
this.removeTab(t)
|
||||||
|
this.tabRemoved.next(t)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeTab (tab: BaseTabComponent): void {
|
||||||
|
const newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||||
|
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||||
|
if (tab === this._activeTab) {
|
||||||
|
this.selectTab(this.tabs[newIndex])
|
||||||
|
}
|
||||||
|
this.tabsChanged.next()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new tab **without** wrapping it in a SplitTabComponent
|
* Adds a new tab **without** wrapping it in a SplitTabComponent
|
||||||
* @param inputs Properties to be assigned on the new tab component instance
|
* @param inputs Properties to be assigned on the new tab component instance
|
||||||
|
@ -344,6 +357,16 @@ export class AppService {
|
||||||
this.hostApp.emitReady()
|
this.hostApp.emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
emitTabDragStarted (): void {
|
||||||
|
this.tabDragActive.next(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
emitTabDragEnded (): void {
|
||||||
|
this.tabDragActive.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable that fires once
|
* Returns an observable that fires once
|
||||||
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
||||||
import * as angularCoreModule from '@angular/core'
|
import * as angularCoreModule from '@angular/core'
|
||||||
|
import * as angularCDKModule from '@angular/cdk'
|
||||||
import * as angularCompilerModule from '@angular/compiler'
|
import * as angularCompilerModule from '@angular/compiler'
|
||||||
import * as angularCommonModule from '@angular/common'
|
import * as angularCommonModule from '@angular/common'
|
||||||
import * as angularFormsModule from '@angular/forms'
|
import * as angularFormsModule from '@angular/forms'
|
||||||
|
@ -147,6 +148,7 @@ Tabby.registerModule('readline', {
|
||||||
})
|
})
|
||||||
|
|
||||||
Tabby.registerModule('@angular/core', angularCoreModule)
|
Tabby.registerModule('@angular/core', angularCoreModule)
|
||||||
|
Tabby.registerModule('@angular/cdk', angularCDKModule)
|
||||||
Tabby.registerModule('@angular/compiler', angularCompilerModule)
|
Tabby.registerModule('@angular/compiler', angularCompilerModule)
|
||||||
Tabby.registerModule('@angular/common', angularCommonModule)
|
Tabby.registerModule('@angular/common', angularCommonModule)
|
||||||
Tabby.registerModule('@angular/forms', angularFormsModule)
|
Tabby.registerModule('@angular/forms', angularFormsModule)
|
||||||
|
|
Loading…
Reference in a new issue