Skip to content

Add the tree-widget example to the generator #241

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

Merged
merged 5 commits into from
Apr 28, 2025
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The generator allows to generate an example extension that is directly part of t
| `hello-world` | Creates a simple extension which provides a command and menu item which displays a message | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/hello-world/README.md) |
| `widget` | Creates the basis for a simple widget including a toggle command, alert message and button displaying a message. The template also contains an example unit test. | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/widget/README.md) |
| `labelprovider` | Creates a simple extension which adds a custom label (with icon) for .my files | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/labelprovider/README.md) |
| `tree-widget` | Creates a tree view extension | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/tree-widget/README.md) |
| `empty` | Creates a simple, minimal extension | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/empty/README.md) |
| `backend` | Creates a backend communication extension | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/backend/README.md) |
| `no-extension` | Creates a Theia application without any extension | |
Expand Down
26 changes: 26 additions & 0 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ enum ExtensionType {
HelloWorld = 'hello-world',
Widget = 'widget',
LabelProvider = 'labelprovider',
TreeEditor = 'tree-editor',
TreeWidget = 'tree-widget',
Empty = 'empty',
Backend = 'backend',
NoExtension = 'no-extension'
Expand Down Expand Up @@ -174,6 +176,7 @@ module.exports = class TheiaExtension extends Base {
{ value: ExtensionType.HelloWorld, name: 'Hello World' },
{ value: ExtensionType.Widget, name: 'Widget (with unit tests)' },
{ value: ExtensionType.LabelProvider, name: 'LabelProvider' },
{ value: ExtensionType.TreeWidget, name: 'TreeWidget View' },
{ value: ExtensionType.Backend, name: 'Backend Communication' },
{ value: ExtensionType.Empty, name: 'Empty' },
{ value: ExtensionType.NoExtension, name: 'No Extension (just a Theia application)' }
Expand Down Expand Up @@ -429,6 +432,29 @@ module.exports = class TheiaExtension extends Base {
{ params: this.params }
);
}

/** TreeWidget */
if (this.params.extensionType === ExtensionType.TreeWidget) {
['treeview-example-widget.tsx',
'treeview-example-view-contribution.ts',
'treeview-example-tree.ts',
'treeview-example-tree-item-factory.ts',
'treeview-example-model.ts',
'treeview-example-label-provider.ts',
'README.md',
'styles',
'decorator'].forEach((file) =>
this.fs.copyTpl(
this.templatePath(`tree-widget/${file}`),
this.extensionPath(`src/browser/${file}`),
{ params: this.params }
));

this.fs.copyTpl(
this.templatePath('tree-widget/treeview-example-frontend-module.ts'),
this.extensionPath(`src/browser/${this.params.extensionPath}-frontend-module.ts`),
);
}
}

protected extensionPath(...paths: string[]) {
Expand Down
11 changes: 11 additions & 0 deletions templates/tree-widget/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Example TreeWidget implementation

The example extension demonstrates how to contribute a TreeWidget based view and how to use and customize the different features provided by the Theia `TreeWidget` framework.

## How to use the label provider example

In the running application, from the Command Palette, execute _View: Toggle Example Tree View ..._.

## Further Reading

This example is accompanied by a tutorial that is available [here](https://theia-ide.org/docs/tree_widget/).
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ContributionProvider } from '@theia/core';
import { AbstractTreeDecoratorService, TreeDecorator } from '@theia/core/lib/browser';
import { inject, injectable, named } from '@theia/core/shared/inversify';

export const TreeviewExampleDecorator = Symbol('TreeviewExampleDecorator');

/**
* The TreeDecoratorService which manages the TreeDecorator contributions for our tree widget implementation.
* (Every tree widget has its own TreeDecoratorService instance to manage decorations specifically for that widget.)
*/
@injectable()
export class TreeviewExampleDecorationService extends AbstractTreeDecoratorService {
constructor(@inject(ContributionProvider) @named(TreeviewExampleDecorator) protected readonly contributions: ContributionProvider<TreeDecorator>) {
super(contributions.getContributions());
}
}
63 changes: 63 additions & 0 deletions templates/tree-widget/decorator/treeview-example-demo-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Emitter, MaybePromise } from '@theia/core';
import { DepthFirstTreeIterator, Tree, TreeDecorator } from '@theia/core/lib/browser';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { Event } from '@theia/core/lib/common';
import { injectable } from '@theia/core/shared/inversify';
import { ExampleTreeLeaf } from '../treeview-example-model';

