Skip to content

Commit

Permalink
Rework sidepanel behavior on tab overflow (#12593)
Browse files Browse the repository at this point in the history
Rework behavior of the sidepanels if there more tabs than they can fit. 
Introduce behavior similar to VS Code: Overflowing tabs will simply be hidden or 'merged' into one menu button at the end
of the tabbar. Clicking the button allows revealing of a currently hidden view.
Special handling is implemented to ensure that the currently selected view never gets hidden away.

Contributed on behalf of STMicroelectronics
Fixes #12416
  • Loading branch information
tortmayr authored Jun 28, 2023
1 parent 6a13e1f commit b1788ea
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 19 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import { MarkdownRenderer, MarkdownRendererFactory, MarkdownRendererImpl } from
import { StylingParticipant, StylingService } from './styling-service';
import { bindCommonStylingParticipants } from './common-styling-participants';
import { HoverService } from './hover-service';
import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget';

export { bindResourceProvider, bindMessageService, bindPreferenceService };

Expand Down Expand Up @@ -167,6 +168,12 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is
bind(SidebarMenuWidget).toSelf();
bind(SidebarBottomMenuWidget).toSelf();
bind(SidebarBottomMenuWidgetFactory).toAutoFactory(SidebarBottomMenuWidget);
bind(AdditionalViewsMenuWidget).toSelf();
bind(AdditionalViewsMenuWidgetFactory).toFactory(ctx => (side: 'left' | 'right') => {
const widget = ctx.container.resolve(AdditionalViewsMenuWidget);
widget.side = side;
return widget;
});
bind(SplitPositionHandler).toSelf().inSingletonScope();

bindContributionProvider(bind, TabBarToolbarContribution);
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/browser/shell/additional-views-menu-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics 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 { inject, injectable } from '../../../shared/inversify';
import { Command, CommandRegistry, Disposable, MenuModelRegistry, MenuPath, nls } from '../../common';
import { Title, Widget, codicon } from '../widgets';
import { SidebarMenuWidget } from './sidebar-menu-widget';
import { SideTabBar } from './tab-bars';

export const AdditionalViewsMenuWidgetFactory = Symbol('AdditionalViewsMenuWidgetFactory');
export type AdditionalViewsMenuWidgetFactory = (side: 'left' | 'right') => AdditionalViewsMenuWidget;

export const ADDITIONAL_VIEWS_MENU_PATH: MenuPath = ['additional_views_menu'];

@injectable()
export class AdditionalViewsMenuWidget extends SidebarMenuWidget {
static readonly ID = 'sidebar.additional.views';

side: 'left' | 'right';

@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;

@inject(MenuModelRegistry)
protected readonly menuModelRegistry: MenuModelRegistry;

protected menuDisposables: Disposable[] = [];

updateAdditionalViews(sender: SideTabBar, event: { titles: Title<Widget>[], startIndex: number }): void {
if (event.startIndex === -1) {
this.removeMenu(AdditionalViewsMenuWidget.ID);
} else {
this.addMenu({
title: nls.localizeByDefault('Additional Views'),
iconClass: codicon('ellipsis'),
id: AdditionalViewsMenuWidget.ID,
menuPath: ADDITIONAL_VIEWS_MENU_PATH,
order: 0
});
}

this.menuDisposables.forEach(disposable => disposable.dispose());
this.menuDisposables = [];
event.titles.forEach((title, i) => this.registerMenuAction(sender, title, i));
}

protected registerMenuAction(sender: SideTabBar, title: Title<Widget>, index: number): void {
const command: Command = { id: `reveal.${title.label}.${index}`, label: title.label };
this.menuDisposables.push(this.commandRegistry.registerCommand(command, {
execute: () => {
window.requestAnimationFrame(() => {
sender.currentIndex = sender.titles.indexOf(title);
});
}
}));
this.menuDisposables.push(this.menuModelRegistry.registerMenuAction(ADDITIONAL_VIEWS_MENU_PATH, { commandId: command.id, order: index.toString() }));
}
}
24 changes: 24 additions & 0 deletions packages/core/src/browser/shell/side-panel-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MenuPath } from '../../common/menu';
import { SidebarBottomMenuWidget } from './sidebar-bottom-menu-widget';
import { SidebarTopMenuWidget } from './sidebar-top-menu-widget';
import { PINNED_CLASS } from '../widgets';
import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './additional-views-menu-widget';

