/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import * as platform from 'vs/base/common/platform'; import * as touch from 'vs/base/browser/touch'; import * as errors from 'vs/base/common/errors'; import * as dom from 'vs/base/browser/dom'; import * as mouse from 'vs/base/browser/mouseEvent'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as _ from 'vs/base/parts/tree/browser/tree'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { KeyCode, KeyMod, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes'; export interface IKeyBindingCallback { (tree: _.ITree, event: IKeyboardEvent): void; } export interface ICancelableEvent { preventDefault(): void; stopPropagation(): void; } export const enum ClickBehavior { /** * Handle the click when the mouse button is pressed but not released yet. */ ON_MOUSE_DOWN, /** * Handle the click when the mouse button is released. */ ON_MOUSE_UP } export const enum OpenMode { SINGLE_CLICK, DOUBLE_CLICK } export interface IControllerOptions { clickBehavior?: ClickBehavior; openMode?: OpenMode; keyboardSupport?: boolean; } interface IKeybindingDispatcherItem { keybinding: Keybinding | null; callback: IKeyBindingCallback; } export class KeybindingDispatcher { private _arr: IKeybindingDispatcherItem[]; constructor() { this._arr = []; } public has(keybinding: KeyCode): boolean { let target = createKeybinding(keybinding, platform.OS); if (target !== null) { for (const a of this._arr) { if (target.equals(a.keybinding)) { return true; } } } return false; } public set(keybinding: number, callback: IKeyBindingCallback) { this._arr.push({ keybinding: createKeybinding(keybinding, platform.OS), callback: callback }); } public dispatch(keybinding: SimpleKeybinding): IKeyBindingCallback | null { // Loop from the last to the first to handle overwrites for (let i = this._arr.length - 1; i >= 0; i--) { let item = this._arr[i]; if (keybinding.toChord().equals(item.keybinding)) { return item.callback; } } return null; } } export class DefaultController implements _.IController { protected downKeyBindingDispatcher: KeybindingDispatcher; protected upKeyBindingDispatcher: KeybindingDispatcher; private options: IControllerOptions; constructor(options: IControllerOptions = { clickBehavior: ClickBehavior.ON_MOUSE_DOWN, keyboardSupport: true, openMode: OpenMode.SINGLE_CLICK }) { this.options = options; this.downKeyBindingDispatcher = new KeybindingDispatcher(); this.upKeyBindingDispatcher = new KeybindingDispatcher(); if (typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport) { this.downKeyBindingDispatcher.set(KeyCode.UpArrow, (t, e) => this.onUp(t, e)); this.downKeyBindingDispatcher.set(KeyCode.DownArrow, (t, e) => this.onDown(t, e)); this.downKeyBindingDispatcher.set(KeyCode.LeftArrow, (t, e) => this.onLeft(t, e)); this.downKeyBindingDispatcher.set(KeyCode.RightArrow, (t, e) => this.onRight(t, e)); if (platform.isMacintosh) { this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.UpArrow, (t, e) => this.onLeft(t, e)); this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KEY_N, (t, e) => this.onDown(t, e)); this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KEY_P, (t, e) => this.onUp(t, e)); } this.downKeyBindingDispatcher.set(KeyCode.PageUp, (t, e) => this.onPageUp(t, e)); this.downKeyBindingDispatcher.set(KeyCode.PageDown, (t, e) => this.onPageDown(t, e)); this.downKeyBindingDispatcher.set(KeyCode.Home, (t, e) => this.onHome(t, e)); this.downKeyBindingDispatcher.set(KeyCode.End, (t, e) => this.onEnd(t, e)); this.downKeyBindingDispatcher.set(KeyCode.Space, (t, e) => this.onSpace(t, e)); this.downKeyBindingDispatcher.set(KeyCode.Escape, (t, e) => this.onEscape(t, e)); this.upKeyBindingDispatcher.set(KeyCode.Enter, this.onEnter.bind(this)); this.upKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this)); } } public onMouseDown(tree: _.ITree, element: any, event: mouse.IMouseEvent, origin: string = 'mouse'): boolean { if (this.options.clickBehavior === ClickBehavior.ON_MOUSE_DOWN && (event.leftButton || event.middleButton)) { if (event.target) { if (event.target.tagName && event.target.tagName.toLowerCase() === 'input') { return false; // Ignore event if target is a form input field (avoids browser specific issues) } if (dom.findParentWithClass(event.target, 'scrollbar', 'monaco-tree')) { return false; } if (dom.findParentWithClass(event.target, 'monaco-action-bar', 'row')) { // TODO@Joao not very nice way of checking for the action bar (implicit knowledge) return false; // Ignore event if target is over an action bar of the row } } // Propagate to onLeftClick now return this.onLeftClick(tree, element, event, origin); } return false; } public onClick(tree: _.ITree, element: any, event: mouse.IMouseEvent): boolean { const isMac = platform.isMacintosh; // A Ctrl click on the Mac is a context menu event if (isMac && event.ctrlKey) { event.preventDefault(); event.stopPropagation(); return false; } if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') { return false; // Ignore event if target is a form input field (avoids browser specific issues) } if (this.options.clickBehavior === ClickBehavior.ON_MOUSE_DOWN && (event.leftButton || event.middleButton)) { return false; // Already handled by onMouseDown } return this.onLeftClick(tree, element, event); } protected onLeftClick(tree: _.ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean { const event = eventish; const payload = { origin: origin, originalEvent: eventish, didClickOnTwistie: this.isClickOnTwistie(event) }; if (tree.getInput() === element) { tree.clearFocus(payload); tree.clearSelection(payload); } else { const isSingleMouseDown = eventish && event.browserEvent && event.browserEvent.type === 'mousedown' && event.browserEvent.detail === 1; if (!isSingleMouseDown) { eventish.preventDefault(); // we cannot preventDefault onMouseDown with single click because this would break DND otherwise } eventish.stopPropagation(); tree.domFocus(); tree.setSelection([element], payload); tree.setFocus(element, payload); if (this.shouldToggleExpansion(element, event, origin)) { if (tree.isExpanded(element)) { tree.collapse(element).then(undefined, errors.onUnexpectedError); } else { tree.expand(element).then(undefined, errors.onUnexpectedError); } } } return true; } protected shouldToggleExpansion(element: any, event: mouse.IMouseEvent, origin: string): boolean { const isDoubleClick = (origin === 'mouse' && event.detail === 2); return this.openOnSingleClick || isDoubleClick || this.isClickOnTwistie(event); } protected setOpenMode(openMode: OpenMode) { this.options.openMode = openMode; } protected get openOnSingleClick(): boolean { return this.options.openMode === OpenMode.SINGLE_CLICK; } protected isClickOnTwistie(event: mouse.IMouseEvent): boolean { let element = event.target as HTMLElement; if (!element.classList.contains('content')) { return false; } const twistieStyle = window.getComputedStyle(element, ':before'); if (twistieStyle.backgroundImage === 'none' || twistieStyle.display === 'none') { return false; } const twistieWidth = parseInt(twistieStyle.width!) + parseInt(twistieStyle.paddingRight!); return event.browserEvent.offsetX <= twistieWidth; } public onContextMenu(tree: _.ITree, element: any, event: _.ContextMenuEvent): boolean { if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') { return false; // allow context menu on input fields } // Prevent native context menu from showing up if (event) { event.preventDefault(); event.stopPropagation(); } return false; } public onTap(tree: _.ITree, element: any, event: touch.GestureEvent): boolean { const target = event.initialTarget; if (target && target.tagName && target.tagName.toLowerCase() === 'input') { return false; // Ignore event if target is a form input field (avoids browser specific issues) } return this.onLeftClick(tree, element, event, 'touch'); } public onKeyDown(tree: _.ITree, event: IKeyboardEvent): boolean { return this.onKey(this.downKeyBindingDispatcher, tree, event); } public onKeyUp(tree: _.ITree, event: IKeyboardEvent): boolean { return this.onKey(this.upKeyBindingDispatcher, tree, event); } private onKey(bindings: KeybindingDispatcher, tree: _.ITree, event: IKeyboardEvent): boolean { const handler: any = bindings.dispatch(event.toKeybinding()); if (handler) { // TODO: TS 3.1 upgrade. Why are we checking against void? if (handler(tree, event)) { event.preventDefault(); event.stopPropagation(); return true; } } return false; } protected onUp(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { tree.focusPrevious(1, payload); tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError); } return true; } protected onPageUp(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { tree.focusPreviousPage(payload); tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError); } return true; } protected onDown(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { tree.focusNext(1, payload); tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError); } return true; } protected onPageDown(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { tree.focusNextPage(payload); tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError); } return true; } protected onHome(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { tree.focusFirst(payload); tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError); } return true; } protected onEnd(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { tree.focusLast(payload); tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError); } return true; } protected onLeft(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { const focus = tree.getFocus(); tree.collapse(focus).then(didCollapse => { if (focus && !didCollapse) { tree.focusParent(payload); return tree.reveal(tree.getFocus()); } return undefined; }).then(undefined, errors.onUnexpectedError); } return true; } protected onRight(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); } else { const focus = tree.getFocus(); tree.expand(focus).then(didExpand => { if (focus && !didExpand) { tree.focusFirstChild(payload); return tree.reveal(tree.getFocus()); } return undefined; }).then(undefined, errors.onUnexpectedError); } return true; } protected onEnter(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { return false; } const focus = tree.getFocus(); if (focus) { tree.setSelection([focus], payload); } return true; } protected onSpace(tree: _.ITree, event: IKeyboardEvent): boolean { if (tree.getHighlight()) { return false; } const focus = tree.getFocus(); if (focus) { tree.toggleExpansion(focus); } return true; } protected onEscape(tree: _.ITree, event: IKeyboardEvent): boolean { const payload = { origin: 'keyboard', originalEvent: event }; if (tree.getHighlight()) { tree.clearHighlight(payload); return true; } if (tree.getSelection().length) { tree.clearSelection(payload); return true; } if (tree.getFocus()) { tree.clearFocus(payload); return true; } return false; } } export class DefaultDragAndDrop implements _.IDragAndDrop { public getDragURI(tree: _.ITree, element: any): string | null { return null; } public onDragStart(tree: _.ITree, data: IDragAndDropData, originalEvent: mouse.DragMouseEvent): void { return; } public onDragOver(tree: _.ITree, data: IDragAndDropData, targetElement: any, originalEvent: mouse.DragMouseEvent): _.IDragOverReaction | null { return null; } public drop(tree: _.ITree, data: IDragAndDropData, targetElement: any, originalEvent: mouse.DragMouseEvent): void { return; } // {{SQL CARBON EDIT}} public dropAbort(tree: _.ITree, data: IDragAndDropData): void { } } export class DefaultFilter implements _.IFilter { public isVisible(tree: _.ITree, element: any): boolean { return true; } } export class DefaultSorter implements _.ISorter { public compare(tree: _.ITree, element: any, otherElement: any): number { return 0; } } export class DefaultAccessibilityProvider implements _.IAccessibilityProvider { getAriaLabel(tree: _.ITree, element: any): string | null { return null; } } export class DefaultTreestyler implements _.ITreeStyler { constructor(private styleElement: HTMLStyleElement, private selectorSuffix?: string) { } style(styles: _.ITreeStyles): void { const suffix = this.selectorSuffix ? `.${this.selectorSuffix}` : ''; const content: string[] = []; if (styles.listFocusBackground) { content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused:not(.highlighted) { background-color: ${styles.listFocusBackground}; }`); } if (styles.listFocusForeground) { content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused:not(.highlighted) { color: ${styles.listFocusForeground}; }`); } if (styles.listActiveSelectionBackground) { content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { background-color: ${styles.listActiveSelectionBackground}; }`); } if (styles.listActiveSelectionForeground) { content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { color: ${styles.listActiveSelectionForeground}; }`); } if (styles.listFocusAndSelectionBackground) { content.push(` .monaco-tree-drag-image, .monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused.selected:not(.highlighted) { background-color: ${styles.listFocusAndSelectionBackground}; } `); } if (styles.listFocusAndSelectionForeground) { content.push(` .monaco-tree-drag-image, .monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused.selected:not(.highlighted) { color: ${styles.listFocusAndSelectionForeground}; } `); } if (styles.listInactiveSelectionBackground) { content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { background-color: ${styles.listInactiveSelectionBackground}; }`); } if (styles.listInactiveSelectionForeground) { content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { color: ${styles.listInactiveSelectionForeground}; }`); } if (styles.listHoverBackground) { content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); } if (styles.listHoverForeground) { content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); } if (styles.listDropBackground) { content.push(` .monaco-tree${suffix} .monaco-tree-wrapper.drop-target, .monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; } `); } if (styles.listFocusOutline) { content.push(` .monaco-tree-drag-image { border: 1px solid ${styles.listFocusOutline}; background: #000; } .monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row { border: 1px solid transparent; } .monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused:not(.highlighted) { border: 1px dotted ${styles.listFocusOutline}; } .monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { border: 1px solid ${styles.listFocusOutline}; } .monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { border: 1px solid ${styles.listFocusOutline}; } .monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) { border: 1px dashed ${styles.listFocusOutline}; } .monaco-tree${suffix} .monaco-tree-wrapper.drop-target, .monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.drop-target { border: 1px dashed ${styles.listFocusOutline}; } `); } const newStyles = content.join('\n'); if (newStyles !== this.styleElement.innerHTML) { this.styleElement.innerHTML = newStyles; } } } export class CollapseAllAction extends Action { constructor(private viewer: _.ITree, enabled: boolean) { super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'monaco-tree-action collapse-all', enabled); } public override run(context?: any): Promise { if (this.viewer.getHighlight()) { return Promise.resolve(); // Global action disabled if user is in edit mode from another action } this.viewer.collapseAll(); this.viewer.clearSelection(); this.viewer.clearFocus(); this.viewer.domFocus(); this.viewer.focusFirst(); return Promise.resolve(); } }