Skip to content

Commit

Permalink
Make preference node rendering customizable and provide file selection (
Browse files Browse the repository at this point in the history
#10766)

- Introduce preference node render registry to support contributions
-- Convert previous renderer factory content to default contribution
-- Let new renderer factory implementation use service
-- Contributions are ranked similar to open handlers

- Add 'typeDetails' as generic optional property to preference schema
-- Contributions may use type details for further properties

- Provide base for leaf node contributions and render hints

- Implement concrete string renderer for single file selection
-- Usage Example: Windows terminal (external and internal)
  • Loading branch information
martin-fleck-at authored Mar 30, 2022
1 parent ed8aa41 commit 7b8d3b7
Show file tree
Hide file tree
Showing 17 changed files with 456 additions and 54 deletions.
1 change: 1 addition & 0 deletions packages/core/src/common/preferences/preference-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface PreferenceDataProperty extends PreferenceItem {
description?: string;
markdownDescription?: string;
scope?: PreferenceScope;
typeDetails?: any;
}
export namespace PreferenceDataProperty {
export function fromPreferenceSchemaProperty(schemaProps: PreferenceSchemaProperty, defaultScope: PreferenceScope = PreferenceScope.Workspace): PreferenceDataProperty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function getExternalTerminalSchema(externalTerminalService: Externa
properties: {
'terminal.external.windowsExec': {
type: 'string',
typeDetails: { isFilepath: true },
description: nls.localizeByDefault('Customizes which terminal to run on Windows.'),
default: `${isWindows ? hostExec : 'C:\\WINDOWS\\System32\\cmd.exe'}`
},
Expand Down
1 change: 1 addition & 0 deletions packages/preferences/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

@import url("./preference-context-menu.css");
@import url("./preference-array.css");
@import url("./preference-file.css");
@import url("./preference-object.css");
@import url("./search-input.css");

Expand Down
32 changes: 32 additions & 0 deletions packages/preferences/src/browser/style/preference-file.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/********************************************************************************
* Copyright (C) 2022 EclipseSource 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 WITH Classpath-exception-2.0
********************************************************************************/

.theia-settings-container .preference-file-container {
position: relative;
display: flex;
align-items: center;
}

.theia-settings-container .preference-file-input {
flex: 1;
padding-right: 4px;
}

.theia-settings-container .preference-file-button {
position: absolute;
padding: 2px 9px;
right: 4px;
}
5 changes: 5 additions & 0 deletions packages/preferences/src/browser/util/preference-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
CompositeTreeNode as BaseCompositeTreeNode,
PreferenceInspection,
CommonCommands,
JsonType,
} from '@theia/core/lib/browser';
import { Command, MenuPath } from '@theia/core';
import { JSONValue } from '@theia/core/shared/@phosphor/coreutils';
Expand Down Expand Up @@ -69,6 +70,10 @@ export namespace Preference {

export namespace LeafNode {
export const is = (node: BaseTreeNode | LeafNode): node is LeafNode => 'preference' in node && !!node.preference.data;

export const getType = (node: BaseTreeNode | LeafNode): JsonType | undefined => is(node)
? Array.isArray(node.preference.data.type) ? node.preference.data.type[0] : node.preference.data.type
: undefined;
}

export const getValueInScope = <T extends JSONValue>(preferenceInfo: PreferenceInspection<T> | undefined, scope: number): T | undefined => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
// *****************************************************************************

import { codiconArray } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
import { PreferenceLeafNodeRenderer } from './preference-node-renderer';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import { Preference } from '../../util/preference-types';
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';

@injectable()
export class PreferenceArrayInputRenderer extends PreferenceLeafNodeRenderer<string[], HTMLInputElement> {
Expand Down Expand Up @@ -154,3 +157,18 @@ export class PreferenceArrayInputRenderer extends PreferenceLeafNodeRenderer<str
super.dispose();
}
}

@injectable()
export class PreferenceArrayInputRendererContribution extends PreferenceLeafNodeRendererContribution {
static ID = 'preference-array-input-renderer';
id = PreferenceArrayInputRendererContribution.ID;

canHandleLeafNode(node: Preference.LeafNode): number {
const type = Preference.LeafNode.getType(node);
return type === 'array' && (node.preference.data.items as IJSONSchema)?.type === 'string' ? 2 : 0;
}

createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
return container.get(PreferenceArrayInputRenderer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable } from '@theia/core/shared/inversify';
import { PreferenceLeafNodeRenderer } from './preference-node-renderer';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import { Preference } from '../../util/preference-types';
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';

@injectable()
export class PreferenceBooleanInputRenderer extends PreferenceLeafNodeRenderer<boolean, HTMLInputElement> {
Expand Down Expand Up @@ -51,3 +53,17 @@ export class PreferenceBooleanInputRenderer extends PreferenceLeafNodeRenderer<b
}
}
}

@injectable()
export class PreferenceBooleanInputRendererContribution extends PreferenceLeafNodeRendererContribution {
static ID = 'preference-boolean-input-renderer';
id = PreferenceBooleanInputRendererContribution.ID;

canHandleLeafNode(node: Preference.LeafNode): number {
return Preference.LeafNode.getType(node) === 'boolean' ? 2 : 0;
}

createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
return container.get(PreferenceBooleanInputRenderer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2022 EclipseSource 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 WITH Classpath-exception-2.0
// *****************************************************************************

import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { OpenFileDialogProps } from '@theia/filesystem/lib/browser';
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
import { Preference } from '../../util/preference-types';
import { PreferenceNodeRenderer } from './preference-node-renderer';
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';
import { PreferenceStringInputRenderer } from './preference-string-input';

export interface FileNodeTypeDetails {
isFilepath: boolean;
selectionProps?: Partial<OpenFileDialogProps>;
}

export namespace FileNodeTypeDetails {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function is(typeDetails?: any): typeDetails is FileNodeTypeDetails {
return !!typeDetails && !!typeDetails.isFilepath;
}
}

@injectable()
export class PreferenceSingleFilePathInputRendererContribution extends PreferenceLeafNodeRendererContribution {
static ID = 'preference-single-file-path-input-renderer';
id = PreferenceSingleFilePathInputRendererContribution.ID;

canHandleLeafNode(node: Preference.LeafNode): number {
const typeDetails = node.preference.data.typeDetails;
return FileNodeTypeDetails.is(typeDetails) && !typeDetails.selectionProps?.canSelectMany ? 5 : 0;
}

createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
return container.get(PreferenceSingleFilePathInputRenderer);
}
}

@injectable()
export class PreferenceSingleFilePathInputRenderer extends PreferenceStringInputRenderer {
@inject(FileDialogService) fileDialogService: FileDialogService;

get typeDetails(): FileNodeTypeDetails {
return this.preferenceNode.preference.data.typeDetails as FileNodeTypeDetails;
}

protected createInputWrapper(): HTMLElement {
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('preference-file-container');
return inputWrapper;
}

protected override createInteractable(parent: HTMLElement): void {
const inputWrapper = this.createInputWrapper();

super.createInteractable(inputWrapper);
this.interactable.classList.add('preference-file-input');

this.createBrowseButton(inputWrapper);

parent.appendChild(inputWrapper);
}

protected createBrowseButton(parent: HTMLElement): void {
const button = document.createElement('button');
button.classList.add('theia-button', 'main', 'preference-file-button');
button.textContent = nls.localize('theia/core/file/browse', 'Browse');
const handler = this.browse.bind(this);
button.onclick = handler;
button.onkeydown = handler;
button.tabIndex = 0;
button.setAttribute('aria-label', 'Submit Preference Input');
parent.appendChild(button);
}

protected async browse(): Promise<void> {
const selectionProps = this.typeDetails.selectionProps;
const title = selectionProps?.title ?? selectionProps?.canSelectFolders ? WorkspaceCommands.OPEN_FOLDER.dialogLabel : WorkspaceCommands.OPEN_FILE.dialogLabel;
const selection = await this.fileDialogService.showOpenDialog({ title, ...selectionProps });
if (selection) {
this.setPreferenceImmediately(selection.path.toString());
}
}

protected override setPreferenceImmediately(value: string): Promise<void> {
this.interactable.value = value;
return super.setPreferenceImmediately(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import { PreferenceLeafNodeRenderer } from './preference-node-renderer';
import { injectable, inject } from '@theia/core/shared/inversify';
import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer';
import { injectable, inject, interfaces } from '@theia/core/shared/inversify';
import { CommandService, nls } from '@theia/core/lib/common';
import { PreferencesCommands } from '../../util/preference-types';
import { Preference, PreferencesCommands } from '../../util/preference-types';
import { JSONValue } from '@theia/core/shared/@phosphor/coreutils';
import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator';

@injectable()
export class PreferenceJSONLinkRenderer extends PreferenceLeafNodeRenderer<JSONValue, HTMLAnchorElement> {
Expand Down Expand Up @@ -61,3 +62,17 @@ export class PreferenceJSONLinkRenderer extends PreferenceLeafNodeRenderer<JSONV
this.commandService.executeCommand(PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, this.id);
}
}

@injectable()
export class PreferenceJSONLinkRendererContribution extends PreferenceLeafNodeRendererContribution {
static ID = 'preference-json-link-renderer';
id = PreferenceJSONLinkRendererContribution.ID;

canHandleLeafNode(_node: Preference.LeafNode): number {
return 1;
}

createLeafNodeRenderer(container: interfaces.Container): PreferenceNodeRenderer {
return container.get(PreferenceJSONLinkRenderer);
}
}
Loading

0 comments on commit 7b8d3b7

Please sign in to comment.