mirror of
https://github.com/Eugeny/tabby
synced 2024-12-12 22:32:57 +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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^12.1.2",
|
||||
"@electron/remote": "1.2.0",
|
||||
"any-promise": "^1.3.0",
|
||||
"electron-config": "2.0.0",
|
||||
|
|
|
@ -2,6 +2,15 @@
|
|||
# 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":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f"
|
||||
|
@ -2558,6 +2567,11 @@ parse-json@^2.2.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
|
||||
.tabs(
|
||||
dnd-sortable-container,
|
||||
[sortableData]='app.tabs',
|
||||
cdkDropList,
|
||||
[cdkDropListOrientation]='(config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "bottom") ? "horizontal" : "vertical"',
|
||||
(cdkDropListDropped)='onTabsReordered($event)',
|
||||
cdkAutoDropGroup='app-tabs'
|
||||
)
|
||||
tab-header(
|
||||
*ngFor='let tab of app.tabs; let idx = index',
|
||||
dnd-sortable,
|
||||
[sortableIndex]='idx',
|
||||
(onDragStart)='onTabDragStart()',
|
||||
(onDragEnd)='onTabDragEnd()',
|
||||
cdkDrag,
|
||||
[cdkDragData]='tab',
|
||||
(cdkDragStarted)='onTabDragStart()',
|
||||
(cdkDragEnded)='onTabDragEnd()',
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
|
@ -30,7 +32,7 @@ title-bar(
|
|||
[@.disabled]='hasVerticalTabs()',
|
||||
(click)='app.selectTab(tab)',
|
||||
[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
|
||||
|
@ -109,6 +111,7 @@ title-bar(
|
|||
start-page.content-tab.content-tab-active(*ngIf='ready && app.tabs.length == 0')
|
||||
|
||||
tab-body.content-tab(
|
||||
#tabBodies,
|
||||
*ngFor='let tab of unsortedTabs',
|
||||
[class.content-tab-active]='tab == app.activeTab',
|
||||
[active]='tab == app.activeTab',
|
||||
|
|
|
@ -132,6 +132,14 @@ $side-tab-width: 200px;
|
|||
window-controls {
|
||||
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 {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* 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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
|
||||
import { HostAppService, Platform } from '../api/hostApp'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
|
@ -12,6 +13,7 @@ import { UpdaterService } from '../services/updater.service'
|
|||
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { TabBodyComponent } from './tabBody.component'
|
||||
import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
|
@ -57,7 +59,7 @@ export class AppRootComponent {
|
|||
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
||||
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
||||
@HostBinding('class.no-tabs') noTabs = true
|
||||
tabsDragging = false
|
||||
@ViewChildren(TabBodyComponent) tabBodies: TabBodyComponent[]
|
||||
unsortedTabs: BaseTabComponent[] = []
|
||||
updatesAvailable = false
|
||||
activeTransfers: FileTransfer[] = []
|
||||
|
@ -126,11 +128,18 @@ export class AppRootComponent {
|
|||
this.app.tabOpened$.subscribe(tab => {
|
||||
this.unsortedTabs.push(tab)
|
||||
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.noTabs = app.tabs.length === 0
|
||||
this.app.emitTabDragEnded()
|
||||
})
|
||||
|
||||
platform.fileTransferStarted$.subscribe(transfer => {
|
||||
|
@ -174,12 +183,12 @@ export class AppRootComponent {
|
|||
}
|
||||
|
||||
onTabDragStart () {
|
||||
this.tabsDragging = true
|
||||
this.app.emitTabDragStarted()
|
||||
}
|
||||
|
||||
onTabDragEnd () {
|
||||
setTimeout(() => {
|
||||
this.tabsDragging = false
|
||||
this.app.emitTabDragEnded()
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
@ -194,6 +203,11 @@ export class AppRootComponent {
|
|||
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[] {
|
||||
let buttons: ToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Observable, Subject } from 'rxjs'
|
||||
import { ViewRef } from '@angular/core'
|
||||
import { EmbeddedViewRef, ViewContainerRef, ViewRef } from '@angular/core'
|
||||
import { RecoveryToken } from '../api/tabRecovery'
|
||||
import { BaseComponent } from './base.component'
|
||||
|
||||
|
@ -52,6 +52,10 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||
* your tab state to be saved sooner
|
||||
*/
|
||||
protected recoveryStateChangedHint = new Subject<void>()
|
||||
protected viewContainer?: ViewContainerRef
|
||||
|
||||
/* @hidden */
|
||||
viewContainerEmbeddedRef?: EmbeddedViewRef<any>
|
||||
|
||||
private progressClearTimeout: number
|
||||
private titleChange = new Subject<string>()
|
||||
|
@ -61,6 +65,8 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||
private activity = new Subject<boolean>()
|
||||
private destroyed = new Subject<void>()
|
||||
|
||||
private _destroyCalled = false
|
||||
|
||||
get focused$ (): Observable<void> { return this.focused }
|
||||
get blurred$ (): Observable<void> { return this.blurred }
|
||||
get titleChange$ (): Observable<string> { return this.titleChange }
|
||||
|
@ -152,10 +158,29 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||
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
|
||||
*/
|
||||
destroy (skipDestroyedEvent = false): void {
|
||||
if (this._destroyCalled) {
|
||||
return
|
||||
}
|
||||
this._destroyCalled = true
|
||||
this.focused.complete()
|
||||
this.blurred.complete()
|
||||
this.titleChange.complete()
|
||||
|
@ -166,6 +191,7 @@ export abstract class BaseTabComponent extends BaseComponent {
|
|||
this.destroyed.next()
|
||||
}
|
||||
this.destroyed.complete()
|
||||
this.hostView.destroy()
|
||||
}
|
||||
|
||||
/** @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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* You'll mainly encounter it inside [[AppService]].tabs
|
||||
|
@ -137,6 +145,12 @@ export interface SplitSpannerInfo {
|
|||
[index]='spanner.index'
|
||||
(change)='onSpannerAdjusted(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')],
|
||||
})
|
||||
|
@ -157,6 +171,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
/** @hidden */
|
||||
_spanners: SplitSpannerInfo[] = []
|
||||
|
||||
/** @hidden */
|
||||
_dropZones: SplitDropZoneInfo[] = []
|
||||
|
||||
/** @hidden */
|
||||
_allFocusMode = false
|
||||
|
||||
|
@ -166,12 +183,19 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||
|
||||
private tabAdded = new Subject<BaseTabComponent>()
|
||||
private tabAdopted = new Subject<BaseTabComponent>()
|
||||
private tabRemoved = new Subject<BaseTabComponent>()
|
||||
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
||||
private focusChanged = new Subject<BaseTabComponent>()
|
||||
private initialized = new Subject<void>()
|
||||
|
||||
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 }
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||
tab.parent = this
|
||||
async add (thing: BaseTabComponent|SplitContainer, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> {
|
||||
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 insertIndex = relative ? target.children.indexOf(relative) : -1
|
||||
|
@ -362,14 +402,16 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
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)
|
||||
target.children.splice(insertIndex, 0, thing)
|
||||
|
||||
this.recoveryStateChangedHint.next()
|
||||
|
||||
await this.initialized$.toPromise()
|
||||
|
||||
this.attachTabView(tab)
|
||||
this.onAfterTabAdded(tab)
|
||||
for (const tab of thing instanceof SplitContainer ? thing.getAllTabs() : [thing]) {
|
||||
this.attachTabView(tab)
|
||||
this.onAfterTabAdded(tab)
|
||||
}
|
||||
}
|
||||
|
||||
removeTab (tab: BaseTabComponent): void {
|
||||
|
@ -381,8 +423,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
parent.ratios.splice(index, 1)
|
||||
parent.children.splice(index, 1)
|
||||
|
||||
this.detachTabView(tab)
|
||||
tab.removeFromContainer()
|
||||
tab.parent = null
|
||||
this.viewRefs.delete(tab)
|
||||
|
||||
this.layout()
|
||||
|
||||
|
@ -401,7 +444,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
}
|
||||
const position = parent.children.indexOf(tab)
|
||||
parent.children[position] = newTab
|
||||
this.detachTabView(tab)
|
||||
tab.removeFromContainer()
|
||||
this.attachTabView(newTab)
|
||||
tab.parent = null
|
||||
newTab.parent = this
|
||||
|
@ -508,6 +551,16 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
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 {
|
||||
super.destroy()
|
||||
for (const x of this.getAllTabs()) {
|
||||
|
@ -518,13 +571,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
layout (): void {
|
||||
this.root.normalize()
|
||||
this._spanners = []
|
||||
this._dropZones = []
|
||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
|
||||
|
||||
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) {
|
||||
setImmediate(() => {
|
||||
this.layout()
|
||||
|
@ -593,6 +638,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
element.style.width = '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]
|
||||
|
@ -612,6 +664,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
|
|||
root.ratios = state.ratios
|
||||
root.children = children
|
||||
for (const childState of state.children) {
|
||||
if (!childState) {
|
||||
continue
|
||||
}
|
||||
if (childState.type === 'app:split-tab') {
|
||||
const child = new SplitContainer()
|
||||
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 */
|
||||
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||
import { SelfPositioningComponent } from './selfPositioning.component'
|
||||
import { SplitContainer } from './splitTab.component'
|
||||
|
||||
/** @hidden */
|
||||
|
@ -8,20 +9,19 @@ import { SplitContainer } from './splitTab.component'
|
|||
template: '',
|
||||
styles: [require('./splitTabSpanner.component.scss')],
|
||||
})
|
||||
export class SplitTabSpannerComponent {
|
||||
export class SplitTabSpannerComponent extends SelfPositioningComponent {
|
||||
@Input() container: SplitContainer
|
||||
@Input() index: number
|
||||
@Output() change = new EventEmitter<void>()
|
||||
@HostBinding('class.active') isActive = false
|
||||
@HostBinding('class.h') isHorizontal = false
|
||||
@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
|
||||
|
||||
constructor (private element: ElementRef) { }
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (element: ElementRef) {
|
||||
super(element)
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.element.nativeElement.addEventListener('dblclick', () => {
|
||||
|
@ -92,11 +92,4 @@ export class SplitTabSpannerComponent {
|
|||
this.container.ratios[this.index] = ratio
|
||||
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 () {
|
||||
this.placeholder?.detach()
|
||||
}
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||
.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(
|
||||
[title]='tab.customTitle || tab.title',
|
||||
[class.no-hover]='config.store.terminal.hideCloseButton'
|
||||
) {{tab.customTitle || tab.title}}
|
||||
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 */
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef, NgZone } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
|
@ -13,11 +12,6 @@ import { BaseComponent } from './base.component'
|
|||
import { MenuItemOptions } from '../api/menu'
|
||||
import { PlatformService } from '../api/platform'
|
||||
|
||||
/** @hidden */
|
||||
export interface SortableComponentProxy {
|
||||
setDragHandle: (_: HTMLElement) => void
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
|
@ -29,17 +23,16 @@ export class TabHeaderComponent extends BaseComponent {
|
|||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() progress: number|null
|
||||
@ViewChild('handle') handle?: ElementRef
|
||||
Platform = Platform
|
||||
|
||||
constructor (
|
||||
public app: AppService,
|
||||
public config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
public hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private platform: PlatformService,
|
||||
private zone: NgZone,
|
||||
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
) {
|
||||
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 {
|
||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
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 { DndModule } from 'ng2-dnd'
|
||||
import { SortablejsModule } from 'ngx-sortablejs'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
|
||||
import { AppRootComponent } from './components/appRoot.component'
|
||||
import { CheckboxComponent } from './components/checkbox.component'
|
||||
|
@ -22,6 +23,7 @@ import { RenameTabModalComponent } from './components/renameTabModal.component'
|
|||
import { SelectorModalComponent } from './components/selectorModal.component'
|
||||
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||
import { SplitTabDropZoneComponent } from './components/splitTabDropZone.component'
|
||||
import { UnlockVaultModalComponent } from './components/unlockVaultModal.component'
|
||||
import { WelcomeTabComponent } from './components/welcomeTab.component'
|
||||
import { TransfersMenuComponent } from './components/transfersMenu.component'
|
||||
|
@ -30,6 +32,7 @@ import { AutofocusDirective } from './directives/autofocus.directive'
|
|||
import { AlwaysVisibleTypeaheadDirective } from './directives/alwaysVisibleTypeahead.directive'
|
||||
import { FastHtmlBindDirective } from './directives/fastHtmlBind.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'
|
||||
|
||||
|
@ -78,6 +81,7 @@ const PROVIDERS = [
|
|||
NgxFilesizeModule,
|
||||
PerfectScrollbarModule,
|
||||
DndModule.forRoot(),
|
||||
DragDropModule,
|
||||
SortablejsModule.forRoot({ animation: 150 }),
|
||||
],
|
||||
declarations: [
|
||||
|
@ -98,10 +102,12 @@ const PROVIDERS = [
|
|||
SelectorModalComponent,
|
||||
SplitTabComponent,
|
||||
SplitTabSpannerComponent,
|
||||
SplitTabDropZoneComponent,
|
||||
UnlockVaultModalComponent,
|
||||
WelcomeTabComponent,
|
||||
TransfersMenuComponent,
|
||||
DropZoneDirective,
|
||||
CdkAutoDropGroup,
|
||||
],
|
||||
entryComponents: [
|
||||
PromptModalComponent,
|
||||
|
@ -121,6 +127,7 @@ const PROVIDERS = [
|
|||
FastHtmlBindDirective,
|
||||
AlwaysVisibleTypeaheadDirective,
|
||||
SortablejsModule,
|
||||
DragDropModule,
|
||||
],
|
||||
})
|
||||
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 tabsChanged = new Subject<void>()
|
||||
private tabOpened = new Subject<BaseTabComponent>()
|
||||
private tabRemoved = new Subject<BaseTabComponent>()
|
||||
private tabClosed = new Subject<BaseTabComponent>()
|
||||
private tabDragActive = new Subject<boolean>()
|
||||
private ready = new AsyncSubject<void>()
|
||||
|
||||
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
||||
|
@ -62,7 +64,9 @@ export class AppService {
|
|||
get activeTabChange$ (): Observable<BaseTabComponent|null> { return this.activeTabChange }
|
||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||
get tabDragActive$ (): Observable<boolean> { return this.tabDragActive }
|
||||
|
||||
/** Fires once when the app is ready */
|
||||
get ready$ (): Observable<void> { return this.ready }
|
||||
|
@ -131,21 +135,30 @@ export class AppService {
|
|||
})
|
||||
|
||||
tab.destroyed$.subscribe(() => {
|
||||
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()
|
||||
this.removeTab(tab)
|
||||
this.tabRemoved.next(tab)
|
||||
this.tabClosed.next(tab)
|
||||
})
|
||||
|
||||
if (tab instanceof SplitTabComponent) {
|
||||
tab.tabAdded$.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
|
||||
* @param inputs Properties to be assigned on the new tab component instance
|
||||
|
@ -344,6 +357,16 @@ export class AppService {
|
|||
this.hostApp.emitReady()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabDragStarted (): void {
|
||||
this.tabDragActive.next(true)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabDragEnded (): void {
|
||||
this.tabDragActive.next(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that fires once
|
||||
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
||||
import * as angularCoreModule from '@angular/core'
|
||||
import * as angularCDKModule from '@angular/cdk'
|
||||
import * as angularCompilerModule from '@angular/compiler'
|
||||
import * as angularCommonModule from '@angular/common'
|
||||
import * as angularFormsModule from '@angular/forms'
|
||||
|
@ -147,6 +148,7 @@ Tabby.registerModule('readline', {
|
|||
})
|
||||
|
||||
Tabby.registerModule('@angular/core', angularCoreModule)
|
||||
Tabby.registerModule('@angular/cdk', angularCDKModule)
|
||||
Tabby.registerModule('@angular/compiler', angularCompilerModule)
|
||||
Tabby.registerModule('@angular/common', angularCommonModule)
|
||||
Tabby.registerModule('@angular/forms', angularFormsModule)
|
||||
|
|
Loading…
Reference in a new issue