From 5b78a5c1edd8f0ac4fd305bc1cbf6248b5620a32 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Mon, 7 Jan 2019 19:30:03 +0300 Subject: [PATCH] made tab context menu extensible --- terminus-core/src/api/index.ts | 1 + .../src/api/tabContextMenuProvider.ts | 8 + .../src/components/tabHeader.component.ts | 118 ++------------- terminus-core/src/index.ts | 5 + terminus-core/src/tabContextMenu.ts | 139 ++++++++++++++++++ .../src/components/terminalTab.component.ts | 16 -- terminus-terminal/src/index.ts | 5 +- terminus-terminal/src/tabContextMenu.ts | 41 ++++++ 8 files changed, 212 insertions(+), 121 deletions(-) create mode 100644 terminus-core/src/api/tabContextMenuProvider.ts create mode 100644 terminus-core/src/tabContextMenu.ts create mode 100644 terminus-terminal/src/tabContextMenu.ts diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index 8c533ba8..6cb04371 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -4,6 +4,7 @@ export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider' export { ConfigProvider } from './configProvider' export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider' export { Theme } from './theme' +export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { AppService } from '../services/app.service' export { ConfigService } from '../services/config.service' diff --git a/terminus-core/src/api/tabContextMenuProvider.ts b/terminus-core/src/api/tabContextMenuProvider.ts new file mode 100644 index 00000000..69953fc0 --- /dev/null +++ b/terminus-core/src/api/tabContextMenuProvider.ts @@ -0,0 +1,8 @@ +import { BaseTabComponent } from '../components/baseTab.component' +import { TabHeaderComponent } from '../components/tabHeader.component' + +export abstract class TabContextMenuItemProvider { + weight = 0 + + abstract async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise +} diff --git a/terminus-core/src/components/tabHeader.component.ts b/terminus-core/src/components/tabHeader.component.ts index 18fa92b1..ae7458eb 100644 --- a/terminus-core/src/components/tabHeader.component.ts +++ b/terminus-core/src/components/tabHeader.component.ts @@ -1,6 +1,7 @@ -import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core' +import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core' import { SortableComponent } from 'ng2-dnd' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider' import { BaseTabComponent } from './baseTab.component' import { RenameTabModalComponent } from './renameTabModal.component' import { HotkeysService } from '../services/hotkeys.service' @@ -8,16 +9,6 @@ import { ElectronService } from '../services/electron.service' import { AppService } from '../services/app.service' import { HostAppService, Platform } from '../services/hostApp.service' -const COLORS = [ - { name: 'No color', value: null }, - { name: 'Blue', value: '#0275d8' }, - { name: 'Green', value: '#5cb85c' }, - { name: 'Orange', value: '#f0ad4e' }, - { name: 'Purple', value: '#613d7c' }, - { name: 'Red', value: '#d9534f' }, - { name: 'Yellow', value: '#ffd500' }, -] - @Component({ selector: 'tab-header', template: require('./tabHeader.component.pug'), @@ -31,16 +22,14 @@ export class TabHeaderComponent { @Input() progress: number @ViewChild('handle') handle: ElementRef - private completionNotificationEnabled = false - constructor ( public app: AppService, private electron: ElectronService, - private zone: NgZone, private hostApp: HostAppService, private ngbModal: NgbModal, private hotkeys: HotkeysService, private parentDraggable: SortableComponent, + @Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[], ) { this.hotkeys.matchedHotkey.subscribe((hotkey) => { if (this.app.activeTab === this.tab) { @@ -49,6 +38,7 @@ export class TabHeaderComponent { } } }) + this.contextMenuProviders.sort((a, b) => a.weight - b.weight) } ngOnInit () { @@ -69,6 +59,15 @@ export class TabHeaderComponent { }).catch(() => null) } + async buildContextMenu (): Promise { + let items: Electron.MenuItemConstructorOptions[] = [] + for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) { + items.push({ type: 'separator' }) + items = items.concat(section) + } + return items.slice(1) + } + @HostListener('dblclick') onDoubleClick (): void { this.showRenameTabModal() } @@ -80,96 +79,7 @@ export class TabHeaderComponent { if ($event.which === 3) { event.preventDefault() - let contextMenu = this.electron.remote.Menu.buildFromTemplate([ - { - label: 'Close', - click: () => this.zone.run(() => { - this.app.closeTab(this.tab, true) - }) - }, - { - label: 'Close other tabs', - click: () => this.zone.run(() => { - for (let tab of this.app.tabs.filter(x => x !== this.tab)) { - this.app.closeTab(tab, true) - } - }) - }, - { - label: 'Close tabs to the right', - click: () => this.zone.run(() => { - for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) { - this.app.closeTab(tab, true) - } - }) - }, - { - label: 'Close tabs to the left', - click: () => this.zone.run(() => { - for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) { - this.app.closeTab(tab, true) - } - }) - }, - { - label: 'Rename', - click: () => this.zone.run(() => this.showRenameTabModal()) - }, - { - label: 'Color', - sublabel: COLORS.find(x => x.value === this.tab.color).name, - submenu: COLORS.map(color => ({ - label: color.name, - type: 'radio', - checked: this.tab.color === color.value, - click: () => this.zone.run(() => { - this.tab.color = color.value - }), - })), - } - ]) - - if ((this.tab as any).saveAsProfile) { - contextMenu.append(new this.electron.MenuItem({ - label: 'Save as a profile', - click: () => this.zone.run(() => (this.tab as any).saveAsProfile()) - })) - } - - let process = await this.tab.getCurrentProcess() - if (process) { - contextMenu.append(new this.electron.MenuItem({ - id: 'sep', - type: 'separator', - })) - contextMenu.append(new this.electron.MenuItem({ - id: 'process-name', - enabled: false, - label: 'Current process: ' + process.name, - })) - contextMenu.append(new this.electron.MenuItem({ - id: 'completion', - label: 'Notify when done', - type: 'checkbox', - checked: this.completionNotificationEnabled, - click: () => this.zone.run(() => { - this.completionNotificationEnabled = !this.completionNotificationEnabled - - if (this.completionNotificationEnabled) { - this.app.observeTabCompletion(this.tab).subscribe(() => { - new Notification('Process completed', { - body: process.name, - }).addEventListener('click', () => { - this.app.selectTab(this.tab) - }) - this.completionNotificationEnabled = false - }) - } else { - this.app.stopObservingTabCompletion(this.tab) - } - }) - })) - } + const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu()) contextMenu.popup({ x: $event.pageX, diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 2d3ae6c4..e49223d6 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -24,9 +24,11 @@ import { AutofocusDirective } from './directives/autofocus.directive' import { HotkeyProvider } from './api/hotkeyProvider' import { ConfigProvider } from './api/configProvider' import { Theme } from './api/theme' +import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { CoreConfigProvider } from './config' +import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu' import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'ng2-dnd/bundles/style.css' @@ -37,6 +39,9 @@ const PROVIDERS = [ { provide: Theme, useClass: StandardCompactTheme, multi: true }, { provide: Theme, useClass: PaperTheme, multi: true }, { provide: ConfigProvider, useClass: CoreConfigProvider, multi: true }, + { provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true }, + { provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true }, + { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } } ] diff --git a/terminus-core/src/tabContextMenu.ts b/terminus-core/src/tabContextMenu.ts new file mode 100644 index 00000000..726f99f6 --- /dev/null +++ b/terminus-core/src/tabContextMenu.ts @@ -0,0 +1,139 @@ +import { Injectable, NgZone } from '@angular/core' +import { AppService } from './services/app.service' +import { BaseTabComponent } from './components/baseTab.component' +import { TabHeaderComponent } from './components/tabHeader.component' +import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' + +@Injectable() +export class CloseContextMenu extends TabContextMenuItemProvider { + weight = -5 + + constructor ( + private app: AppService, + private zone: NgZone, + ) { + super() + } + + async getItems (tab: BaseTabComponent): Promise { + return [ + { + label: 'Close', + click: () => this.zone.run(() => { + this.app.closeTab(tab, true) + }) + }, + { + label: 'Close other tabs', + click: () => this.zone.run(() => { + for (let t of this.app.tabs.filter(x => x !== tab)) { + this.app.closeTab(t, true) + } + }) + }, + { + label: 'Close tabs to the right', + click: () => this.zone.run(() => { + for (let t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) { + this.app.closeTab(t, true) + } + }) + }, + { + label: 'Close tabs to the left', + click: () => this.zone.run(() => { + for (let t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) { + this.app.closeTab(t, true) + } + }) + }, + ] + } +} + +const COLORS = [ + { name: 'No color', value: null }, + { name: 'Blue', value: '#0275d8' }, + { name: 'Green', value: '#5cb85c' }, + { name: 'Orange', value: '#f0ad4e' }, + { name: 'Purple', value: '#613d7c' }, + { name: 'Red', value: '#d9534f' }, + { name: 'Yellow', value: '#ffd500' }, +] + +@Injectable() +export class CommonOptionsContextMenu extends TabContextMenuItemProvider { + weight = -1 + + constructor ( + private zone: NgZone, + ) { + super() + } + + async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise { + return [ + { + label: 'Rename', + click: () => this.zone.run(() => tabHeader.showRenameTabModal()) + }, + { + label: 'Color', + sublabel: COLORS.find(x => x.value === tab.color).name, + submenu: COLORS.map(color => ({ + label: color.name, + type: 'radio', + checked: tab.color === color.value, + click: () => this.zone.run(() => { + tab.color = color.value + }), + })) as Electron.MenuItemConstructorOptions[], + } + ] + } +} + +@Injectable() +export class TaskCompletionContextMenu extends TabContextMenuItemProvider { + constructor ( + private app: AppService, + private zone: NgZone, + ) { + super() + } + + async getItems (tab: BaseTabComponent): Promise { + let process = await tab.getCurrentProcess() + if (process) { + return [ + { + id: 'process-name', + enabled: false, + label: 'Current process: ' + process.name, + }, + { + label: 'Notify when done', + type: 'checkbox', + checked: (tab as any).__completionNotificationEnabled, + click: () => this.zone.run(() => { + ;(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled + + if ((tab as any).__completionNotificationEnabled) { + this.app.observeTabCompletion(tab).subscribe(() => { + new Notification('Process completed', { + body: process.name, + }).addEventListener('click', () => { + this.app.selectTab(tab) + }) + ;(tab as any).__completionNotificationEnabled = false + }) + } else { + this.app.stopObservingTabCompletion(tab) + } + }) + }, + ] + } + return [] + } +} diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index 58f20d81..59e2e18b 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -72,20 +72,4 @@ export class TerminalTabComponent extends BaseTerminalTabComponent { } )).response === 1 } - - async saveAsProfile () { - let profile = { - sessionOptions: { - ...this.sessionOptions, - cwd: (await this.session.getWorkingDirectory()) || this.sessionOptions.cwd, - }, - name: this.sessionOptions.command, - } - this.config.store.terminal.profiles = [ - ...this.config.store.terminal.profiles, - profile, - ] - this.config.save() - this.toastr.info('Saved') - } } diff --git a/terminus-terminal/src/index.ts b/terminus-terminal/src/index.ts index f8cd4c7d..6ccfc62a 100644 --- a/terminus-terminal/src/index.ts +++ b/terminus-terminal/src/index.ts @@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { ToastrModule } from 'ngx-toastr' -import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core' +import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService, TabContextMenuItemProvider } from 'terminus-core' import { SettingsTabProvider } from 'terminus-settings' import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component' @@ -32,6 +32,7 @@ import { TerminalConfigProvider } from './config' import { TerminalHotkeyProvider } from './hotkeys' import { HyperColorSchemes } from './colorSchemes' import { NewTabContextMenu, CopyPasteContextMenu } from './contextMenu' +import { SaveAsProfileContextMenu } from './tabContextMenu' import { CmderShellProvider } from './shells/cmder' import { CustomShellProvider } from './shells/custom' @@ -84,6 +85,8 @@ import { hterm } from './hterm' { provide: TerminalContextMenuItemProvider, useClass: NewTabContextMenu, multi: true }, { provide: TerminalContextMenuItemProvider, useClass: CopyPasteContextMenu, multi: true }, + { provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true }, + // For WindowsDefaultShellProvider PowerShellCoreShellProvider, WSLShellProvider, diff --git a/terminus-terminal/src/tabContextMenu.ts b/terminus-terminal/src/tabContextMenu.ts new file mode 100644 index 00000000..5010c414 --- /dev/null +++ b/terminus-terminal/src/tabContextMenu.ts @@ -0,0 +1,41 @@ +import { Injectable, NgZone } from '@angular/core' +import { ToastrService } from 'ngx-toastr' +import { ConfigService, BaseTabComponent, TabContextMenuItemProvider } from 'terminus-core' +import { TerminalTabComponent } from './components/terminalTab.component' + +@Injectable() +export class SaveAsProfileContextMenu extends TabContextMenuItemProvider { + constructor ( + private config: ConfigService, + private zone: NgZone, + private toastr: ToastrService, + ) { + super() + } + + async getItems (tab: BaseTabComponent): Promise { + if (!(tab instanceof TerminalTabComponent)) { + return [] + } + return [ + { + label: 'Save as profile', + click: () => this.zone.run(async () => { + let profile = { + sessionOptions: { + ...tab.sessionOptions, + cwd: (await tab.session.getWorkingDirectory()) || tab.sessionOptions.cwd, + }, + name: tab.sessionOptions.command, + } + this.config.store.terminal.profiles = [ + ...this.config.store.terminal.profiles, + profile, + ] + this.config.save() + this.toastr.info('Saved') + }) + } + ] + } +} -- GitLab