/**
* Example TreeDecorator implementation for our tree widget.
*/
@injectable()
export class TreeviewExampleDemoDecorator implements TreeDecorator {
/** Decorator id - required by the TreeDecorator interface */
id = 'TreeviewExampleDecorator';

/** Event Emitter for when the decorations change - required by the TreeDecorator interface */
protected readonly emitter = new Emitter<(tree: Tree) => Map<string, WidgetDecoration.Data>>();
get onDidChangeDecorations(): Event<(tree: Tree) => Map<string, WidgetDecoration.Data>> {
return this.emitter.event;
}

/**
* The actual decoration calculation.
*
* In contrast to label providers, decorators provide decorations for the complete tree at once.
*
* @param tree the tree to decorate.
* @returns a Map of node IDs mapped to decorations.
*/
decorations(tree: Tree): MaybePromise<Map<string, WidgetDecoration.Data>> {
const result = new Map();

if (tree.root === undefined) {
return result;
}

// iterate the tree
for (const treeNode of new DepthFirstTreeIterator(tree.root)) {
// in our case, we only decorate leaf nodes
if (ExampleTreeLeaf.is(treeNode)) {
// we distinguish between high and low stock levels based on the quantity
const amount = treeNode.data.quantity || 0;
if (amount > 4) {
// we use a green checkmark icon decoration for high stock levels
result.set(treeNode.id, <WidgetDecoration.Data>{
iconOverlay: {
position: WidgetDecoration.IconOverlayPosition.BOTTOM_RIGHT,
iconClass: ['fa', 'fa-check-circle'],
color: 'green'
}
});
} else {
// for low stock levels, we use a red background color and a warning text suffix
result.set(treeNode.id, <WidgetDecoration.Data>{
backgroundColor: 'red',
captionSuffixes: [{ data: 'Warning: low stock', fontData: { style: 'italic' } }]
});
}
}
}
return result;
}
}
3 changes: 3 additions & 0 deletions templates/tree-widget/styles/treeview-example-widget.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.theia-example-tree-node .a {
padding-right: 4px
}
54 changes: 54 additions & 0 deletions templates/tree-widget/treeview-example-frontend-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { bindContributionProvider } from '@theia/core';
import { bindViewContribution, createTreeContainer, LabelProviderContribution, WidgetFactory } from '@theia/core/lib/browser';
import { Container, ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { TreeviewExampleDecorationService, TreeviewExampleDecorator } from './decorator/treeview-example-decoration-service';
import { TreeviewExampleDemoDecorator } from './decorator/treeview-example-demo-decorator';
import { TreeViewExampleLabelProvider } from './treeview-example-label-provider';
import { TreeViewExampleModel } from './treeview-example-model';
import { TreeviewExampleTree } from './treeview-example-tree';
import { TreeviewExampleViewContribution } from './treeview-example-view-contribution';
import { TREEVIEW_EXAMPLE_CONTEXT_MENU, TreeViewExampleWidget } from './treeview-example-widget';
import { TreeViewExampleTreeItemFactory } from './treeview-example-tree-item-factory';

/**
* Frontend contribution bindings.
*/
export default new ContainerModule(bind => {
bindViewContribution(bind, TreeviewExampleViewContribution);

bind(WidgetFactory).toDynamicValue(ctx => ({
id: TreeViewExampleWidget.ID,
createWidget: () => createTreeViewExampleViewContainer(ctx.container).get(TreeViewExampleWidget)
})).inSingletonScope();

bind(TreeViewExampleModel).toSelf().inSingletonScope();
bind(LabelProviderContribution).to(TreeViewExampleLabelProvider);

bind(TreeviewExampleDemoDecorator).toSelf().inSingletonScope();
bind(TreeviewExampleDecorator).toService(TreeviewExampleDemoDecorator);
});

/**
* Create the child container which contains the `TreeViewExampleWidget` and all its collaborators
* in an isolated child container so the bound services affect only the `TreeViewExampleWidget`
*
* @param parent the parent container
* @returns the new child container
*/
function createTreeViewExampleViewContainer(parent: interfaces.Container): Container {
const child = createTreeContainer(parent, {
tree: TreeviewExampleTree,
model: TreeViewExampleModel,
widget: TreeViewExampleWidget,
props: {
contextMenuPath: TREEVIEW_EXAMPLE_CONTEXT_MENU,
multiSelect: false,
search: true,
expandOnlyOnExpansionToggleClick: false
},
decoratorService: TreeviewExampleDecorationService,
});
bindContributionProvider(child, TreeviewExampleDecorator);
child.bind(TreeViewExampleTreeItemFactory).toSelf().inSingletonScope();
return child;
}
110 changes: 110 additions & 0 deletions templates/tree-widget/treeview-example-label-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Emitter, Event } from '@theia/core';
import { wait } from '@theia/core/lib/common/promise-util'
import { DidChangeLabelEvent, LabelProviderContribution, TreeNode } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
import { ExampleTreeLeaf, ExampleTreeNode } from './treeview-example-model';

