Skip to content

Commit

Permalink
Fixed #1278: implemented error marker for editor tabs
Browse files Browse the repository at this point in the history
Signed-off-by: fangnx <naxin.fang@ericsson.com>
  • Loading branch information
fangnx authored and fangnx committed Aug 7, 2019
1 parent f9ff237 commit 8361c65
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 7 deletions.
12 changes: 11 additions & 1 deletion packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { MimeService } from './mime-service';
import { ApplicationShellMouseTracker } from './shell/application-shell-mouse-tracker';
import { ViewContainer } from './view-container';
import { Widget } from './widgets';
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 @@ -112,9 +113,18 @@ 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);
});

// bind(MainTabBarDecoratorService).toSelf().inSingletonScope();
// bind(TabBarDecoratorService).to(MainTabBarDecoratorService).inSingletonScope();
// bindContributionProvider(bind, MainTabBarDecorator);
// bindContributionProvider(bind, TabBarDecoratorService);

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

bindContributionProvider(bind, OpenHandler);
bind(DefaultOpenerService).toSelf().inSingletonScope();
bind(OpenerService).toService(DefaultOpenerService);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/browser/shell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export * from './side-panel-handler';
export * from './split-panels';
export * from './tab-bars';
export * from './view-contribution';
// export * from './tab-bar-decorator';
// export * from './tab-bar-decorator-service';
186 changes: 186 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,186 @@
/********************************************************************************
* 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';

export const TabBarDecorator = Symbol('TabBarDecorator');

export interface TabBarDecorator {

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

readonly onDidChangeDecorations: Event<(titles: Title<Widget>[]) => Map<string, TabBarDecoration.Data>>;

decorations(titles: Title<Widget>[]): Map<string, TabBarDecoration.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>;

protected decorators: ReadonlyArray<TabBarDecorator>;

constructor(
) {
}

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

}

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

getDecorations(titles: Title<Widget>[]): Map<string, TabBarDecoration.Data[]> {
const changes: Map<string, TabBarDecoration.Data[]> = new Map();
for (const decorator of this.decorators) {
console.log(decorator);
for (const [id, data] of (decorator.decorations(titles)).entries()) {
if (changes.has(id)) {
changes.get(id)!.push(data);
} else {
changes.set(id, [data]);
}
}
}
return changes;
}
}

export namespace TabBarDecoration {

export namespace Styles {
export const CAPTION_HIGHLIGHT_CLASS = 'theia-caption-highlight';
export const CAPTION_PREFIX_CLASS = 'theia-caption-prefix';
export const CAPTION_SUFFIX_CLASS = 'theia-caption-suffix';
export const ICON_WRAPPER_CLASS = 'theia-icon-wrapper';
export const DECORATOR_SIZE_CLASS = 'theia-decorator-size';
export const TOP_RIGHT_CLASS = 'theia-top-right';
export const BOTTOM_RIGHT_CLASS = 'theia-bottom-right';
export const BOTTOM_LEFT_CLASS = 'theia-bottom-left';
export const TOP_LEFT_CLASS = 'theia-top-left';
}

/**
* Enumeration for the quadrant to overlay the image on.
*/
export enum IconOverlayPosition {
TOP_RIGHT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
TOP_LEFT
}

export type Color = string;

export namespace IconOverlayPosition {

/**
* Returns with the CSS class style for the enum.
*/
export function getStyle(position: IconOverlayPosition): string {
switch (position) {
case IconOverlayPosition.TOP_RIGHT: return TabBarDecoration.Styles.TOP_RIGHT_CLASS;
case IconOverlayPosition.BOTTOM_RIGHT: return TabBarDecoration.Styles.BOTTOM_RIGHT_CLASS;
case IconOverlayPosition.BOTTOM_LEFT: return TabBarDecoration.Styles.BOTTOM_LEFT_CLASS;
case IconOverlayPosition.TOP_LEFT: return TabBarDecoration.Styles.TOP_LEFT_CLASS;
}
}
}

/**
* A shape that can be optionally rendered behind the overlay icon. Can be used to further refine colors.
*/
export interface IconOverlayBackground {

/**
* Either `circle` or `square`.
*/
readonly shape: 'circle' | 'square';

/**
* The color of the background shape.
*/
readonly color?: Color;
}

/**
* Has not effect if the tree node being decorated has no associated icon.
*/
export interface BaseOverlay {

/**
* The position where the decoration will be placed on the top of the original icon.
*/
readonly position: IconOverlayPosition;

/**
* The color of the overlaying icon. If not specified, then the default icon color will be used.
*/
readonly color?: Color;

/**
* The optional background color of the overlay icon.
*/
readonly background?: IconOverlayBackground;

}

export interface IconOverlay extends BaseOverlay {

/**
* This should be the name of the Font Awesome icon with out the `fa fa-` prefix, just the name, for instance `paw`.
* For the existing icons, see here: https://fontawesome.com/v4.7.0/icons/.
*/
readonly icon: string;
}

export interface IconClassOverlay extends BaseOverlay {

/**
* This should be the entire Font Awesome class array, for instance ['fa', 'fa-paw']
* For the existing icons, see here: https://fontawesome.com/v4.7.0/icons/.
*/
readonly iconClass: string[];
}

export interface Data {

readonly iconOverlay?: IconOverlay | IconClassOverlay
}
}
71 changes: 67 additions & 4 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
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 { 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 { TabBarDecoration, 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 @@ -74,8 +75,14 @@ export class TabBarRenderer extends TabBar.Renderer {
// 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.decoratorService.onDidChangeDecorations(() => this.tabBar && this.tabBar.update());
}
}

/**
Expand Down Expand Up @@ -155,6 +162,27 @@ export class TabBarRenderer extends TabBar.Renderer {
return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label);
}

protected getDecorations(tab: string): TabBarDecoration.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;
}

protected getDecorationData<K extends keyof TabBarDecoration.Data>(tab: string, key: K): TabBarDecoration.Data[K][] {
return this.getDecorations(tab).filter(data => data[key] !== undefined).map(data => data[key]);

}

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(' ');
}

/**
* If size information is available for the icon, set it as inline style. Tab padding
* is also considered in the `top` position.
Expand All @@ -164,9 +192,44 @@ export class TabBarRenderer extends TabBar.Renderer {
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 = TabBarDecoration.Styles.ICON_WRAPPER_CLASS;
decorationData
.filter(notEmpty)
.map(overlay => [overlay.position, overlay] as [TabBarDecoration.IconOverlayPosition, TabBarDecoration.IconOverlay | TabBarDecoration.IconClassOverlay])
.forEach(([position, overlay]) => {
const iconAdditionalClasses: string[] = [TabBarDecoration.Styles.DECORATOR_SIZE_CLASS, TabBarDecoration.IconOverlayPosition.getStyle(position)];
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 TabBarDecoration.IconOverlay).icon || (overlay as TabBarDecoration.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
Loading

0 comments on commit 8361c65

Please sign in to comment.