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

Fixed #1278: implemented tab bar decorator & supported error marker in editor tabs #5845

Merged
merged 1 commit into from
Aug 22, 2019
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
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.
vince-fugnitto marked this conversation as resolved.
Show resolved Hide resolved
*/
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 =>
akosyakov marked this conversation as resolved.
Show resolved Hide resolved
this.onDidChangeDecorationsEmitter.fire(undefined)
))
);
}

dispose(): void {
vince-fugnitto marked this conversation as resolved.
Show resolved Hide resolved
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();
vince-fugnitto marked this conversation as resolved.
Show resolved Hide resolved

// 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[] {
vince-fugnitto marked this conversation as resolved.
Show resolved Hide resolved
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