Skip to content

Commit

Permalink
Add "open tabs" drop down
Browse files Browse the repository at this point in the history
Adds a drop down allowing to chose among open editors when there is too
little space to show all open tabs.

Part of eclipse-theia#12328

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Apr 20, 2023
1 parent a9471e7 commit 709b29b
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 83 deletions.
154 changes: 93 additions & 61 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import { IDragEvent } from '@phosphor/dragdrop';
import { LOCKED_CLASS, PINNED_CLASS } from '../widgets/widget';
import { CorePreferences } from '../core-preferences';
import { HoverService } from '../hover-service';
import { Root, createRoot } from 'react-dom/client';
import { SelectComponent } from '../widgets/select-component';
import { createElement } from 'react';

/** The class name added to hidden content nodes, which are required to render vertical side bars. */
const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content';
Expand Down Expand Up @@ -228,7 +231,7 @@ export class TabBarRenderer extends TabBar.Renderer {
} else {
width = '';
}
return { zIndex, height, width };
return { zIndex, height, minWidth: width, maxWidth: width };
}

/**
Expand Down Expand Up @@ -575,13 +578,6 @@ export class TabBarRenderer extends TabBar.Renderer {

}

export namespace ScrollableTabBar {
export interface Options {
minimumTabSize: number;
defaultTabSize: number;
}
}

/**
* A specialized tab bar for the main and bottom areas.
*/
Expand All @@ -595,13 +591,18 @@ export class ScrollableTabBar extends TabBar<Widget> {
protected needsRecompute = false;
protected tabSize = 0;
private _dynamicTabOptions?: ScrollableTabBar.Options;
protected contentContainer: HTMLElement;
protected topRow: HTMLElement;

protected readonly toDispose = new DisposableCollection();
protected openTabsContainer: HTMLDivElement;
protected openTabsRoot: Root;

constructor(options?: TabBar.IOptions<Widget> & PerfectScrollbar.Options, dynamicTabOptions?: ScrollableTabBar.Options) {
super(options);
this.scrollBarFactory = () => new PerfectScrollbar(this.scrollbarHost, options);
this._dynamicTabOptions = dynamicTabOptions;
this.rewireDOM();
}

set dynamicTabOptions(options: ScrollableTabBar.Options | undefined) {
Expand All @@ -621,6 +622,35 @@ export class ScrollableTabBar extends TabBar<Widget> {
this.toDispose.dispose();
}

/**
* Restructures the DOM defined in PhosphorJS.
*
* By default the tabs (`li`) are contained in the `this.contentNode` (`ul`) which is wrapped in a `div` (`this.node`).
* Instead of this structure, we add a container for the `this.contentNode` and for the toolbar.
* The scrollbar will only work for the `ul` part but it does not affect the toolbar, so it can be on the right hand-side.
*/
private rewireDOM(): void {
const contentNode = this.node.getElementsByClassName(ScrollableTabBar.Styles.TAB_BAR_CONTENT)[0];
if (!contentNode) {
throw new Error("'this.node' does not have the content as a direct child with class name 'p-TabBar-content'.");
}
this.node.removeChild(contentNode);
this.contentContainer = document.createElement('div');
this.contentContainer.classList.add(ScrollableTabBar.Styles.TAB_BAR_CONTENT_CONTAINER);
this.contentContainer.appendChild(contentNode);

this.topRow = document.createElement('div');
this.topRow.classList.add('theia-tabBar-tab-row');
this.topRow.appendChild(this.contentContainer);

this.openTabsContainer = document.createElement('div');
this.openTabsContainer.classList.add('theia-tabBar-open-tabs');
this.openTabsRoot = createRoot(this.openTabsContainer);
this.topRow.appendChild(this.openTabsContainer);

this.node.appendChild(this.topRow);
}

protected override onAfterAttach(msg: Message): void {
if (!this.scrollBar) {
this.scrollBar = this.scrollBarFactory();
Expand Down Expand Up @@ -652,15 +682,36 @@ export class ScrollableTabBar extends TabBar<Widget> {

const content = [];
if (this.dynamicTabOptions) {

this.openTabsRoot.render(createElement(SelectComponent, {
options: this.titles,
onChange: (option, index) => {
this.currentIndex = index;
},
alignment: 'right'
}));

if (this.isMouseOver) {
this.needsRecompute = true;
} else {
this.needsRecompute = false;
if (this.orientation === 'horizontal') {
this.tabSize = Math.max(Math.min(this.scrollbarHost.clientWidth / this.titles.length,
this.dynamicTabOptions.defaultTabSize), this.dynamicTabOptions.minimumTabSize);

let availableWidth = this.scrollbarHost.clientWidth;
if (!this.openTabsContainer.classList.contains('p-mod-hidden')) {
availableWidth += this.openTabsContainer.getBoundingClientRect().width;
}
if (this.dynamicTabOptions.minimumTabSize * this.titles.length <= availableWidth) {
this.openTabsContainer.classList.add('p-mod-hidden');
} else {
this.openTabsContainer.classList.remove('p-mod-hidden');
}
}
}
} else {
this.openTabsContainer.classList.add('p-mod-hidden');
}
for (let i = 0, n = this.titles.length; i < n; ++i) {
const title = this.titles[i];
Expand Down Expand Up @@ -739,10 +790,38 @@ export class ScrollableTabBar extends TabBar<Widget> {
return result;
}

/**
* Overrides the `contentNode` property getter in PhosphorJS' TabBar.
*/
// @ts-expect-error TS2611 `TabBar<T>.contentNode` is declared as `readonly contentNode` but is implemented as a getter.
get contentNode(): HTMLUListElement {
return this.tabBarContainer.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0] as HTMLUListElement;
}

/**
* Overrides the scrollable host from the parent class.
*/
protected get scrollbarHost(): HTMLElement {
return this.node;
return this.tabBarContainer;
}

protected get tabBarContainer(): HTMLElement {
return this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER)[0] as HTMLElement;
}
}

export namespace ScrollableTabBar {

export interface Options {
minimumTabSize: number;
defaultTabSize: number;
}
export namespace Styles {

export const TAB_BAR_CONTENT = 'p-TabBar-content';
export const TAB_BAR_CONTENT_CONTAINER = 'p-TabBar-content-container';

}
}

/**
Expand All @@ -761,12 +840,9 @@ export class ScrollableTabBar extends TabBar<Widget> {
*
*/
export class ToolbarAwareTabBar extends ScrollableTabBar {

protected contentContainer: HTMLElement;
protected toolbar: TabBarToolbar | undefined;
protected breadcrumbsContainer: HTMLElement;
protected readonly breadcrumbsRenderer: BreadcrumbsRenderer;
protected topRow: HTMLElement;

constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
Expand All @@ -777,7 +853,8 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
) {
super(options, dynamicTabOptions);
this.breadcrumbsRenderer = this.breadcrumbsRendererFactory();
this.rewireDOM();
this.addBreadcrumbs();
this.toolbar = this.tabBarToolbarFactory();
this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update()));
this.toDispose.push(this.breadcrumbsRenderer);
this.toDispose.push(this.breadcrumbsRenderer.onDidChangeActiveState(active => {
Expand All @@ -792,25 +869,6 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
this.toDispose.push(Disposable.create(() => this.currentChanged.disconnect(handler)));
}

/**
* Overrides the `contentNode` property getter in PhosphorJS' TabBar.
*/
// @ts-expect-error TS2611 `TabBar<T>.contentNode` is declared as `readonly contentNode` but is implemented as a getter.
get contentNode(): HTMLUListElement {
return this.tabBarContainer.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0] as HTMLUListElement;
}

/**
* Overrides the scrollable host from the parent class.
*/
protected override get scrollbarHost(): HTMLElement {
return this.tabBarContainer;
}

protected get tabBarContainer(): HTMLElement {
return this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER)[0] as HTMLElement;
}

protected async updateBreadcrumbs(): Promise<void> {
const current = this.currentTitle?.owner;
const uri = NavigatableWidget.is(current) ? current.getResourceUri() : undefined;
Expand Down Expand Up @@ -853,11 +911,9 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
}

override handleEvent(event: Event): void {
if (this.toolbar && event instanceof MouseEvent && this.toolbar.shouldHandleMouseEvent(event)) {
// if the mouse event is over the toolbar part don't handle it.
return;
if (event.target instanceof Element && this.tabBarContainer.contains(event.target)) {
super.handleEvent(event);
}
super.handleEvent(event);
}

/**
Expand All @@ -867,38 +923,14 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
* Instead of this structure, we add a container for the `this.contentNode` and for the toolbar.
* The scrollbar will only work for the `ul` part but it does not affect the toolbar, so it can be on the right hand-side.
*/
protected rewireDOM(): void {
const contentNode = this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0];
if (!contentNode) {
throw new Error("'this.node' does not have the content as a direct child with class name 'p-TabBar-content'.");
}
this.node.removeChild(contentNode);
this.topRow = document.createElement('div');
this.topRow.classList.add('theia-tabBar-tab-row');
this.contentContainer = document.createElement('div');
this.contentContainer.classList.add(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER);
this.contentContainer.appendChild(contentNode);
this.topRow.appendChild(this.contentContainer);
this.node.appendChild(this.topRow);
this.toolbar = this.tabBarToolbarFactory();
private addBreadcrumbs(): void {
this.breadcrumbsContainer = document.createElement('div');
this.breadcrumbsContainer.classList.add('theia-tabBar-breadcrumb-row');
this.breadcrumbsContainer.appendChild(this.breadcrumbsRenderer.host);
this.node.appendChild(this.breadcrumbsContainer);
}
}

export namespace ToolbarAwareTabBar {

export namespace Styles {

export const TAB_BAR_CONTENT = 'p-TabBar-content';
export const TAB_BAR_CONTENT_CONTAINER = 'p-TabBar-content-container';

}

}

/**
* A specialized tab bar for side areas.
*/
Expand Down
Loading

0 comments on commit 709b29b

Please sign in to comment.