From add212dd9c702f451a138b8d4984f0881da8f0dc Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Fri, 3 Feb 2017 16:28:34 +0100 Subject: [PATCH 01/14] Update to TypeScript 2.1.5. Updated CodeMirror type defs. --- package.json | 2 +- src/codemirror_extra.d.ts | 4 +- src/domutils.ts | 4 +- src/extra_lib.d.ts | 14 -- src/typings/codemirror/codemirror.d.ts | 207 +++++++++++++++++++++---- 5 files changed, 180 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 4e0d94f7d..f8a439d5f 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "shelljs": "0.6.0", "tsd": "0.6.0-beta.5", "typedoc": "0.3.12", - "typescript": "2.0.3", + "typescript": "2.1.5", "webfont": "^4.0.0" }, "jshintConfig": { diff --git a/src/codemirror_extra.d.ts b/src/codemirror_extra.d.ts index d69ac1f67..b35b9604e 100644 --- a/src/codemirror_extra.d.ts +++ b/src/codemirror_extra.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. */ -declare module CodeMirror { +declare namespace CodeMirror { interface Editor { on(eventName: 'keyHandled', handler: (instance: CodeMirror.Editor, name: string, event: KeyboardEvent) => void ): void; off(eventName: 'keyHandled', handler: (instance: CodeMirror.Editor, name: string, event: KeyboardEvent) => void ): void; @@ -30,7 +30,7 @@ declare module CodeMirror { interface Doc { getSelection(linesep?: string): string; getSelections(linesep?: string): string[]; - setSelections(ranges: {anchor: CodeMirror.Position, head: CodeMirror.Position}[], primary?: number, options?: Object); + setSelections(ranges: {anchor: Position, head: Position}[], primary?: number, options?: Object); replaceSelections(replacements: string[], select?: 'around' | 'start'); } diff --git a/src/domutils.ts b/src/domutils.ts index bf8865134..e1f3bbc55 100644 --- a/src/domutils.ts +++ b/src/domutils.ts @@ -372,12 +372,12 @@ export function getEventDeepPath(ev: Event): Node[] { export function focusWithoutScroll(el: HTMLElement): void { const preScrollTops: {element: Element, top: number}[] = []; - let p = el; + let p: Element = el; do { preScrollTops.push( { element: p, top: p.scrollTop } ); - let parent = p.parentElement; + let parent: Element = p.parentElement; if (parent == null) { const nodeParent = p.parentNode; if (nodeParent != null && nodeParent.nodeName === "#document-fragment") { diff --git a/src/extra_lib.d.ts b/src/extra_lib.d.ts index f6b3b9ca8..a78e035e8 100644 --- a/src/extra_lib.d.ts +++ b/src/extra_lib.d.ts @@ -20,16 +20,10 @@ interface CaretPosition { } interface DocumentOrShadowRoot { - getSelection(): Selection | null; - elementFromPoint(x: number, y: number): Element | null; - elementsFromPoint(x: number, y: number): NodeListOf; caretPositionFromPoint(x: number, y: number): CaretPosition | null; - activeElement: Element | null; - styleSheets: StyleSheetList; } interface ShadowRoot extends DocumentFragment, DocumentOrShadowRoot { - host: HTMLElement; mode: ShadowRootMode; } @@ -44,13 +38,6 @@ interface HTMLSlotElement extends HTMLElement { assignedNodes(options?: AssignedNodesOptions): NodeList; } -interface Element { - attachShadow(shadowRootInitDict: ShadowRootInit): ShadowRoot; - assignedSlot: HTMLSlotElement | null; - slot: string; - shadowRoot: ShadowRoot | null; -} - interface Event { composedPath(): Node[]; } @@ -121,7 +108,6 @@ interface KeyboardEventInit { interface Event { path: Node[]; // <- obsolete. Removed from later the Shadow DOM spec. - deepPath: Node[]; encapsulated: boolean; } diff --git a/src/typings/codemirror/codemirror.d.ts b/src/typings/codemirror/codemirror.d.ts index bbac12d6d..4af27f5f1 100644 --- a/src/typings/codemirror/codemirror.d.ts +++ b/src/typings/codemirror/codemirror.d.ts @@ -1,12 +1,15 @@ // Type definitions for CodeMirror // Project: https://github.com/marijnh/CodeMirror // Definitions by: mihailik -// Definitions: https://github.com/borisyankov/DefinitelyTyped +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +export = CodeMirror; +export as namespace CodeMirror; declare function CodeMirror(host: HTMLElement, options?: CodeMirror.EditorConfiguration): CodeMirror.Editor; declare function CodeMirror(callback: (host: HTMLElement) => void , options?: CodeMirror.EditorConfiguration): CodeMirror.Editor; -declare module CodeMirror { +declare namespace CodeMirror { export var Doc : CodeMirror.DocConstructor; export var Pos: CodeMirror.PositionConstructor; export var Pass: any; @@ -32,6 +35,11 @@ declare module CodeMirror { whenever a new CodeMirror instance is initialized. */ function defineInitHook(func: Function): void; + /** Registers a helper value with the given name in the given namespace (type). This is used to define functionality + that may be looked up by mode. Will create (if it doesn't already exist) a property on the CodeMirror object for + the given type, pointing to an object that maps names to values. I.e. after doing + CodeMirror.registerHelper("hint", "foo", myFoo), the value CodeMirror.hint.foo will point to myFoo. */ + function registerHelper(namespace: string, name: string, helper: any): void; function on(element: any, eventName: string, handler: Function): void; @@ -145,7 +153,11 @@ declare module CodeMirror { /** Attach a new document to the editor. Returns the old document, which is now no longer associated with an editor. */ swapDoc(doc: CodeMirror.Doc): CodeMirror.Doc; + /** Get the content of the current editor document. You can pass it an optional argument to specify the string to be used to separate lines (defaults to "\n"). */ + getValue(seperator?: string): string; + /** Set the content of the current editor document. */ + setValue(content: string): void; /** Sets the gutter marker for the given gutter (identified by its CSS class, see the gutters option) to the given value. Value can be either null, to clear the marker, or a DOM element, to set it. The DOM element will be shown in the specified gutter next to the specified line. */ @@ -166,13 +178,20 @@ declare module CodeMirror { class can be left off to remove all classes for the specified node, or be a string to remove only a specific class. */ removeLineClass(line: any, where: string, class_: string): CodeMirror.LineHandle; + /** + * Compute the line at the given pixel height. + * + * `mode` is the relative element to use to compute this line - defaults to 'page' if not specified + */ + lineAtHeight(height: number, mode?: 'window' | 'page' | 'local'): number + /** Returns the line number, text content, and marker status of the given line, which can be either a number or a line handle. */ lineInfo(line: any): { line: any; handle: any; text: string; /** Object mapping gutter IDs to marker elements. */ - gutterMarks: any; + gutterMarkers: any; textClass: string; bgClass: string; wrapClass: string; @@ -211,14 +230,7 @@ declare module CodeMirror { /** Get an { left , top , width , height , clientWidth , clientHeight } object that represents the current scroll position, the size of the scrollable area, and the size of the visible area(minus scrollbars). */ - getScrollInfo(): { - left: any; - top: any; - width: any; - height: any; - clientWidth: any; - clientHeight: any; - } + getScrollInfo(): CodeMirror.ScrollInfo; /** Scrolls the given element into view. pos is a { line , ch } position, referring to a given character, null, to refer to the cursor. The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ @@ -228,6 +240,14 @@ declare module CodeMirror { The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ scrollIntoView(pos: { left: number; top: number; right: number; bottom: number; }, margin: number): void; + /** Scrolls the given element into view. pos is a { line, ch } object, in editor-local coordinates. + The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ + scrollIntoView(pos: { line: number, ch: number }, margin?: number): void; + + /** Scrolls the given element into view. pos is a { from, to } object, in editor-local coordinates. + The margin parameter is optional. When given, it indicates the amount of pixels around the given area that should be made visible as well. */ + scrollIntoView(pos: { from: CodeMirror.Position, to: CodeMirror.Position }, margin: number): void; + /** Returns an { left , top , bottom } object containing the coordinates of the cursor position. If mode is "local" , they will be relative to the top-left corner of the editable document. If it is "page" or not given, they are relative to the top-left corner of the page. @@ -380,8 +400,11 @@ declare module CodeMirror { /** Fired whenever a line is (re-)rendered to the DOM. Fired right after the DOM element is built, before it is added to the document. The handler may mess with the style of the resulting element, or add event handlers, but should not try to change the state of the editor. */ - on(eventName: 'renderLine', handler: (instance: CodeMirror.Editor, line: number, element: HTMLElement) => void ): void; - off(eventName: 'renderLine', handler: (instance: CodeMirror.Editor, line: number, element: HTMLElement) => void ): void; + on(eventName: 'renderLine', handler: (instance: CodeMirror.Editor, line: CodeMirror.LineHandle, element: HTMLElement) => void ): void; + off(eventName: 'renderLine', handler: (instance: CodeMirror.Editor, line: CodeMirror.LineHandle, element: HTMLElement) => void ): void; + + /** Expose the state object, so that the Editor.state.completionActive property is reachable*/ + state: any; } interface EditorFromTextArea extends Editor { @@ -414,7 +437,7 @@ declare module CodeMirror { /** Replace the part of the document between from and to with the given string. from and to must be {line, ch} objects. to can be left off to simply insert the string at position from. */ - replaceRange(replacement: string, from: CodeMirror.Position, to: CodeMirror.Position): void; + replaceRange(replacement: string, from: CodeMirror.Position, to?: CodeMirror.Position): void; /** Get the content of line n. */ getLine(n: number): string; @@ -471,8 +494,8 @@ declare module CodeMirror { It may be "start" , "end" , "head"(the side of the selection that moves when you press shift + arrow), or "anchor"(the fixed side of the selection).Omitting the argument is the same as passing "head".A { line , ch } object will be returned. */ getCursor(start?: string): CodeMirror.Position; - - /** Retrieves a list of all current selections. These will always be sorted, and never overlap (overlapping selections are merged). + + /** Retrieves a list of all current selections. These will always be sorted, and never overlap (overlapping selections are merged). Each object in the array contains anchor and head properties referring to {line, ch} objects. */ listSelections(): { anchor: CodeMirror.Position; head: CodeMirror.Position }[]; @@ -558,7 +581,7 @@ declare module CodeMirror { /** By default, text typed when the cursor is on top of the bookmark will end up to the right of the bookmark. Set this option to true to make it go to the left instead. */ insertLeft?: boolean; - }): CodeMirror.TextBookmarkMarker; + }): CodeMirror.TextMarker; /** Returns an array of all the bookmarks and marked ranges found between the given positions. */ findMarks(from: CodeMirror.Position, to: CodeMirror.Position): TextMarker[]; @@ -581,15 +604,21 @@ declare module CodeMirror { /** The reverse of posFromIndex. */ indexFromPos(object: CodeMirror.Position): number; + /** Expose the state object, so that the Doc.state.completionActive property is reachable*/ + state: any; } interface LineHandle { text: string; } - interface TextBookmarkMarker { - find(): CodeMirror.Position; - clear(): void; + interface ScrollInfo { + left: any; + top: any; + width: any; + height: any; + clientWidth: any; + clientHeight: any; } interface TextMarker { @@ -598,17 +627,12 @@ declare module CodeMirror { /** Returns a {from, to} object (both holding document positions), indicating the current position of the marked range, or undefined if the marker is no longer in the document. */ - find(): CodeMirror.TextMakerRange; + find(): CodeMirror.Range; /** Returns an object representing the options for the marker. If copyWidget is given true, it will clone the value of the replacedWith option, if any. */ getOptions(copyWidget: boolean): CodeMirror.TextMarkerOptions; } - - interface TextMakerRange { - from: CodeMirror.Position; - to: CodeMirror.Position; - } - + interface LineWidget { /** Removes the widget. */ clear(): void; @@ -644,8 +668,13 @@ declare module CodeMirror { } interface PositionConstructor { - new (line: number, ch: number): Position; - (line: number, ch: number): Position; + new (line: number, ch?: number): Position; + (line: number, ch?: number): Position; + } + + interface Range{ + from: CodeMirror.Position; + to: CodeMirror.Position; } interface Position { @@ -798,6 +827,9 @@ declare module CodeMirror { /** Optional lint configuration to be used in conjunction with CodeMirror's linter addon. */ lint?: boolean | LintOptions; + + /** Optional value to be used in conjunction with CodeMirror’s placeholder add-on. */ + placeholder?: string; } interface TextMarkerOptions { @@ -1043,6 +1075,12 @@ declare module CodeMirror { */ function defineMode(id: string, modefactory: ModeFactory): void; + /** + * id will be the id for the defined mode. Typically, you should use this second argument to defineMode as your module scope function + * (modes should not leak anything into the global scope!), i.e. write your whole mode inside this function. + */ + function defineMode(id: string, modefactory: ModeFactory): void; + /** * The first argument is a configuration object as passed to the mode constructor function, and the second argument * is a mode specification as in the EditorConfiguration mode option. @@ -1099,8 +1137,113 @@ declare module CodeMirror { severity?: string; to?: Position; } -} -declare module "codemirror" { - export = CodeMirror; + /** + * A function that calculates either a two-way or three-way merge between different sets of content. + */ + function MergeView(element: HTMLElement, options?: MergeView.MergeViewEditorConfiguration): MergeView.MergeViewEditor; + + namespace MergeView { + /** + * Options available to MergeView. + */ + interface MergeViewEditorConfiguration extends EditorConfiguration { + /** + * Determines whether the original editor allows editing. Defaults to false. + */ + allowEditingOriginals?: boolean; + + /** + * When true stretches of unchanged text will be collapsed. When a number is given, this indicates the amount + * of lines to leave visible around such stretches (which defaults to 2). Defaults to false. + */ + collapseIdentical?: boolean | number; + + /** + * Sets the style used to connect changed chunks of code. By default, connectors are drawn. When this is set to "align", + * the smaller chunk is padded to align with the bigger chunk instead. + */ + connect?: string; + + /** + * Callback for when stretches of unchanged text are collapsed. + */ + onCollapse?(mergeView: MergeViewEditor, line: number, size: number, mark: TextMarker): void; + + /** + * Provides original version of the document to be shown on the right of the editor. + */ + orig: any; + + /** + * Provides original version of the document to be shown on the left of the editor. + * To create a 2-way (as opposed to 3-way) merge view, provide only one of origLeft and origRight. + */ + origLeft?: any; + + /** + * Provides original version of document to be shown on the right of the editor. + * To create a 2-way (as opposed to 3-way) merge view, provide only one of origLeft and origRight. + */ + origRight?: any; + + /** + * Determines whether buttons that allow the user to revert changes are shown. Defaults to true. + */ + revertButtons?: boolean; + + /** + * When true, changed pieces of text are highlighted. Defaults to true. + */ + showDifferences?: boolean; + } + + interface MergeViewEditor extends Editor { + /** + * Returns the editor instance. + */ + editor(): Editor; + + /** + * Left side of the merge view. + */ + left: DiffView; + leftChunks(): MergeViewDiffChunk; + leftOriginal(): Editor; + + /** + * Right side of the merge view. + */ + right: DiffView; + rightChunks(): MergeViewDiffChunk; + rightOriginal(): Editor; + + /** + * Sets whether or not the merge view should show the differences between the editor views. + */ + setShowDifferences(showDifferences: boolean): void; + } + + /** + * Tracks changes in chunks from oroginal to new. + */ + interface MergeViewDiffChunk { + editFrom: number; + editTo: number; + origFrom: number; + origTo: number; + } + + interface DiffView { + /** + * Forces the view to reload. + */ + forceUpdate(): (mode: string) => void; + + /** + * Sets whether or not the merge view should show the differences between the editor views. + */ + setShowDifferences(showDifferences: boolean): void; + } + } } From a6bb21ec10f7dddb721dd82d83c0f926829c143e Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Mon, 6 Feb 2017 21:26:01 +0100 Subject: [PATCH 02/14] The beginnings of a plugin API. --- .gitignore | 4 + src/InternalExtratermApi.ts | 7 ++ src/PluginApi.ts | 28 ++++++ src/PluginManager.ts | 93 +++++++++++++++++++ src/mainweb.ts | 50 ++++++++++ src/mainwebui.ts | 14 ++- .../TerminalViewerExtra.js | 18 ++++ .../TerminalViewerExtra.ts | 30 ++++++ src/plugins/TerminalViewerExtra/metadata.json | 4 + tsconfig.json | 4 + 10 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/InternalExtratermApi.ts create mode 100644 src/PluginApi.ts create mode 100644 src/PluginManager.ts create mode 100644 src/plugins/TerminalViewerExtra/TerminalViewerExtra.js create mode 100644 src/plugins/TerminalViewerExtra/TerminalViewerExtra.ts create mode 100644 src/plugins/TerminalViewerExtra/metadata.json diff --git a/.gitignore b/.gitignore index 2bc547a96..6e159adab 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,7 @@ src/BulkDOMOperation.js src/codemirroroperation.ts src/BulkDOMOperationTest.js src/SupportsClipboardPaste.js +src/PluginManager.js +src/PluginApi.js +src/InternalExtratermApi.js +src/codemirroroperation.js diff --git a/src/InternalExtratermApi.ts b/src/InternalExtratermApi.ts new file mode 100644 index 000000000..ce4388e90 --- /dev/null +++ b/src/InternalExtratermApi.ts @@ -0,0 +1,7 @@ +import PluginApi = require('./PluginApi'); + +export interface InternalExtratermApi extends PluginApi.ExtratermApi { + setTopLevel(el: HTMLElement): void; + addTab(el: HTMLElement): void; + removeTab(el: HTMLElement): void; +} diff --git a/src/PluginApi.ts b/src/PluginApi.ts new file mode 100644 index 000000000..7b97d01c9 --- /dev/null +++ b/src/PluginApi.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ + +export interface PluginMetaData { + name: string; + factory: string; +} + +export interface ElementListener { + (element: HTMLElement): void; +} + +export interface ExtratermApi { + addNewTopLevelEventListener(callback: ElementListener): void; + addNewTabEventListener(callback: ElementListener): void; + // registerViewer(): void; +} + +export interface ExtratermPluginFactory { + (api: ExtratermApi): ExtratermPlugin; +} + +export interface ExtratermPlugin { + +} diff --git a/src/PluginManager.ts b/src/PluginManager.ts new file mode 100644 index 000000000..b673d4866 --- /dev/null +++ b/src/PluginManager.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ + +import fs = require('fs'); +import path = require('path'); +import Logger = require('./logger'); +import PluginApi = require('./PluginApi'); + +const PLUGIN_METADATA = "metadata.json"; + +// Partial because people forget to fill in json files correctly. +type PartialPluginMetaData = { + [P in keyof PluginApi.PluginMetaData]?: PluginApi.PluginMetaData[P]; +} + +interface PluginInfo { + path: string; + name: string; + factoryName: string; + instance: PluginApi.ExtratermPlugin; +} + +export class PluginManager { + + private _log: Logger = null; + + private _pluginDir: string = null; + + private _pluginData: PluginInfo[] = []; + + constructor(pluginDir: string) { + this._log = new Logger("PluginManager", this); + this._pluginDir = pluginDir; + } + + /** + * Load all of the plugins and create instances. + * + * @param api the API instance to pass to the plugins at creation time. + */ + load(api: PluginApi.ExtratermApi): void { + this._pluginData = this._scan(this._pluginDir); + + for (const pluginData of this._pluginData) { + const factory = this._loadPlugin(pluginData); + pluginData.instance = factory(api); + } + } + + private _loadPlugin(pluginData: PluginInfo): PluginApi.ExtratermPluginFactory { + const factoryPath = path.join(pluginData.path, pluginData.factoryName); + return require(factoryPath); + } + + /** + * Scan a directory for available plugins. + * + * @param pluginDir the directory to scan. + * @return list of plugin info describing what was found. + */ + private _scan(pluginDir: string): PluginInfo[] { + const result: PluginInfo[] = []; + + if (fs.existsSync(pluginDir)) { + const contents = fs.readdirSync(pluginDir); + for (const item of contents) { + const metadataPath = path.join(pluginDir, item, PLUGIN_METADATA); + + if (fs.existsSync(metadataPath)) { + const metadataString = fs.readFileSync(metadataPath, "UTF8"); + try { + const metadata = JSON.parse(metadataString); + if (metadata.name == null || metadata.factory == null) { + this._log.warn(`An error occurred while reading the metadata from ${metadataPath}. It is missing 'name' or 'factory' fields.`); + } else { + result.push( { path: path.join(pluginDir, item), name: metadata.name, factoryName: metadata.factory, instance: null } ); + } + + } catch(ex) { + this._log.warn(`An error occurred while processing ${metadataPath}.`, ex); + } + } else { + this._log.warn(`Couldn't find a ${PLUGIN_METADATA} file in ${item}. Ignoring.`); + } + } + } + return result; + } +} + diff --git a/src/mainweb.ts b/src/mainweb.ts index 1b5712750..aa944b425 100755 --- a/src/mainweb.ts +++ b/src/mainweb.ts @@ -3,6 +3,7 @@ * * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. */ +import path = require('path'); import electron = require('electron'); const Menu = electron.remote.Menu; const MenuItem = electron.remote.MenuItem; @@ -20,6 +21,11 @@ import CbCommandPalette = require('./gui/commandpalette'); import ResizeRefreshElementBase = require('./ResizeRefreshElementBase'); import CommandPaletteTypes = require('./gui/commandpalettetypes'); import CommandPaletteRequestTypes = require('./commandpaletterequesttypes'); + +import PluginApi = require('./PluginApi'); +import PluginManager = require('./PluginManager'); +import InternalExtratermApi = require('./InternalExtratermApi'); + import MainWebUi = require('./mainwebui'); import EtTerminal = require('./terminal'); import domutils = require('./domutils'); @@ -47,6 +53,8 @@ type KeyBindingContexts = keybindingmanager.KeyBindingContexts; sourceMapSupport.install(); +const PLUGINS_DIRECTORY = "plugins"; + const PALETTE_GROUP = "mainweb"; const MENU_ITEM_SPLIT = 'split'; const MENU_ITEM_SETTINGS = 'settings'; @@ -69,6 +77,8 @@ let keyBindingManager: KeyBindingManager = null; let themes: ThemeInfo[]; let mainWebUi: MainWebUi = null; let configManager: ConfigManagerImpl = null; +let pluginManager: PluginManager.PluginManager = null; +let internalExtratermApi: InternalExtratermApiImpl = null; /** * @@ -139,7 +149,13 @@ export function startUp(): void { } }); + // Get the plugins loaded. + pluginManager = new PluginManager.PluginManager(path.join(__dirname, PLUGINS_DIRECTORY)); + internalExtratermApi = new InternalExtratermApiImpl(); + pluginManager.load(internalExtratermApi); + mainWebUi = doc.createElement(MainWebUi.TAG_NAME); + mainWebUi.setInternalExtratermApi(internalExtratermApi); config.injectConfigManager(mainWebUi, configManager); keybindingmanager.injectKeyBindingManager(mainWebUi, keyBindingManager); mainWebUi.innerHTML = `
@@ -650,3 +666,37 @@ class KeyBindingManagerImpl { this._listenerList = this._listenerList.filter( (tup) => tup.key !== key); } } + +class InternalExtratermApiImpl implements InternalExtratermApi.InternalExtratermApi { + + private _topLevelElement: HTMLElement = null; + + private _topLevelEventListeners: PluginApi.ElementListener[] = []; + + private _tabElements: HTMLElement[] = []; + + private _tabEventListeners: PluginApi.ElementListener[] = []; + + + addNewTopLevelEventListener(callback: PluginApi.ElementListener): void { + this._topLevelEventListeners.push(callback); + } + + setTopLevel(el: HTMLElement): void { + this._topLevelElement = el; + this._topLevelEventListeners.forEach( listener => listener(el) ); + } + + addNewTabEventListener(callback: PluginApi.ElementListener): void { + this._tabEventListeners.push(callback); + } + + addTab(el: HTMLElement): void { + this._tabElements.push(el); + this._tabEventListeners.forEach( listener => listener(el) ); + } + + removeTab(el: HTMLElement): void { + this._tabElements = this._tabElements.filter( listEl => listEl !== el ); + } +} diff --git a/src/mainwebui.ts b/src/mainwebui.ts index a7915270f..89e184716 100755 --- a/src/mainwebui.ts +++ b/src/mainwebui.ts @@ -26,6 +26,8 @@ import CommandPaletteTypes = require('./gui/commandpalettetypes'); import CommandPaletteRequestTypes = require('./commandpaletterequesttypes'); type CommandPaletteRequest = CommandPaletteRequestTypes.CommandPaletteRequest; +import InternalExtratermApi = require('./InternalExtratermApi'); + import webipc = require('./webipc'); import Messages = require('./windowmessages'); import path = require('path'); @@ -353,6 +355,8 @@ class ExtratermMainWebUI extends ThemeableElementBase implements keybindingmanag private _split: boolean; + private _internalExtratermApi: InternalExtratermApi.InternalExtratermApi; + private _initProperties(): void { this._log = new Logger("ExtratermMainWebUI", this); this._tabInfo = []; @@ -361,6 +365,7 @@ class ExtratermMainWebUI extends ThemeableElementBase implements keybindingmanag this._keyBindingManager = null; this._themes = []; this._split = false; + this._internalExtratermApi = null; } //----------------------------------------------------------------------- @@ -512,7 +517,12 @@ class ExtratermMainWebUI extends ThemeableElementBase implements keybindingmanag tabInfo.focus(); } } - + + setInternalExtratermApi(api: InternalExtratermApi.InternalExtratermApi): void { + this._internalExtratermApi = api; + api.setTopLevel(this); + } + setConfigManager(configManager: ConfigManager): void { this._configManager = configManager; } @@ -763,6 +773,8 @@ class ExtratermMainWebUI extends ThemeableElementBase implements keybindingmanag tabInfo.updateTabTitle(); this._sendTabOpenedEvent(); + + this._internalExtratermApi.addTab(newTerminal); return tabInfo.id; } diff --git a/src/plugins/TerminalViewerExtra/TerminalViewerExtra.js b/src/plugins/TerminalViewerExtra/TerminalViewerExtra.js new file mode 100644 index 000000000..d2fbcaa08 --- /dev/null +++ b/src/plugins/TerminalViewerExtra/TerminalViewerExtra.js @@ -0,0 +1,18 @@ +"use strict"; +const Logger = require("../../logger"); +class TerminalViewerExtra { + constructor(api) { + this._log = new Logger("TerminalViewerExtra", this); + api.addNewTopLevelEventListener((el) => { + this._log.debug("Saw a new top level"); + }); + api.addNewTabEventListener((el) => { + this._log.debug("Saw a new tab level"); + }); + } +} +function factory(api) { + return new TerminalViewerExtra(api); +} +module.exports = factory; +//# sourceMappingURL=TerminalViewerExtra.js.map \ No newline at end of file diff --git a/src/plugins/TerminalViewerExtra/TerminalViewerExtra.ts b/src/plugins/TerminalViewerExtra/TerminalViewerExtra.ts new file mode 100644 index 000000000..b9cbb6192 --- /dev/null +++ b/src/plugins/TerminalViewerExtra/TerminalViewerExtra.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ +import * as PluginApi from '../../PluginApi'; +import Logger = require('../../logger'); + +class TerminalViewerExtra implements PluginApi.ExtratermPlugin { + + private _log: Logger; + + constructor(api: PluginApi.ExtratermApi) { + + this._log = new Logger("TerminalViewerExtra", this); + api.addNewTopLevelEventListener( (el: HTMLElement): void => { + this._log.debug("Saw a new top level"); + }); + + api.addNewTabEventListener( (el: HTMLElement): void => { + this._log.debug("Saw a new tab level"); + }); + } + +} + +function factory(api: PluginApi.ExtratermApi): PluginApi.ExtratermPlugin { + return new TerminalViewerExtra(api); +} +export = factory; diff --git a/src/plugins/TerminalViewerExtra/metadata.json b/src/plugins/TerminalViewerExtra/metadata.json new file mode 100644 index 000000000..2973e7177 --- /dev/null +++ b/src/plugins/TerminalViewerExtra/metadata.json @@ -0,0 +1,4 @@ +{ + "name": "TerminalViewerExtra", + "factory": "TerminalViewerExtra.js" +} diff --git a/tsconfig.json b/tsconfig.json index ba32feecf..7d8d5ae51 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "./src/viewers/*.ts", "./src/settings/*.ts", "./src/settings/*.tsx", + "./src/plugins/**/*.ts", "./src/typings/**/*.ts", "!./src/typings/tsd.d.ts", "!./src/node_modules/**/*.ts" @@ -25,6 +26,9 @@ "files": [ "./src/BulkDOMOperation.ts", "./src/BulkDOMOperationTest.ts", + "./src/PluginApi.ts", + "./src/PluginManager.ts", + "./src/plugins/TerminalViewerExtra/TerminalViewerExtra.ts", "./src/abouttab.ts", "./src/codemirror_extra.d.ts", "./src/codemirrorcommands.ts", From df7be0322b48286c55e14c220c0b7d98388c023b Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Mon, 6 Feb 2017 21:54:05 +0100 Subject: [PATCH 03/14] This ID in command palette was the same as in context menu. oops. --- src/gui/commandpalette.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/commandpalette.ts b/src/gui/commandpalette.ts index e5f6f01e5..0a44f7148 100644 --- a/src/gui/commandpalette.ts +++ b/src/gui/commandpalette.ts @@ -11,7 +11,7 @@ import util = require('./util'); import he = require('he'); import CommandPaletteTypes = require('./commandpalettetypes'); -const ID = "CbContextMenuTemplate"; +const ID = "CbCommandPaletteTemplate"; const ID_COVER = "ID_COVER"; const ID_CONTEXT_COVER = "ID_CONTEXT_COVER"; const ID_CONTAINER = "ID_CONTAINER"; From 60c545feb7f02a5112379076646232f7fbdda804 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Fri, 10 Feb 2017 22:00:31 +0100 Subject: [PATCH 04/14] Can now set the syntax highlighting inside a text viewer. --- .gitignore | 2 + src/extra_lib.d.ts | 4 + src/gui/PopDownDialog.ts | 189 ++++++++++++ src/gui/PopDownListPicker.ts | 343 +++++++++++++++++++++ src/plugins/TextViewer/TextViewerPlugin.js | 58 ++++ src/plugins/TextViewer/TextViewerPlugin.ts | 82 +++++ src/plugins/TextViewer/metadata.json | 4 + src/viewers/textviewer.ts | 18 +- tsconfig.json | 3 + 9 files changed, 699 insertions(+), 4 deletions(-) create mode 100644 src/gui/PopDownDialog.ts create mode 100644 src/gui/PopDownListPicker.ts create mode 100644 src/plugins/TextViewer/TextViewerPlugin.js create mode 100644 src/plugins/TextViewer/TextViewerPlugin.ts create mode 100644 src/plugins/TextViewer/metadata.json diff --git a/.gitignore b/.gitignore index 6e159adab..8a457d9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,5 @@ src/PluginManager.js src/PluginApi.js src/InternalExtratermApi.js src/codemirroroperation.js +src/gui/PopDownDialog.js +src/gui/PopDownListPicker.js diff --git a/src/extra_lib.d.ts b/src/extra_lib.d.ts index a78e035e8..008a72abe 100644 --- a/src/extra_lib.d.ts +++ b/src/extra_lib.d.ts @@ -111,6 +111,10 @@ interface Event { encapsulated: boolean; } +interface EventInit { + composed?: boolean; +} + interface Console { timeStamp(label: string): void; } diff --git a/src/gui/PopDownDialog.ts b/src/gui/PopDownDialog.ts new file mode 100644 index 000000000..3f99960e9 --- /dev/null +++ b/src/gui/PopDownDialog.ts @@ -0,0 +1,189 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ + +import ThemeableElementBase = require('../themeableelementbase'); +import ThemeTypes = require('../theme'); +import domutils = require('../domutils'); + +const ID = "CbPopDownDialogTemplate"; +const ID_COVER = "ID_COVER"; +const ID_CONTEXT_COVER = "ID_CONTEXT_COVER"; +const ID_CONTAINER = "ID_CONTAINER"; + + +const ID_TITLE_PRIMARY = "ID_TITLE_PRIMARY"; +const ID_TITLE_SECONDARY = "ID_TITLE_SECONDARY"; +const ID_TITLE_CONTAINER = "ID_TITLE_CONTAINER"; + +const CLASS_CONTEXT_COVER_OPEN = "CLASS_CONTEXT_COVER_OPEN"; +const CLASS_CONTEXT_COVER_CLOSED = "CLASS_CONTEXT_COVER_CLOSED"; +const CLASS_COVER_CLOSED = "CLASS_COVER_CLOSED"; +const CLASS_COVER_OPEN = "CLASS_COVER_OPEN"; + +const ATTR_DATA_ID = "data-id"; + +let registered = false; + +/** + * A Pop Down Dialog. + */ +class PopDownDialog extends ThemeableElementBase { + + /** + * The HTML tag name of this element. + */ + static TAG_NAME = "CB-POPDOWNDIALOG"; + + static EVENT_CLOSE_REQUEST = "CB-POPDOWNDIALOG-CLOSE_REQUEST"; + + /** + * Initialize the PopDownDialog class and resources. + * + * When PopDownDialog is imported into a render process, this static method + * must be called before an instances may be created. This is can be safely + * called multiple times. + */ + static init(): void { + if (registered === false) { + window.document.registerElement(PopDownDialog.TAG_NAME, {prototype: PopDownDialog.prototype}); + registered = true; + } + } + + private _titlePrimary: string; + + private _titleSecondary: string; + + private _laterHandle: domutils.LaterHandle; + + private _initProperties(): void { + this._laterHandle = null; + this._titlePrimary = ""; + this._titleSecondary = ""; + } + + setTitlePrimary(text: string): void { + this._titlePrimary = text; + this._updateTitle(); + } + + getTitlePrimary(): string { + return this._titlePrimary; + } + + setTitleSecondary(text: string): void { + this._titleSecondary = text; + this._updateTitle(); + } + + getTitleSecondary(): string { + return this._titleSecondary; + } + + //----------------------------------------------------------------------- + // + // # + // # # ###### ###### #### # # #### # ###### + // # # # # # # # # # # # # + // # # ##### ##### # # # # ##### + // # # # # # # # # # + // # # # # # # # # # # # + // ####### # # ###### #### # #### ###### ###### + // + //----------------------------------------------------------------------- + /** + * Custom Element 'created' life cycle hook. + */ + createdCallback() { + this._initProperties(); // Initialise our properties. The constructor was not called. + const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true }); + const clone = this.createClone(); + shadow.appendChild(clone); + this.updateThemeCss(); + + const containerDiv = domutils.getShadowId(this, ID_CONTAINER); + containerDiv.addEventListener('contextmenu', (ev) => { + this.dispatchEvent(new CustomEvent(PopDownDialog.EVENT_CLOSE_REQUEST, {bubbles: false})); + }); + + const coverDiv = domutils.getShadowId(this, ID_COVER); + coverDiv.addEventListener('mousedown', (ev) => { + this.dispatchEvent(new CustomEvent(PopDownDialog.EVENT_CLOSE_REQUEST, {bubbles: false})); + }); + } + + /** + * + */ + private createClone() { + let template = window.document.getElementById(ID); + if (template === null) { + template = window.document.createElement('template'); + template.id = ID; + template.innerHTML = ` +
+
+
+
+ +
+
`; + window.document.body.appendChild(template); + } + + return window.document.importNode(template.content, true); + } + + protected _themeCssFiles(): ThemeTypes.CssFile[] { + return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_COMMANDPALETTE]; + } + + //----------------------------------------------------------------------- + + private _updateTitle(): void { + const titlePrimaryDiv = domutils.getShadowId(this, ID_TITLE_PRIMARY); + const titleSecondaryDiv = domutils.getShadowId(this, ID_TITLE_SECONDARY); + + titlePrimaryDiv.innerText = this._titlePrimary; + titleSecondaryDiv.innerText = this._titleSecondary; + } + + /** + * + */ + open(x: number, y: number, width: number, height: number): void { + // Nuke any style like 'display: none' which can be use to prevent flicker. + this.setAttribute('style', ''); + + const container = domutils.getShadowId(this, ID_CONTEXT_COVER); + container.classList.remove(CLASS_CONTEXT_COVER_CLOSED); + container.classList.add(CLASS_CONTEXT_COVER_OPEN); + + container.style.left = `${x}px`; + container.style.top = `${y}px`; + container.style.width = `${width}px`; + container.style.height = `${height}px`; + + const cover = domutils.getShadowId(this, ID_COVER); + cover.classList.remove(CLASS_COVER_CLOSED); + cover.classList.add(CLASS_COVER_OPEN); + } + + /** + * + */ + close(): void { + const cover = domutils.getShadowId(this, ID_COVER); + cover.classList.remove(CLASS_COVER_OPEN); + cover.classList.add(CLASS_COVER_CLOSED); + + const container = domutils.getShadowId(this, ID_CONTEXT_COVER); + container.classList.remove(CLASS_CONTEXT_COVER_OPEN); + container.classList.add(CLASS_CONTEXT_COVER_CLOSED); + } +} + +export = PopDownDialog; diff --git a/src/gui/PopDownListPicker.ts b/src/gui/PopDownListPicker.ts new file mode 100644 index 000000000..453edcd31 --- /dev/null +++ b/src/gui/PopDownListPicker.ts @@ -0,0 +1,343 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ + +import ThemeableElementBase = require('../themeableelementbase'); +import ThemeTypes = require('../theme'); +import domutils = require('../domutils'); +import PopDownDialog = require('./PopDownDialog'); + +const ID = "CbPopDownListPickerTemplate"; +const ID_DIALOG = "ID_DIALOG"; +const ID_FILTER = "ID_FILTER"; +const ID_RESULTS = "ID_RESULTS"; + +let registered = false; + +/** + * A Pop Down List Picker. + */ +class PopDownListPicker extends ThemeableElementBase { + + /** + * The HTML tag name of this element. + */ + static TAG_NAME = "CB-POPDOWNLISTPICKER"; + + static EVENT_CLOSE_REQUEST = "CB-POPDOWNLISTPICKER-CLOSE_REQUEST"; + + static ATTR_DATA_ID = "data-id"; + + static CLASS_RESULT_SELECTED = "CLASS_RESULT_SELECTED"; + + /** + * Initialize the PopDownListPicker class and resources. + * + * When PopDownListPicker is imported into a render process, this static method + * must be called before an instances may be created. This is can be safely + * called multiple times. + */ + static init(): void { + PopDownDialog.init(); + if (registered === false) { + window.document.registerElement(PopDownListPicker.TAG_NAME, {prototype: PopDownListPicker.prototype}); + registered = true; + } + } + + // WARNING: Fields like this will not be initialised automatically. + private _entries: T[]; + + private _selectedId: string; + + private _titlePrimary: string; + + private _titleSecondary: string; + + private _filterEntries: (entries: T[], filterText: string) => T[]; + + private _formatEntries: (filteredEntries: T[], selectedId: string, filterInputValue: string) => string; + + private _laterHandle: domutils.LaterHandle; + + private _initProperties(): void { + this._entries = []; + this._selectedId = null; + this._titlePrimary = ""; + this._titleSecondary = ""; + this._filterEntries = (entries: T[], filterText: string): T[] => entries; + this._formatEntries = (filteredEntries: T[], selectedId: string, filterInputValue: string): string => + filteredEntries.map(entry => `
${entry.id}
`).join(""); + this._laterHandle = null; + } + + geSelected(): string { + return this._selectedId; + } + + setSelected(selectedId: string): void { + this._selectedId = selectedId; + this._updateEntries(); + this._scrollToSelected(); + } + + setEntries(entries: T[]): void { + this._entries = entries; + this._selectedId = null; + + const filterInput = domutils.getShadowId(this, ID_FILTER); + if (filterInput !== null) { + filterInput.value = ""; + } + this._updateEntries(); + } + + getEntries(): T[] { + return this._entries; + } + + setTitlePrimary(text: string): void { + this._titlePrimary = text; + const dialog = domutils.getShadowId(this, ID_DIALOG); + if (dialog != null) { + dialog.setTitlePrimary(text); + } + } + + getTitlePrimary(): string { + return this._titlePrimary; + } + + setTitleSecondary(text: string): void { + this._titleSecondary = text; + const dialog = domutils.getShadowId(this, ID_DIALOG); + if (dialog != null) { + dialog.setTitleSecondary(text); + } + } + + getTitleSecondary(): string { + return this._titleSecondary; + } + + setFilterEntriesFunc(func: (entries: T[], filterText: string) => T[]): void { + this._filterEntries = func; + } + + setFormatEntriesFunc(func: (filteredEntries: T[], selectedId: string, filterInputValue: string) => string): void { + this._formatEntries = func; + } + + //----------------------------------------------------------------------- + // + // # + // # # ###### ###### #### # # #### # ###### + // # # # # # # # # # # # # + // # # ##### ##### # # # # ##### + // # # # # # # # # # + // # # # # # # # # # # # + // ####### # # ###### #### # #### ###### ###### + // + //----------------------------------------------------------------------- + /** + * Custom Element 'created' life cycle hook. + */ + createdCallback() { + this._initProperties(); // Initialise our properties. The constructor was not called. + const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true }); + const clone = this.createClone(); + shadow.appendChild(clone); + this.updateThemeCss(); + + const dialog = domutils.getShadowId(this, ID_DIALOG); + dialog.setTitlePrimary(this._titlePrimary); + dialog.setTitleSecondary(this._titleSecondary); + dialog.addEventListener(PopDownDialog.EVENT_CLOSE_REQUEST, () => { + dialog.close(); + }); + + const filterInput = domutils.getShadowId(this, ID_FILTER); + filterInput.addEventListener('input', (ev: Event) => { + this._updateEntries(); + }); + + filterInput.addEventListener('keydown', (ev: KeyboardEvent) => { this.handleKeyDown(ev); }); + + const resultsDiv = domutils.getShadowId(this, ID_RESULTS); + resultsDiv.addEventListener('click', (ev: Event) => { + for (let node of ev.path) { + if (node instanceof HTMLElement) { + const dataId = node.attributes.getNamedItem(PopDownListPicker.ATTR_DATA_ID); + if (dataId !== undefined && dataId !== null) { + this._okId(dataId.value); + } + } + } + }); + } + + /** + * + */ + private createClone(): Node { + let template = window.document.getElementById(ID); + if (template === null) { + template = window.document.createElement('template'); + template.id = ID; + template.innerHTML = ` + <${PopDownDialog.TAG_NAME} id="${ID_DIALOG}"> +
+
+ + `; + window.document.body.appendChild(template); + } + + return window.document.importNode(template.content, true); + } + + protected _themeCssFiles(): ThemeTypes.CssFile[] { + return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_COMMANDPALETTE]; + } + + private _updateEntries(): void { + const filterInputValue = ( domutils.getShadowId(this, ID_FILTER)).value; + const filteredEntries = this._filterEntries(this._entries, filterInputValue); + + if (filteredEntries.length === 0) { + this._selectedId = null; + } else { + const newSelectedIndex = filteredEntries.findIndex( (entry) => entry.id === this._selectedId); + this._selectedId = filteredEntries[Math.max(0, newSelectedIndex)].id; + } + + const html = this._formatEntries(filteredEntries, this._selectedId, filterInputValue); + domutils.getShadowId(this, ID_RESULTS).innerHTML = html; + } + + private _scrollToSelected(): void { + const resultsDiv = domutils.getShadowId(this, ID_RESULTS); + const selectedElement = resultsDiv.querySelector("." + PopDownListPicker.CLASS_RESULT_SELECTED); + const selectedRelativeTop = selectedElement.offsetTop - resultsDiv.offsetTop; + resultsDiv.scrollTop = selectedRelativeTop; + } + + //----------------------------------------------------------------------- + /** + * + */ + private handleKeyDown(ev: KeyboardEvent) { + // Escape. + if (ev.keyIdentifier === "U+001B") { + this._okId(null); + ev.preventDefault(); + ev.stopPropagation(); + return; + } + + const isPageKey = ev.keyIdentifier === "PageUp" || ev.keyIdentifier === "PageDown"; + const isUp = ev.keyIdentifier === "PageUp" || ev.keyIdentifier === "Up" || ev.keyIdentifier === "Home"; + + if (isPageKey || isUp || ev.keyIdentifier === "Down" || ev.keyIdentifier === "End" || ev.keyIdentifier === "Enter") { + ev.preventDefault(); + ev.stopPropagation(); + + const filterInput = domutils.getShadowId(this, ID_FILTER); + const filteredEntries = this._filterEntries(this._entries, filterInput.value); + if (filteredEntries.length === 0) { + return; + } + + const selectedIndex = filteredEntries.findIndex( (entry) => entry.id === this._selectedId); + + if (ev.keyIdentifier === "Enter") { + // Enter + if (this._selectedId !== null) { + this._okId(this._selectedId); + } + } else { + + const resultsDiv = domutils.getShadowId(this, ID_RESULTS); + + // Determine the step size. + let stepSize = 1; + if (isPageKey) { + const selectedElement = resultsDiv.querySelector("." + PopDownListPicker.CLASS_RESULT_SELECTED); + const selectedElementDimensions = selectedElement.getBoundingClientRect(); + + stepSize = Math.floor(resultsDiv.clientHeight / selectedElementDimensions.height); + } + + if (isUp) { + if (ev.keyIdentifier === "Home") { + this._selectedId = filteredEntries[0].id; + } else { + this._selectedId = filteredEntries[Math.max(0, selectedIndex-stepSize)].id; + } + } else { + if (ev.keyIdentifier === "End") { + this._selectedId = filteredEntries[filteredEntries.length-1].id; + } else { + this._selectedId = filteredEntries[Math.min(filteredEntries.length-1, selectedIndex+stepSize)].id; + } + } + + const top = resultsDiv.scrollTop; + this._updateEntries(); + resultsDiv.scrollTop = top; + + const selectedElement = resultsDiv.querySelector("." + PopDownListPicker.CLASS_RESULT_SELECTED); + const selectedRelativeTop = selectedElement.offsetTop - resultsDiv.offsetTop; + if (top > selectedRelativeTop) { + resultsDiv.scrollTop = selectedRelativeTop; + } else { + const selectedElementDimensions = selectedElement.getBoundingClientRect(); + if (selectedRelativeTop + selectedElementDimensions.height > top + resultsDiv.clientHeight) { + resultsDiv.scrollTop = selectedRelativeTop + selectedElementDimensions.height - resultsDiv.clientHeight; + } + } + } + } + } + + /** + * + */ + open(x: number, y: number, width: number, height: number): void { + const resultsDiv = domutils.getShadowId(this, ID_RESULTS); + resultsDiv.style.maxHeight = `${height/2}px`; + + const filterInput = domutils.getShadowId(this, ID_FILTER); + filterInput.value = ""; + this._updateEntries(); + filterInput.focus(); + + const dialog = domutils.getShadowId(this, ID_DIALOG); + dialog.open(x, y, width, height); + + this._scrollToSelected(); + } + + /** + * + */ + close(): void { + const dialog = domutils.getShadowId(this, ID_DIALOG); + dialog.close(); + } + + private _okId(selectedId: string): void { + if (this._laterHandle === null) { + this._laterHandle = domutils.doLater( () => { + this.close(); + this._laterHandle = null; + const event = new CustomEvent("selected", { detail: {selected: selectedId } }); + this.dispatchEvent(event); + }); + } + } +} + +export = PopDownListPicker; diff --git a/src/plugins/TextViewer/TextViewerPlugin.js b/src/plugins/TextViewer/TextViewerPlugin.js new file mode 100644 index 000000000..1922ad5ee --- /dev/null +++ b/src/plugins/TextViewer/TextViewerPlugin.js @@ -0,0 +1,58 @@ +"use strict"; +const Logger = require("../../logger"); +const PopDownListPicker = require("../../gui/PopDownListPicker"); +const CodeMirror = require("codemirror"); +const he = require("he"); +class TextViewerPlugin { + constructor(api) { + this._syntaxDialog = null; + this._log = new Logger("TextViewerPlugin", this); + // api.addNewTopLevelEventListener( (el: HTMLElement): void => { + // this._log.debug("Saw a new top level"); + // }); + api.addNewTabEventListener((el) => { + el.addEventListener("TEXTVIEWER_EVENT_COMMAND_SYNTAX_HIGHLIGHTING", this._handleSyntaxHighlighting.bind(this)); + }); + } + _handleSyntaxHighlighting(ev) { + const srcElement = ev.detail.srcElement; + if (this._syntaxDialog == null) { + this._syntaxDialog = window.document.createElement(PopDownListPicker.TAG_NAME); + this._syntaxDialog.setTitlePrimary("Syntax Highlighting"); + this._syntaxDialog.setFormatEntriesFunc((filteredEntries, selectedId, filterInputValue) => { + return filteredEntries.map((entry) => { + return `
+ ${he.encode(entry.name)} ${he.encode(entry.id)} +
`; + }).join(""); + }); + this._syntaxDialog.setFilterEntriesFunc((entries, filterText) => { + const lowerFilterText = filterText.toLowerCase(); + return entries.filter((entry) => { + return entry.name.toLowerCase().indexOf(lowerFilterText) !== -1 || entry.id.toLowerCase().indexOf(lowerFilterText) !== -1; + }); + }); + this._syntaxDialog.addEventListener("selected", (ev) => { + if (ev.detail.selected != null) { + srcElement.mimeType = ev.detail.selected; + } + srcElement.focus(); + }); + window.document.body.appendChild(this._syntaxDialog); + } + const mimeList = CodeMirror.modeInfo.map((info) => { + return { id: info.mime, name: info.name }; + }); + this._syntaxDialog.setEntries(mimeList); + this._syntaxDialog.setSelected(srcElement.mimeType); + const rect = ev.target.getBoundingClientRect(); + this._syntaxDialog.open(rect.left, rect.top, rect.width, rect.height); + this._syntaxDialog.focus(); + } +} +function factory(api) { + PopDownListPicker.init(); + return new TextViewerPlugin(api); +} +module.exports = factory; +//# sourceMappingURL=TextViewerPlugin.js.map \ No newline at end of file diff --git a/src/plugins/TextViewer/TextViewerPlugin.ts b/src/plugins/TextViewer/TextViewerPlugin.ts new file mode 100644 index 000000000..83b7af310 --- /dev/null +++ b/src/plugins/TextViewer/TextViewerPlugin.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ +import * as PluginApi from '../../PluginApi'; +import Logger = require('../../logger'); +import TextViewer = require('../../viewers/textviewer'); +import PopDownListPicker = require('../../gui/PopDownListPicker'); +import CodeMirror = require('codemirror'); +import he = require('he'); + +interface SyntaxEntry { + id: string; + name: string; +} + +class TextViewerPlugin implements PluginApi.ExtratermPlugin { + + private _log: Logger; + + private _syntaxDialog: PopDownListPicker = null; + + constructor(api: PluginApi.ExtratermApi) { + this._log = new Logger("TextViewerPlugin", this); + // api.addNewTopLevelEventListener( (el: HTMLElement): void => { + // this._log.debug("Saw a new top level"); + // }); + + api.addNewTabEventListener( (el: HTMLElement): void => { + el.addEventListener("TEXTVIEWER_EVENT_COMMAND_SYNTAX_HIGHLIGHTING", this._handleSyntaxHighlighting.bind(this)); + }); + } + + private _handleSyntaxHighlighting(ev: CustomEvent): void { + const srcElement = ev.detail.srcElement; + if (this._syntaxDialog == null) { + this._syntaxDialog = > window.document.createElement(PopDownListPicker.TAG_NAME); + this._syntaxDialog.setTitlePrimary("Syntax Highlighting"); + + this._syntaxDialog.setFormatEntriesFunc( (filteredEntries: SyntaxEntry[], selectedId: string, filterInputValue: string): string => { + return filteredEntries.map( (entry): string => { + return `
+ ${he.encode(entry.name)} ${he.encode(entry.id)} +
`; + }).join(""); + }); + + this._syntaxDialog.setFilterEntriesFunc( (entries: SyntaxEntry[], filterText: string): SyntaxEntry[] => { + const lowerFilterText = filterText.toLowerCase(); + return entries.filter( (entry: SyntaxEntry): boolean => { + return entry.name.toLowerCase().indexOf(lowerFilterText) !== -1 || entry.id.toLowerCase().indexOf(lowerFilterText) !== -1; + }); + }); + + this._syntaxDialog.addEventListener("selected", (ev: CustomEvent): void => { + if (ev.detail.selected != null) { + srcElement.mimeType = ev.detail.selected; + } + srcElement.focus(); + }); + window.document.body.appendChild(this._syntaxDialog); + } + + const mimeList = CodeMirror.modeInfo.map( (info) => { + return { id: info.mime, name: info.name}; + }); + this._syntaxDialog.setEntries(mimeList); + this._syntaxDialog.setSelected(srcElement.mimeType); + + const rect = ( ev.target).getBoundingClientRect(); + this._syntaxDialog.open(rect.left, rect.top, rect.width, rect.height); + this._syntaxDialog.focus(); + } + +} + +function factory(api: PluginApi.ExtratermApi): PluginApi.ExtratermPlugin { + PopDownListPicker.init(); + return new TextViewerPlugin(api); +} +export = factory; diff --git a/src/plugins/TextViewer/metadata.json b/src/plugins/TextViewer/metadata.json new file mode 100644 index 000000000..89924788b --- /dev/null +++ b/src/plugins/TextViewer/metadata.json @@ -0,0 +1,4 @@ +{ + "name": "TextViewer", + "factory": "TextViewerPlugin.js" +} diff --git a/src/viewers/textviewer.ts b/src/viewers/textviewer.ts index 8543dfb0e..619f0e5ff 100644 --- a/src/viewers/textviewer.ts +++ b/src/viewers/textviewer.ts @@ -50,6 +50,9 @@ const COMMAND_TYPE_AND_CR_SELECTION = "typeSelectionAndCr"; const COMMAND_TYPE_SELECTION = "typeSelection"; const COMMAND_OPEN_COMMAND_PALETTE = CommandPaletteRequestTypes.COMMAND_OPEN_COMMAND_PALETTE; +const COMMAND_SYNTAX_HIGHLIGHTING = "syntaxHighlighting"; +const EVENT_COMMAND_SYNTAX_HIGHLIGHTING = "TEXTVIEWER_EVENT_COMMAND_SYNTAX_HIGHLIGHTING"; + const COMMANDS = [ COMMAND_TYPE_AND_CR_SELECTION, COMMAND_TYPE_SELECTION, @@ -144,7 +147,7 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C this._mimeType = null; this._log = new Logger(EtTextViewer.TAG_NAME, this); this._commandLine = null; - this._returnCode =null; + this._returnCode = null; this._editable = false; this._codeMirror = null; this._height = 0; @@ -256,8 +259,10 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C this._mimeType = mimeType; const modeInfo = CodeMirror.findModeByMIME(mimeType); - if (modeInfo.mode !== undefined && modeInfo.mode !== null && modeInfo.mode !== "null") { - LoadCodeMirrorMode(modeInfo.mode); + if (modeInfo.mode !== undefined) { + if (modeInfo.mode !== null && modeInfo.mode !== "null") { + LoadCodeMirrorMode(modeInfo.mode); + } if (this._codeMirror !== null) { this._codeMirror.setOption("mode", mimeType); } @@ -892,7 +897,8 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C private _commandPaletteEntries(): CommandPaletteRequestTypes.CommandEntry[] { let commandList: CommandPaletteRequestTypes.CommandEntry[] = [ { id: COMMAND_TYPE_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection", target: this }, - { id: COMMAND_TYPE_AND_CR_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection & Execute", target: this } + { id: COMMAND_TYPE_AND_CR_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection & Execute", target: this }, + { id: COMMAND_SYNTAX_HIGHLIGHTING, group: PALETTE_GROUP, iconRight: "", label: "Syntax", target: this } ]; if (this._mode ===ViewerElementTypes.Mode.CURSOR) { @@ -956,6 +962,10 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C commandPaletteRequestDetail); this.dispatchEvent(commandPaletteRequestEvent); break; + + case COMMAND_SYNTAX_HIGHLIGHTING: + this.dispatchEvent(new CustomEvent(EVENT_COMMAND_SYNTAX_HIGHLIGHTING, {bubbles: true, composed: true, detail: { srcElement: this } } )); + break; default: if (this._mode === ViewerElementTypes.Mode.CURSOR && CodeMirrorCommands.isCommand(command)) { diff --git a/tsconfig.json b/tsconfig.json index 7d8d5ae51..112389bbe 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,7 @@ "./src/PluginApi.ts", "./src/PluginManager.ts", "./src/plugins/TerminalViewerExtra/TerminalViewerExtra.ts", + "./src/plugins/TextViewer/TextViewerPlugin.ts", "./src/abouttab.ts", "./src/codemirror_extra.d.ts", "./src/codemirrorcommands.ts", @@ -84,6 +85,8 @@ "./src/webipc.ts", "./src/webresourceloader.ts", "./src/windowmessages.ts", + "./src/gui/PopDownDialog.ts", + "./src/gui/PopDownListPicker.ts", "./src/gui/checkboxmenuitem.ts", "./src/gui/commandpalette.ts", "./src/gui/commandpalettetypes.ts", From 1dd5d18fb5fd748767e4dd9c5e34b488bae4aa8b Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Fri, 10 Feb 2017 22:14:48 +0100 Subject: [PATCH 05/14] Set a hover effect for the command palette and other lists. --- src/themes/atomic-dark-ui/gui-commandpalette.scss | 6 ++++++ src/themes/atomic-light-ui/gui-commandpalette.scss | 6 ++++++ src/themes/default/gui-commandpalette.scss | 8 +++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/themes/atomic-dark-ui/gui-commandpalette.scss b/src/themes/atomic-dark-ui/gui-commandpalette.scss index 915f3112e..09c933be9 100644 --- a/src/themes/atomic-dark-ui/gui-commandpalette.scss +++ b/src/themes/atomic-dark-ui/gui-commandpalette.scss @@ -113,6 +113,12 @@ div > #ID_FILTER { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); + &:hover { + cursor: pointer; + outline: 1px solid $background-color-selected; + outline-offset: -1px; + } + &.CLASS_RESULT_SELECTED { outline: none; text-shadow: none; diff --git a/src/themes/atomic-light-ui/gui-commandpalette.scss b/src/themes/atomic-light-ui/gui-commandpalette.scss index 8e3f6df9c..38b89f887 100644 --- a/src/themes/atomic-light-ui/gui-commandpalette.scss +++ b/src/themes/atomic-light-ui/gui-commandpalette.scss @@ -111,6 +111,12 @@ div > #ID_FILTER { padding-left: $padding-xs-horizontal; padding-right: $padding-xs-horizontal; + &:hover { + cursor: pointer; + outline: 1px solid $background-color-selected; + outline-offset: -1px; + } + &.CLASS_RESULT_SELECTED { outline: none; color: $text-color-selected; diff --git a/src/themes/default/gui-commandpalette.scss b/src/themes/default/gui-commandpalette.scss index 1e739f47f..0da3110a3 100644 --- a/src/themes/default/gui-commandpalette.scss +++ b/src/themes/default/gui-commandpalette.scss @@ -107,10 +107,16 @@ div > #ID_FILTER { .CLASS_RESULT_ENTRY { display: flex; width: 100%; - + padding-left: $padding-xs-horizontal; padding-right: $padding-xs-horizontal; + &:hover { + cursor: pointer; + outline: 1px solid $btn-primary-bg; + outline-offset: -1px; + } + &.CLASS_RESULT_SELECTED { outline: none; color: $btn-primary-color; From 387d79ed0174733e630fa67ba9657828c2c3647d Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sat, 11 Feb 2017 09:48:36 +0100 Subject: [PATCH 06/14] Improved the sorting on syntax names in the text viewer. --- src/gui/PopDownListPicker.ts | 2 +- src/plugins/TextViewer/TextViewerPlugin.js | 27 +++++++++++++++-- src/plugins/TextViewer/TextViewerPlugin.ts | 34 ++++++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/gui/PopDownListPicker.ts b/src/gui/PopDownListPicker.ts index 453edcd31..3ad346e93 100644 --- a/src/gui/PopDownListPicker.ts +++ b/src/gui/PopDownListPicker.ts @@ -122,7 +122,7 @@ class PopDownListPicker extends ThemeableElementBase return this._titleSecondary; } - setFilterEntriesFunc(func: (entries: T[], filterText: string) => T[]): void { + setFilterAndRankEntriesFunc(func: (entries: T[], filterText: string) => T[]): void { this._filterEntries = func; } diff --git a/src/plugins/TextViewer/TextViewerPlugin.js b/src/plugins/TextViewer/TextViewerPlugin.js index 1922ad5ee..947d9a888 100644 --- a/src/plugins/TextViewer/TextViewerPlugin.js +++ b/src/plugins/TextViewer/TextViewerPlugin.js @@ -26,11 +26,32 @@ class TextViewerPlugin {
`; }).join(""); }); - this._syntaxDialog.setFilterEntriesFunc((entries, filterText) => { - const lowerFilterText = filterText.toLowerCase(); - return entries.filter((entry) => { + this._syntaxDialog.setFilterAndRankEntriesFunc((entries, filterText) => { + const lowerFilterText = filterText.toLowerCase().trim(); + const filtered = entries.filter((entry) => { return entry.name.toLowerCase().indexOf(lowerFilterText) !== -1 || entry.id.toLowerCase().indexOf(lowerFilterText) !== -1; }); + const rankFunc = (entry, lowerFilterText) => { + const lowerName = entry.name.toLowerCase(); + if (lowerName === lowerFilterText) { + return 1000; + } + const lowerId = entry.id.toLowerCase(); + if (lowerId === lowerFilterText) { + return 800; + } + const pos = lowerName.indexOf(lowerFilterText); + if (pos !== -1) { + return 500 - pos; // Bias it for matches at the front of the text. + } + const pos2 = lowerId.indexOf(lowerFilterText); + if (pos2 !== -1) { + return 400 - pos2; + } + return 0; + }; + filtered.sort((a, b) => rankFunc(b, lowerFilterText) - rankFunc(a, lowerFilterText)); + return filtered; }); this._syntaxDialog.addEventListener("selected", (ev) => { if (ev.detail.selected != null) { diff --git a/src/plugins/TextViewer/TextViewerPlugin.ts b/src/plugins/TextViewer/TextViewerPlugin.ts index 83b7af310..b8bac123c 100644 --- a/src/plugins/TextViewer/TextViewerPlugin.ts +++ b/src/plugins/TextViewer/TextViewerPlugin.ts @@ -46,11 +46,39 @@ class TextViewerPlugin implements PluginApi.ExtratermPlugin { }).join(""); }); - this._syntaxDialog.setFilterEntriesFunc( (entries: SyntaxEntry[], filterText: string): SyntaxEntry[] => { - const lowerFilterText = filterText.toLowerCase(); - return entries.filter( (entry: SyntaxEntry): boolean => { + this._syntaxDialog.setFilterAndRankEntriesFunc( (entries: SyntaxEntry[], filterText: string): SyntaxEntry[] => { + const lowerFilterText = filterText.toLowerCase().trim(); + const filtered = entries.filter( (entry: SyntaxEntry): boolean => { return entry.name.toLowerCase().indexOf(lowerFilterText) !== -1 || entry.id.toLowerCase().indexOf(lowerFilterText) !== -1; }); + + const rankFunc = (entry: SyntaxEntry, lowerFilterText: string): number => { + const lowerName = entry.name.toLowerCase(); + if (lowerName === lowerFilterText) { + return 1000; + } + + const lowerId = entry.id.toLowerCase(); + if (lowerId === lowerFilterText) { + return 800; + } + + const pos = lowerName.indexOf(lowerFilterText); + if (pos !== -1) { + return 500 - pos; // Bias it for matches at the front of the text. + } + + const pos2 = lowerId.indexOf(lowerFilterText); + if (pos2 !== -1) { + return 400 - pos2; + } + + return 0; + }; + + filtered.sort( (a: SyntaxEntry,b: SyntaxEntry): number => rankFunc(b, lowerFilterText) - rankFunc(a, lowerFilterText)); + + return filtered; }); this._syntaxDialog.addEventListener("selected", (ev: CustomEvent): void => { From eeab8590b0b257c5a71cd70ec01a3bc37d97f8f3 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sat, 11 Feb 2017 15:53:48 +0100 Subject: [PATCH 07/14] CSS files for the PopDownDialog component. --- src/gui/PopDownDialog.ts | 2 +- src/theme.ts | 6 +- .../atomic-dark-ui/gui-popdowndialog.scss | 90 +++++++++++++++++++ .../atomic-light-ui/gui-popdowndialog.scss | 90 +++++++++++++++++++ src/themes/default/gui-popdowndialog.scss | 90 +++++++++++++++++++ src/viewers/textviewer.ts | 2 +- 6 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 src/themes/atomic-dark-ui/gui-popdowndialog.scss create mode 100644 src/themes/atomic-light-ui/gui-popdowndialog.scss create mode 100644 src/themes/default/gui-popdowndialog.scss diff --git a/src/gui/PopDownDialog.ts b/src/gui/PopDownDialog.ts index 3f99960e9..a9acd63ef 100644 --- a/src/gui/PopDownDialog.ts +++ b/src/gui/PopDownDialog.ts @@ -138,7 +138,7 @@ class PopDownDialog extends ThemeableElementBase { } protected _themeCssFiles(): ThemeTypes.CssFile[] { - return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_COMMANDPALETTE]; + return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_POP_DOWN_DIALOG]; } //----------------------------------------------------------------------- diff --git a/src/theme.ts b/src/theme.ts index 7ab7f178f..79cdfa393 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -37,6 +37,7 @@ export enum CssFile { GUI_SCROLLBAR, KEY_BINDINGS_TAB, GUI_COMMANDPALETTE, + GUI_POP_DOWN_DIALOG, TIP_VIEWER, FONT_AWESOME, TERMINAL_VARS, @@ -61,6 +62,7 @@ export const cssFileEnumItems: CssFile[] = [ CssFile.GUI_SCROLLBAR, CssFile.KEY_BINDINGS_TAB, CssFile.GUI_COMMANDPALETTE, + CssFile.GUI_POP_DOWN_DIALOG, CssFile.TIP_VIEWER, CssFile.FONT_AWESOME, CssFile.TERMINAL_VARS, @@ -85,6 +87,7 @@ const _CssFileNameMapping = { [CssFile.GUI_SCROLLBAR]: "gui-scrollbar", [CssFile.KEY_BINDINGS_TAB]: "key-bindings-tab", [CssFile.GUI_COMMANDPALETTE]: "gui-commandpalette", + [CssFile.GUI_POP_DOWN_DIALOG]: "gui-popdowndialog", [CssFile.TIP_VIEWER]: "tip-viewer", [CssFile.FONT_AWESOME]: "font-awesome", [CssFile.TERMINAL_VARS]: "terminal-vars", @@ -123,5 +126,6 @@ export const UiCssFiles: CssFile[] = [ CssFile.GUI_COMMANDPALETTE, CssFile.TIP_VIEWER, CssFile.FONT_AWESOME, - CssFile.VIEWER_TAB + CssFile.VIEWER_TAB, + CssFile.GUI_POP_DOWN_DIALOG, ]; diff --git a/src/themes/atomic-dark-ui/gui-popdowndialog.scss b/src/themes/atomic-dark-ui/gui-popdowndialog.scss new file mode 100644 index 000000000..b251d1ac1 --- /dev/null +++ b/src/themes/atomic-dark-ui/gui-popdowndialog.scss @@ -0,0 +1,90 @@ +/** + * Copyright 2016 Simon Edwards + */ + +/* Command Palette */ +@import "bootstrap/variables"; +@import "extraterm-scrollbars"; + +/* +:host { + visibility: hidden; +} +*/ + +#ID_COVER { + &.CLASS_COVER_CLOSED { + visibility: hidden; + } + + &.CLASS_COVER_OPEN { + position: fixed; + visibility: visible; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + } +} + +#ID_CONTAINER { + padding: $padding-base-vertical $padding-base-horizontal; +} + +#ID_TITLE_CONTAINER { + width: 100%; + display: flex; + + font-family: $font-family-base; + font-size: $input-font-size; + line-height: $line-height-base; + color: $text-color; + margin-bottom: 5px; +} + +#ID_TITLE_PRIMARY { + flex-grow: 1; + font-weight: normal; +} + +#ID_TITLE_SECONDARY { + flex-grow: 0; +} + +#ID_CONTEXT_COVER { + position: relative; +} + +#ID_CONTEXT_COVER { + &.CLASS_CONTEXT_COVER_OPEN { + + } + + &.CLASS_CONTEXT_COVER_CLOSED { + display: none; + } +} + + +#ID_CONTAINER { + position: absolute; + + $width: 400px; + width: $width; + left: calc(50% - #{$width} / 2); + + font-family: $font-family-base; + font-size: $input-font-size; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + background-color: $dropdown-bg; + + border: 1px solid $dropdown-border; + border-radius: $border-radius-base; + box-shadow: 0px 6px 12px rgba(0,0,0,.175); + background-clip: padding-box; + + z-index: 101; +} diff --git a/src/themes/atomic-light-ui/gui-popdowndialog.scss b/src/themes/atomic-light-ui/gui-popdowndialog.scss new file mode 100644 index 000000000..b251d1ac1 --- /dev/null +++ b/src/themes/atomic-light-ui/gui-popdowndialog.scss @@ -0,0 +1,90 @@ +/** + * Copyright 2016 Simon Edwards + */ + +/* Command Palette */ +@import "bootstrap/variables"; +@import "extraterm-scrollbars"; + +/* +:host { + visibility: hidden; +} +*/ + +#ID_COVER { + &.CLASS_COVER_CLOSED { + visibility: hidden; + } + + &.CLASS_COVER_OPEN { + position: fixed; + visibility: visible; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + } +} + +#ID_CONTAINER { + padding: $padding-base-vertical $padding-base-horizontal; +} + +#ID_TITLE_CONTAINER { + width: 100%; + display: flex; + + font-family: $font-family-base; + font-size: $input-font-size; + line-height: $line-height-base; + color: $text-color; + margin-bottom: 5px; +} + +#ID_TITLE_PRIMARY { + flex-grow: 1; + font-weight: normal; +} + +#ID_TITLE_SECONDARY { + flex-grow: 0; +} + +#ID_CONTEXT_COVER { + position: relative; +} + +#ID_CONTEXT_COVER { + &.CLASS_CONTEXT_COVER_OPEN { + + } + + &.CLASS_CONTEXT_COVER_CLOSED { + display: none; + } +} + + +#ID_CONTAINER { + position: absolute; + + $width: 400px; + width: $width; + left: calc(50% - #{$width} / 2); + + font-family: $font-family-base; + font-size: $input-font-size; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + background-color: $dropdown-bg; + + border: 1px solid $dropdown-border; + border-radius: $border-radius-base; + box-shadow: 0px 6px 12px rgba(0,0,0,.175); + background-clip: padding-box; + + z-index: 101; +} diff --git a/src/themes/default/gui-popdowndialog.scss b/src/themes/default/gui-popdowndialog.scss new file mode 100644 index 000000000..a8c7a3aed --- /dev/null +++ b/src/themes/default/gui-popdowndialog.scss @@ -0,0 +1,90 @@ +/** + * Copyright 2016 Simon Edwards + */ + +/* Command Palette */ +@import "bootstrap/variables"; +@import "extraterm-scrollbars"; + +/* +:host { + visibility: hidden; +} +*/ + +#ID_COVER { + &.CLASS_COVER_CLOSED { + visibility: hidden; + } + + &.CLASS_COVER_OPEN { + position: fixed; + visibility: visible; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + } +} + +#ID_CONTAINER { + padding: $padding-base-vertical $padding-base-horizontal; +} + +#ID_CONTEXT_COVER { + position: relative; +} + +#ID_CONTEXT_COVER { + &.CLASS_CONTEXT_COVER_OPEN { + + } + + &.CLASS_CONTEXT_COVER_CLOSED { + display: none; + } +} + + +#ID_CONTAINER { + position: absolute; + + $width: 400px; + width: $width; + left: calc(50% - #{$width} / 2); + + font-family: $font-family-base; + font-size: $font-size-base; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + background-color: $dropdown-bg; + + border: 1px solid $dropdown-border; + border-radius: $border-radius-base; + box-shadow: 0px 6px 12px rgba(0,0,0,.175); + background-clip: padding-box; + + z-index: 101; +} + +#ID_TITLE_CONTAINER { + width: 100%; + display: flex; + + font-family: $font-family-base; + font-size: $font-size-base; + line-height: $line-height-base; + color: $text-color; + margin-bottom: 5px; +} + +#ID_TITLE_PRIMARY { + flex-grow: 1; + font-weight: bold; +} + +#ID_TITLE_SECONDARY { + flex-grow: 0; +} diff --git a/src/viewers/textviewer.ts b/src/viewers/textviewer.ts index 619f0e5ff..f5ec4884e 100644 --- a/src/viewers/textviewer.ts +++ b/src/viewers/textviewer.ts @@ -259,7 +259,7 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C this._mimeType = mimeType; const modeInfo = CodeMirror.findModeByMIME(mimeType); - if (modeInfo.mode !== undefined) { + if (modeInfo != null) { if (modeInfo.mode !== null && modeInfo.mode !== "null") { LoadCodeMirrorMode(modeInfo.mode); } From d6c4e6a72cad941b4f5b53941e8eadbf9fadf3c8 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sat, 11 Feb 2017 16:08:30 +0100 Subject: [PATCH 08/14] CSS for the Pop Down List Picker. --- src/gui/PopDownListPicker.ts | 2 +- src/theme.ts | 4 ++ .../atomic-dark-ui/gui-popdownlistpicker.scss | 51 +++++++++++++++++++ .../gui-popdownlistpicker.scss | 48 +++++++++++++++++ src/themes/default/gui-popdownlistpicker.scss | 48 +++++++++++++++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/themes/atomic-dark-ui/gui-popdownlistpicker.scss create mode 100644 src/themes/atomic-light-ui/gui-popdownlistpicker.scss create mode 100644 src/themes/default/gui-popdownlistpicker.scss diff --git a/src/gui/PopDownListPicker.ts b/src/gui/PopDownListPicker.ts index 3ad346e93..e62fc3656 100644 --- a/src/gui/PopDownListPicker.ts +++ b/src/gui/PopDownListPicker.ts @@ -199,7 +199,7 @@ class PopDownListPicker extends ThemeableElementBase } protected _themeCssFiles(): ThemeTypes.CssFile[] { - return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_COMMANDPALETTE]; + return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_POP_DOWN_LIST_PICKER]; } private _updateEntries(): void { diff --git a/src/theme.ts b/src/theme.ts index 79cdfa393..c21a1612c 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -38,6 +38,7 @@ export enum CssFile { KEY_BINDINGS_TAB, GUI_COMMANDPALETTE, GUI_POP_DOWN_DIALOG, + GUI_POP_DOWN_LIST_PICKER, TIP_VIEWER, FONT_AWESOME, TERMINAL_VARS, @@ -63,6 +64,7 @@ export const cssFileEnumItems: CssFile[] = [ CssFile.KEY_BINDINGS_TAB, CssFile.GUI_COMMANDPALETTE, CssFile.GUI_POP_DOWN_DIALOG, + CssFile.GUI_POP_DOWN_LIST_PICKER, CssFile.TIP_VIEWER, CssFile.FONT_AWESOME, CssFile.TERMINAL_VARS, @@ -88,6 +90,7 @@ const _CssFileNameMapping = { [CssFile.KEY_BINDINGS_TAB]: "key-bindings-tab", [CssFile.GUI_COMMANDPALETTE]: "gui-commandpalette", [CssFile.GUI_POP_DOWN_DIALOG]: "gui-popdowndialog", + [CssFile.GUI_POP_DOWN_LIST_PICKER]: "gui-popdownlistpicker", [CssFile.TIP_VIEWER]: "tip-viewer", [CssFile.FONT_AWESOME]: "font-awesome", [CssFile.TERMINAL_VARS]: "terminal-vars", @@ -128,4 +131,5 @@ export const UiCssFiles: CssFile[] = [ CssFile.FONT_AWESOME, CssFile.VIEWER_TAB, CssFile.GUI_POP_DOWN_DIALOG, + CssFile.GUI_POP_DOWN_LIST_PICKER, ]; diff --git a/src/themes/atomic-dark-ui/gui-popdownlistpicker.scss b/src/themes/atomic-dark-ui/gui-popdownlistpicker.scss new file mode 100644 index 000000000..251c40645 --- /dev/null +++ b/src/themes/atomic-dark-ui/gui-popdownlistpicker.scss @@ -0,0 +1,51 @@ +/** + * Copyright 2016 Simon Edwards + */ + +/* Command Palette */ +@import "bootstrap/variables"; +@import "extraterm-scrollbars"; + +/* +:host { + visibility: hidden; +} +*/ + +div > #ID_FILTER { + box-sizing: border-box; + width: 100%; +} + +#ID_FILTER { + box-sizing: border-box; + width: 100%; +} + +#ID_RESULTS { + overflow: auto; + cursor: default; +} + +.CLASS_RESULT_ENTRY { + display: flex; + width: 100%; + + padding-left: $padding-xs-horizontal; + padding-right: $padding-xs-horizontal; + + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); + + &:hover { + cursor: pointer; + outline: 1px solid $background-color-selected; + outline-offset: -1px; + } + + &.CLASS_RESULT_SELECTED { + outline: none; + text-shadow: none; + color: $text-color-selected; + background-color: $background-color-selected; + } +} diff --git a/src/themes/atomic-light-ui/gui-popdownlistpicker.scss b/src/themes/atomic-light-ui/gui-popdownlistpicker.scss new file mode 100644 index 000000000..dd3396704 --- /dev/null +++ b/src/themes/atomic-light-ui/gui-popdownlistpicker.scss @@ -0,0 +1,48 @@ +/** + * Copyright 2016 Simon Edwards + */ + +/* Command Palette */ +@import "bootstrap/variables"; +@import "extraterm-scrollbars"; + +/* +:host { + visibility: hidden; +} +*/ + +div > #ID_FILTER { + box-sizing: border-box; + width: 100%; +} + +#ID_FILTER { + box-sizing: border-box; + width: 100%; +} + +#ID_RESULTS { + overflow: auto; + cursor: default; +} + +.CLASS_RESULT_ENTRY { + display: flex; + width: 100%; + + padding-left: $padding-xs-horizontal; + padding-right: $padding-xs-horizontal; + + &:hover { + cursor: pointer; + outline: 1px solid $background-color-selected; + outline-offset: -1px; + } + + &.CLASS_RESULT_SELECTED { + outline: none; + color: $text-color-selected; + background-color: $background-color-selected; + } +} diff --git a/src/themes/default/gui-popdownlistpicker.scss b/src/themes/default/gui-popdownlistpicker.scss new file mode 100644 index 000000000..521b51376 --- /dev/null +++ b/src/themes/default/gui-popdownlistpicker.scss @@ -0,0 +1,48 @@ +/** + * Copyright 2016 Simon Edwards + */ + +/* Command Palette */ +@import "bootstrap/variables"; +@import "extraterm-scrollbars"; + +/* +:host { + visibility: hidden; +} +*/ + +div > #ID_FILTER { + box-sizing: border-box; + width: 100%; +} + +#ID_FILTER { + box-sizing: border-box; + width: 100%; +} + +#ID_RESULTS { + overflow: auto; + cursor: default; +} + +.CLASS_RESULT_ENTRY { + display: flex; + width: 100%; + + padding-left: $padding-xs-horizontal; + padding-right: $padding-xs-horizontal; + + &:hover { + cursor: pointer; + outline: 1px solid $btn-primary-bg; + outline-offset: -1px; + } + + &.CLASS_RESULT_SELECTED { + outline: none; + color: $btn-primary-color; + background-color: $btn-primary-bg; + } +} From 4b64cc541bdf1d6c1082cbd7de1a971c7494d7f5 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sat, 11 Feb 2017 21:20:03 +0100 Subject: [PATCH 09/14] Show the textviewer's currently active syntax highlighter in the menu. --- src/viewers/textviewer.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/viewers/textviewer.ts b/src/viewers/textviewer.ts index f5ec4884e..a3a043f89 100644 --- a/src/viewers/textviewer.ts +++ b/src/viewers/textviewer.ts @@ -272,7 +272,7 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C get mimeType(): string { return this._mimeType; } - + setBytes(buffer: Buffer, mimeType: string): void { let charset = "utf-8"; let cleanMimeType = mimeType; @@ -758,6 +758,15 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C this.dispatchEvent(event); } + private _getMimeTypeName(): string { + for (const info of CodeMirror.modeInfo) { + if (info.mime === this._mimeType) { + return info.name; + } + } + return this._mimeType; + } + scrollTo(optionsOrX: ScrollToOptions | number, y?: number): void { let xCoord = 0; let yCoord = 0; @@ -891,14 +900,14 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C if (this._mode === ViewerElementTypes.Mode.DEFAULT) { ev.stopPropagation(); ev.preventDefault(); - } + } } private _commandPaletteEntries(): CommandPaletteRequestTypes.CommandEntry[] { let commandList: CommandPaletteRequestTypes.CommandEntry[] = [ { id: COMMAND_TYPE_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection", target: this }, { id: COMMAND_TYPE_AND_CR_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection & Execute", target: this }, - { id: COMMAND_SYNTAX_HIGHLIGHTING, group: PALETTE_GROUP, iconRight: "", label: "Syntax", target: this } + { id: COMMAND_SYNTAX_HIGHLIGHTING, group: PALETTE_GROUP, iconRight: "", label: "Syntax: " + this._getMimeTypeName(), target: this } ]; if (this._mode ===ViewerElementTypes.Mode.CURSOR) { From 5689d517895fa0e2003a57838874ae131ac6a0df Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sat, 11 Feb 2017 21:39:33 +0100 Subject: [PATCH 10/14] Make it possible to specify extra Css files in the Pop Down List Picker. --- src/gui/PopDownListPicker.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/gui/PopDownListPicker.ts b/src/gui/PopDownListPicker.ts index e62fc3656..ac115b4ab 100644 --- a/src/gui/PopDownListPicker.ts +++ b/src/gui/PopDownListPicker.ts @@ -62,6 +62,8 @@ class PopDownListPicker extends ThemeableElementBase private _laterHandle: domutils.LaterHandle; + private _extraCssFiles: ThemeTypes.CssFile[]; + private _initProperties(): void { this._entries = []; this._selectedId = null; @@ -71,6 +73,7 @@ class PopDownListPicker extends ThemeableElementBase this._formatEntries = (filteredEntries: T[], selectedId: string, filterInputValue: string): string => filteredEntries.map(entry => `
${entry.id}
`).join(""); this._laterHandle = null; + this._extraCssFiles = []; } geSelected(): string { @@ -129,6 +132,16 @@ class PopDownListPicker extends ThemeableElementBase setFormatEntriesFunc(func: (filteredEntries: T[], selectedId: string, filterInputValue: string) => string): void { this._formatEntries = func; } + + /** + * Specify extra Css files to load into this element. + * + * @param extraCssFiles extra Css files which should be loaded along side the default set. + */ + addExtraCss(extraCssFiles: ThemeTypes.CssFile[]): void { + this._extraCssFiles = [...this._extraCssFiles, ...extraCssFiles]; + this.updateThemeCss(); + } //----------------------------------------------------------------------- // @@ -199,7 +212,8 @@ class PopDownListPicker extends ThemeableElementBase } protected _themeCssFiles(): ThemeTypes.CssFile[] { - return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_POP_DOWN_LIST_PICKER]; + return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, + ThemeTypes.CssFile.GUI_POP_DOWN_LIST_PICKER, ...this._extraCssFiles]; } private _updateEntries(): void { From 71f6434698d0a42f76fe959449cd482beb9d4abc Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sun, 12 Feb 2017 09:37:36 +0100 Subject: [PATCH 11/14] Converted the Command Palette to use PopDownListPicker instead. --- src/gui/PopDownListPicker.ts | 4 +- src/mainweb.ts | 95 +++++++++++--- .../atomic-dark-ui/gui-commandpalette.scss | 120 ------------------ .../atomic-light-ui/gui-commandpalette.scss | 119 ----------------- src/themes/default/gui-commandpalette.scss | 118 ----------------- 5 files changed, 81 insertions(+), 375 deletions(-) diff --git a/src/gui/PopDownListPicker.ts b/src/gui/PopDownListPicker.ts index ac115b4ab..0fdc9fdcd 100644 --- a/src/gui/PopDownListPicker.ts +++ b/src/gui/PopDownListPicker.ts @@ -31,6 +31,8 @@ class PopDownListPicker extends ThemeableElementBase static ATTR_DATA_ID = "data-id"; static CLASS_RESULT_SELECTED = "CLASS_RESULT_SELECTED"; + + static CLASS_RESULT_ENTRY = "CLASS_RESULT_ENTRY"; /** * Initialize the PopDownListPicker class and resources. @@ -132,7 +134,7 @@ class PopDownListPicker extends ThemeableElementBase setFormatEntriesFunc(func: (filteredEntries: T[], selectedId: string, filterInputValue: string) => string): void { this._formatEntries = func; } - + /** * Specify extra Css files to load into this element. * diff --git a/src/mainweb.ts b/src/mainweb.ts index aa944b425..228321678 100755 --- a/src/mainweb.ts +++ b/src/mainweb.ts @@ -10,6 +10,7 @@ const MenuItem = electron.remote.MenuItem; import sourceMapSupport = require('source-map-support'); import _ = require('lodash'); +import he = require('he'); import Logger = require('./logger'); import Messages = require('./windowmessages'); import webipc = require('./webipc'); @@ -17,7 +18,7 @@ import CbContextMenu = require('./gui/contextmenu'); import CbMenuItem = require('./gui/menuitem'); import CbDropDown = require('./gui/dropdown'); import CbCheckBoxMenuItem = require('./gui/checkboxmenuitem'); -import CbCommandPalette = require('./gui/commandpalette'); +import PopDownListPicker = require('./gui/PopDownListPicker'); import ResizeRefreshElementBase = require('./ResizeRefreshElementBase'); import CommandPaletteTypes = require('./gui/commandpalettetypes'); import CommandPaletteRequestTypes = require('./commandpaletterequesttypes'); @@ -65,6 +66,12 @@ const MENU_ITEM_RELOAD_CSS = 'reload_css'; const ID_COMMAND_PALETTE = "ID_COMMAND_PALETTE"; const ID_MENU_BUTTON = "ID_MENU_BUTTON"; +const CLASS_RESULT_GROUP_HEAD = "CLASS_RESULT_GROUP_HEAD"; +const CLASS_RESULT_ICON_LEFT = "CLASS_RESULT_ICON_LEFT"; +const CLASS_RESULT_ICON_RIGHT = "CLASS_RESULT_ICON_RIGHT"; +const CLASS_RESULT_LABEL = "CLASS_RESULT_LABEL"; +const CLASS_RESULT_SHORTCUT = "CLASS_RESULT_SHORTCUT"; + const _log = new Logger("mainweb"); /** @@ -140,7 +147,7 @@ export function startUp(): void { CbDropDown.init(); MainWebUi.init(); CbCheckBoxMenuItem.init(); - CbCommandPalette.init(); + PopDownListPicker.init(); ResizeCanary.init(); window.addEventListener('resize', () => { @@ -190,14 +197,8 @@ export function startUp(): void { mainWebUi.refresh(ResizeRefreshElementBase.RefreshLevel.COMPLETE); }); - // Command palette - const commandPalette = doc.createElement(CbCommandPalette.TAG_NAME); - commandPalette.id = ID_COMMAND_PALETTE; - commandPalette.titlePrimary = "Command Palette"; - commandPalette.titleSecondary = "Ctrl+Shift+P"; - doc.body.appendChild(commandPalette); - commandPalette.addEventListener('selected', handleCommandPaletteSelected); - + setUpCommandPalette(); + // Make sure something sensible is focussed if the window gets the focus. window.addEventListener('focus', () => { mainWebUi.focus(); @@ -522,6 +523,23 @@ function setCssVars(fontName: string, fontPath: string, terminalFontSize: number let commandPaletteRequestSource: HTMLElement = null; let commandPaletteRequestEntries: CommandPaletteRequestTypes.CommandEntry[] = null; +function setUpCommandPalette(): void { + const doc = window.document; + + // Command palette + const commandPalette = > doc.createElement(PopDownListPicker.TAG_NAME); + commandPalette.id = ID_COMMAND_PALETTE; + commandPalette.setTitlePrimary("Command Palette"); + commandPalette.setTitleSecondary("Ctrl+Shift+P"); + + commandPalette.setFilterAndRankEntriesFunc(commandPaletteFilterEntries); + commandPalette.setFormatEntriesFunc(commandPaletteFormatEntries); + commandPalette.addExtraCss([ThemeTypes.CssFile.GUI_COMMANDPALETTE]); + + doc.body.appendChild(commandPalette); + commandPalette.addEventListener('selected', handleCommandPaletteSelected); +} + function handleCommandPaletteRequest(request: CommandPaletteRequestTypes.CommandPaletteRequest): void { domutils.doLater( () => { @@ -540,11 +558,11 @@ function handleCommandPaletteRequest(request: CommandPaletteRequestTypes.Command }; }); - const commandPalette = document.getElementById(ID_COMMAND_PALETTE); + const commandPalette = > document.getElementById(ID_COMMAND_PALETTE); const shortcut = keyBindingManager.getKeyBindingContexts().context("main-ui").mapCommandToKeyBinding("openCommandPalette"); - commandPalette.titleSecondary = shortcut !== null ? shortcut : ""; + commandPalette.setTitleSecondary(shortcut !== null ? shortcut : ""); - commandPalette.entries = paletteEntries; + commandPalette.setEntries(paletteEntries); let rect: ClientRect = { left: 0, top: 0, width: 500, height: 500, right: 500, bottom: 500 }; if (request.contextElement !== null && request.contextElement !== undefined) { @@ -552,6 +570,7 @@ function handleCommandPaletteRequest(request: CommandPaletteRequestTypes.Command } commandPalette.open(rect.left, rect.top, rect.width, rect.height); + commandPalette.focus(); }); } @@ -575,15 +594,15 @@ function commandPaletteEntries(): CommandPaletteRequestTypes.CommandEntry[] { } function handleCommandPaletteSelected(ev: CustomEvent): void { - const commandPalette = document.getElementById(ID_COMMAND_PALETTE); + const commandPalette = > document.getElementById(ID_COMMAND_PALETTE); commandPalette.close(); if (commandPaletteRequestSource !== null) { commandPaletteRequestSource.focus(); } - const entryId = ev.detail.entryId; - if (entryId !== null) { - const commandIndex = Number.parseInt(entryId); + const selectedId = ev.detail.selected; + if (selectedId !== null) { + const commandIndex = Number.parseInt(selectedId); const commandEntry = commandPaletteRequestEntries[commandIndex]; domutils.doLater( () => { commandEntry.target.executeCommand(commandEntry.id); @@ -593,6 +612,48 @@ function handleCommandPaletteSelected(ev: CustomEvent): void { } } +function commandPaletteFilterEntries(entries: CommandPaletteTypes.CommandEntry[], filter: string): CommandPaletteTypes.CommandEntry[] { + const lowerFilter = filter.toLowerCase(); + return entries.filter( (entry) => entry.label.toLowerCase().includes(lowerFilter) ); +} + +function commandPaletteFormatEntries(entries: CommandPaletteTypes.CommandEntry[], selectedId: string, filterInputValue: string): string { + return (filterInputValue.trim() === "" ? commandPaletteFormatEntriesWithGroups : commandPaletteFormatEntriesAsList)(entries, this._selectedId); +} + +function commandPaletteFormatEntriesAsList(entries: CommandPaletteTypes.CommandEntry[], selectedId: string): string { + return entries.map( (entry) => commandPaletteFormatEntry(entry, entry.id === selectedId) ).join(""); +} + +function commandPaletteFormatEntriesWithGroups(entries: CommandPaletteTypes.CommandEntry[], selectedId: string): string { + let currentGroup: string = null; + const htmlParts: string[] = []; + + for (let entry of entries) { + let extraClass = ""; + if (entry.group !== currentGroup && currentGroup !== null) { + extraClass = CLASS_RESULT_GROUP_HEAD; + } + currentGroup = entry.group; + htmlParts.push(commandPaletteFormatEntry(entry, entry.id === selectedId, extraClass)); + } + + return htmlParts.join(""); +} + +function commandPaletteFormatEntry(entry: CommandPaletteTypes.CommandEntry, selected: boolean, extraClassString = ""): string { + return `
+
${commandPaletteFormatIcon(entry.iconLeft)}
+
${commandPaletteFormatIcon(entry.iconRight)}
+
${he.encode(entry.label)}
+
${entry.shortcut !== undefined && entry.shortcut !== null ? he.encode(entry.shortcut) : ""}
+
`; +} + +function commandPaletteFormatIcon(iconName?: string): string { + return ``; +} + class ConfigManagerImpl implements ConfigManager { private _config: Config = null; diff --git a/src/themes/atomic-dark-ui/gui-commandpalette.scss b/src/themes/atomic-dark-ui/gui-commandpalette.scss index 09c933be9..b2dd94ccc 100644 --- a/src/themes/atomic-dark-ui/gui-commandpalette.scss +++ b/src/themes/atomic-dark-ui/gui-commandpalette.scss @@ -4,128 +4,8 @@ /* Command Palette */ @import "bootstrap/variables"; -@import "extraterm-scrollbars"; - -/* -:host { - visibility: hidden; -} -*/ - -#ID_COVER { - &.CLASS_COVER_CLOSED { - visibility: hidden; - } - - &.CLASS_COVER_OPEN { - position: fixed; - visibility: visible; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 100; - } -} - -#ID_CONTAINER { - padding: $padding-base-vertical $padding-base-horizontal; -} - -#ID_TITLE_CONTAINER { - width: 100%; - display: flex; - - font-family: $font-family-base; - font-size: $input-font-size; - line-height: $line-height-base; - color: $text-color; - margin-bottom: 5px; -} - -#ID_TITLE_PRIMARY { - flex-grow: 1; - font-weight: normal; -} - -#ID_TITLE_SECONDARY { - flex-grow: 0; -} - -#ID_CONTEXT_COVER { - position: relative; -} - -#ID_CONTEXT_COVER { - &.CLASS_CONTEXT_COVER_OPEN { - - } - - &.CLASS_CONTEXT_COVER_CLOSED { - display: none; - } -} - - -#ID_CONTAINER { - position: absolute; - - $width: 400px; - width: $width; - left: calc(50% - #{$width} / 2); - - font-family: $font-family-base; - font-size: $input-font-size; - font-weight: normal; - line-height: $line-height-base; - color: $dropdown-link-color; - background-color: $dropdown-bg; - - border: 1px solid $dropdown-border; - border-radius: $border-radius-base; - box-shadow: 0px 6px 12px rgba(0,0,0,.175); - background-clip: padding-box; - - z-index: 101; -} - -div > #ID_FILTER { - box-sizing: border-box; - width: 100%; -} - -#ID_FILTER { - box-sizing: border-box; - width: 100%; -} - -#ID_RESULTS { - overflow: auto; - cursor: default; -} .CLASS_RESULT_ENTRY { - display: flex; - width: 100%; - - padding-left: $padding-xs-horizontal; - padding-right: $padding-xs-horizontal; - - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); - - &:hover { - cursor: pointer; - outline: 1px solid $background-color-selected; - outline-offset: -1px; - } - - &.CLASS_RESULT_SELECTED { - outline: none; - text-shadow: none; - color: $text-color-selected; - background-color: $background-color-selected; - } - &.CLASS_RESULT_GROUP_HEAD { border-top: 1px solid $dropdown-link-color; } diff --git a/src/themes/atomic-light-ui/gui-commandpalette.scss b/src/themes/atomic-light-ui/gui-commandpalette.scss index 38b89f887..e02c5a488 100644 --- a/src/themes/atomic-light-ui/gui-commandpalette.scss +++ b/src/themes/atomic-light-ui/gui-commandpalette.scss @@ -4,129 +4,11 @@ /* Command Palette */ @import "bootstrap/variables"; -@import "extraterm-scrollbars"; - -/* -:host { - visibility: hidden; -} -*/ - -#ID_COVER { - &.CLASS_COVER_CLOSED { - visibility: hidden; - } - - &.CLASS_COVER_OPEN { - position: fixed; - visibility: visible; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 100; - } -} - -#ID_CONTAINER { - padding: $padding-base-vertical $padding-base-horizontal; -} - -#ID_TITLE_CONTAINER { - width: 100%; - display: flex; - - font-family: $font-family-base; - font-size: $input-font-size; - line-height: $line-height-base; - color: $text-color; - margin-bottom: 5px; -} - -#ID_TITLE_PRIMARY { - flex-grow: 1; - font-weight: normal; -} - -#ID_TITLE_SECONDARY { - flex-grow: 0; -} - -#ID_CONTEXT_COVER { - position: relative; -} - -#ID_CONTEXT_COVER { - &.CLASS_CONTEXT_COVER_OPEN { - - } - - &.CLASS_CONTEXT_COVER_CLOSED { - display: none; - } -} - - -#ID_CONTAINER { - position: absolute; - - $width: 400px; - width: $width; - left: calc(50% - #{$width} / 2); - - font-family: $font-family-base; - font-size: $input-font-size; - font-weight: normal; - line-height: $line-height-base; - color: $dropdown-link-color; - background-color: $dropdown-bg; - - border: 1px solid $dropdown-border; - border-radius: $border-radius-base; - box-shadow: 0px 6px 12px rgba(0,0,0,.175); - background-clip: padding-box; - - z-index: 101; -} - -div > #ID_FILTER { - box-sizing: border-box; - width: 100%; -} - -#ID_FILTER { - box-sizing: border-box; - width: 100%; -} - -#ID_RESULTS { - overflow: auto; - cursor: default; -} .CLASS_RESULT_ENTRY { - display: flex; - width: 100%; - - padding-left: $padding-xs-horizontal; - padding-right: $padding-xs-horizontal; - - &:hover { - cursor: pointer; - outline: 1px solid $background-color-selected; - outline-offset: -1px; - } - - &.CLASS_RESULT_SELECTED { - outline: none; - color: $text-color-selected; - background-color: $background-color-selected; - } - &.CLASS_RESULT_GROUP_HEAD { border-top: 1px solid $dropdown-link-color; } - } .CLASS_RESULT_ICON_LEFT { @@ -144,4 +26,3 @@ div > #ID_FILTER { .CLASS_RESULT_SHORTCUT { flex: auto 0 0; } - diff --git a/src/themes/default/gui-commandpalette.scss b/src/themes/default/gui-commandpalette.scss index 0da3110a3..e02c5a488 100644 --- a/src/themes/default/gui-commandpalette.scss +++ b/src/themes/default/gui-commandpalette.scss @@ -4,125 +4,8 @@ /* Command Palette */ @import "bootstrap/variables"; -@import "extraterm-scrollbars"; - -/* -:host { - visibility: hidden; -} -*/ - -#ID_COVER { - &.CLASS_COVER_CLOSED { - visibility: hidden; - } - - &.CLASS_COVER_OPEN { - position: fixed; - visibility: visible; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 100; - } -} - -#ID_CONTAINER { - padding: $padding-base-vertical $padding-base-horizontal; -} - -#ID_CONTEXT_COVER { - position: relative; -} - -#ID_CONTEXT_COVER { - &.CLASS_CONTEXT_COVER_OPEN { - - } - - &.CLASS_CONTEXT_COVER_CLOSED { - display: none; - } -} - - -#ID_CONTAINER { - position: absolute; - - $width: 400px; - width: $width; - left: calc(50% - #{$width} / 2); - - font-family: $font-family-base; - font-size: $font-size-base; - font-weight: normal; - line-height: $line-height-base; - color: $dropdown-link-color; - background-color: $dropdown-bg; - - border: 1px solid $dropdown-border; - border-radius: $border-radius-base; - box-shadow: 0px 6px 12px rgba(0,0,0,.175); - background-clip: padding-box; - - z-index: 101; -} - -#ID_TITLE_CONTAINER { - width: 100%; - display: flex; - - font-family: $font-family-base; - font-size: $font-size-base; - line-height: $line-height-base; - color: $text-color; - margin-bottom: 5px; -} - -#ID_TITLE_PRIMARY { - flex-grow: 1; - font-weight: bold; -} - -#ID_TITLE_SECONDARY { - flex-grow: 0; -} - -div > #ID_FILTER { - box-sizing: border-box; - width: 100%; -} - -#ID_FILTER { - box-sizing: border-box; - width: 100%; -} - -#ID_RESULTS { - overflow: auto; - cursor: default; -} .CLASS_RESULT_ENTRY { - display: flex; - width: 100%; - - padding-left: $padding-xs-horizontal; - padding-right: $padding-xs-horizontal; - - &:hover { - cursor: pointer; - outline: 1px solid $btn-primary-bg; - outline-offset: -1px; - } - - &.CLASS_RESULT_SELECTED { - outline: none; - color: $btn-primary-color; - background-color: $btn-primary-bg; - } - &.CLASS_RESULT_GROUP_HEAD { border-top: 1px solid $dropdown-link-color; } @@ -143,4 +26,3 @@ div > #ID_FILTER { .CLASS_RESULT_SHORTCUT { flex: auto 0 0; } - From 296a8c997534087fbd2acb9dc78d46637f9fd1d5 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Sun, 12 Feb 2017 10:59:44 +0100 Subject: [PATCH 12/14] Removed the old command palette code. --- .gitignore | 1 - src/gui/commandpalette.ts | 388 -------------------------------------- tsconfig.json | 1 - 3 files changed, 390 deletions(-) delete mode 100644 src/gui/commandpalette.ts diff --git a/.gitignore b/.gitignore index 8a457d9a6..78a6976c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ src/config.js src/configuredialog.js src/domutils.js src/gui/checkboxmenuitem.js -src/gui/commandpalette.js src/gui/contextmenu.js src/gui/dropdown.js src/gui/markdownviewer.js diff --git a/src/gui/commandpalette.ts b/src/gui/commandpalette.ts deleted file mode 100644 index 0a44f7148..000000000 --- a/src/gui/commandpalette.ts +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright 2016 Simon Edwards - * - * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. - */ - -import ThemeableElementBase = require('../themeableelementbase'); -import ThemeTypes = require('../theme'); -import domutils = require('../domutils'); -import util = require('./util'); -import he = require('he'); -import CommandPaletteTypes = require('./commandpalettetypes'); - -const ID = "CbCommandPaletteTemplate"; -const ID_COVER = "ID_COVER"; -const ID_CONTEXT_COVER = "ID_CONTEXT_COVER"; -const ID_CONTAINER = "ID_CONTAINER"; -const ID_FILTER = "ID_FILTER"; -const ID_RESULTS = "ID_RESULTS"; -const ID_TITLE_PRIMARY = "ID_TITLE_PRIMARY"; -const ID_TITLE_SECONDARY = "ID_TITLE_SECONDARY"; -const ID_TITLE_CONTAINER = "ID_TITLE_CONTAINER"; - -const CLASS_RESULT_GROUP_HEAD = "CLASS_RESULT_GROUP_HEAD"; -const CLASS_RESULT_ENTRY = "CLASS_RESULT_ENTRY"; -const CLASS_RESULT_ICON_LEFT = "CLASS_RESULT_ICON_LEFT"; -const CLASS_RESULT_ICON_RIGHT = "CLASS_RESULT_ICON_RIGHT"; -const CLASS_RESULT_LABEL = "CLASS_RESULT_LABEL"; -const CLASS_RESULT_SHORTCUT = "CLASS_RESULT_SHORTCUT"; -const CLASS_RESULT_SELECTED = "CLASS_RESULT_SELECTED"; -const CLASS_CONTEXT_COVER_OPEN = "CLASS_CONTEXT_COVER_OPEN"; -const CLASS_CONTEXT_COVER_CLOSED = "CLASS_CONTEXT_COVER_CLOSED"; -const CLASS_COVER_CLOSED = "CLASS_COVER_CLOSED"; -const CLASS_COVER_OPEN = "CLASS_COVER_OPEN"; - -const ATTR_DATA_ID = "data-id"; - -let registered = false; - -/** - * A context menu. - */ -class CbCommandPalette extends ThemeableElementBase { - - /** - * The HTML tag name of this element. - */ - static TAG_NAME = "CB-COMMANDPALETTE"; - - /** - * Initialize the CbCommandPalette class and resources. - * - * When CbContextMenu is imported into a render process, this static method - * must be called before an instances may be created. This is can be safely - * called multiple times. - */ - static init(): void { - if (registered === false) { - window.document.registerElement(CbCommandPalette.TAG_NAME, {prototype: CbCommandPalette.prototype}); - registered = true; - } - } - - // WARNING: Fields like this will not be initialised automatically. - private _commandEntries: CommandPaletteTypes.CommandEntry[]; - - private _selectedId: string; - - private _titlePrimary: string; - - private _titleSecondary: string; - - private _laterHandle: domutils.LaterHandle; - - private _initProperties(): void { - this._commandEntries = []; - this._selectedId = null; - this._laterHandle = null; - this._titlePrimary = ""; - this._titleSecondary = ""; - } - - set entries(entries: CommandPaletteTypes.CommandEntry[]) { - this._commandEntries = entries; - this._selectedId = null; - - const filterInput = domutils.getShadowId(this, ID_FILTER); - if (filterInput !== null) { - filterInput.value = ""; - } - this._updateEntries(); - } - - get entries(): CommandPaletteTypes.CommandEntry[] { - return this._commandEntries; - } - - set titlePrimary(text: string) { - this._titlePrimary = text; - this._updateTitle(); - } - - get titlePrimary(): string { - return this._titlePrimary; - } - - set titleSecondary(text: string) { - this._titleSecondary = text; - this._updateTitle(); - } - - get titleSecondary(): string { - return this._titleSecondary; - } - - //----------------------------------------------------------------------- - // - // # - // # # ###### ###### #### # # #### # ###### - // # # # # # # # # # # # # - // # # ##### ##### # # # # ##### - // # # # # # # # # # - // # # # # # # # # # # # - // ####### # # ###### #### # #### ###### ###### - // - //----------------------------------------------------------------------- - /** - * Custom Element 'created' life cycle hook. - */ - createdCallback() { - this._initProperties(); // Initialise our properties. The constructor was not called. - const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true }); - const clone = this.createClone(); - shadow.appendChild(clone); - this.updateThemeCss(); - - const filterInput = domutils.getShadowId(this, ID_FILTER); - filterInput.addEventListener('input', (ev: Event) => { - this._updateEntries(); - }); - - filterInput.addEventListener('keydown', (ev: KeyboardEvent) => { this.handleKeyDown(ev); }); - - const resultsDiv = domutils.getShadowId(this, ID_RESULTS); - resultsDiv.addEventListener('click', (ev: Event) => { - for (let node of ev.path) { - if (node instanceof HTMLElement) { - const dataId = node.attributes.getNamedItem(ATTR_DATA_ID); - if (dataId !== undefined && dataId !== null) { - this._executeId(dataId.value); - } - } - } - }); - - const containerDiv = domutils.getShadowId(this, ID_CONTAINER); - containerDiv.addEventListener('contextmenu', (ev) => { - this._executeId(null); - }); - - const coverDiv = domutils.getShadowId(this, ID_COVER); - coverDiv.addEventListener('mousedown', (ev) => { - this._executeId(null); - }); - } - - /** - * - */ - private createClone() { - let template = window.document.getElementById(ID); - if (template === null) { - template = window.document.createElement('template'); - template.id = ID; - template.innerHTML = ` -
-
-
-
-
-
-
-
`; - window.document.body.appendChild(template); - } - - return window.document.importNode(template.content, true); - } - - protected _themeCssFiles(): ThemeTypes.CssFile[] { - return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ThemeTypes.CssFile.GUI_COMMANDPALETTE]; - } - - //----------------------------------------------------------------------- - - private _updateTitle(): void { - const titlePrimaryDiv = domutils.getShadowId(this, ID_TITLE_PRIMARY); - const titleSecondaryDiv = domutils.getShadowId(this, ID_TITLE_SECONDARY); - - titlePrimaryDiv.innerText = this._titlePrimary; - titleSecondaryDiv.innerText = this._titleSecondary; - } - - private _updateEntries(): void { - const filterInputValue = ( domutils.getShadowId(this, ID_FILTER)).value; - const filteredEntries = filterEntries(this._commandEntries, filterInputValue); - - if (filteredEntries.length === 0) { - this._selectedId = null; - } else { - const newSelectedIndex = filteredEntries.findIndex( (entry) => entry.id === this._selectedId); - this._selectedId = filteredEntries[Math.max(0, newSelectedIndex)].id; - } - - const html = (filterInputValue.trim() === "" ? formatEntriesWithGroups : formatEntries)(filteredEntries, this._selectedId); - domutils.getShadowId(this, ID_RESULTS).innerHTML = html; - } - - /** - * - */ - private handleKeyDown(ev: KeyboardEvent) { - // Escape. - if (ev.keyIdentifier === "U+001B") { - this._executeId(null); - ev.preventDefault(); - ev.stopPropagation(); - return; - } - - const isPageKey = ev.keyIdentifier === "PageUp" || ev.keyIdentifier === "PageDown"; - const isUp = ev.keyIdentifier === "PageUp" || ev.keyIdentifier === "Up" || ev.keyIdentifier === "Home"; - - if (isPageKey || isUp || ev.keyIdentifier === "Down" || ev.keyIdentifier === "End" || ev.keyIdentifier === "Enter") { - ev.preventDefault(); - ev.stopPropagation(); - - const filterInput = domutils.getShadowId(this, ID_FILTER); - const filteredEntries = filterEntries(this._commandEntries, filterInput.value); - if (filteredEntries.length === 0) { - return; - } - - const selectedIndex = filteredEntries.findIndex( (entry) => entry.id === this._selectedId); - - if (ev.keyIdentifier === "Enter") { - // Enter - if (this._selectedId !== null) { - this._executeId(this._selectedId); - } - } else { - - const resultsDiv = domutils.getShadowId(this, ID_RESULTS); - - // Determine the step size. - let stepSize = 1; - if (isPageKey) { - const selectedElement = resultsDiv.querySelector("."+CLASS_RESULT_SELECTED); - const selectedElementDimensions = selectedElement.getBoundingClientRect(); - - stepSize = Math.floor(resultsDiv.clientHeight / selectedElementDimensions.height); - } - - if (isUp) { - if (ev.keyIdentifier === "Home") { - this._selectedId = filteredEntries[0].id; - } else { - this._selectedId = filteredEntries[Math.max(0, selectedIndex-stepSize)].id; - } - } else { - if (ev.keyIdentifier === "End") { - this._selectedId = filteredEntries[filteredEntries.length-1].id; - } else { - this._selectedId = filteredEntries[Math.min(filteredEntries.length-1, selectedIndex+stepSize)].id; - } - } - - const top = resultsDiv.scrollTop; - this._updateEntries(); - resultsDiv.scrollTop = top; - - const selectedElement = resultsDiv.querySelector("."+CLASS_RESULT_SELECTED); - const selectedRelativeTop = selectedElement.offsetTop - resultsDiv.offsetTop; - if (top > selectedRelativeTop) { - resultsDiv.scrollTop = selectedRelativeTop; - } else { - const selectedElementDimensions = selectedElement.getBoundingClientRect(); - if (selectedRelativeTop + selectedElementDimensions.height > top + resultsDiv.clientHeight) { - resultsDiv.scrollTop = selectedRelativeTop + selectedElementDimensions.height - resultsDiv.clientHeight; - } - } - } - } - } - - /** - * - */ - open(x: number, y: number, width: number, height: number): void { - // Nuke any style like 'display: none' which can be use to prevent flicker. - this.setAttribute('style', ''); - - const container = domutils.getShadowId(this, ID_CONTEXT_COVER); - container.classList.remove(CLASS_CONTEXT_COVER_CLOSED); - container.classList.add(CLASS_CONTEXT_COVER_OPEN); - - container.style.left = `${x}px`; - container.style.top = `${y}px`; - container.style.width = `${width}px`; - container.style.height = `${height}px`; - - const cover = domutils.getShadowId(this, ID_COVER); - cover.classList.remove(CLASS_COVER_CLOSED); - cover.classList.add(CLASS_COVER_OPEN); - - const resultsDiv = domutils.getShadowId(this, ID_RESULTS); - resultsDiv.style.maxHeight = `${height/2}px`; - - const filterInput = domutils.getShadowId(this, ID_FILTER); - filterInput.value = ""; - this._updateEntries(); - filterInput.focus(); - } - - /** - * - */ - close(): void { - const cover = domutils.getShadowId(this, ID_COVER); - cover.classList.remove(CLASS_COVER_OPEN); - cover.classList.add(CLASS_COVER_CLOSED); - - const container = domutils.getShadowId(this, ID_CONTEXT_COVER); - container.classList.remove(CLASS_CONTEXT_COVER_OPEN); - container.classList.add(CLASS_CONTEXT_COVER_CLOSED); - } - - private _executeId(dataId: string): void { - if (this._laterHandle === null) { - this._laterHandle = domutils.doLater( () => { - this._laterHandle = null; - const event = new CustomEvent('selected', { detail: {entryId: dataId } }); - this.dispatchEvent(event); - }); - } - } - -} - -function filterEntries(entries: CommandPaletteTypes.CommandEntry[], filter: string): CommandPaletteTypes.CommandEntry[] { - const lowerFilter = filter.toLowerCase(); - return entries.filter( (entry) => entry.label.toLowerCase().includes(lowerFilter) ); -} - -function formatEntries(entries: CommandPaletteTypes.CommandEntry[], selectedId: string): string { - return entries.map( (entry) => formatEntry(entry, entry.id === selectedId) ).join(""); -} - -function formatEntriesWithGroups(entries: CommandPaletteTypes.CommandEntry[], selectedId: string): string { - let currentGroup: string = null; - const htmlParts: string[] = []; - - for (let entry of entries) { - let extraClass = ""; - if (entry.group !== currentGroup && currentGroup !== null) { - extraClass = CLASS_RESULT_GROUP_HEAD; - } - currentGroup = entry.group; - htmlParts.push(formatEntry(entry, entry.id === selectedId, extraClass)); - } - - return htmlParts.join(""); -} - -function formatEntry(entry: CommandPaletteTypes.CommandEntry, selected: boolean, extraClassString = ""): string { - return `
-
${formatIcon(entry.iconLeft)}
-
${formatIcon(entry.iconRight)}
-
${he.encode(entry.label)}
-
${entry.shortcut !== undefined && entry.shortcut !== null ? he.encode(entry.shortcut) : ""}
-
`; -} - -function formatIcon(iconName?: string): string { - return ``; -} - -export = CbCommandPalette; diff --git a/tsconfig.json b/tsconfig.json index 112389bbe..84194fb4e 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -88,7 +88,6 @@ "./src/gui/PopDownDialog.ts", "./src/gui/PopDownListPicker.ts", "./src/gui/checkboxmenuitem.ts", - "./src/gui/commandpalette.ts", "./src/gui/commandpalettetypes.ts", "./src/gui/contextmenu.ts", "./src/gui/dropdown.ts", From 791f430940b33e2ba900784ac0110c5c53523613 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Mon, 13 Feb 2017 21:26:27 +0100 Subject: [PATCH 13/14] Added PopDownNumberDialog component. It is now possible to set the tab width in the text viewer. --- .gitignore | 2 + src/gui/PopDownNumberDialog.ts | 226 +++++++++++++++++++++ src/plugins/TextViewer/TextViewerPlugin.ts | 30 +++ src/viewers/textviewer.ts | 20 +- tsconfig.json | 1 + 5 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/gui/PopDownNumberDialog.ts diff --git a/.gitignore b/.gitignore index 78a6976c1..8de3040aa 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ src/InternalExtratermApi.js src/codemirroroperation.js src/gui/PopDownDialog.js src/gui/PopDownListPicker.js +src/gui/PopDownNumberDialog.js +src/plugins/TextViewer/TextViewerPlugin.js diff --git a/src/gui/PopDownNumberDialog.ts b/src/gui/PopDownNumberDialog.ts new file mode 100644 index 000000000..4bba4d70a --- /dev/null +++ b/src/gui/PopDownNumberDialog.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2017 Simon Edwards + * + * This source code is licensed under the MIT license which is detailed in the LICENSE.txt file. + */ + +import ThemeableElementBase = require('../themeableelementbase'); +import ThemeTypes = require('../theme'); +import domutils = require('../domutils'); +import PopDownDialog = require('./PopDownDialog'); + +const ID = "CbPopDownNumberDialogTemplate"; +const ID_DIALOG = "ID_DIALOG"; +const ID_INPUT = "ID_INPUT"; + +let registered = false; + +/** + * A Pop Down Number Dialog + */ +class PopDownNumberDialog extends ThemeableElementBase { + + /** + * The HTML tag name of this element. + */ + static TAG_NAME = "CB-POPDOWNNUMBERDIALOG"; + + /** + * Initialize the PopDownNumberDialog class and resources. + * + * When PopDownNumberPicker is imported into a render process, this static method + * must be called before an instances may be created. This is can be safely + * called multiple times. + */ + static init(): void { + PopDownDialog.init(); + if (registered === false) { + window.document.registerElement(PopDownNumberDialog.TAG_NAME, {prototype: PopDownNumberDialog.prototype}); + registered = true; + } + } + + // WARNING: Fields like this will not be initialised automatically. + private _titlePrimary: string; + + private _titleSecondary: string; + + private _laterHandle: domutils.LaterHandle; + + private _extraCssFiles: ThemeTypes.CssFile[]; + + private _initProperties(): void { + this._titlePrimary = ""; + this._titleSecondary = ""; + this._laterHandle = null; + this._extraCssFiles = []; + } + + getValue(): number { + const textInput = domutils.getShadowId(this, ID_INPUT); + return textInput.valueAsNumber; + } + + setValue(value: number): void { + const textInput = domutils.getShadowId(this, ID_INPUT); + textInput.valueAsNumber = value; + } + + setMinimum(min: number): void { + const textInput = domutils.getShadowId(this, ID_INPUT); + textInput.setAttribute("min", "" + min); + } + + setMaximum(max: number): void { + const textInput = domutils.getShadowId(this, ID_INPUT); + textInput.setAttribute("max", "" + max); + } + + setTitlePrimary(text: string): void { + this._titlePrimary = text; + const dialog = domutils.getShadowId(this, ID_DIALOG); + if (dialog != null) { + dialog.setTitlePrimary(text); + } + } + + getTitlePrimary(): string { + return this._titlePrimary; + } + + setTitleSecondary(text: string): void { + this._titleSecondary = text; + const dialog = domutils.getShadowId(this, ID_DIALOG); + if (dialog != null) { + dialog.setTitleSecondary(text); + } + } + + getTitleSecondary(): string { + return this._titleSecondary; + } + + /** + * Specify extra Css files to load into this element. + * + * @param extraCssFiles extra Css files which should be loaded along side the default set. + */ + addExtraCss(extraCssFiles: ThemeTypes.CssFile[]): void { + this._extraCssFiles = [...this._extraCssFiles, ...extraCssFiles]; + this.updateThemeCss(); + } + + //----------------------------------------------------------------------- + // + // # + // # # ###### ###### #### # # #### # ###### + // # # # # # # # # # # # # + // # # ##### ##### # # # # ##### + // # # # # # # # # # + // # # # # # # # # # # # + // ####### # # ###### #### # #### ###### ###### + // + //----------------------------------------------------------------------- + /** + * Custom Element 'created' life cycle hook. + */ + createdCallback() { + this._initProperties(); // Initialise our properties. The constructor was not called. + const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true }); + const clone = this.createClone(); + shadow.appendChild(clone); + this.updateThemeCss(); + + const dialog = domutils.getShadowId(this, ID_DIALOG); + dialog.setTitlePrimary(this._titlePrimary); + dialog.setTitleSecondary(this._titleSecondary); + dialog.addEventListener(PopDownDialog.EVENT_CLOSE_REQUEST, () => { + dialog.close(); + }); + + const textInput = domutils.getShadowId(this, ID_INPUT); + textInput.addEventListener('keydown', (ev: KeyboardEvent) => { this.handleKeyDown(ev); }); + } + + /** + * + */ + private createClone(): Node { + let template = window.document.getElementById(ID); + if (template === null) { + template = window.document.createElement('template'); + template.id = ID; + template.innerHTML = ` + <${PopDownDialog.TAG_NAME} id="${ID_DIALOG}"> +
+ + `; + window.document.body.appendChild(template); + } + + return window.document.importNode(template.content, true); + } + + protected _themeCssFiles(): ThemeTypes.CssFile[] { + return [ThemeTypes.CssFile.GUI_CONTROLS, ThemeTypes.CssFile.FONT_AWESOME, ...this._extraCssFiles]; + } + + //----------------------------------------------------------------------- + /** + * + */ + private handleKeyDown(ev: KeyboardEvent) { + // Escape. + if (ev.keyIdentifier === "U+001B") { + this._okId(null); + ev.preventDefault(); + ev.stopPropagation(); + return; + } + + + if (ev.keyIdentifier === "Enter") { + ev.preventDefault(); + ev.stopPropagation(); + + const filterInput = domutils.getShadowId(this, ID_INPUT); + + if (ev.keyIdentifier === "Enter") { + // Enter + this._okId(this.getValue()); + } + } + } + + /** + * + */ + open(x: number, y: number, width: number, height: number): void { + const textInput = domutils.getShadowId(this, ID_INPUT); + textInput.focus(); + + const dialog = domutils.getShadowId(this, ID_DIALOG); + dialog.open(x, y, width, height); + } + + /** + * + */ + close(): void { + const dialog = domutils.getShadowId(this, ID_DIALOG); + dialog.close(); + } + + private _okId(value: number): void { + if (this._laterHandle === null) { + this._laterHandle = domutils.doLater( () => { + this.close(); + this._laterHandle = null; + const event = new CustomEvent("selected", { detail: {value: value } }); + this.dispatchEvent(event); + }); + } + } +} + +export = PopDownNumberDialog; diff --git a/src/plugins/TextViewer/TextViewerPlugin.ts b/src/plugins/TextViewer/TextViewerPlugin.ts index b8bac123c..255c23c7c 100644 --- a/src/plugins/TextViewer/TextViewerPlugin.ts +++ b/src/plugins/TextViewer/TextViewerPlugin.ts @@ -7,6 +7,7 @@ import * as PluginApi from '../../PluginApi'; import Logger = require('../../logger'); import TextViewer = require('../../viewers/textviewer'); import PopDownListPicker = require('../../gui/PopDownListPicker'); +import PopDownNumberDialog = require('../../gui/PopDownNumberDialog'); import CodeMirror = require('codemirror'); import he = require('he'); @@ -21,6 +22,8 @@ class TextViewerPlugin implements PluginApi.ExtratermPlugin { private _syntaxDialog: PopDownListPicker = null; + private _tabSizeDialog: PopDownNumberDialog = null; + constructor(api: PluginApi.ExtratermApi) { this._log = new Logger("TextViewerPlugin", this); // api.addNewTopLevelEventListener( (el: HTMLElement): void => { @@ -30,6 +33,9 @@ class TextViewerPlugin implements PluginApi.ExtratermPlugin { api.addNewTabEventListener( (el: HTMLElement): void => { el.addEventListener("TEXTVIEWER_EVENT_COMMAND_SYNTAX_HIGHLIGHTING", this._handleSyntaxHighlighting.bind(this)); }); + api.addNewTabEventListener( (el: HTMLElement): void => { + el.addEventListener("TEXTVIEWER_EVENT_COMMAND_TAB_WIDTH", this._handleTabSize.bind(this)); + }); } private _handleSyntaxHighlighting(ev: CustomEvent): void { @@ -101,10 +107,34 @@ class TextViewerPlugin implements PluginApi.ExtratermPlugin { this._syntaxDialog.focus(); } + private _handleTabSize(ev: CustomEvent): void { + const srcElement = ev.detail.srcElement; + if (this._tabSizeDialog == null) { + this._tabSizeDialog = window.document.createElement(PopDownNumberDialog.TAG_NAME); + this._tabSizeDialog.setTitlePrimary("Tab Size"); + this._tabSizeDialog.setMinimum(0); + this._tabSizeDialog.setMaximum(32); + + this._tabSizeDialog.addEventListener("selected", (ev: CustomEvent): void => { + if (ev.detail.value != null) { + srcElement.setTabSize(ev.detail.value); + } + srcElement.focus(); + }); + window.document.body.appendChild(this._tabSizeDialog); + } + + this._tabSizeDialog.setValue(srcElement.getTabSize()); + + const rect = ( ev.target).getBoundingClientRect(); + this._tabSizeDialog.open(rect.left, rect.top, rect.width, rect.height); + this._tabSizeDialog.focus(); + } } function factory(api: PluginApi.ExtratermApi): PluginApi.ExtratermPlugin { PopDownListPicker.init(); + PopDownNumberDialog.init(); return new TextViewerPlugin(api); } export = factory; diff --git a/src/viewers/textviewer.ts b/src/viewers/textviewer.ts index a3a043f89..4d8ea569d 100644 --- a/src/viewers/textviewer.ts +++ b/src/viewers/textviewer.ts @@ -53,6 +53,9 @@ const COMMAND_OPEN_COMMAND_PALETTE = CommandPaletteRequestTypes.COMMAND_OPEN_COM const COMMAND_SYNTAX_HIGHLIGHTING = "syntaxHighlighting"; const EVENT_COMMAND_SYNTAX_HIGHLIGHTING = "TEXTVIEWER_EVENT_COMMAND_SYNTAX_HIGHLIGHTING"; +const COMMAND_TAB_SIZE = "tabSize"; +const EVENT_COMMAND_TAB_SIZE = "TEXTVIEWER_EVENT_COMMAND_TAB_WIDTH"; + const COMMANDS = [ COMMAND_TYPE_AND_CR_SELECTION, COMMAND_TYPE_SELECTION, @@ -273,6 +276,14 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C return this._mimeType; } + getTabSize(): number { + return parseInt(this._codeMirror.getOption("tabSize"), 10); + } + + setTabSize(size: number): void { + this._codeMirror.setOption("tabSize", size); + } + setBytes(buffer: Buffer, mimeType: string): void { let charset = "utf-8"; let cleanMimeType = mimeType; @@ -907,7 +918,8 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C let commandList: CommandPaletteRequestTypes.CommandEntry[] = [ { id: COMMAND_TYPE_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection", target: this }, { id: COMMAND_TYPE_AND_CR_SELECTION, group: PALETTE_GROUP, iconRight: "terminal", label: "Type Selection & Execute", target: this }, - { id: COMMAND_SYNTAX_HIGHLIGHTING, group: PALETTE_GROUP, iconRight: "", label: "Syntax: " + this._getMimeTypeName(), target: this } + { id: COMMAND_SYNTAX_HIGHLIGHTING, group: PALETTE_GROUP, iconRight: "", label: "Syntax: " + this._getMimeTypeName(), target: this }, + { id: COMMAND_TAB_SIZE, group: PALETTE_GROUP, iconRight: "", label: "Tab Size: " + this.getTabSize(), target: this } ]; if (this._mode ===ViewerElementTypes.Mode.CURSOR) { @@ -975,7 +987,11 @@ class EtTextViewer extends ViewerElement implements CommandPaletteRequestTypes.C case COMMAND_SYNTAX_HIGHLIGHTING: this.dispatchEvent(new CustomEvent(EVENT_COMMAND_SYNTAX_HIGHLIGHTING, {bubbles: true, composed: true, detail: { srcElement: this } } )); break; - + + case COMMAND_TAB_SIZE: + this.dispatchEvent(new CustomEvent(EVENT_COMMAND_TAB_SIZE, {bubbles: true, composed: true, detail: { srcElement: this } } )); + break; + default: if (this._mode === ViewerElementTypes.Mode.CURSOR && CodeMirrorCommands.isCommand(command)) { CodeMirrorCommands.executeCommand(this._codeMirror, command); diff --git a/tsconfig.json b/tsconfig.json index 84194fb4e..63003071c 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -87,6 +87,7 @@ "./src/windowmessages.ts", "./src/gui/PopDownDialog.ts", "./src/gui/PopDownListPicker.ts", + "./src/gui/PopDownNumberDialog.ts", "./src/gui/checkboxmenuitem.ts", "./src/gui/commandpalettetypes.ts", "./src/gui/contextmenu.ts", From c50f6166faeaf63bcf0e0a9be9c568c2ca61f967 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Mon, 13 Feb 2017 21:30:38 +0100 Subject: [PATCH 14/14] Some clean ups. --- src/gui/PopDownListPicker.ts | 4 +- .../TerminalViewerExtra.js | 18 ----- src/plugins/TextViewer/TextViewerPlugin.js | 79 ------------------- 3 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 src/plugins/TerminalViewerExtra/TerminalViewerExtra.js delete mode 100644 src/plugins/TextViewer/TextViewerPlugin.js diff --git a/src/gui/PopDownListPicker.ts b/src/gui/PopDownListPicker.ts index 0fdc9fdcd..2b7754daa 100644 --- a/src/gui/PopDownListPicker.ts +++ b/src/gui/PopDownListPicker.ts @@ -26,8 +26,6 @@ class PopDownListPicker extends ThemeableElementBase */ static TAG_NAME = "CB-POPDOWNLISTPICKER"; - static EVENT_CLOSE_REQUEST = "CB-POPDOWNLISTPICKER-CLOSE_REQUEST"; - static ATTR_DATA_ID = "data-id"; static CLASS_RESULT_SELECTED = "CLASS_RESULT_SELECTED"; @@ -78,7 +76,7 @@ class PopDownListPicker extends ThemeableElementBase this._extraCssFiles = []; } - geSelected(): string { + getSelected(): string { return this._selectedId; } diff --git a/src/plugins/TerminalViewerExtra/TerminalViewerExtra.js b/src/plugins/TerminalViewerExtra/TerminalViewerExtra.js deleted file mode 100644 index d2fbcaa08..000000000 --- a/src/plugins/TerminalViewerExtra/TerminalViewerExtra.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -const Logger = require("../../logger"); -class TerminalViewerExtra { - constructor(api) { - this._log = new Logger("TerminalViewerExtra", this); - api.addNewTopLevelEventListener((el) => { - this._log.debug("Saw a new top level"); - }); - api.addNewTabEventListener((el) => { - this._log.debug("Saw a new tab level"); - }); - } -} -function factory(api) { - return new TerminalViewerExtra(api); -} -module.exports = factory; -//# sourceMappingURL=TerminalViewerExtra.js.map \ No newline at end of file diff --git a/src/plugins/TextViewer/TextViewerPlugin.js b/src/plugins/TextViewer/TextViewerPlugin.js deleted file mode 100644 index 947d9a888..000000000 --- a/src/plugins/TextViewer/TextViewerPlugin.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -const Logger = require("../../logger"); -const PopDownListPicker = require("../../gui/PopDownListPicker"); -const CodeMirror = require("codemirror"); -const he = require("he"); -class TextViewerPlugin { - constructor(api) { - this._syntaxDialog = null; - this._log = new Logger("TextViewerPlugin", this); - // api.addNewTopLevelEventListener( (el: HTMLElement): void => { - // this._log.debug("Saw a new top level"); - // }); - api.addNewTabEventListener((el) => { - el.addEventListener("TEXTVIEWER_EVENT_COMMAND_SYNTAX_HIGHLIGHTING", this._handleSyntaxHighlighting.bind(this)); - }); - } - _handleSyntaxHighlighting(ev) { - const srcElement = ev.detail.srcElement; - if (this._syntaxDialog == null) { - this._syntaxDialog = window.document.createElement(PopDownListPicker.TAG_NAME); - this._syntaxDialog.setTitlePrimary("Syntax Highlighting"); - this._syntaxDialog.setFormatEntriesFunc((filteredEntries, selectedId, filterInputValue) => { - return filteredEntries.map((entry) => { - return `
- ${he.encode(entry.name)} ${he.encode(entry.id)} -
`; - }).join(""); - }); - this._syntaxDialog.setFilterAndRankEntriesFunc((entries, filterText) => { - const lowerFilterText = filterText.toLowerCase().trim(); - const filtered = entries.filter((entry) => { - return entry.name.toLowerCase().indexOf(lowerFilterText) !== -1 || entry.id.toLowerCase().indexOf(lowerFilterText) !== -1; - }); - const rankFunc = (entry, lowerFilterText) => { - const lowerName = entry.name.toLowerCase(); - if (lowerName === lowerFilterText) { - return 1000; - } - const lowerId = entry.id.toLowerCase(); - if (lowerId === lowerFilterText) { - return 800; - } - const pos = lowerName.indexOf(lowerFilterText); - if (pos !== -1) { - return 500 - pos; // Bias it for matches at the front of the text. - } - const pos2 = lowerId.indexOf(lowerFilterText); - if (pos2 !== -1) { - return 400 - pos2; - } - return 0; - }; - filtered.sort((a, b) => rankFunc(b, lowerFilterText) - rankFunc(a, lowerFilterText)); - return filtered; - }); - this._syntaxDialog.addEventListener("selected", (ev) => { - if (ev.detail.selected != null) { - srcElement.mimeType = ev.detail.selected; - } - srcElement.focus(); - }); - window.document.body.appendChild(this._syntaxDialog); - } - const mimeList = CodeMirror.modeInfo.map((info) => { - return { id: info.mime, name: info.name }; - }); - this._syntaxDialog.setEntries(mimeList); - this._syntaxDialog.setSelected(srcElement.mimeType); - const rect = ev.target.getBoundingClientRect(); - this._syntaxDialog.open(rect.left, rect.top, rect.width, rect.height); - this._syntaxDialog.focus(); - } -} -function factory(api) { - PopDownListPicker.init(); - return new TextViewerPlugin(api); -} -module.exports = factory; -//# sourceMappingURL=TextViewerPlugin.js.map \ No newline at end of file