Skip to content

Commit

Permalink
Add tree editor example
Browse files Browse the repository at this point in the history
Adds a new template `Tree Editor` to the generator which allows to generate an extension with a tree master detail editor.
Additionally, add a new menu Tree Editor with an entry to generate an example file to open in the editor.
The tree editor uses a coffee machine example model.

Co-authored-by: Florian Gareis <[email protected]>
Co-authored-by: Jonas Helming <[email protected]>
Signed-off-by: Lucas Koehler <[email protected]>
  • Loading branch information
3 people authored and vince-fugnitto committed Jan 4, 2021
1 parent 05ead39 commit 7f2af8c
Show file tree
Hide file tree
Showing 16 changed files with 1,130 additions and 3 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ yo theia-extension
Extension options
1. `hello-world`: creates a simple extension which provides a command and menu item which displays a message.
2. `widget`: creates the basis for a simple widget including a toggle command, alert message and button displaying a message.
3. `labelprovider`: create a simple extension which adds a custom label (with icon) for `.my` files
3. `labelprovider`: create a simple extension which adds a custom label (with icon) for `.my` files.
4. `tree-editor`: create a tree editor extension.

For configuration options, see

Expand Down Expand Up @@ -60,4 +61,3 @@ Publish with [np](https://github.com/sindresorhus/np#np--).
## Trademark
"Theia" is a trademark of the Eclipse Foundation
https://www.eclipse.org/theia

27 changes: 27 additions & 0 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum ExtensionType {
HelloWorld = 'hello-world',
Widget = 'widget',
LabelProvider = 'labelprovider',
TreeEditor = 'tree-editor',
Empty = 'empty',
Backend = 'backend'
}
Expand All @@ -44,6 +45,7 @@ module.exports = class TheiaExtension extends Base {
lernaVersion: string
skipInstall: boolean
standalone: boolean
dependencies: string
};

constructor(args: string | string[], options: any) {
Expand Down Expand Up @@ -147,6 +149,7 @@ module.exports = class TheiaExtension extends Base {
{ value: ExtensionType.HelloWorld, name: 'Hello World' },
{ value: ExtensionType.Widget, name: 'Widget' },
{ value: ExtensionType.LabelProvider, name: 'LabelProvider' },
{ value: ExtensionType.TreeEditor, name: 'TreeEditor' },
{ value: ExtensionType.Backend, name: 'Backend Communication' },
{ value: ExtensionType.Empty, name: 'Empty' }
]
Expand Down Expand Up @@ -189,6 +192,11 @@ module.exports = class TheiaExtension extends Base {
lernaVersion: options["lerna-version"],
backend: options["extensionType"] === ExtensionType.Backend
}
if (this.params.extensionType === ExtensionType.TreeEditor) {
this.params.dependencies = `,\n "@theia/editor": "${this.params.theiaVersion}",\n "@theia/filesystem": "${this.params.theiaVersion}",\n "@theia/workspace": "${this.params.theiaVersion}",\n "@eclipse-emfcloud/theia-tree-editor": "latest",\n "uuid": "^3.3.2"`;
} else {
this.params.dependencies = '';
}
options.params = this.params
if (!options.standalone) {
if ((options).browser)
Expand Down Expand Up @@ -349,6 +357,25 @@ module.exports = class TheiaExtension extends Base {
);
}

/** tree-editor */
if (this.params.extensionType === ExtensionType.TreeEditor) {
this.fs.copyTpl(
this.templatePath('tree-editor/'),
this.extensionPath(`src/browser/`),
{ params: this.params }
);
this.fs.move(
this.extensionPath('src/browser/README.md'),
this.extensionPath(`README.md`),
{ params: this.params }
);
this.fs.move(
this.extensionPath('src/browser/tree-frontend-module.ts'),
this.extensionPath(`src/browser/${this.params.extensionPath}-frontend-module.ts`),
{ params: this.params }
);
}

}

protected extensionPath(...paths: string[]) {
Expand Down
2 changes: 1 addition & 1 deletion templates/extension-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"src"
],
"dependencies": {
"@theia/core": "<%= params.theiaVersion %>"
"@theia/core": "<%= params.theiaVersion %>"<% if (params.dependencies) { %><%- params.dependencies %><% } %>
},
"devDependencies": {
"rimraf": "latest",
Expand Down
10 changes: 10 additions & 0 deletions templates/tree-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Example tree editor

The example extension demonstrates how to build tree editors in Eclipse Theia, which show the content of JSON files.

## How to use the example tree editor

In the running application, open the menu "Tree Editor"=>"New Example File".
The created `.tree` file will automatically be opened with the example tree editor.
The left side of the editor shows the hierarchy of the JSON data allowing you to create new children and delete nodes.
The right site shows the properties of a selected node and allows the modification of the same.
102 changes: 102 additions & 0 deletions templates/tree-editor/example-file/example-file-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { SingleTextInputDialog } from '@theia/core/lib/browser/dialogs';
import { ILogger } from '@theia/core/lib/common';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { Command } from '@theia/core/lib/common/command';
import URI from '@theia/core/lib/common/uri';
import { SingleUriCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileSystemUtils } from '@theia/filesystem/lib/common';
import { inject, injectable } from 'inversify';
import { OpenerService } from '@theia/core/lib/browser';

export const NewTreeExampleFileCommand: Command = {
id: '<%= params.extensionPath %>-tree.newExampleFile',
label: 'New Tree Example File'
};

@injectable()
export class NewTreeExampleFileCommandHandler implements SingleUriCommandHandler {
constructor(
@inject(OpenerService)
protected readonly openerService: OpenerService,
@inject(FileService)
protected readonly fileService: FileService,
@inject(ILogger)
protected readonly logger: ILogger,
) { }

async execute(uri: URI) {
const stat = await this.fileService.resolve(uri);
if (!stat) {
this.logger.error(`[NewTreeExampleFileCommandHandler] Could not create file stat for uri`, uri);
return;
}

const dir = stat.isDirectory ? stat : await this.fileService.resolve(uri.parent);
if (!dir) {
this.logger.error(`[NewTreeExampleFileCommandHandler] Could not create file stat for uri`, uri.parent);
return;
}

const dirUri = dir.resource;
const preliminaryFileUri = FileSystemUtils.generateUniqueResourceURI(dirUri, dir, 'tree-example', '.tree');
const dialog = new SingleTextInputDialog({
title: 'New Example File',
initialValue: preliminaryFileUri.path.base
});

const fileName = await dialog.open();
if (fileName) {
const fileUri = dirUri.resolve(fileName);
const contentBuffer = BinaryBuffer.fromString(JSON.stringify(defaultData, null, 2));
this.fileService.createFile(fileUri, contentBuffer)
.then(_ => this.openerService.getOpener(fileUri))
.then(openHandler => openHandler.open(fileUri));
}
}
}

const defaultData = {
"typeId": "Machine",
"name": "Super Coffee 4000",
"children": [
{
"typeId": "ControlUnit",
"processor": {
"socketconnectorType": "A1T",
"manufactoringProcess": "18nm",
"thermalDesignPower": 10,
"numberOfCores": 2,
"clockSpeed": 800,
"vendor": "CMD",
"advancedConfiguration": true
},
"display": {
"width": 70,
"height": 40
},
"dimension": {
"width": 100,
"height": 80,
"length": 50
},
"userDescription": "Small processing unit for user input"
},
{
"typeId": "MultiComponent",
"width": 100,
"height": 100,
"length": 60,
"children": [
{
"typeId":"WaterTank",
"capacity":400
},
{
"typeId":"DripTray",
"material":"aluminium"
}
]
}
]
}
43 changes: 43 additions & 0 deletions templates/tree-editor/example-file/example-file-contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService, MAIN_MENU_BAR } from '@theia/core/lib/common';
import { inject, injectable } from 'inversify';
import { WorkspaceRootUriAwareCommandHandler } from '@theia/workspace/lib/browser/workspace-commands';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { NewTreeExampleFileCommandHandler, NewTreeExampleFileCommand } from './example-file-command';

const TREE_EDITOR_MAIN_MENU = [...MAIN_MENU_BAR, '9_treeeditormenu'];

@injectable()
export class NewTreeExampleFileCommandContribution implements CommandContribution {

constructor(
@inject(SelectionService)
private readonly selectionService: SelectionService,
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService,
@inject(NewTreeExampleFileCommandHandler)
private readonly newExampleFileHandler: NewTreeExampleFileCommandHandler
) { }

registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewTreeExampleFileCommand,
new WorkspaceRootUriAwareCommandHandler(
this.workspaceService,
this.selectionService,
this.newExampleFileHandler
)
);
}
}

