diff --git a/packages/core/src/browser/shell/tab-bar-decorator.ts b/packages/core/src/browser/shell/tab-bar-decorator.ts index 41be55578f7f4..d8c64a2eeb1dd 100644 --- a/packages/core/src/browser/shell/tab-bar-decorator.ts +++ b/packages/core/src/browser/shell/tab-bar-decorator.ts @@ -17,7 +17,7 @@ import debounce = require('lodash.debounce'); import { Title, Widget } from '@phosphor/widgets'; import { inject, injectable, named, postConstruct } from 'inversify'; -import { Event, Emitter, Disposable, DisposableCollection, ContributionProvider } from '../../common'; +import { Event, Emitter, ContributionProvider } from '../../common'; import { WidgetDecoration } from '../widget-decoration'; export const TabBarDecorator = Symbol('TabBarDecorator'); @@ -43,27 +43,18 @@ export interface TabBarDecorator { } @injectable() -export class TabBarDecoratorService implements Disposable { +export class TabBarDecoratorService { protected readonly onDidChangeDecorationsEmitter = new Emitter(); readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event; - protected readonly toDispose = new DisposableCollection(); - @inject(ContributionProvider) @named(TabBarDecorator) protected readonly contributions: ContributionProvider; @postConstruct() protected init(): void { - const decorators = this.contributions.getContributions(); - this.toDispose.pushAll(decorators.map(decorator => - decorator.onDidChangeDecorations(this.fireDidChangeDecorations) - )); - } - - dispose(): void { - this.toDispose.dispose(); + this.contributions.getContributions().map(decorator => decorator.onDidChangeDecorations(this.fireDidChangeDecorations)); } protected fireDidChangeDecorations = debounce(() => this.onDidChangeDecorationsEmitter.fire(undefined), 150); diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 4a13ab9204ff4..82df6a7d63ac0 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -82,7 +82,6 @@ export class TabBarRenderer extends TabBar.Renderer { super(); if (this.decoratorService) { this.toDispose.push(Disposable.create(() => this.resetDecorations())); - this.toDispose.push(this.decoratorService); this.toDispose.push(this.decoratorService.onDidChangeDecorations(() => this.resetDecorations())); } if (this.iconThemeService) { @@ -151,7 +150,8 @@ export class TabBarRenderer extends TabBar.Renderer { h.div( { className: 'theia-tab-icon-label' }, this.renderIcon(data, isInSidePanel), - this.renderLabel(data, isInSidePanel) + this.renderLabel(data, isInSidePanel), + this.renderBadge(data, isInSidePanel) ), this.renderCloseIcon(data) ); @@ -226,6 +226,17 @@ export class TabBarRenderer extends TabBar.Renderer { return h.div({ className: 'p-TabBar-tabLabel', style }, data.title.label); } + renderBadge(data: SideBarRenderData, isInSidePanel?: boolean): VirtualElement { + const badge: number | undefined = this.getDecorationData(data.title, 'badge')[0]; + if (!badge) { + return h.div({}); + } + const limitedBadge = badge >= 100 ? '99+' : badge; + return isInSidePanel + ? h.div({ className: 'theia-badge-decorator-sidebar' }, `${limitedBadge}`) + : h.div({ className: 'theia-badge-decorator-horizontal' }, `${limitedBadge}`); + } + protected readonly decorations = new Map, WidgetDecoration.Data[]>(); protected resetDecorations(title?: Title): void { diff --git a/packages/core/src/browser/style/notification.css b/packages/core/src/browser/style/notification.css index b9a6f7011d0e9..66f7410cee145 100644 --- a/packages/core/src/browser/style/notification.css +++ b/packages/core/src/browser/style/notification.css @@ -16,6 +16,7 @@ :root { --theia-notification-count-height: 15.5px; + --theia-notification-count-width: 15.5px; } .notification-count-container { diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index ab0246c860cb3..d05be40385f58 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -242,6 +242,37 @@ body.theia-editor-highlightModifiedTabs display: none !important; } +.p-TabBar .theia-badge-decorator-sidebar { + background-color: var(--theia-activityBarBadge-background); + border-radius: 20px; + color: var(--theia-activityBarBadge-foreground); + font-size: calc(var(--theia-ui-font-size0) * 0.85); + font-weight: 600; + height: var(--theia-notification-count-height); + width: var(--theia-notification-count-width); + padding: calc(var(--theia-ui-padding)/6); + line-height: calc(var(--theia-content-line-height) * 0.70); + position: absolute; + top: calc(var(--theia-ui-padding) * 4); + right: calc(var(--theia-ui-padding) * 1.33); + text-align: center; +} + +.p-TabBar .theia-badge-decorator-horizontal { + background-color:var(--theia-badge-background); + border-radius: 20px; + box-sizing: border-box; + color: var(--theia-activityBarBadge-foreground); + font-size: calc(var(--theia-ui-font-size0) * 0.8); + font-weight: 400; + height: var(--theia-notification-count-height); + width: var(--theia-notification-count-width); + padding: calc(var(--theia-ui-padding)/6); + line-height: calc(var(--theia-content-line-height) * 0.61); + margin-left: var(--theia-ui-padding); + text-align: center; +} + /*----------------------------------------------------------------------------- | Perfect scrollbar |----------------------------------------------------------------------------*/ diff --git a/packages/core/src/browser/widget-decoration.ts b/packages/core/src/browser/widget-decoration.ts index 264abba848d73..b2326f5a8ae62 100644 --- a/packages/core/src/browser/widget-decoration.ts +++ b/packages/core/src/browser/widget-decoration.ts @@ -330,6 +330,10 @@ export namespace WidgetDecoration { * An array of ranges to highlight the caption. */ readonly highlight?: CaptionHighlight; + /** + * A count badge for widgets. + */ + readonly badge?: number; } export namespace Data { /** diff --git a/packages/markers/src/browser/problem/problem-frontend-module.ts b/packages/markers/src/browser/problem/problem-frontend-module.ts index f846723f30166..32cb24a0e49a0 100644 --- a/packages/markers/src/browser/problem/problem-frontend-module.ts +++ b/packages/markers/src/browser/problem/problem-frontend-module.ts @@ -31,6 +31,7 @@ import { ProblemLayoutVersion3Migration } from './problem-layout-migrations'; import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator'; import { bindProblemPreferences } from './problem-preferences'; import { MarkerTreeLabelProvider } from '../marker-tree-label-provider'; +import { ProblemWidgetTabBarDecorator } from './problem-widget-tab-bar-decorator'; export default new ContainerModule(bind => { bindProblemPreferences(bind); @@ -57,4 +58,7 @@ export default new ContainerModule(bind => { bind(MarkerTreeLabelProvider).toSelf().inSingletonScope(); bind(LabelProviderContribution).toService(MarkerTreeLabelProvider); + + bind(ProblemWidgetTabBarDecorator).toSelf().inSingletonScope(); + bind(TabBarDecorator).toService(ProblemWidgetTabBarDecorator); }); diff --git a/packages/markers/src/browser/problem/problem-widget-tab-bar-decorator.ts b/packages/markers/src/browser/problem/problem-widget-tab-bar-decorator.ts new file mode 100644 index 0000000000000..f65096ce3212f --- /dev/null +++ b/packages/markers/src/browser/problem/problem-widget-tab-bar-decorator.ts @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (C) 2020 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 { injectable, inject, postConstruct } from 'inversify'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { ProblemManager } from './problem-manager'; +import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator'; +import { Title, Widget } from '@theia/core/lib/browser'; +import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration'; + +@injectable() +export class ProblemWidgetTabBarDecorator implements TabBarDecorator { + + readonly id = 'theia-problems-widget-tabbar-decorator'; + protected readonly emitter = new Emitter(); + + @inject(ProblemManager) + protected readonly problemManager: ProblemManager; + + @postConstruct() + protected init(): void { + this.problemManager.onDidChangeMarkers(() => this.fireDidChangeDecorations()); + } + + decorate(title: Title): WidgetDecoration.Data[] { + if (title.owner.id === 'problems') { + const { infos, warnings, errors } = this.problemManager.getProblemStat(); + const markerCount = infos + warnings + errors; + return markerCount > 0 ? [{ badge: markerCount }] : []; + } else { + return []; + } + } + + get onDidChangeDecorations(): Event { + return this.emitter.event; + } + + protected fireDidChangeDecorations(): void { + this.emitter.fire(undefined); + } +} diff --git a/packages/scm/src/browser/decorations/scm-tab-bar-decorator.ts b/packages/scm/src/browser/decorations/scm-tab-bar-decorator.ts new file mode 100644 index 0000000000000..2f53cb8e941db --- /dev/null +++ b/packages/scm/src/browser/decorations/scm-tab-bar-decorator.ts @@ -0,0 +1,82 @@ +/******************************************************************************** + * Copyright (C) 2020 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 { injectable, inject, postConstruct } from 'inversify'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { SCM_VIEW_CONTAINER_ID } from '../scm-contribution'; +import { ScmService } from '../scm-service'; +import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator'; +import { Title, Widget } from '@theia/core/lib/browser'; +import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; + +@injectable() +export class ScmTabBarDecorator implements TabBarDecorator { + + readonly id = 'theia-scm-tabbar-decorator'; + protected readonly emitter = new Emitter(); + + private readonly toDispose = new DisposableCollection(); + private readonly toDisposeOnDidChange = new DisposableCollection(); + + @inject(ScmService) + protected readonly scmService: ScmService; + + @postConstruct() + protected init(): void { + this.toDispose.push(this.scmService.onDidChangeSelectedRepository(repository => { + this.toDisposeOnDidChange.dispose(); + if (repository) { + this.toDisposeOnDidChange.push( + repository.provider.onDidChange(() => this.fireDidChangeDecorations()) + ); + } + this.fireDidChangeDecorations(); + })); + } + + decorate(title: Title): WidgetDecoration.Data[] { + if (title.owner.id === SCM_VIEW_CONTAINER_ID) { + const changes = this.collectChangesCount(); + return changes > 0 ? [{ badge: changes }] : []; + } else { + return []; + } + } + + protected collectChangesCount(): number { + const repository = this.scmService.selectedRepository; + let changes = 0; + if (!repository) { + return 0; + } + repository.provider.groups.map(group => { + if (group.id === 'index' || group.id === 'workingTree') { + changes += group.resources.length; + } + }); + return changes; + } + + get onDidChangeDecorations(): Event { + return this.emitter.event; + } + + protected fireDidChangeDecorations(): void { + this.emitter.fire(undefined); + } + +} diff --git a/packages/scm/src/browser/scm-frontend-module.ts b/packages/scm/src/browser/scm-frontend-module.ts index c498d2f1a6f95..bf87c34ae6333 100644 --- a/packages/scm/src/browser/scm-frontend-module.ts +++ b/packages/scm/src/browser/scm-frontend-module.ts @@ -45,6 +45,8 @@ import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider'; import { bindScmPreferences } from './scm-preferences'; +import { ScmTabBarDecorator } from './decorations/scm-tab-bar-decorator'; +import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator'; export default new ContainerModule(bind => { bind(ScmContextKeyService).toSelf().inSingletonScope(); @@ -118,6 +120,9 @@ export default new ContainerModule(bind => { bind(LabelProviderContribution).toService(ScmTreeLabelProvider); bindScmPreferences(bind); + + bind(ScmTabBarDecorator).toSelf().inSingletonScope(); + bind(TabBarDecorator).toService(ScmTabBarDecorator); }); export function createScmTreeContainer(parent: interfaces.Container): Container {