Skip to content

Commit

Permalink
Fixed #1278: implemented tab bar decorator & supported error marker i…
Browse files Browse the repository at this point in the history
…n editor tabs

- Implemented `TabBarDecorator` that provides tabs with decorations, similar to what we already had for tree nodes.
- Supported diagnostic problem markers (error, warning, ...) in editor tabs in the main area. Tabs in side bars can be decorated as well in the future using the same code.
- Refactored `TreeDecoration` to a more generic `NodeDecoration`, which is currently used for decorating tree nodes and tabbar tabs.

Signed-off-by: fangnx <naxin.fang@ericsson.com>
  • Loading branch information
fangnx committed Aug 22, 2019
1 parent 865e0f9 commit f681ae7
Show file tree
Hide file tree
Showing 12 changed files with 821 additions and 423 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [task] allowed users to override any task properties other than the ones used in the task definition [#5777](https://github.com/theia-ide/theia/pull/5777)
- [task] notified clients of TaskDefinitionRegistry on change [#5915](https://github.com/theia-ide/theia/pull/5915)
- [outline] added `OutlineViewTreeModel` for the outline view tree widget [#5687](https://github.com/theia-ide/theia/pull/5687)
- [core] supported diagnostic marker in the tab bar [#5845](https://github.com/theia-ide/theia/pull/5845)

Breaking changes:

Expand All @@ -23,6 +24,7 @@ Breaking changes:
- `Source Control` and `Explorer` are view containers now and previous layout data cannot be loaded for them. Because of it the layout is completely reset.
- [vscode] complete support of variable substitution [#5835](https://github.com/theia-ide/theia/pull/5835)
- inline `VariableQuickOpenItem`
- [core] refactored `TreeDecoration` to `WidgetDecoration` and moved it to shell, since it is a generic decoration that can be used by different types of widgets (currently by tree nodes and tabs) [#5845](https://github.com/theia-ide/theia/pull/5845)

## v0.9.0
- [core] added `theia-widget-noInfo` css class to be used by widgets when displaying no information messages [#5717](https://github.com/theia-ide/theia/pull/5717)
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { ProgressClient } from '../common/progress-service-protocol';
import { ProgressService } from '../common/progress-service';
import { DispatchingProgressClient } from './progress-client';
import { ProgressStatusBarItem } from './progress-status-bar-item';
import { TabBarDecoratorService, TabBarDecorator } from './shell/tab-bar-decorator';

export const frontendApplicationModule = new ContainerModule((bind, unbind, isBound, rebind) => {
const themeService = ThemeService.get();
Expand Down Expand Up @@ -119,9 +120,13 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
bind(DockPanelRenderer).toSelf();
bind(TabBarRendererFactory).toFactory(context => () => {
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
return new TabBarRenderer(contextMenuRenderer);
const decoratorService = context.container.get<TabBarDecoratorService>(TabBarDecoratorService);
return new TabBarRenderer(contextMenuRenderer, decoratorService);
});

bindContributionProvider(bind, TabBarDecorator);
bind(TabBarDecoratorService).toSelf().inSingletonScope();

bindContributionProvider(bind, OpenHandler);
bind(DefaultOpenerService).toSelf().inSingletonScope();
bind(OpenerService).toService(DefaultOpenerService);
Expand Down
89 changes: 89 additions & 0 deletions packages/core/src/browser/shell/tab-bar-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/********************************************************************************
* Copyright (C) 2019 Ericsson 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 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable, named, postConstruct } from 'inversify';
import { Event, Emitter, Disposable, DisposableCollection, ContributionProvider } from '../../common';
import { Title, Widget } from '@phosphor/widgets';
import { WidgetDecoration } from '../widget-decoration';

export const TabBarDecorator = Symbol('TabBarDecorator');

export interface TabBarDecorator {

/**
* The unique identifier of the tab bar decorator.
*/
readonly id: string;

/**
* Event that is fired when any of the available tab bar decorators has changes.
*/
readonly onDidChangeDecorations: Event<void>;

/**
* Decorate tabs by the underlying URI.
* @param {Title<Widget>[]} titles An array of the titles of the tabs.
* @returns A map from the URI of the tab to its decoration data.
*/
decorate(titles: Title<Widget>[]): Map<string, WidgetDecoration.Data>;
}

@injectable()
export class TabBarDecoratorService implements Disposable {

protected readonly onDidChangeDecorationsEmitter = new Emitter<void>();

readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event;

protected readonly toDispose = new DisposableCollection();

@inject(ContributionProvider) @named(TabBarDecorator)
protected readonly contributions: ContributionProvider<TabBarDecorator>;

@postConstruct()
protected init(): void {
const decorators = this.contributions.getContributions();
this.toDispose.pushAll(decorators.map(decorator =>
decorator.onDidChangeDecorations(data =>
this.onDidChangeDecorationsEmitter.fire(undefined)
))
);
}

dispose(): void {
this.toDispose.dispose();
}

/**
* Assign tabs the decorators provided by all the contributions.
* @param {Title<Widget>[]} titles An array of the titles of the tabs.
* @returns A map from the URI of the tab to an array of its decoration data.
*/
getDecorations(titles: Title<Widget>[]): Map<string, WidgetDecoration.Data[]> {
const decorators = this.contributions.getContributions();
const changes: Map<string, WidgetDecoration.Data[]> = new Map();
for (const decorator of decorators) {
for (const [id, data] of (decorator.decorate(titles)).entries()) {
if (changes.has(id)) {
changes.get(id)!.push(data);
} else {
changes.set(id, [data]);
}
}
}
return changes;
}
}
115 changes: 100 additions & 15 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
import PerfectScrollbar from 'perfect-scrollbar';
import { TabBar, Title, Widget } from '@phosphor/widgets';
import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/virtualdom';
import { MenuPath } from '../../common';
import { DisposableCollection, MenuPath, notEmpty } from '../../common';
import { ContextMenuRenderer } from '../context-menu-renderer';
import { Signal } from '@phosphor/signaling';
import { Message } from '@phosphor/messaging';
import { ArrayExt } from '@phosphor/algorithm';
import { ElementExt } from '@phosphor/domutils';
import { TabBarToolbarRegistry, TabBarToolbar } from './tab-bar-toolbar';
import { TheiaDockPanel, MAIN_AREA_ID, BOTTOM_AREA_ID } from './theia-dock-panel';
import { WidgetDecoration } from '../widget-decoration';
import { TabBarDecoratorService } from './tab-bar-decorator';

/** 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 @@ -71,17 +73,26 @@ export class TabBarRenderer extends TabBar.Renderer {
*/
contextMenuPath?: MenuPath;

protected readonly toDispose = new DisposableCollection();

// TODO refactor shell, rendered should only receive props with event handlers
// events should be handled by clients, like ApplicationShell
// right now it is mess: (1) client logic belong to renderer, (2) cyclic dependencies between renderes and clients
constructor(protected readonly contextMenuRenderer?: ContextMenuRenderer) {
constructor(
protected readonly contextMenuRenderer?: ContextMenuRenderer,
protected readonly decoratorService?: TabBarDecoratorService
) {
super();
if (this.decoratorService) {
this.toDispose.push(this.decoratorService);
this.toDispose.push(this.decoratorService.onDidChangeDecorations(() => this.tabBar && this.tabBar.update()));
}
}

/**
* Render tabs with the default DOM structure, but additionally register a context menu listener.
* @param data {SideBarRenderData} data used to render the tab.
* @param isInSidePanel {boolean} an optional check which determines if the tab is in the side-panel.
* @param {SideBarRenderData} data Data used to render the tab.
* @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel.
* @returns {VirtualElement} The virtual element of the rendered tab.
*/
renderTab(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
Expand All @@ -97,7 +108,7 @@ export class TabBarRenderer extends TabBar.Renderer {
oncontextmenu: this.handleContextMenuEvent,
ondblclick: this.handleDblClickEvent
},
this.renderIcon(data),
this.renderIcon(data, isInSidePanel),
this.renderLabel(data, isInSidePanel),
this.renderCloseIcon(data)
);
Expand Down Expand Up @@ -131,10 +142,10 @@ export class TabBarRenderer extends TabBar.Renderer {
}

/**
* If size information is available for the label, set it as inline style. Tab padding
* and icon size are also considered in the `top` position.
* @param data {SideBarRenderData} data used to render the tab.
* @param isInSidePanel {boolean} an optional check which determines if the tab is in the side-panel.
* If size information is available for the label, set it as inline style.
* Tab padding and icon size are also considered in the `top` position.
* @param {SideBarRenderData} data Data used to render the tab.
* @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel.
* @returns {VirtualElement} The virtual element of the rendered label.
*/
renderLabel(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement {
Expand All @@ -157,7 +168,7 @@ export class TabBarRenderer extends TabBar.Renderer {
top = `${paddingTop + iconHeight}px`;
}
const style: ElementInlineStyle = { width, height, top };
// No need to check for duplicate labels if the tab is rendered in the side panel (title is not displayed)
// No need to check for duplicate labels if the tab is rendered in the side panel (title is not displayed),
// or if there are less than two files in the tab bar.
if (isInSidePanel || (this.tabBar && this.tabBar.titles.length < 2)) {
return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label);
Expand All @@ -172,14 +183,49 @@ export class TabBarRenderer extends TabBar.Renderer {
return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label);
}

/**
* Get all available decorations of a given tab.
* @param {string} tab The URI of the tab.
*/
protected getDecorations(tab: string): WidgetDecoration.Data[] {
const tabDecorations = [];
if (this.tabBar && this.decoratorService) {
const allDecorations = this.decoratorService.getDecorations([...this.tabBar.titles]);
if (allDecorations.has(tab)) {
tabDecorations.push(...allDecorations.get(tab));
}
}
return tabDecorations;
}

/**
* Get the decoration data given the tab URI and the decoration data type.
* @param {string} tab The URI of the tab.
* @param {K} key The type of the decoration data.
*/
protected getDecorationData<K extends keyof WidgetDecoration.Data>(tab: string, key: K): WidgetDecoration.Data[K][] {
return this.getDecorations(tab).filter(data => data[key] !== undefined).map(data => data[key]);

}

/**
* Get the class of an icon.
* @param {string | string[]} iconName The name of the icon.
* @param {string[]} additionalClasses Additional classes of the icon.
*/
private getIconClass(iconName: string | string[], additionalClasses: string[] = []): string {
const iconClass = (typeof iconName === 'string') ? ['a', 'fa', `fa-${iconName}`] : ['a'].concat(iconName);
return iconClass.concat(additionalClasses).join(' ');
}

/**
* Find duplicate labels from the currently opened tabs in the tab bar.
* Return the approriate partial paths that can distinguish the identical labels.
* Return the appropriate partial paths that can distinguish the identical labels.
*
* E.g., a/p/index.ts => a/..., b/p/index.ts => b/...
*
* To prevent excessively long path displayed, show at maximum three levels from the end by default.
* @param {Title<Widget>[]} titles - array of titles in the current tab bar.
* @param {Title<Widget>[]} titles Array of titles in the current tab bar.
* @returns {Map<string, string>} A map from each tab's original path to its displayed partial path.
*/
findDuplicateLabels(titles: Title<Widget>[]): Map<string, string> {
Expand Down Expand Up @@ -260,15 +306,54 @@ export class TabBarRenderer extends TabBar.Renderer {
/**
* If size information is available for the icon, set it as inline style. Tab padding
* is also considered in the `top` position.
* @param {SideBarRenderData} data Data used to render the tab icon.
* @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel.
*/
renderIcon(data: SideBarRenderData): VirtualElement {
renderIcon(data: SideBarRenderData, inSidePanel?: boolean): VirtualElement {
let top: string | undefined;
if (data.paddingTop) {
top = `${data.paddingTop || 0}px`;
}
const className = this.createIconClass(data);
const style: ElementInlineStyle = { top };
return h.div({ className, style }, data.title.iconLabel);
const baseClassName = this.createIconClass(data);

const overlayIcons: VirtualElement[] = [];
const decorationData = this.getDecorationData(data.title.caption, 'iconOverlay');

// Check if the tab has decoration markers to be rendered on top.
if (decorationData.length > 0) {
const baseIcon: VirtualElement = h.div({ className: baseClassName, style }, data.title.iconLabel);
const wrapperClassName: string = WidgetDecoration.Styles.ICON_WRAPPER_CLASS;
const decoratorSizeClassName: string = inSidePanel ? WidgetDecoration.Styles.DECORATOR_SIDEBAR_SIZE_CLASS : WidgetDecoration.Styles.DECORATOR_SIZE_CLASS;

decorationData
.filter(notEmpty)
.map(overlay => [overlay.position, overlay] as [WidgetDecoration.IconOverlayPosition, WidgetDecoration.IconOverlay | WidgetDecoration.IconClassOverlay])
.forEach(([position, overlay]) => {
const iconAdditionalClasses: string[] = [decoratorSizeClassName, WidgetDecoration.IconOverlayPosition.getStyle(position, inSidePanel)];
const overlayIconStyle = (color?: string) => {
if (color === undefined) {
return {};
}
return { color };
};
// Parse the optional background (if it exists) of the overlay icon.
if (overlay.background) {
const backgroundIconClassName = this.getIconClass(overlay.background.shape, iconAdditionalClasses);
overlayIcons.push(
h.div({ key: data.title.label + '-background', className: backgroundIconClassName, style: overlayIconStyle(overlay.background.color) })
);
}
// Parse the overlay icon.
const overlayIcon = (overlay as WidgetDecoration.IconOverlay).icon || (overlay as WidgetDecoration.IconClassOverlay).iconClass;
const overlayIconClassName = this.getIconClass(overlayIcon, iconAdditionalClasses);
overlayIcons.push(
h.span({ key: data.title.label, className: overlayIconClassName, style: overlayIconStyle(overlay.color) })
);
});
return h.div({ className: wrapperClassName, style }, [baseIcon, ...overlayIcons]);
}
return h.div({ className: baseClassName, style }, data.title.iconLabel);
}

protected handleContextMenuEvent = (event: MouseEvent) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/browser/style/tree-decorators.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
}

.theia-icon-wrapper {
top: 0px !important;
position: relative;
display: inline-block
}
Expand All @@ -42,6 +43,13 @@
width: 100%;
}

.theia-decorator-sidebar-size {
height: 100%;
text-align: center;
transform: scale(1.2);
width: 100%;
}

.theia-top-right {
position: absolute;
bottom: 40%;
Expand All @@ -54,6 +62,12 @@
left: 25%;
}

.theia-bottom-right-sidebar {
position: absolute;
top: 80%;
left: 50%;
}

.theia-bottom-left {
position: absolute;
top: 40%;
Expand Down
Loading

0 comments on commit f681ae7

Please sign in to comment.