mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-31 09:35:39 -05:00
Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 (#15681)
* Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 * Fixes and cleanup * Distro * Fix hygiene yarn * delete no yarn lock changes file * Fix hygiene * Fix layer check * Fix CI * Skip lib checks * Remove tests deleted in vs code * Fix tests * Distro * Fix tests and add removed extension point * Skip failing notebook tests for now * Disable broken tests and cleanup build folder * Update yarn.lock and fix smoke tests * Bump sqlite * fix contributed actions and file spacing * Fix user data path * Update yarn.locks Co-authored-by: ADS Merger <karlb@microsoft.com>
This commit is contained in:
@@ -110,13 +110,13 @@ export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscree
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
export const isEdgeLegacy = (userAgent.indexOf('Edge/') >= 0);
|
||||
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
|
||||
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 = isEdgeLegacy && (userAgent.indexOf('WebView/') >= 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);
|
||||
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
|
||||
|
||||
@@ -25,19 +25,7 @@ export const BrowserFeatures = {
|
||||
readText: (
|
||||
platform.isNative
|
||||
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText)
|
||||
),
|
||||
richText: (() => {
|
||||
if (browser.isEdgeLegacy) {
|
||||
let index = navigator.userAgent.indexOf('Edge/');
|
||||
let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10);
|
||||
|
||||
if (!version || (version >= 12 && version <= 16)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})()
|
||||
)
|
||||
},
|
||||
keyboard: (() => {
|
||||
if (platform.isNative || browser.isStandalone) {
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
export interface IContextMenuEvent {
|
||||
readonly shiftKey?: boolean;
|
||||
@@ -19,7 +20,7 @@ export interface IContextMenuDelegate {
|
||||
getActions(): readonly IAction[];
|
||||
getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox';
|
||||
getActionViewItem?(action: IAction): IActionViewItem | undefined;
|
||||
getActionsContext?(event?: IContextMenuEvent): any;
|
||||
getActionsContext?(event?: IContextMenuEvent): unknown;
|
||||
getKeyBinding?(action: IAction): ResolvedKeybinding | undefined;
|
||||
getMenuClassName?(): string;
|
||||
onHide?(didCancel: boolean): void;
|
||||
|
||||
@@ -42,7 +42,7 @@ export class DelayedDragHandler extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.clearDragTimeout();
|
||||
@@ -89,7 +89,7 @@ export function applyDragImage(event: DragEvent, label: string | null, clazz: st
|
||||
|
||||
export interface IDragAndDropData {
|
||||
update(dataTransfer: DataTransfer): void;
|
||||
getData(): any;
|
||||
getData(): unknown;
|
||||
}
|
||||
|
||||
export class DragAndDropData<T> implements IDragAndDropData {
|
||||
|
||||
@@ -908,7 +908,7 @@ export const EventType = {
|
||||
MOUSE_OUT: 'mouseout',
|
||||
MOUSE_ENTER: 'mouseenter',
|
||||
MOUSE_LEAVE: 'mouseleave',
|
||||
MOUSE_WHEEL: browser.isEdgeLegacy ? 'mousewheel' : 'wheel',
|
||||
MOUSE_WHEEL: 'wheel',
|
||||
POINTER_UP: 'pointerup',
|
||||
POINTER_DOWN: 'pointerdown',
|
||||
POINTER_MOVE: 'pointermove',
|
||||
@@ -1260,18 +1260,21 @@ export function computeScreenAwareSize(cssPx: number): number {
|
||||
* to change the location of the current page.
|
||||
* See https://mathiasbynens.github.io/rel-noopener/
|
||||
*/
|
||||
export function windowOpenNoOpener(url: string): void {
|
||||
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);
|
||||
return true;
|
||||
} else {
|
||||
let newTab = window.open();
|
||||
if (newTab) {
|
||||
(newTab as any).opener = null;
|
||||
newTab.location.href = url;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1477,37 +1480,8 @@ export function multibyteAwareBtoa(str: string): string {
|
||||
*/
|
||||
export namespace WebFileSystemAccess {
|
||||
|
||||
// https://wicg.github.io/file-system-access/#dom-window-showdirectorypicker
|
||||
export interface FileSystemAccess {
|
||||
showDirectoryPicker: () => Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/file-system-access/#api-filesystemdirectoryhandle
|
||||
export interface FileSystemDirectoryHandle {
|
||||
readonly kind: 'directory',
|
||||
readonly name: string,
|
||||
|
||||
getFileHandle: (name: string, options?: { create?: boolean }) => Promise<FileSystemFileHandle>;
|
||||
getDirectoryHandle: (name: string, options?: { create?: boolean }) => Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/file-system-access/#api-filesystemfilehandle
|
||||
export interface FileSystemFileHandle {
|
||||
readonly kind: 'file',
|
||||
readonly name: string,
|
||||
|
||||
createWritable: (options?: { keepExistingData?: boolean }) => Promise<FileSystemWritableFileStream>;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/file-system-access/#api-filesystemwritablefilestream
|
||||
export interface FileSystemWritableFileStream {
|
||||
write: (buffer: Uint8Array) => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function supported(obj: any & Window): obj is FileSystemAccess {
|
||||
const candidate = obj as FileSystemAccess | undefined;
|
||||
if (typeof candidate?.showDirectoryPicker === 'function') {
|
||||
export function supported(obj: any & Window): boolean {
|
||||
if (typeof obj?.showDirectoryPicker === 'function') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1543,13 +1517,14 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
metaKey: false
|
||||
};
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => {
|
||||
// if keydown event is repeated, ignore it #112347
|
||||
if (e.repeat) {
|
||||
return;
|
||||
}
|
||||
this._subscriptions.add(domEvent(window, 'keydown', true)(e => {
|
||||
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
// If Alt-key keydown event is repeated, ignore it #112347
|
||||
// Only known to be necessary for Alt-Key at the moment #115810
|
||||
if (event.keyCode === KeyCode.Alt && e.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.altKey && !this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyPressed = 'alt';
|
||||
@@ -1576,7 +1551,7 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'keyup', true)(e => {
|
||||
this._subscriptions.add(domEvent(window, 'keyup', true)(e => {
|
||||
if (!e.altKey && this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyReleased = 'alt';
|
||||
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
|
||||
@@ -1656,7 +1631,7 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
return ModifierKeyEmitter.instance;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this._subscriptions.dispose();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type EventHandler = HTMLElement | HTMLDocument | Window;
|
||||
|
||||
export interface IDomEvent {
|
||||
<K extends keyof HTMLElementEventMap>(element: EventHandler, type: K, useCapture?: boolean): BaseEvent<HTMLElementEventMap[K]>;
|
||||
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<any>;
|
||||
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<unknown>;
|
||||
}
|
||||
|
||||
export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface FormattedTextRenderOptions {
|
||||
readonly className?: string;
|
||||
readonly inline?: boolean;
|
||||
readonly actionHandler?: IContentActionHandler;
|
||||
readonly renderCodeSegements?: boolean;
|
||||
}
|
||||
|
||||
export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement {
|
||||
@@ -26,7 +27,7 @@ export function renderText(text: string, options: FormattedTextRenderOptions = {
|
||||
|
||||
export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
|
||||
_renderFormattedText(element, parseFormattedText(formattedText, !!options.renderCodeSegements), options.actionHandler, options.renderCodeSegements);
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -75,6 +76,7 @@ const enum FormatType {
|
||||
Italics,
|
||||
Action,
|
||||
ActionClose,
|
||||
Code,
|
||||
NewLine
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ interface IFormatParseTree {
|
||||
children?: IFormatParseTree[];
|
||||
}
|
||||
|
||||
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
|
||||
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegements?: boolean) {
|
||||
let child: Node | undefined;
|
||||
|
||||
if (treeNode.type === FormatType.Text) {
|
||||
@@ -94,6 +96,8 @@ function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionH
|
||||
child = document.createElement('b');
|
||||
} else if (treeNode.type === FormatType.Italics) {
|
||||
child = document.createElement('i');
|
||||
} else if (treeNode.type === FormatType.Code && renderCodeSegements) {
|
||||
child = document.createElement('code');
|
||||
} else if (treeNode.type === FormatType.Action && actionHandler) {
|
||||
const a = document.createElement('a');
|
||||
a.href = '#';
|
||||
@@ -114,12 +118,12 @@ function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionH
|
||||
|
||||
if (child && Array.isArray(treeNode.children)) {
|
||||
treeNode.children.forEach((nodeChild) => {
|
||||
_renderFormattedText(child!, nodeChild, actionHandler);
|
||||
_renderFormattedText(child!, nodeChild, actionHandler, renderCodeSegements);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseFormattedText(content: string): IFormatParseTree {
|
||||
function parseFormattedText(content: string, parseCodeSegments: boolean): IFormatParseTree {
|
||||
|
||||
const root: IFormatParseTree = {
|
||||
type: FormatType.Root,
|
||||
@@ -134,19 +138,19 @@ function parseFormattedText(content: string): IFormatParseTree {
|
||||
while (!stream.eos()) {
|
||||
let next = stream.next();
|
||||
|
||||
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
|
||||
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek(), parseCodeSegments) !== FormatType.Invalid);
|
||||
if (isEscapedFormatType) {
|
||||
next = stream.next(); // unread the backslash if it escapes a format tag type
|
||||
}
|
||||
|
||||
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
|
||||
if (!isEscapedFormatType && isFormatTag(next, parseCodeSegments) && next === stream.peek()) {
|
||||
stream.advance();
|
||||
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
const type = formatTagType(next);
|
||||
const type = formatTagType(next, parseCodeSegments);
|
||||
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
|
||||
current = stack.pop()!;
|
||||
} else {
|
||||
@@ -200,11 +204,11 @@ function parseFormattedText(content: string): IFormatParseTree {
|
||||
return root;
|
||||
}
|
||||
|
||||
function isFormatTag(char: string): boolean {
|
||||
return formatTagType(char) !== FormatType.Invalid;
|
||||
function isFormatTag(char: string, supportCodeSegments: boolean): boolean {
|
||||
return formatTagType(char, supportCodeSegments) !== FormatType.Invalid;
|
||||
}
|
||||
|
||||
function formatTagType(char: string): FormatType {
|
||||
function formatTagType(char: string, supportCodeSegments: boolean): FormatType {
|
||||
switch (char) {
|
||||
case '*':
|
||||
return FormatType.Bold;
|
||||
@@ -214,6 +218,8 @@ function formatTagType(char: string): FormatType {
|
||||
return FormatType.Action;
|
||||
case ']':
|
||||
return FormatType.ActionClose;
|
||||
case '`':
|
||||
return supportCodeSegments ? FormatType.Code : FormatType.Invalid;
|
||||
default:
|
||||
return FormatType.Invalid;
|
||||
}
|
||||
|
||||
@@ -168,27 +168,24 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
// when code-block rendering is async we return sync
|
||||
// but update the node with the real result later.
|
||||
const id = defaultGenerator.nextId();
|
||||
|
||||
// {{SQL CARBON EDIT}} - Promise.all not returning the strValue properly in original code? @todo anthonydresser 4/12/19 investigate a better way to do this.
|
||||
const promise = value.then(strValue => {
|
||||
withInnerHTML.then(e => {
|
||||
const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
|
||||
if (span) {
|
||||
span.innerHTML = strValue.innerHTML;
|
||||
DOM.reset(span, strValue);
|
||||
}
|
||||
}).catch(err => {
|
||||
// ignore
|
||||
});
|
||||
});
|
||||
|
||||
// original VS Code source
|
||||
// const promise = Promise.all([value, withInnerHTML]).then(values => {
|
||||
// const strValue = values[0];
|
||||
// const span = element.querySelector(`div[data-code="${id}"]`);
|
||||
// const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
|
||||
// if (span) {
|
||||
// span.innerHTML = strValue;
|
||||
// DOM.reset(span, values[0]);
|
||||
// }
|
||||
// }).catch(err => {
|
||||
// }).catch(_err => {
|
||||
// // ignore
|
||||
// });
|
||||
|
||||
@@ -398,5 +395,16 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString) {
|
||||
if (value.length > 100_000) {
|
||||
value = `${value.substr(0, 100_000)}…`;
|
||||
}
|
||||
return sanitizeRenderedMarkdown({ isTrusted: false }, marked.parse(value, { renderer })).toString();
|
||||
|
||||
const unescapeInfo = new Map<string, string>([
|
||||
['"', '"'],
|
||||
['&', '&'],
|
||||
[''', '\''],
|
||||
['<', '<'],
|
||||
['>', '>'],
|
||||
]);
|
||||
|
||||
const html = marked.parse(value, { renderer }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m);
|
||||
|
||||
return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString();
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export class Gesture extends Disposable {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
if (this.handle) {
|
||||
this.handle.dispose();
|
||||
this.handle = null;
|
||||
|
||||
@@ -8,13 +8,14 @@ import * as platform from 'vs/base/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { SelectBox, ISelectOptionItem, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, Separator, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, Separator } from 'vs/base/common/actions';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
import { $, addDisposableListener, append, EventHelper, EventLike, EventType, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
|
||||
import { $, addDisposableListener, append, EventHelper, EventLike, EventType } from 'vs/base/browser/dom';
|
||||
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
|
||||
export interface IBaseActionViewItemOptions {
|
||||
draggable?: boolean;
|
||||
@@ -26,12 +27,12 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
|
||||
element: HTMLElement | undefined;
|
||||
|
||||
_context: any;
|
||||
_context: unknown;
|
||||
_action: IAction;
|
||||
|
||||
private _actionRunner: IActionRunner | undefined;
|
||||
|
||||
constructor(context: any, action: IAction, protected options: IBaseActionViewItemOptions = {}) {
|
||||
constructor(context: unknown, action: IAction, protected options: IBaseActionViewItemOptions = {}) {
|
||||
super();
|
||||
|
||||
this._context = context || this;
|
||||
@@ -168,20 +169,38 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
this.actionRunner.run(this._action, context);
|
||||
}
|
||||
|
||||
// Only set the tabIndex on the element once it is about to get focused
|
||||
// That way this element wont be a tab stop when it is not needed #106441
|
||||
focus(): void {
|
||||
if (this.element) {
|
||||
this.element.tabIndex = 0;
|
||||
this.element.focus();
|
||||
this.element.classList.add('focused');
|
||||
}
|
||||
}
|
||||
|
||||
isFocused(): boolean {
|
||||
return !!this.element?.classList.contains('focused');
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.element) {
|
||||
this.element.blur();
|
||||
this.element.tabIndex = -1;
|
||||
this.element.classList.remove('focused');
|
||||
}
|
||||
}
|
||||
|
||||
setFocusable(focusable: boolean): void {
|
||||
if (this.element) {
|
||||
this.element.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
get trapsArrowNavigation(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected updateEnabled(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
@@ -207,7 +226,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
if (this.element) {
|
||||
this.element.remove();
|
||||
this.element = undefined;
|
||||
@@ -226,7 +245,7 @@ export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
|
||||
export class ActionViewItem extends BaseActionViewItem {
|
||||
|
||||
protected label: HTMLElement | undefined;
|
||||
protected options: IActionViewItemOptions;
|
||||
protected override options: IActionViewItemOptions;
|
||||
|
||||
private cssClass?: string;
|
||||
|
||||
@@ -239,7 +258,7 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
this.cssClass = '';
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
if (this.element) {
|
||||
@@ -269,21 +288,38 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
|
||||
// Only set the tabIndex on the element once it is about to get focused
|
||||
// That way this element wont be a tab stop when it is not needed #106441
|
||||
override focus(): void {
|
||||
if (this.label) {
|
||||
this.label.tabIndex = 0;
|
||||
this.label.focus();
|
||||
}
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
override isFocused(): boolean {
|
||||
return !!this.label && this.label?.tabIndex === 0;
|
||||
}
|
||||
|
||||
override blur(): void {
|
||||
if (this.label) {
|
||||
this.label.tabIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
override setFocusable(focusable: boolean): void {
|
||||
if (this.label) {
|
||||
this.label.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
override updateLabel(): void {
|
||||
if (this.options.label && this.label) {
|
||||
this.label.textContent = this.getAction().label;
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltip(): void {
|
||||
override updateTooltip(): void {
|
||||
let title: string | null = null;
|
||||
|
||||
if (this.getAction().tooltip) {
|
||||
@@ -302,7 +338,7 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateClass(): void {
|
||||
override updateClass(): void {
|
||||
if (this.cssClass && this.label) {
|
||||
this.label.classList.remove(...this.cssClass.split(' '));
|
||||
}
|
||||
@@ -337,12 +373,11 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
override updateEnabled(): void {
|
||||
if (this.getAction().enabled) {
|
||||
if (this.label) {
|
||||
this.label.removeAttribute('aria-disabled');
|
||||
this.label.classList.remove('disabled');
|
||||
this.label.tabIndex = 0;
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
@@ -352,7 +387,6 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
if (this.label) {
|
||||
this.label.setAttribute('aria-disabled', 'true');
|
||||
this.label.classList.add('disabled');
|
||||
removeTabIndexAndUpdateFocus(this.label);
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
@@ -361,7 +395,7 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
override updateChecked(): void {
|
||||
if (this.label) {
|
||||
if (this.getAction().checked) {
|
||||
this.label.classList.add('checked');
|
||||
@@ -372,7 +406,7 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
updateExpanded(): void {
|
||||
override updateExpanded(): void {
|
||||
if (this.label) {
|
||||
if (this.getAction().expanded !== undefined) {
|
||||
this.label.setAttribute('aria-expanded', `${this.getAction().expanded}`);
|
||||
@@ -390,6 +424,7 @@ export class SelectActionViewItem extends BaseActionViewItem {
|
||||
super(ctx, action);
|
||||
|
||||
this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
|
||||
this.selectBox.setFocusable(false);
|
||||
|
||||
this._register(this.selectBox);
|
||||
this.registerListeners();
|
||||
@@ -413,19 +448,23 @@ export class SelectActionViewItem extends BaseActionViewItem {
|
||||
return option;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
override setFocusable(focusable: boolean): void {
|
||||
this.selectBox.setFocusable(focusable);
|
||||
}
|
||||
|
||||
override focus(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
override blur(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.blur();
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
this.selectBox.render(container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,30 +4,28 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-action-bar {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-action-bar .actions-container {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .actions-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-action-bar.reverse .actions-container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item {
|
||||
display: block;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
transition: transform 50ms ease;
|
||||
position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
|
||||
}
|
||||
|
||||
@@ -35,26 +33,26 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-action-bar.animated .action-item.active {
|
||||
transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .icon,
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-label {
|
||||
font-size: 11px;
|
||||
margin-right: 4px;
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled .action-label,
|
||||
.monaco-action-bar .action-item.disabled .action-label::before,
|
||||
.monaco-action-bar .action-item.disabled .action-label:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -77,10 +75,6 @@
|
||||
margin-right: .8em;
|
||||
}
|
||||
|
||||
.monaco-action-bar.animated.vertical .action-item.active {
|
||||
transform: translate(5px, 0);
|
||||
}
|
||||
|
||||
.secondary-actions .monaco-action-bar .action-label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./actionbar';
|
||||
import { Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator, IActionViewItem, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator } from 'vs/base/common/actions';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
@@ -13,11 +13,22 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IActionViewItemOptions, ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
|
||||
export interface IActionViewItem extends IDisposable {
|
||||
actionRunner: IActionRunner;
|
||||
setActionContext(context: unknown): void;
|
||||
render(element: HTMLElement): void;
|
||||
isEnabled(): boolean;
|
||||
focus(fromRight?: boolean): void; // TODO@isidorn what is this?
|
||||
blur(): void;
|
||||
}
|
||||
|
||||
export interface IActionViewItemProvider {
|
||||
(action: IAction): IActionViewItem | undefined;
|
||||
}
|
||||
|
||||
export const enum ActionsOrientation {
|
||||
HORIZONTAL,
|
||||
HORIZONTAL_REVERSE,
|
||||
VERTICAL,
|
||||
VERTICAL_REVERSE,
|
||||
}
|
||||
|
||||
export interface ActionTrigger {
|
||||
@@ -27,7 +38,7 @@ export interface ActionTrigger {
|
||||
|
||||
export interface IActionBarOptions {
|
||||
readonly orientation?: ActionsOrientation;
|
||||
readonly context?: any;
|
||||
readonly context?: unknown;
|
||||
readonly actionViewItemProvider?: IActionViewItemProvider;
|
||||
readonly actionRunner?: IActionRunner;
|
||||
readonly ariaLabel?: string;
|
||||
@@ -35,7 +46,7 @@ export interface IActionBarOptions {
|
||||
readonly triggerKeys?: ActionTrigger;
|
||||
readonly allowContextMenu?: boolean;
|
||||
readonly preventLoopNavigation?: boolean;
|
||||
readonly ignoreOrientationForPreviousAndNextKey?: boolean;
|
||||
readonly focusOnlyEnabledItems?: boolean;
|
||||
}
|
||||
|
||||
export interface IActionOptions extends IActionViewItemOptions {
|
||||
@@ -63,6 +74,8 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
// Trigger Key Tracking
|
||||
private triggerKeyDown: boolean = false;
|
||||
|
||||
private focusable: boolean = true;
|
||||
|
||||
// Elements
|
||||
domNode: HTMLElement;
|
||||
protected actionsList: HTMLElement;
|
||||
@@ -117,29 +130,20 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
switch (this._orientation) {
|
||||
case ActionsOrientation.HORIZONTAL:
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow];
|
||||
break;
|
||||
case ActionsOrientation.HORIZONTAL_REVERSE:
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow];
|
||||
this.domNode.className += ' reverse';
|
||||
previousKeys = [KeyCode.LeftArrow];
|
||||
nextKeys = [KeyCode.RightArrow];
|
||||
break;
|
||||
case ActionsOrientation.VERTICAL:
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow];
|
||||
previousKeys = [KeyCode.UpArrow];
|
||||
nextKeys = [KeyCode.DownArrow];
|
||||
this.domNode.className += ' vertical';
|
||||
break;
|
||||
case ActionsOrientation.VERTICAL_REVERSE:
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
|
||||
this.domNode.className += ' vertical reverse';
|
||||
break;
|
||||
}
|
||||
|
||||
this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = true;
|
||||
const focusedItem = typeof this.focusedItem === 'number' ? this.viewItems[this.focusedItem] : undefined;
|
||||
|
||||
if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) {
|
||||
eventHandled = this.focusPrevious();
|
||||
@@ -147,6 +151,12 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
eventHandled = this.focusNext();
|
||||
} else if (event.equals(KeyCode.Escape) && this.cancelHasListener) {
|
||||
this._onDidCancel.fire();
|
||||
} else if (event.equals(KeyCode.Home)) {
|
||||
eventHandled = this.focusFirst();
|
||||
} else if (event.equals(KeyCode.End)) {
|
||||
eventHandled = this.focusLast();
|
||||
} else if (event.equals(KeyCode.Tab) && focusedItem instanceof BaseActionViewItem && focusedItem.trapsArrowNavigation) {
|
||||
eventHandled = this.focusNext();
|
||||
} else if (this.isTriggerKeyEvent(event)) {
|
||||
// Staying out of the else branch even if not triggered
|
||||
if (this._triggerKeys.keyDown) {
|
||||
@@ -216,6 +226,25 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
}
|
||||
|
||||
// Some action bars should not be focusable at times
|
||||
// When an action bar is not focusable make sure to make all the elements inside it not focusable
|
||||
// When an action bar is focusable again, make sure the first item can be focused
|
||||
setFocusable(focusable: boolean): void {
|
||||
this.focusable = focusable;
|
||||
if (this.focusable) {
|
||||
const firstEnabled = this.viewItems.find(vi => vi instanceof BaseActionViewItem && vi.isEnabled());
|
||||
if (firstEnabled instanceof BaseActionViewItem) {
|
||||
firstEnabled.setFocusable(true);
|
||||
}
|
||||
} else {
|
||||
this.viewItems.forEach(vi => {
|
||||
if (vi instanceof BaseActionViewItem) {
|
||||
vi.setFocusable(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
|
||||
let ret = false;
|
||||
this._triggerKeys.keys.forEach(keyCode => {
|
||||
@@ -235,11 +264,11 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
}
|
||||
|
||||
get context(): any {
|
||||
get context(): unknown {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
set context(context: any) {
|
||||
set context(context: unknown) {
|
||||
this._context = context;
|
||||
this.viewItems.forEach(i => i.setActionContext(context));
|
||||
}
|
||||
@@ -294,6 +323,11 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
item.setActionContext(this.context);
|
||||
item.render(actionViewItemElement);
|
||||
|
||||
if (this.focusable && item instanceof BaseActionViewItem && this.viewItems.length === 0) {
|
||||
// We need to allow for the first enabled item to be focused on using tab navigation #106441
|
||||
item.setFocusable(true);
|
||||
}
|
||||
|
||||
if (index === null || index < 0 || index >= this.actionsList.children.length) {
|
||||
this.actionsList.appendChild(actionViewItemElement);
|
||||
this.viewItems.push(item);
|
||||
@@ -305,7 +339,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
index++;
|
||||
}
|
||||
});
|
||||
if (this.focusedItem) {
|
||||
if (typeof this.focusedItem === 'number') {
|
||||
// After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
|
||||
this.focus(this.focusedItem);
|
||||
}
|
||||
@@ -383,15 +417,27 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private focusFirst(): boolean {
|
||||
this.focusedItem = this.length() > 1 ? 1 : 0;
|
||||
return this.focusPrevious();
|
||||
}
|
||||
|
||||
private focusLast(): boolean {
|
||||
this.focusedItem = this.length() < 2 ? 0 : this.length() - 2;
|
||||
return this.focusNext();
|
||||
}
|
||||
|
||||
protected focusNext(): boolean {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.focusedItem = this.viewItems.length - 1;
|
||||
} else if (this.viewItems.length <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startIndex = this.focusedItem;
|
||||
let item: IActionViewItem;
|
||||
|
||||
do {
|
||||
|
||||
if (this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
|
||||
this.focusedItem = startIndex;
|
||||
return false;
|
||||
@@ -399,11 +445,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
this.focusedItem = (this.focusedItem + 1) % this.viewItems.length;
|
||||
item = this.viewItems[this.focusedItem];
|
||||
} while (this.focusedItem !== startIndex && !item.isEnabled());
|
||||
|
||||
if (this.focusedItem === startIndex && !item.isEnabled()) {
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
} while (this.focusedItem !== startIndex && this.options.focusOnlyEnabledItems && !item.isEnabled());
|
||||
|
||||
this.updateFocus();
|
||||
return true;
|
||||
@@ -412,6 +454,8 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
protected focusPrevious(): boolean {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.focusedItem = 0;
|
||||
} else if (this.viewItems.length <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startIndex = this.focusedItem;
|
||||
@@ -419,7 +463,6 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
do {
|
||||
this.focusedItem = this.focusedItem - 1;
|
||||
|
||||
if (this.focusedItem < 0) {
|
||||
if (this.options.preventLoopNavigation) {
|
||||
this.focusedItem = startIndex;
|
||||
@@ -428,13 +471,9 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
this.focusedItem = this.viewItems.length - 1;
|
||||
}
|
||||
|
||||
item = this.viewItems[this.focusedItem];
|
||||
} while (this.focusedItem !== startIndex && !item.isEnabled());
|
||||
} while (this.focusedItem !== startIndex && this.options.focusOnlyEnabledItems && !item.isEnabled());
|
||||
|
||||
if (this.focusedItem === startIndex && !item.isEnabled()) {
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
|
||||
this.updateFocus(true);
|
||||
return true;
|
||||
@@ -450,12 +489,20 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
const actionViewItem = item;
|
||||
|
||||
if (i === this.focusedItem) {
|
||||
if (types.isFunction(actionViewItem.isEnabled)) {
|
||||
if (actionViewItem.isEnabled() && types.isFunction(actionViewItem.focus)) {
|
||||
actionViewItem.focus(fromRight);
|
||||
} else {
|
||||
this.actionsList.focus({ preventScroll });
|
||||
}
|
||||
let focusItem = true;
|
||||
|
||||
if (!types.isFunction(actionViewItem.focus)) {
|
||||
focusItem = false;
|
||||
}
|
||||
|
||||
if (this.options.focusOnlyEnabledItems && types.isFunction(item.isEnabled) && !item.isEnabled()) {
|
||||
focusItem = false;
|
||||
}
|
||||
|
||||
if (focusItem) {
|
||||
actionViewItem.focus(fromRight);
|
||||
} else {
|
||||
this.actionsList.focus({ preventScroll });
|
||||
}
|
||||
} else {
|
||||
if (types.isFunction(actionViewItem.blur)) {
|
||||
@@ -478,11 +525,11 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
}
|
||||
|
||||
run(action: IAction, context?: unknown): Promise<void> {
|
||||
return this._actionRunner.run(action, context);
|
||||
async run(action: IAction, context?: unknown): Promise<void> {
|
||||
await this._actionRunner.run(action, context);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
dispose(this.viewItems);
|
||||
this.viewItems = [];
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
height: 100%;
|
||||
outline: none;
|
||||
}
|
||||
.monaco-breadcrumbs.disabled .monaco-breadcrumb-item {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item .codicon-breadcrumb-separator {
|
||||
color: inherit;
|
||||
|
||||
@@ -56,6 +56,7 @@ export class BreadcrumbsWidget {
|
||||
private readonly _nodes = new Array<HTMLDivElement>();
|
||||
private readonly _freeNodes = new Array<HTMLDivElement>();
|
||||
|
||||
private _enabled: boolean = true;
|
||||
private _focusedItemIdx: number = -1;
|
||||
private _selectedItemIdx: number = -1;
|
||||
|
||||
@@ -155,13 +156,18 @@ export class BreadcrumbsWidget {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { color: ${style.breadcrumbsFocusAndSelectionForeground}}\n`;
|
||||
}
|
||||
if (style.breadcrumbsHoverForeground) {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\n`;
|
||||
content += `.monaco-breadcrumbs:not(.disabled ) .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\n`;
|
||||
}
|
||||
if (this._styleElement.innerText !== content) {
|
||||
this._styleElement.innerText = content;
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(value: boolean) {
|
||||
this._enabled = value;
|
||||
this._domNode.classList.toggle('disabled', !this._enabled);
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
let idx = this._focusedItemIdx >= 0 ? this._focusedItemIdx : this._items.length - 1;
|
||||
if (idx >= 0 && idx < this._items.length) {
|
||||
@@ -326,6 +332,9 @@ export class BreadcrumbsWidget {
|
||||
}
|
||||
|
||||
private _onClick(event: IMouseEvent): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
for (let el: HTMLElement | null = event.target; el; el = el.parentElement) {
|
||||
let idx = this._nodes.indexOf(el as HTMLDivElement);
|
||||
if (idx >= 0) {
|
||||
|
||||
@@ -14,12 +14,17 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-text-button:focus {
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.monaco-text-button:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.monaco-button.disabled:focus,
|
||||
.monaco-button.disabled {
|
||||
opacity: 0.4;
|
||||
opacity: 0.4 !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -35,3 +40,15 @@
|
||||
.monaco-button-dropdown > .monaco-dropdown-button {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.monaco-description-button {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monaco-description-button .monaco-button-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.monaco-description-button .monaco-button-description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
|
||||
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
|
||||
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset } from 'vs/base/browser/dom';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { CSSIcon, Codicon } from 'vs/base/common/codicons';
|
||||
@@ -55,6 +55,10 @@ export interface IButton extends IDisposable {
|
||||
hasFocus(): boolean;
|
||||
}
|
||||
|
||||
export interface IButtonWithDescription extends IButton {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class Button extends Disposable implements IButton {
|
||||
|
||||
private _element: HTMLElement;
|
||||
@@ -290,7 +294,6 @@ export class Button extends Disposable implements IButton {
|
||||
} else {
|
||||
this._element.classList.add('disabled');
|
||||
this._element.setAttribute('aria-disabled', String(true));
|
||||
removeTabIndexAndUpdateFocus(this._element);
|
||||
}
|
||||
this.applyStyles(); // {{SQL CARBON EDIT}}
|
||||
}
|
||||
@@ -381,6 +384,207 @@ export class ButtonWithDropdown extends Disposable implements IButton {
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonWithDescription extends Disposable implements IButtonWithDescription {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private _labelElement: HTMLElement;
|
||||
private _descriptionElement: HTMLElement;
|
||||
private options: IButtonOptions;
|
||||
|
||||
private buttonBackground: Color | undefined;
|
||||
private buttonHoverBackground: Color | undefined;
|
||||
private buttonForeground: Color | undefined;
|
||||
private buttonSecondaryBackground: Color | undefined;
|
||||
private buttonSecondaryHoverBackground: Color | undefined;
|
||||
private buttonSecondaryForeground: Color | undefined;
|
||||
private buttonBorder: Color | undefined;
|
||||
|
||||
private _onDidClick = this._register(new Emitter<Event>());
|
||||
get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; }
|
||||
|
||||
private focusTracker: IFocusTracker;
|
||||
|
||||
constructor(container: HTMLElement, options?: IButtonOptions) {
|
||||
super();
|
||||
|
||||
this.options = options || Object.create(null);
|
||||
mixin(this.options, defaultOptions, false);
|
||||
|
||||
this.buttonForeground = this.options.buttonForeground;
|
||||
this.buttonBackground = this.options.buttonBackground;
|
||||
this.buttonHoverBackground = this.options.buttonHoverBackground;
|
||||
|
||||
this.buttonSecondaryForeground = this.options.buttonSecondaryForeground;
|
||||
this.buttonSecondaryBackground = this.options.buttonSecondaryBackground;
|
||||
this.buttonSecondaryHoverBackground = this.options.buttonSecondaryHoverBackground;
|
||||
|
||||
this.buttonBorder = this.options.buttonBorder;
|
||||
|
||||
this._element = document.createElement('a');
|
||||
this._element.classList.add('monaco-button');
|
||||
this._element.classList.add('monaco-description-button');
|
||||
this._element.tabIndex = 0;
|
||||
this._element.setAttribute('role', 'button');
|
||||
|
||||
this._labelElement = document.createElement('div');
|
||||
this._labelElement.classList.add('monaco-button-label');
|
||||
this._labelElement.tabIndex = -1;
|
||||
this._element.appendChild(this._labelElement);
|
||||
|
||||
this._descriptionElement = document.createElement('div');
|
||||
this._descriptionElement.classList.add('monaco-button-description');
|
||||
this._descriptionElement.tabIndex = -1;
|
||||
this._element.appendChild(this._descriptionElement);
|
||||
|
||||
container.appendChild(this._element);
|
||||
|
||||
this._register(Gesture.addTarget(this._element));
|
||||
|
||||
[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {
|
||||
this._register(addDisposableListener(this._element, eventType, e => {
|
||||
if (!this.enabled) {
|
||||
EventHelper.stop(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onDidClick.fire(e);
|
||||
}));
|
||||
});
|
||||
|
||||
this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = false;
|
||||
if (this.enabled && (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) {
|
||||
this._onDidClick.fire(e);
|
||||
eventHandled = true;
|
||||
} else if (event.equals(KeyCode.Escape)) {
|
||||
this._element.blur();
|
||||
eventHandled = true;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
EventHelper.stop(event, true);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => {
|
||||
if (!this._element.classList.contains('disabled')) {
|
||||
this.setHoverBackground();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => {
|
||||
this.applyStyles(); // restore standard styles
|
||||
}));
|
||||
|
||||
// Also set hover background when button is focused for feedback
|
||||
this.focusTracker = this._register(trackFocus(this._element));
|
||||
this._register(this.focusTracker.onDidFocus(() => this.setHoverBackground()));
|
||||
this._register(this.focusTracker.onDidBlur(() => this.applyStyles())); // restore standard styles
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private setHoverBackground(): void {
|
||||
let hoverBackground;
|
||||
if (this.options.secondary) {
|
||||
hoverBackground = this.buttonSecondaryHoverBackground ? this.buttonSecondaryHoverBackground.toString() : null;
|
||||
} else {
|
||||
hoverBackground = this.buttonHoverBackground ? this.buttonHoverBackground.toString() : null;
|
||||
}
|
||||
if (hoverBackground) {
|
||||
this._element.style.backgroundColor = hoverBackground;
|
||||
}
|
||||
}
|
||||
|
||||
style(styles: IButtonStyles): void {
|
||||
this.buttonForeground = styles.buttonForeground;
|
||||
this.buttonBackground = styles.buttonBackground;
|
||||
this.buttonHoverBackground = styles.buttonHoverBackground;
|
||||
this.buttonSecondaryForeground = styles.buttonSecondaryForeground;
|
||||
this.buttonSecondaryBackground = styles.buttonSecondaryBackground;
|
||||
this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground;
|
||||
this.buttonBorder = styles.buttonBorder;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private applyStyles(): void {
|
||||
if (this._element) {
|
||||
let background, foreground;
|
||||
if (this.options.secondary) {
|
||||
foreground = this.buttonSecondaryForeground ? this.buttonSecondaryForeground.toString() : '';
|
||||
background = this.buttonSecondaryBackground ? this.buttonSecondaryBackground.toString() : '';
|
||||
} else {
|
||||
foreground = this.buttonForeground ? this.buttonForeground.toString() : '';
|
||||
background = this.buttonBackground ? this.buttonBackground.toString() : '';
|
||||
}
|
||||
|
||||
const border = this.buttonBorder ? this.buttonBorder.toString() : '';
|
||||
|
||||
this._element.style.color = foreground;
|
||||
this._element.style.backgroundColor = background;
|
||||
|
||||
this._element.style.borderWidth = border ? '1px' : '';
|
||||
this._element.style.borderStyle = border ? 'solid' : '';
|
||||
this._element.style.borderColor = border;
|
||||
}
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
set label(value: string) {
|
||||
this._element.classList.add('monaco-text-button');
|
||||
if (this.options.supportIcons) {
|
||||
reset(this._labelElement, ...renderLabelWithIcons(value));
|
||||
} else {
|
||||
this._labelElement.textContent = value;
|
||||
}
|
||||
if (typeof this.options.title === 'string') {
|
||||
this._element.title = this.options.title;
|
||||
} else if (this.options.title) {
|
||||
this._element.title = value;
|
||||
}
|
||||
}
|
||||
|
||||
set description(value: string) {
|
||||
if (this.options.supportIcons) {
|
||||
reset(this._descriptionElement, ...renderLabelWithIcons(value));
|
||||
} else {
|
||||
this._descriptionElement.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
set icon(icon: CSSIcon) {
|
||||
this._element.classList.add(...CSSIcon.asClassNameArray(icon));
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
if (value) {
|
||||
this._element.classList.remove('disabled');
|
||||
this._element.setAttribute('aria-disabled', String(false));
|
||||
this._element.tabIndex = 0;
|
||||
} else {
|
||||
this._element.classList.add('disabled');
|
||||
this._element.setAttribute('aria-disabled', String(true));
|
||||
}
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return !this._element.classList.contains('disabled');
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._element.focus();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this._element === document.activeElement;
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonBar extends Disposable {
|
||||
|
||||
private _buttons: IButton[] = [];
|
||||
@@ -399,6 +603,12 @@ export class ButtonBar extends Disposable {
|
||||
return button;
|
||||
}
|
||||
|
||||
addButtonWithDescription(options?: IButtonOptions): IButtonWithDescription {
|
||||
const button = this._register(new ButtonWithDescription(this.container, options));
|
||||
this.pushButton(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
addButtonWithDropdown(options: IButtonWithDropdownOptions): IButton {
|
||||
const button = this._register(new ButtonWithDropdown(this.container, options));
|
||||
this.pushButton(button);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./checkbox';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
@@ -19,6 +18,7 @@ export interface ICheckboxOpts extends ICheckboxStyles {
|
||||
readonly icon?: CSSIcon;
|
||||
readonly title: string;
|
||||
readonly isChecked: boolean;
|
||||
readonly notFocusable?: boolean;
|
||||
}
|
||||
|
||||
export interface ICheckboxStyles {
|
||||
@@ -44,21 +44,22 @@ export class CheckboxActionViewItem extends BaseActionViewItem {
|
||||
protected checkbox: Checkbox | undefined;
|
||||
protected readonly disposables = new DisposableStore();
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
this.element = container;
|
||||
|
||||
this.disposables.clear();
|
||||
this.checkbox = new Checkbox({
|
||||
actionClassName: this._action.class,
|
||||
isChecked: this._action.checked,
|
||||
title: this._action.label
|
||||
title: this._action.label,
|
||||
notFocusable: true
|
||||
});
|
||||
this.disposables.add(this.checkbox);
|
||||
this.disposables.add(this.checkbox.onChange(() => this._action.checked = !!this.checkbox && this.checkbox.checked, this));
|
||||
this.element.appendChild(this.checkbox.domNode);
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
override updateEnabled(): void {
|
||||
if (this.checkbox) {
|
||||
if (this.isEnabled()) {
|
||||
this.checkbox.enable();
|
||||
@@ -68,13 +69,33 @@ export class CheckboxActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
override updateChecked(): void {
|
||||
if (this.checkbox) {
|
||||
this.checkbox.checked = this._action.checked;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override focus(): void {
|
||||
if (this.checkbox) {
|
||||
this.checkbox.domNode.tabIndex = 0;
|
||||
this.checkbox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
override blur(): void {
|
||||
if (this.checkbox) {
|
||||
this.checkbox.domNode.tabIndex = -1;
|
||||
this.checkbox.domNode.blur();
|
||||
}
|
||||
}
|
||||
|
||||
override setFocusable(focusable: boolean): void {
|
||||
if (this.checkbox) {
|
||||
this.checkbox.domNode.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.disposables.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -113,7 +134,9 @@ export class Checkbox extends Widget {
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.title = this._opts.title;
|
||||
this.domNode.classList.add(...classes);
|
||||
this.domNode.tabIndex = 0;
|
||||
if (!this._opts.notFocusable) {
|
||||
this.domNode.tabIndex = 0;
|
||||
}
|
||||
this.domNode.setAttribute('role', 'checkbox');
|
||||
this.domNode.setAttribute('aria-checked', String(this._checked));
|
||||
this.domNode.setAttribute('aria-label', this._opts.title);
|
||||
@@ -187,12 +210,10 @@ export class Checkbox extends Widget {
|
||||
}
|
||||
|
||||
enable(): void {
|
||||
this.domNode.tabIndex = 0;
|
||||
this.domNode.setAttribute('aria-disabled', String(false));
|
||||
}
|
||||
|
||||
disable(): void {
|
||||
DOM.removeTabIndexAndUpdateFocus(this.domNode);
|
||||
this.domNode.setAttribute('aria-disabled', String(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.codicon-sync.codicon-modifier-spin, .codicon-loading.codicon-modifier-spin{
|
||||
.codicon-sync.codicon-modifier-spin,
|
||||
.codicon-loading.codicon-modifier-spin,
|
||||
.codicon-gear.codicon-modifier-spin,
|
||||
.codicon-notebook-state-executing.codicon-modifier-spin {
|
||||
/* Use steps to throttle FPS to reduce CPU usage */
|
||||
animation: codicon-spin 1.5s steps(30) infinite;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -13,5 +13,5 @@ export function formatRule(c: Codicon) {
|
||||
while (def instanceof Codicon) {
|
||||
def = def.definition;
|
||||
}
|
||||
return `.codicon-${c.id}:before { content: '${def.character}'; }`;
|
||||
return `.codicon-${c.id}:before { content: '${def.fontCharacter}'; }`;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface IDelegate {
|
||||
anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical
|
||||
canRelayout?: boolean; // default: true
|
||||
onDOMEvent?(e: Event, activeElement: HTMLElement): void;
|
||||
onHide?(data?: any): void;
|
||||
onHide?(data?: unknown): void;
|
||||
}
|
||||
|
||||
export interface IContextViewProvider {
|
||||
@@ -324,7 +324,7 @@ export class ContextView extends Disposable {
|
||||
this.view.style.width = 'initial';
|
||||
}
|
||||
|
||||
hide(data?: any): void {
|
||||
hide(data?: unknown): void {
|
||||
const delegate = this.delegate;
|
||||
this.delegate = null;
|
||||
|
||||
@@ -352,7 +352,7 @@ export class ContextView extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
this.hide();
|
||||
|
||||
super.dispose();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
width: 100%;
|
||||
left:0;
|
||||
top:0;
|
||||
z-index: 2000;
|
||||
z-index: 2600;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -34,17 +34,12 @@
|
||||
|
||||
/** Dialog: Title Actions Row */
|
||||
.monaco-dialog-box .dialog-toolbar-row {
|
||||
height: 22px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.monaco-dialog-box .action-label {
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
background-size: 16px;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0px;
|
||||
margin-left: 4px;
|
||||
.monaco-dialog-box .dialog-toolbar-row .actions-container {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/** Dialog: Message Row */
|
||||
@@ -144,6 +139,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button {
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { domEvent } from 'vs/base/browser/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ButtonBar, IButtonStyles } from 'vs/base/browser/ui/button/button';
|
||||
import { ButtonBar, ButtonWithDescription, IButtonStyles } from 'vs/base/browser/ui/button/button';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
@@ -34,6 +34,10 @@ export interface IDialogOptions {
|
||||
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
|
||||
readonly inputs?: IDialogInputOptions[];
|
||||
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
|
||||
readonly renderBody?: (container: HTMLElement) => void;
|
||||
readonly icon?: Codicon;
|
||||
readonly buttonDetails?: string[];
|
||||
readonly disableCloseAction?: boolean;
|
||||
}
|
||||
|
||||
export interface IDialogResult {
|
||||
@@ -53,6 +57,8 @@ export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles {
|
||||
readonly inputBackground?: Color;
|
||||
readonly inputForeground?: Color;
|
||||
readonly inputBorder?: Color;
|
||||
readonly textLinkForeground?: Color;
|
||||
|
||||
}
|
||||
|
||||
interface ButtonMapEntry {
|
||||
@@ -71,6 +77,7 @@ export class Dialog extends Disposable {
|
||||
private modalElement: HTMLElement | undefined;
|
||||
private readonly buttonsContainer: HTMLElement;
|
||||
private readonly messageDetailElement: HTMLElement;
|
||||
private readonly messageContainer: HTMLElement;
|
||||
private readonly iconElement: HTMLElement;
|
||||
private readonly checkbox: SimpleCheckbox | undefined;
|
||||
private readonly toolbarContainer: HTMLElement;
|
||||
@@ -83,7 +90,7 @@ export class Dialog extends Disposable {
|
||||
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
|
||||
super();
|
||||
|
||||
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
|
||||
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`));
|
||||
this.shadowElement = this.modalElement.appendChild($('.dialog-shadow'));
|
||||
this.element = this.shadowElement.appendChild($('.monaco-dialog-box'));
|
||||
this.element.setAttribute('role', 'dialog');
|
||||
@@ -95,20 +102,29 @@ export class Dialog extends Disposable {
|
||||
|
||||
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
|
||||
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
|
||||
const messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
|
||||
this.messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
|
||||
|
||||
if (this.options.detail) {
|
||||
const messageElement = messageContainer.appendChild($('.dialog-message'));
|
||||
if (this.options.detail || this.options.renderBody) {
|
||||
const messageElement = this.messageContainer.appendChild($('.dialog-message'));
|
||||
const messageTextElement = messageElement.appendChild($('.dialog-message-text'));
|
||||
messageTextElement.innerText = this.message;
|
||||
}
|
||||
|
||||
this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail'));
|
||||
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
|
||||
this.messageDetailElement = this.messageContainer.appendChild($('.dialog-message-detail'));
|
||||
if (this.options.detail || !this.options.renderBody) {
|
||||
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
|
||||
} else {
|
||||
this.messageDetailElement.style.display = 'none';
|
||||
}
|
||||
|
||||
if (this.options.renderBody) {
|
||||
const customBody = this.messageContainer.appendChild($('.dialog-message-body'));
|
||||
this.options.renderBody(customBody);
|
||||
}
|
||||
|
||||
if (this.options.inputs) {
|
||||
this.inputs = this.options.inputs.map(input => {
|
||||
const inputRowElement = messageContainer.appendChild($('.dialog-message-input'));
|
||||
const inputRowElement = this.messageContainer.appendChild($('.dialog-message-input'));
|
||||
|
||||
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
|
||||
placeholder: input.placeholder,
|
||||
@@ -126,7 +142,7 @@ export class Dialog extends Disposable {
|
||||
}
|
||||
|
||||
if (this.options.checkboxLabel) {
|
||||
const checkboxRowElement = messageContainer.appendChild($('.dialog-checkbox-row'));
|
||||
const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row'));
|
||||
|
||||
const checkbox = this.checkbox = this._register(new SimpleCheckbox(this.options.checkboxLabel, !!this.options.checkboxChecked));
|
||||
|
||||
@@ -175,12 +191,16 @@ export class Dialog extends Disposable {
|
||||
|
||||
const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer));
|
||||
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
|
||||
this.buttonsContainer.classList.toggle('centered');
|
||||
|
||||
// Handle button clicks
|
||||
buttonMap.forEach((entry, index) => {
|
||||
const button = this._register(buttonBar.addButton({ title: true }));
|
||||
const primary = buttonMap[index].index === 0;
|
||||
const button = this.options.buttonDetails ? this._register(buttonBar.addButtonWithDescription({ title: true, secondary: !primary })) : this._register(buttonBar.addButton({ title: true, secondary: !primary }));
|
||||
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
|
||||
|
||||
if (button instanceof ButtonWithDescription) {
|
||||
button.description = this.options.buttonDetails![buttonMap[index].index];
|
||||
}
|
||||
this._register(button.onDidClick(e => {
|
||||
if (e) {
|
||||
EventHelper.stop(e);
|
||||
@@ -287,7 +307,7 @@ export class Dialog extends Disposable {
|
||||
EventHelper.stop(e, true);
|
||||
const evt = new StandardKeyboardEvent(e);
|
||||
|
||||
if (evt.equals(KeyCode.Escape)) {
|
||||
if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape)) {
|
||||
resolve({
|
||||
button: this.options.cancelId || 0,
|
||||
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
|
||||
@@ -313,34 +333,41 @@ export class Dialog extends Disposable {
|
||||
|
||||
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray, spinModifierClassName);
|
||||
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
this.iconElement.classList.add(...dialogErrorIcon.classNamesArray);
|
||||
break;
|
||||
case 'warning':
|
||||
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
|
||||
break;
|
||||
case 'pending':
|
||||
this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName);
|
||||
break;
|
||||
case 'none':
|
||||
case 'info':
|
||||
case 'question':
|
||||
default:
|
||||
this.iconElement.classList.add(...dialogInfoIcon.classNamesArray);
|
||||
break;
|
||||
if (this.options.icon) {
|
||||
this.iconElement.classList.add(...this.options.icon.classNamesArray);
|
||||
} else {
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
this.iconElement.classList.add(...dialogErrorIcon.classNamesArray);
|
||||
break;
|
||||
case 'warning':
|
||||
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
|
||||
break;
|
||||
case 'pending':
|
||||
this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName);
|
||||
break;
|
||||
case 'none':
|
||||
case 'info':
|
||||
case 'question':
|
||||
default:
|
||||
this.iconElement.classList.add(...dialogInfoIcon.classNamesArray);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
|
||||
|
||||
const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => {
|
||||
resolve({
|
||||
button: this.options.cancelId || 0,
|
||||
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
|
||||
});
|
||||
}));
|
||||
if (!this.options.disableCloseAction) {
|
||||
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
|
||||
|
||||
actionBar.push(action, { icon: true, label: false, });
|
||||
const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => {
|
||||
resolve({
|
||||
button: this.options.cancelId || 0,
|
||||
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
|
||||
});
|
||||
}));
|
||||
|
||||
actionBar.push(action, { icon: true, label: false, });
|
||||
}
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
@@ -369,6 +396,7 @@ export class Dialog extends Disposable {
|
||||
const bgColor = style.dialogBackground;
|
||||
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
|
||||
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
|
||||
const linkFgColor = style.textLinkForeground;
|
||||
|
||||
this.shadowElement.style.boxShadow = shadowColor;
|
||||
|
||||
@@ -389,6 +417,12 @@ export class Dialog extends Disposable {
|
||||
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
|
||||
}
|
||||
|
||||
if (linkFgColor) {
|
||||
for (const el of this.messageContainer.getElementsByTagName('a')) {
|
||||
el.style.color = linkFgColor.toString();
|
||||
}
|
||||
}
|
||||
|
||||
let color;
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
@@ -417,7 +451,7 @@ export class Dialog extends Disposable {
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
if (this.modalElement) {
|
||||
|
||||
@@ -11,8 +11,29 @@
|
||||
.monaco-dropdown > .dropdown-label {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.monaco-dropdown > .dropdown-label > .action-label.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary {
|
||||
display: flex !important;
|
||||
flex-direction: row;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary > .action-container > .action-label {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label .codicon[class*='codicon-'] {
|
||||
font-size: 12px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
line-height: 16px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export class BaseDropdown extends ActionRunner {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.hide();
|
||||
|
||||
@@ -159,7 +159,7 @@ export class Dropdown extends BaseDropdown {
|
||||
this.contextViewProvider = options.contextViewProvider;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
override show(): void {
|
||||
super.show();
|
||||
|
||||
this.element.classList.add('active');
|
||||
@@ -187,7 +187,7 @@ export class Dropdown extends BaseDropdown {
|
||||
this.element.classList.remove('active');
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
override hide(): void {
|
||||
super.hide();
|
||||
|
||||
if (this.contextViewProvider) {
|
||||
@@ -250,7 +250,7 @@ export class DropdownMenu extends BaseDropdown {
|
||||
this._actions = actions;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
override show(): void {
|
||||
super.show();
|
||||
|
||||
this.element.classList.add('active');
|
||||
@@ -269,7 +269,7 @@ export class DropdownMenu extends BaseDropdown {
|
||||
});
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
override hide(): void {
|
||||
super.hide();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dropdown';
|
||||
import { Action, IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { append, $ } from 'vs/base/browser/dom';
|
||||
import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { append, $, addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
|
||||
export interface IKeybindingProvider {
|
||||
(action: IAction): ResolvedKeybinding | undefined;
|
||||
@@ -41,23 +43,26 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
|
||||
protected override readonly options: IDropdownMenuActionViewItemOptions;
|
||||
|
||||
constructor(
|
||||
action: IAction,
|
||||
menuActionsOrProvider: readonly IAction[] | IActionProvider,
|
||||
contextMenuProvider: IContextMenuProvider,
|
||||
protected options: IDropdownMenuActionViewItemOptions = {}
|
||||
options: IDropdownMenuActionViewItemOptions = Object.create(null)
|
||||
) {
|
||||
super(null, action, options);
|
||||
|
||||
this.menuActionsOrProvider = menuActionsOrProvider;
|
||||
this.contextMenuProvider = contextMenuProvider;
|
||||
this.options = options;
|
||||
|
||||
if (this.options.actionRunner) {
|
||||
this.actionRunner = this.options.actionRunner;
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
this.actionItem = container;
|
||||
|
||||
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
|
||||
@@ -78,7 +83,6 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
|
||||
this.element.classList.add(...classNames);
|
||||
|
||||
this.element.tabIndex = 0;
|
||||
this.element.setAttribute('role', 'button');
|
||||
this.element.setAttribute('aria-haspopup', 'true');
|
||||
this.element.setAttribute('aria-expanded', 'false');
|
||||
@@ -123,7 +127,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
this.updateEnabled();
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
override setActionContext(newContext: unknown): void {
|
||||
super.setActionContext(newContext);
|
||||
|
||||
if (this.dropdownMenu) {
|
||||
@@ -141,7 +145,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
protected updateEnabled(): void {
|
||||
protected override updateEnabled(): void {
|
||||
const disabled = !this.getAction().enabled;
|
||||
this.actionItem?.classList.toggle('disabled', disabled);
|
||||
this.element?.classList.toggle('disabled', disabled);
|
||||
@@ -166,19 +170,51 @@ export class ActionWithDropdownActionViewItem extends ActionViewItem {
|
||||
super(context, action, options);
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
if (this.element) {
|
||||
this.element.classList.add('action-dropdown-item');
|
||||
const menuActionsProvider = {
|
||||
getActions: () => {
|
||||
const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;
|
||||
return [this._action, ...(Array.isArray(actionsProvider) ? actionsProvider : actionsProvider.getActions())];
|
||||
return [this._action, ...(Array.isArray(actionsProvider)
|
||||
? actionsProvider
|
||||
: (actionsProvider as IActionProvider).getActions()) // TODO: microsoft/TypeScript#42768
|
||||
];
|
||||
}
|
||||
};
|
||||
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(this._register(new Action('dropdownAction', undefined)), menuActionsProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
|
||||
this.dropdownMenuActionViewItem.render(this.element);
|
||||
|
||||
this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let handled: boolean = false;
|
||||
if (this.dropdownMenuActionViewItem?.isFocused() && event.equals(KeyCode.LeftArrow)) {
|
||||
handled = true;
|
||||
this.dropdownMenuActionViewItem?.blur();
|
||||
this.focus();
|
||||
} else if (this.isFocused() && event.equals(KeyCode.RightArrow)) {
|
||||
handled = true;
|
||||
this.blur();
|
||||
this.dropdownMenuActionViewItem?.focus();
|
||||
}
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
override blur(): void {
|
||||
super.blur();
|
||||
this.dropdownMenuActionViewItem?.blur();
|
||||
}
|
||||
|
||||
override setFocusable(focusable: boolean): void {
|
||||
super.setFocusable(focusable);
|
||||
this.dropdownMenuActionViewItem?.setFocusable(focusable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,7 +382,7 @@ export class ReplaceInput extends Widget {
|
||||
this.domNode.style.width = newWidth + 'px';
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,51 +521,6 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
|
||||
return { type: 'branch', data: node.children.map(c => SerializableGrid.serializeNode(c, orthogonal(orientation))), size };
|
||||
}
|
||||
|
||||
private static deserializeNode<T extends ISerializableView>(json: ISerializedNode, orientation: Orientation, box: Box, deserializer: IViewDeserializer<T>): GridNode<T> {
|
||||
if (!json || typeof json !== 'object') {
|
||||
throw new Error('Invalid JSON');
|
||||
}
|
||||
|
||||
if (json.type === 'branch') {
|
||||
if (!Array.isArray(json.data)) {
|
||||
throw new Error('Invalid JSON: \'data\' property of branch must be an array.');
|
||||
}
|
||||
|
||||
const children: GridNode<T>[] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const child of json.data) {
|
||||
if (typeof child.size !== 'number') {
|
||||
throw new Error('Invalid JSON: \'size\' property of node must be a number.');
|
||||
}
|
||||
|
||||
const childSize = child.type === 'leaf' && child.visible === false ? 0 : child.size;
|
||||
const childBox: Box = orientation === Orientation.HORIZONTAL
|
||||
? { top: box.top, left: box.left + offset, width: childSize, height: box.height }
|
||||
: { top: box.top + offset, left: box.left, width: box.width, height: childSize };
|
||||
|
||||
children.push(SerializableGrid.deserializeNode(child, orthogonal(orientation), childBox, deserializer));
|
||||
offset += childSize;
|
||||
}
|
||||
|
||||
return { children, box };
|
||||
|
||||
} else if (json.type === 'leaf') {
|
||||
const view: T = deserializer.fromJSON(json.data);
|
||||
return { view, box, cachedVisibleSize: json.visible === false ? json.size : undefined };
|
||||
}
|
||||
|
||||
throw new Error('Invalid JSON: \'type\' property must be either \'branch\' or \'leaf\'.');
|
||||
}
|
||||
|
||||
private static getFirstLeaf<T extends IView>(node: GridNode<T>): GridLeafNode<T> {
|
||||
if (!isGridBranchNode(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return SerializableGrid.getFirstLeaf(node.children[0]);
|
||||
}
|
||||
|
||||
static deserialize<T extends ISerializableView>(json: ISerializedGrid, deserializer: IViewDeserializer<T>, options: IGridOptions = {}): SerializableGrid<T> {
|
||||
if (typeof json.orientation !== 'number') {
|
||||
throw new Error('Invalid JSON: \'orientation\' property must be a number.');
|
||||
@@ -596,7 +551,7 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
|
||||
};
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
override layout(width: number, height: number): void {
|
||||
super.layout(width, height);
|
||||
|
||||
if (this.initialLayoutContext) {
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
}
|
||||
|
||||
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr {
|
||||
/* This is a strange rule but it avoids https://github.com/microsoft/vscode/issues/96795, just 100vw on its own caused the actual hover width to increase */
|
||||
min-width: calc(100% + 100vw);
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-hover p,
|
||||
|
||||
@@ -10,6 +10,10 @@ import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableEle
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export const enum HoverPosition {
|
||||
LEFT, RIGHT, BELOW, ABOVE
|
||||
}
|
||||
|
||||
export class HoverWidget extends Disposable {
|
||||
|
||||
public readonly containerDomNode: HTMLElement;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
@@ -15,9 +15,10 @@ export interface IHoverDelegateTarget extends IDisposable {
|
||||
export interface IHoverDelegateOptions {
|
||||
text: IMarkdownString | string;
|
||||
target: IHoverDelegateTarget | HTMLElement;
|
||||
anchorPosition?: AnchorPosition;
|
||||
hoverPosition?: HoverPosition;
|
||||
}
|
||||
|
||||
export interface IHoverDelegate {
|
||||
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@ 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 { isMacintosh } from 'vs/base/common/platform';
|
||||
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
|
||||
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
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';
|
||||
|
||||
export interface IIconLabelCreationOptions {
|
||||
supportHighlights?: boolean;
|
||||
@@ -218,20 +217,23 @@ export class IconLabel extends Disposable {
|
||||
htmlElement.removeAttribute('title');
|
||||
let tooltip = this.getTooltipForCustom(markdownTooltip);
|
||||
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
const hoverDelay = isMacintosh ? 1500 : 500;
|
||||
let hoverOptions: IHoverDelegateOptions | undefined;
|
||||
let mouseX: number | undefined;
|
||||
let isHovering = false;
|
||||
let tokenSource: CancellationTokenSource;
|
||||
function mouseOver(this: HTMLElement, e: MouseEvent): any {
|
||||
let hoverDisposable: IDisposable | undefined;
|
||||
function mouseOver(this: HTMLElement, e: MouseEvent): void {
|
||||
if (isHovering) {
|
||||
return;
|
||||
}
|
||||
tokenSource = new CancellationTokenSource();
|
||||
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any {
|
||||
if ((<any>e).fromElement === htmlElement) {
|
||||
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);
|
||||
@@ -243,7 +245,7 @@ export class IconLabel extends Disposable {
|
||||
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));
|
||||
isHovering = true;
|
||||
|
||||
function mouseMove(this: HTMLElement, e: MouseEvent): any {
|
||||
function mouseMove(this: HTMLElement, e: MouseEvent): void {
|
||||
mouseX = e.x;
|
||||
}
|
||||
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
|
||||
@@ -258,27 +260,28 @@ export class IconLabel extends Disposable {
|
||||
hoverOptions = {
|
||||
text: localize('iconLabel.loading', "Loading..."),
|
||||
target,
|
||||
anchorPosition: AnchorPosition.BELOW
|
||||
hoverPosition: HoverPosition.BELOW
|
||||
};
|
||||
const hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
|
||||
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
|
||||
if (resolvedTooltip) {
|
||||
hoverOptions = {
|
||||
text: resolvedTooltip,
|
||||
target,
|
||||
anchorPosition: AnchorPosition.BELOW
|
||||
hoverPosition: HoverPosition.BELOW
|
||||
};
|
||||
// awaiting the tooltip could take a while. Make sure we're still hovering.
|
||||
IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
} else if (hoverDisposable) {
|
||||
hoverDisposable.dispose();
|
||||
hoverDisposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
mouseMoveDisposable.dispose();
|
||||
}, hoverDelay);
|
||||
}, hoverDelegate.delay);
|
||||
}
|
||||
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
|
||||
this.customHovers.set(htmlElement, mouseOverDisposable);
|
||||
|
||||
@@ -27,6 +27,7 @@ const $ = dom.$;
|
||||
|
||||
export interface IInputOptions extends IInputBoxStyles {
|
||||
readonly placeholder?: string;
|
||||
readonly tooltip?: string;
|
||||
readonly ariaLabel?: string;
|
||||
readonly type?: string;
|
||||
readonly validationOptions?: IInputValidationOptions;
|
||||
@@ -102,6 +103,7 @@ export class InputBox extends Widget {
|
||||
private options: IInputOptions;
|
||||
private message: IMessage | null;
|
||||
private placeholder: string;
|
||||
private tooltip: string;
|
||||
private ariaLabel: string;
|
||||
private validation?: IInputValidator;
|
||||
private state: 'idle' | 'open' | 'closed' = 'idle';
|
||||
@@ -143,6 +145,7 @@ export class InputBox extends Widget {
|
||||
mixin(this.options, defaultOpts, false);
|
||||
this.message = null;
|
||||
this.placeholder = this.options.placeholder || '';
|
||||
this.tooltip = this.options.tooltip ?? (this.placeholder || '');
|
||||
this.ariaLabel = this.options.ariaLabel || '';
|
||||
|
||||
this.inputBackground = this.options.inputBackground;
|
||||
@@ -229,6 +232,10 @@ export class InputBox extends Widget {
|
||||
this.setPlaceHolder(this.placeholder);
|
||||
}
|
||||
|
||||
if (this.tooltip) {
|
||||
this.setTooltip(this.tooltip);
|
||||
}
|
||||
|
||||
this.oninput(this.input, () => this.onValueChange());
|
||||
this.onblur(this.input, () => this.onBlur());
|
||||
this.onfocus(this.input, () => this.onFocus());
|
||||
@@ -259,7 +266,11 @@ export class InputBox extends Widget {
|
||||
public setPlaceHolder(placeHolder: string): void {
|
||||
this.placeholder = placeHolder;
|
||||
this.input.setAttribute('placeholder', placeHolder);
|
||||
this.input.title = placeHolder;
|
||||
}
|
||||
|
||||
public setTooltip(tooltip: string): void {
|
||||
this.tooltip = tooltip;
|
||||
this.input.title = tooltip;
|
||||
}
|
||||
|
||||
public setAriaLabel(label: string): void {
|
||||
@@ -632,7 +643,7 @@ export class InputBox extends Widget {
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
this._hideMessage();
|
||||
|
||||
this.message = null;
|
||||
|
||||
@@ -11,13 +11,10 @@
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key {
|
||||
display: inline-block;
|
||||
border: solid 1px rgba(204, 204, 204, 0.4);
|
||||
border-bottom-color: rgba(187, 187, 187, 0.4);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 rgba(187, 187, 187, 0.4);
|
||||
background-color: rgba(221, 221, 221, 0.4);
|
||||
vertical-align: middle;
|
||||
color: #555;
|
||||
font-size: 11px;
|
||||
padding: 3px 5px;
|
||||
margin: 0 2px;
|
||||
@@ -31,19 +28,10 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.hc-black .monaco-keybinding > .monaco-keybinding-key,
|
||||
.vs-dark .monaco-keybinding > .monaco-keybinding-key {
|
||||
background-color: rgba(128, 128, 128, 0.17);
|
||||
color: #ccc;
|
||||
border: solid 1px rgba(51, 51, 51, 0.6);
|
||||
border-bottom-color: rgba(68, 68, 68, 0.6);
|
||||
box-shadow: inset 0 -1px 0 rgba(68, 68, 68, 0.6);
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key-separator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key-chord-separator {
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCo
|
||||
import { UILabelProvider } from 'vs/base/common/keybindingLabels';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IThemable } from 'vs/base/common/styler';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -26,18 +28,44 @@ export interface Matches {
|
||||
chordPart: PartMatches;
|
||||
}
|
||||
|
||||
export interface KeybindingLabelOptions {
|
||||
renderUnboundKeybindings: boolean;
|
||||
export interface KeybindingLabelOptions extends IKeybindingLabelStyles {
|
||||
renderUnboundKeybindings?: boolean;
|
||||
}
|
||||
|
||||
export class KeybindingLabel {
|
||||
export interface IKeybindingLabelStyles {
|
||||
keybindingLabelBackground?: Color;
|
||||
keybindingLabelForeground?: Color;
|
||||
keybindingLabelBorder?: Color;
|
||||
keybindingLabelBottomBorder?: Color;
|
||||
keybindingLabelShadow?: Color;
|
||||
}
|
||||
|
||||
export class KeybindingLabel implements IThemable {
|
||||
|
||||
private domNode: HTMLElement;
|
||||
private options: KeybindingLabelOptions;
|
||||
|
||||
private readonly keyElements = new Set<HTMLSpanElement>();
|
||||
|
||||
private keybinding: ResolvedKeybinding | undefined;
|
||||
private matches: Matches | undefined;
|
||||
private didEverRender: boolean;
|
||||
|
||||
constructor(container: HTMLElement, private os: OperatingSystem, private options?: KeybindingLabelOptions) {
|
||||
private labelBackground: Color | undefined;
|
||||
private labelForeground: Color | undefined;
|
||||
private labelBorder: Color | undefined;
|
||||
private labelBottomBorder: Color | undefined;
|
||||
private labelShadow: Color | undefined;
|
||||
|
||||
constructor(container: HTMLElement, private os: OperatingSystem, options?: KeybindingLabelOptions) {
|
||||
this.options = options || Object.create(null);
|
||||
|
||||
this.labelBackground = this.options.keybindingLabelBackground;
|
||||
this.labelForeground = this.options.keybindingLabelForeground;
|
||||
this.labelBorder = this.options.keybindingLabelBorder;
|
||||
this.labelBottomBorder = this.options.keybindingLabelBottomBorder;
|
||||
this.labelShadow = this.options.keybindingLabelShadow;
|
||||
|
||||
this.domNode = dom.append(container, $('.monaco-keybinding'));
|
||||
this.didEverRender = false;
|
||||
container.appendChild(this.domNode);
|
||||
@@ -58,7 +86,7 @@ export class KeybindingLabel {
|
||||
}
|
||||
|
||||
private render() {
|
||||
dom.clearNode(this.domNode);
|
||||
this.clear();
|
||||
|
||||
if (this.keybinding) {
|
||||
let [firstPart, chordPart] = this.keybinding.getParts();
|
||||
@@ -74,9 +102,16 @@ export class KeybindingLabel {
|
||||
this.renderUnbound(this.domNode);
|
||||
}
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
this.didEverRender = true;
|
||||
}
|
||||
|
||||
private clear(): void {
|
||||
dom.clearNode(this.domNode);
|
||||
this.keyElements.clear();
|
||||
}
|
||||
|
||||
private renderPart(parent: HTMLElement, part: ResolvedKeybindingPart, match: PartMatches | null) {
|
||||
const modifierLabels = UILabelProvider.modifierLabels[this.os];
|
||||
if (part.ctrlKey) {
|
||||
@@ -98,14 +133,54 @@ export class KeybindingLabel {
|
||||
}
|
||||
|
||||
private renderKey(parent: HTMLElement, label: string, highlight: boolean, separator: string): void {
|
||||
dom.append(parent, $('span.monaco-keybinding-key' + (highlight ? '.highlight' : ''), undefined, label));
|
||||
dom.append(parent, this.createKeyElement(label, highlight ? '.highlight' : ''));
|
||||
if (separator) {
|
||||
dom.append(parent, $('span.monaco-keybinding-key-separator', undefined, separator));
|
||||
}
|
||||
}
|
||||
|
||||
private renderUnbound(parent: HTMLElement): void {
|
||||
dom.append(parent, $('span.monaco-keybinding-key', undefined, localize('unbound', "Unbound")));
|
||||
dom.append(parent, this.createKeyElement(localize('unbound', "Unbound")));
|
||||
}
|
||||
|
||||
private createKeyElement(label: string, extraClass = ''): HTMLElement {
|
||||
const keyElement = $('span.monaco-keybinding-key' + extraClass, undefined, label);
|
||||
this.keyElements.add(keyElement);
|
||||
|
||||
return keyElement;
|
||||
}
|
||||
|
||||
style(styles: IKeybindingLabelStyles): void {
|
||||
this.labelBackground = styles.keybindingLabelBackground;
|
||||
this.labelForeground = styles.keybindingLabelForeground;
|
||||
this.labelBorder = styles.keybindingLabelBorder;
|
||||
this.labelBottomBorder = styles.keybindingLabelBottomBorder;
|
||||
this.labelShadow = styles.keybindingLabelShadow;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private applyStyles() {
|
||||
if (this.element) {
|
||||
for (const keyElement of this.keyElements) {
|
||||
if (this.labelBackground) {
|
||||
keyElement.style.backgroundColor = this.labelBackground?.toString();
|
||||
}
|
||||
if (this.labelBorder) {
|
||||
keyElement.style.borderColor = this.labelBorder.toString();
|
||||
}
|
||||
if (this.labelBottomBorder) {
|
||||
keyElement.style.borderBottomColor = this.labelBottomBorder.toString();
|
||||
}
|
||||
if (this.labelShadow) {
|
||||
keyElement.style.boxShadow = `inset 0 -1px 0 ${this.labelShadow}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.labelForeground) {
|
||||
this.element.style.color = this.labelForeground.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static areSame(a: Matches | undefined, b: Matches | undefined): boolean {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface IListVirtualDelegate<T> {
|
||||
}
|
||||
|
||||
export interface IListRenderer<T, TTemplateData> {
|
||||
templateId: string;
|
||||
readonly templateId: string;
|
||||
renderTemplate(container: HTMLElement): TTemplateData;
|
||||
renderElement(element: T, index: number, templateData: TTemplateData, height: number | undefined): void;
|
||||
disposeElement?(element: T, index: number, templateData: TTemplateData, height: number | undefined): void;
|
||||
|
||||
@@ -226,6 +226,14 @@ export class PagedList<T> implements IThemable, IDisposable {
|
||||
this.list.scrollLeft = scrollLeft;
|
||||
}
|
||||
|
||||
setAnchor(index: number | undefined): void {
|
||||
this.list.setAnchor(index);
|
||||
}
|
||||
|
||||
getAnchor(): number | undefined {
|
||||
return this.list.getAnchor();
|
||||
}
|
||||
|
||||
setFocus(indexes: number[]): void {
|
||||
this.list.setFocus(indexes);
|
||||
}
|
||||
@@ -238,12 +246,20 @@ export class PagedList<T> implements IThemable, IDisposable {
|
||||
this.list.focusPrevious(n, loop);
|
||||
}
|
||||
|
||||
focusNextPage(): void {
|
||||
this.list.focusNextPage();
|
||||
focusNextPage(): Promise<void> {
|
||||
return this.list.focusNextPage();
|
||||
}
|
||||
|
||||
focusPreviousPage(): void {
|
||||
this.list.focusPreviousPage();
|
||||
focusPreviousPage(): Promise<void> {
|
||||
return this.list.focusPreviousPage();
|
||||
}
|
||||
|
||||
focusLast(): void {
|
||||
this.list.focusLast();
|
||||
}
|
||||
|
||||
focusFirst(): void {
|
||||
this.list.focusFirst();
|
||||
}
|
||||
|
||||
getFocus(): number[] {
|
||||
|
||||
@@ -339,7 +339,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
container.appendChild(this.domNode);
|
||||
|
||||
this.scrollableElement.onScroll(this.onScroll, this, this.disposables);
|
||||
domEvent(this.rowsContainer, TouchEventType.Change)(this.onTouchChange, this, this.disposables);
|
||||
domEvent(this.rowsContainer, TouchEventType.Change)(e => this.onTouchChange(e as GestureEvent), this, this.disposables);
|
||||
|
||||
// Prevent the monaco-scrollable-element from scrolling
|
||||
// https://github.com/microsoft/vscode/issues/44181
|
||||
@@ -362,6 +362,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
updateOptions(options: IListViewOptionsUpdate) {
|
||||
if (options.additionalScrollHeight !== undefined) {
|
||||
this.additionalScrollHeight = options.additionalScrollHeight;
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });
|
||||
}
|
||||
|
||||
if (options.smoothScrolling !== undefined) {
|
||||
@@ -407,6 +408,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
this.items[index].size = size;
|
||||
|
||||
this.render(lastRenderRange, Math.max(0, this.lastRenderTop + heightDiff), this.lastRenderHeight, undefined, undefined, true);
|
||||
this.setScrollTop(this.lastRenderTop);
|
||||
|
||||
this.eventuallyUpdateScrollDimensions();
|
||||
|
||||
@@ -436,7 +438,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
const removeRange = Range.intersect(previousRenderRange, deleteRange);
|
||||
|
||||
// try to reuse rows, avoid removing them from DOM
|
||||
const rowsToDispose = new Map<string, [IRow, T, number, number][]>();
|
||||
const rowsToDispose = new Map<string, IRow[]>();
|
||||
for (let i = removeRange.start; i < removeRange.end; i++) {
|
||||
const item = this.items[i];
|
||||
item.dragStartDisposable.dispose();
|
||||
@@ -449,7 +451,13 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
rowsToDispose.set(item.templateId, rows);
|
||||
}
|
||||
|
||||
rows.push([item.row, item.element, i, item.size]);
|
||||
const renderer = this.renderers.get(item.templateId);
|
||||
|
||||
if (renderer && renderer.disposeElement) {
|
||||
renderer.disposeElement(item.element, i, item.row.templateData, item.size);
|
||||
}
|
||||
|
||||
rows.push(item.row);
|
||||
}
|
||||
|
||||
item.row = null;
|
||||
@@ -479,8 +487,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
if (start === 0 && deleteCount >= this.items.length) {
|
||||
this.rangeMap = new RangeMap();
|
||||
this.rangeMap.splice(0, 0, inserted);
|
||||
deleted = this.items;
|
||||
this.items = inserted;
|
||||
deleted = [];
|
||||
} else {
|
||||
this.rangeMap.splice(start, deleteCount, inserted);
|
||||
deleted = this.items.splice(start, deleteCount, ...inserted);
|
||||
@@ -512,31 +520,13 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
for (let i = range.start; i < range.end; i++) {
|
||||
const item = this.items[i];
|
||||
const rows = rowsToDispose.get(item.templateId);
|
||||
const rowData = rows?.pop();
|
||||
|
||||
if (!rowData) {
|
||||
this.insertItemInDOM(i, beforeElement);
|
||||
} else {
|
||||
const [row, element, index, size] = rowData;
|
||||
const renderer = this.renderers.get(item.templateId);
|
||||
|
||||
if (renderer && renderer.disposeElement) {
|
||||
renderer.disposeElement(element, index, row.templateData, size);
|
||||
}
|
||||
|
||||
this.insertItemInDOM(i, beforeElement, row);
|
||||
}
|
||||
const row = rows?.pop();
|
||||
this.insertItemInDOM(i, beforeElement, row);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [templateId, rows] of rowsToDispose) {
|
||||
for (const [row, element, index, size] of rows) {
|
||||
const renderer = this.renderers.get(templateId);
|
||||
|
||||
if (renderer && renderer.disposeElement) {
|
||||
renderer.disposeElement(element, index, row.templateData, size);
|
||||
}
|
||||
|
||||
for (const rows of rowsToDispose.values()) {
|
||||
for (const row of rows) {
|
||||
this.cache.release(row);
|
||||
}
|
||||
}
|
||||
@@ -691,12 +681,12 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
if (this.supportDynamicHeights) {
|
||||
this._rerender(this.scrollTop, this.renderHeight);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.horizontalScrolling) {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
width: typeof width === 'number' ? width : getContentWidth(this.domNode)
|
||||
});
|
||||
}
|
||||
if (this.horizontalScrolling) {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
width: typeof width === 'number' ? width : getContentWidth(this.domNode)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,7 +900,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
@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 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)); }
|
||||
@memoize get onTap(): Event<IListGestureEvent<T>> { return Event.map(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e as GestureEvent)); }
|
||||
|
||||
private toMouseEvent(browserEvent: MouseEvent): IListMouseEvent<T> {
|
||||
const index = this.getItemIndexFromEventTarget(browserEvent.target || null);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import 'vs/css!./list';
|
||||
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
import { range, binarySearch } from 'vs/base/common/arrays';
|
||||
import { range, binarySearch, firstOrDefault } from 'vs/base/common/arrays';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Gesture } from 'vs/base/browser/touch';
|
||||
@@ -27,6 +27,7 @@ import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { IThemable } from 'vs/base/common/styler';
|
||||
import { createStyleSheet } from 'vs/base/browser/dom';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
interface ITraitChangeEvent {
|
||||
indexes: number[];
|
||||
@@ -187,7 +188,7 @@ class SelectionTrait<T> extends Trait<T> {
|
||||
super('selected');
|
||||
}
|
||||
|
||||
renderIndex(index: number, container: HTMLElement): void {
|
||||
override renderIndex(index: number, container: HTMLElement): void {
|
||||
super.renderIndex(index, container);
|
||||
|
||||
if (this.setAriaSelected) {
|
||||
@@ -617,27 +618,25 @@ export class MouseController<T> implements IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
let reference = this.list.getFocus()[0];
|
||||
const selection = this.list.getSelection();
|
||||
reference = reference === undefined ? selection[0] : reference;
|
||||
|
||||
const focus = e.index;
|
||||
|
||||
if (typeof focus === 'undefined') {
|
||||
this.list.setFocus([], e.browserEvent);
|
||||
this.list.setSelection([], e.browserEvent);
|
||||
this.list.setAnchor(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) {
|
||||
return this.changeSelection(e, reference);
|
||||
return this.changeSelection(e);
|
||||
}
|
||||
|
||||
if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) {
|
||||
return this.changeSelection(e, reference);
|
||||
return this.changeSelection(e);
|
||||
}
|
||||
|
||||
this.list.setFocus([focus], e.browserEvent);
|
||||
this.list.setAnchor(focus);
|
||||
|
||||
if (!isMouseRightClick(e.browserEvent)) {
|
||||
this.list.setSelection([focus], e.browserEvent);
|
||||
@@ -659,15 +658,16 @@ export class MouseController<T> implements IDisposable {
|
||||
this.list.setSelection(focus, e.browserEvent);
|
||||
}
|
||||
|
||||
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>, reference: number | undefined): void {
|
||||
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
|
||||
const focus = e.index!;
|
||||
const anchor = this.list.getAnchor();
|
||||
|
||||
if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) {
|
||||
const min = Math.min(reference, focus);
|
||||
const max = Math.max(reference, focus);
|
||||
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);
|
||||
const selection = this.list.getSelection();
|
||||
const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference);
|
||||
const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor);
|
||||
|
||||
if (contiguousRange.length === 0) {
|
||||
return;
|
||||
@@ -675,12 +675,14 @@ export class MouseController<T> implements IDisposable {
|
||||
|
||||
const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));
|
||||
this.list.setSelection(newSelection, e.browserEvent);
|
||||
this.list.setFocus([focus], e.browserEvent);
|
||||
|
||||
} else if (this.isSelectionSingleChangeEvent(e)) {
|
||||
const selection = this.list.getSelection();
|
||||
const newSelection = selection.filter(i => i !== focus);
|
||||
|
||||
this.list.setFocus([focus]);
|
||||
this.list.setAnchor(focus);
|
||||
|
||||
if (selection.length === newSelection.length) {
|
||||
this.list.setSelection([...newSelection, focus], e.browserEvent);
|
||||
@@ -761,6 +763,11 @@ export class DefaultStyleController implements IStyleController {
|
||||
`);
|
||||
}
|
||||
|
||||
if (styles.listInactiveFocusForeground) {
|
||||
content.push(`.monaco-list${suffix} .monaco-list-row.focused { color: ${styles.listInactiveFocusForeground}; }`);
|
||||
content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { color: ${styles.listInactiveFocusForeground}; }`); // overwrite :hover style in this case!
|
||||
}
|
||||
|
||||
if (styles.listInactiveFocusBackground) {
|
||||
content.push(`.monaco-list${suffix} .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`);
|
||||
content.push(`.monaco-list${suffix} .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
|
||||
@@ -776,7 +783,7 @@ export class DefaultStyleController implements IStyleController {
|
||||
}
|
||||
|
||||
if (styles.listHoverBackground) {
|
||||
content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
|
||||
content.push(`.monaco-list${suffix}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
|
||||
}
|
||||
|
||||
if (styles.listHoverForeground) {
|
||||
@@ -826,6 +833,14 @@ export class DefaultStyleController implements IStyleController {
|
||||
content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`);
|
||||
}
|
||||
|
||||
if (styles.tableColumnsBorder) {
|
||||
content.push(`
|
||||
.monaco-table:hover > .monaco-split-view2,
|
||||
.monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
border-color: ${styles.tableColumnsBorder};
|
||||
}`);
|
||||
}
|
||||
|
||||
this.styleElement.textContent = content.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -867,6 +882,7 @@ export interface IListStyles {
|
||||
listFocusAndSelectionForeground?: Color;
|
||||
listInactiveSelectionBackground?: Color;
|
||||
listInactiveSelectionForeground?: Color;
|
||||
listInactiveFocusForeground?: Color;
|
||||
listInactiveFocusBackground?: Color;
|
||||
listHoverBackground?: Color;
|
||||
listHoverForeground?: Color;
|
||||
@@ -880,6 +896,7 @@ export interface IListStyles {
|
||||
listFilterWidgetNoMatchesOutline?: Color;
|
||||
listMatchesShadow?: Color;
|
||||
treeIndentGuidesStroke?: Color;
|
||||
tableColumnsBorder?: Color;
|
||||
}
|
||||
|
||||
const defaultStyles: IListStyles = {
|
||||
@@ -891,7 +908,8 @@ const defaultStyles: IListStyles = {
|
||||
listInactiveSelectionBackground: Color.fromHex('#3F3F46'),
|
||||
listHoverBackground: Color.fromHex('#2A2D2E'),
|
||||
listDropBackground: Color.fromHex('#383B3D'),
|
||||
treeIndentGuidesStroke: Color.fromHex('#a9a9a9')
|
||||
treeIndentGuidesStroke: Color.fromHex('#a9a9a9'),
|
||||
tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2)
|
||||
};
|
||||
|
||||
const DefaultOptions: IListOptions<any> = {
|
||||
@@ -1114,8 +1132,9 @@ export interface IListOptionsUpdate extends IListViewOptionsUpdate {
|
||||
|
||||
export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
|
||||
private focus: Trait<T>;
|
||||
private focus = new Trait<T>('focused');
|
||||
private selection: Trait<T>;
|
||||
private anchor = new Trait<T>('anchor');
|
||||
private eventBufferer = new EventBufferer();
|
||||
protected view: ListView<T>;
|
||||
private spliceable: ISpliceable<T>;
|
||||
@@ -1149,8 +1168,25 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }
|
||||
get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }
|
||||
|
||||
/**
|
||||
* Possible context menu trigger events:
|
||||
* - ContextMenu key
|
||||
* - Shift F10
|
||||
* - Ctrl Option Shift M (macOS with VoiceOver)
|
||||
* - Mouse right click
|
||||
*/
|
||||
@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
|
||||
const fromKeyboard = Event.chain(domEvent(this.view.domNode, 'keyup'))
|
||||
let didJustPressContextMenuKey = false;
|
||||
|
||||
const fromKeyDown = Event.chain(domEvent(this.view.domNode, 'keydown'))
|
||||
.map(e => new StandardKeyboardEvent(e))
|
||||
.filter(e => didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
|
||||
.map(stopEvent)
|
||||
.filter(() => false)
|
||||
.event as Event<any>;
|
||||
|
||||
const fromKeyUp = Event.chain(domEvent(this.view.domNode, 'keyup'))
|
||||
.forEach(() => didJustPressContextMenuKey = false)
|
||||
.map(e => new StandardKeyboardEvent(e))
|
||||
.filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
|
||||
.map(stopEvent)
|
||||
@@ -1164,11 +1200,11 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
.event;
|
||||
|
||||
const fromMouse = Event.chain(this.view.onContextMenu)
|
||||
.filter(e => !(e.browserEvent.button === 0 && e.browserEvent.buttons === 0 && !e.browserEvent.ctrlKey && !e.browserEvent.altKey && !e.browserEvent.metaKey))
|
||||
.filter(_ => !didJustPressContextMenuKey)
|
||||
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
|
||||
.event;
|
||||
|
||||
return Event.any<IListContextMenuEvent<T>>(fromKeyboard, fromMouse);
|
||||
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);
|
||||
}
|
||||
|
||||
get onKeyDown(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keydown'); }
|
||||
@@ -1190,7 +1226,6 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
) {
|
||||
const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list';
|
||||
this.selection = new SelectionTrait(role !== 'listbox');
|
||||
this.focus = new Trait('focused');
|
||||
|
||||
mixin(_options, defaultStyles, false);
|
||||
|
||||
@@ -1226,11 +1261,13 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
this.spliceable = new CombinedSpliceable([
|
||||
new TraitSpliceable(this.focus, this.view, _options.identityProvider),
|
||||
new TraitSpliceable(this.selection, this.view, _options.identityProvider),
|
||||
new TraitSpliceable(this.anchor, this.view, _options.identityProvider),
|
||||
this.view
|
||||
]);
|
||||
|
||||
this.disposables.add(this.focus);
|
||||
this.disposables.add(this.selection);
|
||||
this.disposables.add(this.anchor);
|
||||
this.disposables.add(this.view);
|
||||
this.disposables.add(this._onDidDispose);
|
||||
|
||||
@@ -1403,6 +1440,23 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
return this.getSelection().map(i => this.view.element(i));
|
||||
}
|
||||
|
||||
setAnchor(index: number | undefined): void {
|
||||
if (typeof index === 'undefined') {
|
||||
this.anchor.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= this.length) {
|
||||
throw new ListError(this.user, `Invalid index ${index}`);
|
||||
}
|
||||
|
||||
this.anchor.set([index]);
|
||||
}
|
||||
|
||||
getAnchor(): number | undefined {
|
||||
return firstOrDefault(this.anchor.get(), undefined);
|
||||
}
|
||||
|
||||
setFocus(indexes: number[], browserEvent?: UIEvent): void {
|
||||
for (const index of indexes) {
|
||||
if (index < 0 || index >= this.length) {
|
||||
@@ -1435,7 +1489,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
|
||||
async focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise<void> {
|
||||
let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);
|
||||
lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
|
||||
const lastPageElement = this.view.element(lastPageIndex);
|
||||
@@ -1457,12 +1511,13 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
this.setFocus([]);
|
||||
|
||||
// Let the scroll event listener run
|
||||
setTimeout(() => this.focusNextPage(browserEvent, filter), 0);
|
||||
await timeout(0);
|
||||
await this.focusNextPage(browserEvent, filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): void {
|
||||
async focusPreviousPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise<void> {
|
||||
let firstPageIndex: number;
|
||||
const scrollTop = this.view.getScrollTop();
|
||||
|
||||
@@ -1491,7 +1546,8 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
this.setFocus([]);
|
||||
|
||||
// Let the scroll event listener run
|
||||
setTimeout(() => this.focusPreviousPage(browserEvent, filter), 0);
|
||||
await timeout(0);
|
||||
await this.focusPreviousPage(browserEvent, filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1579,13 +1635,13 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop);
|
||||
} else {
|
||||
const viewItemBottom = elementTop + elementHeight;
|
||||
const wrapperBottom = scrollTop + this.view.renderHeight;
|
||||
const scrollBottom = scrollTop + this.view.renderHeight;
|
||||
|
||||
if (elementTop < scrollTop && viewItemBottom >= wrapperBottom) {
|
||||
if (elementTop < scrollTop && viewItemBottom >= scrollBottom) {
|
||||
// The element is already overflowing the viewport, no-op
|
||||
} else if (elementTop < scrollTop) {
|
||||
} else if (elementTop < scrollTop || (viewItemBottom >= scrollBottom && elementHeight >= this.view.renderHeight)) {
|
||||
this.view.setScrollTop(elementTop);
|
||||
} else if (viewItemBottom >= wrapperBottom) {
|
||||
} else if (viewItemBottom >= scrollBottom) {
|
||||
this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider, EmptySubmenuAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IActionRunner, IAction, SubmenuAction, Separator, EmptySubmenuAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, addDisposableListener, append, $, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { EventType, EventHelper, EventLike, isAncestor, addDisposableListener, append, $, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
@@ -37,7 +37,7 @@ export enum Direction {
|
||||
}
|
||||
|
||||
export interface IMenuOptions {
|
||||
context?: any;
|
||||
context?: unknown;
|
||||
actionViewItemProvider?: IActionViewItemProvider;
|
||||
actionRunner?: IActionRunner;
|
||||
getKeyBinding?: (action: IAction) => ResolvedKeybinding | undefined;
|
||||
@@ -86,6 +86,7 @@ export class Menu extends ActionBar {
|
||||
context: options.context,
|
||||
actionRunner: options.actionRunner,
|
||||
ariaLabel: options.ariaLabel,
|
||||
focusOnlyEnabledItems: true,
|
||||
triggerKeys: { keys: [KeyCode.Enter, ...(isMacintosh || isLinux ? [KeyCode.Space] : [])], keyDown: true }
|
||||
});
|
||||
|
||||
@@ -263,7 +264,7 @@ export class Menu extends ActionBar {
|
||||
}
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
override getContainer(): HTMLElement {
|
||||
return this.scrollableElement.getDomNode();
|
||||
}
|
||||
|
||||
@@ -308,7 +309,7 @@ export class Menu extends ActionBar {
|
||||
}
|
||||
}
|
||||
|
||||
protected updateFocus(fromRight?: boolean): void {
|
||||
protected override updateFocus(fromRight?: boolean): void {
|
||||
super.updateFocus(fromRight, true);
|
||||
|
||||
if (typeof this.focusedItem !== 'undefined') {
|
||||
@@ -384,7 +385,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
|
||||
public container: HTMLElement | undefined;
|
||||
|
||||
protected options: IMenuItemOptions;
|
||||
protected override options: IMenuItemOptions;
|
||||
protected item: HTMLElement | undefined;
|
||||
|
||||
private runOnceToEnableMouseUp: RunOnceScheduler;
|
||||
@@ -464,7 +465,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
this._register(this.runOnceToEnableMouseUp);
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
if (!this.element) {
|
||||
@@ -503,12 +504,12 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
override blur(): void {
|
||||
super.blur();
|
||||
this.applyStyle();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
override focus(): void {
|
||||
super.focus();
|
||||
|
||||
if (this.item) {
|
||||
@@ -525,7 +526,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
override updateLabel(): void {
|
||||
if (!this.label) {
|
||||
return;
|
||||
}
|
||||
@@ -578,7 +579,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltip(): void {
|
||||
override updateTooltip(): void {
|
||||
let title: string | null = null;
|
||||
|
||||
if (this.getAction().tooltip) {
|
||||
@@ -597,7 +598,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateClass(): void {
|
||||
override updateClass(): void {
|
||||
if (this.cssClass && this.item) {
|
||||
this.item.classList.remove(...this.cssClass.split(' '));
|
||||
}
|
||||
@@ -613,29 +614,32 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
override updateEnabled(): void {
|
||||
if (this.getAction().enabled) {
|
||||
if (this.element) {
|
||||
this.element.classList.remove('disabled');
|
||||
this.element.removeAttribute('aria-disabled');
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
this.item.classList.remove('disabled');
|
||||
this.item.removeAttribute('aria-disabled');
|
||||
this.item.tabIndex = 0;
|
||||
}
|
||||
} else {
|
||||
if (this.element) {
|
||||
this.element.classList.add('disabled');
|
||||
this.element.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
this.item.classList.add('disabled');
|
||||
removeTabIndexAndUpdateFocus(this.item);
|
||||
this.item.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
override updateChecked(): void {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
@@ -720,7 +724,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}, 750);
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
if (!this.element) {
|
||||
@@ -779,7 +783,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}));
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
override updateEnabled(): void {
|
||||
// override on submenu entry
|
||||
// native menus do not observe enablement on sumbenus
|
||||
// we mimic that behavior
|
||||
@@ -790,7 +794,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
this.createSubmenu(selectFirst);
|
||||
}
|
||||
|
||||
onClick(e: EventLike): void {
|
||||
override onClick(e: EventLike): void {
|
||||
// stop clicking from trying to run an action
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
@@ -921,7 +925,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
protected applyStyle(): void {
|
||||
protected override applyStyle(): void {
|
||||
super.applyStyle();
|
||||
|
||||
if (!this.menuStyle) {
|
||||
@@ -940,7 +944,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.hideScheduler.dispose();
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
opacity: 1;
|
||||
z-index: 2000;
|
||||
@@ -85,3 +85,11 @@
|
||||
.menubar.compact .toolbar-toggle-more::before {
|
||||
content: "\eb94" !important;
|
||||
}
|
||||
|
||||
/* Match behavior of outline for activity bar icons */
|
||||
.menubar.compact > .menubar-menu-button.open,
|
||||
.menubar.compact > .menubar-menu-button:focus,
|
||||
.menubar.compact > .menubar-menu-button:hover {
|
||||
outline-width: 1px !important;
|
||||
outline-offset: -8px !important;
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ export class MenuBar extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
@@ -746,7 +746,7 @@ export class MenuBar extends Disposable {
|
||||
private setUnfocusedState(): void {
|
||||
if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {
|
||||
this.focusState = MenubarState.HIDDEN;
|
||||
} else if (this.options.visibility === 'default' && browser.isFullscreen()) {
|
||||
} else if (this.options.visibility === 'classic' && browser.isFullscreen()) {
|
||||
this.focusState = MenubarState.HIDDEN;
|
||||
} else {
|
||||
this.focusState = MenubarState.VISIBLE;
|
||||
@@ -838,6 +838,22 @@ export class MenuBar extends Disposable {
|
||||
this._mnemonicsInUse = value;
|
||||
}
|
||||
|
||||
private get shouldAltKeyFocus(): boolean {
|
||||
if (isMacintosh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.options.disableAltFocus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.options.visibility === 'toggle') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public get onVisibilityChange(): Event<boolean> {
|
||||
return this._onVisibilityChange.event;
|
||||
}
|
||||
@@ -869,7 +885,7 @@ export class MenuBar extends Disposable {
|
||||
}
|
||||
|
||||
// Prevent alt-key default if the menu is not hidden and we use alt to focus
|
||||
if (modifierKeyStatus.event && !this.options.disableAltFocus) {
|
||||
if (modifierKeyStatus.event && this.shouldAltKeyFocus) {
|
||||
if (ScanCodeUtils.toEnum(modifierKeyStatus.event.code) === ScanCode.AltLeft) {
|
||||
modifierKeyStatus.event.preventDefault();
|
||||
}
|
||||
@@ -885,7 +901,7 @@ export class MenuBar extends Disposable {
|
||||
// Clean alt key press and release
|
||||
if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') {
|
||||
if (!this.awaitingAltRelease) {
|
||||
if (!this.isFocused && !(this.options.disableAltFocus && this.options.visibility !== 'toggle')) {
|
||||
if (!this.isFocused && this.shouldAltKeyFocus) {
|
||||
this.mnemonicsInUse = true;
|
||||
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
|
||||
this.focusState = MenubarState.FOCUSED;
|
||||
@@ -960,7 +976,7 @@ export class MenuBar extends Disposable {
|
||||
menuHolder.style.right = `${this.container.clientWidth}px`;
|
||||
menuHolder.style.left = 'auto';
|
||||
} else {
|
||||
menuHolder.style.top = `${this.container.clientHeight}px`;
|
||||
menuHolder.style.top = `${buttonBoundingRect.bottom}px`;
|
||||
menuHolder.style.left = `${buttonBoundingRect.left}px`;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
height: var(--sash-size);
|
||||
}
|
||||
|
||||
.monaco-sash:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash:not(.disabled).orthogonal-end::after {
|
||||
.monaco-sash:not(.disabled) > .orthogonal-drag-handle {
|
||||
content: " ";
|
||||
height: calc(var(--sash-size) * 2);
|
||||
width: calc(var(--sash-size) * 2);
|
||||
@@ -71,38 +70,57 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled).orthogonal-end::after {
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
|
||||
> .orthogonal-drag-handle.start,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
|
||||
> .orthogonal-drag-handle.end {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled).orthogonal-end::after,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled).orthogonal-start::before {
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
|
||||
> .orthogonal-drag-handle.end,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
|
||||
> .orthogonal-drag-handle.start {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.orthogonal-start.vertical::before {
|
||||
left: -calc(var(--sash-size) / 2);
|
||||
.monaco-sash.vertical > .orthogonal-drag-handle.start {
|
||||
left: calc(var(--sash-size) * -0.5);
|
||||
top: calc(var(--sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.orthogonal-end.vertical::after {
|
||||
left: -calc(var(--sash-size) / 2);
|
||||
.monaco-sash.vertical > .orthogonal-drag-handle.end {
|
||||
left: calc(var(--sash-size) * -0.5);
|
||||
bottom: calc(var(--sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.orthogonal-start.horizontal::before {
|
||||
top: -calc(var(--sash-size) / 2);
|
||||
.monaco-sash.horizontal > .orthogonal-drag-handle.start {
|
||||
top: calc(var(--sash-size) * -0.5);
|
||||
left: calc(var(--sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.orthogonal-end.horizontal::after {
|
||||
top: -calc(var(--sash-size) / 2);
|
||||
.monaco-sash.horizontal > .orthogonal-drag-handle.end {
|
||||
top: calc(var(--sash-size) * -0.5);
|
||||
right: calc(var(--sash-size) * -1);
|
||||
}
|
||||
|
||||
.monaco-sash {
|
||||
.monaco-sash:before {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: background-color 0.1s ease-out;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical:before {
|
||||
width: var(--sash-hover-size);
|
||||
left: calc(50% - (var(--sash-hover-size) / 2));
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal:before {
|
||||
height: var(--sash-hover-size);
|
||||
top: calc(50% - (var(--sash-hover-size) / 2));
|
||||
}
|
||||
|
||||
/** Debug **/
|
||||
|
||||
.monaco-sash.debug {
|
||||
@@ -113,7 +131,6 @@
|
||||
background: rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.monaco-sash.debug:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash.debug:not(.disabled).orthogonal-end::after {
|
||||
.monaco-sash.debug:not(.disabled) > .orthogonal-drag-handle {
|
||||
background: red;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./sash';
|
||||
import { IDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, dispose, 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';
|
||||
@@ -12,8 +12,10 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
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 { Delayer } from 'vs/base/common/async';
|
||||
|
||||
const DEBUG = false;
|
||||
let DEBUG = false;
|
||||
// DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
|
||||
|
||||
export interface ISashLayoutProvider { }
|
||||
|
||||
@@ -79,6 +81,13 @@ export function setGlobalSashSize(size: number): void {
|
||||
onDidChangeGlobalSize.fire(size);
|
||||
}
|
||||
|
||||
let globalHoverDelay = 300;
|
||||
const onDidChangeHoverDelay = new Emitter<number>();
|
||||
export function setGlobalHoverDelay(size: number): void {
|
||||
globalHoverDelay = size;
|
||||
onDidChangeHoverDelay.fire(size);
|
||||
}
|
||||
|
||||
export class Sash extends Disposable {
|
||||
|
||||
private el: HTMLElement;
|
||||
@@ -86,6 +95,8 @@ export class Sash extends Disposable {
|
||||
private hidden: boolean;
|
||||
private orientation!: Orientation;
|
||||
private size: number;
|
||||
private hoverDelay = globalHoverDelay;
|
||||
private hoverDelayer = this._register(new Delayer(this.hoverDelay));
|
||||
|
||||
private _state: SashState = SashState.Enabled;
|
||||
get state(): SashState { return this._state; }
|
||||
@@ -121,15 +132,29 @@ export class Sash extends Disposable {
|
||||
|
||||
private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalStartSash: Sash | undefined;
|
||||
private readonly orthogonalStartDragHandleDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalStartDragHandle: HTMLElement | undefined;
|
||||
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
|
||||
set orthogonalStartSash(sash: Sash | undefined) {
|
||||
this.orthogonalStartDragHandleDisposables.clear();
|
||||
this.orthogonalStartSashDisposables.clear();
|
||||
|
||||
if (sash) {
|
||||
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalStartSashEnablementChange, this));
|
||||
this.onOrthogonalStartSashEnablementChange(sash.state);
|
||||
} else {
|
||||
this.onOrthogonalStartSashEnablementChange(SashState.Disabled);
|
||||
const onChange = (state: SashState) => {
|
||||
this.orthogonalStartDragHandleDisposables.clear();
|
||||
|
||||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
|
||||
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseenter')
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseleave')
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
|
||||
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange(onChange, this));
|
||||
onChange(sash.state);
|
||||
}
|
||||
|
||||
this._orthogonalStartSash = sash;
|
||||
@@ -137,15 +162,29 @@ export class Sash extends Disposable {
|
||||
|
||||
private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalEndSash: Sash | undefined;
|
||||
private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalEndDragHandle: HTMLElement | undefined;
|
||||
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
|
||||
set orthogonalEndSash(sash: Sash | undefined) {
|
||||
this.orthogonalEndDragHandleDisposables.clear();
|
||||
this.orthogonalEndSashDisposables.clear();
|
||||
|
||||
if (sash) {
|
||||
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalEndSashEnablementChange, this));
|
||||
this.onOrthogonalEndSashEnablementChange(sash.state);
|
||||
} else {
|
||||
this.onOrthogonalEndSashEnablementChange(SashState.Disabled);
|
||||
const onChange = (state: SashState) => {
|
||||
this.orthogonalEndDragHandleDisposables.clear();
|
||||
|
||||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
|
||||
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseenter')
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseleave')
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
|
||||
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange(onChange, this));
|
||||
onChange(sash.state);
|
||||
}
|
||||
|
||||
this._orthogonalEndSash = sash;
|
||||
@@ -168,9 +207,11 @@ export class Sash extends Disposable {
|
||||
|
||||
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)));
|
||||
|
||||
this._register(Gesture.addTarget(this.el));
|
||||
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
|
||||
this._register(domEvent(this.el, EventType.Start)(e => this.onTouchStart(e as GestureEvent), this));
|
||||
|
||||
if (typeof options.size === 'number') {
|
||||
this.size = options.size;
|
||||
@@ -188,6 +229,8 @@ export class Sash extends Disposable {
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(onDidChangeHoverDelay.event(delay => this.hoverDelay = delay));
|
||||
|
||||
this.hidden = false;
|
||||
this.layoutProvider = layoutProvider;
|
||||
|
||||
@@ -359,12 +402,38 @@ export class Sash extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
listeners.push(addDisposableListener(this.el, EventType.End, (event: GestureEvent) => {
|
||||
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();
|
||||
sash.el.classList.add('hover');
|
||||
} else {
|
||||
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { });
|
||||
}
|
||||
|
||||
if (!fromLinkedSash && sash.linkedSash) {
|
||||
Sash.onMouseEnter(sash.linkedSash, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static onMouseLeave(sash: Sash, fromLinkedSash: boolean = false): void {
|
||||
sash.hoverDelayer.cancel();
|
||||
sash.el.classList.remove('hover');
|
||||
|
||||
if (!fromLinkedSash && sash.linkedSash) {
|
||||
Sash.onMouseLeave(sash.linkedSash, true);
|
||||
}
|
||||
}
|
||||
|
||||
clearSashHoverState(): void {
|
||||
Sash.onMouseLeave(this);
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
|
||||
@@ -407,33 +476,19 @@ export class Sash extends Disposable {
|
||||
return this.hidden;
|
||||
}
|
||||
|
||||
private onOrthogonalStartSashEnablementChange(state: SashState): void {
|
||||
this.el.classList.toggle('orthogonal-start', state !== SashState.Disabled);
|
||||
}
|
||||
|
||||
private onOrthogonalEndSashEnablementChange(state: SashState): void {
|
||||
this.el.classList.toggle('orthogonal-end', state !== SashState.Disabled);
|
||||
}
|
||||
|
||||
private getOrthogonalSash(e: MouseEvent): Sash | undefined {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
if (e.offsetY <= this.size) {
|
||||
return this.orthogonalStartSash;
|
||||
} else if (e.offsetY >= this.el.clientHeight - this.size) {
|
||||
return this.orthogonalEndSash;
|
||||
}
|
||||
} else {
|
||||
if (e.offsetX <= this.size) {
|
||||
return this.orthogonalStartSash;
|
||||
} else if (e.offsetX >= this.el.clientWidth - this.size) {
|
||||
return this.orthogonalEndSash;
|
||||
}
|
||||
if (!e.target || !(e.target instanceof HTMLElement)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('orthogonal-drag-handle')) {
|
||||
return e.target.classList.contains('start') ? this.orthogonalStartSash : this.orthogonalEndSash;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.el.remove();
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
this._domNode.appendChild(this._topShadowDomNode.domNode);
|
||||
|
||||
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._topLeftShadowDomNode.setClassName('shadow top-left-corner');
|
||||
this._topLeftShadowDomNode.setClassName('shadow');
|
||||
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
|
||||
} else {
|
||||
this._leftShadowDomNode = null;
|
||||
@@ -239,7 +239,7 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
this._revealOnScroll = true;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -484,9 +484,12 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
const enableTop = scrollState.scrollTop > 0;
|
||||
const enableLeft = scrollState.scrollLeft > 0;
|
||||
|
||||
this._leftShadowDomNode!.setClassName('shadow' + (enableLeft ? ' left' : ''));
|
||||
this._topShadowDomNode!.setClassName('shadow' + (enableTop ? ' top' : ''));
|
||||
this._topLeftShadowDomNode!.setClassName('shadow top-left-corner' + (enableTop ? ' top' : '') + (enableLeft ? ' left' : ''));
|
||||
const leftClassName = (enableLeft ? ' left' : '');
|
||||
const topClassName = (enableTop ? ' top' : '');
|
||||
const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
|
||||
this._leftShadowDomNode!.setClassName(`shadow${leftClassName}`);
|
||||
this._topShadowDomNode!.setClassName(`shadow${topClassName}`);
|
||||
this._topLeftShadowDomNode!.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ISelectBoxDelegate extends IDisposable {
|
||||
setAriaLabel(label: string): void;
|
||||
focus(): void;
|
||||
blur(): void;
|
||||
setFocusable(focus: boolean): void;
|
||||
|
||||
// Delegated Widget interface
|
||||
render(container: HTMLElement): void;
|
||||
@@ -100,41 +101,43 @@ export class SelectBox extends Widget implements ISelectBoxDelegate {
|
||||
|
||||
// Public SelectBox Methods - routed through delegate interface
|
||||
|
||||
public get onDidSelect(): Event<ISelectData> {
|
||||
get onDidSelect(): Event<ISelectData> {
|
||||
return this.selectBoxDelegate.onDidSelect;
|
||||
}
|
||||
|
||||
public setOptions(options: ISelectOptionItem[], selected?: number): void {
|
||||
setOptions(options: ISelectOptionItem[], selected?: number): void {
|
||||
this.selectBoxDelegate.setOptions(options, selected);
|
||||
}
|
||||
|
||||
public select(index: number): void {
|
||||
select(index: number): void {
|
||||
this.selectBoxDelegate.select(index);
|
||||
}
|
||||
|
||||
public setAriaLabel(label: string): void {
|
||||
setAriaLabel(label: string): void {
|
||||
this.selectBoxDelegate.setAriaLabel(label);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
focus(): void {
|
||||
this.selectBoxDelegate.focus();
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
blur(): void {
|
||||
this.selectBoxDelegate.blur();
|
||||
}
|
||||
|
||||
// Public Widget Methods - routed through delegate interface
|
||||
setFocusable(focusable: boolean): void {
|
||||
this.selectBoxDelegate.setFocusable(focusable);
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
render(container: HTMLElement): void {
|
||||
this.selectBoxDelegate.render(container);
|
||||
}
|
||||
|
||||
public style(styles: ISelectBoxStyles): void {
|
||||
style(styles: ISelectBoxStyles): void {
|
||||
this.selectBoxDelegate.style(styles);
|
||||
}
|
||||
|
||||
public applyStyles(): void {
|
||||
applyStyles(): void {
|
||||
this.selectBoxDelegate.applyStyles();
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,9 @@ class SelectListRenderer implements IListRenderer<ISelectOptionItem, ISelectList
|
||||
const isDisabled = element.isDisabled;
|
||||
|
||||
data.text.textContent = text;
|
||||
data.text.setAttribute('aria-label', text); // {{SQL CARBON EDIT}}
|
||||
data.detail.textContent = !!detail ? detail : '';
|
||||
data.decoratorRight.innerText = (!!decoratorRight ? decoratorRight : '');
|
||||
// {{SQL CARBON EDIT}}
|
||||
data.text.setAttribute('aria-label', text);
|
||||
data.decoratorRight.innerText = !!decoratorRight ? decoratorRight : '';
|
||||
|
||||
// pseudo-select disabled option
|
||||
if (isDisabled) {
|
||||
@@ -295,16 +294,22 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
|
||||
public focus(): void {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = 0;
|
||||
this.selectElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = -1;
|
||||
this.selectElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
public setFocusable(focusable: boolean): void {
|
||||
this.selectElement.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
container.classList.add('select-container');
|
||||
@@ -689,8 +694,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
}
|
||||
});
|
||||
|
||||
container.innerHTML = this.options[longest]?.text + (!!this.options[longest]?.decoratorRight ? (this.options[longest].decoratorRight + ' ') : ''); // {{SQL CARBON EDIT}} Don't error if no option found (empty list)
|
||||
|
||||
container.textContent = this.options[longest]?.text + (!!this.options[longest]?.decoratorRight ? (this.options[longest].decoratorRight + ' ') : ''); // {{SQL CARBON EDIT}} Don't error if no option found (empty list)
|
||||
elementWidth = dom.getTotalWidth(container);
|
||||
}
|
||||
|
||||
@@ -747,8 +752,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(e => this.onEnter(e), this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(e => this.onEscape(e), this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(e => this.onUpArrow(e), this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(e => this.onDownArrow(e), this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDown, this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUp, this));
|
||||
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Home).on(this.onHome, this));
|
||||
@@ -815,8 +820,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
// Set current = selected
|
||||
this._currentSelection = this.selected;
|
||||
|
||||
// {{SQL CARBON EDIT}} - Update the selection before firing the handler instead of after
|
||||
this.hideSelectDropDown(true);
|
||||
this.hideSelectDropDown(true); // {{SQL CARBON EDIT}} - Update the selection before firing the handler instead of after
|
||||
|
||||
this._onDidSelect.fire({
|
||||
index: this.selectElement.selectedIndex,
|
||||
@@ -827,6 +831,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
this.selectElement.title = this.options[this.selected].text;
|
||||
}
|
||||
}
|
||||
|
||||
// this.hideSelectDropDown(true); // {{SQL CARBON EDIT}} Moved up
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,8 +882,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
|
||||
private updateDetail(selectedIndex: number): void {
|
||||
this.selectionDetailsPane.innerText = '';
|
||||
const description = this.options[selectedIndex]?.description;
|
||||
const descriptionIsMarkdown = this.options[selectedIndex]?.descriptionIsMarkdown;
|
||||
const description = this.options[selectedIndex]?.description; // {{SQL CARBON EDIT}} Handle undefined options
|
||||
const descriptionIsMarkdown = this.options[selectedIndex]?.descriptionIsMarkdown; // {{SQL CARBON EDIT}} Handle undefined options
|
||||
|
||||
if (description) {
|
||||
if (descriptionIsMarkdown) {
|
||||
@@ -928,8 +934,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
}
|
||||
|
||||
// List navigation - have to handle a disabled option (jump over)
|
||||
private onDownArrow(): void {
|
||||
private onDownArrow(e: StandardKeyboardEvent): void {
|
||||
if (this.selected < this.options.length - 1) {
|
||||
dom.EventHelper.stop(e, true);
|
||||
|
||||
// Skip disabled options
|
||||
const nextOptionDisabled = this.options[this.selected + 1].isDisabled;
|
||||
@@ -949,8 +956,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
}
|
||||
}
|
||||
|
||||
private onUpArrow(): void {
|
||||
private onUpArrow(e: StandardKeyboardEvent): void {
|
||||
if (this.selected > 0) {
|
||||
dom.EventHelper.stop(e, true);
|
||||
// Skip disabled options
|
||||
const previousOptionDisabled = this.options[this.selected - 1].isDisabled;
|
||||
if (previousOptionDisabled && this.selected > 1) {
|
||||
@@ -1050,7 +1058,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
this.hideSelectDropDown(false);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -133,16 +133,22 @@ export class SelectBoxNative extends Disposable implements ISelectBoxDelegate {
|
||||
|
||||
public focus(): void {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = 0;
|
||||
this.selectElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = -1;
|
||||
this.selectElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
public setFocusable(focusable: boolean): void {
|
||||
this.selectElement.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
container.classList.add('select-container');
|
||||
container.appendChild(this.selectElement);
|
||||
|
||||
@@ -52,26 +52,20 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* TODO: actions should be part of the pane, but they aren't yet */
|
||||
.monaco-pane-view .pane:hover > .pane-header.expanded > .actions,
|
||||
.monaco-pane-view .pane > .pane-header.actions-always-visible.expanded > .actions,
|
||||
.monaco-pane-view .pane > .pane-header.focused.expanded > .actions {
|
||||
display: initial;
|
||||
.monaco-pane-view .pane > .pane-header > .actions .action-item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header > .actions .action-label {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* TODO: actions should be part of the pane, but they aren't yet */
|
||||
.monaco-pane-view .pane > .pane-header > .actions .action-label.icon,
|
||||
.monaco-pane-view .pane > .pane-header > .actions .action-label.codicon {
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
.monaco-pane-view .pane:hover > .pane-header.expanded > .actions,
|
||||
.monaco-pane-view .pane:focus-within > .pane-header.expanded > .actions,
|
||||
.monaco-pane-view .pane > .pane-header.actions-always-visible.expanded > .actions,
|
||||
.monaco-pane-view .pane > .pane-header.focused.expanded > .actions {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header .monaco-action-bar .action-item.select-container {
|
||||
|
||||
@@ -432,7 +432,7 @@ export class PaneView extends Disposable {
|
||||
|
||||
private dnd: IPaneDndController | undefined;
|
||||
private dndContext: IDndContext = { draggable: null };
|
||||
private el: HTMLElement;
|
||||
readonly element: HTMLElement;
|
||||
private paneItems: IPaneItem[] = [];
|
||||
private orthogonalSize: number = 0;
|
||||
private size: number = 0;
|
||||
@@ -450,8 +450,8 @@ export class PaneView extends Disposable {
|
||||
|
||||
this.dnd = options.dnd;
|
||||
this.orientation = options.orientation ?? Orientation.VERTICAL;
|
||||
this.el = append(container, $('.monaco-pane-view'));
|
||||
this.splitview = this._register(new SplitView(this.el, { orientation: this.orientation }));
|
||||
this.element = append(container, $('.monaco-pane-view'));
|
||||
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
|
||||
this.onDidSashChange = this.splitview.onDidSashChange;
|
||||
}
|
||||
|
||||
@@ -534,9 +534,9 @@ export class PaneView extends Disposable {
|
||||
const paneSizes = this.paneItems.map(pane => this.getPaneSize(pane.pane));
|
||||
|
||||
this.splitview.dispose();
|
||||
clearNode(this.el);
|
||||
clearNode(this.element);
|
||||
|
||||
this.splitview = this._register(new SplitView(this.el, { orientation: this.orientation }));
|
||||
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
|
||||
|
||||
const newOrthogonalSize = this.orientation === Orientation.VERTICAL ? width : height;
|
||||
const newSize = this.orientation === Orientation.HORIZONTAL ? width : height;
|
||||
@@ -560,15 +560,15 @@ export class PaneView extends Disposable {
|
||||
window.clearTimeout(this.animationTimer);
|
||||
}
|
||||
|
||||
this.el.classList.add('animated');
|
||||
this.element.classList.add('animated');
|
||||
|
||||
this.animationTimer = window.setTimeout(() => {
|
||||
this.animationTimer = undefined;
|
||||
this.el.classList.remove('animated');
|
||||
this.element.classList.remove('animated');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.paneItems.forEach(i => i.disposable.dispose());
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface ISplitViewOptions<TLayoutContext = undefined> {
|
||||
readonly inverseAltBehavior?: boolean;
|
||||
readonly proportionalLayout?: boolean; // default true,
|
||||
readonly descriptor?: ISplitViewDescriptor<TLayoutContext>;
|
||||
readonly scrollbarVisibility?: ScrollbarVisibility;
|
||||
readonly getSashOrthogonalSize?: () => number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +202,7 @@ export namespace Sizing {
|
||||
export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; }
|
||||
}
|
||||
|
||||
export interface ISplitViewDescriptor<TLayoutContext> {
|
||||
export interface ISplitViewDescriptor<TLayoutContext = undefined> {
|
||||
size: number;
|
||||
views: {
|
||||
visible?: boolean;
|
||||
@@ -227,6 +229,7 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
private state: State = State.Idle;
|
||||
private inverseAltBehavior: boolean;
|
||||
private proportionalLayout: boolean;
|
||||
private readonly getSashOrthogonalSize: { (): number } | undefined;
|
||||
|
||||
private _onDidSashChange = this._register(new Emitter<number>());
|
||||
readonly onDidSashChange = this._onDidSashChange.event;
|
||||
@@ -298,6 +301,7 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation;
|
||||
this.inverseAltBehavior = !!options.inverseAltBehavior;
|
||||
this.proportionalLayout = types.isUndefined(options.proportionalLayout) ? true : !!options.proportionalLayout;
|
||||
this.getSashOrthogonalSize = options.getSashOrthogonalSize;
|
||||
|
||||
this.el = document.createElement('div');
|
||||
this.el.classList.add('monaco-split-view2');
|
||||
@@ -309,8 +313,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
|
||||
this.scrollable = new Scrollable(125, scheduleAtNextAnimationFrame);
|
||||
this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
|
||||
vertical: this.orientation === Orientation.VERTICAL ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden,
|
||||
horizontal: this.orientation === Orientation.HORIZONTAL ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden
|
||||
vertical: this.orientation === Orientation.VERTICAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden,
|
||||
horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden
|
||||
}, this.scrollable));
|
||||
|
||||
this._register(this.scrollableElement.onScroll(e => {
|
||||
@@ -706,17 +710,11 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
|
||||
// Add sash
|
||||
if (this.viewItems.length > 1) {
|
||||
let opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash };
|
||||
|
||||
const sash = this.orientation === Orientation.VERTICAL
|
||||
? new Sash(this.sashContainer, { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) }, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
orthogonalStartSash: this.orthogonalStartSash,
|
||||
orthogonalEndSash: this.orthogonalEndSash
|
||||
})
|
||||
: new Sash(this.sashContainer, { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
orthogonalStartSash: this.orthogonalStartSash,
|
||||
orthogonalEndSash: this.orthogonalEndSash
|
||||
});
|
||||
? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, { ...opts, orientation: Orientation.HORIZONTAL })
|
||||
: new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, { ...opts, orientation: Orientation.VERTICAL });
|
||||
|
||||
const sashEventMapper = this.orientation === Orientation.VERTICAL
|
||||
? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
|
||||
@@ -1024,7 +1022,7 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.viewItems.forEach(i => i.dispose());
|
||||
|
||||
61
src/vs/base/browser/ui/table/table.css
Normal file
61
src/vs/base/browser/ui/table/table.css
Normal file
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-split-view2 {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-table-tr {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-table-th {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-table-th,
|
||||
.monaco-table-td {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: calc(var(--sash-size) / 2);
|
||||
width: 0;
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-split-view2,
|
||||
.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
transition: border-color 0.2s ease-out;
|
||||
}
|
||||
/*
|
||||
.monaco-table:hover > .monaco-split-view2,
|
||||
.monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
border-color: rgba(204, 204, 204, 0.2);
|
||||
} */
|
||||
40
src/vs/base/browser/ui/table/table.ts
Normal file
40
src/vs/base/browser/ui/table/table.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export interface ITableColumn<TRow, TCell> {
|
||||
readonly label: string;
|
||||
readonly tooltip?: string;
|
||||
readonly weight: number;
|
||||
readonly templateId: string;
|
||||
|
||||
readonly minimumWidth?: number;
|
||||
readonly maximumWidth?: number;
|
||||
readonly onDidChangeWidthConstraints?: Event<void>;
|
||||
|
||||
project(row: TRow): TCell;
|
||||
}
|
||||
|
||||
export interface ITableVirtualDelegate<TRow> {
|
||||
readonly headerRowHeight: number;
|
||||
getHeight(row: TRow): number;
|
||||
}
|
||||
|
||||
export interface ITableRenderer<TCell, TTemplateData> extends IListRenderer<TCell, TTemplateData> { }
|
||||
|
||||
export interface ITableEvent<TRow> extends IListEvent<TRow> { }
|
||||
export interface ITableMouseEvent<TRow> extends IListMouseEvent<TRow> { }
|
||||
export interface ITableTouchEvent<TRow> extends IListTouchEvent<TRow> { }
|
||||
export interface ITableGestureEvent<TRow> extends IListGestureEvent<TRow> { }
|
||||
export interface ITableContextMenuEvent<TRow> extends IListContextMenuEvent<TRow> { }
|
||||
|
||||
export class TableError extends Error {
|
||||
|
||||
constructor(user: string, message: string) {
|
||||
super(`TableError [${user}] ${message}`);
|
||||
}
|
||||
}
|
||||
344
src/vs/base/browser/ui/table/tableWidget.ts
Normal file
344
src/vs/base/browser/ui/table/tableWidget.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./table';
|
||||
import { IListOptions, IListOptionsUpdate, IListStyles, List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { ITableColumn, ITableContextMenuEvent, ITableEvent, ITableGestureEvent, ITableMouseEvent, ITableRenderer, ITableTouchEvent, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { IThemable } from 'vs/base/common/styler';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { $, append, clearNode, createStyleSheet, getContentHeight, getContentWidth } from 'vs/base/browser/dom';
|
||||
import { ISplitViewDescriptor, IView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
|
||||
|
||||
// TODO@joao
|
||||
type TCell = any;
|
||||
|
||||
interface RowTemplateData {
|
||||
readonly container: HTMLElement;
|
||||
readonly cellContainers: HTMLElement[];
|
||||
readonly cellTemplateData: unknown[];
|
||||
}
|
||||
|
||||
class TableListRenderer<TRow> implements IListRenderer<TRow, RowTemplateData> {
|
||||
|
||||
static TemplateId = 'row';
|
||||
readonly templateId = TableListRenderer.TemplateId;
|
||||
private renderers: ITableRenderer<TCell, unknown>[];
|
||||
private renderedTemplates = new Set<RowTemplateData>();
|
||||
|
||||
constructor(
|
||||
private columns: ITableColumn<TRow, TCell>[],
|
||||
renderers: ITableRenderer<TCell, unknown>[],
|
||||
private getColumnSize: (index: number) => number
|
||||
) {
|
||||
const rendererMap = new Map(renderers.map(r => [r.templateId, r]));
|
||||
this.renderers = [];
|
||||
|
||||
for (const column of columns) {
|
||||
const renderer = rendererMap.get(column.templateId);
|
||||
|
||||
if (!renderer) {
|
||||
throw new Error(`Table cell renderer for template id ${column.templateId} not found.`);
|
||||
}
|
||||
|
||||
this.renderers.push(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement) {
|
||||
const rowContainer = append(container, $('.monaco-table-tr'));
|
||||
const cellContainers: HTMLElement[] = [];
|
||||
const cellTemplateData: unknown[] = [];
|
||||
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const renderer = this.renderers[i];
|
||||
const cellContainer = append(rowContainer, $('.monaco-table-td', { 'data-col-index': i }));
|
||||
|
||||
cellContainer.style.width = `${this.getColumnSize(i)}px`;
|
||||
cellContainers.push(cellContainer);
|
||||
cellTemplateData.push(renderer.renderTemplate(cellContainer));
|
||||
}
|
||||
|
||||
const result = { container, cellContainers, cellTemplateData };
|
||||
this.renderedTemplates.add(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
renderElement(element: TRow, index: number, templateData: RowTemplateData, height: number | undefined): void {
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const column = this.columns[i];
|
||||
const cell = column.project(element);
|
||||
const renderer = this.renderers[i];
|
||||
renderer.renderElement(cell, index, templateData.cellTemplateData[i], height);
|
||||
}
|
||||
}
|
||||
|
||||
disposeElement(element: TRow, index: number, templateData: RowTemplateData, height: number | undefined): void {
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const renderer = this.renderers[i];
|
||||
|
||||
if (renderer.disposeElement) {
|
||||
const column = this.columns[i];
|
||||
const cell = column.project(element);
|
||||
|
||||
renderer.disposeElement(cell, index, templateData.cellTemplateData[i], height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: RowTemplateData): void {
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const renderer = this.renderers[i];
|
||||
renderer.disposeTemplate(templateData.cellTemplateData[i]);
|
||||
}
|
||||
|
||||
clearNode(templateData.container);
|
||||
this.renderedTemplates.delete(templateData);
|
||||
}
|
||||
|
||||
layoutColumn(index: number, size: number): void {
|
||||
for (const { cellContainers } of this.renderedTemplates) {
|
||||
cellContainers[index].style.width = `${size}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function asListVirtualDelegate<TRow>(delegate: ITableVirtualDelegate<TRow>): IListVirtualDelegate<TRow> {
|
||||
return {
|
||||
getHeight(row) { return delegate.getHeight(row); },
|
||||
getTemplateId() { return TableListRenderer.TemplateId; },
|
||||
};
|
||||
}
|
||||
|
||||
class ColumnHeader<TRow, TCell> implements IView {
|
||||
|
||||
readonly element: HTMLElement;
|
||||
|
||||
get minimumSize() { return this.column.minimumWidth ?? 120; }
|
||||
get maximumSize() { return this.column.maximumWidth ?? Number.POSITIVE_INFINITY; }
|
||||
get onDidChange() { return this.column.onDidChangeWidthConstraints ?? Event.None; }
|
||||
|
||||
private _onDidLayout = new Emitter<[number, number]>();
|
||||
readonly onDidLayout = this._onDidLayout.event;
|
||||
|
||||
constructor(readonly column: ITableColumn<TRow, TCell>, private index: number) {
|
||||
this.element = $('.monaco-table-th', { 'data-col-index': index, title: column.tooltip }, column.label);
|
||||
}
|
||||
|
||||
layout(size: number): void {
|
||||
this._onDidLayout.fire([this.index, size]);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITableOptions<TRow> extends IListOptions<TRow> { }
|
||||
export interface ITableOptionsUpdate extends IListOptionsUpdate { }
|
||||
export interface ITableStyles extends IListStyles { }
|
||||
|
||||
export class Table<TRow> implements ISpliceable<TRow>, IThemable, IDisposable {
|
||||
|
||||
private static InstanceCount = 0;
|
||||
readonly domId = `table_id_${++Table.InstanceCount}`;
|
||||
|
||||
readonly domNode: HTMLElement;
|
||||
private splitview: SplitView;
|
||||
private list: List<TRow>;
|
||||
private columnLayoutDisposable: IDisposable;
|
||||
private cachedHeight: number = 0;
|
||||
private styleElement: HTMLStyleElement;
|
||||
|
||||
get onDidChangeFocus(): Event<ITableEvent<TRow>> { return this.list.onDidChangeFocus; }
|
||||
get onDidChangeSelection(): Event<ITableEvent<TRow>> { return this.list.onDidChangeSelection; }
|
||||
|
||||
get onDidScroll(): Event<ScrollEvent> { return this.list.onDidScroll; }
|
||||
get onMouseClick(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseClick; }
|
||||
get onMouseDblClick(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseDblClick; }
|
||||
get onMouseMiddleClick(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseMiddleClick; }
|
||||
get onPointer(): Event<ITableMouseEvent<TRow>> { return this.list.onPointer; }
|
||||
get onMouseUp(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseUp; }
|
||||
get onMouseDown(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseDown; }
|
||||
get onMouseOver(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseOver; }
|
||||
get onMouseMove(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseMove; }
|
||||
get onMouseOut(): Event<ITableMouseEvent<TRow>> { return this.list.onMouseOut; }
|
||||
get onTouchStart(): Event<ITableTouchEvent<TRow>> { return this.list.onTouchStart; }
|
||||
get onTap(): Event<ITableGestureEvent<TRow>> { return this.list.onTap; }
|
||||
get onContextMenu(): Event<ITableContextMenuEvent<TRow>> { return this.list.onContextMenu; }
|
||||
|
||||
get onDidFocus(): Event<void> { return this.list.onDidFocus; }
|
||||
get onDidBlur(): Event<void> { return this.list.onDidBlur; }
|
||||
|
||||
get scrollTop(): number { return this.list.scrollTop; }
|
||||
set scrollTop(scrollTop: number) { this.list.scrollTop = scrollTop; }
|
||||
get scrollLeft(): number { return this.list.scrollLeft; }
|
||||
set scrollLeft(scrollLeft: number) { this.list.scrollLeft = scrollLeft; }
|
||||
get scrollHeight(): number { return this.list.scrollHeight; }
|
||||
get renderHeight(): number { return this.list.renderHeight; }
|
||||
get onDidDispose(): Event<void> { return this.list.onDidDispose; }
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
container: HTMLElement,
|
||||
private virtualDelegate: ITableVirtualDelegate<TRow>,
|
||||
columns: ITableColumn<TRow, TCell>[],
|
||||
renderers: ITableRenderer<TCell, unknown>[],
|
||||
_options?: ITableOptions<TRow>
|
||||
) {
|
||||
this.domNode = append(container, $(`.monaco-table.${this.domId}`));
|
||||
|
||||
const headers = columns.map((c, i) => new ColumnHeader(c, i));
|
||||
const descriptor: ISplitViewDescriptor = {
|
||||
size: headers.reduce((a, b) => a + b.column.weight, 0),
|
||||
views: headers.map(view => ({ size: view.column.weight, view }))
|
||||
};
|
||||
|
||||
this.splitview = new SplitView(this.domNode, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
scrollbarVisibility: ScrollbarVisibility.Hidden,
|
||||
getSashOrthogonalSize: () => this.cachedHeight,
|
||||
descriptor
|
||||
});
|
||||
|
||||
this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`;
|
||||
this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`;
|
||||
|
||||
const renderer = new TableListRenderer(columns, renderers, i => this.splitview.getViewSize(i));
|
||||
this.list = new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [renderer], _options);
|
||||
|
||||
this.columnLayoutDisposable = Event.any(...headers.map(h => h.onDidLayout))
|
||||
(([index, size]) => renderer.layoutColumn(index, size));
|
||||
|
||||
this.styleElement = createStyleSheet(this.domNode);
|
||||
this.style({});
|
||||
}
|
||||
|
||||
updateOptions(options: ITableOptionsUpdate): void {
|
||||
this.list.updateOptions(options);
|
||||
}
|
||||
|
||||
splice(start: number, deleteCount: number, elements: TRow[] = []): void {
|
||||
this.list.splice(start, deleteCount, elements);
|
||||
}
|
||||
|
||||
rerender(): void {
|
||||
this.list.rerender();
|
||||
}
|
||||
|
||||
row(index: number): TRow {
|
||||
return this.list.element(index);
|
||||
}
|
||||
|
||||
indexOf(element: TRow): number {
|
||||
return this.list.indexOf(element);
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
getHTMLElement(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
layout(height?: number, width?: number): void {
|
||||
height = height ?? getContentHeight(this.domNode);
|
||||
width = width ?? getContentWidth(this.domNode);
|
||||
|
||||
this.cachedHeight = height;
|
||||
this.splitview.layout(width);
|
||||
|
||||
const listHeight = height - this.virtualDelegate.headerRowHeight;
|
||||
this.list.getHTMLElement().style.height = `${listHeight}px`;
|
||||
this.list.layout(listHeight, width);
|
||||
}
|
||||
|
||||
toggleKeyboardNavigation(): void {
|
||||
this.list.toggleKeyboardNavigation();
|
||||
}
|
||||
|
||||
style(styles: ITableStyles): void {
|
||||
const content: string[] = [];
|
||||
|
||||
content.push(`.monaco-table.${this.domId} > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
top: ${this.virtualDelegate.headerRowHeight + 1}px;
|
||||
height: calc(100% - ${this.virtualDelegate.headerRowHeight}px);
|
||||
}`);
|
||||
|
||||
this.styleElement.textContent = content.join('\n');
|
||||
this.list.style(styles);
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.list.domFocus();
|
||||
}
|
||||
|
||||
setAnchor(index: number | undefined): void {
|
||||
this.list.setAnchor(index);
|
||||
}
|
||||
|
||||
getAnchor(): number | undefined {
|
||||
return this.list.getAnchor();
|
||||
}
|
||||
|
||||
getSelectedElements(): TRow[] {
|
||||
return this.list.getSelectedElements();
|
||||
}
|
||||
|
||||
setSelection(indexes: number[], browserEvent?: UIEvent): void {
|
||||
this.list.setSelection(indexes, browserEvent);
|
||||
}
|
||||
|
||||
getSelection(): number[] {
|
||||
return this.list.getSelection();
|
||||
}
|
||||
|
||||
setFocus(indexes: number[], browserEvent?: UIEvent): void {
|
||||
this.list.setFocus(indexes, browserEvent);
|
||||
}
|
||||
|
||||
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.list.focusNext(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.list.focusPrevious(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent): Promise<void> {
|
||||
return this.list.focusNextPage(browserEvent);
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent): Promise<void> {
|
||||
return this.list.focusPreviousPage(browserEvent);
|
||||
}
|
||||
|
||||
focusFirst(browserEvent?: UIEvent): void {
|
||||
this.list.focusFirst(browserEvent);
|
||||
}
|
||||
|
||||
focusLast(browserEvent?: UIEvent): void {
|
||||
this.list.focusLast(browserEvent);
|
||||
}
|
||||
|
||||
getFocus(): number[] {
|
||||
return this.list.getFocus();
|
||||
}
|
||||
|
||||
getFocusedElements(): TRow[] {
|
||||
return this.list.getFocusedElements();
|
||||
}
|
||||
|
||||
reveal(index: number, relativeTop?: number): void {
|
||||
this.list.reveal(index, relativeTop);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.splitview.dispose();
|
||||
this.list.dispose();
|
||||
this.columnLayoutDisposable.dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-toolbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-toolbar .toolbar-toggle-more {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import 'vs/css!./toolbar';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action, IActionRunner, IAction, IActionViewItemProvider, SubmenuAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action, IActionRunner, IAction, SubmenuAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
@@ -181,7 +181,7 @@ export class ToolBar extends Disposable {
|
||||
this.actionBar.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
this.clear();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -202,7 +202,7 @@ class ToggleMenuAction extends Action {
|
||||
this.toggleDropdownMenu = toggleDropdownMenu;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
override async run(): Promise<void> {
|
||||
this.toggleDropdownMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd';
|
||||
import { range, equals, distinctES6 } from 'vs/base/common/arrays';
|
||||
import { range, equals, distinctES6, firstOrDefault } from 'vs/base/common/arrays';
|
||||
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
|
||||
@@ -29,11 +29,11 @@ import { treeItemExpandedIcon, treeFilterOnTypeOnIcon, treeFilterOnTypeOffIcon,
|
||||
|
||||
class TreeElementsDragAndDropData<T, TFilterData, TContext> extends ElementsDragAndDropData<T, TContext> {
|
||||
|
||||
set context(context: TContext | undefined) {
|
||||
override set context(context: TContext | undefined) {
|
||||
this.data.context = context;
|
||||
}
|
||||
|
||||
get context(): TContext | undefined {
|
||||
override get context(): TContext | undefined {
|
||||
return this.data.context;
|
||||
}
|
||||
|
||||
@@ -683,6 +683,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
if (typeof options.filterOnType !== 'undefined') {
|
||||
this._filterOnType = !!options.filterOnType;
|
||||
this.filterOnTypeDomNode.checked = this._filterOnType;
|
||||
this.updateFilterOnTypeTitleAndIcon();
|
||||
}
|
||||
|
||||
if (typeof options.automaticKeyboardNavigation !== 'undefined') {
|
||||
@@ -961,7 +962,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
|
||||
readonly filterOnType?: boolean;
|
||||
readonly smoothScrolling?: boolean;
|
||||
readonly horizontalScrolling?: boolean;
|
||||
readonly expandOnlyOnDoubleClick?: boolean;
|
||||
readonly expandOnDoubleClick?: boolean;
|
||||
readonly expandOnlyOnTwistieClick?: boolean | ((e: any) => boolean); // e is T
|
||||
}
|
||||
|
||||
@@ -1090,7 +1091,7 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
|
||||
super(list);
|
||||
}
|
||||
|
||||
protected onViewPointer(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
protected override onViewPointer(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
if (isInputElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
@@ -1121,7 +1122,7 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
|
||||
return super.onViewPointer(e);
|
||||
}
|
||||
|
||||
if (this.tree.expandOnlyOnDoubleClick && e.browserEvent.detail !== 2 && !onTwistie) {
|
||||
if (!this.tree.expandOnDoubleClick && e.browserEvent.detail === 2) {
|
||||
return super.onViewPointer(e);
|
||||
}
|
||||
|
||||
@@ -1129,6 +1130,7 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
|
||||
const model = ((this.tree as any).model as ITreeModel<T, TFilterData, TRef>); // internal
|
||||
const location = model.getNodeLocation(node);
|
||||
const recursive = e.browserEvent.altKey;
|
||||
this.tree.setFocus([location]);
|
||||
model.setCollapsed(location, undefined, recursive);
|
||||
|
||||
if (expandOnlyOnTwistieClick && onTwistie) {
|
||||
@@ -1139,10 +1141,10 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
|
||||
super.onViewPointer(e);
|
||||
}
|
||||
|
||||
protected onDoubleClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
protected override onDoubleClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
const onTwistie = (e.browserEvent.target as HTMLElement).classList.contains('monaco-tl-twistie');
|
||||
|
||||
if (onTwistie) {
|
||||
if (onTwistie || !this.tree.expandOnDoubleClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1167,16 +1169,17 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
||||
renderers: IListRenderer<any /* TODO@joao */, any>[],
|
||||
private focusTrait: Trait<T>,
|
||||
private selectionTrait: Trait<T>,
|
||||
private anchorTrait: Trait<T>,
|
||||
options: ITreeNodeListOptions<T, TFilterData, TRef>
|
||||
) {
|
||||
super(user, container, virtualDelegate, renderers, options);
|
||||
}
|
||||
|
||||
protected createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> {
|
||||
protected override createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> {
|
||||
return new TreeNodeListMouseController(this, options.tree);
|
||||
}
|
||||
|
||||
splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
|
||||
override splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
|
||||
super.splice(start, deleteCount, elements);
|
||||
|
||||
if (elements.length === 0) {
|
||||
@@ -1185,6 +1188,7 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
||||
|
||||
const additionalFocus: number[] = [];
|
||||
const additionalSelection: number[] = [];
|
||||
let anchor: number | undefined;
|
||||
|
||||
elements.forEach((node, index) => {
|
||||
if (this.focusTrait.has(node)) {
|
||||
@@ -1194,6 +1198,10 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
||||
if (this.selectionTrait.has(node)) {
|
||||
additionalSelection.push(start + index);
|
||||
}
|
||||
|
||||
if (this.anchorTrait.has(node)) {
|
||||
anchor = start + index;
|
||||
}
|
||||
});
|
||||
|
||||
if (additionalFocus.length > 0) {
|
||||
@@ -1203,9 +1211,13 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
||||
if (additionalSelection.length > 0) {
|
||||
super.setSelection(distinctES6([...super.getSelection(), ...additionalSelection]));
|
||||
}
|
||||
|
||||
if (typeof anchor === 'number') {
|
||||
super.setAnchor(anchor);
|
||||
}
|
||||
}
|
||||
|
||||
setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
|
||||
override setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
|
||||
super.setFocus(indexes, browserEvent);
|
||||
|
||||
if (!fromAPI) {
|
||||
@@ -1213,13 +1225,25 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
|
||||
}
|
||||
}
|
||||
|
||||
setSelection(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
|
||||
override setSelection(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
|
||||
super.setSelection(indexes, browserEvent);
|
||||
|
||||
if (!fromAPI) {
|
||||
this.selectionTrait.set(indexes.map(i => this.element(i)), browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
override setAnchor(index: number | undefined, fromAPI = false): void {
|
||||
super.setAnchor(index);
|
||||
|
||||
if (!fromAPI) {
|
||||
if (typeof index === 'undefined') {
|
||||
this.anchorTrait.set([]);
|
||||
} else {
|
||||
this.anchorTrait.set([this.element(index)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
|
||||
@@ -1229,6 +1253,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
protected model: ITreeModel<T, TFilterData, TRef>;
|
||||
private focus: Trait<T>;
|
||||
private selection: Trait<T>;
|
||||
private anchor: Trait<T>;
|
||||
private eventBufferer = new EventBufferer();
|
||||
private typeFilterController?: TypeFilterController<T, TFilterData>;
|
||||
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
|
||||
@@ -1262,8 +1287,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
get filterOnType(): boolean { return !!this._options.filterOnType; }
|
||||
get onDidChangeTypeFilterPattern(): Event<string> { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; }
|
||||
|
||||
get expandOnlyOnDoubleClick(): boolean { return this._options.expandOnlyOnDoubleClick ?? false; }
|
||||
get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? false : this._options.expandOnlyOnTwistieClick; }
|
||||
get expandOnDoubleClick(): boolean { return typeof this._options.expandOnDoubleClick === 'undefined' ? true : this._options.expandOnDoubleClick; }
|
||||
get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? true : this._options.expandOnlyOnTwistieClick; }
|
||||
|
||||
private readonly _onDidUpdateOptions = new Emitter<IAbstractTreeOptions<T, TFilterData>>();
|
||||
readonly onDidUpdateOptions: Event<IAbstractTreeOptions<T, TFilterData>> = this._onDidUpdateOptions.event;
|
||||
@@ -1297,7 +1322,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
|
||||
this.focus = new Trait(_options.identityProvider);
|
||||
this.selection = new Trait(_options.identityProvider);
|
||||
this.view = new TreeNodeList(user, container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this });
|
||||
this.anchor = new Trait(_options.identityProvider);
|
||||
this.view = new TreeNodeList(user, container, treeDelegate, this.renderers, this.focus, this.selection, this.anchor, { ...asListOptions(() => this.model, _options), tree: this });
|
||||
|
||||
this.model = this.createModel(user, this.view, _options);
|
||||
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
|
||||
@@ -1551,6 +1577,25 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
this.model.refilter();
|
||||
}
|
||||
|
||||
setAnchor(element: TRef | undefined): void {
|
||||
if (typeof element === 'undefined') {
|
||||
return this.view.setAnchor(undefined);
|
||||
}
|
||||
|
||||
const node = this.model.getNode(element);
|
||||
this.anchor.set([node]);
|
||||
|
||||
const index = this.model.getListIndex(element);
|
||||
|
||||
if (index > -1) {
|
||||
this.view.setAnchor(index, true);
|
||||
}
|
||||
}
|
||||
|
||||
getAnchor(): T | undefined {
|
||||
return firstOrDefault(this.anchor.get(), undefined);
|
||||
}
|
||||
|
||||
setSelection(elements: TRef[], browserEvent?: UIEvent): void {
|
||||
const nodes = elements.map(e => this.model.getNode(e));
|
||||
this.selection.set(nodes, browserEvent);
|
||||
@@ -1579,12 +1624,12 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
this.view.focusPrevious(n, loop, browserEvent, filter);
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusNextPage(browserEvent, filter);
|
||||
focusNextPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): Promise<void> {
|
||||
return this.view.focusNextPage(browserEvent, filter);
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusPreviousPage(browserEvent, filter);
|
||||
focusPreviousPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): Promise<void> {
|
||||
return this.view.focusPreviousPage(browserEvent, filter);
|
||||
}
|
||||
|
||||
focusLast(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
|
||||
@@ -157,11 +157,11 @@ function asTreeContextMenuEvent<TInput, T>(e: ITreeContextMenuEvent<IAsyncDataTr
|
||||
|
||||
class AsyncDataTreeElementsDragAndDropData<TInput, T, TContext> extends ElementsDragAndDropData<T, TContext> {
|
||||
|
||||
set context(context: TContext | undefined) {
|
||||
override set context(context: TContext | undefined) {
|
||||
this.data.context = context;
|
||||
}
|
||||
|
||||
get context(): TContext | undefined {
|
||||
override get context(): TContext | undefined {
|
||||
return this.data.context;
|
||||
}
|
||||
|
||||
@@ -616,7 +616,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
return this.tree.isCollapsible(this.getDataNode(element));
|
||||
}
|
||||
|
||||
isCollapsed(element: T): boolean {
|
||||
isCollapsed(element: TInput | T): boolean {
|
||||
return this.tree.isCollapsed(this.getDataNode(element));
|
||||
}
|
||||
|
||||
@@ -628,6 +628,15 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
this.tree.refilter();
|
||||
}
|
||||
|
||||
setAnchor(element: T | undefined): void {
|
||||
this.tree.setAnchor(typeof element === 'undefined' ? undefined : this.getDataNode(element));
|
||||
}
|
||||
|
||||
getAnchor(): T | undefined {
|
||||
const node = this.tree.getAnchor();
|
||||
return node?.element as T;
|
||||
}
|
||||
|
||||
setSelection(elements: T[], browserEvent?: UIEvent): void {
|
||||
const nodes = elements.map(e => this.getDataNode(e));
|
||||
this.tree.setSelection(nodes, browserEvent);
|
||||
@@ -651,12 +660,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
this.tree.focusPrevious(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent): void {
|
||||
this.tree.focusNextPage(browserEvent);
|
||||
focusNextPage(browserEvent?: UIEvent): Promise<void> {
|
||||
return this.tree.focusNextPage(browserEvent);
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent): void {
|
||||
this.tree.focusPreviousPage(browserEvent);
|
||||
focusPreviousPage(browserEvent?: UIEvent): Promise<void> {
|
||||
return this.tree.focusPreviousPage(browserEvent);
|
||||
}
|
||||
|
||||
focusLast(browserEvent?: UIEvent): void {
|
||||
@@ -1122,7 +1131,7 @@ export interface ICompressibleAsyncDataTreeOptionsUpdate extends IAsyncDataTreeO
|
||||
|
||||
export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends AsyncDataTree<TInput, T, TFilterData> {
|
||||
|
||||
protected readonly tree!: CompressibleObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
|
||||
protected override readonly tree!: CompressibleObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
|
||||
protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper<TInput, T, TFilterData> = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node));
|
||||
private filter?: ITreeFilter<T, TFilterData>;
|
||||
|
||||
@@ -1139,7 +1148,7 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
|
||||
this.filter = options.filter;
|
||||
}
|
||||
|
||||
protected createTree(
|
||||
protected override createTree(
|
||||
user: string,
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
@@ -1153,18 +1162,18 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
|
||||
return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
|
||||
}
|
||||
|
||||
protected asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ICompressedTreeElement<IAsyncDataTreeNode<TInput, T>> {
|
||||
protected override asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ICompressedTreeElement<IAsyncDataTreeNode<TInput, T>> {
|
||||
return {
|
||||
incompressible: this.compressionDelegate.isIncompressible(node.element as T),
|
||||
...super.asTreeElement(node, viewStateContext)
|
||||
};
|
||||
}
|
||||
|
||||
updateOptions(options: ICompressibleAsyncDataTreeOptionsUpdate = {}): void {
|
||||
override updateOptions(options: ICompressibleAsyncDataTreeOptionsUpdate = {}): void {
|
||||
this.tree.updateOptions(options);
|
||||
}
|
||||
|
||||
getViewState(): IAsyncDataTreeViewState {
|
||||
override getViewState(): IAsyncDataTreeViewState {
|
||||
if (!this.identityProvider) {
|
||||
throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider');
|
||||
}
|
||||
@@ -1192,7 +1201,7 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
|
||||
return { focus, selection, expanded, scrollTop: this.scrollTop };
|
||||
}
|
||||
|
||||
protected render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
|
||||
protected override render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
|
||||
if (!this.identityProvider) {
|
||||
return super.render(node, viewStateContext);
|
||||
}
|
||||
@@ -1268,7 +1277,7 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
|
||||
// For compressed async data trees, `TreeVisibility.Recurse` doesn't currently work
|
||||
// and we have to filter everything beforehand
|
||||
// Related to #85193 and #85835
|
||||
protected processChildren(children: Iterable<T>): Iterable<T> {
|
||||
protected override processChildren(children: Iterable<T>): Iterable<T> {
|
||||
if (this.filter) {
|
||||
children = Iterable.filter(children, e => {
|
||||
const result = this.filter!.filter(e, TreeVisibility.Visible);
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface IDataTreeViewState {
|
||||
|
||||
export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
|
||||
|
||||
protected model!: ObjectTreeModel<T, TFilterData>;
|
||||
protected override model!: ObjectTreeModel<T, TFilterData>;
|
||||
private input: TInput | undefined;
|
||||
|
||||
private identityProvider: IIdentityProvider<T> | undefined;
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IIndexTreeOptions<T, TFilterData = void> extends IAbstractTreeO
|
||||
|
||||
export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterData, number[]> {
|
||||
|
||||
protected model!: IndexTreeModel<T, TFilterData>;
|
||||
protected override model!: IndexTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie::before {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie.collapsed::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ export interface IObjectTreeSetChildrenOptions<T> {
|
||||
|
||||
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
|
||||
|
||||
protected model!: IObjectTreeModel<T, TFilterData>;
|
||||
protected override model!: IObjectTreeModel<T, TFilterData>;
|
||||
|
||||
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T | null, TFilterData>> { return this.model.onDidChangeCollapseState; }
|
||||
override get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T | null, TFilterData>> { return this.model.onDidChangeCollapseState; }
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
@@ -69,7 +69,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
|
||||
this.model.updateElementHeight(element, height);
|
||||
}
|
||||
|
||||
resort(element: T, recursive = true): void {
|
||||
resort(element: T | null, recursive = true): void {
|
||||
this.model.resort(element, recursive);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptio
|
||||
|
||||
export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = void> extends ObjectTree<T, TFilterData> implements ICompressedTreeNodeProvider<T, TFilterData> {
|
||||
|
||||
protected model!: CompressibleObjectTreeModel<T, TFilterData>;
|
||||
protected override model!: CompressibleObjectTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
@@ -208,15 +208,15 @@ export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = vo
|
||||
super(user, container, delegate, compressibleRenderers, asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider, options));
|
||||
}
|
||||
|
||||
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
|
||||
override setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
|
||||
this.model.setChildren(element, children, options);
|
||||
}
|
||||
|
||||
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: ICompressibleObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
protected override createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: ICompressibleObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new CompressibleObjectTreeModel(user, view, options);
|
||||
}
|
||||
|
||||
updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void {
|
||||
override updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void {
|
||||
super.updateOptions(optionsUpdate);
|
||||
|
||||
if (typeof optionsUpdate.compressionEnabled !== 'undefined') {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { IndexTreeModel, IIndexTreeModelOptions, IList, IIndexTreeModelSpliceOpt
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
|
||||
export type ITreeNodeCallback<T, TFilterData> = (node: ITreeNode<T, TFilterData>) => void;
|
||||
|
||||
@@ -130,7 +129,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
|
||||
private preserveCollapseState(elements: Iterable<ITreeElement<T>> = Iterable.empty()): Iterable<ITreeElement<T>> {
|
||||
if (this.sorter) {
|
||||
elements = mergeSort([...elements], this.sorter.compare.bind(this.sorter));
|
||||
elements = [...elements].sort(this.sorter.compare.bind(this.sorter));
|
||||
}
|
||||
|
||||
return Iterable.map(elements, treeElement => {
|
||||
@@ -185,7 +184,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
let childrenNodes = [...node.children] as ITreeNode<T, TFilterData>[];
|
||||
|
||||
if (recursive || first) {
|
||||
childrenNodes = mergeSort(childrenNodes, this.sorter!.compare.bind(this.sorter)) as ITreeNode<T, TFilterData>[]; // {{SQL CARBON EDIT}} strict-null-checks
|
||||
childrenNodes = childrenNodes.sort(this.sorter!.compare.bind(this.sorter));
|
||||
}
|
||||
|
||||
return Iterable.map<ITreeNode<T | null, TFilterData>, ITreeElement<T>>(childrenNodes, node => ({
|
||||
|
||||
@@ -13,11 +13,9 @@ export class CollapseAllAction<TInput, T, TFilterData = void> extends Action {
|
||||
super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'collapse-all', enabled);
|
||||
}
|
||||
|
||||
async run(): Promise<any> {
|
||||
override async run(): Promise<any> {
|
||||
this.viewer.collapseAll();
|
||||
this.viewer.setSelection([]);
|
||||
this.viewer.setFocus([]);
|
||||
this.viewer.domFocus();
|
||||
this.viewer.focusFirst();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user