@injectable()
export class NewTreeExampleFileMenuContribution implements MenuContribution {

registerMenus(menus: MenuModelRegistry): void {
menus.registerSubmenu(TREE_EDITOR_MAIN_MENU, 'Tree Editor');

menus.registerMenuAction(TREE_EDITOR_MAIN_MENU, {
commandId: NewTreeExampleFileCommand.id,
label: 'New Example File'
});
}
}
8 changes: 8 additions & 0 deletions templates/tree-editor/style/editor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.theia-tree-editor-form {
display: flex;
}

.theia-tree-editor-form > .jsonforms-container {
flex-grow: 1;
max-width: 960px; /* Half the width of a full hd display. */
}
60 changes: 60 additions & 0 deletions templates/tree-editor/tree-contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { CommandRegistry, MenuModelRegistry } from '@theia/core';
import { ApplicationShell, NavigatableWidgetOptions, OpenerService, WidgetOpenerOptions } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from 'inversify';
import {
BaseTreeEditorContribution,
MasterTreeWidget,
TreeEditor,
} from '@eclipse-emfcloud/theia-tree-editor';

import { TreeModelService } from './tree/tree-model-service';
import { TreeEditorWidget } from './tree/tree-editor-widget';
import { TreeLabelProvider } from './tree/tree-label-provider';