/**
* Provider for labels and icons for the `TreeViewExampleWidget`
*/
@injectable()
export class TreeViewExampleLabelProvider implements LabelProviderContribution {
/**
* Emitter for the event that is emitted when the label of a tree item changes.
*/
protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();

/**
* Decides whether this label provider can provide labels for the given object (in this case only
* nodes in the TreeViewExampleWidget tree).
*
* @param element the element to consider
* @returns 0 if this label provider cannot handle the element, otherwise a positive integer indicating a
* priority. The framework chooses the provider with the highest priority for the given element.
*/
canHandle(element: object): number {
if ((ExampleTreeNode.is(element) || ExampleTreeLeaf.is(element))) {
return 100;
}
return 0;
}

/**
* Provides the name for the given tree node.
*
* This example demonstrates a name that is partially resolved asynchronously.
* Whenever a name is requested for an `ExampleTreeLeaf` for the first time, a timer
* is scheduled. After the timer resolves, the quantity from the model is reported.
* In the meantime, a "calculating..." label is shown.
*
* This works by emitting a label change event when the Promise is resolved.
*
* @param element the element for which the name shall be retrieved
* @returns the name of this element
*/
getName(element: object): string | undefined {
// simple implementation for nodes:
if (ExampleTreeNode.is(element)) {
return element.data.name;
}

// in case of leaves, we simulate asynchronous retrieval
if (ExampleTreeLeaf.is(element)) {
if (!element.quantityLabel) {
// if the quantityLabel is not yet set (not even 'calculating ...'), we schedule its retrieval
// by simulating a delay using wait(). In practice, you would call an expensive function returnung an
// actual promise instead of calling wait().
element.quantityLabel = 'calculating ...';
wait(1000).then(() => {
element.quantityLabel = `${element.data.quantity}`;
this.fireNodeChange(element);
});
}

// assemble the complete name from its parts
const orderedLabel = element.data.backOrdered ? ' - more are ordered' : '';
return element.data.name + ` (${element.quantityLabel + orderedLabel})`;
}

// this should not happen, because the canHandle() would only return >0 for the tree node types
return undefined;
}

/**
* Provides an icon (in this case, a fontawesome icon name without the fa- prefix, as the TreeWidget provides built-in support
* for fontawesome icons).
*
* @param element the element for which to provide the icon
* @returns the icon
*/
getIcon(element: object): string | undefined {
if (ExampleTreeNode.is(element)) {
return 'folder';
}
if (ExampleTreeLeaf.is(element)) {
return 'smile-o';
}

return undefined;
}

/**
* Fire the node change event.
*
* @param node the node that has been changed
*/
fireNodeChange(node: TreeNode): void {
this.onDidChangeEmitter.fire({
// The element here is the tree row which has a `node` property
// Since we know exactly which node we have changed, we can match the changed node with the tree row's node
affects: (element: object) => 'node' in element && element.node === node
});
}

/**
* Accessor for the emitter (defined by the LabelProviderContribution interface)
*/
get onDidChange(): Event<DidChangeLabelEvent> {
return this.onDidChangeEmitter.event;
}
}
Loading