Skip to content

Commit

Permalink
Improved QuickInput/InputBox API's. Partially fixes: eclipse-theia#5109
Browse files Browse the repository at this point in the history
Signed-off-by: Josh Pinkney <joshpinkney@gmail.com>
  • Loading branch information
JPinkney committed Jun 21, 2019
1 parent df80217 commit ae9ad2e
Show file tree
Hide file tree
Showing 10 changed files with 965 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Breaking changes:
- [plugin] 'Hosted mode' extracted in `plugin-dev` extension
- [core] `scheme` is mandatory for URI
- `URI.withoutScheme` is removed, in order to get a path use `URI.path`
- [plugin-ext] improved QuickInput and InputBox API's

## v0.7.0

Expand Down
122 changes: 120 additions & 2 deletions packages/core/src/browser/quick-open/quick-input-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,45 @@ import { Deferred } from '../../common/promise-util';
import { MaybePromise } from '../../common/types';
import { MessageType } from '../../common/message-service-protocol';
import { Emitter, Event } from '../../common/event';
import { QuickInputTitleBar, QuickInputTitleButton } from './quick-input-title-bar';

export interface QuickInputOptions {

/**
* Show the progress indicator if true
*/
busy?: boolean

/**
* Allow user input
*/
enabled?: boolean;

/**
* Current step count
*/
step?: number | undefined

/**
* The title of the input
*/
title?: string | undefined

/**
* Total number of steps
*/
totalSteps?: number | undefined

/**
* Buttons that are displayed on the title panel
*/
buttons?: ReadonlyArray<QuickInputTitleButton>

/**
* Text for when there is a problem with the current input value
*/
validationMessage?: string | undefined;

/**
* The prefill value.
*/
Expand Down Expand Up @@ -64,15 +101,32 @@ export class QuickInputService {
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;

protected _titlePanel: QuickInputTitleBar;
protected titleBarContainer: HTMLElement;
protected titleElement: HTMLElement | undefined;

constructor() {
this._titlePanel = new QuickInputTitleBar();
this.createTitleBarContainer();
}

open(options: QuickInputOptions): Promise<string | undefined> {
const result = new Deferred<string | undefined>();
const prompt = this.createPrompt(options.prompt);
let label = prompt;
let currentText = '';
const validateInput = options && options.validateInput;

this.titlePanel.title = options.title;
this.titlePanel.step = options.step;
this.titlePanel.totalSteps = options.totalSteps;
this.titlePanel.buttons = options.buttons;

this.removeAndAttachTitleBar();

this.quickOpenService.open({
onType: async (lookFor, acceptor) => {
const error = validateInput ? await validateInput(lookFor) : undefined;
const error = validateInput && lookFor !== undefined ? await validateInput(lookFor) : undefined;
label = error || prompt;
if (error) {
this.quickOpenService.showDecoration(MessageType.Error);
Expand All @@ -97,11 +151,70 @@ export class QuickInputService {
placeholder: options.placeHolder,
password: options.password,
ignoreFocusOut: options.ignoreFocusOut,
onClose: () => result.resolve(undefined)
enabled: options.enabled,
onClose: () => {
result.resolve(undefined);
this.titlePanel.dispose();
this.onDidHideEmitter.fire(undefined);
this.removeTitleElement();
this.createTitleBarContainer();
this.titlePanel.isAttached = false;
}
});
return result.promise;
}

private createTitleBarContainer(): void {
this.titleBarContainer = document.createElement('div');
this.titleBarContainer.style.backgroundColor = 'var(--theia-menu-color0)';
}

setStep(step: number | undefined) {
this.titlePanel.step = step;
this.attachTitleBarIfNeeded();
}

setTitle(title: string | undefined) {
this.titlePanel.title = title;
this.attachTitleBarIfNeeded();
}

setTotalSteps(totalSteps: number | undefined) {
this.titlePanel.totalSteps = totalSteps;
}

setButtons(buttons: ReadonlyArray<QuickInputTitleButton>) {
this.titlePanel.buttons = buttons;
this.attachTitleBarIfNeeded();
}

get titlePanel(): QuickInputTitleBar {
return this._titlePanel;
}

private removeTitleElement(): void {
if (this.titleElement) {
this.titleElement.remove();
}
}

private attachTitleBarIfNeeded() {
if (this.titlePanel.shouldShowTitleBar() && !this.titlePanel.isAttached) {
if (!this.quickOpenService.widgetNode.contains(this.titleBarContainer)) {
this.quickOpenService.widgetNode.prepend(this.titleBarContainer);
}
this.titleElement = this.titlePanel.attachTitleBar();
this.titleBarContainer.appendChild(this.titleElement);
this.titlePanel.isAttached = true;
}
}

private removeAndAttachTitleBar(): void {
this.removeTitleElement();
this.titlePanel.isAttached = false;
this.attachTitleBarIfNeeded();
}

protected defaultPrompt = "Press 'Enter' to confirm your input or 'Escape' to cancel";
protected createPrompt(prompt?: string): string {
return prompt ? `${prompt} (${this.defaultPrompt})` : this.defaultPrompt;
Expand All @@ -112,4 +225,9 @@ export class QuickInputService {
return this.onDidAcceptEmitter.event;
}

readonly onDidHideEmitter: Emitter<void> = new Emitter();
get onDidHide(): Event<void> {
return this.onDidHideEmitter.event;
}

}
220 changes: 220 additions & 0 deletions packages/core/src/browser/quick-open/quick-input-title-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/********************************************************************************
* Copyright (C) 2019 Red Hat, Inc. 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 { Emitter } from '../../common/event';
import { DisposableCollection } from '../../common/disposable';

export enum QuickInputTitleButtonSide {
LEFT = 0,
RIGHT = 1
}

export interface QuickInputTitleButton {
icon: string; // a background image coming from a url
iconClass?: string; // a class such as one coming from font awesome
tooltip?: string | undefined;
side: QuickInputTitleButtonSide
}

export class QuickInputTitleBar {

private readonly onDidTriggerButtonEmitter: Emitter<QuickInputTitleButton>;
private _isAttached: boolean;

private titleElement: HTMLElement;

private _title: string | undefined;
private _step: number | undefined;
private _totalSteps: number | undefined;
private _buttons: ReadonlyArray<QuickInputTitleButton>;

private disposableCollection: DisposableCollection;
constructor() {
this.titleElement = document.createElement('h3');
this.titleElement.style.flex = '1';
this.titleElement.style.textAlign = 'center';
this.titleElement.style.margin = '5px 0';

this.disposableCollection = new DisposableCollection();
this.disposableCollection.push(this.onDidTriggerButtonEmitter = new Emitter());
}

get onDidTriggerButton() {
return this.onDidTriggerButtonEmitter.event;
}

get isAttached(): boolean {
return this._isAttached;
}

set isAttached(isAttached: boolean) {
this._isAttached = isAttached;
}

set title(title: string | undefined) {
this._title = title;
this.updateInnerTitleText();
}

get title(): string | undefined {
return this._title;
}

set step(step: number | undefined) {
this._step = step;
this.updateInnerTitleText();
}

get step(): number | undefined {
return this._step;
}

set totalSteps(totalSteps: number | undefined) {
this._totalSteps = totalSteps;
this.updateInnerTitleText();
}

get totalSteps(): number | undefined {
return this._totalSteps;
}

set buttons(buttons: ReadonlyArray<QuickInputTitleButton> | undefined) {
if (buttons === undefined) {
this._buttons = [];
return;
}

this._buttons = buttons;
}

get buttons() {
return this._buttons;
}

private updateInnerTitleText(): void {
let innerTitle = '';

if (this.title) {
innerTitle = this.title + ' ';
}

if (this.step && this.totalSteps) {
innerTitle += `(${this.step} / ${this.totalSteps})`;
} else if (this.step) {
innerTitle += this.step;
}

this.titleElement.innerText = innerTitle;
}

// Left buttons are for the buttons dervied from QuickInputButtons
private getLeftButtons() {
if (this._buttons === undefined || this._buttons.length === 0) {
return [];
}
return this._buttons.filter(btn => btn.side === QuickInputTitleButtonSide.LEFT);
}

private getRightButtons() {
if (this._buttons === undefined || this._buttons.length === 0) {
return [];
}
return this._buttons.filter(btn => btn.side === QuickInputTitleButtonSide.RIGHT);
}

private createButtonElement(buttons: ReadonlyArray<QuickInputTitleButton>) {
const buttonDiv = document.createElement('div');
buttonDiv.style.display = 'inline-flex';
for (const btn of buttons) {
const aElement = document.createElement('a');
aElement.style.width = '20px';
aElement.style.height = '20px';

if (btn.iconClass) {
aElement.classList.add(...btn.iconClass.split(' '));
}

if (btn.icon !== '') {
aElement.style.backgroundImage = `url(\'${btn.icon}\')`;
}

aElement.classList.add('icon');
aElement.style.display = 'flex';
aElement.style.justifyContent = 'center';
aElement.style.alignItems = 'center';
aElement.title = btn.tooltip ? btn.tooltip : '';
aElement.onclick = () => {
this.onDidTriggerButtonEmitter.fire(btn);
};
buttonDiv.appendChild(aElement);
}
return buttonDiv;
}

private createTitleBarDiv() {
const div = document.createElement('div');
div.style.height = '1%'; // Reset the height to be valid
div.style.display = 'flex';
div.style.flexDirection = 'row';
div.style.flexWrap = 'wrap';
div.style.justifyContent = 'flex-start';
div.style.alignItems = 'center';
div.onclick = event => {
event.stopPropagation();
event.preventDefault();
};
return div;
}

private createLeftButtonDiv() {
const leftButtonDiv = document.createElement('div'); // Holds all the buttons that get added to the left
leftButtonDiv.style.flex = '1';
leftButtonDiv.style.textAlign = 'left';

leftButtonDiv.appendChild(this.createButtonElement(this.getLeftButtons()));
return leftButtonDiv;
}

private createRightButtonDiv() {
const rightButtonDiv = document.createElement('div');
rightButtonDiv.style.flex = '1';
rightButtonDiv.style.textAlign = 'right';

rightButtonDiv.appendChild(this.createButtonElement(this.getRightButtons()));
return rightButtonDiv;
}

public attachTitleBar() {
const div = this.createTitleBarDiv();

this.updateInnerTitleText();

div.appendChild(this.createLeftButtonDiv());
div.appendChild(this.titleElement);
div.appendChild(this.createRightButtonDiv());

return div;
}

shouldShowTitleBar(): boolean {
return ((this._step !== undefined) || (this._title !== undefined));
}

dispose() {
this.disposableCollection.dispose();
}

}
Loading

0 comments on commit ae9ad2e

Please sign in to comment.