@injectable()
export class TreeContribution extends BaseTreeEditorContribution {
@inject(ApplicationShell) protected shell: ApplicationShell;
@inject(OpenerService) protected opener: OpenerService;

constructor(
@inject(TreeModelService) modelService: TreeEditor.ModelService,
@inject(TreeLabelProvider) labelProvider: TreeLabelProvider
) {
super(TreeEditorWidget.WIDGET_ID, modelService, labelProvider);
}

readonly id = TreeEditorWidget.WIDGET_ID;
readonly label = MasterTreeWidget.WIDGET_LABEL;

canHandle(uri: URI): number {
if (uri.path.ext === '.tree') {
return 1000;
}
return 0;
}

registerCommands(commands: CommandRegistry): void {
// register your custom commands here

super.registerCommands(commands);
}

registerMenus(menus: MenuModelRegistry): void {
// register your custom menu actions here

super.registerMenus(menus);
}

protected createWidgetOptions(uri: URI, options?: WidgetOpenerOptions): NavigatableWidgetOptions {
return {
kind: 'navigatable',
uri: this.serializeUri(uri)
};
}

protected serializeUri(uri: URI): string {
return uri.withoutFragment().toString();
}

}
55 changes: 55 additions & 0 deletions templates/tree-editor/tree-frontend-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import '@eclipse-emfcloud/theia-tree-editor/style/index.css';
import '@eclipse-emfcloud/theia-tree-editor/style/forms.css';
import '../../src/browser/style/editor.css';

import { CommandContribution, MenuContribution } from '@theia/core';
import { LabelProviderContribution, NavigatableWidgetOptions, OpenHandler, WidgetFactory } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { ContainerModule } from 'inversify';
import { createBasicTreeContainter, NavigatableTreeEditorOptions } from '@eclipse-emfcloud/theia-tree-editor';

import { TreeContribution } from './tree-contribution';
import { TreeModelService } from './tree/tree-model-service';
import { TreeNodeFactory } from './tree/tree-node-factory';
import { TreeEditorWidget } from './tree/tree-editor-widget';
import { TreeLabelProvider } from './tree/tree-label-provider';
import { TreeLabelProviderContribution } from './tree-label-provider-contribution';
import { NewTreeExampleFileCommandHandler } from './example-file/example-file-command';
import { NewTreeExampleFileCommandContribution, NewTreeExampleFileMenuContribution } from './example-file/example-file-contribution';

export default new ContainerModule(bind => {
// Bind Theia IDE contributions for the example file creation menu entry.
bind(NewTreeExampleFileCommandHandler).toSelf();
bind(CommandContribution).to(NewTreeExampleFileCommandContribution);
bind(MenuContribution).to(NewTreeExampleFileMenuContribution)

// Bind Theia IDE contributions for the tree editor.
bind(LabelProviderContribution).to(TreeLabelProviderContribution);
bind(OpenHandler).to(TreeContribution);
bind(MenuContribution).to(TreeContribution);
bind(CommandContribution).to(TreeContribution);
bind(LabelProviderContribution).to(TreeLabelProvider);

// bind services to themselves because we use them outside of the editor widget, too.
bind(TreeModelService).toSelf().inSingletonScope();
bind(TreeLabelProvider).toSelf().inSingletonScope();

bind<WidgetFactory>(WidgetFactory).toDynamicValue(context => ({
id: TreeEditorWidget.WIDGET_ID,
createWidget: (options: NavigatableWidgetOptions) => {

const treeContainer = createBasicTreeContainter(
context.container,
TreeEditorWidget,
TreeModelService,
TreeNodeFactory
);

// Bind options.
const uri = new URI(options.uri);
treeContainer.bind(NavigatableTreeEditorOptions).toConstantValue({ uri });

return treeContainer.get(TreeEditorWidget);
}
}));
});
26 changes: 26 additions & 0 deletions templates/tree-editor/tree-label-provider-contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LabelProviderContribution } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { FileStat } from '@theia/filesystem/lib/common';
import { injectable } from 'inversify';

@injectable()
export class TreeLabelProviderContribution implements LabelProviderContribution {
canHandle(uri: object): number {
let toCheck = uri;
if (FileStat.is(toCheck)) {
toCheck = new URI(toCheck.uri);
}
if (toCheck instanceof URI) {
if (toCheck.path.ext === '.tree') {
return 1000;
}
}
return 0;
}

getIcon(): string {
return 'fa fa-coffee dark-purple';
}

// We don't need to specify getName() nor getLongName() because the default uri label provider is responsible for them.
}
Loading

0 comments on commit 7f2af8c

Please sign in to comment.