Skip to content

Add an example for the TreeWidget #15127

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

Closed
wants to merge 3 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-f
import { bindChatNodeToolbarActionContribution } from './chat/chat-node-toolbar-action-contribution';
import { bindAskAndContinueChatAgentContribution } from './chat/ask-and-continue-chat-agent-contribution';
import { bindChangeSetChatAgentContribution } from './chat/change-set-chat-agent-contribution';
import { bindTreeViewExample } from './tree-widget/treeview-example-frontend-module';

export default new ContainerModule((
bind: interfaces.Bind,
Expand All @@ -56,4 +57,5 @@ export default new ContainerModule((
bindTestSample(bind);
bindSampleFileSystemCapabilitiesCommands(bind);
rebindOVSXClientFactory(rebind);
bindTreeViewExample(bind);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2025 Stefan Winkler 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-only WITH Classpath-exception-2.0
// *****************************************************************************

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

@injectable()
export class TreeviewExampleDecorationService extends AbstractTreeDecoratorService {
constructor(@inject(ContributionProvider) @named(TreeviewExampleDecorator) protected readonly contributions: ContributionProvider<TreeDecorator>) {
super(contributions.getContributions());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2025 Stefan Winkler 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-only WITH Classpath-exception-2.0
// *****************************************************************************

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';

@injectable()
export class TreeviewExampleDemoDecorator implements TreeDecorator {
id = 'TreeviewExampleDecorator';

protected readonly emitter = new Emitter<(tree: Tree) => Map<string, WidgetDecoration.Data>>();

get onDidChangeDecorations(): Event<(tree: Tree) => Map<string, WidgetDecoration.Data>> {
return this.emitter.event;
}

decorations(tree: Tree): MaybePromise<Map<string, WidgetDecoration.Data>> {
const result = new Map();

if (tree.root === undefined) {
return result;
}
for (const treeNode of new DepthFirstTreeIterator(tree.root)) {
if (ExampleTreeLeaf.is(treeNode)) {
const amount = treeNode.data.quantity || 0;
if (amount > 4) {
result.set(treeNode.id, <WidgetDecoration.Data>{
iconOverlay: {
position: WidgetDecoration.IconOverlayPosition.BOTTOM_RIGHT,
iconClass: ['fa', 'fa-check-circle'],
color: 'green'
}
});
} else {
result.set(treeNode.id, <WidgetDecoration.Data>{
backgroundColor: 'red',
captionSuffixes: [{ data: 'Warning: low stock', fontData: { style: 'italic' } }]
});
}
}
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// *****************************************************************************
// Copyright (C) 2025 Stefan Winkler 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { bindContributionProvider } from '@theia/core';
import { bindViewContribution, createTreeContainer, LabelProviderContribution, WidgetFactory } from '@theia/core/lib/browser';
import { Container, 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';

/**
* Frontend contribution bindings.
*
* (in a standalone extension, this would be `export default new ContainerModule(bind => { ... })`)
*
* @param bind the binding function
*/
export function bindTreeViewExample(bind: interfaces.Bind): void {
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);
return child;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// *****************************************************************************
// Copyright (C) 2025 Stefan Winkler 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { Emitter, Event } from '@theia/core';
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 setTimeout().
element.quantityLabel = 'calculating ...';
element.quantityLabelPromise = new Promise(resolve => setTimeout(() => resolve(`${element.data.quantity}`), 1000));

// after the detail has been retrieved, set the quantityLabel to its final value and emit a change event
element.quantityLabelPromise.then(quantity => {
element.quantityLabel = 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