UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

294 lines (261 loc) • 11.5 kB
// ***************************************************************************** // Copyright (C) 2018 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { find, toArray } from '@lumino/algorithm'; import { TabBar, Widget, DockPanel, Title } from '@lumino/widgets'; import { Signal } from '@lumino/signaling'; import { Disposable, DisposableCollection } from '../../common/disposable'; import { CorePreferences } from '../../common/core-preferences'; import { Emitter, Event, environment } from '../../common'; import { ToolbarAwareTabBar } from './tab-bars'; export const ACTIVE_TABBAR_CLASS = 'theia-tabBar-active'; export const MAIN_AREA_ID = 'theia-main-content-panel'; export const BOTTOM_AREA_ID = 'theia-bottom-content-panel'; /** * This specialization of DockPanel adds various events that are used for implementing the * side panels of the application shell. */ export class TheiaDockPanel extends DockPanel { /** * Emitted when a widget is added to the panel. */ readonly widgetAdded = new Signal<this, Widget>(this); /** * Emitted when a widget is activated by calling `activateWidget`. */ readonly widgetActivated = new Signal<this, Widget>(this); /** * Emitted when a widget is removed from the panel. */ readonly widgetRemoved = new Signal<this, Widget>(this); protected readonly onDidChangeCurrentEmitter = new Emitter<Title<Widget> | undefined>(); protected disableDND: boolean | undefined = false; protected tabWithDNDDisabledStyling?: HTMLElement = undefined; get onDidChangeCurrent(): Event<Title<Widget> | undefined> { return this.onDidChangeCurrentEmitter.event; } constructor(options?: DockPanel.IOptions, protected readonly preferences?: CorePreferences, protected readonly maximizeCallback?: (area: TheiaDockPanel) => void ) { super(options); this.disableDND = TheiaDockPanel.isTheiaDockPanelIOptions(options) && options.disableDragAndDrop; this['_onCurrentChanged'] = (sender: TabBar<Widget>, args: TabBar.ICurrentChangedArgs<Widget>) => { this.markAsCurrent(args.currentTitle || undefined); super['_onCurrentChanged'](sender, args); }; this['_createTabBar'] = () => { // necessary for https://github.com/eclipse-theia/theia/issues/15273 const tabBar = super['_createTabBar'](); if (tabBar instanceof ToolbarAwareTabBar) { tabBar.setDockPanel(this); } if (this.disableDND) { tabBar['tabDetachRequested'].disconnect(this['_onTabDetachRequested'], this); tabBar['tabDetachRequested'].connect(this.onTabDetachRequestedWithDisabledDND, this); // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-null/no-null let dragDataValue: any = null; Object.defineProperty(tabBar, '_dragData', { get: () => dragDataValue, // eslint-disable-next-line @typescript-eslint/no-explicit-any set: (value: any) => { dragDataValue = value; // eslint-disable-next-line no-null/no-null if (value === null) { this.onNullTabDragDataWithDisabledDND(); } }, configurable: true }); } return tabBar; }; this['_onTabActivateRequested'] = (sender: TabBar<Widget>, args: TabBar.ITabActivateRequestedArgs<Widget>) => { this.markAsCurrent(args.title); super['_onTabActivateRequested'](sender, args); }; this['_onTabCloseRequested'] = (sender: TabBar<Widget>, args: TabBar.ITabCloseRequestedArgs<Widget>) => { if (TheiaDockPanel.isTheiaDockPanelIOptions(options) && options.closeHandler !== undefined) { if (options.closeHandler(sender, args)) { return; } } super['_onTabCloseRequested'](sender, args); }; } protected onTabDetachRequestedWithDisabledDND(sender: TabBar<Widget>, args: TabBar.ITabDetachRequestedArgs<Widget>): void { // don't process the detach request at all. We still want to support other drag starts, e.g. tab reorder // provide visual feedback that DnD is disabled by adding not-allowed class const tab = sender.contentNode.children[args.index] as HTMLElement; if (tab) { tab.classList.add('theia-drag-not-allowed'); this.tabWithDNDDisabledStyling = tab; } } protected onNullTabDragDataWithDisabledDND(): void { if (this.tabWithDNDDisabledStyling) { this.tabWithDNDDisabledStyling.classList.remove('theia-drag-not-allowed'); this.tabWithDNDDisabledStyling = undefined; } } override handleEvent(event: globalThis.Event): void { if (this.disableDND) { switch (event.type) { case 'lm-dragenter': case 'lm-dragleave': case 'lm-dragover': case 'lm-drop': /* no-op */ break; default: super.handleEvent(event); } } super.handleEvent(event); } toggleMaximized(): void { if (this.maximizeCallback) { this.maximizeCallback(this); } } isElectron(): boolean { return environment.electron.is(); } protected _currentTitle: Title<Widget> | undefined; get currentTitle(): Title<Widget> | undefined { return this._currentTitle; } get currentTabBar(): TabBar<Widget> | undefined { return this._currentTitle && this.findTabBar(this._currentTitle); } findTabBar(title: Title<Widget>): TabBar<Widget> | undefined { return find(this.tabBars(), bar => bar.titles.includes(title)); } protected readonly toDisposeOnMarkAsCurrent = new DisposableCollection(); markAsCurrent(title: Title<Widget> | undefined): void { this.toDisposeOnMarkAsCurrent.dispose(); this._currentTitle = title; this.markActiveTabBar(title); if (title) { const resetCurrent = () => this.markAsCurrent(undefined); title.owner.disposed.connect(resetCurrent); this.toDisposeOnMarkAsCurrent.push(Disposable.create(() => title.owner.disposed.disconnect(resetCurrent) )); } this.onDidChangeCurrentEmitter.fire(title); } markActiveTabBar(title?: Title<Widget>): void { const tabBars = toArray(this.tabBars()); tabBars.forEach(tabBar => tabBar.removeClass(ACTIVE_TABBAR_CLASS)); const activeTabBar = title && this.findTabBar(title); if (activeTabBar) { activeTabBar.addClass(ACTIVE_TABBAR_CLASS); } else if (tabBars.length > 0) { // At least one tabbar needs to be active tabBars[0].addClass(ACTIVE_TABBAR_CLASS); } } override addWidget(widget: Widget, options?: TheiaDockPanel.AddOptions): void { if (this.mode === 'single-document' && widget.parent === this) { return; } super.addWidget(widget, options); if (options?.closeRef) { options.ref?.close(); } this.widgetAdded.emit(widget); this.markActiveTabBar(widget.title); } override activateWidget(widget: Widget): void { super.activateWidget(widget); this.widgetActivated.emit(widget); this.markActiveTabBar(widget.title); } protected override onChildRemoved(msg: Widget.ChildMessage): void { super.onChildRemoved(msg); this.widgetRemoved.emit(msg.child); } nextTabBarWidget(widget: Widget): Widget | undefined { const current = this.findTabBar(widget.title); const next = current && this.nextTabBarInPanel(current); return next && next.currentTitle && next.currentTitle.owner || undefined; } nextTabBarInPanel(tabBar: TabBar<Widget>): TabBar<Widget> | undefined { const tabBars = toArray(this.tabBars()); const index = tabBars.indexOf(tabBar); if (index !== -1) { return tabBars[index + 1]; } return undefined; } previousTabBarWidget(widget: Widget): Widget | undefined { const current = this.findTabBar(widget.title); const previous = current && this.previousTabBarInPanel(current); return previous && previous.currentTitle && previous.currentTitle.owner || undefined; } previousTabBarInPanel(tabBar: TabBar<Widget>): TabBar<Widget> | undefined { const tabBars = toArray(this.tabBars()); const index = tabBars.indexOf(tabBar); if (index !== -1) { return tabBars[index - 1]; } return undefined; } override dispose(): void { super.dispose(); this.onDidChangeCurrentEmitter.dispose(); this._currentTitle = undefined; this.toDisposeOnMarkAsCurrent.dispose(); } } export namespace TheiaDockPanel { export const Factory = Symbol('TheiaDockPanel#Factory'); export interface Factory { (options?: DockPanel.IOptions | TheiaDockPanel.IOptions, maximizeCallback?: (area: TheiaDockPanel) => void): TheiaDockPanel; } export interface IOptions extends DockPanel.IOptions { /** whether drag and drop for tabs should be disabled */ disableDragAndDrop?: boolean; /** * @param sender the tab bar * @param args the widget (title) * @returns true if the request was handled by this handler, false if the tabbar should handle the request */ closeHandler?: (sender: TabBar<Widget>, args: TabBar.ITabCloseRequestedArgs<Widget>) => boolean; } export function isTheiaDockPanelIOptions(options: DockPanel.IOptions | undefined): options is IOptions { if (options === undefined) { return false; } if ('disableDragAndDrop' in options) { if (options.disableDragAndDrop !== undefined && typeof options.disableDragAndDrop !== 'boolean') { return false; } } if ('closeHandler' in options) { if (options.closeHandler !== undefined && typeof options.closeHandler !== 'function') { return false; } } return true; } export interface AddOptions extends DockPanel.IAddOptions { /** * Whether to also close the widget referenced by `ref`. */ closeRef?: boolean } }