Skip to content

Commit

Permalink
KM round 2
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <colin.grant@ericsson.com>
  • Loading branch information
colin-grant-work committed Aug 25, 2021
1 parent 8ebc75f commit 34bf101
Show file tree
Hide file tree
Showing 18 changed files with 107 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Emitter, Event } from '../../common';
import { Disposable, DisposableCollection } from '../../common/disposable';
import { Coordinate } from '../context-menu-renderer';
import { RendererHost } from '../widgets/react-renderer';
import { Breadcrumbs } from './breadcrumbs';
import { Styles } from './breadcrumbs-constants';

export interface BreadcrumbPopupContainerFactory {
(parent: HTMLElement, breadcrumbId: string, position: Coordinate): BreadcrumbPopupContainer;
Expand Down Expand Up @@ -68,7 +68,7 @@ export class BreadcrumbPopupContainer implements Disposable {

protected createPopupDiv(position: Coordinate): HTMLDivElement {
const result = window.document.createElement('div');
result.className = Breadcrumbs.Styles.BREADCRUMB_POPUP;
result.className = Styles.BREADCRUMB_POPUP;
result.style.left = `${position.x}px`;
result.style.top = `${position.y}px`;
result.tabIndex = 0;
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

import * as React from 'react';
import { injectable } from 'inversify';
import { Breadcrumb } from './breadcrumb';
import { Breadcrumbs } from './breadcrumbs';
import { Breadcrumb, Styles } from './breadcrumbs-constants';

export const BreadcrumbRenderer = Symbol('BreadcrumbRenderer');
export interface BreadcrumbRenderer {
Expand All @@ -31,7 +30,7 @@ export interface BreadcrumbRenderer {
export class DefaultBreadcrumbRenderer implements BreadcrumbRenderer {
render(breadcrumb: Breadcrumb, onMouseDown?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode {
return <li key={breadcrumb.id} title={breadcrumb.longLabel}
className={Breadcrumbs.Styles.BREADCRUMB_ITEM + (!onMouseDown ? '' : ' ' + Breadcrumbs.Styles.BREADCRUMB_ITEM_HAS_POPUP)}
className={Styles.BREADCRUMB_ITEM + (!onMouseDown ? '' : ' ' + Styles.BREADCRUMB_ITEM_HAS_POPUP)}
onMouseDown={event => onMouseDown && onMouseDown(breadcrumb, event)}
tabIndex={0}
data-breadcrumb-id={breadcrumb.id}
Expand Down
34 changes: 0 additions & 34 deletions packages/core/src/browser/breadcrumbs/breadcrumb.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,36 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { MaybePromise, Event } from '../../common';
import { Disposable } from '../../../shared/vscode-languageserver-protocol';
import URI from '../../common/uri';
import { Breadcrumb } from './breadcrumb';
import { Disposable, MaybePromise } from '../../common';

export namespace Styles {
export const BREADCRUMBS = 'theia-breadcrumbs';
export const BREADCRUMB_ITEM = 'theia-breadcrumb-item';
export const BREADCRUMB_POPUP_OVERLAY_CONTAINER = 'theia-breadcrumbs-popups-overlay';
export const BREADCRUMB_POPUP = 'theia-breadcrumbs-popup';
export const BREADCRUMB_ITEM_HAS_POPUP = 'theia-breadcrumb-item-haspopup';
}

/** A single breadcrumb in the breadcrumbs bar. */
export interface Breadcrumb {

/** An ID of this breadcrumb that should be unique in the breadcrumbs bar. */
readonly id: string

/** The breadcrumb type. Should be the same as the contribution type `BreadcrumbsContribution#type`. */
readonly type: symbol

/** The text that will be rendered as label. */
readonly label: string

/** A longer text that will be used as tooltip text. */
readonly longLabel: string

/** A CSS class for the icon. */
readonly iconClass?: string
}

export const BreadcrumbsContribution = Symbol('BreadcrumbsContribution');
export interface BreadcrumbsContribution {
Expand All @@ -31,6 +58,11 @@ export interface BreadcrumbsContribution {
*/
readonly priority: number;

/**
* An event emitter that should fire when breadcrumbs change for a given URI.
*/
readonly onDidChangeBreadcrumbs: Event<URI>;

/**
* Computes breadcrumbs for a given URI.
*/
Expand Down
96 changes: 25 additions & 71 deletions packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
import * as React from 'react';
import { injectable, inject, postConstruct } from 'inversify';
import { ReactRenderer } from '../widgets';
import { Breadcrumb } from './breadcrumb';
import { Breadcrumbs } from './breadcrumbs';
import { BreadcrumbsService } from './breadcrumbs-service';
import { BreadcrumbRenderer } from './breadcrumb-renderer';
import PerfectScrollbar from 'perfect-scrollbar';
Expand All @@ -27,7 +25,8 @@ import { Emitter, Event } from '../../common';
import { BreadcrumbPopupContainer } from './breadcrumb-popup-container';
import { DisposableCollection } from '../../common/disposable';
import { CorePreferences } from '../core-preferences';
import { Coordinate } from '../context-menu-renderer';
import { Breadcrumb, Styles } from './breadcrumbs-constants';
import { LabelProvider } from '../label-provider';

interface Cancelable {
canceled: boolean;
Expand All @@ -45,6 +44,9 @@ export class BreadcrumbsRenderer extends ReactRenderer {
@inject(CorePreferences)
protected readonly corePreferences: CorePreferences;

@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;

protected readonly onDidChangeActiveStateEmitter = new Emitter<boolean>();
get onDidChangeActiveState(): Event<boolean> {
return this.onDidChangeActiveStateEmitter.event;
Expand All @@ -60,21 +62,26 @@ export class BreadcrumbsRenderer extends ReactRenderer {
return !!this.breadcrumbs.length;
}

protected get breadCrumbsContainer(): Element | undefined {
return this.host.firstElementChild ?? undefined;
}

protected refreshCancellationMarker: Cancelable = { canceled: true };

@postConstruct()
init(): void {
protected init(): void {
this.toDispose.push(this.onDidChangeActiveStateEmitter);
this.toDispose.push(this.breadcrumbsService.onDidChangeBreadcrumbs(uri => {
if (this.uri?.isEqual(uri)) {
this.refresh(uri);
this.refresh(this.uri);
}
}));
this.toDispose.push(this.corePreferences.onPreferenceChanged(change => {
if (change.preferenceName === 'breadcrumbs.enabled') {
this.refresh(this.uri);
}
}));
this.toDispose.push(this.labelProvider.onDidChange(() => this.refresh(this.uri)));
}

dispose(): void {
Expand All @@ -88,6 +95,7 @@ export class BreadcrumbsRenderer extends ReactRenderer {
}

async refresh(uri?: URI): Promise<void> {
this.uri = uri;
this.refreshCancellationMarker.canceled = true;
const currentCallCanceled = { canceled: false };
this.refreshCancellationMarker = currentCallCanceled;
Expand All @@ -101,7 +109,6 @@ export class BreadcrumbsRenderer extends ReactRenderer {
return;
}

this.uri = uri;
const wasActive = this.active;
this.breadcrumbs = breadcrumbs;
const isActive = this.active;
Expand All @@ -124,8 +131,9 @@ export class BreadcrumbsRenderer extends ReactRenderer {
}

protected createScrollbar(): void {
if (this.host.firstChild) {
this.scrollbar = new PerfectScrollbar(this.host.firstChild as HTMLElement, {
const { breadCrumbsContainer } = this;
if (breadCrumbsContainer) {
this.scrollbar = new PerfectScrollbar(breadCrumbsContainer, {
handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
useBothWheelAxes: true,
scrollXMarginOffset: 4,
Expand All @@ -135,14 +143,14 @@ export class BreadcrumbsRenderer extends ReactRenderer {
}

protected scrollToEnd(): void {
if (this.host.firstChild) {
const breadcrumbsHtmlElement = (this.host.firstChild as HTMLElement);
breadcrumbsHtmlElement.scrollLeft = breadcrumbsHtmlElement.scrollWidth;
const { breadCrumbsContainer } = this;
if (breadCrumbsContainer) {
breadCrumbsContainer.scrollLeft = breadCrumbsContainer.scrollWidth;
}
}

protected doRender(): React.ReactNode {
return <ul className={Breadcrumbs.Styles.BREADCRUMBS}>{this.renderBreadcrumbs()}</ul>;
return <ul className={Styles.BREADCRUMBS}>{this.renderBreadcrumbs()}</ul>;
}

protected renderBreadcrumbs(): React.ReactNode {
Expand All @@ -163,70 +171,16 @@ export class BreadcrumbsRenderer extends ReactRenderer {
this.popup = undefined;
}
if (openPopup) {
if (event.nativeEvent.target && event.nativeEvent.target instanceof HTMLElement) {
const breadcrumbsHtmlElement = BreadcrumbsRenderer.findParentBreadcrumbsHtmlElement(event.nativeEvent.target as HTMLElement);
if (breadcrumbsHtmlElement && breadcrumbsHtmlElement.parentElement && breadcrumbsHtmlElement.parentElement.lastElementChild) {
const position: Coordinate = BreadcrumbsRenderer.determinePopupAnchor(event.nativeEvent) || event.nativeEvent;
this.breadcrumbsService.openPopup(breadcrumb, position).then(popup => { this.popup = popup; });
}
const { currentTarget } = event;
const breadcrumbElement = currentTarget.closest(`.${Styles.BREADCRUMB_ITEM}`);
if (breadcrumbElement) {
const { left: x, bottom: y } = breadcrumbElement.getBoundingClientRect();
this.breadcrumbsService.openPopup(breadcrumb, { x, y }).then(popup => { this.popup = popup; });
}
}
};
}

export namespace BreadcrumbsRenderer {

/**
* Traverse upstream (starting with the HTML element `child`) to find a parent HTML element
* that has the CSS class `Breadcrumbs.Styles.BREADCRUMB_ITEM`.
*/
export function findParentItemHtmlElement(child: HTMLElement): HTMLElement | undefined {
return findParentHtmlElement(child, Breadcrumbs.Styles.BREADCRUMB_ITEM);
}

/**
* Traverse upstream (starting with the HTML element `child`) to find a parent HTML element
* that has the CSS class `Breadcrumbs.Styles.BREADCRUMBS`.
*/
export function findParentBreadcrumbsHtmlElement(child: HTMLElement): HTMLElement | undefined {
return findParentHtmlElement(child, Breadcrumbs.Styles.BREADCRUMBS);
}

/**
* Traverse upstream (starting with the HTML element `child`) to find a parent HTML element
* that has the given CSS class.
*/
export function findParentHtmlElement(child: HTMLElement, cssClass: string): HTMLElement | undefined {
if (child.classList.contains(cssClass)) {
return child;
} else {
if (child.parentElement) {
return findParentHtmlElement(child.parentElement, cssClass);
}
}
}

/**
* Determines the popup anchor for the given mouse event.
*
* It finds the parent HTML element with CSS class `Breadcrumbs.Styles.BREADCRUMB_ITEM` of event's target element
* and return the bottom left corner of this element.
*/
export function determinePopupAnchor(event: MouseEvent): Coordinate | undefined {
if (!(event.target instanceof HTMLElement)) {
return undefined;
}
const itemHtmlElement = findParentItemHtmlElement(event.target);
if (itemHtmlElement) {
const { left, bottom } = itemHtmlElement.getBoundingClientRect();
return {
x: left,
y: bottom,
};
}
}
}

export const BreadcrumbsRendererFactory = Symbol('BreadcrumbsRendererFactory');
export interface BreadcrumbsRendererFactory {
(): BreadcrumbsRenderer;
Expand Down
27 changes: 18 additions & 9 deletions packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
import { inject, injectable, named, postConstruct } from 'inversify';
import { ContributionProvider, Prioritizeable, Emitter, Event } from '../../common';
import URI from '../../common/uri';
import { Breadcrumb } from './breadcrumb';
import { Coordinate } from '../context-menu-renderer';
import { BreadcrumbPopupContainer, BreadcrumbPopupContainerFactory } from './breadcrumb-popup-container';
import { BreadcrumbsContribution } from './breadcrumbs-contribution';
import { Breadcrumbs } from './breadcrumbs';
import { BreadcrumbsContribution, Styles, Breadcrumb } from './breadcrumbs-constants';

@injectable()
export class BreadcrumbsService {
Expand All @@ -30,6 +29,8 @@ export class BreadcrumbsService {

@inject(BreadcrumbPopupContainerFactory) protected readonly breadcrumbPopupContainerFactory: BreadcrumbPopupContainerFactory;

protected hasSubscribed = false;

protected popupsOverlayContainer: HTMLDivElement;

protected readonly onDidChangeBreadcrumbsEmitter = new Emitter<URI>();
Expand All @@ -41,7 +42,7 @@ export class BreadcrumbsService {

protected createOverlayContainer(): void {
this.popupsOverlayContainer = window.document.createElement('div');
this.popupsOverlayContainer.id = Breadcrumbs.Styles.BREADCRUMB_POPUP_OVERLAY_CONTAINER;
this.popupsOverlayContainer.id = Styles.BREADCRUMB_POPUP_OVERLAY_CONTAINER;
if (window.document.body) {
window.document.body.appendChild(this.popupsOverlayContainer);
}
Expand All @@ -52,15 +53,23 @@ export class BreadcrumbsService {
* The URI is the URI of the editor the breadcrumbs have changed for.
*/
get onDidChangeBreadcrumbs(): Event<URI> {
// This lazy subscription is to address problems in iversify's instantiation routine
// related to use of the IconThemeService by different components instantiated by the
// ContributionProvider.
if (!this.hasSubscribed) {
this.subscribeToContributions();
}
return this.onDidChangeBreadcrumbsEmitter.event;
}

/**
* Notifies that the breadcrumbs for the given URI have changed and should be re-rendered.
* This fires an `onBreadcrumsChange` event.
* Subscribes to the onDidChangeBreadcrumbs events for all contributions
*/
breadcrumbsChanges(uri: URI): void {
this.onDidChangeBreadcrumbsEmitter.fire(uri);
protected subscribeToContributions(): void {
this.hasSubscribed = true;
for (const contribution of this.contributions.getContributions()) {
contribution.onDidChangeBreadcrumbs(uri => this.onDidChangeBreadcrumbsEmitter.fire(uri));
}
}

/**
Expand All @@ -83,7 +92,7 @@ export class BreadcrumbsService {
/**
* Opens a popup for the given breadcrumb at the given position.
*/
async openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }): Promise<BreadcrumbPopupContainer | undefined> {
async openPopup(breadcrumb: Breadcrumb, position: Coordinate): Promise<BreadcrumbPopupContainer | undefined> {
const contribution = this.contributions.getContributions().find(c => c.type === breadcrumb.type);
if (contribution) {
const popup = this.breadcrumbPopupContainerFactory(this.popupsOverlayContainer, breadcrumb.id, position);
Expand Down
25 changes: 0 additions & 25 deletions packages/core/src/browser/breadcrumbs/breadcrumbs.ts

This file was deleted.

Loading

0 comments on commit 34bf101

Please sign in to comment.