Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "open tabs" drop down #12411

Merged
merged 5 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)


## v1.38.0 - 04/27/2023

<a name="breaking_changes_1.38.0">[Breaking Changes:](#breaking_changes_1.38.0)</a>

- [core] moved `ToolbarAwareTabBar.Styles` to `ScrollableTabBar.Styles` [12411](https://github.com/eclipse-theia/theia/pull/12411/)

## v1.37.0 - 04/27/2023

- [application-package] bumped the default supported VS Code API from `1.72.2` to `1.74.2` [#12468](https://github.com/eclipse-theia/theia/pull/12468)
Expand Down
1 change: 1 addition & 0 deletions examples/playwright/src/tests/theia-terminal-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ test.describe('Theia Terminal View', () => {

// close all terminals
for (const terminal of allTerminals) {
await terminal.activate();
await terminal.close();
}

Expand Down
1 change: 1 addition & 0 deletions examples/playwright/src/tests/theia-text-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ test.describe('Theia Text Editor', () => {

// close all editors
for (const editor of allEditors) {
await editor.activate();
await editor.close();
}

Expand Down
3 changes: 1 addition & 2 deletions examples/playwright/src/theia-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,14 @@ export class TheiaView extends TheiaPageObject {
throw Error(`Unable to determine side of invisible view tab '${this.tabSelector}'`);
}
const tab = await this.tabElement();
let appAreaElement = tab?.$('xpath=../..');
const appAreaElement = tab?.$('xpath=../../../..');
if (await containsClass(appAreaElement, 'theia-app-left')) {
return 'left';
}
if (await containsClass(appAreaElement, 'theia-app-right')) {
return 'right';
}

appAreaElement = (await appAreaElement)?.$('xpath=../..');
if (await containsClass(appAreaElement, 'theia-app-bottom')) {
return 'bottom';
}
Expand Down
168 changes: 105 additions & 63 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 @@ -649,18 +679,41 @@ export class ScrollableTabBar extends TabBar<Widget> {
}

protected updateTabs(): void {

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,
let availableWidth = this.scrollbarHost.clientWidth;
let effectiveWidth = availableWidth;
if (!this.openTabsContainer.classList.contains('p-mod-hidden')) {
availableWidth += this.openTabsContainer.getBoundingClientRect().width;
}
if (this.dynamicTabOptions.minimumTabSize * this.titles.length <= availableWidth) {
effectiveWidth += this.openTabsContainer.getBoundingClientRect().width;
this.openTabsContainer.classList.add('p-mod-hidden');
} else {
this.openTabsContainer.classList.remove('p-mod-hidden');
}
this.tabSize = Math.max(Math.min(effectiveWidth / this.titles.length,
this.dynamicTabOptions.defaultTabSize), this.dynamicTabOptions.minimumTabSize);
}
}
this.node.classList.add('dynamic-tabs');
} else {
this.openTabsContainer.classList.add('p-mod-hidden');
this.node.classList.remove('dynamic-tabs');
}
for (let i = 0, n = this.titles.length; i < n; ++i) {
const title = this.titles[i];
Expand All @@ -673,7 +726,7 @@ export class ScrollableTabBar extends TabBar<Widget> {
content[i] = this.renderer.renderTab(renderData);
}
VirtualDOM.render(content, this.contentNode);
if (this.scrollBar) {
if (this.dynamicTabOptions && !this.isMouseOver && this.scrollBar) {
this.scrollBar.update();
}
}
Expand Down Expand Up @@ -739,10 +792,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 +842,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 +855,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 +871,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,52 +913,34 @@ 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 instanceof MouseEvent) {
if (this.toolbar && this.toolbar.shouldHandleMouseEvent(event) || this.isOver(event, this.openTabsContainer)) {
// if the mouse event is over the toolbar part don't handle it.
return;
}
}
super.handleEvent(event);
}

private isOver(event: Event, element: Element): boolean {
return element && event.target instanceof Element && element.contains(event.target);
}

/**
* 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.
*/
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';

}

}
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved

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