Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569)

This commit is contained in:
Cory Rivera
2021-08-25 16:28:29 -07:00
committed by GitHub
parent ab1112bfb3
commit cb7b7da0a4
1752 changed files with 59525 additions and 33878 deletions

View File

@@ -28,7 +28,7 @@ class WindowManager {
}
this._zoomLevel = zoomLevel;
// See https://github.com/Microsoft/vscode/issues/26151
// See https://github.com/microsoft/vscode/issues/26151
this._lastZoomLevelChangeTime = isTrusted ? 0 : Date.now();
this._onDidChangeZoomLevel.fire(this._zoomLevel);
}
@@ -115,7 +115,6 @@ export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0));
export const isEdgeLegacyWebView = (userAgent.indexOf('Edge/') >= 0) && (userAgent.indexOf('WebView/') >= 0);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isAndroid = (userAgent.indexOf('Android') >= 0);

View File

@@ -17,6 +17,7 @@ import { FileAccess, RemoteAuthorities } from 'vs/base/common/network';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
import { KeyCode } from 'vs/base/common/keyCodes';
import { withNullAsUndefined } from 'vs/base/common/types';
export function clearNode(node: HTMLElement): void {
while (node.firstChild) {
@@ -25,79 +26,12 @@ export function clearNode(node: HTMLElement): void {
}
/**
* @deprecated use `node.remove()` instead
* @deprecated Use node.isConnected directly
*/
export function removeNode(node: HTMLElement): void {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
export function trustedInnerHTML(node: Element, value: TrustedHTML): void {
// this is a workaround for innerHTML not allowing for "asymetric" accessors
// see https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393
// and https://github.com/microsoft/TypeScript/issues/30024
node.innerHTML = value as unknown as string;
}
export function isInDOM(node: Node | null): boolean {
return node?.isConnected ?? false;
}
interface IDomClassList {
hasClass(node: HTMLElement | SVGElement, className: string): boolean;
addClass(node: HTMLElement | SVGElement, className: string): void;
addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void;
removeClass(node: HTMLElement | SVGElement, className: string): void;
removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void;
toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void;
}
const _classList: IDomClassList = new class implements IDomClassList {
hasClass(node: HTMLElement, className: string): boolean {
return Boolean(className) && node.classList && node.classList.contains(className);
}
addClasses(node: HTMLElement, ...classNames: string[]): void {
classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.addClass(node, name)));
}
addClass(node: HTMLElement, className: string): void {
if (className && node.classList) {
node.classList.add(className);
}
}
removeClass(node: HTMLElement, className: string): void {
if (className && node.classList) {
node.classList.remove(className);
}
}
removeClasses(node: HTMLElement, ...classNames: string[]): void {
classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.removeClass(node, name)));
}
toggleClass(node: HTMLElement, className: string, shouldHaveIt?: boolean): void {
if (node.classList) {
node.classList.toggle(className, shouldHaveIt);
}
}
};
/** @deprecated ES6 - use classList*/
export function hasClass(node: HTMLElement | SVGElement, className: string): boolean { return _classList.hasClass(node, className); }
/** @deprecated ES6 - use classList*/
export function addClass(node: HTMLElement | SVGElement, className: string): void { return _classList.addClass(node, className); }
/** @deprecated ES6 - use classList*/
export function addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.addClasses(node, ...classNames); }
/** @deprecated ES6 - use classList*/
export function removeClass(node: HTMLElement | SVGElement, className: string): void { return _classList.removeClass(node, className); }
/** @deprecated ES6 - use classList*/
export function removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.removeClasses(node, ...classNames); }
/** @deprecated ES6 - use classList*/
export function toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void { return _classList.toggleClass(node, className, shouldHaveIt); }
class DomListener implements IDisposable {
private _handler: (e: any) => void;
@@ -414,16 +348,7 @@ export function getClientArea(element: HTMLElement): Dimension {
// If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight
if (platform.isIOS && window.visualViewport) {
const width = window.visualViewport.width;
const height = window.visualViewport.height - (
browser.isStandalone
// in PWA mode, the visual viewport always includes the safe-area-inset-bottom (which is for the home indicator)
// even when you are using the onscreen monitor, the visual viewport will include the area between system statusbar and the onscreen keyboard
// plus the area between onscreen keyboard and the bottom bezel, which is 20px on iOS.
? (20 + 4) // + 4px for body margin
: 0
);
return new Dimension(width, height);
return new Dimension(window.visualViewport.width, window.visualViewport.height);
}
// Try innerWidth / innerHeight
@@ -1254,28 +1179,44 @@ export function computeScreenAwareSize(cssPx: number): number {
}
/**
* Open safely a new window. This is the best way to do so, but you cannot tell
* if the window was opened or if it was blocked by the brower's popup blocker.
* If you want to tell if the browser blocked the new window, use `windowOpenNoOpenerWithSuccess`.
*
* See https://github.com/microsoft/monaco-editor/issues/601
* To protect against malicious code in the linked site, particularly phishing attempts,
* the window.opener should be set to null to prevent the linked site from having access
* to change the location of the current page.
* See https://mathiasbynens.github.io/rel-noopener/
*/
export function windowOpenNoOpener(url: string): boolean {
if (browser.isElectron || browser.isEdgeLegacyWebView) {
// In VSCode, window.open() always returns null...
// The same is true for a WebView (see https://github.com/microsoft/monaco-editor/issues/628)
// Also call directly window.open in sandboxed Electron (see https://github.com/microsoft/monaco-editor/issues/2220)
window.open(url);
export function windowOpenNoOpener(url: string): void {
// By using 'noopener' in the `windowFeatures` argument, the newly created window will
// not be able to use `window.opener` to reach back to the current page.
// See https://stackoverflow.com/a/46958731
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener
// However, this also doesn't allow us to realize if the browser blocked
// the creation of the window.
window.open(url, '_blank', 'noopener');
}
/**
* Open safely a new window. This technique is not appropiate in certain contexts,
* like for example when the JS context is executing inside a sandboxed iframe.
* If it is not necessary to know if the browser blocked the new window, use
* `windowOpenNoOpener`.
*
* See https://github.com/microsoft/monaco-editor/issues/601
* See https://github.com/microsoft/monaco-editor/issues/2474
* See https://mathiasbynens.github.io/rel-noopener/
*/
export function windowOpenNoOpenerWithSuccess(url: string): boolean {
const newTab = window.open();
if (newTab) {
(newTab as any).opener = null;
newTab.location.href = url;
return true;
} else {
let newTab = window.open();
if (newTab) {
(newTab as any).opener = null;
newTab.location.href = url;
return true;
}
return false;
}
return false;
}
export function animate(fn: () => void): IDisposable {
@@ -1333,6 +1274,29 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void
setTimeout(() => document.body.removeChild(anchor));
}
export function triggerUpload(): Promise<FileList | undefined> {
return new Promise<FileList | undefined>(resolve => {
// In order to upload to the browser, create a
// input element of type `file` and click it
// to gather the selected files
const input = document.createElement('input');
document.body.appendChild(input);
input.type = 'file';
input.multiple = true;
// Resolve once the input event has fired once
Event.once(Event.fromDOMEventEmitter(input, 'input'))(() => {
resolve(withNullAsUndefined(input.files));
});
input.click();
// Ensure to remove the element from DOM eventually
setTimeout(() => document.body.removeChild(input));
});
}
export enum DetectedFullscreenMode {
/**
@@ -1518,6 +1482,9 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
};
this._subscriptions.add(domEvent(window, 'keydown', true)(e => {
if (e.defaultPrevented) {
return;
}
const event = new StandardKeyboardEvent(e);
// If Alt-key keydown event is repeated, ignore it #112347
@@ -1552,6 +1519,10 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
}));
this._subscriptions.add(domEvent(window, 'keyup', true)(e => {
if (e.defaultPrevented) {
return;
}
if (!e.altKey && this._keyStatus.altKey) {
this._keyStatus.lastKeyReleased = 'alt';
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
@@ -1642,3 +1613,13 @@ export function getCookieValue(name: string): string | undefined {
return match ? match.pop() : undefined;
}
export function addMatchMediaChangeListener(query: string, callback: () => void): void {
const mediaQueryList = window.matchMedia(query);
if (typeof mediaQueryList.addEventListener === 'function') {
mediaQueryList.addEventListener('change', callback);
} else {
// Safari 13.x
mediaQueryList.addListener(callback);
}
}

View File

@@ -3,7 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { GestureEvent } from 'vs/base/browser/touch';
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
export type EventHandler = HTMLElement | HTMLDocument | Window;
@@ -12,6 +14,9 @@ export interface IDomEvent {
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<unknown>;
}
/**
* @deprecated Use `DomEmitter` instead
*/
export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => {
const fn = (e: Event) => emitter.fire(e);
const emitter = new Emitter<Event>({
@@ -26,6 +31,35 @@ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapt
return emitter.event;
};
export interface DOMEventMap extends HTMLElementEventMap {
'-monaco-gesturetap': GestureEvent;
'-monaco-gesturechange': GestureEvent;
'-monaco-gesturestart': GestureEvent;
'-monaco-gesturesend': GestureEvent;
'-monaco-gesturecontextmenu': GestureEvent;
}
export class DomEmitter<K extends keyof DOMEventMap> implements IDisposable {
private emitter: Emitter<DOMEventMap[K]>;
get event(): BaseEvent<DOMEventMap[K]> {
return this.emitter.event;
}
constructor(element: EventHandler, type: K, useCapture?: boolean) {
const fn = (e: Event) => this.emitter.fire(e as DOMEventMap[K]);
this.emitter = new Emitter({
onFirstListenerAdd: () => element.addEventListener(type, fn, useCapture),
onLastListenerRemove: () => element.removeEventListener(type, fn, useCapture)
});
}
dispose(): void {
this.emitter.dispose();
}
}
export interface CancellableEvent {
preventDefault(): void;
stopPropagation(): void;

View File

@@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom';
import { IframeUtils } from 'vs/base/browser/iframe';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { isIOS } from 'vs/base/common/platform';
export interface IStandardMouseMoveEventData {
leftButton: boolean;
@@ -88,7 +89,7 @@ export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements I
this._onStopCallback = onStopCallback;
const windowChain = IframeUtils.getSameOriginWindowChain();
const mouseMove = 'mousemove';
const mouseMove = isIOS ? 'pointermove' : 'mousemove'; // Safari sends wrong event, workaround for #122653
const mouseUp = 'mouseup';
const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document);

View File

@@ -9,4 +9,4 @@ export interface IHistoryNavigationWidget {
showNextValue(): void;
}
}

View File

@@ -43,18 +43,6 @@ function getParentWindowIfSameOrigin(w: Window): Window | null {
return w.parent;
}
function findIframeElementInParentWindow(parentWindow: Window, childWindow: Window): HTMLIFrameElement | null {
let parentWindowIframes = parentWindow.document.getElementsByTagName('iframe');
let iframe: HTMLIFrameElement;
for (let i = 0, len = parentWindowIframes.length; i < len; i++) {
iframe = parentWindowIframes[i];
if (iframe.contentWindow === childWindow) {
return iframe;
}
}
return null;
}
export class IframeUtils {
/**
@@ -72,7 +60,7 @@ export class IframeUtils {
if (parent) {
sameOriginWindowChainCache.push({
window: w,
iframeElement: findIframeElementInParentWindow(parent, w)
iframeElement: w.frameElement || null
});
} else {
sameOriginWindowChainCache.push({

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom'; // {{SQL CARBON EDIT}} added missing import to fix build break
import * as DOM from 'vs/base/browser/dom';
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
@@ -180,15 +180,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
});
});
// const promise = Promise.all([value, withInnerHTML]).then(values => {
// const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
// if (span) {
// DOM.reset(span, values[0]);
// }
// }).catch(_err => {
// // ignore
// });
if (options.asyncRenderCallback) {
promise.then(options.asyncRenderCallback);
}
@@ -324,6 +315,14 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti
};
}
/**
* Strips all markdown from `string`, if it's an IMarkdownString. For example
* `# Header` would be output as `Header`. If it's not, the string is returned.
*/
export function renderStringAsPlaintext(string: IMarkdownString | string) {
return typeof string === 'string' ? string : renderMarkdownAsPlaintext(string);
}
/**
* Strips all markdown from `markdown`. For example `# Header` would be output as `Header`.
*/
@@ -398,6 +397,7 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString) {
const unescapeInfo = new Map<string, string>([
['&quot;', '"'],
['&nbsp;', ' '],
['&amp;', '&'],
['&#39;', '\''],
['&lt;', '<'],

View File

@@ -75,6 +75,16 @@
margin-right: .8em;
}
.monaco-action-bar .action-item .action-label.separator {
width: 1px;
height: 16px;
margin: 5px 4px !important;
cursor: default;
min-width: 1px;
padding: 0;
background-color: #bbb;
}
.secondary-actions .monaco-action-bar .action-label {
margin-left: 6px;
}

View File

@@ -6,4 +6,4 @@
.monaco-aria-container {
position: absolute; /* try to hide from window but not from screen readers */
left:-999em;
}
}

View File

@@ -35,6 +35,7 @@
.monaco-button-dropdown {
display: flex;
cursor: pointer;
}
.monaco-button-dropdown > .monaco-dropdown-button {

View File

@@ -185,6 +185,7 @@ export class Button extends Disposable implements IButton {
this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground;
this.buttonBorder = styles.buttonBorder;
// {{SQL CARBON EDIT}}
this.buttonSecondaryBorder = styles.buttonSecondaryBorder;
this.buttonDisabledBackground = styles.buttonDisabledBackground;
this.buttonDisabledForeground = styles.buttonDisabledForeground;

View File

@@ -165,6 +165,7 @@ export class CenteredViewLayout implements IDisposable {
this.splitView = undefined;
this.emptyViews = undefined;
this.container.appendChild(this.view.element);
this.view.layout(this.width, this.height, 0, 0);
}
}

View File

@@ -87,7 +87,7 @@ export class Dialog extends Disposable {
private readonly inputs: InputBox[];
private readonly buttons: string[];
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
constructor(private container: HTMLElement, private message: string, buttons: string[] | undefined, private options: IDialogOptions) {
super();
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`));
@@ -96,21 +96,22 @@ export class Dialog extends Disposable {
this.element.setAttribute('role', 'dialog');
hide(this.element);
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
this.buttons = Array.isArray(buttons) && buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon'));
this.iconElement.setAttribute('aria-label', this.getIconAriaLabel());
this.messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
if (this.options.detail || this.options.renderBody) {
const messageElement = this.messageContainer.appendChild($('.dialog-message'));
const messageTextElement = messageElement.appendChild($('.dialog-message-text'));
const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text'));
messageTextElement.innerText = this.message;
}
this.messageDetailElement = this.messageContainer.appendChild($('.dialog-message-detail'));
this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail'));
if (this.options.detail || !this.options.renderBody) {
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
} else {
@@ -118,8 +119,12 @@ export class Dialog extends Disposable {
}
if (this.options.renderBody) {
const customBody = this.messageContainer.appendChild($('.dialog-message-body'));
const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body'));
this.options.renderBody(customBody);
for (const el of this.messageContainer.querySelectorAll('a')) {
el.tabIndex = 0;
}
}
if (this.options.inputs) {
@@ -157,7 +162,7 @@ export class Dialog extends Disposable {
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
}
private getAriaLabel(): string {
private getIconAriaLabel(): string {
let typeLabel = nls.localize('dialogInfoMessage', 'Info');
switch (this.options.type) {
case 'error':
@@ -176,7 +181,7 @@ export class Dialog extends Disposable {
break;
}
return `${typeLabel}: ${this.message} ${this.options.detail || ''}`;
return typeLabel;
}
updateMessage(message: string): void {
@@ -218,6 +223,10 @@ export class Dialog extends Disposable {
this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyMod.Alt)) {
evt.preventDefault();
}
if (evt.equals(KeyCode.Enter)) {
// Enter in input field should OK the dialog
@@ -246,6 +255,17 @@ export class Dialog extends Disposable {
// Build a list of focusable elements in their visual order
const focusableElements: { focus: () => void }[] = [];
let focusedIndex = -1;
if (this.messageContainer) {
const links = this.messageContainer.querySelectorAll('a');
for (const link of links) {
focusableElements.push(link);
if (link === document.activeElement) {
focusedIndex = focusableElements.length - 1;
}
}
}
for (const input of this.inputs) {
focusableElements.push(input);
if (input.hasFocus()) {
@@ -371,7 +391,7 @@ export class Dialog extends Disposable {
this.applyStyles();
this.element.setAttribute('aria-label', this.getAriaLabel());
this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body');
show(this.element);
// Focus first element (input or button)

View File

@@ -37,3 +37,10 @@
line-height: 16px;
margin-left: -4px;
}
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label > .action-label {
display: block;
background-size: 16px;
background-position: center center;
background-repeat: no-repeat;
}

View File

@@ -57,7 +57,7 @@ export class BaseDropdown extends ActionRunner {
for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {
this._register(addDisposableListener(this._label, event, e => {
if (e instanceof MouseEvent && e.detail > 1) {
return; // prevent multiple clicks to open multiple context menus (https://github.com/Microsoft/vscode/issues/41363)
return; // prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363)
}
if (this.visible) {
@@ -71,7 +71,7 @@ export class BaseDropdown extends ActionRunner {
this._register(addDisposableListener(this._label, EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
EventHelper.stop(e, true); // https://github.com/Microsoft/vscode/issues/57997
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
if (this.visible) {
this.hide();

View File

@@ -1,106 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
import { ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { IAction } from 'vs/base/common/actions';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem {
private _primaryAction: ActionViewItem;
private _dropdown: DropdownMenuActionViewItem;
private _container: HTMLElement | null = null;
private toDispose: IDisposable[];
constructor(
primaryAction: IAction,
dropdownAction: IAction,
dropdownMenuActions: IAction[],
_className: string,
private readonly _contextMenuProvider: IContextMenuProvider,
dropdownIcon?: string
) {
super(null, primaryAction);
this._primaryAction = new ActionViewItem(undefined, primaryAction, {
icon: true,
label: false
});
this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, {
menuAsChild: true
});
this.toDispose = [];
}
override render(container: HTMLElement): void {
this._container = container;
super.render(this._container);
this._container.classList.add('monaco-dropdown-with-primary');
const primaryContainer = DOM.$('.action-container');
this._primaryAction.render(DOM.append(this._container, primaryContainer));
const dropdownContainer = DOM.$('.dropdown-action-container');
this._dropdown.render(DOM.append(this._container, dropdownContainer));
this.toDispose.push(DOM.addDisposableListener(primaryContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.RightArrow)) {
this._primaryAction.element!.tabIndex = -1;
this._dropdown.focus();
event.stopPropagation();
}
}));
this.toDispose.push(DOM.addDisposableListener(dropdownContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.LeftArrow)) {
this._primaryAction.element!.tabIndex = 0;
this._dropdown.setFocusable(false);
this._primaryAction.element?.focus();
event.stopPropagation();
}
}));
}
override focus(fromRight?: boolean): void {
if (fromRight) {
this._dropdown.focus();
} else {
this._primaryAction.element!.tabIndex = 0;
this._primaryAction.element!.focus();
}
}
override blur(): void {
this._primaryAction.element!.tabIndex = -1;
this._dropdown.blur();
this._container!.blur();
}
override setFocusable(focusable: boolean): void {
if (focusable) {
this._primaryAction.element!.tabIndex = 0;
} else {
this._primaryAction.element!.tabIndex = -1;
this._dropdown.setFocusable(false);
}
}
override dispose(): void {
this.toDispose = dispose(this.toDispose);
}
update(dropdownAction: IAction, dropdownMenuActions: IAction[], dropdownIcon?: string): void {
this._dropdown?.dispose();
this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, {
menuAsChild: true,
classNames: ['codicon', dropdownIcon || 'codicon-chevron-down']
});
if (this.element) {
this._dropdown.render(this.element);
}
}
}

View File

@@ -62,4 +62,4 @@
0% { background: rgba(255, 255, 255, 0.44); }
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
99% { background: transparent; }
}
}

View File

@@ -128,7 +128,7 @@ export class FindInput extends Widget {
const flexibleMaxHeight = options.flexibleMaxHeight;
this.domNode = document.createElement('div');
dom.addClass(this.domNode, 'monaco-findInput');
this.domNode.classList.add('monaco-findInput');
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
placeholder: this.placeholder || '',
@@ -263,7 +263,7 @@ export class FindInput extends Widget {
}
public enable(): void {
dom.removeClass(this.domNode, 'disabled');
this.domNode.classList.remove('disabled');
this.inputBox.enable();
this.regex.enable();
this.wholeWords.enable();
@@ -271,7 +271,7 @@ export class FindInput extends Widget {
}
public disable(): void {
dom.addClass(this.domNode, 'disabled');
this.domNode.classList.add('disabled');
this.inputBox.disable();
this.regex.disable();
this.wholeWords.disable();
@@ -403,9 +403,9 @@ export class FindInput extends Widget {
private _lastHighlightFindOptions: number = 0;
public highlightFindOptions(): void {
dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
}
public validate(): void {

View File

@@ -136,7 +136,7 @@ export class ReplaceInput extends Widget {
const flexibleMaxHeight = options.flexibleMaxHeight;
this.domNode = document.createElement('div');
dom.addClass(this.domNode, 'monaco-findInput');
this.domNode.classList.add('monaco-findInput');
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
ariaLabel: this.label || '',
@@ -234,13 +234,13 @@ export class ReplaceInput extends Widget {
}
public enable(): void {
dom.removeClass(this.domNode, 'disabled');
this.domNode.classList.remove('disabled');
this.inputBox.enable();
this.preserveCase.enable();
}
public disable(): void {
dom.addClass(this.domNode, 'disabled');
this.domNode.classList.add('disabled');
this.inputBox.disable();
this.preserveCase.disable();
}
@@ -347,9 +347,9 @@ export class ReplaceInput extends Widget {
private _lastHighlightFindOptions: number = 0;
public highlightFindOptions(): void {
dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
}
public validate(): void {

View File

@@ -210,7 +210,9 @@ export class Grid<T extends IView = IView> extends Disposable {
get minimumHeight(): number { return this.gridview.minimumHeight; }
get maximumWidth(): number { return this.gridview.maximumWidth; }
get maximumHeight(): number { return this.gridview.maximumHeight; }
get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; }
readonly onDidChange: Event<{ width: number; height: number; } | undefined>;
readonly onDidScroll: Event<void>;
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
@@ -232,8 +234,8 @@ export class Grid<T extends IView = IView> extends Disposable {
} else {
this.gridview = new GridView(options);
}
this._register(this.gridview);
this._register(this.gridview);
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number'
@@ -243,6 +245,9 @@ export class Grid<T extends IView = IView> extends Disposable {
if (!(view instanceof GridView)) {
this._addView(view, size, [0]);
}
this.onDidChange = this.gridview.onDidChange;
this.onDidScroll = this.gridview.onDidScroll;
}
style(styles: IGridStyles): void {

View File

@@ -13,4 +13,4 @@
.monaco-grid-branch-node {
width: 100%;
height: 100%;
}
}

View File

@@ -9,9 +9,10 @@ import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { SplitView, IView as ISplitView, Sizing, LayoutPriority, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { $ } from 'vs/base/browser/dom';
import { tail2 as tail } from 'vs/base/common/arrays';
import { equals as arrayEquals, tail2 as tail } from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { clamp } from 'vs/base/common/numbers';
import { isUndefined } from 'vs/base/common/types';
export { Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
@@ -242,6 +243,10 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
private readonly _onDidChange = new Emitter<number | undefined>();
readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
private _onDidScroll = new Emitter<void>();
private onDidScrollDisposable: IDisposable = Disposable.None;
readonly onDidScroll: Event<void> = this._onDidScroll.event;
private childrenChangeDisposable: IDisposable = Disposable.None;
private readonly _onDidSashReset = new Emitter<number[]>();
@@ -343,11 +348,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);
this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);
const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);
this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
this.updateChildrenEvents();
}
style(styles: IGridViewStyles): void {
@@ -566,6 +567,11 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
}
private onDidChildrenChange(): void {
this.updateChildrenEvents();
this._onDidChange.fire(undefined);
}
private updateChildrenEvents(): void {
const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);
this.childrenChangeDisposable.dispose();
this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
@@ -574,7 +580,9 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
this.childrenSashResetDisposable.dispose();
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
this._onDidChange.fire(undefined);
const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll));
this.onDidScrollDisposable.dispose();
this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll);
}
trySet2x2(other: BranchNode): IDisposable {
@@ -646,6 +654,25 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
}
}
/**
* Creates a latched event that avoids being fired when the view
* constraints do not change at all.
*/
function createLatchedOnDidChangeViewEvent(view: IView): Event<IViewSize | undefined> {
const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split<undefined, IViewSize>(view.onDidChange, isUndefined);
return Event.any(
onDidSetViewSize,
Event.map(
Event.latch(
Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])),
arrayEquals
),
_ => undefined
)
);
}
class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
private _size: number = 0;
@@ -657,6 +684,7 @@ class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
private absoluteOffset: number = 0;
private absoluteOrthogonalOffset: number = 0;
readonly onDidScroll: Event<void> = Event.None;
readonly onDidSashReset: Event<number[]> = Event.None;
private _onDidLinkedWidthNodeChange = new Relay<number | undefined>();
@@ -691,7 +719,8 @@ class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
this._orthogonalSize = orthogonalSize;
this._size = size;
this._onDidViewChange = Event.map(this.view.onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height));
const onDidChange = createLatchedOnDidChangeViewEvent(view);
this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height));
this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);
}
@@ -778,7 +807,25 @@ class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
this._orthogonalSize = ctx.orthogonalSize;
this.absoluteOffset = ctx.absoluteOffset + offset;
this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
this.view.layout(this.width, this.height, this.top, this.left);
this._layout(this.width, this.height, this.top, this.left);
}
private cachedWidth: number = 0;
private cachedHeight: number = 0;
private cachedTop: number = 0;
private cachedLeft: number = 0;
private _layout(width: number, height: number, top: number, left: number): void {
if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) {
return;
}
this.cachedWidth = width;
this.cachedHeight = height;
this.cachedTop = top;
this.cachedLeft = left;
this.view.layout(width, height, top, left);
}
setVisible(visible: boolean): void {
@@ -852,6 +899,7 @@ export class GridView implements IDisposable {
this.element.appendChild(root.element);
this.onDidSashResetRelay.input = root.onDidSashReset;
this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO
this._onDidScroll.input = root.onDidScroll;
}
get orientation(): Orientation {
@@ -877,6 +925,9 @@ export class GridView implements IDisposable {
get maximumWidth(): number { return this.root.maximumHeight; }
get maximumHeight(): number { return this.root.maximumHeight; }
private _onDidScroll = new Relay<void>();
readonly onDidScroll = this._onDidScroll.event;
private _onDidChange = new Relay<IViewSize | undefined>();
readonly onDidChange = this._onDidChange.event;

View File

@@ -138,3 +138,8 @@
margin-bottom: 4px;
display: inline-block;
}
.monaco-hover-content .action-container a {
-webkit-user-select: none;
user-select: none;
}

View File

@@ -16,9 +16,11 @@ export interface IHoverDelegateOptions {
text: IMarkdownString | string;
target: IHoverDelegateTarget | HTMLElement;
hoverPosition?: HoverPosition;
showPointer?: boolean;
}
export interface IHoverDelegate {
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
delay: number;
placement?: 'mouse' | 'element';
}

View File

@@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
import { equals } from 'vs/base/common/objects';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { isFunction, isString } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
import { localize } from 'vs/nls';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { CancellationToken } from 'vs/base/common/cancellation';
import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
@@ -91,17 +88,17 @@ class FastLabelNode {
export class IconLabel extends Disposable {
private domNode: FastLabelNode;
private readonly domNode: FastLabelNode;
private nameNode: Label | LabelWithHighlights;
private readonly nameNode: Label | LabelWithHighlights;
private descriptionContainer: FastLabelNode;
private readonly descriptionContainer: FastLabelNode;
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
private readonly descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
private labelContainer: HTMLElement;
private readonly labelContainer: HTMLElement;
private hoverDelegate: IHoverDelegate | undefined = undefined;
private readonly hoverDelegate: IHoverDelegate | undefined;
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
@@ -114,7 +111,7 @@ export class IconLabel extends Disposable {
const nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container'));
this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
if (options?.supportHighlights) {
if (options?.supportHighlights || options?.supportIcons) {
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons);
} else {
this.nameNode = new Label(nameContainer);
@@ -126,9 +123,7 @@ export class IconLabel extends Disposable {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
}
if (options?.hoverDelegate) {
this.hoverDelegate = options.hoverDelegate;
}
this.hoverDelegate = options?.hoverDelegate;
}
get element(): HTMLElement {
@@ -185,116 +180,21 @@ export class IconLabel extends Disposable {
}
if (!this.hoverDelegate) {
return this.setupNativeHover(htmlElement, tooltip);
setupNativeHover(htmlElement, tooltip);
} else {
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
const hoverDisposable = setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
if (hoverDisposable) {
this.customHovers.set(htmlElement, hoverDisposable);
}
}
}
private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined {
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
return hoverDelegate.showHover(hoverOptions);
public override dispose() {
super.dispose();
for (const disposable of this.customHovers.values()) {
disposable.dispose();
}
return undefined;
}
private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
return async () => markdown;
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
htmlElement.setAttribute('title', '');
htmlElement.removeAttribute('title');
let tooltip = this.getTooltipForCustom(markdownTooltip);
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
let hoverDisposable: IDisposable | undefined;
function mouseOver(this: HTMLElement, e: MouseEvent): void {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void {
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
if (isMouseDown) {
hoverDisposable?.dispose();
hoverDisposable = undefined;
}
if (isMouseDown || (<any>e).fromElement === htmlElement) {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
}
}
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement));
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));
isHovering = true;
function mouseMove(this: HTMLElement, e: MouseEvent): void {
mouseX = e.x;
}
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
setTimeout(async () => {
if (isHovering && tooltip) {
// Re-use the already computed hover options if they exist.
if (!hoverOptions) {
const target: IHoverDelegateTarget = {
targetElements: [this],
dispose: () => { }
};
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
hoverPosition: HoverPosition.BELOW
};
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
hoverPosition: HoverPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else if (hoverDisposable) {
hoverDisposable.dispose();
hoverDisposable = undefined;
}
}
}
mouseMoveDisposable.dispose();
}, hoverDelegate.delay);
}
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
this.customHovers.set(htmlElement, mouseOverDisposable);
}
private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
let stringTooltip: string = '';
if (isString(tooltip)) {
stringTooltip = tooltip;
} else if (tooltip?.markdownNotSupportedFallback) {
stringTooltip = tooltip.markdownNotSupportedFallback;
}
htmlElement.title = stringTooltip;
this.customHovers.clear();
}
}

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isFunction, isString } from 'vs/base/common/types';
import * as dom from 'vs/base/browser/dom';
import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { DomEmitter } from 'vs/base/browser/event';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { localize } from 'vs/nls';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
if (isString(tooltip)) {
htmlElement.title = tooltip;
} else if (tooltip?.markdownNotSupportedFallback) {
htmlElement.title = tooltip.markdownNotSupportedFallback;
} else {
htmlElement.removeAttribute('title');
}
}
export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined {
if (!markdownTooltip) {
return undefined;
}
const tooltip = getTooltipForCustom(markdownTooltip);
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
let hoverDisposable: IDisposable | undefined;
const mouseOverDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_OVER, true);
mouseOverDomEmitter.event((e: MouseEvent) => {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(e: MouseEvent): void {
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
if (isMouseDown) {
hoverDisposable?.dispose();
hoverDisposable = undefined;
}
if (isMouseDown || (<any>e).fromElement === htmlElement) {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDomEmitter.dispose();
mouseDownDomEmitter.dispose();
}
}
const mouseLeaveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_LEAVE, true);
mouseLeaveDomEmitter.event(mouseLeaveOrDown);
const mouseDownDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_DOWN, true);
mouseDownDomEmitter.event(mouseLeaveOrDown);
isHovering = true;
function mouseMove(e: MouseEvent): void {
mouseX = e.x;
}
const mouseMoveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_MOVE, true);
mouseMoveDomEmitter.event(mouseMove);
setTimeout(async () => {
if (isHovering && tooltip) {
// Re-use the already computed hover options if they exist.
if (!hoverOptions) {
const target: IHoverDelegateTarget = {
targetElements: [htmlElement],
dispose: () => { }
};
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
hoverPosition: HoverPosition.BELOW
};
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
showPointer: hoverDelegate.placement === 'element',
hoverPosition: HoverPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else if (hoverDisposable) {
hoverDisposable.dispose();
hoverDisposable = undefined;
}
}
}
mouseMoveDomEmitter.dispose();
}, hoverDelegate.delay);
});
return mouseOverDomEmitter;
}
function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
return async () => markdown;
}
}
function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined {
if (hoverOptions && isHovering) {
if (mouseX !== undefined && (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse')) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
return hoverDelegate.showHover(hoverOptions);
}
return undefined;
}

View File

@@ -28,7 +28,7 @@
-moz-osx-font-smoothing: grayscale;
vertical-align: top;
flex-shrink: 0; /* fix for https://github.com/Microsoft/vscode/issues/13787 */
flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */
}
.monaco-icon-label > .monaco-icon-label-container {

View File

@@ -898,7 +898,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); }
@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); }
@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); }
@memoize get onContextMenu(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); }
@memoize get onContextMenu(): Event<IListMouseEvent<T> | IListGestureEvent<T>> { return Event.any(Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)), Event.map(domEvent(this.domNode, TouchEventType.Contextmenu) as Event<GestureEvent>, e => this.toGestureEvent(e))); }
@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return Event.map(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)); }
@memoize get onTap(): Event<IListGestureEvent<T>> { return Event.map(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e as GestureEvent)); }

View File

@@ -660,9 +660,15 @@ export class MouseController<T> implements IDisposable {
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
const focus = e.index!;
const anchor = this.list.getAnchor();
let anchor = this.list.getAnchor();
if (this.isSelectionRangeChangeEvent(e)) {
if (typeof anchor === 'undefined') {
const currentFocus = this.list.getFocus()[0];
anchor = currentFocus ?? focus;
this.list.setAnchor(anchor);
}
if (this.isSelectionRangeChangeEvent(e) && typeof anchor === 'number') {
const min = Math.min(anchor, focus);
const max = Math.max(anchor, focus);
const rangeSelection = range(min, max + 1);
@@ -1201,7 +1207,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
const fromMouse = Event.chain(this.view.onContextMenu)
.filter(_ => !didJustPressContextMenuKey)
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent }))
.event;
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);

View File

@@ -16,4 +16,4 @@ export class CombinedSpliceable<T> implements ISpliceable<T> {
splice(start: number, deleteCount: number, elements: T[]): void {
this.spliceables.forEach(s => s.splice(start, deleteCount, elements));
}
}
}

View File

@@ -4,15 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./sash';
import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { isMacintosh } from 'vs/base/common/platform';
import * as types from 'vs/base/common/types';
import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
import { Event, Emitter } from 'vs/base/common/event';
import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $ } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
import { getElementsByTagName, EventHelper, createStyleSheet, append, $, EventLike } from 'vs/base/browser/dom';
import { DomEmitter } from 'vs/base/browser/event';
import { Delayer } from 'vs/base/common/async';
import { memoize } from 'vs/base/common/decorators';
let DEBUG = false;
// DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
@@ -88,6 +87,78 @@ export function setGlobalHoverDelay(size: number): void {
onDidChangeHoverDelay.fire(size);
}
interface PointerEvent extends EventLike {
readonly pageX: number;
readonly pageY: number;
readonly altKey: boolean;
readonly target: EventTarget | null;
}
interface IPointerEventFactory {
readonly onPointerMove: Event<PointerEvent>;
readonly onPointerUp: Event<PointerEvent>;
dispose(): void;
}
class MouseEventFactory implements IPointerEventFactory {
private disposables = new DisposableStore();
@memoize
get onPointerMove(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(window, 'mousemove')).event;
}
@memoize
get onPointerUp(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(window, 'mouseup')).event;
}
dispose(): void {
this.disposables.dispose();
}
}
class GestureEventFactory implements IPointerEventFactory {
private disposables = new DisposableStore();
@memoize
get onPointerMove(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
}
@memoize
get onPointerUp(): Event<PointerEvent> {
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
}
constructor(private el: HTMLElement) { }
dispose(): void {
this.disposables.dispose();
}
}
class OrthogonalPointerEventFactory implements IPointerEventFactory {
@memoize
get onPointerMove(): Event<PointerEvent> {
return this.factory.onPointerMove;
}
@memoize
get onPointerUp(): Event<PointerEvent> {
return this.factory.onPointerUp;
}
constructor(private factory: IPointerEventFactory) { }
dispose(): void {
// noop
}
}
export class Sash extends Disposable {
private el: HTMLElement;
@@ -146,9 +217,9 @@ export class Sash extends Disposable {
if (state !== SashState.Disabled) {
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
domEvent(this._orthogonalStartDragHandle, 'mouseenter')
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseenter')).event
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables);
domEvent(this._orthogonalStartDragHandle, 'mouseleave')
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseleave')).event
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables);
}
};
@@ -176,9 +247,9 @@ export class Sash extends Disposable {
if (state !== SashState.Disabled) {
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
domEvent(this._orthogonalEndDragHandle, 'mouseenter')
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseenter')).event
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables);
domEvent(this._orthogonalEndDragHandle, 'mouseleave')
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseleave')).event
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables);
}
};
@@ -205,13 +276,28 @@ export class Sash extends Disposable {
this.el.classList.add('mac');
}
this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
this._register(domEvent(this.el, 'mouseenter')(() => Sash.onMouseEnter(this)));
this._register(domEvent(this.el, 'mouseleave')(() => Sash.onMouseLeave(this)));
const onMouseDown = this._register(new DomEmitter(this.el, 'mousedown')).event;
this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this));
const onMouseDoubleClick = this._register(new DomEmitter(this.el, 'dblclick')).event;
this._register(onMouseDoubleClick(this.onPointerDoublePress, this));
const onMouseEnter = this._register(new DomEmitter(this.el, 'mouseenter')).event;
this._register(onMouseEnter(() => Sash.onMouseEnter(this)));
const onMouseLeave = this._register(new DomEmitter(this.el, 'mouseleave')).event;
this._register(onMouseLeave(() => Sash.onMouseLeave(this)));
this._register(Gesture.addTarget(this.el));
this._register(domEvent(this.el, EventType.Start)(e => this.onTouchStart(e as GestureEvent), this));
const onTouchStart = Event.map(this._register(new DomEmitter(this.el, EventType.Start)).event, e => ({ ...e, target: e.initialTarget ?? null }));
this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this.el)), this));
const onTap = this._register(new DomEmitter(this.el, EventType.Tap)).event;
const onDoubleTap = Event.map(
Event.filter(
Event.debounce<GestureEvent, { event: GestureEvent, count: number }>(onTap, (res, event) => ({ event, count: (res?.count ?? 0) + 1 }), 250),
({ count }) => count === 2
),
({ event }) => ({ ...event, target: event.initialTarget ?? null })
);
this._register(onDoubleTap(this.onPointerDoublePress, this));
if (typeof options.size === 'number') {
this.size = options.size;
@@ -252,24 +338,24 @@ export class Sash extends Disposable {
this.layout();
}
private onMouseDown(e: MouseEvent): void {
EventHelper.stop(e, false);
private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void {
EventHelper.stop(event);
let isMultisashResize = false;
if (!(e as any).__orthogonalSashEvent) {
const orthogonalSash = this.getOrthogonalSash(e);
if (!(event as any).__orthogonalSashEvent) {
const orthogonalSash = this.getOrthogonalSash(event);
if (orthogonalSash) {
isMultisashResize = true;
(e as any).__orthogonalSashEvent = true;
orthogonalSash.onMouseDown(e);
(event as any).__orthogonalSashEvent = true;
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
}
}
if (this.linkedSash && !(e as any).__linkedSashEvent) {
(e as any).__linkedSashEvent = true;
this.linkedSash.onMouseDown(e);
if (this.linkedSash && !(event as any).__linkedSashEvent) {
(event as any).__linkedSashEvent = true;
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
}
if (!this.state) {
@@ -287,10 +373,9 @@ export class Sash extends Disposable {
iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash
}
const mouseDownEvent = new StandardMouseEvent(e);
const startX = mouseDownEvent.posx;
const startY = mouseDownEvent.posy;
const altKey = mouseDownEvent.altKey;
const startX = event.pageX;
const startY = event.pageY;
const altKey = event.altKey;
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
this.el.classList.add('active');
@@ -332,15 +417,14 @@ export class Sash extends Disposable {
this.onDidEnablementChange(updateStyle, null, disposables);
}
const onMouseMove = (e: MouseEvent) => {
const onPointerMove = (e: PointerEvent) => {
EventHelper.stop(e, false);
const mouseMoveEvent = new StandardMouseEvent(e);
const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
this._onDidChange.fire(event);
};
const onMouseUp = (e: MouseEvent) => {
const onPointerUp = (e: PointerEvent) => {
EventHelper.stop(e, false);
this.el.removeChild(style);
@@ -355,11 +439,12 @@ export class Sash extends Disposable {
}
};
domEvent(window, 'mousemove')(onMouseMove, null, disposables);
domEvent(window, 'mouseup')(onMouseUp, null, disposables);
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
disposables.add(pointerEventFactory);
}
private onMouseDoubleClick(e: MouseEvent): void {
private onPointerDoublePress(e: MouseEvent): void {
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
@@ -373,41 +458,6 @@ export class Sash extends Disposable {
this._onDidReset.fire();
}
private onTouchStart(event: GestureEvent): void {
EventHelper.stop(event);
const listeners: IDisposable[] = [];
const startX = event.pageX;
const startY = event.pageY;
const altKey = event.altKey;
this._onDidStart.fire({
startX: startX,
currentX: startX,
startY: startY,
currentY: startY,
altKey
});
listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => {
if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) {
this._onDidChange.fire({
startX: startX,
currentX: event.pageX,
startY: startY,
currentY: event.pageY,
altKey
});
}
}));
listeners.push(addDisposableListener(this.el, EventType.End, () => {
this._onDidEnd.fire();
dispose(listeners);
}));
}
private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void {
if (sash.el.classList.contains('active')) {
sash.hoverDelayer.cancel();
@@ -476,7 +526,7 @@ export class Sash extends Disposable {
return this.hidden;
}
private getOrthogonalSash(e: MouseEvent): Sash | undefined {
private getOrthogonalSash(e: PointerEvent): Sash | undefined {
if (!e.target || !(e.target instanceof HTMLElement)) {
return undefined;
}

View File

@@ -330,7 +330,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
}
if (this.styles.listFocusForeground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`);
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`);
}
if (this.styles.decoratorRightForeground) {

View File

@@ -16,6 +16,7 @@ import { isFirefox } from 'vs/base/browser/browser';
import { DataTransfers } from 'vs/base/browser/dnd';
import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { localize } from 'vs/nls';
import { ScrollEvent } from 'vs/base/common/scrollable';
export interface IPaneOptions {
minimumBodySize?: number;
@@ -444,6 +445,7 @@ export class PaneView extends Disposable {
orientation: Orientation;
readonly onDidSashChange: Event<number>;
readonly onDidScroll: Event<ScrollEvent>;
constructor(container: HTMLElement, options: IPaneViewOptions = {}) {
super();
@@ -453,6 +455,7 @@ export class PaneView extends Disposable {
this.element = append(container, $('.monaco-pane-view'));
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
this.onDidSashChange = this.splitview.onDidSashChange;
this.onDidScroll = this.splitview.onDidScroll;
}
addPane(pane: Pane, size: number, index = this.splitview.length): void {

View File

@@ -14,7 +14,7 @@ import { Color } from 'vs/base/common/color';
import { domEvent } from 'vs/base/browser/event';
import { $, append, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
import { Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
export interface ISplitViewStyles {
@@ -237,6 +237,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
private _onDidSashReset = this._register(new Emitter<number>());
readonly onDidSashReset = this._onDidSashReset.event;
readonly onDidScroll: Event<ScrollEvent>;
get length(): number {
return this.viewItems.length;
}
@@ -317,7 +319,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden
}, this.scrollable));
this._register(this.scrollableElement.onScroll(e => {
this.onDidScroll = this.scrollableElement.onScroll;
this._register(this.onDidScroll(e => {
this.viewContainer.scrollTop = e.scrollTop;
this.viewContainer.scrollLeft = e.scrollLeft;
}));

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-table {

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list';

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./table';

View File

@@ -5,7 +5,7 @@
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
import { splice, tail2 } from 'vs/base/common/arrays';
import { tail2 } from 'vs/base/common/arrays';
import { LcsDiff } from 'vs/base/common/diff/diff';
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
@@ -256,7 +256,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
}
const deletedNodes = splice(parentNode.children, lastIndex, deleteCount, nodesToInsert);
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
// figure out what is the count of deleted visible children
let deletedVisibleChildrenCount = 0;