Skip to content

Commit d818971

Browse files
authored
Add the tree-widget example to the generator (#241)
Signed-Off-By: Stefan Winkler <stefan@winklerweb.net>
1 parent a26218d commit d818971

13 files changed

+864
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The generator allows to generate an example extension that is directly part of t
5353
| `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) |
5454
| `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) |
5555
| `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) |
56+
| `tree-widget` | Creates a tree view extension | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/tree-widget/README.md) |
5657
| `empty` | Creates a simple, minimal extension | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/empty/README.md) |
5758
| `backend` | Creates a backend communication extension | [readme](https://github.com/eclipse-theia/generator-theia-extension/blob/master/templates/backend/README.md) |
5859
| `no-extension` | Creates a Theia application without any extension | |

src/app/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ enum ExtensionType {
2626
HelloWorld = 'hello-world',
2727
Widget = 'widget',
2828
LabelProvider = 'labelprovider',
29+
TreeEditor = 'tree-editor',
30+
TreeWidget = 'tree-widget',
2931
Empty = 'empty',
3032
Backend = 'backend',
3133
NoExtension = 'no-extension'
@@ -174,6 +176,7 @@ module.exports = class TheiaExtension extends Base {
174176
{ value: ExtensionType.HelloWorld, name: 'Hello World' },
175177
{ value: ExtensionType.Widget, name: 'Widget (with unit tests)' },
176178
{ value: ExtensionType.LabelProvider, name: 'LabelProvider' },
179+
{ value: ExtensionType.TreeWidget, name: 'TreeWidget View' },
177180
{ value: ExtensionType.Backend, name: 'Backend Communication' },
178181
{ value: ExtensionType.Empty, name: 'Empty' },
179182
{ value: ExtensionType.NoExtension, name: 'No Extension (just a Theia application)' }
@@ -429,6 +432,29 @@ module.exports = class TheiaExtension extends Base {
429432
{ params: this.params }
430433
);
431434
}
435+
436+
/** TreeWidget */
437+
if (this.params.extensionType === ExtensionType.TreeWidget) {
438+
['treeview-example-widget.tsx',
439+
'treeview-example-view-contribution.ts',
440+
'treeview-example-tree.ts',
441+
'treeview-example-tree-item-factory.ts',
442+
'treeview-example-model.ts',
443+
'treeview-example-label-provider.ts',
444+
'README.md',
445+
'styles',
446+
'decorator'].forEach((file) =>
447+
this.fs.copyTpl(
448+
this.templatePath(`tree-widget/${file}`),
449+
this.extensionPath(`src/browser/${file}`),
450+
{ params: this.params }
451+
));
452+
453+
this.fs.copyTpl(
454+
this.templatePath('tree-widget/treeview-example-frontend-module.ts'),
455+
this.extensionPath(`src/browser/${this.params.extensionPath}-frontend-module.ts`),
456+
);
457+
}
432458
}
433459

434460
protected extensionPath(...paths: string[]) {

templates/tree-widget/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Example TreeWidget implementation
2+
3+
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.
4+
5+
## How to use the label provider example
6+
7+
In the running application, from the Command Palette, execute _View: Toggle Example Tree View ..._.
8+
9+
## Further Reading
10+
11+
This example is accompanied by a tutorial that is available [here](https://theia-ide.org/docs/tree_widget/).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ContributionProvider } from '@theia/core';
2+
import { AbstractTreeDecoratorService, TreeDecorator } from '@theia/core/lib/browser';
3+
import { inject, injectable, named } from '@theia/core/shared/inversify';
4+
5+
export const TreeviewExampleDecorator = Symbol('TreeviewExampleDecorator');
6+
7+
/**
8+
* The TreeDecoratorService which manages the TreeDecorator contributions for our tree widget implementation.
9+
* (Every tree widget has its own TreeDecoratorService instance to manage decorations specifically for that widget.)
10+
*/
11+
@injectable()
12+
export class TreeviewExampleDecorationService extends AbstractTreeDecoratorService {
13+
constructor(@inject(ContributionProvider) @named(TreeviewExampleDecorator) protected readonly contributions: ContributionProvider<TreeDecorator>) {
14+
super(contributions.getContributions());
15+
}
16+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Emitter, MaybePromise } from '@theia/core';
2+
import { DepthFirstTreeIterator, Tree, TreeDecorator } from '@theia/core/lib/browser';
3+
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
4+
import { Event } from '@theia/core/lib/common';
5+
import { injectable } from '@theia/core/shared/inversify';
6+
import { ExampleTreeLeaf } from '../treeview-example-model';
7+
8+
/**
9+
* Example TreeDecorator implementation for our tree widget.
10+
*/
11+
@injectable()
12+
export class TreeviewExampleDemoDecorator implements TreeDecorator {
13+
/** Decorator id - required by the TreeDecorator interface */
14+
id = 'TreeviewExampleDecorator';
15+
16+
/** Event Emitter for when the decorations change - required by the TreeDecorator interface */
17+
protected readonly emitter = new Emitter<(tree: Tree) => Map<string, WidgetDecoration.Data>>();
18+
get onDidChangeDecorations(): Event<(tree: Tree) => Map<string, WidgetDecoration.Data>> {
19+
return this.emitter.event;
20+
}
21+
22+
/**
23+
* The actual decoration calculation.
24+
*
25+
* In contrast to label providers, decorators provide decorations for the complete tree at once.
26+
*
27+
* @param tree the tree to decorate.
28+
* @returns a Map of node IDs mapped to decorations.
29+
*/
30+
decorations(tree: Tree): MaybePromise<Map<string, WidgetDecoration.Data>> {
31+
const result = new Map();
32+
33+
if (tree.root === undefined) {
34+
return result;
35+
}
36+
37+
// iterate the tree
38+
for (const treeNode of new DepthFirstTreeIterator(tree.root)) {
39+
// in our case, we only decorate leaf nodes
40+
if (ExampleTreeLeaf.is(treeNode)) {
41+
// we distinguish between high and low stock levels based on the quantity
42+
const amount = treeNode.data.quantity || 0;
43+
if (amount > 4) {
44+
// we use a green checkmark icon decoration for high stock levels
45+
result.set(treeNode.id, <WidgetDecoration.Data>{
46+
iconOverlay: {
47+
position: WidgetDecoration.IconOverlayPosition.BOTTOM_RIGHT,
48+
iconClass: ['fa', 'fa-check-circle'],
49+
color: 'green'
50+
}
51+
});
52+
} else {
53+
// for low stock levels, we use a red background color and a warning text suffix
54+
result.set(treeNode.id, <WidgetDecoration.Data>{
55+
backgroundColor: 'red',
56+
captionSuffixes: [{ data: 'Warning: low stock', fontData: { style: 'italic' } }]
57+
});
58+
}
59+
}
60+
}
61+
return result;
62+
}
63+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.theia-example-tree-node .a {
2+
padding-right: 4px
3+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { bindContributionProvider } from '@theia/core';
2+
import { bindViewContribution, createTreeContainer, LabelProviderContribution, WidgetFactory } from '@theia/core/lib/browser';
3+
import { Container, ContainerModule, interfaces } from '@theia/core/shared/inversify';
4+
import { TreeviewExampleDecorationService, TreeviewExampleDecorator } from './decorator/treeview-example-decoration-service';
5+
import { TreeviewExampleDemoDecorator } from './decorator/treeview-example-demo-decorator';
6+
import { TreeViewExampleLabelProvider } from './treeview-example-label-provider';
7+
import { TreeViewExampleModel } from './treeview-example-model';
8+
import { TreeviewExampleTree } from './treeview-example-tree';
9+
import { TreeviewExampleViewContribution } from './treeview-example-view-contribution';
10+
import { TREEVIEW_EXAMPLE_CONTEXT_MENU, TreeViewExampleWidget } from './treeview-example-widget';
11+
import { TreeViewExampleTreeItemFactory } from './treeview-example-tree-item-factory';
12+
13+
/**
14+
* Frontend contribution bindings.
15+
*/
16+
export default new ContainerModule(bind => {
17+
bindViewContribution(bind, TreeviewExampleViewContribution);
18+
19+
bind(WidgetFactory).toDynamicValue(ctx => ({
20+
id: TreeViewExampleWidget.ID,
21+
createWidget: () => createTreeViewExampleViewContainer(ctx.container).get(TreeViewExampleWidget)
22+
})).inSingletonScope();
23+
24+
bind(TreeViewExampleModel).toSelf().inSingletonScope();
25+
bind(LabelProviderContribution).to(TreeViewExampleLabelProvider);
26+
27+
bind(TreeviewExampleDemoDecorator).toSelf().inSingletonScope();
28+
bind(TreeviewExampleDecorator).toService(TreeviewExampleDemoDecorator);
29+
});
30+
31+
/**
32+
* Create the child container which contains the `TreeViewExampleWidget` and all its collaborators
33+
* in an isolated child container so the bound services affect only the `TreeViewExampleWidget`
34+
*
35+
* @param parent the parent container
36+
* @returns the new child container
37+
*/
38+
function createTreeViewExampleViewContainer(parent: interfaces.Container): Container {
39+
const child = createTreeContainer(parent, {
40+
tree: TreeviewExampleTree,
41+
model: TreeViewExampleModel,
42+
widget: TreeViewExampleWidget,
43+
props: {
44+
contextMenuPath: TREEVIEW_EXAMPLE_CONTEXT_MENU,
45+
multiSelect: false,
46+
search: true,
47+
expandOnlyOnExpansionToggleClick: false
48+
},
49+
decoratorService: TreeviewExampleDecorationService,
50+
});
51+
bindContributionProvider(child, TreeviewExampleDecorator);
52+
child.bind(TreeViewExampleTreeItemFactory).toSelf().inSingletonScope();
53+
return child;
54+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Emitter, Event } from '@theia/core';
2+
import { wait } from '@theia/core/lib/common/promise-util'
3+
import { DidChangeLabelEvent, LabelProviderContribution, TreeNode } from '@theia/core/lib/browser';
4+
import { injectable } from '@theia/core/shared/inversify';
5+
import { ExampleTreeLeaf, ExampleTreeNode } from './treeview-example-model';
6+
7+
/**
8+
* Provider for labels and icons for the `TreeViewExampleWidget`
9+
*/
10+
@injectable()
11+
export class TreeViewExampleLabelProvider implements LabelProviderContribution {
12+
/**
13+
* Emitter for the event that is emitted when the label of a tree item changes.
14+
*/
15+
protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
16+
17+
/**
18+
* Decides whether this label provider can provide labels for the given object (in this case only
19+
* nodes in the TreeViewExampleWidget tree).
20+
*
21+
* @param element the element to consider
22+
* @returns 0 if this label provider cannot handle the element, otherwise a positive integer indicating a
23+
* priority. The framework chooses the provider with the highest priority for the given element.
24+
*/
25+
canHandle(element: object): number {
26+
if ((ExampleTreeNode.is(element) || ExampleTreeLeaf.is(element))) {
27+
return 100;
28+
}
29+
return 0;
30+
}
31+
32+
/**
33+
* Provides the name for the given tree node.
34+
*
35+
* This example demonstrates a name that is partially resolved asynchronously.
36+
* Whenever a name is requested for an `ExampleTreeLeaf` for the first time, a timer
37+
* is scheduled. After the timer resolves, the quantity from the model is reported.
38+
* In the meantime, a "calculating..." label is shown.
39+
*
40+
* This works by emitting a label change event when the Promise is resolved.
41+
*
42+
* @param element the element for which the name shall be retrieved
43+
* @returns the name of this element
44+
*/
45+
getName(element: object): string | undefined {
46+
// simple implementation for nodes:
47+
if (ExampleTreeNode.is(element)) {
48+
return element.data.name;
49+
}
50+
51+
// in case of leaves, we simulate asynchronous retrieval
52+
if (ExampleTreeLeaf.is(element)) {
53+
if (!element.quantityLabel) {
54+
// if the quantityLabel is not yet set (not even 'calculating ...'), we schedule its retrieval
55+
// by simulating a delay using wait(). In practice, you would call an expensive function returnung an
56+
// actual promise instead of calling wait().
57+
element.quantityLabel = 'calculating ...';
58+
wait(1000).then(() => {
59+
element.quantityLabel = `${element.data.quantity}`;
60+
this.fireNodeChange(element);
61+
});
62+
}
63+
64+
// assemble the complete name from its parts
65+
const orderedLabel = element.data.backOrdered ? ' - more are ordered' : '';
66+
return element.data.name + ` (${element.quantityLabel + orderedLabel})`;
67+
}
68+
69+
// this should not happen, because the canHandle() would only return >0 for the tree node types
70+
return undefined;
71+
}
72+
73+
/**
74+
* Provides an icon (in this case, a fontawesome icon name without the fa- prefix, as the TreeWidget provides built-in support
75+
* for fontawesome icons).
76+
*
77+
* @param element the element for which to provide the icon
78+
* @returns the icon
79+
*/
80+
getIcon(element: object): string | undefined {
81+
if (ExampleTreeNode.is(element)) {
82+
return 'folder';
83+
}
84+
if (ExampleTreeLeaf.is(element)) {
85+
return 'smile-o';
86+
}
87+
88+
return undefined;
89+
}
90+
91+
/**
92+
* Fire the node change event.
93+
*
94+
* @param node the node that has been changed
95+
*/
96+
fireNodeChange(node: TreeNode): void {
97+
this.onDidChangeEmitter.fire({
98+
// The element here is the tree row which has a `node` property
99+
// Since we know exactly which node we have changed, we can match the changed node with the tree row's node
100+
affects: (element: object) => 'node' in element && element.node === node
101+
});
102+
}
103+
104+
/**
105+
* Accessor for the emitter (defined by the LabelProviderContribution interface)
106+
*/
107+
get onDidChange(): Event<DidChangeLabelEvent> {
108+
return this.onDidChangeEmitter.event;
109+
}
110+
}

0 commit comments

Comments
 (0)