/** The class name added to the left and right area panels. */
export const LEFT_RIGHT_AREA_CLASS = 'theia-app-sides';
Expand Down Expand Up @@ -68,6 +69,11 @@ export class SidePanelHandler {
* tab bar itself remains visible as long as there is at least one widget.
*/
tabBar: SideTabBar;
/**
* Conditional menu placed below the tabBar. Manages overflowing/hidden tabs.
* Is only visible if there are overflowing tabs.
*/
additionalViewsMenu: AdditionalViewsMenuWidget;
/**
* The menu placed on the sidebar top.
* Displayed as icons.
Expand Down Expand Up @@ -118,6 +124,7 @@ export class SidePanelHandler {
@inject(TabBarRendererFactory) protected tabBarRendererFactory: () => TabBarRenderer;
@inject(SidebarTopMenuWidgetFactory) protected sidebarTopWidgetFactory: () => SidebarTopMenuWidget;
@inject(SidebarBottomMenuWidgetFactory) protected sidebarBottomWidgetFactory: () => SidebarBottomMenuWidget;
@inject(AdditionalViewsMenuWidgetFactory) protected additionalViewsMenuFactory: AdditionalViewsMenuWidgetFactory;
@inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler;
@inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService;
@inject(TheiaDockPanel.Factory) protected readonly dockPanelFactory: TheiaDockPanel.Factory;
Expand All @@ -133,6 +140,7 @@ export class SidePanelHandler {
this.options = options;
this.topMenu = this.createSidebarTopMenu();
this.tabBar = this.createSideBar();
this.additionalViewsMenu = this.createAdditionalViewsWidget();
this.bottomMenu = this.createSidebarBottomMenu();
this.toolBar = this.createToolbar();
this.dockPanel = this.createSidePanel();
Expand Down Expand Up @@ -175,6 +183,7 @@ export class SidePanelHandler {
sideBar.collapseRequested.connect(() => this.collapse(), this);
sideBar.currentChanged.connect(this.onCurrentTabChanged, this);
sideBar.tabDetachRequested.connect(this.onTabDetachRequested, this);
sideBar.tabsOverflowChanged.connect(this.onTabsOverflowChanged, this);
return sideBar;
}

Expand All @@ -199,6 +208,12 @@ export class SidePanelHandler {
return toolbar;
}

protected createAdditionalViewsWidget(): AdditionalViewsMenuWidget {
const widget = this.additionalViewsMenuFactory(this.side);
widget.addClass('theia-sidebar-menu');
return widget;
}

protected createSidebarTopMenu(): SidebarTopMenuWidget {
return this.createSidebarMenu(this.sidebarTopWidgetFactory);
}
Expand Down Expand Up @@ -254,6 +269,7 @@ export class SidePanelHandler {
sidebarContainer.addClass('theia-app-sidebar-container');
sidebarContainerLayout.addWidget(this.topMenu);
sidebarContainerLayout.addWidget(this.tabBar);
sidebarContainerLayout.addWidget(this.additionalViewsMenu);
sidebarContainerLayout.addWidget(this.bottomMenu);

BoxPanel.setStretch(sidebarContainer, 0);
Expand Down Expand Up @@ -636,6 +652,14 @@ export class SidePanelHandler {
});
}

protected onTabsOverflowChanged(sender: SideTabBar, event: { titles: Title<Widget>[], startIndex: number }): void {
if (event.startIndex >= 0 && event.startIndex <= sender.currentIndex) {
sender.revealTab(sender.currentIndex);
} else {
this.additionalViewsMenu.updateAdditionalViews(sender, event);
}
}

/*
* Handle the `widgetAdded` signal from the dock panel. The widget's title is inserted into the
* tab bar according to the `rankProperty` value that may be attached to the widget.
Expand Down
Loading

0 comments on commit b1788ea

Please sign in to comment.