Allow reordering tabs (fixes #82)

This commit is contained in:
Eugene Pankov 2018-08-07 08:51:19 +02:00
parent 0419900e1d
commit 538b5c4c28
15 changed files with 81 additions and 30 deletions

View File

@ -9,6 +9,8 @@ html
script(src='./preload.js') script(src='./preload.js')
script(src='./bundle.js', defer) script(src='./bundle.js', defer)
style#custom-css style#custom-css
style.
body { transition: 0.5s background; }
body(style='min-height: 100vh; overflow: hidden') body(style='min-height: 100vh; overflow: hidden')
app-root app-root
.preload-logo .preload-logo

View File

@ -66,7 +66,3 @@
[ngbradiogroup] input[type="radio"] { [ngbradiogroup] input[type="radio"] {
display: none; display: none;
} }
body {
background: #131d27;
}

View File

@ -25,6 +25,7 @@
"bootstrap": "4.0.0-alpha.6", "bootstrap": "4.0.0-alpha.6",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"electron-updater": "^2.8.9", "electron-updater": "^2.8.9",
"ng2-dnd": "^5.0.2",
"ngx-perfect-scrollbar": "^6.0.0" "ngx-perfect-scrollbar": "^6.0.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -10,16 +10,25 @@ title-bar(
*ngIf='!hostApp.isFullScreen', *ngIf='!hostApp.isFullScreen',
) )
.inset.background(*ngIf='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"') .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( tab-header(
*ngFor='let tab of app.tabs; let idx = index', *ngFor='let tab of app.tabs; let idx = index',
dnd-sortable,
[sortableIndex]='idx',
(onDragStart)='onTabDragStart()',
(onDragEnd)='onTabDragEnd()',
[index]='idx', [index]='idx',
[tab]='tab', [tab]='tab',
[active]='tab == app.activeTab', [active]='tab == app.activeTab',
[hasActivity]='tab.hasActivity', [hasActivity]='tab.hasActivity',
[class.drag-region]='hostApp.platform == Platform.macOS',
@animateTab, @animateTab,
(click)='app.selectTab(tab)', (click)='app.selectTab(tab)',
[class.fully-draggable]='hostApp.platform != Platform.macOS',
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
) )
.btn-group.background .btn-group.background
@ -54,10 +63,9 @@ title-bar(
start-page(*ngIf='ready && app.tabs.length == 0') start-page(*ngIf='ready && app.tabs.length == 0')
tab-body( tab-body(
*ngFor='let tab of app.tabs; trackBy: tab?.id', *ngFor='let tab of unsortedTabs',
[active]='tab == app.activeTab', [active]='tab == app.activeTab',
[tab]='tab', [tab]='tab',
[scrollable]='tab.scrollable',
) )
ng-template(ngbModalContainer) ng-template(ngbModalContainer)

View File

@ -13,6 +13,7 @@ import { ThemesService } from '../services/themes.service'
import { UpdaterService, Update } from '../services/updater.service' import { UpdaterService, Update } from '../services/updater.service'
import { TouchbarService } from '../services/touchbar.service' import { TouchbarService } from '../services/touchbar.service'
import { BaseTabComponent } from './baseTab.component'
import { SafeModeModalComponent } from './safeModeModal.component' import { SafeModeModalComponent } from './safeModeModal.component'
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api' import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
@ -55,6 +56,8 @@ export class AppRootComponent {
@Input() leftToolbarButtons: IToolbarButton[] @Input() leftToolbarButtons: IToolbarButton[]
@Input() rightToolbarButtons: IToolbarButton[] @Input() rightToolbarButtons: IToolbarButton[]
@HostBinding('class') hostClass = `platform-${process.platform}` @HostBinding('class') hostClass = `platform-${process.platform}`
tabsDragging = false
unsortedTabs: BaseTabComponent[] = []
private logger: Logger private logger: Logger
private appUpdate: Update private appUpdate: Update
@ -129,6 +132,11 @@ export class AppRootComponent {
config.changed$.subscribe(() => this.updateVibrancy()) config.changed$.subscribe(() => this.updateVibrancy())
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 () { onGlobalHotkey () {
@ -183,6 +191,17 @@ export class AppRootComponent {
this.electron.shell.openExternal(this.appUpdate.url) this.electron.shell.openExternal(this.appUpdate.url)
} }
onTabDragStart () {
this.tabsDragging = true
}
onTabDragEnd () {
setTimeout(() => {
this.tabsDragging = false
this.app.emitTabsChanged()
})
}
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] { private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
let buttons: IToolbarButton[] = [] let buttons: IToolbarButton[] = []
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => { this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {

View File

@ -7,7 +7,6 @@ export abstract class BaseTabComponent {
title: string title: string
titleChange$ = new Subject<string>() titleChange$ = new Subject<string>()
customTitle: string customTitle: string
scrollable: boolean
hasActivity = false hasActivity = false
focused$ = new Subject<void>() focused$ = new Subject<void>()
blurred$ = new Subject<void>() blurred$ = new Subject<void>()

View File

@ -4,10 +4,6 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&.scrollable {
overflow-y: auto;
}
&.active { &.active {
display: flex; display: flex;

View File

@ -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' import { BaseTabComponent } from '../components/baseTab.component'
@Component({ @Component({
selector: 'tab-body', selector: 'tab-body',
template: ` template: `
<perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable"> <!--perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
<ng-template #scrollablePlaceholder></ng-template> <ng-template #scrollablePlaceholder></ng-template>
</perfect-scrollbar> </perfect-scrollbar-->
<ng-template #nonScrollablePlaceholder *ngIf="!scrollable"></ng-template> <ng-template #placeholder></ng-template>
`, `,
styles: [ styles: [
require('./tabBody.component.scss'), require('./tabBody.component.scss'),
require('./tabBody.deep.component.css'), require('./tabBody.deep.component.css'),
], ],
}) })
export class TabBodyComponent { export class TabBodyComponent implements OnChanges {
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent @Input() tab: BaseTabComponent
@Input() scrollable: boolean @ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
@ViewChild('scrollablePlaceholder', {read: ViewContainerRef}) scrollablePlaceholder: ViewContainerRef
@ViewChild('nonScrollablePlaceholder', {read: ViewContainerRef}) nonScrollablePlaceholder: ViewContainerRef
ngAfterViewInit () { ngOnChanges (changes) {
setImmediate(() => { if (changes.tab) {
(this.scrollable ? this.scrollablePlaceholder : this.nonScrollablePlaceholder).insert(this.tab.hostView) if (this.placeholder) {
}) this.placeholder.detach()
}
setImmediate(() => {
this.placeholder.insert(this.tab.hostView)
})
}
}
ngOnDestroy () {
this.placeholder.detach()
} }
} }

View File

@ -1,3 +1,3 @@
.index {{index + 1}} .index(#handle) {{index + 1}}
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}} .name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
button((click)='app.closeTab(tab, true)') &times; button((click)='app.closeTab(tab, true)') &times;

View File

@ -17,6 +17,8 @@ $tabs-height: 36px;
.index { .index {
flex: none; flex: none;
font-weight: bold; font-weight: bold;
-webkit-app-region: no-drag;
cursor: grab;
margin-left: 10px; margin-left: 10px;
width: 20px; width: 20px;
@ -67,4 +69,8 @@ $tabs-height: 36px;
&.drag-region { &.drag-region {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
&.fully-draggable {
cursor: grab;
}
} }

View File

@ -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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { BaseTabComponent } from './baseTab.component' import { BaseTabComponent } from './baseTab.component'
import { RenameTabModalComponent } from './renameTabModal.component' import { RenameTabModalComponent } from './renameTabModal.component'
import { ElectronService } from '../services/electron.service' import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
@Component({ @Component({
selector: 'tab-header', selector: 'tab-header',
@ -15,13 +17,16 @@ export class TabHeaderComponent {
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() @HostBinding('class.has-activity') hasActivity: boolean @Input() @HostBinding('class.has-activity') hasActivity: boolean
@Input() tab: BaseTabComponent @Input() tab: BaseTabComponent
@ViewChild('handle') handle: ElementRef
private contextMenu: any private contextMenu: any
constructor ( constructor (
zone: NgZone, zone: NgZone,
electron: ElectronService, electron: ElectronService,
public app: AppService, public app: AppService,
private hostApp: HostAppService,
private ngbModal: NgbModal, private ngbModal: NgbModal,
private parentDraggable: SortableComponent,
) { ) {
this.contextMenu = electron.remote.Menu.buildFromTemplate([ 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 { @HostListener('dblclick') onDoubleClick (): void {
let modal = this.ngbModal.open(RenameTabModalComponent) let modal = this.ngbModal.open(RenameTabModalComponent)
modal.componentInstance.value = this.tab.customTitle || this.tab.title modal.componentInstance.value = this.tab.customTitle || this.tab.title

View File

@ -4,6 +4,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar' import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { DndModule } from 'ng2-dnd'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
import { ConfigService } from './services/config.service' import { ConfigService } from './services/config.service'
@ -35,6 +36,7 @@ import { StandardTheme, StandardCompactTheme } from './theme'
import { CoreConfigProvider } from './config' import { CoreConfigProvider } from './config'
import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'perfect-scrollbar/css/perfect-scrollbar.css'
import 'ng2-dnd/bundles/style.css'
const PROVIDERS = [ const PROVIDERS = [
AppService, AppService,
@ -62,6 +64,7 @@ const PROVIDERS = [
FormsModule, FormsModule,
NgbModule.forRoot(), NgbModule.forRoot(),
PerfectScrollbarModule, PerfectScrollbarModule,
DndModule.forRoot(),
], ],
declarations: [ declarations: [
AppRootComponent, AppRootComponent,

View File

@ -98,6 +98,10 @@ export class AppService {
} }
} }
emitTabsChanged () {
this.tabsChanged$.next()
}
async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise<void> { async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise<void> {
if (!this.tabs.includes(tab)) { if (!this.tabs.includes(tab)) {
return return
@ -105,9 +109,9 @@ export class AppService {
if (checkCanClose && !await tab.canClose()) { if (checkCanClose && !await tab.canClose()) {
return return
} }
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
this.tabs = this.tabs.filter((x) => x !== tab) this.tabs = this.tabs.filter((x) => x !== tab)
tab.destroy() tab.destroy()
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
if (tab === this.activeTab) { if (tab === this.activeTab) {
this.selectTab(this.tabs[newIndex]) this.selectTab(this.tabs[newIndex])
} }

View File

@ -111,7 +111,7 @@ body {
background: $body-bg; background: $body-bg;
&.vibrant { &.vibrant {
background: rgba($body-bg, 0.65); background: rgba(0,0,0,.4);
} }
} }

View File

@ -30,7 +30,6 @@ export class SettingsTabComponent extends BaseTabComponent {
super() super()
this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b)) this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
this.setTitle('Settings') this.setTitle('Settings')
this.scrollable = true
this.screens = this.docking.getScreens() this.screens = this.docking.getScreens()
this.settingsProviders = config.enabledServices(this.settingsProviders) this.settingsProviders = config.enabledServices(this.settingsProviders)
this.themes = config.enabledServices(this.themes) this.themes = config.enabledServices(this.themes)