mirror of
https://github.com/Eugeny/tabby
synced 2024-12-04 18:40:16 +00:00
Allow reordering tabs (fixes #82)
This commit is contained in:
parent
0419900e1d
commit
538b5c4c28
15 changed files with 81 additions and 30 deletions
|
@ -9,6 +9,8 @@ html
|
|||
script(src='./preload.js')
|
||||
script(src='./bundle.js', defer)
|
||||
style#custom-css
|
||||
style.
|
||||
body { transition: 0.5s background; }
|
||||
body(style='min-height: 100vh; overflow: hidden')
|
||||
app-root
|
||||
.preload-logo
|
||||
|
|
|
@ -66,7 +66,3 @@
|
|||
[ngbradiogroup] input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #131d27;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"bootstrap": "4.0.0-alpha.6",
|
||||
"core-js": "^2.4.1",
|
||||
"electron-updater": "^2.8.9",
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-perfect-scrollbar": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -10,16 +10,25 @@ title-bar(
|
|||
*ngIf='!hostApp.isFullScreen',
|
||||
)
|
||||
.inset.background(*ngIf='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"')
|
||||
.tabs
|
||||
.tabs(
|
||||
dnd-sortable-container,
|
||||
[sortableData]='app.tabs',
|
||||
)
|
||||
tab-header(
|
||||
*ngFor='let tab of app.tabs; let idx = index',
|
||||
dnd-sortable,
|
||||
[sortableIndex]='idx',
|
||||
(onDragStart)='onTabDragStart()',
|
||||
(onDragEnd)='onTabDragEnd()',
|
||||
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
[hasActivity]='tab.hasActivity',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS',
|
||||
@animateTab,
|
||||
(click)='app.selectTab(tab)',
|
||||
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
|
||||
)
|
||||
|
||||
.btn-group.background
|
||||
|
@ -54,10 +63,9 @@ title-bar(
|
|||
start-page(*ngIf='ready && app.tabs.length == 0')
|
||||
|
||||
tab-body(
|
||||
*ngFor='let tab of app.tabs; trackBy: tab?.id',
|
||||
*ngFor='let tab of unsortedTabs',
|
||||
[active]='tab == app.activeTab',
|
||||
[tab]='tab',
|
||||
[scrollable]='tab.scrollable',
|
||||
)
|
||||
|
||||
ng-template(ngbModalContainer)
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ThemesService } from '../services/themes.service'
|
|||
import { UpdaterService, Update } from '../services/updater.service'
|
||||
import { TouchbarService } from '../services/touchbar.service'
|
||||
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
|
@ -55,6 +56,8 @@ export class AppRootComponent {
|
|||
@Input() leftToolbarButtons: IToolbarButton[]
|
||||
@Input() rightToolbarButtons: IToolbarButton[]
|
||||
@HostBinding('class') hostClass = `platform-${process.platform}`
|
||||
tabsDragging = false
|
||||
unsortedTabs: BaseTabComponent[] = []
|
||||
private logger: Logger
|
||||
private appUpdate: Update
|
||||
|
||||
|
@ -129,6 +132,11 @@ export class AppRootComponent {
|
|||
|
||||
config.changed$.subscribe(() => this.updateVibrancy())
|
||||
this.updateVibrancy()
|
||||
|
||||
this.app.tabOpened$.subscribe(tab => this.unsortedTabs.push(tab))
|
||||
this.app.tabClosed$.subscribe(tab => {
|
||||
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
||||
})
|
||||
}
|
||||
|
||||
onGlobalHotkey () {
|
||||
|
@ -183,6 +191,17 @@ export class AppRootComponent {
|
|||
this.electron.shell.openExternal(this.appUpdate.url)
|
||||
}
|
||||
|
||||
onTabDragStart () {
|
||||
this.tabsDragging = true
|
||||
}
|
||||
|
||||
onTabDragEnd () {
|
||||
setTimeout(() => {
|
||||
this.tabsDragging = false
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
|
|
|
@ -7,7 +7,6 @@ export abstract class BaseTabComponent {
|
|||
title: string
|
||||
titleChange$ = new Subject<string>()
|
||||
customTitle: string
|
||||
scrollable: boolean
|
||||
hasActivity = false
|
||||
focused$ = new Subject<void>()
|
||||
blurred$ = new Subject<void>()
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.scrollable {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
|
||||
|
|
|
@ -1,29 +1,36 @@
|
|||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef } from '@angular/core'
|
||||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-body',
|
||||
template: `
|
||||
<perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
|
||||
<!--perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
|
||||
<ng-template #scrollablePlaceholder></ng-template>
|
||||
</perfect-scrollbar>
|
||||
<ng-template #nonScrollablePlaceholder *ngIf="!scrollable"></ng-template>
|
||||
</perfect-scrollbar-->
|
||||
<ng-template #placeholder></ng-template>
|
||||
`,
|
||||
styles: [
|
||||
require('./tabBody.component.scss'),
|
||||
require('./tabBody.deep.component.css'),
|
||||
],
|
||||
})
|
||||
export class TabBodyComponent {
|
||||
export class TabBodyComponent implements OnChanges {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() scrollable: boolean
|
||||
@ViewChild('scrollablePlaceholder', {read: ViewContainerRef}) scrollablePlaceholder: ViewContainerRef
|
||||
@ViewChild('nonScrollablePlaceholder', {read: ViewContainerRef}) nonScrollablePlaceholder: ViewContainerRef
|
||||
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
|
||||
|
||||
ngAfterViewInit () {
|
||||
setImmediate(() => {
|
||||
(this.scrollable ? this.scrollablePlaceholder : this.nonScrollablePlaceholder).insert(this.tab.hostView)
|
||||
})
|
||||
ngOnChanges (changes) {
|
||||
if (changes.tab) {
|
||||
if (this.placeholder) {
|
||||
this.placeholder.detach()
|
||||
}
|
||||
setImmediate(() => {
|
||||
this.placeholder.insert(this.tab.hostView)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.placeholder.detach()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.index {{index + 1}}
|
||||
.index(#handle) {{index + 1}}
|
||||
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
|
||||
button((click)='app.closeTab(tab, true)') ×
|
||||
|
|
|
@ -17,6 +17,8 @@ $tabs-height: 36px;
|
|||
.index {
|
||||
flex: none;
|
||||
font-weight: bold;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: grab;
|
||||
|
||||
margin-left: 10px;
|
||||
width: 20px;
|
||||
|
@ -67,4 +69,8 @@ $tabs-height: 36px;
|
|||
&.drag-region {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
&.fully-draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Component, Input, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
|
@ -15,13 +17,16 @@ export class TabHeaderComponent {
|
|||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() @HostBinding('class.has-activity') hasActivity: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@ViewChild('handle') handle: ElementRef
|
||||
private contextMenu: any
|
||||
|
||||
constructor (
|
||||
zone: NgZone,
|
||||
electron: ElectronService,
|
||||
public app: AppService,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private parentDraggable: SortableComponent,
|
||||
) {
|
||||
this.contextMenu = electron.remote.Menu.buildFromTemplate([
|
||||
{
|
||||
|
@ -65,6 +70,12 @@ export class TabHeaderComponent {
|
|||
])
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
if (this.hostApp.platform !== Platform.macOS) {
|
||||
this.parentDraggable.setDragHandle(this.handle.nativeElement)
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
let modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
|
|
|
@ -4,6 +4,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
|||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
||||
import { DndModule } from 'ng2-dnd'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
|
@ -35,6 +36,7 @@ import { StandardTheme, StandardCompactTheme } from './theme'
|
|||
import { CoreConfigProvider } from './config'
|
||||
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||
import 'ng2-dnd/bundles/style.css'
|
||||
|
||||
const PROVIDERS = [
|
||||
AppService,
|
||||
|
@ -62,6 +64,7 @@ const PROVIDERS = [
|
|||
FormsModule,
|
||||
NgbModule.forRoot(),
|
||||
PerfectScrollbarModule,
|
||||
DndModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
AppRootComponent,
|
||||
|
|
|
@ -98,6 +98,10 @@ export class AppService {
|
|||
}
|
||||
}
|
||||
|
||||
emitTabsChanged () {
|
||||
this.tabsChanged$.next()
|
||||
}
|
||||
|
||||
async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise<void> {
|
||||
if (!this.tabs.includes(tab)) {
|
||||
return
|
||||
|
@ -105,9 +109,9 @@ export class AppService {
|
|||
if (checkCanClose && !await tab.canClose()) {
|
||||
return
|
||||
}
|
||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||
tab.destroy()
|
||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
if (tab === this.activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@ body {
|
|||
background: $body-bg;
|
||||
|
||||
&.vibrant {
|
||||
background: rgba($body-bg, 0.65);
|
||||
background: rgba(0,0,0,.4);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ export class SettingsTabComponent extends BaseTabComponent {
|
|||
super()
|
||||
this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.setTitle('Settings')
|
||||
this.scrollable = true
|
||||
this.screens = this.docking.getScreens()
|
||||
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
||||
this.themes = config.enabledServices(this.themes)
|
||||
|
|
Loading…
Reference in a new issue