Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
57
lib/vscode/src/bootstrap-amd.js
vendored
Normal file
57
lib/vscode/src/bootstrap-amd.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
const loader = require('./vs/loader');
|
||||
const bootstrap = require('./bootstrap');
|
||||
|
||||
// Bootstrap: NLS
|
||||
const nlsConfig = bootstrap.setupNLS();
|
||||
|
||||
// Bootstrap: Loader
|
||||
loader.config({
|
||||
baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }),
|
||||
catchError: true,
|
||||
nodeRequire: require,
|
||||
nodeMain: __filename,
|
||||
'vs/nls': nlsConfig
|
||||
});
|
||||
|
||||
// Running in Electron
|
||||
if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) {
|
||||
loader.define('fs', ['original-fs'], function (originalFS) {
|
||||
return originalFS; // replace the patched electron fs with the original node fs for all AMD code
|
||||
});
|
||||
}
|
||||
|
||||
// Pseudo NLS support
|
||||
if (nlsConfig.pseudo) {
|
||||
loader(['vs/nls'], function (nlsPlugin) {
|
||||
nlsPlugin.setPseudoTranslation(nlsConfig.pseudo);
|
||||
});
|
||||
}
|
||||
|
||||
exports.load = function (entrypoint, onLoad, onError) {
|
||||
if (!entrypoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cached data config
|
||||
if (process.env['VSCODE_NODE_CACHED_DATA_DIR']) {
|
||||
loader.config({
|
||||
nodeCachedData: {
|
||||
path: process.env['VSCODE_NODE_CACHED_DATA_DIR'],
|
||||
seed: entrypoint
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onLoad = onLoad || function () { };
|
||||
onError = onError || function (err) { console.error(err); };
|
||||
|
||||
loader([entrypoint], onLoad, onError);
|
||||
};
|
||||
189
lib/vscode/src/bootstrap-fork.js
vendored
Normal file
189
lib/vscode/src/bootstrap-fork.js
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
const bootstrap = require('./bootstrap');
|
||||
const bootstrapNode = require('./bootstrap-node');
|
||||
|
||||
// Remove global paths from the node module lookup
|
||||
bootstrapNode.removeGlobalNodeModuleLookupPaths();
|
||||
|
||||
// Enable ASAR in our forked processes
|
||||
bootstrap.enableASARSupport();
|
||||
|
||||
if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) {
|
||||
bootstrapNode.injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']);
|
||||
}
|
||||
|
||||
// Configure: pipe logging to parent process
|
||||
if (!!process.send && process.env.PIPE_LOGGING === 'true') {
|
||||
pipeLoggingToParent();
|
||||
}
|
||||
|
||||
// Handle Exceptions
|
||||
if (!process.env['VSCODE_HANDLES_UNCAUGHT_ERRORS']) {
|
||||
handleExceptions();
|
||||
}
|
||||
|
||||
// Terminate when parent terminates
|
||||
if (process.env['VSCODE_PARENT_PID']) {
|
||||
terminateWhenParentTerminates();
|
||||
}
|
||||
|
||||
// Configure Crash Reporter
|
||||
configureCrashReporter();
|
||||
|
||||
// Load AMD entry point
|
||||
require('./bootstrap-amd').load(process.env['AMD_ENTRYPOINT']);
|
||||
|
||||
|
||||
//#region Helpers
|
||||
|
||||
function pipeLoggingToParent() {
|
||||
const MAX_LENGTH = 100000;
|
||||
|
||||
// Prevent circular stringify and convert arguments to real array
|
||||
function safeToArray(args) {
|
||||
const seen = [];
|
||||
const argsArray = [];
|
||||
|
||||
// Massage some arguments with special treatment
|
||||
if (args.length) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
|
||||
// Any argument of type 'undefined' needs to be specially treated because
|
||||
// JSON.stringify will simply ignore those. We replace them with the string
|
||||
// 'undefined' which is not 100% right, but good enough to be logged to console
|
||||
if (typeof args[i] === 'undefined') {
|
||||
args[i] = 'undefined';
|
||||
}
|
||||
|
||||
// Any argument that is an Error will be changed to be just the error stack/message
|
||||
// itself because currently cannot serialize the error over entirely.
|
||||
else if (args[i] instanceof Error) {
|
||||
const errorObj = args[i];
|
||||
if (errorObj.stack) {
|
||||
args[i] = errorObj.stack;
|
||||
} else {
|
||||
args[i] = errorObj.toString();
|
||||
}
|
||||
}
|
||||
|
||||
argsArray.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the stack trace as payload if we are told so. We remove the message and the 2 top frames
|
||||
// to start the stacktrace where the console message was being written
|
||||
if (process.env.VSCODE_LOG_STACK === 'true') {
|
||||
const stack = new Error().stack;
|
||||
argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') });
|
||||
}
|
||||
|
||||
try {
|
||||
const res = JSON.stringify(argsArray, function (key, value) {
|
||||
|
||||
// Objects get special treatment to prevent circles
|
||||
if (isObject(value) || Array.isArray(value)) {
|
||||
if (seen.indexOf(value) !== -1) {
|
||||
return '[Circular]';
|
||||
}
|
||||
|
||||
seen.push(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
if (res.length > MAX_LENGTH) {
|
||||
return 'Output omitted for a large object that exceeds the limits';
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
return `Output omitted for an object that cannot be inspected ('${error.toString()}')`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ type: string; severity: string; arguments: string; }} arg
|
||||
*/
|
||||
function safeSend(arg) {
|
||||
try {
|
||||
process.send(arg);
|
||||
} catch (error) {
|
||||
// Can happen if the parent channel is closed meanwhile
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} obj
|
||||
*/
|
||||
function isObject(obj) {
|
||||
return typeof obj === 'object'
|
||||
&& obj !== null
|
||||
&& !Array.isArray(obj)
|
||||
&& !(obj instanceof RegExp)
|
||||
&& !(obj instanceof Date);
|
||||
}
|
||||
|
||||
// Pass console logging to the outside so that we have it in the main side if told so
|
||||
if (process.env.VERBOSE_LOGGING === 'true') {
|
||||
console.log = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); };
|
||||
console.info = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); };
|
||||
console.warn = function () { safeSend({ type: '__$console', severity: 'warn', arguments: safeToArray(arguments) }); };
|
||||
} else {
|
||||
console.log = function () { /* ignore */ };
|
||||
console.warn = function () { /* ignore */ };
|
||||
console.info = function () { /* ignore */ };
|
||||
}
|
||||
|
||||
console.error = function () { safeSend({ type: '__$console', severity: 'error', arguments: safeToArray(arguments) }); };
|
||||
}
|
||||
|
||||
function handleExceptions() {
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', function (err) {
|
||||
console.error('Uncaught Exception: ', err);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', function (reason) {
|
||||
console.error('Unhandled Promise Rejection: ', reason);
|
||||
});
|
||||
}
|
||||
|
||||
function terminateWhenParentTerminates() {
|
||||
const parentPid = Number(process.env['VSCODE_PARENT_PID']);
|
||||
|
||||
if (typeof parentPid === 'number' && !isNaN(parentPid)) {
|
||||
setInterval(function () {
|
||||
try {
|
||||
process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore.
|
||||
} catch (e) {
|
||||
process.exit();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function configureCrashReporter() {
|
||||
const crashReporterOptionsRaw = process.env['CRASH_REPORTER_START_OPTIONS'];
|
||||
if (typeof crashReporterOptionsRaw === 'string') {
|
||||
try {
|
||||
const crashReporterOptions = JSON.parse(crashReporterOptionsRaw);
|
||||
if (crashReporterOptions) {
|
||||
process['crashReporter'].start(crashReporterOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
60
lib/vscode/src/bootstrap-node.js
vendored
Normal file
60
lib/vscode/src/bootstrap-node.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Add support for redirecting the loading of node modules
|
||||
*
|
||||
* @param {string} injectPath
|
||||
*/
|
||||
exports.injectNodeModuleLookupPath = function (injectPath) {
|
||||
if (!injectPath) {
|
||||
throw new Error('Missing injectPath');
|
||||
}
|
||||
|
||||
const Module = require('module');
|
||||
const path = require('path');
|
||||
|
||||
const nodeModulesPath = path.join(__dirname, '../node_modules');
|
||||
|
||||
// @ts-ignore
|
||||
const originalResolveLookupPaths = Module._resolveLookupPaths;
|
||||
|
||||
// @ts-ignore
|
||||
Module._resolveLookupPaths = function (moduleName, parent) {
|
||||
const paths = originalResolveLookupPaths(moduleName, parent);
|
||||
if (Array.isArray(paths)) {
|
||||
for (let i = 0, len = paths.length; i < len; i++) {
|
||||
if (paths[i] === nodeModulesPath) {
|
||||
paths.splice(i, 0, injectPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
};
|
||||
};
|
||||
|
||||
exports.removeGlobalNodeModuleLookupPaths = function () {
|
||||
const Module = require('module');
|
||||
// @ts-ignore
|
||||
const globalPaths = Module.globalPaths;
|
||||
|
||||
// @ts-ignore
|
||||
const originalResolveLookupPaths = Module._resolveLookupPaths;
|
||||
|
||||
// @ts-ignore
|
||||
Module._resolveLookupPaths = function (moduleName, parent) {
|
||||
const paths = originalResolveLookupPaths(moduleName, parent);
|
||||
let commonSuffixLength = 0;
|
||||
while (commonSuffixLength < paths.length && paths[paths.length - 1 - commonSuffixLength] === globalPaths[globalPaths.length - 1 - commonSuffixLength]) {
|
||||
commonSuffixLength++;
|
||||
}
|
||||
return paths.slice(0, paths.length - commonSuffixLength);
|
||||
};
|
||||
};
|
||||
262
lib/vscode/src/bootstrap-window.js
vendored
Normal file
262
lib/vscode/src/bootstrap-window.js
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path="typings/require.d.ts" />
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
// Simple module style to support node.js and browser environments
|
||||
(function (globalThis, factory) {
|
||||
|
||||
// Node.js
|
||||
if (typeof exports === 'object') {
|
||||
module.exports = factory();
|
||||
}
|
||||
|
||||
// Browser
|
||||
else {
|
||||
globalThis.MonacoBootstrapWindow = factory();
|
||||
}
|
||||
}(this, function () {
|
||||
const bootstrapLib = bootstrap();
|
||||
const preloadGlobals = globals();
|
||||
const sandbox = preloadGlobals.context.sandbox;
|
||||
const webFrame = preloadGlobals.webFrame;
|
||||
const safeProcess = sandbox ? preloadGlobals.process : process;
|
||||
|
||||
/**
|
||||
* @param {string[]} modulePaths
|
||||
* @param {(result, configuration: object) => any} resultCallback
|
||||
* @param {{ forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean, canModifyDOM?: (config: object) => void, beforeLoaderConfig?: (config: object, loaderConfig: object) => void, beforeRequire?: () => void }=} options
|
||||
*/
|
||||
function load(modulePaths, resultCallback, options) {
|
||||
const args = parseURLQueryArgs();
|
||||
/**
|
||||
* // configuration: INativeWindowConfiguration
|
||||
* @type {{
|
||||
* zoomLevel?: number,
|
||||
* extensionDevelopmentPath?: string[],
|
||||
* extensionTestsPath?: string,
|
||||
* userEnv?: { [key: string]: string | undefined },
|
||||
* appRoot?: string,
|
||||
* nodeCachedDataDir?: string
|
||||
* }} */
|
||||
const configuration = JSON.parse(args['config'] || '{}') || {};
|
||||
|
||||
// Apply zoom level early to avoid glitches
|
||||
const zoomLevel = configuration.zoomLevel;
|
||||
if (typeof zoomLevel === 'number' && zoomLevel !== 0) {
|
||||
webFrame.setZoomLevel(zoomLevel);
|
||||
}
|
||||
|
||||
// Error handler
|
||||
safeProcess.on('uncaughtException', function (error) {
|
||||
onUnexpectedError(error, enableDeveloperTools);
|
||||
});
|
||||
|
||||
// Developer tools
|
||||
const enableDeveloperTools = (safeProcess.env['VSCODE_DEV'] || !!configuration.extensionDevelopmentPath) && !configuration.extensionTestsPath;
|
||||
let developerToolsUnbind;
|
||||
if (enableDeveloperTools || (options && options.forceEnableDeveloperKeybindings)) {
|
||||
developerToolsUnbind = registerDeveloperKeybindings(options && options.disallowReloadKeybinding);
|
||||
}
|
||||
|
||||
// Correctly inherit the parent's environment (TODO@sandbox non-sandboxed only)
|
||||
if (!sandbox) {
|
||||
Object.assign(safeProcess.env, configuration.userEnv);
|
||||
}
|
||||
|
||||
// Enable ASAR support (TODO@sandbox non-sandboxed only)
|
||||
if (!sandbox) {
|
||||
globalThis.MonacoBootstrap.enableASARSupport(configuration.appRoot);
|
||||
}
|
||||
|
||||
if (options && typeof options.canModifyDOM === 'function') {
|
||||
options.canModifyDOM(configuration);
|
||||
}
|
||||
|
||||
// Get the nls configuration into the process.env as early as possible (TODO@sandbox non-sandboxed only)
|
||||
const nlsConfig = sandbox ? { availableLanguages: {} } : globalThis.MonacoBootstrap.setupNLS();
|
||||
|
||||
let locale = nlsConfig.availableLanguages['*'] || 'en';
|
||||
if (locale === 'zh-tw') {
|
||||
locale = 'zh-Hant';
|
||||
} else if (locale === 'zh-cn') {
|
||||
locale = 'zh-Hans';
|
||||
}
|
||||
|
||||
window.document.documentElement.setAttribute('lang', locale);
|
||||
|
||||
// do not advertise AMD to avoid confusing UMD modules loaded with nodejs
|
||||
if (!sandbox) {
|
||||
window['define'] = undefined;
|
||||
}
|
||||
|
||||
// replace the patched electron fs with the original node fs for all AMD code (TODO@sandbox non-sandboxed only)
|
||||
if (!sandbox) {
|
||||
require.define('fs', ['original-fs'], function (originalFS) { return originalFS; });
|
||||
}
|
||||
|
||||
window['MonacoEnvironment'] = {};
|
||||
|
||||
const loaderConfig = {
|
||||
baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32' })}/out`,
|
||||
'vs/nls': nlsConfig
|
||||
};
|
||||
|
||||
// Enable loading of node modules:
|
||||
// - sandbox: we list paths of webpacked modules to help the loader
|
||||
// - non-sandbox: we signal that any module that does not begin with
|
||||
// `vs/` should be loaded using node.js require()
|
||||
if (sandbox) {
|
||||
loaderConfig.paths = {
|
||||
'vscode-textmate': `../node_modules/vscode-textmate/release/main`,
|
||||
'vscode-oniguruma': `../node_modules/vscode-oniguruma/release/main`,
|
||||
'xterm': `../node_modules/xterm/lib/xterm.js`,
|
||||
'xterm-addon-search': `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
|
||||
'xterm-addon-unicode11': `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||
'xterm-addon-webgl': `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||
'iconv-lite-umd': `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||
'jschardet': `../node_modules/jschardet/dist/jschardet.min.js`,
|
||||
};
|
||||
} else {
|
||||
loaderConfig.amdModulesPattern = /^vs\//;
|
||||
}
|
||||
|
||||
// cached data config
|
||||
if (configuration.nodeCachedDataDir) {
|
||||
loaderConfig.nodeCachedData = {
|
||||
path: configuration.nodeCachedDataDir,
|
||||
seed: modulePaths.join('')
|
||||
};
|
||||
}
|
||||
|
||||
if (options && typeof options.beforeLoaderConfig === 'function') {
|
||||
options.beforeLoaderConfig(configuration, loaderConfig);
|
||||
}
|
||||
|
||||
require.config(loaderConfig);
|
||||
|
||||
if (nlsConfig.pseudo) {
|
||||
require(['vs/nls'], function (nlsPlugin) {
|
||||
nlsPlugin.setPseudoTranslation(nlsConfig.pseudo);
|
||||
});
|
||||
}
|
||||
|
||||
if (options && typeof options.beforeRequire === 'function') {
|
||||
options.beforeRequire();
|
||||
}
|
||||
|
||||
require(modulePaths, result => {
|
||||
try {
|
||||
const callbackResult = resultCallback(result, configuration);
|
||||
if (callbackResult && typeof callbackResult.then === 'function') {
|
||||
callbackResult.then(() => {
|
||||
if (developerToolsUnbind && options && options.removeDeveloperKeybindingsAfterLoad) {
|
||||
developerToolsUnbind();
|
||||
}
|
||||
}, error => {
|
||||
onUnexpectedError(error, enableDeveloperTools);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
onUnexpectedError(error, enableDeveloperTools);
|
||||
}
|
||||
}, onUnexpectedError);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{[param: string]: string }}
|
||||
*/
|
||||
function parseURLQueryArgs() {
|
||||
const search = window.location.search || '';
|
||||
|
||||
return search.split(/[?&]/)
|
||||
.filter(function (param) { return !!param; })
|
||||
.map(function (param) { return param.split('='); })
|
||||
.filter(function (param) { return param.length === 2; })
|
||||
.reduce(function (r, param) { r[param[0]] = decodeURIComponent(param[1]); return r; }, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} disallowReloadKeybinding
|
||||
* @returns {() => void}
|
||||
*/
|
||||
function registerDeveloperKeybindings(disallowReloadKeybinding) {
|
||||
const ipcRenderer = preloadGlobals.ipcRenderer;
|
||||
|
||||
const extractKey = function (e) {
|
||||
return [
|
||||
e.ctrlKey ? 'ctrl-' : '',
|
||||
e.metaKey ? 'meta-' : '',
|
||||
e.altKey ? 'alt-' : '',
|
||||
e.shiftKey ? 'shift-' : '',
|
||||
e.keyCode
|
||||
].join('');
|
||||
};
|
||||
|
||||
// Devtools & reload support
|
||||
const TOGGLE_DEV_TOOLS_KB = (safeProcess.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I
|
||||
const TOGGLE_DEV_TOOLS_KB_ALT = '123'; // F12
|
||||
const RELOAD_KB = (safeProcess.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R
|
||||
|
||||
let listener = function (e) {
|
||||
const key = extractKey(e);
|
||||
if (key === TOGGLE_DEV_TOOLS_KB || key === TOGGLE_DEV_TOOLS_KB_ALT) {
|
||||
ipcRenderer.send('vscode:toggleDevTools');
|
||||
} else if (key === RELOAD_KB && !disallowReloadKeybinding) {
|
||||
ipcRenderer.send('vscode:reloadWindow');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', listener);
|
||||
|
||||
return function () {
|
||||
if (listener) {
|
||||
window.removeEventListener('keydown', listener);
|
||||
listener = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | Error} error
|
||||
* @param {boolean} [enableDeveloperTools]
|
||||
*/
|
||||
function onUnexpectedError(error, enableDeveloperTools) {
|
||||
if (enableDeveloperTools) {
|
||||
const ipcRenderer = preloadGlobals.ipcRenderer;
|
||||
ipcRenderer.send('vscode:openDevTools');
|
||||
}
|
||||
|
||||
console.error(`[uncaught exception]: ${error}`);
|
||||
|
||||
if (error && typeof error !== 'string' && error.stack) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{ fileUriFromPath: (path: string, config: { isWindows?: boolean, scheme?: string, fallbackAuthority?: string }) => string; }}
|
||||
*/
|
||||
function bootstrap() {
|
||||
// @ts-ignore (defined in bootstrap.js)
|
||||
return window.MonacoBootstrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {typeof import('./vs/base/parts/sandbox/electron-sandbox/globals')}
|
||||
*/
|
||||
function globals() {
|
||||
// @ts-ignore (defined in globals.js)
|
||||
return window.vscode;
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
globals
|
||||
};
|
||||
}));
|
||||
274
lib/vscode/src/bootstrap.js
vendored
Normal file
274
lib/vscode/src/bootstrap.js
vendored
Normal file
@@ -0,0 +1,274 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
// Simple module style to support node.js and browser environments
|
||||
(function (globalThis, factory) {
|
||||
|
||||
// Node.js
|
||||
if (typeof exports === 'object') {
|
||||
module.exports = factory();
|
||||
}
|
||||
|
||||
// Browser
|
||||
else {
|
||||
globalThis.MonacoBootstrap = factory();
|
||||
}
|
||||
}(this, function () {
|
||||
const Module = typeof require === 'function' ? require('module') : undefined;
|
||||
const path = typeof require === 'function' ? require('path') : undefined;
|
||||
const fs = typeof require === 'function' ? require('fs') : undefined;
|
||||
|
||||
//#region global bootstrapping
|
||||
|
||||
// increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
|
||||
Error.stackTraceLimit = 100;
|
||||
|
||||
// Workaround for Electron not installing a handler to ignore SIGPIPE
|
||||
// (https://github.com/electron/electron/issues/13254)
|
||||
if (typeof process !== 'undefined') {
|
||||
process.on('SIGPIPE', () => {
|
||||
console.error(new Error('Unexpected SIGPIPE'));
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Add support for using node_modules.asar
|
||||
|
||||
/**
|
||||
* @param {string} appRoot
|
||||
*/
|
||||
function enableASARSupport(appRoot) {
|
||||
if (!path || !Module) {
|
||||
console.warn('enableASARSupport() is only available in node.js environments');
|
||||
return;
|
||||
}
|
||||
|
||||
let NODE_MODULES_PATH = appRoot ? path.join(appRoot, 'node_modules') : undefined;
|
||||
if (!NODE_MODULES_PATH) {
|
||||
NODE_MODULES_PATH = path.join(__dirname, '../node_modules');
|
||||
} else {
|
||||
// use the drive letter casing of __dirname
|
||||
if (process.platform === 'win32') {
|
||||
NODE_MODULES_PATH = __dirname.substr(0, 1) + NODE_MODULES_PATH.substr(1);
|
||||
}
|
||||
}
|
||||
|
||||
const NODE_MODULES_ASAR_PATH = `${NODE_MODULES_PATH}.asar`;
|
||||
|
||||
// @ts-ignore
|
||||
const originalResolveLookupPaths = Module._resolveLookupPaths;
|
||||
|
||||
// @ts-ignore
|
||||
Module._resolveLookupPaths = function (request, parent) {
|
||||
const paths = originalResolveLookupPaths(request, parent);
|
||||
if (Array.isArray(paths)) {
|
||||
for (let i = 0, len = paths.length; i < len; i++) {
|
||||
if (paths[i] === NODE_MODULES_PATH) {
|
||||
paths.splice(i, 0, NODE_MODULES_ASAR_PATH);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region URI helpers
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {{ isWindows?: boolean, scheme?: string, fallbackAuthority?: string }} config
|
||||
* @returns {string}
|
||||
*/
|
||||
function fileUriFromPath(path, config) {
|
||||
|
||||
// Since we are building a URI, we normalize any backlsash
|
||||
// to slashes and we ensure that the path begins with a '/'.
|
||||
let pathName = path.replace(/\\/g, '/');
|
||||
if (pathName.length > 0 && pathName.charAt(0) !== '/') {
|
||||
pathName = `/${pathName}`;
|
||||
}
|
||||
|
||||
/** @type {string} */
|
||||
let uri;
|
||||
|
||||
// Windows: in order to support UNC paths (which start with '//')
|
||||
// that have their own authority, we do not use the provided authority
|
||||
// but rather preserve it.
|
||||
if (config.isWindows && pathName.startsWith('//')) {
|
||||
uri = encodeURI(`${config.scheme || 'file'}:${pathName}`);
|
||||
}
|
||||
|
||||
// Otherwise we optionally add the provided authority if specified
|
||||
else {
|
||||
uri = encodeURI(`${config.scheme || 'file'}://${config.fallbackAuthority || ''}${pathName}`);
|
||||
}
|
||||
|
||||
return uri.replace(/#/g, '%23');
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region NLS helpers
|
||||
|
||||
/**
|
||||
* @returns {{locale?: string, availableLanguages: {[lang: string]: string;}, pseudo?: boolean }}
|
||||
*/
|
||||
function setupNLS() {
|
||||
if (!path || !fs) {
|
||||
console.warn('setupNLS() is only available in node.js environments');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the nls configuration into the process.env as early as possible.
|
||||
let nlsConfig = { availableLanguages: {} };
|
||||
if (process.env['VSCODE_NLS_CONFIG']) {
|
||||
try {
|
||||
nlsConfig = JSON.parse(process.env['VSCODE_NLS_CONFIG']);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
||||
const bundles = Object.create(null);
|
||||
|
||||
nlsConfig.loadBundle = function (bundle, language, cb) {
|
||||
const result = bundles[bundle];
|
||||
if (result) {
|
||||
cb(undefined, result);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, `${bundle.replace(/\//g, '!')}.nls.json`);
|
||||
fs.promises.readFile(bundleFile, 'utf8').then(function (content) {
|
||||
const json = JSON.parse(content);
|
||||
bundles[bundle] = json;
|
||||
|
||||
cb(undefined, json);
|
||||
}).catch((error) => {
|
||||
try {
|
||||
if (nlsConfig._corruptedFile) {
|
||||
fs.promises.writeFile(nlsConfig._corruptedFile, 'corrupted', 'utf8').catch(function (error) { console.error(error); });
|
||||
}
|
||||
} finally {
|
||||
cb(error, undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return nlsConfig;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Portable helpers
|
||||
|
||||
/**
|
||||
* @param {{ portable: string; applicationName: string; }} product
|
||||
* @returns {{ portableDataPath: string; isPortable: boolean; }}
|
||||
*/
|
||||
function configurePortable(product) {
|
||||
if (!path || !fs) {
|
||||
console.warn('configurePortable() is only available in node.js environments');
|
||||
return;
|
||||
}
|
||||
|
||||
const appRoot = path.dirname(__dirname);
|
||||
|
||||
function getApplicationPath() {
|
||||
if (process.env['VSCODE_DEV']) {
|
||||
return appRoot;
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return path.dirname(path.dirname(path.dirname(appRoot)));
|
||||
}
|
||||
|
||||
return path.dirname(path.dirname(appRoot));
|
||||
}
|
||||
|
||||
function getPortableDataPath() {
|
||||
if (process.env['VSCODE_PORTABLE']) {
|
||||
return process.env['VSCODE_PORTABLE'];
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
return path.join(getApplicationPath(), 'data');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const portableDataName = product.portable || `${product.applicationName}-portable-data`;
|
||||
return path.join(path.dirname(getApplicationPath()), portableDataName);
|
||||
}
|
||||
|
||||
const portableDataPath = getPortableDataPath();
|
||||
const isPortable = !('target' in product) && fs.existsSync(portableDataPath);
|
||||
const portableTempPath = path.join(portableDataPath, 'tmp');
|
||||
const isTempPortable = isPortable && fs.existsSync(portableTempPath);
|
||||
|
||||
if (isPortable) {
|
||||
process.env['VSCODE_PORTABLE'] = portableDataPath;
|
||||
} else {
|
||||
delete process.env['VSCODE_PORTABLE'];
|
||||
}
|
||||
|
||||
if (isTempPortable) {
|
||||
if (process.platform === 'win32') {
|
||||
process.env['TMP'] = portableTempPath;
|
||||
process.env['TEMP'] = portableTempPath;
|
||||
} else {
|
||||
process.env['TMPDIR'] = portableTempPath;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
portableDataPath,
|
||||
isPortable
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region ApplicationInsights
|
||||
|
||||
// Prevents appinsights from monkey patching modules.
|
||||
// This should be called before importing the applicationinsights module
|
||||
function avoidMonkeyPatchFromAppInsights() {
|
||||
if (typeof process === 'undefined') {
|
||||
console.warn('avoidMonkeyPatchFromAppInsights() is only available in node.js environments');
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] = true; // Skip monkey patching of 3rd party modules by appinsights
|
||||
global['diagnosticsSource'] = {}; // Prevents diagnostic channel (which patches "require") from initializing entirely
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
return {
|
||||
enableASARSupport,
|
||||
avoidMonkeyPatchFromAppInsights,
|
||||
configurePortable,
|
||||
setupNLS,
|
||||
fileUriFromPath
|
||||
};
|
||||
}));
|
||||
32
lib/vscode/src/buildfile.js
Normal file
32
lib/vscode/src/buildfile.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
function entrypoint(name) {
|
||||
return [{ name: name, include: [], exclude: ['vs/css', 'vs/nls'] }];
|
||||
}
|
||||
|
||||
exports.base = [{
|
||||
name: 'vs/base/common/worker/simpleWorker',
|
||||
include: ['vs/editor/common/services/editorSimpleWorker'],
|
||||
prepend: ['vs/loader.js'],
|
||||
append: ['vs/base/worker/workerMain'],
|
||||
dest: 'vs/base/worker/workerMain.js'
|
||||
}];
|
||||
|
||||
exports.workerExtensionHost = [entrypoint('vs/workbench/services/extensions/worker/extensionHostWorker')];
|
||||
exports.workerNotebook = [entrypoint('vs/workbench/contrib/notebook/common/services/notebookSimpleWorker')];
|
||||
|
||||
exports.workbenchDesktop = require('./vs/workbench/buildfile.desktop').collectModules();
|
||||
exports.workbenchWeb = require('./vs/workbench/buildfile.web').collectModules();
|
||||
|
||||
exports.keyboardMaps = [
|
||||
entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'),
|
||||
entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin'),
|
||||
entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win')
|
||||
];
|
||||
|
||||
exports.code = require('./vs/code/buildfile').collectModules();
|
||||
|
||||
exports.entrypoint = entrypoint;
|
||||
22
lib/vscode/src/cli.js
Normal file
22
lib/vscode/src/cli.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
const bootstrap = require('./bootstrap');
|
||||
const product = require('../product.json');
|
||||
|
||||
// Avoid Monkey Patches from Application Insights
|
||||
bootstrap.avoidMonkeyPatchFromAppInsights();
|
||||
|
||||
// Enable portable support
|
||||
bootstrap.configurePortable(product);
|
||||
|
||||
// Enable ASAR support
|
||||
bootstrap.enableASARSupport();
|
||||
|
||||
// Load CLI through AMD loader
|
||||
require('./bootstrap-amd').load('vs/code/node/cli');
|
||||
596
lib/vscode/src/main.js
Normal file
596
lib/vscode/src/main.js
Normal file
@@ -0,0 +1,596 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
const perf = require('./vs/base/common/performance');
|
||||
const lp = require('./vs/base/node/languagePacks');
|
||||
|
||||
perf.mark('main:started');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const bootstrap = require('./bootstrap');
|
||||
const paths = require('./paths');
|
||||
/** @type {any} */
|
||||
const product = require('../product.json');
|
||||
const { app, protocol, crashReporter } = require('electron');
|
||||
|
||||
// Disable render process reuse, we still have
|
||||
// non-context aware native modules in the renderer.
|
||||
app.allowRendererProcessReuse = false;
|
||||
|
||||
// Enable portable support
|
||||
const portable = bootstrap.configurePortable(product);
|
||||
|
||||
// Enable ASAR support
|
||||
bootstrap.enableASARSupport();
|
||||
|
||||
// Set userData path before app 'ready' event
|
||||
const args = parseCLIArgs();
|
||||
const userDataPath = getUserDataPath(args);
|
||||
app.setPath('userData', userDataPath);
|
||||
|
||||
// Configure static command line arguments
|
||||
const argvConfig = configureCommandlineSwitchesSync(args);
|
||||
|
||||
// If a crash-reporter-directory is specified we store the crash reports
|
||||
// in the specified directory and don't upload them to the crash server.
|
||||
let crashReporterDirectory = args['crash-reporter-directory'];
|
||||
let submitURL = '';
|
||||
if (crashReporterDirectory) {
|
||||
crashReporterDirectory = path.normalize(crashReporterDirectory);
|
||||
|
||||
if (!path.isAbsolute(crashReporterDirectory)) {
|
||||
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
|
||||
app.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(crashReporterDirectory)) {
|
||||
try {
|
||||
fs.mkdirSync(crashReporterDirectory);
|
||||
} catch (error) {
|
||||
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
|
||||
app.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Crashes are stored in the crashDumps directory by default, so we
|
||||
// need to change that directory to the provided one
|
||||
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
|
||||
app.setPath('crashDumps', crashReporterDirectory);
|
||||
} else {
|
||||
const appCenter = product.appCenter;
|
||||
// Disable Appcenter crash reporting if
|
||||
// * --crash-reporter-directory is specified
|
||||
// * enable-crash-reporter runtime argument is set to 'false'
|
||||
// * --disable-crash-reporter command line parameter is set
|
||||
if (appCenter && argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter']) {
|
||||
const isWindows = (process.platform === 'win32');
|
||||
const isLinux = (process.platform === 'linux');
|
||||
const crashReporterId = argvConfig['crash-reporter-id'];
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
if (uuidPattern.test(crashReporterId)) {
|
||||
submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin;
|
||||
submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId);
|
||||
// Send the id for child node process that are explicitly starting crash reporter.
|
||||
// For vscode this is ExtensionHost process currently.
|
||||
const argv = process.argv;
|
||||
const endOfArgsMarkerIndex = argv.indexOf('--');
|
||||
if (endOfArgsMarkerIndex === -1) {
|
||||
argv.push('--crash-reporter-id', crashReporterId);
|
||||
} else {
|
||||
// if the we have an argument "--" (end of argument marker)
|
||||
// we cannot add arguments at the end. rather, we add
|
||||
// arguments before the "--" marker.
|
||||
argv.splice(endOfArgsMarkerIndex, 0, '--crash-reporter-id', crashReporterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start crash reporter for all processes
|
||||
const productName = (product.crashReporter ? product.crashReporter.productName : undefined) || product.nameShort;
|
||||
const companyName = (product.crashReporter ? product.crashReporter.companyName : undefined) || 'Microsoft';
|
||||
crashReporter.start({
|
||||
companyName: companyName,
|
||||
productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName,
|
||||
submitURL,
|
||||
uploadToServer: !crashReporterDirectory
|
||||
});
|
||||
|
||||
// Set logs path before app 'ready' event if running portable
|
||||
// to ensure that no 'logs' folder is created on disk at a
|
||||
// location outside of the portable directory
|
||||
// (https://github.com/microsoft/vscode/issues/56651)
|
||||
if (portable.isPortable) {
|
||||
app.setAppLogsPath(path.join(userDataPath, 'logs'));
|
||||
}
|
||||
|
||||
// Update cwd based on environment and platform
|
||||
setCurrentWorkingDirectory();
|
||||
|
||||
// Register custom schemes with privileges
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'vscode-webview',
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
}
|
||||
}, {
|
||||
scheme: 'vscode-webview-resource',
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
// Global app listeners
|
||||
registerListeners();
|
||||
|
||||
// Cached data
|
||||
const nodeCachedDataDir = getNodeCachedDir();
|
||||
|
||||
/**
|
||||
* Support user defined locale: load it early before app('ready')
|
||||
* to have more things running in parallel.
|
||||
*
|
||||
* @type {Promise<import('./vs/base/node/languagePacks').NLSConfiguration>} nlsConfig | undefined
|
||||
*/
|
||||
let nlsConfigurationPromise = undefined;
|
||||
|
||||
const metaDataFile = path.join(__dirname, 'nls.metadata.json');
|
||||
const locale = getUserDefinedLocale(argvConfig);
|
||||
if (locale) {
|
||||
nlsConfigurationPromise = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
|
||||
}
|
||||
|
||||
// Load our code once ready
|
||||
app.once('ready', function () {
|
||||
if (args['trace']) {
|
||||
const contentTracing = require('electron').contentTracing;
|
||||
|
||||
const traceOptions = {
|
||||
categoryFilter: args['trace-category-filter'] || '*',
|
||||
traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
|
||||
};
|
||||
|
||||
contentTracing.startRecording(traceOptions).finally(() => onReady());
|
||||
} else {
|
||||
onReady();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Main startup routine
|
||||
*
|
||||
* @param {string | undefined} cachedDataDir
|
||||
* @param {import('./vs/base/node/languagePacks').NLSConfiguration} nlsConfig
|
||||
*/
|
||||
function startup(cachedDataDir, nlsConfig) {
|
||||
nlsConfig._languagePackSupport = true;
|
||||
|
||||
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
|
||||
process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
|
||||
|
||||
// Load main in AMD
|
||||
perf.mark('willLoadMainBundle');
|
||||
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
|
||||
perf.mark('didLoadMainBundle');
|
||||
});
|
||||
}
|
||||
|
||||
async function onReady() {
|
||||
perf.mark('main:appReady');
|
||||
|
||||
try {
|
||||
const [cachedDataDir, nlsConfig] = await Promise.all([nodeCachedDataDir.ensureExists(), resolveNlsConfiguration()]);
|
||||
|
||||
startup(cachedDataDir, nlsConfig);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ [arg: string]: any; '--'?: string[]; _: string[]; }} NativeParsedArgs
|
||||
*
|
||||
* @param {NativeParsedArgs} cliArgs
|
||||
*/
|
||||
function configureCommandlineSwitchesSync(cliArgs) {
|
||||
const SUPPORTED_ELECTRON_SWITCHES = [
|
||||
|
||||
// alias from us for --disable-gpu
|
||||
'disable-hardware-acceleration',
|
||||
|
||||
// provided by Electron
|
||||
'disable-color-correct-rendering',
|
||||
|
||||
// override for the color profile to use
|
||||
'force-color-profile'
|
||||
];
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
|
||||
// Force enable screen readers on Linux via this flag
|
||||
SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility');
|
||||
}
|
||||
|
||||
const SUPPORTED_MAIN_PROCESS_SWITCHES = [
|
||||
|
||||
// Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775
|
||||
'enable-proposed-api'
|
||||
];
|
||||
|
||||
// Read argv config
|
||||
const argvConfig = readArgvConfigSync();
|
||||
|
||||
Object.keys(argvConfig).forEach(argvKey => {
|
||||
const argvValue = argvConfig[argvKey];
|
||||
|
||||
// Append Electron flags to Electron
|
||||
if (SUPPORTED_ELECTRON_SWITCHES.indexOf(argvKey) !== -1) {
|
||||
|
||||
// Color profile
|
||||
if (argvKey === 'force-color-profile') {
|
||||
if (argvValue) {
|
||||
app.commandLine.appendSwitch(argvKey, argvValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Others
|
||||
else if (argvValue === true || argvValue === 'true') {
|
||||
if (argvKey === 'disable-hardware-acceleration') {
|
||||
app.disableHardwareAcceleration(); // needs to be called explicitly
|
||||
} else {
|
||||
app.commandLine.appendSwitch(argvKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append main process flags to process.argv
|
||||
else if (SUPPORTED_MAIN_PROCESS_SWITCHES.indexOf(argvKey) !== -1) {
|
||||
if (argvKey === 'enable-proposed-api') {
|
||||
if (Array.isArray(argvValue)) {
|
||||
argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id));
|
||||
} else {
|
||||
console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Support JS Flags
|
||||
const jsFlags = getJSFlags(cliArgs);
|
||||
if (jsFlags) {
|
||||
app.commandLine.appendSwitch('js-flags', jsFlags);
|
||||
}
|
||||
|
||||
return argvConfig;
|
||||
}
|
||||
|
||||
function readArgvConfigSync() {
|
||||
|
||||
// Read or create the argv.json config file sync before app('ready')
|
||||
const argvConfigPath = getArgvConfigPath();
|
||||
let argvConfig;
|
||||
try {
|
||||
argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString()));
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') {
|
||||
createDefaultArgvConfigSync(argvConfigPath);
|
||||
} else {
|
||||
console.warn(`Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
if (!argvConfig) {
|
||||
argvConfig = {
|
||||
'disable-color-correct-rendering': true // Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791)
|
||||
};
|
||||
}
|
||||
|
||||
return argvConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} argvConfigPath
|
||||
*/
|
||||
function createDefaultArgvConfigSync(argvConfigPath) {
|
||||
try {
|
||||
|
||||
// Ensure argv config parent exists
|
||||
const argvConfigPathDirname = path.dirname(argvConfigPath);
|
||||
if (!fs.existsSync(argvConfigPathDirname)) {
|
||||
fs.mkdirSync(argvConfigPathDirname);
|
||||
}
|
||||
|
||||
// Default argv content
|
||||
const defaultArgvConfigContent = [
|
||||
'// This configuration file allows you to pass permanent command line arguments to VS Code.',
|
||||
'// Only a subset of arguments is currently supported to reduce the likelihood of breaking',
|
||||
'// the installation.',
|
||||
'//',
|
||||
'// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT',
|
||||
'//',
|
||||
'// NOTE: Changing this file requires a restart of VS Code.',
|
||||
'{',
|
||||
' // Use software rendering instead of hardware accelerated rendering.',
|
||||
' // This can help in cases where you see rendering issues in VS Code.',
|
||||
' // "disable-hardware-acceleration": true,',
|
||||
'',
|
||||
' // Enabled by default by VS Code to resolve color issues in the renderer',
|
||||
' // See https://github.com/microsoft/vscode/issues/51791 for details',
|
||||
' "disable-color-correct-rendering": true',
|
||||
'}'
|
||||
];
|
||||
|
||||
// Create initial argv.json with default content
|
||||
fs.writeFileSync(argvConfigPath, defaultArgvConfigContent.join('\n'));
|
||||
} catch (error) {
|
||||
console.error(`Unable to create argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`);
|
||||
}
|
||||
}
|
||||
|
||||
function getArgvConfigPath() {
|
||||
const vscodePortable = process.env['VSCODE_PORTABLE'];
|
||||
if (vscodePortable) {
|
||||
return path.join(vscodePortable, 'argv.json');
|
||||
}
|
||||
|
||||
let dataFolderName = product.dataFolderName;
|
||||
if (process.env['VSCODE_DEV']) {
|
||||
dataFolderName = `${dataFolderName}-dev`;
|
||||
}
|
||||
|
||||
return path.join(os.homedir(), dataFolderName, 'argv.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NativeParsedArgs} cliArgs
|
||||
* @returns {string}
|
||||
*/
|
||||
function getJSFlags(cliArgs) {
|
||||
const jsFlags = [];
|
||||
|
||||
// Add any existing JS flags we already got from the command line
|
||||
if (cliArgs['js-flags']) {
|
||||
jsFlags.push(cliArgs['js-flags']);
|
||||
}
|
||||
|
||||
// Support max-memory flag
|
||||
if (cliArgs['max-memory'] && !/max_old_space_size=(\d+)/g.exec(cliArgs['js-flags'])) {
|
||||
jsFlags.push(`--max_old_space_size=${cliArgs['max-memory']}`);
|
||||
}
|
||||
|
||||
return jsFlags.length > 0 ? jsFlags.join(' ') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NativeParsedArgs} cliArgs
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function getUserDataPath(cliArgs) {
|
||||
if (portable.isPortable) {
|
||||
return path.join(portable.portableDataPath, 'user-data');
|
||||
}
|
||||
|
||||
return path.resolve(cliArgs['user-data-dir'] || paths.getDefaultUserDataPath(process.platform));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {NativeParsedArgs}
|
||||
*/
|
||||
function parseCLIArgs() {
|
||||
const minimist = require('minimist');
|
||||
|
||||
return minimist(process.argv, {
|
||||
string: [
|
||||
'user-data-dir',
|
||||
'locale',
|
||||
'js-flags',
|
||||
'max-memory',
|
||||
'crash-reporter-directory'
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentWorkingDirectory() {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
process.env['VSCODE_CWD'] = process.cwd(); // remember as environment variable
|
||||
process.chdir(path.dirname(app.getPath('exe'))); // always set application folder as cwd
|
||||
} else if (process.env['VSCODE_CWD']) {
|
||||
process.chdir(process.env['VSCODE_CWD']);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function registerListeners() {
|
||||
|
||||
/**
|
||||
* macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
|
||||
* the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
const macOpenFiles = [];
|
||||
global['macOpenFiles'] = macOpenFiles;
|
||||
app.on('open-file', function (event, path) {
|
||||
macOpenFiles.push(path);
|
||||
});
|
||||
|
||||
/**
|
||||
* macOS: react to open-url requests.
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
const openUrls = [];
|
||||
const onOpenUrl = function (event, url) {
|
||||
event.preventDefault();
|
||||
|
||||
openUrls.push(url);
|
||||
};
|
||||
|
||||
app.on('will-finish-launching', function () {
|
||||
app.on('open-url', onOpenUrl);
|
||||
});
|
||||
|
||||
global['getOpenUrls'] = function () {
|
||||
app.removeListener('open-url', onOpenUrl);
|
||||
|
||||
return openUrls;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ ensureExists: () => Promise<string | undefined> }}
|
||||
*/
|
||||
function getNodeCachedDir() {
|
||||
return new class {
|
||||
|
||||
constructor() {
|
||||
this.value = this._compute();
|
||||
}
|
||||
|
||||
async ensureExists() {
|
||||
try {
|
||||
await mkdirp(this.value);
|
||||
|
||||
return this.value;
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
_compute() {
|
||||
if (process.argv.indexOf('--no-cached-data') > 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// IEnvironmentService.isBuilt
|
||||
if (process.env['VSCODE_DEV']) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// find commit id
|
||||
const commit = product.commit;
|
||||
if (!commit) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return path.join(userDataPath, 'CachedData', commit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function mkdirp(dir) {
|
||||
const fs = require('fs');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? reject(err) : resolve(dir));
|
||||
});
|
||||
}
|
||||
|
||||
//#region NLS Support
|
||||
|
||||
/**
|
||||
* Resolve the NLS configuration
|
||||
*
|
||||
* @return {Promise<import('./vs/base/node/languagePacks').NLSConfiguration>}
|
||||
*/
|
||||
async function resolveNlsConfiguration() {
|
||||
|
||||
// First, we need to test a user defined locale. If it fails we try the app locale.
|
||||
// If that fails we fall back to English.
|
||||
let nlsConfiguration = nlsConfigurationPromise ? await nlsConfigurationPromise : undefined;
|
||||
if (!nlsConfiguration) {
|
||||
|
||||
// Try to use the app locale. Please note that the app locale is only
|
||||
// valid after we have received the app ready event. This is why the
|
||||
// code is here.
|
||||
let appLocale = app.getLocale();
|
||||
if (!appLocale) {
|
||||
nlsConfiguration = { locale: 'en', availableLanguages: {} };
|
||||
} else {
|
||||
|
||||
// See above the comment about the loader and case sensitiviness
|
||||
appLocale = appLocale.toLowerCase();
|
||||
|
||||
nlsConfiguration = await lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale);
|
||||
if (!nlsConfiguration) {
|
||||
nlsConfiguration = { locale: appLocale, availableLanguages: {} };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We received a valid nlsConfig from a user defined locale
|
||||
}
|
||||
|
||||
return nlsConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {string}
|
||||
*/
|
||||
function stripComments(content) {
|
||||
const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
|
||||
|
||||
return content.replace(regexp, function (match, m1, m2, m3, m4) {
|
||||
// Only one of m1, m2, m3, m4 matches
|
||||
if (m3) {
|
||||
// A block comment. Replace with nothing
|
||||
return '';
|
||||
} else if (m4) {
|
||||
// A line comment. If it ends in \r?\n then keep it.
|
||||
const length_1 = m4.length;
|
||||
if (length_1 > 2 && m4[length_1 - 1] === '\n') {
|
||||
return m4[length_1 - 2] === '\r' ? '\r\n' : '\n';
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// We match a string
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Language tags are case insensitive however an amd loader is case sensitive
|
||||
* To make this work on case preserving & insensitive FS we do the following:
|
||||
* the language bundles have lower case language tags and we always lower case
|
||||
* the locale we receive from the user or OS.
|
||||
*
|
||||
* @param {{ locale: string | undefined; }} argvConfig
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function getUserDefinedLocale(argvConfig) {
|
||||
const locale = args['locale'];
|
||||
if (locale) {
|
||||
return locale.toLowerCase(); // a directly provided --locale always wins
|
||||
}
|
||||
|
||||
return argvConfig.locale && typeof argvConfig.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
35
lib/vscode/src/paths.js
Normal file
35
lib/vscode/src/paths.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
const pkg = require('../package.json');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* @param {string} platform
|
||||
* @returns {string}
|
||||
*/
|
||||
function getAppDataPath(platform) {
|
||||
switch (platform) {
|
||||
case 'win32': return process.env['VSCODE_APPDATA'] || process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming');
|
||||
case 'darwin': return process.env['VSCODE_APPDATA'] || path.join(os.homedir(), 'Library', 'Application Support');
|
||||
case 'linux': return process.env['VSCODE_APPDATA'] || process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
||||
default: throw new Error('Platform not supported');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} platform
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDefaultUserDataPath(platform) {
|
||||
return path.join(getAppDataPath(platform), pkg.name);
|
||||
}
|
||||
|
||||
exports.getAppDataPath = getAppDataPath;
|
||||
exports.getDefaultUserDataPath = getDefaultUserDataPath;
|
||||
27
lib/vscode/src/tsconfig.base.json
Normal file
27
lib/vscode/src/tsconfig.base.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "amd",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"vs/*": [
|
||||
"./vs/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"ES2016.Array.Include",
|
||||
"ES2017.String",
|
||||
"ES2018.Promise",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"WebWorker.ImportScripts"
|
||||
]
|
||||
}
|
||||
}
|
||||
22
lib/vscode/src/tsconfig.json
Normal file
22
lib/vscode/src/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": false,
|
||||
"outDir": "../out/vs",
|
||||
"target": "es2017",
|
||||
"types": [
|
||||
"keytar",
|
||||
"mocha",
|
||||
"semver",
|
||||
"sinon",
|
||||
"winreg",
|
||||
"trusted-types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./typings",
|
||||
"./vs"
|
||||
]
|
||||
}
|
||||
32
lib/vscode/src/tsconfig.monaco.json
Normal file
32
lib/vscode/src/tsconfig.monaco.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"trusted-types"
|
||||
],
|
||||
"paths": {},
|
||||
"module": "amd",
|
||||
"moduleResolution": "classic",
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"target": "es6",
|
||||
"sourceMap": false,
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"typings/require.d.ts",
|
||||
"typings/thenable.d.ts",
|
||||
"vs/css.d.ts",
|
||||
"vs/monaco.d.ts",
|
||||
"vs/nls.d.ts",
|
||||
"vs/editor/*",
|
||||
"vs/base/common/*",
|
||||
"vs/base/browser/*",
|
||||
"vs/platform/*/common/*",
|
||||
"vs/platform/*/browser/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules/*"
|
||||
]
|
||||
}
|
||||
56
lib/vscode/src/typings/require.d.ts
vendored
Normal file
56
lib/vscode/src/typings/require.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare const enum LoaderEventType {
|
||||
LoaderAvailable = 1,
|
||||
|
||||
BeginLoadingScript = 10,
|
||||
EndLoadingScriptOK = 11,
|
||||
EndLoadingScriptError = 12,
|
||||
|
||||
BeginInvokeFactory = 21,
|
||||
EndInvokeFactory = 22,
|
||||
|
||||
NodeBeginEvaluatingScript = 31,
|
||||
NodeEndEvaluatingScript = 32,
|
||||
|
||||
NodeBeginNativeRequire = 33,
|
||||
NodeEndNativeRequire = 34,
|
||||
|
||||
CachedDataFound = 60,
|
||||
CachedDataMissed = 61,
|
||||
CachedDataRejected = 62,
|
||||
CachedDataCreated = 63,
|
||||
}
|
||||
|
||||
declare class LoaderEvent {
|
||||
readonly type: LoaderEventType;
|
||||
readonly timestamp: number;
|
||||
readonly detail: string;
|
||||
}
|
||||
|
||||
declare const define: {
|
||||
(moduleName: string, dependencies: string[], callback: (...args: any[]) => any): any;
|
||||
(moduleName: string, dependencies: string[], definition: any): any;
|
||||
(moduleName: string, callback: (...args: any[]) => any): any;
|
||||
(moduleName: string, definition: any): any;
|
||||
(dependencies: string[], callback: (...args: any[]) => any): any;
|
||||
(dependencies: string[], definition: any): any;
|
||||
};
|
||||
|
||||
interface NodeRequire {
|
||||
/**
|
||||
* @deprecated use `FileAccess.asFileUri()` for node.js contexts or `FileAccess.asBrowserUri` for browser contexts.
|
||||
*/
|
||||
toUrl(path: string): string;
|
||||
(dependencies: string[], callback: (...args: any[]) => any, errorback?: (err: any) => void): any;
|
||||
config(data: any): any;
|
||||
onError: Function;
|
||||
__$__nodeRequire<T>(moduleName: string): T;
|
||||
getStats(): ReadonlyArray<LoaderEvent>;
|
||||
define(amdModuleId: string, dependencies: string[], callback: (...args: any[]) => any): any;
|
||||
}
|
||||
|
||||
declare var require: NodeRequire;
|
||||
21
lib/vscode/src/typings/thenable.d.ts
vendored
Normal file
21
lib/vscode/src/typings/thenable.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
|
||||
* and others. This API makes no assumption about what promise libary is being used which
|
||||
* enables reusing existing code without migrating to a specific promise implementation. Still,
|
||||
* we recommend the use of native promises which are available in VS Code.
|
||||
*/
|
||||
interface Thenable<T> {
|
||||
/**
|
||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||
* @param onrejected The callback to execute when the Promise is rejected.
|
||||
* @returns A Promise for the completion of which ever callback is executed.
|
||||
*/
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
|
||||
}
|
||||
122
lib/vscode/src/vs/base/browser/browser.ts
Normal file
122
lib/vscode/src/vs/base/browser/browser.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
class WindowManager {
|
||||
|
||||
public static readonly INSTANCE = new WindowManager();
|
||||
|
||||
// --- Zoom Level
|
||||
private _zoomLevel: number = 0;
|
||||
private _lastZoomLevelChangeTime: number = 0;
|
||||
private readonly _onDidChangeZoomLevel = new Emitter<number>();
|
||||
|
||||
public readonly onDidChangeZoomLevel: Event<number> = this._onDidChangeZoomLevel.event;
|
||||
public getZoomLevel(): number {
|
||||
return this._zoomLevel;
|
||||
}
|
||||
public getTimeSinceLastZoomLevelChanged(): number {
|
||||
return Date.now() - this._lastZoomLevelChangeTime;
|
||||
}
|
||||
public setZoomLevel(zoomLevel: number, isTrusted: boolean): void {
|
||||
if (this._zoomLevel === zoomLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._zoomLevel = zoomLevel;
|
||||
// See https://github.com/microsoft/vscode/issues/26151
|
||||
this._lastZoomLevelChangeTime = isTrusted ? 0 : Date.now();
|
||||
this._onDidChangeZoomLevel.fire(this._zoomLevel);
|
||||
}
|
||||
|
||||
// --- Zoom Factor
|
||||
private _zoomFactor: number = 1;
|
||||
|
||||
public getZoomFactor(): number {
|
||||
return this._zoomFactor;
|
||||
}
|
||||
public setZoomFactor(zoomFactor: number): void {
|
||||
this._zoomFactor = zoomFactor;
|
||||
}
|
||||
|
||||
// --- Pixel Ratio
|
||||
public getPixelRatio(): number {
|
||||
let ctx: any = document.createElement('canvas').getContext('2d');
|
||||
let dpr = window.devicePixelRatio || 1;
|
||||
let bsr = ctx.webkitBackingStorePixelRatio ||
|
||||
ctx.mozBackingStorePixelRatio ||
|
||||
ctx.msBackingStorePixelRatio ||
|
||||
ctx.oBackingStorePixelRatio ||
|
||||
ctx.backingStorePixelRatio || 1;
|
||||
return dpr / bsr;
|
||||
}
|
||||
|
||||
// --- Fullscreen
|
||||
private _fullscreen: boolean = false;
|
||||
private readonly _onDidChangeFullscreen = new Emitter<void>();
|
||||
|
||||
public readonly onDidChangeFullscreen: Event<void> = this._onDidChangeFullscreen.event;
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
if (this._fullscreen === fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fullscreen = fullscreen;
|
||||
this._onDidChangeFullscreen.fire();
|
||||
}
|
||||
public isFullscreen(): boolean {
|
||||
return this._fullscreen;
|
||||
}
|
||||
}
|
||||
|
||||
/** A zoom index, e.g. 1, 2, 3 */
|
||||
export function setZoomLevel(zoomLevel: number, isTrusted: boolean): void {
|
||||
WindowManager.INSTANCE.setZoomLevel(zoomLevel, isTrusted);
|
||||
}
|
||||
export function getZoomLevel(): number {
|
||||
return WindowManager.INSTANCE.getZoomLevel();
|
||||
}
|
||||
/** Returns the time (in ms) since the zoom level was changed */
|
||||
export function getTimeSinceLastZoomLevelChanged(): number {
|
||||
return WindowManager.INSTANCE.getTimeSinceLastZoomLevelChanged();
|
||||
}
|
||||
export function onDidChangeZoomLevel(callback: (zoomLevel: number) => void): IDisposable {
|
||||
return WindowManager.INSTANCE.onDidChangeZoomLevel(callback);
|
||||
}
|
||||
|
||||
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
|
||||
export function getZoomFactor(): number {
|
||||
return WindowManager.INSTANCE.getZoomFactor();
|
||||
}
|
||||
export function setZoomFactor(zoomFactor: number): void {
|
||||
WindowManager.INSTANCE.setZoomFactor(zoomFactor);
|
||||
}
|
||||
|
||||
export function getPixelRatio(): number {
|
||||
return WindowManager.INSTANCE.getPixelRatio();
|
||||
}
|
||||
|
||||
export function setFullscreen(fullscreen: boolean): void {
|
||||
WindowManager.INSTANCE.setFullscreen(fullscreen);
|
||||
}
|
||||
export function isFullscreen(): boolean {
|
||||
return WindowManager.INSTANCE.isFullscreen();
|
||||
}
|
||||
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
export const isEdge = (userAgent.indexOf('Edge/') >= 0);
|
||||
export const isOpera = (userAgent.indexOf('Opera') >= 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 isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0);
|
||||
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
|
||||
58
lib/vscode/src/vs/base/browser/canIUse.ts
Normal file
58
lib/vscode/src/vs/base/browser/canIUse.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
export const enum KeyboardSupport {
|
||||
Always,
|
||||
FullScreen,
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser feature we can support in current platform, browser and environment.
|
||||
*/
|
||||
export const BrowserFeatures = {
|
||||
clipboard: {
|
||||
writeText: (
|
||||
platform.isNative
|
||||
|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
|
||||
|| !!(navigator && navigator.clipboard && navigator.clipboard.writeText)
|
||||
),
|
||||
readText: (
|
||||
platform.isNative
|
||||
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText)
|
||||
),
|
||||
richText: (() => {
|
||||
if (browser.isEdge) {
|
||||
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) {
|
||||
return KeyboardSupport.Always;
|
||||
}
|
||||
|
||||
if ((<any>navigator).keyboard || browser.isSafari) {
|
||||
return KeyboardSupport.FullScreen;
|
||||
}
|
||||
|
||||
return KeyboardSupport.None;
|
||||
})(),
|
||||
|
||||
// 'ontouchstart' in window always evaluates to true with typescript's modern typings. This causes `window` to be
|
||||
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
|
||||
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0,
|
||||
pointerEvents: window.PointerEvent && ('ontouchstart' in window || (window as Window).navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0)
|
||||
};
|
||||
32
lib/vscode/src/vs/base/browser/codicons.ts
Normal file
32
lib/vscode/src/vs/base/browser/codicons.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi;
|
||||
|
||||
export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
|
||||
const elements = new Array<HTMLSpanElement | string>();
|
||||
let match: RegExpMatchArray | null;
|
||||
|
||||
let textStart = 0, textStop = 0;
|
||||
while ((match = renderCodiconsRegex.exec(text)) !== null) {
|
||||
textStop = match.index || 0;
|
||||
elements.push(text.substring(textStart, textStop));
|
||||
textStart = (match.index || 0) + match[0].length;
|
||||
|
||||
const [, escaped, codicon, name, animation] = match;
|
||||
elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation));
|
||||
}
|
||||
|
||||
if (textStart < text.length) {
|
||||
elements.push(text.substring(textStart));
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
export function renderCodicon(name: string, animation: string): HTMLSpanElement {
|
||||
return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`);
|
||||
}
|
||||
34
lib/vscode/src/vs/base/browser/contextmenu.ts
Normal file
34
lib/vscode/src/vs/base/browser/contextmenu.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
|
||||
export interface IContextMenuEvent {
|
||||
readonly shiftKey?: boolean;
|
||||
readonly ctrlKey?: boolean;
|
||||
readonly altKey?: boolean;
|
||||
readonly metaKey?: boolean;
|
||||
}
|
||||
|
||||
export interface IContextMenuDelegate {
|
||||
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
|
||||
getActions(): IAction[];
|
||||
getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox';
|
||||
getActionViewItem?(action: IAction): IActionViewItem | undefined;
|
||||
getActionsContext?(event?: IContextMenuEvent): any;
|
||||
getKeyBinding?(action: IAction): ResolvedKeybinding | undefined;
|
||||
getMenuClassName?(): string;
|
||||
onHide?(didCancel: boolean): void;
|
||||
actionRunner?: IActionRunner;
|
||||
autoSelectFirstItem?: boolean;
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
domForShadowRoot?: HTMLElement;
|
||||
}
|
||||
|
||||
export interface IContextMenuProvider {
|
||||
showContextMenu(delegate: IContextMenuDelegate): void;
|
||||
}
|
||||
114
lib/vscode/src/vs/base/browser/dnd.ts
Normal file
114
lib/vscode/src/vs/base/browser/dnd.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
|
||||
/**
|
||||
* A helper that will execute a provided function when the provided HTMLElement receives
|
||||
* dragover event for 800ms. If the drag is aborted before, the callback will not be triggered.
|
||||
*/
|
||||
export class DelayedDragHandler extends Disposable {
|
||||
private timeout: any;
|
||||
|
||||
constructor(container: HTMLElement, callback: () => void) {
|
||||
super();
|
||||
|
||||
this._register(addDisposableListener(container, 'dragover', e => {
|
||||
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
if (!this.timeout) {
|
||||
this.timeout = setTimeout(() => {
|
||||
callback();
|
||||
|
||||
this.timeout = null;
|
||||
}, 800);
|
||||
}
|
||||
}));
|
||||
|
||||
['dragleave', 'drop', 'dragend'].forEach(type => {
|
||||
this._register(addDisposableListener(container, type, () => {
|
||||
this.clearDragTimeout();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private clearDragTimeout(): void {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.clearDragTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
// Common data transfers
|
||||
export const DataTransfers = {
|
||||
|
||||
/**
|
||||
* Application specific resource transfer type
|
||||
*/
|
||||
RESOURCES: 'ResourceURLs',
|
||||
|
||||
/**
|
||||
* Browser specific transfer type to download
|
||||
*/
|
||||
DOWNLOAD_URL: 'DownloadURL',
|
||||
|
||||
/**
|
||||
* Browser specific transfer type for files
|
||||
*/
|
||||
FILES: 'Files',
|
||||
|
||||
/**
|
||||
* Typically transfer type for copy/paste transfers.
|
||||
*/
|
||||
TEXT: 'text/plain'
|
||||
};
|
||||
|
||||
export function applyDragImage(event: DragEvent, label: string | null, clazz: string): void {
|
||||
const dragImage = document.createElement('div');
|
||||
dragImage.className = clazz;
|
||||
dragImage.textContent = label;
|
||||
|
||||
if (event.dataTransfer) {
|
||||
document.body.appendChild(dragImage);
|
||||
event.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
|
||||
// Removes the element when the DND operation is done
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDragAndDropData {
|
||||
update(dataTransfer: DataTransfer): void;
|
||||
getData(): any;
|
||||
}
|
||||
|
||||
export class DragAndDropData<T> implements IDragAndDropData {
|
||||
|
||||
constructor(private data: T) { }
|
||||
|
||||
update(): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
getData(): T {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IStaticDND {
|
||||
CurrentDragAndDropData: IDragAndDropData | undefined;
|
||||
}
|
||||
|
||||
export const StaticDND: IStaticDND = {
|
||||
CurrentDragAndDropData: undefined
|
||||
};
|
||||
1555
lib/vscode/src/vs/base/browser/dom.ts
Normal file
1555
lib/vscode/src/vs/base/browser/dom.ts
Normal file
File diff suppressed because it is too large
Load Diff
40
lib/vscode/src/vs/base/browser/event.ts
Normal file
40
lib/vscode/src/vs/base/browser/event.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 { Event as BaseEvent, Emitter } from 'vs/base/common/event';
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => {
|
||||
const fn = (e: Event) => emitter.fire(e);
|
||||
const emitter = new Emitter<Event>({
|
||||
onFirstListenerAdd: () => {
|
||||
element.addEventListener(type, fn, useCapture);
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
element.removeEventListener(type, fn, useCapture);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
};
|
||||
|
||||
export interface CancellableEvent {
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export function stop<T extends CancellableEvent>(event: BaseEvent<T>): BaseEvent<T> {
|
||||
return BaseEvent.map(event, e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return e;
|
||||
});
|
||||
}
|
||||
256
lib/vscode/src/vs/base/browser/fastDomNode.ts
Normal file
256
lib/vscode/src/vs/base/browser/fastDomNode.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class FastDomNode<T extends HTMLElement> {
|
||||
|
||||
public readonly domNode: T;
|
||||
private _maxWidth: number;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
private _top: number;
|
||||
private _left: number;
|
||||
private _bottom: number;
|
||||
private _right: number;
|
||||
private _fontFamily: string;
|
||||
private _fontWeight: string;
|
||||
private _fontSize: number;
|
||||
private _fontFeatureSettings: string;
|
||||
private _lineHeight: number;
|
||||
private _letterSpacing: number;
|
||||
private _className: string;
|
||||
private _display: string;
|
||||
private _position: string;
|
||||
private _visibility: string;
|
||||
private _backgroundColor: string;
|
||||
private _layerHint: boolean;
|
||||
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint';
|
||||
private _boxShadow: string;
|
||||
|
||||
constructor(domNode: T) {
|
||||
this.domNode = domNode;
|
||||
this._maxWidth = -1;
|
||||
this._width = -1;
|
||||
this._height = -1;
|
||||
this._top = -1;
|
||||
this._left = -1;
|
||||
this._bottom = -1;
|
||||
this._right = -1;
|
||||
this._fontFamily = '';
|
||||
this._fontWeight = '';
|
||||
this._fontSize = -1;
|
||||
this._fontFeatureSettings = '';
|
||||
this._lineHeight = -1;
|
||||
this._letterSpacing = -100;
|
||||
this._className = '';
|
||||
this._display = '';
|
||||
this._position = '';
|
||||
this._visibility = '';
|
||||
this._backgroundColor = '';
|
||||
this._layerHint = false;
|
||||
this._contain = 'none';
|
||||
this._boxShadow = '';
|
||||
}
|
||||
|
||||
public setMaxWidth(maxWidth: number): void {
|
||||
if (this._maxWidth === maxWidth) {
|
||||
return;
|
||||
}
|
||||
this._maxWidth = maxWidth;
|
||||
this.domNode.style.maxWidth = this._maxWidth + 'px';
|
||||
}
|
||||
|
||||
public setWidth(width: number): void {
|
||||
if (this._width === width) {
|
||||
return;
|
||||
}
|
||||
this._width = width;
|
||||
this.domNode.style.width = this._width + 'px';
|
||||
}
|
||||
|
||||
public setHeight(height: number): void {
|
||||
if (this._height === height) {
|
||||
return;
|
||||
}
|
||||
this._height = height;
|
||||
this.domNode.style.height = this._height + 'px';
|
||||
}
|
||||
|
||||
public setTop(top: number): void {
|
||||
if (this._top === top) {
|
||||
return;
|
||||
}
|
||||
this._top = top;
|
||||
this.domNode.style.top = this._top + 'px';
|
||||
}
|
||||
|
||||
public unsetTop(): void {
|
||||
if (this._top === -1) {
|
||||
return;
|
||||
}
|
||||
this._top = -1;
|
||||
this.domNode.style.top = '';
|
||||
}
|
||||
|
||||
public setLeft(left: number): void {
|
||||
if (this._left === left) {
|
||||
return;
|
||||
}
|
||||
this._left = left;
|
||||
this.domNode.style.left = this._left + 'px';
|
||||
}
|
||||
|
||||
public setBottom(bottom: number): void {
|
||||
if (this._bottom === bottom) {
|
||||
return;
|
||||
}
|
||||
this._bottom = bottom;
|
||||
this.domNode.style.bottom = this._bottom + 'px';
|
||||
}
|
||||
|
||||
public setRight(right: number): void {
|
||||
if (this._right === right) {
|
||||
return;
|
||||
}
|
||||
this._right = right;
|
||||
this.domNode.style.right = this._right + 'px';
|
||||
}
|
||||
|
||||
public setFontFamily(fontFamily: string): void {
|
||||
if (this._fontFamily === fontFamily) {
|
||||
return;
|
||||
}
|
||||
this._fontFamily = fontFamily;
|
||||
this.domNode.style.fontFamily = this._fontFamily;
|
||||
}
|
||||
|
||||
public setFontWeight(fontWeight: string): void {
|
||||
if (this._fontWeight === fontWeight) {
|
||||
return;
|
||||
}
|
||||
this._fontWeight = fontWeight;
|
||||
this.domNode.style.fontWeight = this._fontWeight;
|
||||
}
|
||||
|
||||
public setFontSize(fontSize: number): void {
|
||||
if (this._fontSize === fontSize) {
|
||||
return;
|
||||
}
|
||||
this._fontSize = fontSize;
|
||||
this.domNode.style.fontSize = this._fontSize + 'px';
|
||||
}
|
||||
|
||||
public setFontFeatureSettings(fontFeatureSettings: string): void {
|
||||
if (this._fontFeatureSettings === fontFeatureSettings) {
|
||||
return;
|
||||
}
|
||||
this._fontFeatureSettings = fontFeatureSettings;
|
||||
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
|
||||
}
|
||||
|
||||
public setLineHeight(lineHeight: number): void {
|
||||
if (this._lineHeight === lineHeight) {
|
||||
return;
|
||||
}
|
||||
this._lineHeight = lineHeight;
|
||||
this.domNode.style.lineHeight = this._lineHeight + 'px';
|
||||
}
|
||||
|
||||
public setLetterSpacing(letterSpacing: number): void {
|
||||
if (this._letterSpacing === letterSpacing) {
|
||||
return;
|
||||
}
|
||||
this._letterSpacing = letterSpacing;
|
||||
this.domNode.style.letterSpacing = this._letterSpacing + 'px';
|
||||
}
|
||||
|
||||
public setClassName(className: string): void {
|
||||
if (this._className === className) {
|
||||
return;
|
||||
}
|
||||
this._className = className;
|
||||
this.domNode.className = this._className;
|
||||
}
|
||||
|
||||
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
|
||||
this.domNode.classList.toggle(className, shouldHaveIt);
|
||||
this._className = this.domNode.className;
|
||||
}
|
||||
|
||||
public setDisplay(display: string): void {
|
||||
if (this._display === display) {
|
||||
return;
|
||||
}
|
||||
this._display = display;
|
||||
this.domNode.style.display = this._display;
|
||||
}
|
||||
|
||||
public setPosition(position: string): void {
|
||||
if (this._position === position) {
|
||||
return;
|
||||
}
|
||||
this._position = position;
|
||||
this.domNode.style.position = this._position;
|
||||
}
|
||||
|
||||
public setVisibility(visibility: string): void {
|
||||
if (this._visibility === visibility) {
|
||||
return;
|
||||
}
|
||||
this._visibility = visibility;
|
||||
this.domNode.style.visibility = this._visibility;
|
||||
}
|
||||
|
||||
public setBackgroundColor(backgroundColor: string): void {
|
||||
if (this._backgroundColor === backgroundColor) {
|
||||
return;
|
||||
}
|
||||
this._backgroundColor = backgroundColor;
|
||||
this.domNode.style.backgroundColor = this._backgroundColor;
|
||||
}
|
||||
|
||||
public setLayerHinting(layerHint: boolean): void {
|
||||
if (this._layerHint === layerHint) {
|
||||
return;
|
||||
}
|
||||
this._layerHint = layerHint;
|
||||
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
|
||||
}
|
||||
|
||||
public setBoxShadow(boxShadow: string): void {
|
||||
if (this._boxShadow === boxShadow) {
|
||||
return;
|
||||
}
|
||||
this._boxShadow = boxShadow;
|
||||
this.domNode.style.boxShadow = boxShadow;
|
||||
}
|
||||
|
||||
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
|
||||
if (this._contain === contain) {
|
||||
return;
|
||||
}
|
||||
this._contain = contain;
|
||||
(<any>this.domNode.style).contain = this._contain;
|
||||
}
|
||||
|
||||
public setAttribute(name: string, value: string): void {
|
||||
this.domNode.setAttribute(name, value);
|
||||
}
|
||||
|
||||
public removeAttribute(name: string): void {
|
||||
this.domNode.removeAttribute(name);
|
||||
}
|
||||
|
||||
public appendChild(child: FastDomNode<T>): void {
|
||||
this.domNode.appendChild(child.domNode);
|
||||
}
|
||||
|
||||
public removeChild(child: FastDomNode<T>): void {
|
||||
this.domNode.removeChild(child.domNode);
|
||||
}
|
||||
}
|
||||
|
||||
export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
|
||||
return new FastDomNode(domNode);
|
||||
}
|
||||
220
lib/vscode/src/vs/base/browser/formattedTextRenderer.ts
Normal file
220
lib/vscode/src/vs/base/browser/formattedTextRenderer.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface IContentActionHandler {
|
||||
callback: (content: string, event?: IMouseEvent) => void;
|
||||
readonly disposeables: DisposableStore;
|
||||
}
|
||||
|
||||
export interface FormattedTextRenderOptions {
|
||||
readonly className?: string;
|
||||
readonly inline?: boolean;
|
||||
readonly actionHandler?: IContentActionHandler;
|
||||
}
|
||||
|
||||
export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
|
||||
export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
|
||||
return element;
|
||||
}
|
||||
|
||||
export function createElement(options: FormattedTextRenderOptions): HTMLElement {
|
||||
const tagName = options.inline ? 'span' : 'div';
|
||||
const element = document.createElement(tagName);
|
||||
if (options.className) {
|
||||
element.className = options.className;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
class StringStream {
|
||||
private source: string;
|
||||
private index: number;
|
||||
|
||||
constructor(source: string) {
|
||||
this.source = source;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public eos(): boolean {
|
||||
return this.index >= this.source.length;
|
||||
}
|
||||
|
||||
public next(): string {
|
||||
const next = this.peek();
|
||||
this.advance();
|
||||
return next;
|
||||
}
|
||||
|
||||
public peek(): string {
|
||||
return this.source[this.index];
|
||||
}
|
||||
|
||||
public advance(): void {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
const enum FormatType {
|
||||
Invalid,
|
||||
Root,
|
||||
Text,
|
||||
Bold,
|
||||
Italics,
|
||||
Action,
|
||||
ActionClose,
|
||||
NewLine
|
||||
}
|
||||
|
||||
interface IFormatParseTree {
|
||||
type: FormatType;
|
||||
content?: string;
|
||||
index?: number;
|
||||
children?: IFormatParseTree[];
|
||||
}
|
||||
|
||||
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
|
||||
let child: Node | undefined;
|
||||
|
||||
if (treeNode.type === FormatType.Text) {
|
||||
child = document.createTextNode(treeNode.content || '');
|
||||
} else if (treeNode.type === FormatType.Bold) {
|
||||
child = document.createElement('b');
|
||||
} else if (treeNode.type === FormatType.Italics) {
|
||||
child = document.createElement('i');
|
||||
} else if (treeNode.type === FormatType.Action && actionHandler) {
|
||||
const a = document.createElement('a');
|
||||
a.href = '#';
|
||||
actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
|
||||
actionHandler.callback(String(treeNode.index), event);
|
||||
}));
|
||||
|
||||
child = a;
|
||||
} else if (treeNode.type === FormatType.NewLine) {
|
||||
child = document.createElement('br');
|
||||
} else if (treeNode.type === FormatType.Root) {
|
||||
child = element;
|
||||
}
|
||||
|
||||
if (child && element !== child) {
|
||||
element.appendChild(child);
|
||||
}
|
||||
|
||||
if (child && Array.isArray(treeNode.children)) {
|
||||
treeNode.children.forEach((nodeChild) => {
|
||||
_renderFormattedText(child!, nodeChild, actionHandler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseFormattedText(content: string): IFormatParseTree {
|
||||
|
||||
const root: IFormatParseTree = {
|
||||
type: FormatType.Root,
|
||||
children: []
|
||||
};
|
||||
|
||||
let actionViewItemIndex = 0;
|
||||
let current = root;
|
||||
const stack: IFormatParseTree[] = [];
|
||||
const stream = new StringStream(content);
|
||||
|
||||
while (!stream.eos()) {
|
||||
let next = stream.next();
|
||||
|
||||
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
|
||||
if (isEscapedFormatType) {
|
||||
next = stream.next(); // unread the backslash if it escapes a format tag type
|
||||
}
|
||||
|
||||
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
|
||||
stream.advance();
|
||||
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
const type = formatTagType(next);
|
||||
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
|
||||
current = stack.pop()!;
|
||||
} else {
|
||||
const newCurrent: IFormatParseTree = {
|
||||
type: type,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (type === FormatType.Action) {
|
||||
newCurrent.index = actionViewItemIndex;
|
||||
actionViewItemIndex++;
|
||||
}
|
||||
|
||||
current.children!.push(newCurrent);
|
||||
stack.push(current);
|
||||
current = newCurrent;
|
||||
}
|
||||
} else if (next === '\n') {
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
current.children!.push({
|
||||
type: FormatType.NewLine
|
||||
});
|
||||
|
||||
} else {
|
||||
if (current.type !== FormatType.Text) {
|
||||
const textCurrent: IFormatParseTree = {
|
||||
type: FormatType.Text,
|
||||
content: next
|
||||
};
|
||||
current.children!.push(textCurrent);
|
||||
stack.push(current);
|
||||
current = textCurrent;
|
||||
|
||||
} else {
|
||||
current.content += next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
if (stack.length) {
|
||||
// incorrectly formatted string literal
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function isFormatTag(char: string): boolean {
|
||||
return formatTagType(char) !== FormatType.Invalid;
|
||||
}
|
||||
|
||||
function formatTagType(char: string): FormatType {
|
||||
switch (char) {
|
||||
case '*':
|
||||
return FormatType.Bold;
|
||||
case '_':
|
||||
return FormatType.Italics;
|
||||
case '[':
|
||||
return FormatType.Action;
|
||||
case ']':
|
||||
return FormatType.ActionClose;
|
||||
default:
|
||||
return FormatType.Invalid;
|
||||
}
|
||||
}
|
||||
140
lib/vscode/src/vs/base/browser/globalMouseMoveMonitor.ts
Normal file
140
lib/vscode/src/vs/base/browser/globalMouseMoveMonitor.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IframeUtils } from 'vs/base/browser/iframe';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
||||
|
||||
export interface IStandardMouseMoveEventData {
|
||||
leftButton: boolean;
|
||||
buttons: number;
|
||||
posx: number;
|
||||
posy: number;
|
||||
}
|
||||
|
||||
export interface IEventMerger<R> {
|
||||
(lastEvent: R | null, currentEvent: MouseEvent): R;
|
||||
}
|
||||
|
||||
export interface IMouseMoveCallback<R> {
|
||||
(mouseMoveData: R): void;
|
||||
}
|
||||
|
||||
export interface IOnStopCallback {
|
||||
(): void;
|
||||
}
|
||||
|
||||
export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData | null, currentEvent: MouseEvent): IStandardMouseMoveEventData {
|
||||
let ev = new StandardMouseEvent(currentEvent);
|
||||
ev.preventDefault();
|
||||
return {
|
||||
leftButton: ev.leftButton,
|
||||
buttons: ev.buttons,
|
||||
posx: ev.posx,
|
||||
posy: ev.posy
|
||||
};
|
||||
}
|
||||
|
||||
export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements IDisposable {
|
||||
|
||||
private readonly _hooks = new DisposableStore();
|
||||
private _mouseMoveEventMerger: IEventMerger<R> | null = null;
|
||||
private _mouseMoveCallback: IMouseMoveCallback<R> | null = null;
|
||||
private _onStopCallback: IOnStopCallback | null = null;
|
||||
|
||||
public dispose(): void {
|
||||
this.stopMonitoring(false);
|
||||
this._hooks.dispose();
|
||||
}
|
||||
|
||||
public stopMonitoring(invokeStopCallback: boolean): void {
|
||||
if (!this.isMonitoring()) {
|
||||
// Not monitoring
|
||||
return;
|
||||
}
|
||||
|
||||
// Unhook
|
||||
this._hooks.clear();
|
||||
this._mouseMoveEventMerger = null;
|
||||
this._mouseMoveCallback = null;
|
||||
const onStopCallback = this._onStopCallback;
|
||||
this._onStopCallback = null;
|
||||
|
||||
if (invokeStopCallback && onStopCallback) {
|
||||
onStopCallback();
|
||||
}
|
||||
}
|
||||
|
||||
public isMonitoring(): boolean {
|
||||
return !!this._mouseMoveEventMerger;
|
||||
}
|
||||
|
||||
public startMonitoring(
|
||||
initialElement: HTMLElement,
|
||||
initialButtons: number,
|
||||
mouseMoveEventMerger: IEventMerger<R>,
|
||||
mouseMoveCallback: IMouseMoveCallback<R>,
|
||||
onStopCallback: IOnStopCallback
|
||||
): void {
|
||||
if (this.isMonitoring()) {
|
||||
// I am already hooked
|
||||
return;
|
||||
}
|
||||
this._mouseMoveEventMerger = mouseMoveEventMerger;
|
||||
this._mouseMoveCallback = mouseMoveCallback;
|
||||
this._onStopCallback = onStopCallback;
|
||||
|
||||
const windowChain = IframeUtils.getSameOriginWindowChain();
|
||||
const mouseMove = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove';
|
||||
const mouseUp = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup';
|
||||
|
||||
const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document);
|
||||
const shadowRoot = dom.getShadowRoot(initialElement);
|
||||
if (shadowRoot) {
|
||||
listenTo.unshift(shadowRoot);
|
||||
}
|
||||
|
||||
for (const element of listenTo) {
|
||||
this._hooks.add(dom.addDisposableThrottledListener(element, mouseMove,
|
||||
(data: R) => {
|
||||
if (data.buttons !== initialButtons) {
|
||||
// Buttons state has changed in the meantime
|
||||
this.stopMonitoring(true);
|
||||
return;
|
||||
}
|
||||
this._mouseMoveCallback!(data);
|
||||
},
|
||||
(lastEvent: R | null, currentEvent) => this._mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent)
|
||||
));
|
||||
this._hooks.add(dom.addDisposableListener(element, mouseUp, (e: MouseEvent) => this.stopMonitoring(true)));
|
||||
}
|
||||
|
||||
if (IframeUtils.hasDifferentOriginAncestor()) {
|
||||
let lastSameOriginAncestor = windowChain[windowChain.length - 1];
|
||||
// We might miss a mouse up if it happens outside the iframe
|
||||
// This one is for Chrome
|
||||
this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseout', (browserEvent: MouseEvent) => {
|
||||
let e = new StandardMouseEvent(browserEvent);
|
||||
if (e.target.tagName.toLowerCase() === 'html') {
|
||||
this.stopMonitoring(true);
|
||||
}
|
||||
}));
|
||||
// This one is for FF
|
||||
this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseover', (browserEvent: MouseEvent) => {
|
||||
let e = new StandardMouseEvent(browserEvent);
|
||||
if (e.target.tagName.toLowerCase() === 'html') {
|
||||
this.stopMonitoring(true);
|
||||
}
|
||||
}));
|
||||
// This one is for IE
|
||||
this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document.body, 'mouseleave', (browserEvent: MouseEvent) => {
|
||||
this.stopMonitoring(true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
12
lib/vscode/src/vs/base/browser/history.ts
Normal file
12
lib/vscode/src/vs/base/browser/history.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IHistoryNavigationWidget {
|
||||
|
||||
showPreviousValue(): void;
|
||||
|
||||
showNextValue(): void;
|
||||
|
||||
}
|
||||
127
lib/vscode/src/vs/base/browser/iframe.ts
Normal file
127
lib/vscode/src/vs/base/browser/iframe.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Represents a window in a possible chain of iframes
|
||||
*/
|
||||
export interface IWindowChainElement {
|
||||
/**
|
||||
* The window object for it
|
||||
*/
|
||||
window: Window;
|
||||
/**
|
||||
* The iframe element inside the window.parent corresponding to window
|
||||
*/
|
||||
iframeElement: Element | null;
|
||||
}
|
||||
|
||||
let hasDifferentOriginAncestorFlag: boolean = false;
|
||||
let sameOriginWindowChainCache: IWindowChainElement[] | null = null;
|
||||
|
||||
function getParentWindowIfSameOrigin(w: Window): Window | null {
|
||||
if (!w.parent || w.parent === w) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cannot really tell if we have access to the parent window unless we try to access something in it
|
||||
try {
|
||||
let location = w.location;
|
||||
let parentLocation = w.parent.location;
|
||||
if (location.origin !== 'null' && parentLocation.origin !== 'null') {
|
||||
if (location.protocol !== parentLocation.protocol || location.hostname !== parentLocation.hostname || location.port !== parentLocation.port) {
|
||||
hasDifferentOriginAncestorFlag = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
hasDifferentOriginAncestorFlag = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
return w.parent;
|
||||
}
|
||||
|
||||
export class IframeUtils {
|
||||
|
||||
/**
|
||||
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
|
||||
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
|
||||
* To distinguish if at one point the current execution environment is running inside a window with a different origin, see hasDifferentOriginAncestor()
|
||||
*/
|
||||
public static getSameOriginWindowChain(): IWindowChainElement[] {
|
||||
if (!sameOriginWindowChainCache) {
|
||||
sameOriginWindowChainCache = [];
|
||||
let w: Window | null = window;
|
||||
let parent: Window | null;
|
||||
do {
|
||||
parent = getParentWindowIfSameOrigin(w);
|
||||
if (parent) {
|
||||
sameOriginWindowChainCache.push({
|
||||
window: w,
|
||||
iframeElement: w.frameElement || null
|
||||
});
|
||||
} else {
|
||||
sameOriginWindowChainCache.push({
|
||||
window: w,
|
||||
iframeElement: null
|
||||
});
|
||||
}
|
||||
w = parent;
|
||||
} while (w);
|
||||
}
|
||||
return sameOriginWindowChainCache.slice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current execution environment is chained in a list of iframes which at one point ends in a window with a different origin.
|
||||
* Returns false if the current execution environment is not running inside an iframe or if the entire chain of iframes have the same origin.
|
||||
*/
|
||||
public static hasDifferentOriginAncestor(): boolean {
|
||||
if (!sameOriginWindowChainCache) {
|
||||
this.getSameOriginWindowChain();
|
||||
}
|
||||
return hasDifferentOriginAncestorFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of `childWindow` relative to `ancestorWindow`
|
||||
*/
|
||||
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) {
|
||||
|
||||
if (!ancestorWindow || childWindow === ancestorWindow) {
|
||||
return {
|
||||
top: 0,
|
||||
left: 0
|
||||
};
|
||||
}
|
||||
|
||||
let top = 0, left = 0;
|
||||
|
||||
let windowChain = this.getSameOriginWindowChain();
|
||||
|
||||
for (const windowChainEl of windowChain) {
|
||||
|
||||
top += windowChainEl.window.scrollY;
|
||||
left += windowChainEl.window.scrollX;
|
||||
|
||||
if (windowChainEl.window === ancestorWindow) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!windowChainEl.iframeElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
let boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
|
||||
top += boundingRect.top;
|
||||
left += boundingRect.left;
|
||||
}
|
||||
|
||||
return {
|
||||
top: top,
|
||||
left: left
|
||||
};
|
||||
}
|
||||
}
|
||||
336
lib/vscode/src/vs/base/browser/keyboardEvent.ts
Normal file
336
lib/vscode/src/vs/base/browser/keyboardEvent.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { KeyCode, KeyCodeUtils, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
let KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230);
|
||||
let INVERSE_KEY_CODE_MAP: KeyCode[] = new Array(KeyCode.MAX_VALUE);
|
||||
|
||||
(function () {
|
||||
for (let i = 0; i < INVERSE_KEY_CODE_MAP.length; i++) {
|
||||
INVERSE_KEY_CODE_MAP[i] = -1;
|
||||
}
|
||||
|
||||
function define(code: number, keyCode: KeyCode): void {
|
||||
KEY_CODE_MAP[code] = keyCode;
|
||||
INVERSE_KEY_CODE_MAP[keyCode] = code;
|
||||
}
|
||||
|
||||
define(3, KeyCode.PauseBreak); // VK_CANCEL 0x03 Control-break processing
|
||||
define(8, KeyCode.Backspace);
|
||||
define(9, KeyCode.Tab);
|
||||
define(13, KeyCode.Enter);
|
||||
define(16, KeyCode.Shift);
|
||||
define(17, KeyCode.Ctrl);
|
||||
define(18, KeyCode.Alt);
|
||||
define(19, KeyCode.PauseBreak);
|
||||
define(20, KeyCode.CapsLock);
|
||||
define(27, KeyCode.Escape);
|
||||
define(32, KeyCode.Space);
|
||||
define(33, KeyCode.PageUp);
|
||||
define(34, KeyCode.PageDown);
|
||||
define(35, KeyCode.End);
|
||||
define(36, KeyCode.Home);
|
||||
define(37, KeyCode.LeftArrow);
|
||||
define(38, KeyCode.UpArrow);
|
||||
define(39, KeyCode.RightArrow);
|
||||
define(40, KeyCode.DownArrow);
|
||||
define(45, KeyCode.Insert);
|
||||
define(46, KeyCode.Delete);
|
||||
|
||||
define(48, KeyCode.KEY_0);
|
||||
define(49, KeyCode.KEY_1);
|
||||
define(50, KeyCode.KEY_2);
|
||||
define(51, KeyCode.KEY_3);
|
||||
define(52, KeyCode.KEY_4);
|
||||
define(53, KeyCode.KEY_5);
|
||||
define(54, KeyCode.KEY_6);
|
||||
define(55, KeyCode.KEY_7);
|
||||
define(56, KeyCode.KEY_8);
|
||||
define(57, KeyCode.KEY_9);
|
||||
|
||||
define(65, KeyCode.KEY_A);
|
||||
define(66, KeyCode.KEY_B);
|
||||
define(67, KeyCode.KEY_C);
|
||||
define(68, KeyCode.KEY_D);
|
||||
define(69, KeyCode.KEY_E);
|
||||
define(70, KeyCode.KEY_F);
|
||||
define(71, KeyCode.KEY_G);
|
||||
define(72, KeyCode.KEY_H);
|
||||
define(73, KeyCode.KEY_I);
|
||||
define(74, KeyCode.KEY_J);
|
||||
define(75, KeyCode.KEY_K);
|
||||
define(76, KeyCode.KEY_L);
|
||||
define(77, KeyCode.KEY_M);
|
||||
define(78, KeyCode.KEY_N);
|
||||
define(79, KeyCode.KEY_O);
|
||||
define(80, KeyCode.KEY_P);
|
||||
define(81, KeyCode.KEY_Q);
|
||||
define(82, KeyCode.KEY_R);
|
||||
define(83, KeyCode.KEY_S);
|
||||
define(84, KeyCode.KEY_T);
|
||||
define(85, KeyCode.KEY_U);
|
||||
define(86, KeyCode.KEY_V);
|
||||
define(87, KeyCode.KEY_W);
|
||||
define(88, KeyCode.KEY_X);
|
||||
define(89, KeyCode.KEY_Y);
|
||||
define(90, KeyCode.KEY_Z);
|
||||
|
||||
define(93, KeyCode.ContextMenu);
|
||||
|
||||
define(96, KeyCode.NUMPAD_0);
|
||||
define(97, KeyCode.NUMPAD_1);
|
||||
define(98, KeyCode.NUMPAD_2);
|
||||
define(99, KeyCode.NUMPAD_3);
|
||||
define(100, KeyCode.NUMPAD_4);
|
||||
define(101, KeyCode.NUMPAD_5);
|
||||
define(102, KeyCode.NUMPAD_6);
|
||||
define(103, KeyCode.NUMPAD_7);
|
||||
define(104, KeyCode.NUMPAD_8);
|
||||
define(105, KeyCode.NUMPAD_9);
|
||||
define(106, KeyCode.NUMPAD_MULTIPLY);
|
||||
define(107, KeyCode.NUMPAD_ADD);
|
||||
define(108, KeyCode.NUMPAD_SEPARATOR);
|
||||
define(109, KeyCode.NUMPAD_SUBTRACT);
|
||||
define(110, KeyCode.NUMPAD_DECIMAL);
|
||||
define(111, KeyCode.NUMPAD_DIVIDE);
|
||||
|
||||
define(112, KeyCode.F1);
|
||||
define(113, KeyCode.F2);
|
||||
define(114, KeyCode.F3);
|
||||
define(115, KeyCode.F4);
|
||||
define(116, KeyCode.F5);
|
||||
define(117, KeyCode.F6);
|
||||
define(118, KeyCode.F7);
|
||||
define(119, KeyCode.F8);
|
||||
define(120, KeyCode.F9);
|
||||
define(121, KeyCode.F10);
|
||||
define(122, KeyCode.F11);
|
||||
define(123, KeyCode.F12);
|
||||
define(124, KeyCode.F13);
|
||||
define(125, KeyCode.F14);
|
||||
define(126, KeyCode.F15);
|
||||
define(127, KeyCode.F16);
|
||||
define(128, KeyCode.F17);
|
||||
define(129, KeyCode.F18);
|
||||
define(130, KeyCode.F19);
|
||||
|
||||
define(144, KeyCode.NumLock);
|
||||
define(145, KeyCode.ScrollLock);
|
||||
|
||||
define(186, KeyCode.US_SEMICOLON);
|
||||
define(187, KeyCode.US_EQUAL);
|
||||
define(188, KeyCode.US_COMMA);
|
||||
define(189, KeyCode.US_MINUS);
|
||||
define(190, KeyCode.US_DOT);
|
||||
define(191, KeyCode.US_SLASH);
|
||||
define(192, KeyCode.US_BACKTICK);
|
||||
define(193, KeyCode.ABNT_C1);
|
||||
define(194, KeyCode.ABNT_C2);
|
||||
define(219, KeyCode.US_OPEN_SQUARE_BRACKET);
|
||||
define(220, KeyCode.US_BACKSLASH);
|
||||
define(221, KeyCode.US_CLOSE_SQUARE_BRACKET);
|
||||
define(222, KeyCode.US_QUOTE);
|
||||
define(223, KeyCode.OEM_8);
|
||||
|
||||
define(226, KeyCode.OEM_102);
|
||||
|
||||
/**
|
||||
* https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
|
||||
* If an Input Method Editor is processing key input and the event is keydown, return 229.
|
||||
*/
|
||||
define(229, KeyCode.KEY_IN_COMPOSITION);
|
||||
|
||||
if (browser.isFirefox) {
|
||||
define(59, KeyCode.US_SEMICOLON);
|
||||
define(107, KeyCode.US_EQUAL);
|
||||
define(109, KeyCode.US_MINUS);
|
||||
if (platform.isMacintosh) {
|
||||
define(224, KeyCode.Meta);
|
||||
}
|
||||
} else if (browser.isWebKit) {
|
||||
define(91, KeyCode.Meta);
|
||||
if (platform.isMacintosh) {
|
||||
// the two meta keys in the Mac have different key codes (91 and 93)
|
||||
define(93, KeyCode.Meta);
|
||||
} else {
|
||||
define(92, KeyCode.Meta);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
function extractKeyCode(e: KeyboardEvent): KeyCode {
|
||||
if (e.charCode) {
|
||||
// "keypress" events mostly
|
||||
let char = String.fromCharCode(e.charCode).toUpperCase();
|
||||
return KeyCodeUtils.fromString(char);
|
||||
}
|
||||
return KEY_CODE_MAP[e.keyCode] || KeyCode.Unknown;
|
||||
}
|
||||
|
||||
export function getCodeForKeyCode(keyCode: KeyCode): number {
|
||||
return INVERSE_KEY_CODE_MAP[keyCode];
|
||||
}
|
||||
|
||||
export interface IKeyboardEvent {
|
||||
|
||||
readonly _standardKeyboardEventBrand: true;
|
||||
|
||||
readonly browserEvent: KeyboardEvent;
|
||||
readonly target: HTMLElement;
|
||||
|
||||
readonly ctrlKey: boolean;
|
||||
readonly shiftKey: boolean;
|
||||
readonly altKey: boolean;
|
||||
readonly metaKey: boolean;
|
||||
readonly keyCode: KeyCode;
|
||||
readonly code: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
toKeybinding(): SimpleKeybinding;
|
||||
equals(keybinding: number): boolean;
|
||||
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);
|
||||
const altKeyMod = KeyMod.Alt;
|
||||
const shiftKeyMod = KeyMod.Shift;
|
||||
const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl);
|
||||
|
||||
export function printKeyboardEvent(e: KeyboardEvent): string {
|
||||
let modifiers: string[] = [];
|
||||
if (e.ctrlKey) {
|
||||
modifiers.push(`ctrl`);
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
modifiers.push(`shift`);
|
||||
}
|
||||
if (e.altKey) {
|
||||
modifiers.push(`alt`);
|
||||
}
|
||||
if (e.metaKey) {
|
||||
modifiers.push(`meta`);
|
||||
}
|
||||
return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`;
|
||||
}
|
||||
|
||||
export function printStandardKeyboardEvent(e: StandardKeyboardEvent): string {
|
||||
let modifiers: string[] = [];
|
||||
if (e.ctrlKey) {
|
||||
modifiers.push(`ctrl`);
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
modifiers.push(`shift`);
|
||||
}
|
||||
if (e.altKey) {
|
||||
modifiers.push(`alt`);
|
||||
}
|
||||
if (e.metaKey) {
|
||||
modifiers.push(`meta`);
|
||||
}
|
||||
return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode} ('${KeyCodeUtils.toString(e.keyCode)}')`;
|
||||
}
|
||||
|
||||
export class StandardKeyboardEvent implements IKeyboardEvent {
|
||||
|
||||
readonly _standardKeyboardEventBrand = true;
|
||||
|
||||
public readonly browserEvent: KeyboardEvent;
|
||||
public readonly target: HTMLElement;
|
||||
|
||||
public readonly ctrlKey: boolean;
|
||||
public readonly shiftKey: boolean;
|
||||
public readonly altKey: boolean;
|
||||
public readonly metaKey: boolean;
|
||||
public readonly keyCode: KeyCode;
|
||||
public readonly code: string;
|
||||
|
||||
private _asKeybinding: number;
|
||||
private _asRuntimeKeybinding: SimpleKeybinding;
|
||||
|
||||
constructor(source: KeyboardEvent) {
|
||||
let e = source;
|
||||
|
||||
this.browserEvent = e;
|
||||
this.target = <HTMLElement>e.target;
|
||||
|
||||
this.ctrlKey = e.ctrlKey;
|
||||
this.shiftKey = e.shiftKey;
|
||||
this.altKey = e.altKey;
|
||||
this.metaKey = e.metaKey;
|
||||
this.keyCode = extractKeyCode(e);
|
||||
this.code = e.code;
|
||||
|
||||
// console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]);
|
||||
|
||||
this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl;
|
||||
this.altKey = this.altKey || this.keyCode === KeyCode.Alt;
|
||||
this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift;
|
||||
this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta;
|
||||
|
||||
this._asKeybinding = this._computeKeybinding();
|
||||
this._asRuntimeKeybinding = this._computeRuntimeKeybinding();
|
||||
|
||||
// console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`);
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
if (this.browserEvent && this.browserEvent.preventDefault) {
|
||||
this.browserEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public stopPropagation(): void {
|
||||
if (this.browserEvent && this.browserEvent.stopPropagation) {
|
||||
this.browserEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
public toKeybinding(): SimpleKeybinding {
|
||||
return this._asRuntimeKeybinding;
|
||||
}
|
||||
|
||||
public equals(other: number): boolean {
|
||||
return this._asKeybinding === other;
|
||||
}
|
||||
|
||||
private _computeKeybinding(): number {
|
||||
let key = KeyCode.Unknown;
|
||||
if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
|
||||
key = this.keyCode;
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
if (this.ctrlKey) {
|
||||
result |= ctrlKeyMod;
|
||||
}
|
||||
if (this.altKey) {
|
||||
result |= altKeyMod;
|
||||
}
|
||||
if (this.shiftKey) {
|
||||
result |= shiftKeyMod;
|
||||
}
|
||||
if (this.metaKey) {
|
||||
result |= metaKeyMod;
|
||||
}
|
||||
result |= key;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _computeRuntimeKeybinding(): SimpleKeybinding {
|
||||
let key = KeyCode.Unknown;
|
||||
if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
|
||||
key = this.keyCode;
|
||||
}
|
||||
return new SimpleKeybinding(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
|
||||
}
|
||||
}
|
||||
303
lib/vscode/src/vs/base/browser/markdownRenderer.ts
Normal file
303
lib/vscode/src/vs/base/browser/markdownRenderer.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import * as marked from 'vs/base/common/marked/marked';
|
||||
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
|
||||
import { parse } from 'vs/base/common/marshalling';
|
||||
import { cloneAndChange } from 'vs/base/common/objects';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileAccess, Schemas } from 'vs/base/common/network';
|
||||
import { markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
|
||||
import { resolvePath } from 'vs/base/common/resources';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
|
||||
export interface MarkedOptions extends marked.MarkedOptions {
|
||||
baseUrl?: never;
|
||||
}
|
||||
|
||||
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
|
||||
codeBlockRenderer?: (modeId: string, value: string) => Promise<HTMLElement>;
|
||||
codeBlockRenderCallback?: () => void;
|
||||
baseUrl?: URI;
|
||||
}
|
||||
|
||||
const _ttpInsane = window.trustedTypes?.createPolicy('insane', {
|
||||
createHTML(value, options: InsaneOptions): string {
|
||||
return insane(value, options);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Low-level way create a html element from a markdown string.
|
||||
*
|
||||
* **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/browser/core/markdownRenderer.ts)
|
||||
* which comes with support for pretty code block rendering and which uses the default way of handling links.
|
||||
*/
|
||||
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
|
||||
const _uriMassage = function (part: string): string {
|
||||
let data: any;
|
||||
try {
|
||||
data = parse(decodeURIComponent(part));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
if (!data) {
|
||||
return part;
|
||||
}
|
||||
data = cloneAndChange(data, value => {
|
||||
if (markdown.uris && markdown.uris[value]) {
|
||||
return URI.revive(markdown.uris[value]);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return encodeURIComponent(JSON.stringify(data));
|
||||
};
|
||||
|
||||
const _href = function (href: string, isDomUri: boolean): string {
|
||||
const data = markdown.uris && markdown.uris[href];
|
||||
if (!data) {
|
||||
return href; // no uri exists
|
||||
}
|
||||
let uri = URI.revive(data);
|
||||
if (URI.parse(href).toString() === uri.toString()) {
|
||||
return href; // no tranformation performed
|
||||
}
|
||||
if (isDomUri) {
|
||||
// this URI will end up as "src"-attribute of a dom node
|
||||
// and because of that special rewriting needs to be done
|
||||
// so that the URI uses a protocol that's understood by
|
||||
// browsers (like http or https)
|
||||
return FileAccess.asBrowserUri(uri).toString(true);
|
||||
}
|
||||
if (uri.query) {
|
||||
uri = uri.with({ query: _uriMassage(uri.query) });
|
||||
}
|
||||
return uri.toString();
|
||||
};
|
||||
|
||||
// signal to code-block render that the
|
||||
// element has been created
|
||||
let signalInnerHTML: () => void;
|
||||
const withInnerHTML = new Promise<void>(c => signalInnerHTML = c);
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.image = (href: string, title: string, text: string) => {
|
||||
let dimensions: string[] = [];
|
||||
let attributes: string[] = [];
|
||||
if (href) {
|
||||
({ href, dimensions } = parseHrefAndDimensions(href));
|
||||
href = _href(href, true);
|
||||
try {
|
||||
const hrefAsUri = URI.parse(href);
|
||||
if (options.baseUrl && hrefAsUri.scheme === Schemas.file) { // absolute or relative local path, or file: uri
|
||||
href = resolvePath(options.baseUrl, href).toString();
|
||||
}
|
||||
} catch (err) { }
|
||||
|
||||
attributes.push(`src="${href}"`);
|
||||
}
|
||||
if (text) {
|
||||
attributes.push(`alt="${text}"`);
|
||||
}
|
||||
if (title) {
|
||||
attributes.push(`title="${title}"`);
|
||||
}
|
||||
if (dimensions.length) {
|
||||
attributes = attributes.concat(dimensions);
|
||||
}
|
||||
return '<img ' + attributes.join(' ') + '>';
|
||||
};
|
||||
renderer.link = (href, title, text): string => {
|
||||
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
|
||||
if (href === text) { // raw link case
|
||||
text = removeMarkdownEscapes(text);
|
||||
}
|
||||
href = _href(href, false);
|
||||
if (options.baseUrl) {
|
||||
const hasScheme = /^\w[\w\d+.-]*:/.test(href);
|
||||
if (!hasScheme) {
|
||||
href = resolvePath(options.baseUrl, href).toString();
|
||||
}
|
||||
}
|
||||
title = removeMarkdownEscapes(title);
|
||||
href = removeMarkdownEscapes(href);
|
||||
if (
|
||||
!href
|
||||
|| href.match(/^data:|javascript:/i)
|
||||
|| (href.match(/^command:/i) && !markdown.isTrusted)
|
||||
|| href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i)
|
||||
) {
|
||||
// drop the link
|
||||
return text;
|
||||
|
||||
} else {
|
||||
// HTML Encode href
|
||||
href = href.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return `<a href="#" data-href="${href}" title="${title || href}">${text}</a>`;
|
||||
}
|
||||
};
|
||||
renderer.paragraph = (text): string => {
|
||||
if (markdown.supportThemeIcons) {
|
||||
const elements = renderCodicons(text);
|
||||
text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
|
||||
}
|
||||
return `<p>${text}</p>`;
|
||||
};
|
||||
|
||||
if (options.codeBlockRenderer) {
|
||||
renderer.code = (code, lang) => {
|
||||
const value = options.codeBlockRenderer!(lang, code);
|
||||
// when code-block rendering is async we return sync
|
||||
// but update the node with the real result later.
|
||||
const id = defaultGenerator.nextId();
|
||||
const promise = Promise.all([value, withInnerHTML]).then(values => {
|
||||
const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
|
||||
if (span) {
|
||||
DOM.reset(span, values[0]);
|
||||
}
|
||||
}).catch(_err => {
|
||||
// ignore
|
||||
});
|
||||
|
||||
if (options.codeBlockRenderCallback) {
|
||||
promise.then(options.codeBlockRenderCallback);
|
||||
}
|
||||
|
||||
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
|
||||
};
|
||||
}
|
||||
|
||||
if (options.actionHandler) {
|
||||
options.actionHandler.disposeables.add(Event.any<MouseEvent>(domEvent(element, 'click'), domEvent(element, 'auxclick'))(e => {
|
||||
const mouseEvent = new StandardMouseEvent(e);
|
||||
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target: HTMLElement | null = mouseEvent.target;
|
||||
if (target.tagName !== 'A') {
|
||||
target = target.parentElement;
|
||||
if (!target || target.tagName !== 'A') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const href = target.dataset['href'];
|
||||
if (href) {
|
||||
options.actionHandler!.callback(href, mouseEvent);
|
||||
}
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
} finally {
|
||||
mouseEvent.preventDefault();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Use our own sanitizer so that we can let through only spans.
|
||||
// Otherwise, we'd be letting all html be rendered.
|
||||
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
|
||||
markedOptions.sanitizer = (html: string): string => {
|
||||
const match = markdown.isTrusted ? html.match(/^(<span[^<]+>)|(<\/\s*span>)$/) : undefined;
|
||||
return match ? html : '';
|
||||
};
|
||||
markedOptions.sanitize = true;
|
||||
markedOptions.renderer = renderer;
|
||||
|
||||
// values that are too long will freeze the UI
|
||||
let value = markdown.value ?? '';
|
||||
if (value.length > 100_000) {
|
||||
value = `${value.substr(0, 100_000)}…`;
|
||||
}
|
||||
// escape theme icons
|
||||
if (markdown.supportThemeIcons) {
|
||||
value = markdownEscapeEscapedCodicons(value);
|
||||
}
|
||||
|
||||
const renderedMarkdown = marked.parse(value, markedOptions);
|
||||
|
||||
// sanitize with insane
|
||||
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown);
|
||||
|
||||
// signal that async code blocks can be now be inserted
|
||||
signalInnerHTML!();
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function sanitizeRenderedMarkdown(
|
||||
options: { isTrusted?: boolean },
|
||||
renderedMarkdown: string,
|
||||
): string {
|
||||
const insaneOptions = getInsaneOptions(options);
|
||||
if (_ttpInsane) {
|
||||
return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
|
||||
} else {
|
||||
return insane(renderedMarkdown, insaneOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
|
||||
const allowedSchemes = [
|
||||
Schemas.http,
|
||||
Schemas.https,
|
||||
Schemas.mailto,
|
||||
Schemas.data,
|
||||
Schemas.file,
|
||||
Schemas.vscodeRemote,
|
||||
Schemas.vscodeRemoteResource,
|
||||
];
|
||||
|
||||
if (options.isTrusted) {
|
||||
allowedSchemes.push(Schemas.command);
|
||||
}
|
||||
|
||||
return {
|
||||
allowedSchemes,
|
||||
// allowedTags should included everything that markdown renders to.
|
||||
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
|
||||
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
|
||||
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
|
||||
allowedTags: ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'name', 'target', 'data-href'],
|
||||
'img': ['src', 'title', 'alt', 'width', 'height'],
|
||||
'div': ['class', 'data-code'],
|
||||
'span': ['class', 'style'],
|
||||
// https://github.com/microsoft/vscode/issues/95937
|
||||
'th': ['align'],
|
||||
'td': ['align']
|
||||
},
|
||||
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
|
||||
if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) {
|
||||
if (token.attrs['style']) {
|
||||
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
|
||||
} else if (token.attrs['class']) {
|
||||
// The class should match codicon rendering in src\vs\base\common\codicons.ts
|
||||
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
224
lib/vscode/src/vs/base/browser/mouseEvent.ts
Normal file
224
lib/vscode/src/vs/base/browser/mouseEvent.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { IframeUtils } from 'vs/base/browser/iframe';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
export interface IMouseEvent {
|
||||
readonly browserEvent: MouseEvent;
|
||||
readonly leftButton: boolean;
|
||||
readonly middleButton: boolean;
|
||||
readonly rightButton: boolean;
|
||||
readonly buttons: number;
|
||||
readonly target: HTMLElement;
|
||||
readonly detail: number;
|
||||
readonly posx: number;
|
||||
readonly posy: number;
|
||||
readonly ctrlKey: boolean;
|
||||
readonly shiftKey: boolean;
|
||||
readonly altKey: boolean;
|
||||
readonly metaKey: boolean;
|
||||
readonly timestamp: number;
|
||||
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export class StandardMouseEvent implements IMouseEvent {
|
||||
|
||||
public readonly browserEvent: MouseEvent;
|
||||
|
||||
public readonly leftButton: boolean;
|
||||
public readonly middleButton: boolean;
|
||||
public readonly rightButton: boolean;
|
||||
public readonly buttons: number;
|
||||
public readonly target: HTMLElement;
|
||||
public detail: number;
|
||||
public readonly posx: number;
|
||||
public readonly posy: number;
|
||||
public readonly ctrlKey: boolean;
|
||||
public readonly shiftKey: boolean;
|
||||
public readonly altKey: boolean;
|
||||
public readonly metaKey: boolean;
|
||||
public readonly timestamp: number;
|
||||
|
||||
constructor(e: MouseEvent) {
|
||||
this.timestamp = Date.now();
|
||||
this.browserEvent = e;
|
||||
this.leftButton = e.button === 0;
|
||||
this.middleButton = e.button === 1;
|
||||
this.rightButton = e.button === 2;
|
||||
this.buttons = e.buttons;
|
||||
|
||||
this.target = <HTMLElement>e.target;
|
||||
|
||||
this.detail = e.detail || 1;
|
||||
if (e.type === 'dblclick') {
|
||||
this.detail = 2;
|
||||
}
|
||||
this.ctrlKey = e.ctrlKey;
|
||||
this.shiftKey = e.shiftKey;
|
||||
this.altKey = e.altKey;
|
||||
this.metaKey = e.metaKey;
|
||||
|
||||
if (typeof e.pageX === 'number') {
|
||||
this.posx = e.pageX;
|
||||
this.posy = e.pageY;
|
||||
} else {
|
||||
// Probably hit by MSGestureEvent
|
||||
this.posx = e.clientX + document.body.scrollLeft + document.documentElement!.scrollLeft;
|
||||
this.posy = e.clientY + document.body.scrollTop + document.documentElement!.scrollTop;
|
||||
}
|
||||
|
||||
// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
|
||||
let iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(self, e.view);
|
||||
this.posx -= iframeOffsets.left;
|
||||
this.posy -= iframeOffsets.top;
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
this.browserEvent.preventDefault();
|
||||
}
|
||||
|
||||
public stopPropagation(): void {
|
||||
this.browserEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDataTransfer {
|
||||
dropEffect: string;
|
||||
effectAllowed: string;
|
||||
types: any[];
|
||||
files: any[];
|
||||
|
||||
setData(type: string, data: string): void;
|
||||
setDragImage(image: any, x: number, y: number): void;
|
||||
|
||||
getData(type: string): string;
|
||||
clearData(types?: string[]): void;
|
||||
}
|
||||
|
||||
export class DragMouseEvent extends StandardMouseEvent {
|
||||
|
||||
public readonly dataTransfer: IDataTransfer;
|
||||
|
||||
constructor(e: MouseEvent) {
|
||||
super(e);
|
||||
this.dataTransfer = (<any>e).dataTransfer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface IMouseWheelEvent extends MouseEvent {
|
||||
readonly wheelDelta: number;
|
||||
readonly wheelDeltaX: number;
|
||||
readonly wheelDeltaY: number;
|
||||
|
||||
readonly deltaX: number;
|
||||
readonly deltaY: number;
|
||||
readonly deltaZ: number;
|
||||
readonly deltaMode: number;
|
||||
}
|
||||
|
||||
interface IWebKitMouseWheelEvent {
|
||||
wheelDeltaY: number;
|
||||
wheelDeltaX: number;
|
||||
}
|
||||
|
||||
interface IGeckoMouseWheelEvent {
|
||||
HORIZONTAL_AXIS: number;
|
||||
VERTICAL_AXIS: number;
|
||||
axis: number;
|
||||
detail: number;
|
||||
}
|
||||
|
||||
export class StandardWheelEvent {
|
||||
|
||||
public readonly browserEvent: IMouseWheelEvent | null;
|
||||
public readonly deltaY: number;
|
||||
public readonly deltaX: number;
|
||||
public readonly target: Node;
|
||||
|
||||
constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {
|
||||
|
||||
this.browserEvent = e || null;
|
||||
this.target = e ? (e.target || (<any>e).targetNode || e.srcElement) : null;
|
||||
|
||||
this.deltaY = deltaY;
|
||||
this.deltaX = deltaX;
|
||||
|
||||
if (e) {
|
||||
// Old (deprecated) wheel events
|
||||
let e1 = <IWebKitMouseWheelEvent><any>e;
|
||||
let e2 = <IGeckoMouseWheelEvent><any>e;
|
||||
|
||||
// vertical delta scroll
|
||||
if (typeof e1.wheelDeltaY !== 'undefined') {
|
||||
this.deltaY = e1.wheelDeltaY / 120;
|
||||
} else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
|
||||
this.deltaY = -e2.detail / 3;
|
||||
} else if (e.type === 'wheel') {
|
||||
// Modern wheel event
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
|
||||
const ev = <WheelEvent><unknown>e;
|
||||
|
||||
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
|
||||
// the deltas are expressed in lines
|
||||
if (browser.isFirefox && !platform.isMacintosh) {
|
||||
this.deltaY = -e.deltaY / 3;
|
||||
} else {
|
||||
this.deltaY = -e.deltaY;
|
||||
}
|
||||
} else {
|
||||
this.deltaY = -e.deltaY / 40;
|
||||
}
|
||||
}
|
||||
|
||||
// horizontal delta scroll
|
||||
if (typeof e1.wheelDeltaX !== 'undefined') {
|
||||
if (browser.isSafari && platform.isWindows) {
|
||||
this.deltaX = - (e1.wheelDeltaX / 120);
|
||||
} else {
|
||||
this.deltaX = e1.wheelDeltaX / 120;
|
||||
}
|
||||
} else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
|
||||
this.deltaX = -e.detail / 3;
|
||||
} else if (e.type === 'wheel') {
|
||||
// Modern wheel event
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
|
||||
const ev = <WheelEvent><unknown>e;
|
||||
|
||||
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
|
||||
// the deltas are expressed in lines
|
||||
if (browser.isFirefox && !platform.isMacintosh) {
|
||||
this.deltaX = -e.deltaX / 3;
|
||||
} else {
|
||||
this.deltaX = -e.deltaX;
|
||||
}
|
||||
} else {
|
||||
this.deltaX = -e.deltaX / 40;
|
||||
}
|
||||
}
|
||||
|
||||
// Assume a vertical scroll if nothing else worked
|
||||
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
|
||||
this.deltaY = e.wheelDelta / 120;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
if (this.browserEvent) {
|
||||
this.browserEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public stopPropagation(): void {
|
||||
if (this.browserEvent) {
|
||||
this.browserEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
363
lib/vscode/src/vs/base/browser/touch.ts
Normal file
363
lib/vscode/src/vs/base/browser/touch.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as DomUtils from 'vs/base/browser/dom';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
|
||||
export namespace EventType {
|
||||
export const Tap = '-monaco-gesturetap';
|
||||
export const Change = '-monaco-gesturechange';
|
||||
export const Start = '-monaco-gesturestart';
|
||||
export const End = '-monaco-gesturesend';
|
||||
export const Contextmenu = '-monaco-gesturecontextmenu';
|
||||
}
|
||||
|
||||
interface TouchData {
|
||||
id: number;
|
||||
initialTarget: EventTarget;
|
||||
initialTimeStamp: number;
|
||||
initialPageX: number;
|
||||
initialPageY: number;
|
||||
rollingTimestamps: number[];
|
||||
rollingPageX: number[];
|
||||
rollingPageY: number[];
|
||||
}
|
||||
|
||||
export interface GestureEvent extends MouseEvent {
|
||||
initialTarget: EventTarget | undefined;
|
||||
translationX: number;
|
||||
translationY: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
tapCount: number;
|
||||
}
|
||||
|
||||
interface Touch {
|
||||
identifier: number;
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
radiusX: number;
|
||||
radiusY: number;
|
||||
rotationAngle: number;
|
||||
force: number;
|
||||
target: Element;
|
||||
}
|
||||
|
||||
interface TouchList {
|
||||
[i: number]: Touch;
|
||||
length: number;
|
||||
item(index: number): Touch;
|
||||
identifiedTouch(id: number): Touch;
|
||||
}
|
||||
|
||||
interface TouchEvent extends Event {
|
||||
touches: TouchList;
|
||||
targetTouches: TouchList;
|
||||
changedTouches: TouchList;
|
||||
}
|
||||
|
||||
export class Gesture extends Disposable {
|
||||
|
||||
private static readonly SCROLL_FRICTION = -0.005;
|
||||
private static INSTANCE: Gesture;
|
||||
private static readonly HOLD_DELAY = 700;
|
||||
|
||||
private dispatched = false;
|
||||
private targets: HTMLElement[];
|
||||
private ignoreTargets: HTMLElement[];
|
||||
private handle: IDisposable | null;
|
||||
|
||||
private activeTouches: { [id: number]: TouchData; };
|
||||
|
||||
private _lastSetTapCountTime: number;
|
||||
|
||||
private static readonly CLEAR_TAP_COUNT_TIME = 400; // ms
|
||||
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
||||
this.activeTouches = {};
|
||||
this.handle = null;
|
||||
this.targets = [];
|
||||
this.ignoreTargets = [];
|
||||
this._lastSetTapCountTime = 0;
|
||||
this._register(DomUtils.addDisposableListener(document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e), { passive: false }));
|
||||
this._register(DomUtils.addDisposableListener(document, 'touchend', (e: TouchEvent) => this.onTouchEnd(e)));
|
||||
this._register(DomUtils.addDisposableListener(document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e), { passive: false }));
|
||||
}
|
||||
|
||||
public static addTarget(element: HTMLElement): IDisposable {
|
||||
if (!Gesture.isTouchDevice()) {
|
||||
return Disposable.None;
|
||||
}
|
||||
if (!Gesture.INSTANCE) {
|
||||
Gesture.INSTANCE = new Gesture();
|
||||
}
|
||||
|
||||
Gesture.INSTANCE.targets.push(element);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
Gesture.INSTANCE.targets = Gesture.INSTANCE.targets.filter(t => t !== element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ignoreTarget(element: HTMLElement): IDisposable {
|
||||
if (!Gesture.isTouchDevice()) {
|
||||
return Disposable.None;
|
||||
}
|
||||
if (!Gesture.INSTANCE) {
|
||||
Gesture.INSTANCE = new Gesture();
|
||||
}
|
||||
|
||||
Gesture.INSTANCE.ignoreTargets.push(element);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
Gesture.INSTANCE.ignoreTargets = Gesture.INSTANCE.ignoreTargets.filter(t => t !== element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@memoize
|
||||
private static isTouchDevice(): boolean {
|
||||
// `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
|
||||
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.handle) {
|
||||
this.handle.dispose();
|
||||
this.handle = null;
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private onTouchStart(e: TouchEvent): void {
|
||||
let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
|
||||
|
||||
if (this.handle) {
|
||||
this.handle.dispose();
|
||||
this.handle = null;
|
||||
}
|
||||
|
||||
for (let i = 0, len = e.targetTouches.length; i < len; i++) {
|
||||
let touch = e.targetTouches.item(i);
|
||||
|
||||
this.activeTouches[touch.identifier] = {
|
||||
id: touch.identifier,
|
||||
initialTarget: touch.target,
|
||||
initialTimeStamp: timestamp,
|
||||
initialPageX: touch.pageX,
|
||||
initialPageY: touch.pageY,
|
||||
rollingTimestamps: [timestamp],
|
||||
rollingPageX: [touch.pageX],
|
||||
rollingPageY: [touch.pageY]
|
||||
};
|
||||
|
||||
let evt = this.newGestureEvent(EventType.Start, touch.target);
|
||||
evt.pageX = touch.pageX;
|
||||
evt.pageY = touch.pageY;
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
if (this.dispatched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatched = false;
|
||||
}
|
||||
}
|
||||
|
||||
private onTouchEnd(e: TouchEvent): void {
|
||||
let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
|
||||
|
||||
let activeTouchCount = Object.keys(this.activeTouches).length;
|
||||
|
||||
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
|
||||
|
||||
let touch = e.changedTouches.item(i);
|
||||
|
||||
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
|
||||
console.warn('move of an UNKNOWN touch', touch);
|
||||
continue;
|
||||
}
|
||||
|
||||
let data = this.activeTouches[touch.identifier],
|
||||
holdTime = Date.now() - data.initialTimeStamp;
|
||||
|
||||
if (holdTime < Gesture.HOLD_DELAY
|
||||
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
|
||||
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {
|
||||
|
||||
let evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
|
||||
evt.pageX = arrays.tail(data.rollingPageX);
|
||||
evt.pageY = arrays.tail(data.rollingPageY);
|
||||
this.dispatchEvent(evt);
|
||||
|
||||
} else if (holdTime >= Gesture.HOLD_DELAY
|
||||
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
|
||||
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {
|
||||
|
||||
let evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
|
||||
evt.pageX = arrays.tail(data.rollingPageX);
|
||||
evt.pageY = arrays.tail(data.rollingPageY);
|
||||
this.dispatchEvent(evt);
|
||||
|
||||
} else if (activeTouchCount === 1) {
|
||||
let finalX = arrays.tail(data.rollingPageX);
|
||||
let finalY = arrays.tail(data.rollingPageY);
|
||||
|
||||
let deltaT = arrays.tail(data.rollingTimestamps) - data.rollingTimestamps[0];
|
||||
let deltaX = finalX - data.rollingPageX[0];
|
||||
let deltaY = finalY - data.rollingPageY[0];
|
||||
|
||||
// We need to get all the dispatch targets on the start of the inertia event
|
||||
const dispatchTo = this.targets.filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
|
||||
this.inertia(dispatchTo, timestamp, // time now
|
||||
Math.abs(deltaX) / deltaT, // speed
|
||||
deltaX > 0 ? 1 : -1, // x direction
|
||||
finalX, // x now
|
||||
Math.abs(deltaY) / deltaT, // y speed
|
||||
deltaY > 0 ? 1 : -1, // y direction
|
||||
finalY // y now
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
|
||||
// forget about this touch
|
||||
delete this.activeTouches[touch.identifier];
|
||||
}
|
||||
|
||||
if (this.dispatched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatched = false;
|
||||
}
|
||||
}
|
||||
|
||||
private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent {
|
||||
let event = document.createEvent('CustomEvent') as unknown as GestureEvent;
|
||||
event.initEvent(type, false, true);
|
||||
event.initialTarget = initialTarget;
|
||||
event.tapCount = 0;
|
||||
return event;
|
||||
}
|
||||
|
||||
private dispatchEvent(event: GestureEvent): void {
|
||||
if (event.type === EventType.Tap) {
|
||||
const currentTime = (new Date()).getTime();
|
||||
let setTapCount = 0;
|
||||
if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
|
||||
setTapCount = 1;
|
||||
} else {
|
||||
setTapCount = 2;
|
||||
}
|
||||
|
||||
this._lastSetTapCountTime = currentTime;
|
||||
event.tapCount = setTapCount;
|
||||
} else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
|
||||
// tap is canceled by scrolling or context menu
|
||||
this._lastSetTapCountTime = 0;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.ignoreTargets.length; i++) {
|
||||
if (event.initialTarget instanceof Node && this.ignoreTargets[i].contains(event.initialTarget)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.targets.forEach(target => {
|
||||
if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) {
|
||||
target.dispatchEvent(event);
|
||||
this.dispatched = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private inertia(dispatchTo: EventTarget[], t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
|
||||
this.handle = DomUtils.scheduleAtNextAnimationFrame(() => {
|
||||
let now = Date.now();
|
||||
|
||||
// velocity: old speed + accel_over_time
|
||||
let deltaT = now - t1,
|
||||
delta_pos_x = 0, delta_pos_y = 0,
|
||||
stopped = true;
|
||||
|
||||
vX += Gesture.SCROLL_FRICTION * deltaT;
|
||||
vY += Gesture.SCROLL_FRICTION * deltaT;
|
||||
|
||||
if (vX > 0) {
|
||||
stopped = false;
|
||||
delta_pos_x = dirX * vX * deltaT;
|
||||
}
|
||||
|
||||
if (vY > 0) {
|
||||
stopped = false;
|
||||
delta_pos_y = dirY * vY * deltaT;
|
||||
}
|
||||
|
||||
// dispatch translation event
|
||||
let evt = this.newGestureEvent(EventType.Change);
|
||||
evt.translationX = delta_pos_x;
|
||||
evt.translationY = delta_pos_y;
|
||||
dispatchTo.forEach(d => d.dispatchEvent(evt));
|
||||
|
||||
if (!stopped) {
|
||||
this.inertia(dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onTouchMove(e: TouchEvent): void {
|
||||
let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
|
||||
|
||||
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
|
||||
|
||||
let touch = e.changedTouches.item(i);
|
||||
|
||||
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
|
||||
console.warn('end of an UNKNOWN touch', touch);
|
||||
continue;
|
||||
}
|
||||
|
||||
let data = this.activeTouches[touch.identifier];
|
||||
|
||||
let evt = this.newGestureEvent(EventType.Change, data.initialTarget);
|
||||
evt.translationX = touch.pageX - arrays.tail(data.rollingPageX);
|
||||
evt.translationY = touch.pageY - arrays.tail(data.rollingPageY);
|
||||
evt.pageX = touch.pageX;
|
||||
evt.pageY = touch.pageY;
|
||||
this.dispatchEvent(evt);
|
||||
|
||||
// only keep a few data points, to average the final speed
|
||||
if (data.rollingPageX.length > 3) {
|
||||
data.rollingPageX.shift();
|
||||
data.rollingPageY.shift();
|
||||
data.rollingTimestamps.shift();
|
||||
}
|
||||
|
||||
data.rollingPageX.push(touch.pageX);
|
||||
data.rollingPageY.push(touch.pageY);
|
||||
data.rollingTimestamps.push(timestamp);
|
||||
}
|
||||
|
||||
if (this.dispatched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatched = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
398
lib/vscode/src/vs/base/browser/ui/actionbar/actionViewItems.ts
Normal file
398
lib/vscode/src/vs/base/browser/ui/actionbar/actionViewItems.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./actionbar';
|
||||
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 * 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';
|
||||
|
||||
export interface IBaseActionViewItemOptions {
|
||||
draggable?: boolean;
|
||||
isMenu?: boolean;
|
||||
useEventAsContext?: boolean;
|
||||
}
|
||||
|
||||
export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
|
||||
element: HTMLElement | undefined;
|
||||
|
||||
_context: any;
|
||||
_action: IAction;
|
||||
|
||||
private _actionRunner: IActionRunner | undefined;
|
||||
|
||||
constructor(context: any, action: IAction, protected options: IBaseActionViewItemOptions = {}) {
|
||||
super();
|
||||
|
||||
this._context = context || this;
|
||||
this._action = action;
|
||||
|
||||
if (action instanceof Action) {
|
||||
this._register(action.onDidChange(event => {
|
||||
if (!this.element) {
|
||||
// we have not been rendered yet, so there
|
||||
// is no point in updating the UI
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleActionChangeEvent(event);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private handleActionChangeEvent(event: IActionChangeEvent): void {
|
||||
if (event.enabled !== undefined) {
|
||||
this.updateEnabled();
|
||||
}
|
||||
|
||||
if (event.checked !== undefined) {
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
if (event.class !== undefined) {
|
||||
this.updateClass();
|
||||
}
|
||||
|
||||
if (event.label !== undefined) {
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
}
|
||||
|
||||
if (event.tooltip !== undefined) {
|
||||
this.updateTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
get actionRunner(): IActionRunner {
|
||||
if (!this._actionRunner) {
|
||||
this._actionRunner = this._register(new ActionRunner());
|
||||
}
|
||||
|
||||
return this._actionRunner;
|
||||
}
|
||||
|
||||
set actionRunner(actionRunner: IActionRunner) {
|
||||
this._actionRunner = actionRunner;
|
||||
}
|
||||
|
||||
getAction(): IAction {
|
||||
return this._action;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this._action.enabled;
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
this._context = newContext;
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
const element = this.element = container;
|
||||
this._register(Gesture.addTarget(container));
|
||||
|
||||
const enableDragging = this.options && this.options.draggable;
|
||||
if (enableDragging) {
|
||||
container.draggable = true;
|
||||
|
||||
if (isFirefox) {
|
||||
// Firefox: requires to set a text data transfer to get going
|
||||
this._register(addDisposableListener(container, EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
|
||||
}
|
||||
}
|
||||
|
||||
this._register(addDisposableListener(element, TouchEventType.Tap, e => this.onClick(e)));
|
||||
|
||||
this._register(addDisposableListener(element, EventType.MOUSE_DOWN, e => {
|
||||
if (!enableDragging) {
|
||||
EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
|
||||
}
|
||||
|
||||
if (this._action.enabled && e.button === 0) {
|
||||
element.classList.add('active');
|
||||
}
|
||||
}));
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
|
||||
// main mouse button. This is for scenarios where e.g. some interaction forces
|
||||
// the Ctrl+key to be pressed and hold but the user still wants to interact
|
||||
// with the actions (for example quick access in quick navigation mode).
|
||||
this._register(addDisposableListener(element, EventType.CONTEXT_MENU, e => {
|
||||
if (e.button === 0 && e.ctrlKey === true) {
|
||||
this.onClick(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(addDisposableListener(element, EventType.CLICK, e => {
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
// menus do not use the click event
|
||||
if (!(this.options && this.options.isMenu)) {
|
||||
platform.setImmediate(() => this.onClick(e));
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(element, EventType.DBLCLICK, e => {
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
|
||||
[EventType.MOUSE_UP, EventType.MOUSE_OUT].forEach(event => {
|
||||
this._register(addDisposableListener(element, event, e => {
|
||||
EventHelper.stop(e);
|
||||
element.classList.remove('active');
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
onClick(event: EventLike): void {
|
||||
EventHelper.stop(event, true);
|
||||
|
||||
const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context;
|
||||
this.actionRunner.run(this._action, context);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.element) {
|
||||
this.element.focus();
|
||||
this.element.classList.add('focused');
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.element) {
|
||||
this.element.blur();
|
||||
this.element.classList.remove('focused');
|
||||
}
|
||||
}
|
||||
|
||||
protected updateEnabled(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateLabel(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateTooltip(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateClass(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
protected updateChecked(): void {
|
||||
// implement in subclass
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.element) {
|
||||
this.element.remove();
|
||||
this.element = undefined;
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
|
||||
icon?: boolean;
|
||||
label?: boolean;
|
||||
keybinding?: string | null;
|
||||
}
|
||||
|
||||
export class ActionViewItem extends BaseActionViewItem {
|
||||
|
||||
protected label: HTMLElement | undefined;
|
||||
protected options: IActionViewItemOptions;
|
||||
|
||||
private cssClass?: string;
|
||||
|
||||
constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) {
|
||||
super(context, action, options);
|
||||
|
||||
this.options = options;
|
||||
this.options.icon = options.icon !== undefined ? options.icon : false;
|
||||
this.options.label = options.label !== undefined ? options.label : true;
|
||||
this.cssClass = '';
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
|
||||
if (this.element) {
|
||||
this.label = append(this.element, $('a.action-label'));
|
||||
}
|
||||
|
||||
if (this.label) {
|
||||
if (this._action.id === Separator.ID) {
|
||||
this.label.setAttribute('role', 'presentation'); // A separator is a presentation item
|
||||
} else {
|
||||
if (this.options.isMenu) {
|
||||
this.label.setAttribute('role', 'menuitem');
|
||||
} else {
|
||||
this.label.setAttribute('role', 'button');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.label && this.options.keybinding && this.element) {
|
||||
append(this.element, $('span.keybinding')).textContent = this.options.keybinding;
|
||||
}
|
||||
|
||||
this.updateClass();
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
this.updateEnabled();
|
||||
this.updateChecked();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
|
||||
if (this.label) {
|
||||
this.label.focus();
|
||||
}
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
if (this.options.label && this.label) {
|
||||
this.label.textContent = this.getAction().label;
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltip(): void {
|
||||
let title: string | null = null;
|
||||
|
||||
if (this.getAction().tooltip) {
|
||||
title = this.getAction().tooltip;
|
||||
|
||||
} else if (!this.options.label && this.getAction().label && this.options.icon) {
|
||||
title = this.getAction().label;
|
||||
|
||||
if (this.options.keybinding) {
|
||||
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
|
||||
}
|
||||
}
|
||||
|
||||
if (title && this.label) {
|
||||
this.label.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
updateClass(): void {
|
||||
if (this.cssClass && this.label) {
|
||||
this.label.classList.remove(...this.cssClass.split(' '));
|
||||
}
|
||||
|
||||
if (this.options.icon) {
|
||||
this.cssClass = this.getAction().class;
|
||||
|
||||
if (this.label) {
|
||||
this.label.classList.add('codicon');
|
||||
if (this.cssClass) {
|
||||
this.label.classList.add(...this.cssClass.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEnabled();
|
||||
} else {
|
||||
if (this.label) {
|
||||
this.label.classList.remove('codicon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this.element.classList.remove('disabled');
|
||||
}
|
||||
} else {
|
||||
if (this.label) {
|
||||
this.label.setAttribute('aria-disabled', 'true');
|
||||
this.label.classList.add('disabled');
|
||||
removeTabIndexAndUpdateFocus(this.label);
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
this.element.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
if (this.label) {
|
||||
if (this.getAction().checked) {
|
||||
this.label.classList.add('checked');
|
||||
} else {
|
||||
this.label.classList.remove('checked');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectActionViewItem extends BaseActionViewItem {
|
||||
protected selectBox: SelectBox;
|
||||
|
||||
constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) {
|
||||
super(ctx, action);
|
||||
|
||||
this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
|
||||
|
||||
this._register(this.selectBox);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
setOptions(options: ISelectOptionItem[], selected?: number): void {
|
||||
this.selectBox.setOptions(options, selected);
|
||||
}
|
||||
|
||||
select(index: number): void {
|
||||
this.selectBox.select(index);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.selectBox.onDidSelect(e => {
|
||||
this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index));
|
||||
}));
|
||||
}
|
||||
|
||||
protected getActionContext(option: string, index: number) {
|
||||
return option;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
if (this.selectBox) {
|
||||
this.selectBox.blur();
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
this.selectBox.render(container);
|
||||
}
|
||||
}
|
||||
110
lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.css
Normal file
110
lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.css
Normal file
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-action-bar {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-action-bar .actions-container {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .actions-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-action-bar.reverse .actions-container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item {
|
||||
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 */
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled {
|
||||
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;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-label {
|
||||
font-size: 11px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled .action-label,
|
||||
.monaco-action-bar .action-item.disabled .action-label:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Vertical actions */
|
||||
|
||||
.monaco-action-bar.vertical {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .action-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .action-label.separator {
|
||||
display: block;
|
||||
border-bottom: 1px solid #bbb;
|
||||
padding-top: 1px;
|
||||
margin-left: .8em;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Action Items */
|
||||
.monaco-action-bar .action-item.select-container {
|
||||
overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */
|
||||
flex: 1;
|
||||
max-width: 170px;
|
||||
min-width: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item > .action-label {
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown {
|
||||
margin-right: 4px;
|
||||
}
|
||||
536
lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.ts
Normal file
536
lib/vscode/src/vs/base/browser/ui/actionbar/actionbar.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./actionbar';
|
||||
import { Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator, IActionViewItem, IActionViewItemProvider } 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';
|
||||
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 const enum ActionsOrientation {
|
||||
HORIZONTAL,
|
||||
HORIZONTAL_REVERSE,
|
||||
VERTICAL,
|
||||
VERTICAL_REVERSE,
|
||||
}
|
||||
|
||||
export interface ActionTrigger {
|
||||
keys?: KeyCode[];
|
||||
keyDown: boolean;
|
||||
}
|
||||
|
||||
export interface IActionBarOptions {
|
||||
readonly orientation?: ActionsOrientation;
|
||||
readonly context?: any;
|
||||
readonly actionViewItemProvider?: IActionViewItemProvider;
|
||||
readonly actionRunner?: IActionRunner;
|
||||
readonly ariaLabel?: string;
|
||||
readonly animated?: boolean;
|
||||
readonly triggerKeys?: ActionTrigger;
|
||||
readonly allowContextMenu?: boolean;
|
||||
readonly preventLoopNavigation?: boolean;
|
||||
readonly ignoreOrientationForPreviousAndNextKey?: boolean;
|
||||
}
|
||||
|
||||
export interface IActionOptions extends IActionViewItemOptions {
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
private readonly options: IActionBarOptions;
|
||||
|
||||
private _actionRunner: IActionRunner;
|
||||
private _context: unknown;
|
||||
private readonly _orientation: ActionsOrientation;
|
||||
private readonly _triggerKeys: {
|
||||
keys: KeyCode[];
|
||||
keyDown: boolean;
|
||||
};
|
||||
private _actionIds: string[];
|
||||
|
||||
// View Items
|
||||
viewItems: IActionViewItem[];
|
||||
protected focusedItem?: number;
|
||||
private focusTracker: DOM.IFocusTracker;
|
||||
|
||||
// Elements
|
||||
domNode: HTMLElement;
|
||||
protected actionsList: HTMLElement;
|
||||
|
||||
private _onDidBlur = this._register(new Emitter<void>());
|
||||
readonly onDidBlur = this._onDidBlur.event;
|
||||
|
||||
private _onDidCancel = this._register(new Emitter<void>({ onFirstListenerAdd: () => this.cancelHasListener = true }));
|
||||
readonly onDidCancel = this._onDidCancel.event;
|
||||
private cancelHasListener = false;
|
||||
|
||||
private _onDidRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onDidRun = this._onDidRun.event;
|
||||
|
||||
private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onDidBeforeRun = this._onDidBeforeRun.event;
|
||||
|
||||
constructor(container: HTMLElement, options: IActionBarOptions = {}) {
|
||||
super();
|
||||
|
||||
this.options = options;
|
||||
this._context = options.context ?? null;
|
||||
this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL;
|
||||
this._triggerKeys = {
|
||||
keyDown: this.options.triggerKeys?.keyDown ?? false,
|
||||
keys: this.options.triggerKeys?.keys ?? [KeyCode.Enter, KeyCode.Space]
|
||||
};
|
||||
|
||||
if (this.options.actionRunner) {
|
||||
this._actionRunner = this.options.actionRunner;
|
||||
} else {
|
||||
this._actionRunner = new ActionRunner();
|
||||
this._register(this._actionRunner);
|
||||
}
|
||||
|
||||
this._register(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
|
||||
this._register(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
|
||||
|
||||
this._actionIds = [];
|
||||
this.viewItems = [];
|
||||
this.focusedItem = undefined;
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = 'monaco-action-bar';
|
||||
|
||||
if (options.animated !== false) {
|
||||
this.domNode.classList.add('animated');
|
||||
}
|
||||
|
||||
let previousKeys: KeyCode[];
|
||||
let nextKeys: KeyCode[];
|
||||
|
||||
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';
|
||||
break;
|
||||
case ActionsOrientation.VERTICAL:
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [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;
|
||||
|
||||
if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) {
|
||||
eventHandled = this.focusPrevious();
|
||||
} else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) {
|
||||
eventHandled = this.focusNext();
|
||||
} else if (event.equals(KeyCode.Escape) && this.cancelHasListener) {
|
||||
this._onDidCancel.fire();
|
||||
} else if (this.isTriggerKeyEvent(event)) {
|
||||
// Staying out of the else branch even if not triggered
|
||||
if (this._triggerKeys.keyDown) {
|
||||
this.doTrigger(event);
|
||||
}
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_UP, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
|
||||
// Run action on Enter/Space
|
||||
if (this.isTriggerKeyEvent(event)) {
|
||||
if (!this._triggerKeys.keyDown) {
|
||||
this.doTrigger(event);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Recompute focused item
|
||||
else if (event.equals(KeyCode.Tab) || event.equals(KeyMod.Shift | KeyCode.Tab)) {
|
||||
this.updateFocusedItem();
|
||||
}
|
||||
}));
|
||||
|
||||
this.focusTracker = this._register(DOM.trackFocus(this.domNode));
|
||||
this._register(this.focusTracker.onDidBlur(() => {
|
||||
if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) {
|
||||
this._onDidBlur.fire();
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
|
||||
|
||||
this.actionsList = document.createElement('ul');
|
||||
this.actionsList.className = 'actions-container';
|
||||
this.actionsList.setAttribute('role', 'toolbar');
|
||||
|
||||
if (this.options.ariaLabel) {
|
||||
this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
|
||||
}
|
||||
|
||||
this.domNode.appendChild(this.actionsList);
|
||||
|
||||
container.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
setAriaLabel(label: string): void {
|
||||
if (label) {
|
||||
this.actionsList.setAttribute('aria-label', label);
|
||||
} else {
|
||||
this.actionsList.removeAttribute('aria-label');
|
||||
}
|
||||
}
|
||||
|
||||
private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
|
||||
let ret = false;
|
||||
this._triggerKeys.keys.forEach(keyCode => {
|
||||
ret = ret || event.equals(keyCode);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private updateFocusedItem(): void {
|
||||
for (let i = 0; i < this.actionsList.children.length; i++) {
|
||||
const elem = this.actionsList.children[i];
|
||||
if (DOM.isAncestor(DOM.getActiveElement(), elem)) {
|
||||
this.focusedItem = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get context(): any {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
set context(context: any) {
|
||||
this._context = context;
|
||||
this.viewItems.forEach(i => i.setActionContext(context));
|
||||
}
|
||||
|
||||
get actionRunner(): IActionRunner {
|
||||
return this._actionRunner;
|
||||
}
|
||||
|
||||
set actionRunner(actionRunner: IActionRunner) {
|
||||
if (actionRunner) {
|
||||
this._actionRunner = actionRunner;
|
||||
this.viewItems.forEach(item => item.actionRunner = actionRunner);
|
||||
}
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
hasAction(action: IAction): boolean {
|
||||
return this._actionIds.includes(action.id);
|
||||
}
|
||||
|
||||
push(arg: IAction | ReadonlyArray<IAction>, options: IActionOptions = {}): void {
|
||||
const actions: ReadonlyArray<IAction> = Array.isArray(arg) ? arg : [arg];
|
||||
|
||||
let index = types.isNumber(options.index) ? options.index : null;
|
||||
|
||||
actions.forEach((action: IAction) => {
|
||||
const actionViewItemElement = document.createElement('li');
|
||||
actionViewItemElement.className = 'action-item';
|
||||
actionViewItemElement.setAttribute('role', 'presentation');
|
||||
|
||||
// Prevent native context menu on actions
|
||||
if (!this.options.allowContextMenu) {
|
||||
this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
}));
|
||||
}
|
||||
|
||||
let item: IActionViewItem | undefined;
|
||||
|
||||
if (this.options.actionViewItemProvider) {
|
||||
item = this.options.actionViewItemProvider(action);
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
item = new ActionViewItem(this.context, action, options);
|
||||
}
|
||||
|
||||
item.actionRunner = this._actionRunner;
|
||||
item.setActionContext(this.context);
|
||||
item.render(actionViewItemElement);
|
||||
|
||||
if (index === null || index < 0 || index >= this.actionsList.children.length) {
|
||||
this.actionsList.appendChild(actionViewItemElement);
|
||||
this.viewItems.push(item);
|
||||
this._actionIds.push(action.id);
|
||||
} else {
|
||||
this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]);
|
||||
this.viewItems.splice(index, 0, item);
|
||||
this._actionIds.splice(index, 0, action.id);
|
||||
index++;
|
||||
}
|
||||
});
|
||||
if (this.focusedItem) {
|
||||
// After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
|
||||
this.focus(this.focusedItem);
|
||||
}
|
||||
}
|
||||
|
||||
getWidth(index: number): number {
|
||||
if (index >= 0 && index < this.actionsList.children.length) {
|
||||
const item = this.actionsList.children.item(index);
|
||||
if (item) {
|
||||
return item.clientWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
getHeight(index: number): number {
|
||||
if (index >= 0 && index < this.actionsList.children.length) {
|
||||
const item = this.actionsList.children.item(index);
|
||||
if (item) {
|
||||
return item.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pull(index: number): void {
|
||||
if (index >= 0 && index < this.viewItems.length) {
|
||||
this.actionsList.removeChild(this.actionsList.childNodes[index]);
|
||||
dispose(this.viewItems.splice(index, 1));
|
||||
this._actionIds.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
dispose(this.viewItems);
|
||||
this.viewItems = [];
|
||||
this._actionIds = [];
|
||||
DOM.clearNode(this.actionsList);
|
||||
}
|
||||
|
||||
length(): number {
|
||||
return this.viewItems.length;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.viewItems.length === 0;
|
||||
}
|
||||
|
||||
focus(index?: number): void;
|
||||
focus(selectFirst?: boolean): void;
|
||||
focus(arg?: number | boolean): void {
|
||||
let selectFirst: boolean = false;
|
||||
let index: number | undefined = undefined;
|
||||
if (arg === undefined) {
|
||||
selectFirst = true;
|
||||
} else if (typeof arg === 'number') {
|
||||
index = arg;
|
||||
} else if (typeof arg === 'boolean') {
|
||||
selectFirst = arg;
|
||||
}
|
||||
|
||||
if (selectFirst && typeof this.focusedItem === 'undefined') {
|
||||
const firstEnabled = this.viewItems.findIndex(item => item.isEnabled());
|
||||
// Focus the first enabled item
|
||||
this.focusedItem = firstEnabled === -1 ? undefined : firstEnabled;
|
||||
this.updateFocus();
|
||||
} else {
|
||||
if (index !== undefined) {
|
||||
this.focusedItem = index;
|
||||
}
|
||||
|
||||
this.updateFocus();
|
||||
}
|
||||
}
|
||||
|
||||
protected focusNext(): boolean {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.focusedItem = this.viewItems.length - 1;
|
||||
}
|
||||
|
||||
const startIndex = this.focusedItem;
|
||||
let item: IActionViewItem;
|
||||
|
||||
do {
|
||||
if (this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
|
||||
this.focusedItem = startIndex;
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.updateFocus();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected focusPrevious(): boolean {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.focusedItem = 0;
|
||||
}
|
||||
|
||||
const startIndex = this.focusedItem;
|
||||
let item: IActionViewItem;
|
||||
|
||||
do {
|
||||
this.focusedItem = this.focusedItem - 1;
|
||||
|
||||
if (this.focusedItem < 0) {
|
||||
if (this.options.preventLoopNavigation) {
|
||||
this.focusedItem = startIndex;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.focusedItem = this.viewItems.length - 1;
|
||||
}
|
||||
|
||||
item = this.viewItems[this.focusedItem];
|
||||
} while (this.focusedItem !== startIndex && !item.isEnabled());
|
||||
|
||||
if (this.focusedItem === startIndex && !item.isEnabled()) {
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
|
||||
this.updateFocus(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected updateFocus(fromRight?: boolean, preventScroll?: boolean): void {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.actionsList.focus({ preventScroll });
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.viewItems.length; i++) {
|
||||
const item = this.viewItems[i];
|
||||
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 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (types.isFunction(actionViewItem.blur)) {
|
||||
actionViewItem.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doTrigger(event: StandardKeyboardEvent): void {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
return; //nothing to focus
|
||||
}
|
||||
|
||||
// trigger action
|
||||
const actionViewItem = this.viewItems[this.focusedItem];
|
||||
if (actionViewItem instanceof BaseActionViewItem) {
|
||||
const context = (actionViewItem._context === null || actionViewItem._context === undefined) ? event : actionViewItem._context;
|
||||
this.run(actionViewItem._action, context);
|
||||
}
|
||||
}
|
||||
|
||||
run(action: IAction, context?: unknown): Promise<void> {
|
||||
return this._actionRunner.run(action, context);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.viewItems);
|
||||
this.viewItems = [];
|
||||
|
||||
this._actionIds = [];
|
||||
|
||||
this.getContainer().remove();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareActions(actions: IAction[]): IAction[] {
|
||||
if (!actions.length) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
// Clean up leading separators
|
||||
let firstIndexOfAction = -1;
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
if (actions[i].id === Separator.ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
firstIndexOfAction = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (firstIndexOfAction === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
actions = actions.slice(firstIndexOfAction);
|
||||
|
||||
// Clean up trailing separators
|
||||
for (let h = actions.length - 1; h >= 0; h--) {
|
||||
const isSeparator = actions[h].id === Separator.ID;
|
||||
if (isSeparator) {
|
||||
actions.splice(h, 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up separator duplicates
|
||||
let foundAction = false;
|
||||
for (let k = actions.length - 1; k >= 0; k--) {
|
||||
const isSeparator = actions[k].id === Separator.ID;
|
||||
if (isSeparator && !foundAction) {
|
||||
actions.splice(k, 1);
|
||||
} else if (!isSeparator) {
|
||||
foundAction = true;
|
||||
} else if (isSeparator) {
|
||||
foundAction = false;
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
9
lib/vscode/src/vs/base/browser/ui/aria/aria.css
Normal file
9
lib/vscode/src/vs/base/browser/ui/aria/aria.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-aria-container {
|
||||
position: absolute; /* try to hide from window but not from screen readers */
|
||||
left:-999em;
|
||||
}
|
||||
95
lib/vscode/src/vs/base/browser/ui/aria/aria.ts
Normal file
95
lib/vscode/src/vs/base/browser/ui/aria/aria.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./aria';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
// Use a max length since we are inserting the whole msg in the DOM and that can cause browsers to freeze for long messages #94233
|
||||
const MAX_MESSAGE_LENGTH = 20000;
|
||||
let ariaContainer: HTMLElement;
|
||||
let alertContainer: HTMLElement;
|
||||
let alertContainer2: HTMLElement;
|
||||
let statusContainer: HTMLElement;
|
||||
let statusContainer2: HTMLElement;
|
||||
export function setARIAContainer(parent: HTMLElement) {
|
||||
ariaContainer = document.createElement('div');
|
||||
ariaContainer.className = 'monaco-aria-container';
|
||||
|
||||
const createAlertContainer = () => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'monaco-alert';
|
||||
element.setAttribute('role', 'alert');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
ariaContainer.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
alertContainer = createAlertContainer();
|
||||
alertContainer2 = createAlertContainer();
|
||||
|
||||
const createStatusContainer = () => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'monaco-status';
|
||||
element.setAttribute('role', 'complementary');
|
||||
element.setAttribute('aria-live', 'polite');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
ariaContainer.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
statusContainer = createStatusContainer();
|
||||
statusContainer2 = createStatusContainer();
|
||||
|
||||
parent.appendChild(ariaContainer);
|
||||
}
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as alert to screen readers.
|
||||
*/
|
||||
export function alert(msg: string): void {
|
||||
if (!ariaContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use alternate containers such that duplicated messages get read out by screen readers #99466
|
||||
if (alertContainer.textContent !== msg) {
|
||||
dom.clearNode(alertContainer2);
|
||||
insertMessage(alertContainer, msg);
|
||||
} else {
|
||||
dom.clearNode(alertContainer);
|
||||
insertMessage(alertContainer2, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as status to screen readers.
|
||||
*/
|
||||
export function status(msg: string): void {
|
||||
if (!ariaContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMacintosh) {
|
||||
alert(msg); // VoiceOver does not seem to support status role
|
||||
} else {
|
||||
if (statusContainer.textContent !== msg) {
|
||||
dom.clearNode(statusContainer2);
|
||||
insertMessage(statusContainer, msg);
|
||||
} else {
|
||||
dom.clearNode(statusContainer);
|
||||
insertMessage(statusContainer2, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertMessage(target: HTMLElement, msg: string): void {
|
||||
dom.clearNode(target);
|
||||
if (msg.length > MAX_MESSAGE_LENGTH) {
|
||||
msg = msg.substr(0, MAX_MESSAGE_LENGTH);
|
||||
}
|
||||
target.textContent = msg;
|
||||
|
||||
// See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
|
||||
target.style.visibility = 'hidden';
|
||||
target.style.visibility = 'visible';
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-breadcrumbs {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
outline-style: none;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item .codicon-breadcrumb-separator {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item:first-of-type::before {
|
||||
content: ' ';
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { commonPrefixLength } from 'vs/base/common/arrays';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import 'vs/css!./breadcrumbsWidget';
|
||||
|
||||
export abstract class BreadcrumbsItem {
|
||||
dispose(): void { }
|
||||
abstract equals(other: BreadcrumbsItem): boolean;
|
||||
abstract render(container: HTMLElement): void;
|
||||
}
|
||||
|
||||
export class SimpleBreadcrumbsItem extends BreadcrumbsItem {
|
||||
|
||||
constructor(
|
||||
readonly text: string,
|
||||
readonly title: string = text
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
equals(other: this) {
|
||||
return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title;
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
let node = document.createElement('div');
|
||||
node.title = this.title;
|
||||
node.innerText = this.text;
|
||||
container.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IBreadcrumbsWidgetStyles {
|
||||
breadcrumbsBackground?: Color;
|
||||
breadcrumbsForeground?: Color;
|
||||
breadcrumbsHoverForeground?: Color;
|
||||
breadcrumbsFocusForeground?: Color;
|
||||
breadcrumbsFocusAndSelectionForeground?: Color;
|
||||
}
|
||||
|
||||
export interface IBreadcrumbsItemEvent {
|
||||
type: 'select' | 'focus';
|
||||
item: BreadcrumbsItem;
|
||||
node: HTMLElement;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
const breadcrumbSeparatorIcon = registerIcon('breadcrumb-separator', Codicon.chevronRight);
|
||||
|
||||
export class BreadcrumbsWidget {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _domNode: HTMLDivElement;
|
||||
private readonly _styleElement: HTMLStyleElement;
|
||||
private readonly _scrollable: DomScrollableElement;
|
||||
|
||||
private readonly _onDidSelectItem = new Emitter<IBreadcrumbsItemEvent>();
|
||||
private readonly _onDidFocusItem = new Emitter<IBreadcrumbsItemEvent>();
|
||||
private readonly _onDidChangeFocus = new Emitter<boolean>();
|
||||
|
||||
readonly onDidSelectItem: Event<IBreadcrumbsItemEvent> = this._onDidSelectItem.event;
|
||||
readonly onDidFocusItem: Event<IBreadcrumbsItemEvent> = this._onDidFocusItem.event;
|
||||
readonly onDidChangeFocus: Event<boolean> = this._onDidChangeFocus.event;
|
||||
|
||||
private readonly _items = new Array<BreadcrumbsItem>();
|
||||
private readonly _nodes = new Array<HTMLDivElement>();
|
||||
private readonly _freeNodes = new Array<HTMLDivElement>();
|
||||
|
||||
private _focusedItemIdx: number = -1;
|
||||
private _selectedItemIdx: number = -1;
|
||||
|
||||
private _pendingLayout: IDisposable | undefined;
|
||||
private _dimension: dom.Dimension | undefined;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
horizontalScrollbarSize: number,
|
||||
) {
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'monaco-breadcrumbs';
|
||||
this._domNode.tabIndex = 0;
|
||||
this._domNode.setAttribute('role', 'list');
|
||||
this._scrollable = new DomScrollableElement(this._domNode, {
|
||||
vertical: ScrollbarVisibility.Hidden,
|
||||
horizontal: ScrollbarVisibility.Auto,
|
||||
horizontalScrollbarSize,
|
||||
useShadows: false,
|
||||
scrollYToX: true
|
||||
});
|
||||
this._disposables.add(this._scrollable);
|
||||
this._disposables.add(dom.addStandardDisposableListener(this._domNode, 'click', e => this._onClick(e)));
|
||||
container.appendChild(this._scrollable.getDomNode());
|
||||
|
||||
this._styleElement = dom.createStyleSheet(this._domNode);
|
||||
|
||||
const focusTracker = dom.trackFocus(this._domNode);
|
||||
this._disposables.add(focusTracker);
|
||||
this._disposables.add(focusTracker.onDidBlur(_ => this._onDidChangeFocus.fire(false)));
|
||||
this._disposables.add(focusTracker.onDidFocus(_ => this._onDidChangeFocus.fire(true)));
|
||||
}
|
||||
|
||||
setHorizontalScrollbarSize(size: number) {
|
||||
this._scrollable.updateOptions({
|
||||
horizontalScrollbarSize: size
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this._pendingLayout?.dispose();
|
||||
this._onDidSelectItem.dispose();
|
||||
this._onDidFocusItem.dispose();
|
||||
this._onDidChangeFocus.dispose();
|
||||
this._domNode.remove();
|
||||
this._nodes.length = 0;
|
||||
this._freeNodes.length = 0;
|
||||
}
|
||||
|
||||
layout(dim: dom.Dimension | undefined): void {
|
||||
if (dim && dom.Dimension.equals(dim, this._dimension)) {
|
||||
return;
|
||||
}
|
||||
this._pendingLayout?.dispose();
|
||||
if (dim) {
|
||||
// only measure
|
||||
this._pendingLayout = this._updateDimensions(dim);
|
||||
} else {
|
||||
this._pendingLayout = this._updateScrollbar();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateDimensions(dim: dom.Dimension): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(dom.modify(() => {
|
||||
this._dimension = dim;
|
||||
this._domNode.style.width = `${dim.width}px`;
|
||||
this._domNode.style.height = `${dim.height}px`;
|
||||
disposables.add(this._updateScrollbar());
|
||||
}));
|
||||
return disposables;
|
||||
}
|
||||
|
||||
private _updateScrollbar(): IDisposable {
|
||||
return dom.measure(() => {
|
||||
dom.measure(() => { // double RAF
|
||||
this._scrollable.setRevealOnScroll(false);
|
||||
this._scrollable.scanDomNode();
|
||||
this._scrollable.setRevealOnScroll(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
style(style: IBreadcrumbsWidgetStyles): void {
|
||||
let content = '';
|
||||
if (style.breadcrumbsBackground) {
|
||||
content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`;
|
||||
}
|
||||
if (style.breadcrumbsForeground) {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsForeground}}\n`;
|
||||
}
|
||||
if (style.breadcrumbsFocusForeground) {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusForeground}}\n`;
|
||||
}
|
||||
if (style.breadcrumbsFocusAndSelectionForeground) {
|
||||
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`;
|
||||
}
|
||||
if (this._styleElement.innerText !== content) {
|
||||
this._styleElement.innerText = content;
|
||||
}
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
let idx = this._focusedItemIdx >= 0 ? this._focusedItemIdx : this._items.length - 1;
|
||||
if (idx >= 0 && idx < this._items.length) {
|
||||
this._focus(idx, undefined);
|
||||
} else {
|
||||
this._domNode.focus();
|
||||
}
|
||||
}
|
||||
|
||||
isDOMFocused(): boolean {
|
||||
let candidate = document.activeElement;
|
||||
while (candidate) {
|
||||
if (this._domNode === candidate) {
|
||||
return true;
|
||||
}
|
||||
candidate = candidate.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getFocused(): BreadcrumbsItem {
|
||||
return this._items[this._focusedItemIdx];
|
||||
}
|
||||
|
||||
setFocused(item: BreadcrumbsItem | undefined, payload?: any): void {
|
||||
this._focus(this._items.indexOf(item!), payload);
|
||||
}
|
||||
|
||||
focusPrev(payload?: any): any {
|
||||
if (this._focusedItemIdx > 0) {
|
||||
this._focus(this._focusedItemIdx - 1, payload);
|
||||
}
|
||||
}
|
||||
|
||||
focusNext(payload?: any): any {
|
||||
if (this._focusedItemIdx + 1 < this._nodes.length) {
|
||||
this._focus(this._focusedItemIdx + 1, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private _focus(nth: number, payload: any): void {
|
||||
this._focusedItemIdx = -1;
|
||||
for (let i = 0; i < this._nodes.length; i++) {
|
||||
const node = this._nodes[i];
|
||||
if (i !== nth) {
|
||||
node.classList.remove('focused');
|
||||
} else {
|
||||
this._focusedItemIdx = i;
|
||||
node.classList.add('focused');
|
||||
node.focus();
|
||||
}
|
||||
}
|
||||
this._reveal(this._focusedItemIdx, true);
|
||||
this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx], payload });
|
||||
}
|
||||
|
||||
reveal(item: BreadcrumbsItem): void {
|
||||
let idx = this._items.indexOf(item);
|
||||
if (idx >= 0) {
|
||||
this._reveal(idx, false);
|
||||
}
|
||||
}
|
||||
|
||||
private _reveal(nth: number, minimal: boolean): void {
|
||||
const node = this._nodes[nth];
|
||||
if (node) {
|
||||
const { width } = this._scrollable.getScrollDimensions();
|
||||
const { scrollLeft } = this._scrollable.getScrollPosition();
|
||||
if (!minimal || node.offsetLeft > scrollLeft + width || node.offsetLeft < scrollLeft) {
|
||||
this._scrollable.setRevealOnScroll(false);
|
||||
this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft });
|
||||
this._scrollable.setRevealOnScroll(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSelection(): BreadcrumbsItem {
|
||||
return this._items[this._selectedItemIdx];
|
||||
}
|
||||
|
||||
setSelection(item: BreadcrumbsItem | undefined, payload?: any): void {
|
||||
this._select(this._items.indexOf(item!), payload);
|
||||
}
|
||||
|
||||
private _select(nth: number, payload: any): void {
|
||||
this._selectedItemIdx = -1;
|
||||
for (let i = 0; i < this._nodes.length; i++) {
|
||||
const node = this._nodes[i];
|
||||
if (i !== nth) {
|
||||
node.classList.remove('selected');
|
||||
} else {
|
||||
this._selectedItemIdx = i;
|
||||
node.classList.add('selected');
|
||||
}
|
||||
}
|
||||
this._onDidSelectItem.fire({ type: 'select', item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx], payload });
|
||||
}
|
||||
|
||||
getItems(): readonly BreadcrumbsItem[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
setItems(items: BreadcrumbsItem[]): void {
|
||||
let prefix: number | undefined;
|
||||
let removed: BreadcrumbsItem[] = [];
|
||||
try {
|
||||
prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b));
|
||||
removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix));
|
||||
this._render(prefix);
|
||||
dispose(removed);
|
||||
this._focus(-1, undefined);
|
||||
} catch (e) {
|
||||
let newError = new Error(`BreadcrumbsItem#setItems: newItems: ${items.length}, prefix: ${prefix}, removed: ${removed.length}`);
|
||||
newError.name = e.name;
|
||||
newError.stack = e.stack;
|
||||
throw newError;
|
||||
}
|
||||
}
|
||||
|
||||
private _render(start: number): void {
|
||||
for (; start < this._items.length && start < this._nodes.length; start++) {
|
||||
let item = this._items[start];
|
||||
let node = this._nodes[start];
|
||||
this._renderItem(item, node);
|
||||
}
|
||||
// case a: more nodes -> remove them
|
||||
while (start < this._nodes.length) {
|
||||
const free = this._nodes.pop();
|
||||
if (free) {
|
||||
this._freeNodes.push(free);
|
||||
free.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// case b: more items -> render them
|
||||
for (; start < this._items.length; start++) {
|
||||
let item = this._items[start];
|
||||
let node = this._freeNodes.length > 0 ? this._freeNodes.pop() : document.createElement('div');
|
||||
if (node) {
|
||||
this._renderItem(item, node);
|
||||
this._domNode.appendChild(node);
|
||||
this._nodes.push(node);
|
||||
}
|
||||
}
|
||||
this.layout(undefined);
|
||||
}
|
||||
|
||||
private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void {
|
||||
dom.clearNode(container);
|
||||
container.className = '';
|
||||
item.render(container);
|
||||
container.tabIndex = -1;
|
||||
container.setAttribute('role', 'listitem');
|
||||
container.classList.add('monaco-breadcrumb-item');
|
||||
const iconContainer = dom.$(breadcrumbSeparatorIcon.cssSelector);
|
||||
container.appendChild(iconContainer);
|
||||
}
|
||||
|
||||
private _onClick(event: IMouseEvent): void {
|
||||
for (let el: HTMLElement | null = event.target; el; el = el.parentElement) {
|
||||
let idx = this._nodes.indexOf(el as HTMLDivElement);
|
||||
if (idx >= 0) {
|
||||
this._focus(idx, event);
|
||||
this._select(idx, event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
lib/vscode/src/vs/base/browser/ui/button/button.css
Normal file
30
lib/vscode/src/vs/base/browser/ui/button/button.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-text-button {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
outline-offset: 2px !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-text-button:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.monaco-button.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-button > .codicon {
|
||||
margin: 0 0.2em;
|
||||
color: inherit !important;
|
||||
}
|
||||
263
lib/vscode/src/vs/base/browser/ui/button/button.ts
Normal file
263
lib/vscode/src/vs/base/browser/ui/button/button.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./button';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
|
||||
|
||||
export interface IButtonOptions extends IButtonStyles {
|
||||
readonly title?: boolean | string;
|
||||
readonly supportCodicons?: boolean;
|
||||
readonly secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface IButtonStyles {
|
||||
buttonBackground?: Color;
|
||||
buttonHoverBackground?: Color;
|
||||
buttonForeground?: Color;
|
||||
buttonSecondaryBackground?: Color;
|
||||
buttonSecondaryHoverBackground?: Color;
|
||||
buttonSecondaryForeground?: Color;
|
||||
buttonBorder?: Color;
|
||||
}
|
||||
|
||||
const defaultOptions: IButtonStyles = {
|
||||
buttonBackground: Color.fromHex('#0E639C'),
|
||||
buttonHoverBackground: Color.fromHex('#006BB3'),
|
||||
buttonForeground: Color.white
|
||||
};
|
||||
|
||||
export class Button extends Disposable {
|
||||
|
||||
private _element: 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.tabIndex = 0;
|
||||
this._element.setAttribute('role', 'button');
|
||||
|
||||
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.supportCodicons) {
|
||||
reset(this._element, ...renderCodicons(value));
|
||||
} else {
|
||||
this._element.textContent = value;
|
||||
}
|
||||
if (typeof this.options.title === 'string') {
|
||||
this._element.title = this.options.title;
|
||||
} else if (this.options.title) {
|
||||
this._element.title = value;
|
||||
}
|
||||
}
|
||||
|
||||
set icon(iconClassName: string) {
|
||||
this._element.classList.add(iconClassName);
|
||||
}
|
||||
|
||||
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));
|
||||
removeTabIndexAndUpdateFocus(this._element);
|
||||
}
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return !this._element.classList.contains('disabled');
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._element.focus();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this._element === document.activeElement;
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonGroup extends Disposable {
|
||||
private _buttons: Button[] = [];
|
||||
|
||||
constructor(container: HTMLElement, count: number, options?: IButtonOptions) {
|
||||
super();
|
||||
|
||||
this.create(container, count, options);
|
||||
}
|
||||
|
||||
get buttons(): Button[] {
|
||||
return this._buttons;
|
||||
}
|
||||
|
||||
private create(container: HTMLElement, count: number, options?: IButtonOptions): void {
|
||||
for (let index = 0; index < count; index++) {
|
||||
const button = this._register(new Button(container, options));
|
||||
this._buttons.push(button);
|
||||
|
||||
// Implement keyboard access in buttons if there are multiple
|
||||
if (count > 1) {
|
||||
this._register(addDisposableListener(button.element, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = true;
|
||||
|
||||
// Next / Previous Button
|
||||
let buttonIndexToFocus: number | undefined;
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1;
|
||||
} else if (event.equals(KeyCode.RightArrow)) {
|
||||
buttonIndexToFocus = index === this._buttons.length - 1 ? 0 : index + 1;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled && typeof buttonIndexToFocus === 'number') {
|
||||
this._buttons[buttonIndexToFocus].focus();
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
183
lib/vscode/src/vs/base/browser/ui/centered/centeredViewLayout.ts
Normal file
183
lib/vscode/src/vs/base/browser/ui/centered/centeredViewLayout.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SplitView, Orientation, ISplitViewStyles, IView as ISplitViewView } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { IBoundarySashes } from 'vs/base/browser/ui/grid/gridview';
|
||||
|
||||
export interface CenteredViewState {
|
||||
leftMarginRatio: number;
|
||||
rightMarginRatio: number;
|
||||
}
|
||||
|
||||
const GOLDEN_RATIO = {
|
||||
leftMarginRatio: 0.1909,
|
||||
rightMarginRatio: 0.1909
|
||||
};
|
||||
|
||||
function createEmptyView(background: Color | undefined): ISplitViewView {
|
||||
const element = $('.centered-layout-margin');
|
||||
element.style.height = '100%';
|
||||
if (background) {
|
||||
element.style.backgroundColor = background.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
element,
|
||||
layout: () => undefined,
|
||||
minimumSize: 60,
|
||||
maximumSize: Number.POSITIVE_INFINITY,
|
||||
onDidChange: Event.None
|
||||
};
|
||||
}
|
||||
|
||||
function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView {
|
||||
return {
|
||||
element: view.element,
|
||||
get maximumSize() { return view.maximumWidth; },
|
||||
get minimumSize() { return view.minimumWidth; },
|
||||
onDidChange: Event.map(view.onDidChange, e => e && e.width),
|
||||
layout: (size, offset) => view.layout(size, getHeight(), 0, offset)
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICenteredViewStyles extends ISplitViewStyles {
|
||||
background: Color;
|
||||
}
|
||||
|
||||
export class CenteredViewLayout implements IDisposable {
|
||||
|
||||
private splitView?: SplitView;
|
||||
private width: number = 0;
|
||||
private height: number = 0;
|
||||
private style!: ICenteredViewStyles;
|
||||
private didLayout = false;
|
||||
private emptyViews: ISplitViewView[] | undefined;
|
||||
private readonly splitViewDisposables = new DisposableStore();
|
||||
|
||||
constructor(private container: HTMLElement, private view: IView, public readonly state: CenteredViewState = { leftMarginRatio: GOLDEN_RATIO.leftMarginRatio, rightMarginRatio: GOLDEN_RATIO.rightMarginRatio }) {
|
||||
this.container.appendChild(this.view.element);
|
||||
// Make sure to hide the split view overflow like sashes #52892
|
||||
this.container.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
get minimumWidth(): number { return this.splitView ? this.splitView.minimumSize : this.view.minimumWidth; }
|
||||
get maximumWidth(): number { return this.splitView ? this.splitView.maximumSize : this.view.maximumWidth; }
|
||||
get minimumHeight(): number { return this.view.minimumHeight; }
|
||||
get maximumHeight(): number { return this.view.maximumHeight; }
|
||||
get onDidChange(): Event<IViewSize | undefined> { return this.view.onDidChange; }
|
||||
|
||||
private _boundarySashes: IBoundarySashes = {};
|
||||
get boundarySashes(): IBoundarySashes { return this._boundarySashes; }
|
||||
set boundarySashes(boundarySashes: IBoundarySashes) {
|
||||
this._boundarySashes = boundarySashes;
|
||||
|
||||
if (!this.splitView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.splitView.orthogonalStartSash = boundarySashes.top;
|
||||
this.splitView.orthogonalEndSash = boundarySashes.bottom;
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
if (this.splitView) {
|
||||
this.splitView.layout(width);
|
||||
if (!this.didLayout) {
|
||||
this.resizeMargins();
|
||||
}
|
||||
} else {
|
||||
this.view.layout(width, height, 0, 0);
|
||||
}
|
||||
this.didLayout = true;
|
||||
}
|
||||
|
||||
private resizeMargins(): void {
|
||||
if (!this.splitView) {
|
||||
return;
|
||||
}
|
||||
this.splitView.resizeView(0, this.state.leftMarginRatio * this.width);
|
||||
this.splitView.resizeView(2, this.state.rightMarginRatio * this.width);
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return !!this.splitView;
|
||||
}
|
||||
|
||||
styles(style: ICenteredViewStyles): void {
|
||||
this.style = style;
|
||||
if (this.splitView && this.emptyViews) {
|
||||
this.splitView.style(this.style);
|
||||
this.emptyViews[0].element.style.backgroundColor = this.style.background.toString();
|
||||
this.emptyViews[1].element.style.backgroundColor = this.style.background.toString();
|
||||
}
|
||||
}
|
||||
|
||||
activate(active: boolean): void {
|
||||
if (active === this.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
this.container.removeChild(this.view.element);
|
||||
this.splitView = new SplitView(this.container, {
|
||||
inverseAltBehavior: true,
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
styles: this.style
|
||||
});
|
||||
this.splitView.orthogonalStartSash = this.boundarySashes.top;
|
||||
this.splitView.orthogonalEndSash = this.boundarySashes.bottom;
|
||||
|
||||
this.splitViewDisposables.add(this.splitView.onDidSashChange(() => {
|
||||
if (this.splitView) {
|
||||
this.state.leftMarginRatio = this.splitView.getViewSize(0) / this.width;
|
||||
this.state.rightMarginRatio = this.splitView.getViewSize(2) / this.width;
|
||||
}
|
||||
}));
|
||||
this.splitViewDisposables.add(this.splitView.onDidSashReset(() => {
|
||||
this.state.leftMarginRatio = GOLDEN_RATIO.leftMarginRatio;
|
||||
this.state.rightMarginRatio = GOLDEN_RATIO.rightMarginRatio;
|
||||
this.resizeMargins();
|
||||
}));
|
||||
|
||||
this.splitView.layout(this.width);
|
||||
this.splitView.addView(toSplitViewView(this.view, () => this.height), 0);
|
||||
const backgroundColor = this.style ? this.style.background : undefined;
|
||||
this.emptyViews = [createEmptyView(backgroundColor), createEmptyView(backgroundColor)];
|
||||
this.splitView.addView(this.emptyViews[0], this.state.leftMarginRatio * this.width, 0);
|
||||
this.splitView.addView(this.emptyViews[1], this.state.rightMarginRatio * this.width, 2);
|
||||
} else {
|
||||
if (this.splitView) {
|
||||
this.container.removeChild(this.splitView.el);
|
||||
}
|
||||
this.splitViewDisposables.clear();
|
||||
if (this.splitView) {
|
||||
this.splitView.dispose();
|
||||
}
|
||||
this.splitView = undefined;
|
||||
this.emptyViews = undefined;
|
||||
this.container.appendChild(this.view.element);
|
||||
}
|
||||
}
|
||||
|
||||
isDefault(state: CenteredViewState): boolean {
|
||||
return state.leftMarginRatio === GOLDEN_RATIO.leftMarginRatio && state.rightMarginRatio === GOLDEN_RATIO.rightMarginRatio;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.splitViewDisposables.dispose();
|
||||
|
||||
if (this.splitView) {
|
||||
this.splitView.dispose();
|
||||
this.splitView = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/vscode/src/vs/base/browser/ui/checkbox/checkbox.css
Normal file
50
lib/vscode/src/vs/base/browser/ui/checkbox/checkbox.css
Normal file
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-custom-checkbox {
|
||||
margin-left: 2px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
opacity: 0.7;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid transparent;
|
||||
padding: 1px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.monaco-custom-checkbox:hover,
|
||||
.monaco-custom-checkbox.checked {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hc-black .monaco-custom-checkbox {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.hc-black .monaco-custom-checkbox:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.monaco-custom-checkbox.monaco-simple-checkbox {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
margin-right: 9px;
|
||||
margin-left: 0px;
|
||||
padding: 0px;
|
||||
opacity: 1;
|
||||
background-size: 16px !important;
|
||||
}
|
||||
|
||||
/* hide check when unchecked */
|
||||
.monaco-custom-checkbox.monaco-simple-checkbox:not(.checked)::before {
|
||||
visibility: hidden;
|
||||
}
|
||||
251
lib/vscode/src/vs/base/browser/ui/checkbox/checkbox.ts
Normal file
251
lib/vscode/src/vs/base/browser/ui/checkbox/checkbox.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./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';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
|
||||
export interface ICheckboxOpts extends ICheckboxStyles {
|
||||
readonly actionClassName?: string;
|
||||
readonly icon?: Codicon;
|
||||
readonly title: string;
|
||||
readonly isChecked: boolean;
|
||||
}
|
||||
|
||||
export interface ICheckboxStyles {
|
||||
inputActiveOptionBorder?: Color;
|
||||
inputActiveOptionForeground?: Color;
|
||||
inputActiveOptionBackground?: Color;
|
||||
}
|
||||
|
||||
export interface ISimpleCheckboxStyles {
|
||||
checkboxBackground?: Color;
|
||||
checkboxBorder?: Color;
|
||||
checkboxForeground?: Color;
|
||||
}
|
||||
|
||||
const defaultOpts = {
|
||||
inputActiveOptionBorder: Color.fromHex('#007ACC00'),
|
||||
inputActiveOptionForeground: Color.fromHex('#FFFFFF'),
|
||||
inputActiveOptionBackground: Color.fromHex('#0E639C50')
|
||||
};
|
||||
|
||||
export class CheckboxActionViewItem extends BaseActionViewItem {
|
||||
|
||||
protected checkbox: Checkbox | undefined;
|
||||
protected readonly disposables = new DisposableStore();
|
||||
|
||||
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
|
||||
});
|
||||
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 {
|
||||
if (this.checkbox) {
|
||||
if (this.isEnabled()) {
|
||||
this.checkbox.enable();
|
||||
} else {
|
||||
this.checkbox.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateChecked(): void {
|
||||
if (this.checkbox) {
|
||||
this.checkbox.checked = this._action.checked;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class Checkbox extends Widget {
|
||||
|
||||
private readonly _onChange = this._register(new Emitter<boolean>());
|
||||
readonly onChange: Event<boolean /* via keyboard */> = this._onChange.event;
|
||||
|
||||
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
|
||||
readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
|
||||
|
||||
private readonly _opts: ICheckboxOpts;
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
private _checked: boolean;
|
||||
|
||||
constructor(opts: ICheckboxOpts) {
|
||||
super();
|
||||
|
||||
this._opts = { ...defaultOpts, ...opts };
|
||||
this._checked = this._opts.isChecked;
|
||||
|
||||
const classes = ['monaco-custom-checkbox'];
|
||||
if (this._opts.icon) {
|
||||
classes.push(this._opts.icon.classNames);
|
||||
} else {
|
||||
classes.push('codicon'); // todo@aeschli: remove once codicon fully adopted
|
||||
}
|
||||
if (this._opts.actionClassName) {
|
||||
classes.push(this._opts.actionClassName);
|
||||
}
|
||||
if (this._checked) {
|
||||
classes.push('checked');
|
||||
}
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.title = this._opts.title;
|
||||
this.domNode.className = classes.join(' ');
|
||||
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);
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
this.onclick(this.domNode, (ev) => {
|
||||
this.checked = !this._checked;
|
||||
this._onChange.fire(false);
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
this.ignoreGesture(this.domNode);
|
||||
|
||||
this.onkeydown(this.domNode, (keyboardEvent) => {
|
||||
if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) {
|
||||
this.checked = !this._checked;
|
||||
this._onChange.fire(true);
|
||||
keyboardEvent.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this._onKeyDown.fire(keyboardEvent);
|
||||
});
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.domNode.getAttribute('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.domNode.focus();
|
||||
}
|
||||
|
||||
get checked(): boolean {
|
||||
return this._checked;
|
||||
}
|
||||
|
||||
set checked(newIsChecked: boolean) {
|
||||
this._checked = newIsChecked;
|
||||
|
||||
this.domNode.setAttribute('aria-checked', String(this._checked));
|
||||
this.domNode.classList.toggle('checked', this._checked);
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return 2 /*marginleft*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
|
||||
}
|
||||
|
||||
style(styles: ICheckboxStyles): void {
|
||||
if (styles.inputActiveOptionBorder) {
|
||||
this._opts.inputActiveOptionBorder = styles.inputActiveOptionBorder;
|
||||
}
|
||||
if (styles.inputActiveOptionForeground) {
|
||||
this._opts.inputActiveOptionForeground = styles.inputActiveOptionForeground;
|
||||
}
|
||||
if (styles.inputActiveOptionBackground) {
|
||||
this._opts.inputActiveOptionBackground = styles.inputActiveOptionBackground;
|
||||
}
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
if (this.domNode) {
|
||||
this.domNode.style.borderColor = this._checked && this._opts.inputActiveOptionBorder ? this._opts.inputActiveOptionBorder.toString() : 'transparent';
|
||||
this.domNode.style.color = this._checked && this._opts.inputActiveOptionForeground ? this._opts.inputActiveOptionForeground.toString() : 'inherit';
|
||||
this.domNode.style.backgroundColor = this._checked && this._opts.inputActiveOptionBackground ? this._opts.inputActiveOptionBackground.toString() : 'transparent';
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleCheckbox extends Widget {
|
||||
private checkbox: Checkbox;
|
||||
private styles: ISimpleCheckboxStyles;
|
||||
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
constructor(private title: string, private isChecked: boolean) {
|
||||
super();
|
||||
|
||||
this.checkbox = new Checkbox({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-simple-checkbox' });
|
||||
|
||||
this.domNode = this.checkbox.domNode;
|
||||
|
||||
this.styles = {};
|
||||
|
||||
this.checkbox.onChange(() => {
|
||||
this.applyStyles();
|
||||
});
|
||||
}
|
||||
|
||||
get checked(): boolean {
|
||||
return this.checkbox.checked;
|
||||
}
|
||||
|
||||
set checked(newIsChecked: boolean) {
|
||||
this.checkbox.checked = newIsChecked;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.domNode.focus();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this.domNode === document.activeElement;
|
||||
}
|
||||
|
||||
style(styles: ISimpleCheckboxStyles): void {
|
||||
this.styles = styles;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
this.domNode.style.color = this.styles.checkboxForeground ? this.styles.checkboxForeground.toString() : '';
|
||||
this.domNode.style.backgroundColor = this.styles.checkboxBackground ? this.styles.checkboxBackground.toString() : '';
|
||||
this.domNode.style.borderColor = this.styles.checkboxBorder ? this.styles.checkboxBorder.toString() : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@keyframes codicon-spin {
|
||||
100% {
|
||||
transform:rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.codicon-animation-spin {
|
||||
/* Use steps to throttle FPS to reduce CPU usage */
|
||||
animation: codicon-spin 1.5s steps(30) infinite;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.codicon-wrench-subaction {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
|
||||
}
|
||||
|
||||
.codicon[class*='codicon-'] {
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* icon rules are dynamically created in codiconStyles */
|
||||
BIN
lib/vscode/src/vs/base/browser/ui/codicons/codicon/codicon.ttf
Normal file
BIN
lib/vscode/src/vs/base/browser/ui/codicons/codicon/codicon.ttf
Normal file
Binary file not shown.
22
lib/vscode/src/vs/base/browser/ui/codicons/codiconLabel.ts
Normal file
22
lib/vscode/src/vs/base/browser/ui/codicons/codiconLabel.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { reset } from 'vs/base/browser/dom';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
|
||||
export class CodiconLabel {
|
||||
|
||||
constructor(
|
||||
private readonly _container: HTMLElement
|
||||
) { }
|
||||
|
||||
set text(text: string) {
|
||||
reset(this._container, ...renderCodicons(text ?? ''));
|
||||
}
|
||||
|
||||
set title(title: string) {
|
||||
this._container.title = title;
|
||||
}
|
||||
}
|
||||
29
lib/vscode/src/vs/base/browser/ui/codicons/codiconStyles.ts
Normal file
29
lib/vscode/src/vs/base/browser/ui/codicons/codiconStyles.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./codicon/codicon';
|
||||
import 'vs/css!./codicon/codicon-modifications';
|
||||
import 'vs/css!./codicon/codicon-animations';
|
||||
|
||||
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
|
||||
|
||||
export const CodiconStyles = new class {
|
||||
onDidChange = iconRegistry.onDidRegister;
|
||||
public getCSS(): string {
|
||||
const rules = [];
|
||||
for (let c of iconRegistry.all) {
|
||||
rules.push(formatRule(c));
|
||||
}
|
||||
return rules.join('\n');
|
||||
}
|
||||
};
|
||||
|
||||
export function formatRule(c: Codicon) {
|
||||
let def = c.definition;
|
||||
while (def instanceof Codicon) {
|
||||
def = def.definition;
|
||||
}
|
||||
return `.codicon-${c.id}:before { content: '${def.character}'; }`;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.context-view {
|
||||
position: absolute;
|
||||
z-index: 2500;
|
||||
}
|
||||
|
||||
.context-view.fixed {
|
||||
all: initial;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
position: fixed;
|
||||
z-index: 2500;
|
||||
color: inherit;
|
||||
}
|
||||
385
lib/vscode/src/vs/base/browser/ui/contextview/contextview.ts
Normal file
385
lib/vscode/src/vs/base/browser/ui/contextview/contextview.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./contextview';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/base/common/range';
|
||||
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
||||
|
||||
export const enum ContextViewDOMPosition {
|
||||
ABSOLUTE = 1,
|
||||
FIXED,
|
||||
FIXED_SHADOW
|
||||
}
|
||||
|
||||
export interface IAnchor {
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export const enum AnchorAlignment {
|
||||
LEFT, RIGHT
|
||||
}
|
||||
|
||||
export const enum AnchorPosition {
|
||||
BELOW, ABOVE
|
||||
}
|
||||
|
||||
export interface IDelegate {
|
||||
getAnchor(): HTMLElement | IAnchor;
|
||||
render(container: HTMLElement): IDisposable | null;
|
||||
focus?(): void;
|
||||
layout?(): void;
|
||||
anchorAlignment?: AnchorAlignment; // default: left
|
||||
anchorPosition?: AnchorPosition; // default: below
|
||||
canRelayout?: boolean; // default: true
|
||||
onDOMEvent?(e: Event, activeElement: HTMLElement): void;
|
||||
onHide?(data?: any): void;
|
||||
}
|
||||
|
||||
export interface IContextViewProvider {
|
||||
showContextView(delegate: IDelegate, container?: HTMLElement): void;
|
||||
hideContextView(): void;
|
||||
layout(): void;
|
||||
}
|
||||
|
||||
export interface IPosition {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
export interface ISize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface IView extends IPosition, ISize { }
|
||||
|
||||
export const enum LayoutAnchorPosition {
|
||||
Before,
|
||||
After
|
||||
}
|
||||
|
||||
export interface ILayoutAnchor {
|
||||
offset: number;
|
||||
size: number;
|
||||
position: LayoutAnchorPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays out a one dimensional view next to an anchor in a viewport.
|
||||
*
|
||||
* @returns The view offset within the viewport.
|
||||
*/
|
||||
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
|
||||
const anchorEnd = anchor.offset + anchor.size;
|
||||
|
||||
if (anchor.position === LayoutAnchorPosition.Before) {
|
||||
if (viewSize <= viewportSize - anchorEnd) {
|
||||
return anchorEnd; // happy case, lay it out after the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= anchor.offset) {
|
||||
return anchor.offset - viewSize; // ok case, lay it out before the anchor
|
||||
}
|
||||
|
||||
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
|
||||
} else {
|
||||
if (viewSize <= anchor.offset) {
|
||||
return anchor.offset - viewSize; // happy case, lay it out before the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= viewportSize - anchorEnd) {
|
||||
return anchorEnd; // ok case, lay it out after the anchor
|
||||
}
|
||||
|
||||
return 0; // sad case, lay it over the anchor
|
||||
}
|
||||
}
|
||||
|
||||
export class ContextView extends Disposable {
|
||||
|
||||
private static readonly BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur'];
|
||||
private static readonly BUBBLE_DOWN_EVENTS = ['click'];
|
||||
|
||||
private container: HTMLElement | null = null;
|
||||
private view: HTMLElement;
|
||||
private useFixedPosition: boolean;
|
||||
private useShadowDOM: boolean;
|
||||
private delegate: IDelegate | null = null;
|
||||
private toDisposeOnClean: IDisposable = Disposable.None;
|
||||
private toDisposeOnSetContainer: IDisposable = Disposable.None;
|
||||
private shadowRoot: ShadowRoot | null = null;
|
||||
private shadowRootHostElement: HTMLElement | null = null;
|
||||
|
||||
constructor(container: HTMLElement, domPosition: ContextViewDOMPosition) {
|
||||
super();
|
||||
|
||||
this.view = DOM.$('.context-view');
|
||||
this.useFixedPosition = false;
|
||||
this.useShadowDOM = false;
|
||||
|
||||
DOM.hide(this.view);
|
||||
|
||||
this.setContainer(container, domPosition);
|
||||
|
||||
this._register(toDisposable(() => this.setContainer(null, ContextViewDOMPosition.ABSOLUTE)));
|
||||
}
|
||||
|
||||
setContainer(container: HTMLElement | null, domPosition: ContextViewDOMPosition): void {
|
||||
if (this.container) {
|
||||
this.toDisposeOnSetContainer.dispose();
|
||||
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.removeChild(this.view);
|
||||
this.shadowRoot = null;
|
||||
this.shadowRootHostElement?.remove();
|
||||
this.shadowRootHostElement = null;
|
||||
} else {
|
||||
this.container.removeChild(this.view);
|
||||
}
|
||||
|
||||
this.container = null;
|
||||
}
|
||||
if (container) {
|
||||
this.container = container;
|
||||
|
||||
this.useFixedPosition = domPosition !== ContextViewDOMPosition.ABSOLUTE;
|
||||
this.useShadowDOM = domPosition === ContextViewDOMPosition.FIXED_SHADOW;
|
||||
|
||||
if (this.useShadowDOM) {
|
||||
this.shadowRootHostElement = DOM.$('.shadow-root-host');
|
||||
this.container.appendChild(this.shadowRootHostElement);
|
||||
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
|
||||
const style = document.createElement('style');
|
||||
style.textContent = SHADOW_ROOT_CSS;
|
||||
this.shadowRoot.appendChild(style);
|
||||
this.shadowRoot.appendChild(this.view);
|
||||
this.shadowRoot.appendChild(DOM.$('slot'));
|
||||
} else {
|
||||
this.container.appendChild(this.view);
|
||||
}
|
||||
|
||||
const toDisposeOnSetContainer = new DisposableStore();
|
||||
|
||||
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
|
||||
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, (e: Event) => {
|
||||
this.onDOMEvent(e, false);
|
||||
}));
|
||||
});
|
||||
|
||||
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
|
||||
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, (e: Event) => {
|
||||
this.onDOMEvent(e, true);
|
||||
}, true));
|
||||
});
|
||||
|
||||
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
|
||||
}
|
||||
}
|
||||
|
||||
show(delegate: IDelegate): void {
|
||||
if (this.isVisible()) {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
// Show static box
|
||||
DOM.clearNode(this.view);
|
||||
this.view.className = 'context-view';
|
||||
this.view.style.top = '0px';
|
||||
this.view.style.left = '0px';
|
||||
this.view.style.zIndex = '2500';
|
||||
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
|
||||
DOM.show(this.view);
|
||||
|
||||
// Render content
|
||||
this.toDisposeOnClean = delegate.render(this.view) || Disposable.None;
|
||||
|
||||
// Set active delegate
|
||||
this.delegate = delegate;
|
||||
|
||||
// Layout
|
||||
this.doLayout();
|
||||
|
||||
// Focus
|
||||
if (this.delegate.focus) {
|
||||
this.delegate.focus();
|
||||
}
|
||||
}
|
||||
|
||||
getViewElement(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.delegate!.canRelayout === false && !(platform.isIOS && BrowserFeatures.pointerEvents)) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.delegate!.layout) {
|
||||
this.delegate!.layout!();
|
||||
}
|
||||
|
||||
this.doLayout();
|
||||
}
|
||||
|
||||
private doLayout(): void {
|
||||
// Check that we still have a delegate - this.delegate.layout may have hidden
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get anchor
|
||||
let anchor = this.delegate!.getAnchor();
|
||||
|
||||
// Compute around
|
||||
let around: IView;
|
||||
|
||||
// Get the element's position and size (to anchor the view)
|
||||
if (DOM.isHTMLElement(anchor)) {
|
||||
let elementPosition = DOM.getDomNodePagePosition(anchor);
|
||||
|
||||
around = {
|
||||
top: elementPosition.top,
|
||||
left: elementPosition.left,
|
||||
width: elementPosition.width,
|
||||
height: elementPosition.height
|
||||
};
|
||||
} else {
|
||||
around = {
|
||||
top: anchor.y,
|
||||
left: anchor.x,
|
||||
width: anchor.width || 1,
|
||||
height: anchor.height || 2
|
||||
};
|
||||
}
|
||||
|
||||
const viewSizeWidth = DOM.getTotalWidth(this.view);
|
||||
const viewSizeHeight = DOM.getTotalHeight(this.view);
|
||||
|
||||
const anchorPosition = this.delegate!.anchorPosition || AnchorPosition.BELOW;
|
||||
const anchorAlignment = this.delegate!.anchorAlignment || AnchorAlignment.LEFT;
|
||||
|
||||
const verticalAnchor: ILayoutAnchor = { offset: around.top - window.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
|
||||
let horizontalAnchor: ILayoutAnchor;
|
||||
|
||||
if (anchorAlignment === AnchorAlignment.LEFT) {
|
||||
horizontalAnchor = { offset: around.left, size: 0, position: LayoutAnchorPosition.Before };
|
||||
} else {
|
||||
horizontalAnchor = { offset: around.left + around.width, size: 0, position: LayoutAnchorPosition.After };
|
||||
}
|
||||
|
||||
const top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset;
|
||||
|
||||
// if view intersects vertically with anchor, shift it horizontally
|
||||
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
|
||||
horizontalAnchor.size = around.width;
|
||||
if (anchorAlignment === AnchorAlignment.RIGHT) {
|
||||
horizontalAnchor.offset = around.left;
|
||||
}
|
||||
}
|
||||
|
||||
const left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
|
||||
this.view.classList.remove('top', 'bottom', 'left', 'right');
|
||||
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
|
||||
this.view.classList.add(anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
|
||||
this.view.classList.toggle('fixed', this.useFixedPosition);
|
||||
|
||||
const containerPosition = DOM.getDomNodePagePosition(this.container!);
|
||||
this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top)}px`;
|
||||
this.view.style.left = `${left - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).left : containerPosition.left)}px`;
|
||||
this.view.style.width = 'initial';
|
||||
}
|
||||
|
||||
hide(data?: any): void {
|
||||
const delegate = this.delegate;
|
||||
this.delegate = null;
|
||||
|
||||
if (delegate?.onHide) {
|
||||
delegate.onHide(data);
|
||||
}
|
||||
|
||||
this.toDisposeOnClean.dispose();
|
||||
|
||||
DOM.hide(this.view);
|
||||
}
|
||||
|
||||
private isVisible(): boolean {
|
||||
return !!this.delegate;
|
||||
}
|
||||
|
||||
private onDOMEvent(e: Event, onCapture: boolean): void {
|
||||
if (this.delegate) {
|
||||
if (this.delegate.onDOMEvent) {
|
||||
this.delegate.onDOMEvent(e, <HTMLElement>document.activeElement);
|
||||
} else if (onCapture && !DOM.isAncestor(<HTMLElement>e.target, this.container)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.hide();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
let SHADOW_ROOT_CSS = /* css */ `
|
||||
:host {
|
||||
all: initial; /* 1st rule so subsequent properties are reset. */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
|
||||
}
|
||||
|
||||
.codicon[class*='codicon-'] {
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
:host {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif;
|
||||
}
|
||||
|
||||
:host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
:host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; }
|
||||
:host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; }
|
||||
:host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; }
|
||||
:host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; }
|
||||
|
||||
:host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; }
|
||||
:host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; }
|
||||
:host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; }
|
||||
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
|
||||
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
|
||||
|
||||
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
|
||||
:host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
|
||||
`;
|
||||
24
lib/vscode/src/vs/base/browser/ui/countBadge/countBadge.css
Normal file
24
lib/vscode/src/vs/base/browser/ui/countBadge/countBadge.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-count-badge {
|
||||
padding: 3px 6px;
|
||||
border-radius: 11px;
|
||||
font-size: 11px;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
line-height: 11px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-count-badge.long {
|
||||
padding: 2px 3px;
|
||||
border-radius: 2px;
|
||||
min-height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
101
lib/vscode/src/vs/base/browser/ui/countBadge/countBadge.ts
Normal file
101
lib/vscode/src/vs/base/browser/ui/countBadge/countBadge.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./countBadge';
|
||||
import { $, append } from 'vs/base/browser/dom';
|
||||
import { format } from 'vs/base/common/strings';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { IThemable } from 'vs/base/common/styler';
|
||||
|
||||
export interface ICountBadgeOptions extends ICountBadgetyles {
|
||||
count?: number;
|
||||
countFormat?: string;
|
||||
titleFormat?: string;
|
||||
}
|
||||
|
||||
export interface ICountBadgetyles {
|
||||
badgeBackground?: Color;
|
||||
badgeForeground?: Color;
|
||||
badgeBorder?: Color;
|
||||
}
|
||||
|
||||
const defaultOpts = {
|
||||
badgeBackground: Color.fromHex('#4D4D4D'),
|
||||
badgeForeground: Color.fromHex('#FFFFFF')
|
||||
};
|
||||
|
||||
export class CountBadge implements IThemable {
|
||||
|
||||
private element: HTMLElement;
|
||||
private count: number = 0;
|
||||
private countFormat: string;
|
||||
private titleFormat: string;
|
||||
|
||||
private badgeBackground: Color | undefined;
|
||||
private badgeForeground: Color | undefined;
|
||||
private badgeBorder: Color | undefined;
|
||||
|
||||
private options: ICountBadgeOptions;
|
||||
|
||||
constructor(container: HTMLElement, options?: ICountBadgeOptions) {
|
||||
this.options = options || Object.create(null);
|
||||
mixin(this.options, defaultOpts, false);
|
||||
|
||||
this.badgeBackground = this.options.badgeBackground;
|
||||
this.badgeForeground = this.options.badgeForeground;
|
||||
this.badgeBorder = this.options.badgeBorder;
|
||||
|
||||
this.element = append(container, $('.monaco-count-badge'));
|
||||
this.countFormat = this.options.countFormat || '{0}';
|
||||
this.titleFormat = this.options.titleFormat || '';
|
||||
this.setCount(this.options.count || 0);
|
||||
}
|
||||
|
||||
setCount(count: number) {
|
||||
this.count = count;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setCountFormat(countFormat: string) {
|
||||
this.countFormat = countFormat;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setTitleFormat(titleFormat: string) {
|
||||
this.titleFormat = titleFormat;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.element.textContent = format(this.countFormat, this.count);
|
||||
this.element.title = format(this.titleFormat, this.count);
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
style(styles: ICountBadgetyles): void {
|
||||
this.badgeBackground = styles.badgeBackground;
|
||||
this.badgeForeground = styles.badgeForeground;
|
||||
this.badgeBorder = styles.badgeBorder;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private applyStyles(): void {
|
||||
if (this.element) {
|
||||
const background = this.badgeBackground ? this.badgeBackground.toString() : '';
|
||||
const foreground = this.badgeForeground ? this.badgeForeground.toString() : '';
|
||||
const border = this.badgeBorder ? this.badgeBorder.toString() : '';
|
||||
|
||||
this.element.style.backgroundColor = background;
|
||||
this.element.style.color = foreground;
|
||||
|
||||
this.element.style.borderWidth = border ? '1px' : '';
|
||||
this.element.style.borderStyle = border ? 'solid' : '';
|
||||
this.element.style.borderColor = border;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
lib/vscode/src/vs/base/browser/ui/dialog/dialog.css
Normal file
154
lib/vscode/src/vs/base/browser/ui/dialog/dialog.css
Normal file
@@ -0,0 +1,154 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/** Dialog: Modal Block */
|
||||
.monaco-dialog-modal-block {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left:0;
|
||||
top:0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-dialog-modal-block.dimmed {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/** Dialog: Container */
|
||||
.monaco-dialog-box {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
width: min-content;
|
||||
min-width: 500px;
|
||||
max-width: 90vw;
|
||||
min-height: 75px;
|
||||
padding: 10px;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
|
||||
/** Dialog: Title Actions Row */
|
||||
.monaco-dialog-box .dialog-toolbar-row {
|
||||
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;
|
||||
}
|
||||
|
||||
/** Dialog: Message Row */
|
||||
.monaco-dialog-box .dialog-message-row {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.monaco-dialog-box .dialog-message-row > .dialog-icon.codicon {
|
||||
flex: 0 0 48px;
|
||||
height: 48px;
|
||||
align-self: baseline;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
/** Dialog: Message Container */
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 24px;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
word-wrap: break-word; /* never overflow long words, but break to next line */
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/** Dialog: Message */
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message {
|
||||
line-height: 22px;
|
||||
font-size: 18px;
|
||||
flex: 1; /* let the message always grow */
|
||||
white-space: normal;
|
||||
word-wrap: break-word; /* never overflow long words, but break to next line */
|
||||
min-height: 48px; /* matches icon height */
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** Dialog: Details */
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-detail {
|
||||
line-height: 22px;
|
||||
flex: 1; /* let the message always grow */
|
||||
}
|
||||
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message a:focus {
|
||||
outline-width: 1px;
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
/** Dialog: Checkbox */
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-checkbox-row {
|
||||
padding: 15px 0px 0px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-checkbox-row .dialog-checkbox-message {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/** Dialog: Input */
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input {
|
||||
padding: 15px 0px 0px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input .monaco-inputbox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/** Dialog: Buttons Row */
|
||||
.monaco-dialog-box > .dialog-buttons-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 1px;
|
||||
overflow: hidden; /* buttons row should never overflow */
|
||||
}
|
||||
|
||||
.monaco-dialog-box > .dialog-buttons-row {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
padding: 20px 10px 10px;
|
||||
}
|
||||
|
||||
/** Dialog: Buttons */
|
||||
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button {
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
padding: 5px 10px;
|
||||
margin: 4px 5px; /* allows button focus outline to be visible */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
448
lib/vscode/src/vs/base/browser/ui/dialog/dialog.ts
Normal file
448
lib/vscode/src/vs/base/browser/ui/dialog/dialog.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dialog';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { $, hide, show, EventHelper, clearNode, isAncestor, addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
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 { ButtonGroup, 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';
|
||||
import { isMacintosh, isLinux } from 'vs/base/common/platform';
|
||||
import { SimpleCheckbox, ISimpleCheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
|
||||
export interface IDialogInputOptions {
|
||||
readonly placeholder?: string;
|
||||
readonly type?: 'text' | 'password';
|
||||
readonly value?: string;
|
||||
}
|
||||
|
||||
export interface IDialogOptions {
|
||||
readonly cancelId?: number;
|
||||
readonly detail?: string;
|
||||
readonly checkboxLabel?: string;
|
||||
readonly checkboxChecked?: boolean;
|
||||
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
|
||||
readonly inputs?: IDialogInputOptions[];
|
||||
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
|
||||
}
|
||||
|
||||
export interface IDialogResult {
|
||||
readonly button: number;
|
||||
readonly checkboxChecked?: boolean;
|
||||
readonly values?: string[];
|
||||
}
|
||||
|
||||
export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles {
|
||||
readonly dialogForeground?: Color;
|
||||
readonly dialogBackground?: Color;
|
||||
readonly dialogShadow?: Color;
|
||||
readonly dialogBorder?: Color;
|
||||
readonly errorIconForeground?: Color;
|
||||
readonly warningIconForeground?: Color;
|
||||
readonly infoIconForeground?: Color;
|
||||
readonly inputBackground?: Color;
|
||||
readonly inputForeground?: Color;
|
||||
readonly inputBorder?: Color;
|
||||
}
|
||||
|
||||
interface ButtonMapEntry {
|
||||
readonly label: string;
|
||||
readonly index: number;
|
||||
}
|
||||
|
||||
const dialogErrorIcon = registerIcon('dialog-error', Codicon.error);
|
||||
const dialogWarningIcon = registerIcon('dialog-warning', Codicon.warning);
|
||||
const dialogInfoIcon = registerIcon('dialog-info', Codicon.info);
|
||||
const dialogCloseIcon = registerIcon('dialog-close', Codicon.close);
|
||||
|
||||
export class Dialog extends Disposable {
|
||||
private readonly element: HTMLElement;
|
||||
private readonly shadowElement: HTMLElement;
|
||||
private modalElement: HTMLElement | undefined;
|
||||
private readonly buttonsContainer: HTMLElement;
|
||||
private readonly messageDetailElement: HTMLElement;
|
||||
private readonly iconElement: HTMLElement;
|
||||
private readonly checkbox: SimpleCheckbox | undefined;
|
||||
private readonly toolbarContainer: HTMLElement;
|
||||
private buttonGroup: ButtonGroup | undefined;
|
||||
private styles: IDialogStyles | undefined;
|
||||
private focusToReturn: HTMLElement | undefined;
|
||||
private readonly inputs: InputBox[];
|
||||
private readonly buttons: string[];
|
||||
|
||||
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.shadowElement = this.modalElement.appendChild($('.dialog-shadow'));
|
||||
this.element = this.shadowElement.appendChild($('.monaco-dialog-box'));
|
||||
this.element.setAttribute('role', 'dialog');
|
||||
hide(this.element);
|
||||
|
||||
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
|
||||
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
|
||||
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
|
||||
|
||||
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
|
||||
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
|
||||
const messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
|
||||
|
||||
if (this.options.detail) {
|
||||
const messageElement = 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;
|
||||
|
||||
if (this.options.inputs) {
|
||||
this.inputs = this.options.inputs.map(input => {
|
||||
const inputRowElement = messageContainer.appendChild($('.dialog-message-input'));
|
||||
|
||||
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
|
||||
placeholder: input.placeholder,
|
||||
type: input.type ?? 'text',
|
||||
}));
|
||||
|
||||
if (input.value) {
|
||||
inputBox.value = input.value;
|
||||
}
|
||||
|
||||
return inputBox;
|
||||
});
|
||||
} else {
|
||||
this.inputs = [];
|
||||
}
|
||||
|
||||
if (this.options.checkboxLabel) {
|
||||
const checkboxRowElement = messageContainer.appendChild($('.dialog-checkbox-row'));
|
||||
|
||||
const checkbox = this.checkbox = this._register(new SimpleCheckbox(this.options.checkboxLabel, !!this.options.checkboxChecked));
|
||||
|
||||
checkboxRowElement.appendChild(checkbox.domNode);
|
||||
|
||||
const checkboxMessageElement = checkboxRowElement.appendChild($('.dialog-checkbox-message'));
|
||||
checkboxMessageElement.innerText = this.options.checkboxLabel;
|
||||
this._register(addDisposableListener(checkboxMessageElement, EventType.CLICK, () => checkbox.checked = !checkbox.checked));
|
||||
}
|
||||
|
||||
const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row'));
|
||||
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
|
||||
}
|
||||
|
||||
private getAriaLabel(): string {
|
||||
let typeLabel = nls.localize('dialogInfoMessage', 'Info');
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
nls.localize('dialogErrorMessage', 'Error');
|
||||
break;
|
||||
case 'warning':
|
||||
nls.localize('dialogWarningMessage', 'Warning');
|
||||
break;
|
||||
case 'pending':
|
||||
nls.localize('dialogPendingMessage', 'In Progress');
|
||||
break;
|
||||
case 'none':
|
||||
case 'info':
|
||||
case 'question':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return `${typeLabel}: ${this.message} ${this.options.detail || ''}`;
|
||||
}
|
||||
|
||||
updateMessage(message: string): void {
|
||||
this.messageDetailElement.innerText = message;
|
||||
}
|
||||
|
||||
async show(): Promise<IDialogResult> {
|
||||
this.focusToReturn = document.activeElement as HTMLElement;
|
||||
|
||||
return new Promise<IDialogResult>((resolve) => {
|
||||
clearNode(this.buttonsContainer);
|
||||
|
||||
const buttonGroup = this.buttonGroup = this._register(new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true }));
|
||||
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
|
||||
|
||||
// Handle button clicks
|
||||
buttonGroup.buttons.forEach((button, index) => {
|
||||
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
|
||||
|
||||
this._register(button.onDidClick(e => {
|
||||
EventHelper.stop(e);
|
||||
|
||||
resolve({
|
||||
button: buttonMap[index].index,
|
||||
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
|
||||
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
// Handle keyboard events gloably: Tab, Arrow-Left/Right
|
||||
this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => {
|
||||
const evt = new StandardKeyboardEvent(e);
|
||||
|
||||
if (evt.equals(KeyCode.Enter)) {
|
||||
|
||||
// Enter in input field should OK the dialog
|
||||
if (this.inputs.some(input => input.hasFocus())) {
|
||||
EventHelper.stop(e);
|
||||
|
||||
resolve({
|
||||
button: buttonMap.find(button => button.index !== this.options.cancelId)?.index ?? 0,
|
||||
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
|
||||
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
|
||||
});
|
||||
}
|
||||
|
||||
return; // leave default handling
|
||||
}
|
||||
|
||||
if (evt.equals(KeyCode.Space)) {
|
||||
return; // leave default handling
|
||||
}
|
||||
|
||||
let eventHandled = false;
|
||||
|
||||
// Focus: Next / Previous
|
||||
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow) || evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
|
||||
|
||||
// Build a list of focusable elements in their visual order
|
||||
const focusableElements: { focus: () => void }[] = [];
|
||||
let focusedIndex = -1;
|
||||
for (const input of this.inputs) {
|
||||
focusableElements.push(input);
|
||||
if (input.hasFocus()) {
|
||||
focusedIndex = focusableElements.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkbox) {
|
||||
focusableElements.push(this.checkbox);
|
||||
if (this.checkbox.hasFocus()) {
|
||||
focusedIndex = focusableElements.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.buttonGroup) {
|
||||
for (const button of this.buttonGroup.buttons) {
|
||||
focusableElements.push(button);
|
||||
if (button.hasFocus()) {
|
||||
focusedIndex = focusableElements.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus next element (with wrapping)
|
||||
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
|
||||
if (focusedIndex === -1) {
|
||||
focusedIndex = 0; // default to focus first element if none have focus
|
||||
}
|
||||
|
||||
const newFocusedIndex = (focusedIndex + 1) % focusableElements.length;
|
||||
focusableElements[newFocusedIndex].focus();
|
||||
}
|
||||
|
||||
// Focus previous element (with wrapping)
|
||||
else {
|
||||
if (focusedIndex === -1) {
|
||||
focusedIndex = focusableElements.length; // default to focus last element if none have focus
|
||||
}
|
||||
|
||||
let newFocusedIndex = focusedIndex - 1;
|
||||
if (newFocusedIndex === -1) {
|
||||
newFocusedIndex = focusableElements.length - 1;
|
||||
}
|
||||
|
||||
focusableElements[newFocusedIndex].focus();
|
||||
}
|
||||
|
||||
eventHandled = true;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
EventHelper.stop(e, true);
|
||||
} else if (this.options.keyEventProcessor) {
|
||||
this.options.keyEventProcessor(evt);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(domEvent(window, 'keyup', true)((e: KeyboardEvent) => {
|
||||
EventHelper.stop(e, true);
|
||||
const evt = new StandardKeyboardEvent(e);
|
||||
|
||||
if (evt.equals(KeyCode.Escape)) {
|
||||
resolve({
|
||||
button: this.options.cancelId || 0,
|
||||
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Detect focus out
|
||||
this._register(domEvent(this.element, 'focusout', false)((e: FocusEvent) => {
|
||||
if (!!e.relatedTarget && !!this.element) {
|
||||
if (!isAncestor(e.relatedTarget as HTMLElement, this.element)) {
|
||||
this.focusToReturn = e.relatedTarget as HTMLElement;
|
||||
|
||||
if (e.target) {
|
||||
(e.target as HTMLElement).focus();
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray);
|
||||
|
||||
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, 'codicon-animation-spin');
|
||||
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
|
||||
});
|
||||
}));
|
||||
|
||||
actionBar.push(action, { icon: true, label: false, });
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
this.element.setAttribute('aria-label', this.getAriaLabel());
|
||||
show(this.element);
|
||||
|
||||
// Focus first element (input or button)
|
||||
if (this.inputs.length > 0) {
|
||||
this.inputs[0].focus();
|
||||
this.inputs[0].select();
|
||||
} else {
|
||||
buttonMap.forEach((value, index) => {
|
||||
if (value.index === 0) {
|
||||
buttonGroup.buttons[index].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private applyStyles() {
|
||||
if (this.styles) {
|
||||
const style = this.styles;
|
||||
|
||||
const fgColor = style.dialogForeground;
|
||||
const bgColor = style.dialogBackground;
|
||||
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
|
||||
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
|
||||
|
||||
this.shadowElement.style.boxShadow = shadowColor;
|
||||
|
||||
this.element.style.color = fgColor?.toString() ?? '';
|
||||
this.element.style.backgroundColor = bgColor?.toString() ?? '';
|
||||
this.element.style.border = border;
|
||||
|
||||
if (this.buttonGroup) {
|
||||
this.buttonGroup.buttons.forEach(button => button.style(style));
|
||||
}
|
||||
|
||||
if (this.checkbox) {
|
||||
this.checkbox.style(style);
|
||||
}
|
||||
|
||||
if (fgColor && bgColor) {
|
||||
const messageDetailColor = fgColor.transparent(.9);
|
||||
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
|
||||
}
|
||||
|
||||
let color;
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
color = style.errorIconForeground;
|
||||
break;
|
||||
case 'warning':
|
||||
color = style.warningIconForeground;
|
||||
break;
|
||||
default:
|
||||
color = style.infoIconForeground;
|
||||
break;
|
||||
}
|
||||
if (color) {
|
||||
this.iconElement.style.color = color.toString();
|
||||
}
|
||||
|
||||
for (const input of this.inputs) {
|
||||
input.style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style(style: IDialogStyles): void {
|
||||
this.styles = style;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
if (this.modalElement) {
|
||||
this.modalElement.remove();
|
||||
this.modalElement = undefined;
|
||||
}
|
||||
|
||||
if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) {
|
||||
this.focusToReturn.focus();
|
||||
this.focusToReturn = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private rearrangeButtons(buttons: Array<string>, cancelId: number | undefined): ButtonMapEntry[] {
|
||||
const buttonMap: ButtonMapEntry[] = [];
|
||||
|
||||
// Maps each button to its current label and old index so that when we move them around it's not a problem
|
||||
buttons.forEach((button, index) => {
|
||||
buttonMap.push({ label: button, index });
|
||||
});
|
||||
|
||||
// macOS/linux: reverse button order
|
||||
if (isMacintosh || isLinux) {
|
||||
if (cancelId !== undefined) {
|
||||
const cancelButton = buttonMap.splice(cancelId, 1)[0];
|
||||
buttonMap.reverse();
|
||||
buttonMap.splice(buttonMap.length - 1, 0, cancelButton);
|
||||
}
|
||||
}
|
||||
|
||||
return buttonMap;
|
||||
}
|
||||
}
|
||||
14
lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.css
Normal file
14
lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.css
Normal file
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-dropdown {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.monaco-dropdown > .dropdown-label {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
280
lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.ts
Normal file
280
lib/vscode/src/vs/base/browser/ui/dropdown/dropdown.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dropdown';
|
||||
import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch';
|
||||
import { ActionRunner, IAction } from 'vs/base/common/actions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IContextViewProvider, IAnchor, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IMenuOptions } from 'vs/base/browser/ui/menu/menu';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { EventHelper, EventType, append, $, addDisposableListener, DOMEvent } from 'vs/base/browser/dom';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export interface ILabelRenderer {
|
||||
(container: HTMLElement): IDisposable | null;
|
||||
}
|
||||
|
||||
export interface IBaseDropdownOptions {
|
||||
label?: string;
|
||||
labelRenderer?: ILabelRenderer;
|
||||
}
|
||||
|
||||
export class BaseDropdown extends ActionRunner {
|
||||
private _element: HTMLElement;
|
||||
private boxContainer?: HTMLElement;
|
||||
private _label?: HTMLElement;
|
||||
private contents?: HTMLElement;
|
||||
|
||||
private visible: boolean | undefined;
|
||||
private _onDidChangeVisibility = new Emitter<boolean>();
|
||||
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
|
||||
constructor(container: HTMLElement, options: IBaseDropdownOptions) {
|
||||
super();
|
||||
|
||||
this._element = append(container, $('.monaco-dropdown'));
|
||||
|
||||
this._label = append(this._element, $('.dropdown-label'));
|
||||
|
||||
let labelRenderer = options.labelRenderer;
|
||||
if (!labelRenderer) {
|
||||
labelRenderer = (container: HTMLElement): IDisposable | null => {
|
||||
container.textContent = options.label || '';
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap]) {
|
||||
this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
|
||||
}
|
||||
|
||||
for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {
|
||||
this._register(addDisposableListener(this._label, event, e => {
|
||||
if (e instanceof MouseEvent && e.detail > 1) {
|
||||
return; // prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363)
|
||||
}
|
||||
|
||||
if (this.visible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(addDisposableListener(this._label, EventType.KEY_UP, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
|
||||
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
|
||||
|
||||
if (this.visible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const cleanupFn = labelRenderer(this._label);
|
||||
if (cleanupFn) {
|
||||
this._register(cleanupFn);
|
||||
}
|
||||
|
||||
this._register(Gesture.addTarget(this._label));
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
set tooltip(tooltip: string) {
|
||||
if (this._label) {
|
||||
this._label.title = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (!this.visible) {
|
||||
this.visible = true;
|
||||
this._onDidChangeVisibility.fire(true);
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (this.visible) {
|
||||
this.visible = false;
|
||||
this._onDidChangeVisibility.fire(false);
|
||||
}
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return !!this.visible;
|
||||
}
|
||||
|
||||
protected onEvent(e: DOMEvent, activeElement: HTMLElement): void {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.hide();
|
||||
|
||||
if (this.boxContainer) {
|
||||
this.boxContainer.remove();
|
||||
this.boxContainer = undefined;
|
||||
}
|
||||
|
||||
if (this.contents) {
|
||||
this.contents.remove();
|
||||
this.contents = undefined;
|
||||
}
|
||||
|
||||
if (this._label) {
|
||||
this._label.remove();
|
||||
this._label = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDropdownOptions extends IBaseDropdownOptions {
|
||||
contextViewProvider: IContextViewProvider;
|
||||
}
|
||||
|
||||
export class Dropdown extends BaseDropdown {
|
||||
private contextViewProvider: IContextViewProvider;
|
||||
|
||||
constructor(container: HTMLElement, options: IDropdownOptions) {
|
||||
super(container, options);
|
||||
|
||||
this.contextViewProvider = options.contextViewProvider;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
super.show();
|
||||
|
||||
this.element.classList.add('active');
|
||||
|
||||
this.contextViewProvider.showContextView({
|
||||
getAnchor: () => this.getAnchor(),
|
||||
|
||||
render: (container) => {
|
||||
return this.renderContents(container);
|
||||
},
|
||||
|
||||
onDOMEvent: (e, activeElement) => {
|
||||
this.onEvent(e, activeElement);
|
||||
},
|
||||
|
||||
onHide: () => this.onHide()
|
||||
});
|
||||
}
|
||||
|
||||
protected getAnchor(): HTMLElement | IAnchor {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
protected onHide(): void {
|
||||
this.element.classList.remove('active');
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
super.hide();
|
||||
|
||||
if (this.contextViewProvider) {
|
||||
this.contextViewProvider.hideContextView();
|
||||
}
|
||||
}
|
||||
|
||||
protected renderContents(container: HTMLElement): IDisposable | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActionProvider {
|
||||
getActions(): IAction[];
|
||||
}
|
||||
|
||||
export interface IDropdownMenuOptions extends IBaseDropdownOptions {
|
||||
contextMenuProvider: IContextMenuProvider;
|
||||
readonly actions?: IAction[];
|
||||
readonly actionProvider?: IActionProvider;
|
||||
menuClassName?: string;
|
||||
menuAsChild?: boolean; // scope down for #99448
|
||||
}
|
||||
|
||||
export class DropdownMenu extends BaseDropdown {
|
||||
private _contextMenuProvider: IContextMenuProvider;
|
||||
private _menuOptions: IMenuOptions | undefined;
|
||||
private _actions: IAction[] = [];
|
||||
private actionProvider?: IActionProvider;
|
||||
private menuClassName: string;
|
||||
private menuAsChild?: boolean;
|
||||
|
||||
constructor(container: HTMLElement, options: IDropdownMenuOptions) {
|
||||
super(container, options);
|
||||
|
||||
this._contextMenuProvider = options.contextMenuProvider;
|
||||
this.actions = options.actions || [];
|
||||
this.actionProvider = options.actionProvider;
|
||||
this.menuClassName = options.menuClassName || '';
|
||||
this.menuAsChild = !!options.menuAsChild;
|
||||
}
|
||||
|
||||
set menuOptions(options: IMenuOptions | undefined) {
|
||||
this._menuOptions = options;
|
||||
}
|
||||
|
||||
get menuOptions(): IMenuOptions | undefined {
|
||||
return this._menuOptions;
|
||||
}
|
||||
|
||||
private get actions(): IAction[] {
|
||||
if (this.actionProvider) {
|
||||
return this.actionProvider.getActions();
|
||||
}
|
||||
|
||||
return this._actions;
|
||||
}
|
||||
|
||||
private set actions(actions: IAction[]) {
|
||||
this._actions = actions;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
super.show();
|
||||
|
||||
this.element.classList.add('active');
|
||||
|
||||
this._contextMenuProvider.showContextMenu({
|
||||
getAnchor: () => this.element,
|
||||
getActions: () => this.actions,
|
||||
getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,
|
||||
getActionViewItem: action => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action) : undefined,
|
||||
getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,
|
||||
getMenuClassName: () => this.menuClassName,
|
||||
onHide: () => this.onHide(),
|
||||
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,
|
||||
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT,
|
||||
domForShadowRoot: this.menuAsChild ? this.element : undefined
|
||||
});
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
super.hide();
|
||||
}
|
||||
|
||||
private onHide(): void {
|
||||
this.hide();
|
||||
this.element.classList.remove('active');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dropdown';
|
||||
import { Action, IAction, IActionRunner, IActionViewItemProvider } 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 { 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';
|
||||
|
||||
export interface IKeybindingProvider {
|
||||
(action: IAction): ResolvedKeybinding | undefined;
|
||||
}
|
||||
|
||||
export interface IAnchorAlignmentProvider {
|
||||
(): AnchorAlignment;
|
||||
}
|
||||
|
||||
export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemOptions {
|
||||
readonly actionViewItemProvider?: IActionViewItemProvider;
|
||||
readonly keybindingProvider?: IKeybindingProvider;
|
||||
readonly actionRunner?: IActionRunner;
|
||||
readonly classNames?: string[] | string;
|
||||
readonly anchorAlignmentProvider?: IAnchorAlignmentProvider;
|
||||
readonly menuAsChild?: boolean;
|
||||
}
|
||||
|
||||
export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
private menuActionsOrProvider: readonly IAction[] | IActionProvider;
|
||||
private dropdownMenu: DropdownMenu | undefined;
|
||||
private contextMenuProvider: IContextMenuProvider;
|
||||
|
||||
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
|
||||
constructor(
|
||||
action: IAction,
|
||||
menuActionsOrProvider: readonly IAction[] | IActionProvider,
|
||||
contextMenuProvider: IContextMenuProvider,
|
||||
protected options: IDropdownMenuActionViewItemOptions = {}
|
||||
) {
|
||||
super(null, action, options);
|
||||
|
||||
this.menuActionsOrProvider = menuActionsOrProvider;
|
||||
this.contextMenuProvider = contextMenuProvider;
|
||||
|
||||
if (this.options.actionRunner) {
|
||||
this.actionRunner = this.options.actionRunner;
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
|
||||
this.element = append(el, $('a.action-label'));
|
||||
|
||||
let classNames: string[] = [];
|
||||
|
||||
if (typeof this.options.classNames === 'string') {
|
||||
classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);
|
||||
} else if (this.options.classNames) {
|
||||
classNames = this.options.classNames;
|
||||
}
|
||||
|
||||
// todo@aeschli: remove codicon, should come through `this.options.classNames`
|
||||
if (!classNames.find(c => c === 'icon')) {
|
||||
classNames.push('codicon');
|
||||
}
|
||||
|
||||
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');
|
||||
this.element.title = this._action.label || '';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isActionsArray = Array.isArray(this.menuActionsOrProvider);
|
||||
const options: IDropdownMenuOptions = {
|
||||
contextMenuProvider: this.contextMenuProvider,
|
||||
labelRenderer: labelRenderer,
|
||||
menuAsChild: this.options.menuAsChild,
|
||||
actions: isActionsArray ? this.menuActionsOrProvider as IAction[] : undefined,
|
||||
actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider as IActionProvider
|
||||
};
|
||||
|
||||
this.dropdownMenu = this._register(new DropdownMenu(container, options));
|
||||
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
|
||||
this.element?.setAttribute('aria-expanded', `${visible}`);
|
||||
this._onDidChangeVisibility.fire(visible);
|
||||
}));
|
||||
|
||||
this.dropdownMenu.menuOptions = {
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
getKeyBinding: this.options.keybindingProvider,
|
||||
context: this._context
|
||||
};
|
||||
|
||||
if (this.options.anchorAlignmentProvider) {
|
||||
const that = this;
|
||||
|
||||
this.dropdownMenu.menuOptions = {
|
||||
...this.dropdownMenu.menuOptions,
|
||||
get anchorAlignment(): AnchorAlignment {
|
||||
return that.options.anchorAlignmentProvider!();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
super.setActionContext(newContext);
|
||||
|
||||
if (this.dropdownMenu) {
|
||||
if (this.dropdownMenu.menuOptions) {
|
||||
this.dropdownMenu.menuOptions.context = newContext;
|
||||
} else {
|
||||
this.dropdownMenu.menuOptions = { context: newContext };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (this.dropdownMenu) {
|
||||
this.dropdownMenu.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {
|
||||
readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;
|
||||
readonly menuActionClassNames?: string[];
|
||||
}
|
||||
|
||||
export class ActionWithDropdownActionViewItem extends ActionViewItem {
|
||||
|
||||
protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;
|
||||
|
||||
constructor(
|
||||
context: unknown,
|
||||
action: IAction,
|
||||
options: IActionWithDropdownActionViewItemOptions,
|
||||
private readonly contextMenuProvider: IContextMenuProvider
|
||||
) {
|
||||
super(context, action, options);
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
if (this.element) {
|
||||
this.element.classList.add('action-dropdown-item');
|
||||
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', 'codicon-chevron-down', ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
|
||||
this.dropdownMenuActionViewItem.render(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
lib/vscode/src/vs/base/browser/ui/findinput/findInput.css
Normal file
65
lib/vscode/src/vs/base/browser/ui/findinput/findInput.css
Normal file
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/* ---------- Find input ---------- */
|
||||
|
||||
.monaco-findInput {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-findInput .monaco-inputbox {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-findInput > .controls {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.vs .monaco-findInput.disabled {
|
||||
background-color: #E1E1E1;
|
||||
}
|
||||
|
||||
/* Theming */
|
||||
.vs-dark .monaco-findInput.disabled {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
/* Highlighting */
|
||||
.monaco-findInput.highlight-0 .controls {
|
||||
animation: monaco-findInput-highlight-0 100ms linear 0s;
|
||||
}
|
||||
.monaco-findInput.highlight-1 .controls {
|
||||
animation: monaco-findInput-highlight-1 100ms linear 0s;
|
||||
}
|
||||
.hc-black .monaco-findInput.highlight-0 .controls,
|
||||
.vs-dark .monaco-findInput.highlight-0 .controls {
|
||||
animation: monaco-findInput-highlight-dark-0 100ms linear 0s;
|
||||
}
|
||||
.hc-black .monaco-findInput.highlight-1 .controls,
|
||||
.vs-dark .monaco-findInput.highlight-1 .controls {
|
||||
animation: monaco-findInput-highlight-dark-1 100ms linear 0s;
|
||||
}
|
||||
|
||||
@keyframes monaco-findInput-highlight-0 {
|
||||
0% { background: rgba(253, 255, 0, 0.8); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
@keyframes monaco-findInput-highlight-1 {
|
||||
0% { background: rgba(253, 255, 0, 0.8); }
|
||||
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
|
||||
99% { background: transparent; }
|
||||
}
|
||||
|
||||
@keyframes monaco-findInput-highlight-dark-0 {
|
||||
0% { background: rgba(255, 255, 255, 0.44); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
@keyframes monaco-findInput-highlight-dark-1 {
|
||||
0% { background: rgba(255, 255, 255, 0.44); }
|
||||
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
|
||||
99% { background: transparent; }
|
||||
}
|
||||
422
lib/vscode/src/vs/base/browser/ui/findinput/findInput.ts
Normal file
422
lib/vscode/src/vs/base/browser/ui/findinput/findInput.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./findInput';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IMessage as InputBoxMessage, IInputValidator, IInputBoxStyles, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { CaseSensitiveCheckbox, WholeWordsCheckbox, RegexCheckbox } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
|
||||
|
||||
export interface IFindInputOptions extends IFindInputStyles {
|
||||
readonly placeholder?: string;
|
||||
readonly width?: number;
|
||||
readonly validation?: IInputValidator;
|
||||
readonly label: string;
|
||||
readonly flexibleHeight?: boolean;
|
||||
readonly flexibleWidth?: boolean;
|
||||
readonly flexibleMaxHeight?: number;
|
||||
|
||||
readonly appendCaseSensitiveLabel?: string;
|
||||
readonly appendWholeWordsLabel?: string;
|
||||
readonly appendRegexLabel?: string;
|
||||
readonly history?: string[];
|
||||
}
|
||||
|
||||
export interface IFindInputStyles extends IInputBoxStyles {
|
||||
inputActiveOptionBorder?: Color;
|
||||
inputActiveOptionForeground?: Color;
|
||||
inputActiveOptionBackground?: Color;
|
||||
}
|
||||
|
||||
const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input");
|
||||
|
||||
export class FindInput extends Widget {
|
||||
|
||||
static readonly OPTION_CHANGE: string = 'optionChange';
|
||||
|
||||
private contextViewProvider: IContextViewProvider;
|
||||
private placeholder: string;
|
||||
private validation?: IInputValidator;
|
||||
private label: string;
|
||||
private fixFocusOnOptionClickEnabled = true;
|
||||
|
||||
private inputActiveOptionBorder?: Color;
|
||||
private inputActiveOptionForeground?: Color;
|
||||
private inputActiveOptionBackground?: Color;
|
||||
private inputBackground?: Color;
|
||||
private inputForeground?: Color;
|
||||
private inputBorder?: Color;
|
||||
|
||||
private inputValidationInfoBorder?: Color;
|
||||
private inputValidationInfoBackground?: Color;
|
||||
private inputValidationInfoForeground?: Color;
|
||||
private inputValidationWarningBorder?: Color;
|
||||
private inputValidationWarningBackground?: Color;
|
||||
private inputValidationWarningForeground?: Color;
|
||||
private inputValidationErrorBorder?: Color;
|
||||
private inputValidationErrorBackground?: Color;
|
||||
private inputValidationErrorForeground?: Color;
|
||||
|
||||
private regex: RegexCheckbox;
|
||||
private wholeWords: WholeWordsCheckbox;
|
||||
private caseSensitive: CaseSensitiveCheckbox;
|
||||
public domNode: HTMLElement;
|
||||
public inputBox: HistoryInputBox;
|
||||
|
||||
private readonly _onDidOptionChange = this._register(new Emitter<boolean>());
|
||||
public readonly onDidOptionChange: Event<boolean /* via keyboard */> = this._onDidOptionChange.event;
|
||||
|
||||
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
|
||||
|
||||
private readonly _onMouseDown = this._register(new Emitter<IMouseEvent>());
|
||||
public readonly onMouseDown: Event<IMouseEvent> = this._onMouseDown.event;
|
||||
|
||||
private readonly _onInput = this._register(new Emitter<void>());
|
||||
public readonly onInput: Event<void> = this._onInput.event;
|
||||
|
||||
private readonly _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
|
||||
|
||||
private _onCaseSensitiveKeyDown = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onCaseSensitiveKeyDown: Event<IKeyboardEvent> = this._onCaseSensitiveKeyDown.event;
|
||||
|
||||
private _onRegexKeyDown = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onRegexKeyDown: Event<IKeyboardEvent> = this._onRegexKeyDown.event;
|
||||
|
||||
constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider, private readonly _showOptionButtons: boolean, options: IFindInputOptions) {
|
||||
super();
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.placeholder = options.placeholder || '';
|
||||
this.validation = options.validation;
|
||||
this.label = options.label || NLS_DEFAULT_LABEL;
|
||||
|
||||
this.inputActiveOptionBorder = options.inputActiveOptionBorder;
|
||||
this.inputActiveOptionForeground = options.inputActiveOptionForeground;
|
||||
this.inputActiveOptionBackground = options.inputActiveOptionBackground;
|
||||
this.inputBackground = options.inputBackground;
|
||||
this.inputForeground = options.inputForeground;
|
||||
this.inputBorder = options.inputBorder;
|
||||
|
||||
this.inputValidationInfoBorder = options.inputValidationInfoBorder;
|
||||
this.inputValidationInfoBackground = options.inputValidationInfoBackground;
|
||||
this.inputValidationInfoForeground = options.inputValidationInfoForeground;
|
||||
this.inputValidationWarningBorder = options.inputValidationWarningBorder;
|
||||
this.inputValidationWarningBackground = options.inputValidationWarningBackground;
|
||||
this.inputValidationWarningForeground = options.inputValidationWarningForeground;
|
||||
this.inputValidationErrorBorder = options.inputValidationErrorBorder;
|
||||
this.inputValidationErrorBackground = options.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = options.inputValidationErrorForeground;
|
||||
|
||||
const appendCaseSensitiveLabel = options.appendCaseSensitiveLabel || '';
|
||||
const appendWholeWordsLabel = options.appendWholeWordsLabel || '';
|
||||
const appendRegexLabel = options.appendRegexLabel || '';
|
||||
const history = options.history || [];
|
||||
const flexibleHeight = !!options.flexibleHeight;
|
||||
const flexibleWidth = !!options.flexibleWidth;
|
||||
const flexibleMaxHeight = options.flexibleMaxHeight;
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.classList.add('monaco-findInput');
|
||||
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
|
||||
placeholder: this.placeholder || '',
|
||||
ariaLabel: this.label || '',
|
||||
validationOptions: {
|
||||
validation: this.validation
|
||||
},
|
||||
inputBackground: this.inputBackground,
|
||||
inputForeground: this.inputForeground,
|
||||
inputBorder: this.inputBorder,
|
||||
inputValidationInfoBackground: this.inputValidationInfoBackground,
|
||||
inputValidationInfoForeground: this.inputValidationInfoForeground,
|
||||
inputValidationInfoBorder: this.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.inputValidationWarningBackground,
|
||||
inputValidationWarningForeground: this.inputValidationWarningForeground,
|
||||
inputValidationWarningBorder: this.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.inputValidationErrorBackground,
|
||||
inputValidationErrorForeground: this.inputValidationErrorForeground,
|
||||
inputValidationErrorBorder: this.inputValidationErrorBorder,
|
||||
history,
|
||||
flexibleHeight,
|
||||
flexibleWidth,
|
||||
flexibleMaxHeight
|
||||
}));
|
||||
|
||||
this.regex = this._register(new RegexCheckbox({
|
||||
appendTitle: appendRegexLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: this.inputActiveOptionBackground
|
||||
}));
|
||||
this._register(this.regex.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.regex.onKeyDown(e => {
|
||||
this._onRegexKeyDown.fire(e);
|
||||
}));
|
||||
|
||||
this.wholeWords = this._register(new WholeWordsCheckbox({
|
||||
appendTitle: appendWholeWordsLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: this.inputActiveOptionBackground
|
||||
}));
|
||||
this._register(this.wholeWords.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
|
||||
this.caseSensitive = this._register(new CaseSensitiveCheckbox({
|
||||
appendTitle: appendCaseSensitiveLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: this.inputActiveOptionBackground
|
||||
}));
|
||||
this._register(this.caseSensitive.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.caseSensitive.onKeyDown(e => {
|
||||
this._onCaseSensitiveKeyDown.fire(e);
|
||||
}));
|
||||
|
||||
if (this._showOptionButtons) {
|
||||
this.inputBox.paddingRight = this.caseSensitive.width() + this.wholeWords.width() + this.regex.width();
|
||||
}
|
||||
|
||||
// Arrow-Key support to navigate between options
|
||||
let indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
|
||||
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
|
||||
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) {
|
||||
let index = indexes.indexOf(<HTMLElement>document.activeElement);
|
||||
if (index >= 0) {
|
||||
let newIndex: number = -1;
|
||||
if (event.equals(KeyCode.RightArrow)) {
|
||||
newIndex = (index + 1) % indexes.length;
|
||||
} else if (event.equals(KeyCode.LeftArrow)) {
|
||||
if (index === 0) {
|
||||
newIndex = indexes.length - 1;
|
||||
} else {
|
||||
newIndex = index - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.equals(KeyCode.Escape)) {
|
||||
indexes[index].blur();
|
||||
this.inputBox.focus();
|
||||
} else if (newIndex >= 0) {
|
||||
indexes[newIndex].focus();
|
||||
}
|
||||
|
||||
dom.EventHelper.stop(event, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let controls = document.createElement('div');
|
||||
controls.className = 'controls';
|
||||
controls.style.display = this._showOptionButtons ? 'block' : 'none';
|
||||
controls.appendChild(this.caseSensitive.domNode);
|
||||
controls.appendChild(this.wholeWords.domNode);
|
||||
controls.appendChild(this.regex.domNode);
|
||||
|
||||
this.domNode.appendChild(controls);
|
||||
|
||||
if (parent) {
|
||||
parent.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
|
||||
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
|
||||
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
|
||||
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
this.domNode.classList.remove('disabled');
|
||||
this.inputBox.enable();
|
||||
this.regex.enable();
|
||||
this.wholeWords.enable();
|
||||
this.caseSensitive.enable();
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
this.domNode.classList.add('disabled');
|
||||
this.inputBox.disable();
|
||||
this.regex.disable();
|
||||
this.wholeWords.disable();
|
||||
this.caseSensitive.disable();
|
||||
}
|
||||
|
||||
public setFocusInputOnOptionClick(value: boolean): void {
|
||||
this.fixFocusOnOptionClickEnabled = value;
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.clearValidation();
|
||||
this.setValue('');
|
||||
this.focus();
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputBox.value;
|
||||
}
|
||||
|
||||
public setValue(value: string): void {
|
||||
if (this.inputBox.value !== value) {
|
||||
this.inputBox.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public onSearchSubmit(): void {
|
||||
this.inputBox.addToHistory();
|
||||
}
|
||||
|
||||
public style(styles: IFindInputStyles): void {
|
||||
this.inputActiveOptionBorder = styles.inputActiveOptionBorder;
|
||||
this.inputActiveOptionForeground = styles.inputActiveOptionForeground;
|
||||
this.inputActiveOptionBackground = styles.inputActiveOptionBackground;
|
||||
this.inputBackground = styles.inputBackground;
|
||||
this.inputForeground = styles.inputForeground;
|
||||
this.inputBorder = styles.inputBorder;
|
||||
|
||||
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
|
||||
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
|
||||
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
|
||||
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
|
||||
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
|
||||
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
|
||||
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
|
||||
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
if (this.domNode) {
|
||||
const checkBoxStyles: ICheckboxStyles = {
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: this.inputActiveOptionBackground,
|
||||
};
|
||||
this.regex.style(checkBoxStyles);
|
||||
this.wholeWords.style(checkBoxStyles);
|
||||
this.caseSensitive.style(checkBoxStyles);
|
||||
|
||||
const inputBoxStyles: IInputBoxStyles = {
|
||||
inputBackground: this.inputBackground,
|
||||
inputForeground: this.inputForeground,
|
||||
inputBorder: this.inputBorder,
|
||||
inputValidationInfoBackground: this.inputValidationInfoBackground,
|
||||
inputValidationInfoForeground: this.inputValidationInfoForeground,
|
||||
inputValidationInfoBorder: this.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.inputValidationWarningBackground,
|
||||
inputValidationWarningForeground: this.inputValidationWarningForeground,
|
||||
inputValidationWarningBorder: this.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.inputValidationErrorBackground,
|
||||
inputValidationErrorForeground: this.inputValidationErrorForeground,
|
||||
inputValidationErrorBorder: this.inputValidationErrorBorder
|
||||
};
|
||||
this.inputBox.style(inputBoxStyles);
|
||||
}
|
||||
}
|
||||
|
||||
public select(): void {
|
||||
this.inputBox.select();
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
|
||||
public getCaseSensitive(): boolean {
|
||||
return this.caseSensitive.checked;
|
||||
}
|
||||
|
||||
public setCaseSensitive(value: boolean): void {
|
||||
this.caseSensitive.checked = value;
|
||||
}
|
||||
|
||||
public getWholeWords(): boolean {
|
||||
return this.wholeWords.checked;
|
||||
}
|
||||
|
||||
public setWholeWords(value: boolean): void {
|
||||
this.wholeWords.checked = value;
|
||||
}
|
||||
|
||||
public getRegex(): boolean {
|
||||
return this.regex.checked;
|
||||
}
|
||||
|
||||
public setRegex(value: boolean): void {
|
||||
this.regex.checked = value;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
public focusOnCaseSensitive(): void {
|
||||
this.caseSensitive.focus();
|
||||
}
|
||||
|
||||
public focusOnRegex(): void {
|
||||
this.regex.focus();
|
||||
}
|
||||
|
||||
private _lastHighlightFindOptions: number = 0;
|
||||
public highlightFindOptions(): void {
|
||||
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
|
||||
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
|
||||
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
|
||||
}
|
||||
|
||||
public validate(): void {
|
||||
this.inputBox.validate();
|
||||
}
|
||||
|
||||
public showMessage(message: InputBoxMessage): void {
|
||||
this.inputBox.showMessage(message);
|
||||
}
|
||||
|
||||
public clearMessage(): void {
|
||||
this.inputBox.hideMessage();
|
||||
}
|
||||
|
||||
private clearValidation(): void {
|
||||
this.inputBox.hideMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export interface IFindInputCheckboxOpts {
|
||||
readonly appendTitle: string;
|
||||
readonly isChecked: boolean;
|
||||
readonly inputActiveOptionBorder?: Color;
|
||||
readonly inputActiveOptionForeground?: Color;
|
||||
readonly inputActiveOptionBackground?: Color;
|
||||
}
|
||||
|
||||
const NLS_CASE_SENSITIVE_CHECKBOX_LABEL = nls.localize('caseDescription', "Match Case");
|
||||
const NLS_WHOLE_WORD_CHECKBOX_LABEL = nls.localize('wordsDescription', "Match Whole Word");
|
||||
const NLS_REGEX_CHECKBOX_LABEL = nls.localize('regexDescription', "Use Regular Expression");
|
||||
|
||||
export class CaseSensitiveCheckbox extends Checkbox {
|
||||
constructor(opts: IFindInputCheckboxOpts) {
|
||||
super({
|
||||
icon: Codicon.caseSensitive,
|
||||
title: NLS_CASE_SENSITIVE_CHECKBOX_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WholeWordsCheckbox extends Checkbox {
|
||||
constructor(opts: IFindInputCheckboxOpts) {
|
||||
super({
|
||||
icon: Codicon.wholeWord,
|
||||
title: NLS_WHOLE_WORD_CHECKBOX_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RegexCheckbox extends Checkbox {
|
||||
constructor(opts: IFindInputCheckboxOpts) {
|
||||
super({
|
||||
icon: Codicon.regex,
|
||||
title: NLS_REGEX_CHECKBOX_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
388
lib/vscode/src/vs/base/browser/ui/findinput/replaceInput.ts
Normal file
388
lib/vscode/src/vs/base/browser/ui/findinput/replaceInput.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./findInput';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IMessage as InputBoxMessage, IInputValidator, IInputBoxStyles, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ICheckboxStyles, Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
|
||||
import { IFindInputCheckboxOpts } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export interface IReplaceInputOptions extends IReplaceInputStyles {
|
||||
readonly placeholder?: string;
|
||||
readonly width?: number;
|
||||
readonly validation?: IInputValidator;
|
||||
readonly label: string;
|
||||
readonly flexibleHeight?: boolean;
|
||||
readonly flexibleWidth?: boolean;
|
||||
readonly flexibleMaxHeight?: number;
|
||||
|
||||
readonly appendPreserveCaseLabel?: string;
|
||||
readonly history?: string[];
|
||||
}
|
||||
|
||||
export interface IReplaceInputStyles extends IInputBoxStyles {
|
||||
inputActiveOptionBorder?: Color;
|
||||
inputActiveOptionForeground?: Color;
|
||||
inputActiveOptionBackground?: Color;
|
||||
}
|
||||
|
||||
const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input");
|
||||
const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case");
|
||||
|
||||
export class PreserveCaseCheckbox extends Checkbox {
|
||||
constructor(opts: IFindInputCheckboxOpts) {
|
||||
super({
|
||||
// TODO: does this need its own icon?
|
||||
icon: Codicon.preserveCase,
|
||||
title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ReplaceInput extends Widget {
|
||||
|
||||
static readonly OPTION_CHANGE: string = 'optionChange';
|
||||
|
||||
private contextViewProvider: IContextViewProvider | undefined;
|
||||
private placeholder: string;
|
||||
private validation?: IInputValidator;
|
||||
private label: string;
|
||||
private fixFocusOnOptionClickEnabled = true;
|
||||
|
||||
private inputActiveOptionBorder?: Color;
|
||||
private inputActiveOptionForeground?: Color;
|
||||
private inputActiveOptionBackground?: Color;
|
||||
private inputBackground?: Color;
|
||||
private inputForeground?: Color;
|
||||
private inputBorder?: Color;
|
||||
|
||||
private inputValidationInfoBorder?: Color;
|
||||
private inputValidationInfoBackground?: Color;
|
||||
private inputValidationInfoForeground?: Color;
|
||||
private inputValidationWarningBorder?: Color;
|
||||
private inputValidationWarningBackground?: Color;
|
||||
private inputValidationWarningForeground?: Color;
|
||||
private inputValidationErrorBorder?: Color;
|
||||
private inputValidationErrorBackground?: Color;
|
||||
private inputValidationErrorForeground?: Color;
|
||||
|
||||
private preserveCase: PreserveCaseCheckbox;
|
||||
private cachedOptionsWidth: number = 0;
|
||||
public domNode: HTMLElement;
|
||||
public inputBox: HistoryInputBox;
|
||||
|
||||
private readonly _onDidOptionChange = this._register(new Emitter<boolean>());
|
||||
public readonly onDidOptionChange: Event<boolean /* via keyboard */> = this._onDidOptionChange.event;
|
||||
|
||||
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
|
||||
|
||||
private readonly _onMouseDown = this._register(new Emitter<IMouseEvent>());
|
||||
public readonly onMouseDown: Event<IMouseEvent> = this._onMouseDown.event;
|
||||
|
||||
private readonly _onInput = this._register(new Emitter<void>());
|
||||
public readonly onInput: Event<void> = this._onInput.event;
|
||||
|
||||
private readonly _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
|
||||
|
||||
private _onPreserveCaseKeyDown = this._register(new Emitter<IKeyboardEvent>());
|
||||
public readonly onPreserveCaseKeyDown: Event<IKeyboardEvent> = this._onPreserveCaseKeyDown.event;
|
||||
|
||||
constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, private readonly _showOptionButtons: boolean, options: IReplaceInputOptions) {
|
||||
super();
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.placeholder = options.placeholder || '';
|
||||
this.validation = options.validation;
|
||||
this.label = options.label || NLS_DEFAULT_LABEL;
|
||||
|
||||
this.inputActiveOptionBorder = options.inputActiveOptionBorder;
|
||||
this.inputActiveOptionForeground = options.inputActiveOptionForeground;
|
||||
this.inputActiveOptionBackground = options.inputActiveOptionBackground;
|
||||
this.inputBackground = options.inputBackground;
|
||||
this.inputForeground = options.inputForeground;
|
||||
this.inputBorder = options.inputBorder;
|
||||
|
||||
this.inputValidationInfoBorder = options.inputValidationInfoBorder;
|
||||
this.inputValidationInfoBackground = options.inputValidationInfoBackground;
|
||||
this.inputValidationInfoForeground = options.inputValidationInfoForeground;
|
||||
this.inputValidationWarningBorder = options.inputValidationWarningBorder;
|
||||
this.inputValidationWarningBackground = options.inputValidationWarningBackground;
|
||||
this.inputValidationWarningForeground = options.inputValidationWarningForeground;
|
||||
this.inputValidationErrorBorder = options.inputValidationErrorBorder;
|
||||
this.inputValidationErrorBackground = options.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = options.inputValidationErrorForeground;
|
||||
|
||||
const appendPreserveCaseLabel = options.appendPreserveCaseLabel || '';
|
||||
const history = options.history || [];
|
||||
const flexibleHeight = !!options.flexibleHeight;
|
||||
const flexibleWidth = !!options.flexibleWidth;
|
||||
const flexibleMaxHeight = options.flexibleMaxHeight;
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.classList.add('monaco-findInput');
|
||||
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
|
||||
ariaLabel: this.label || '',
|
||||
placeholder: this.placeholder || '',
|
||||
validationOptions: {
|
||||
validation: this.validation
|
||||
},
|
||||
inputBackground: this.inputBackground,
|
||||
inputForeground: this.inputForeground,
|
||||
inputBorder: this.inputBorder,
|
||||
inputValidationInfoBackground: this.inputValidationInfoBackground,
|
||||
inputValidationInfoForeground: this.inputValidationInfoForeground,
|
||||
inputValidationInfoBorder: this.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.inputValidationWarningBackground,
|
||||
inputValidationWarningForeground: this.inputValidationWarningForeground,
|
||||
inputValidationWarningBorder: this.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.inputValidationErrorBackground,
|
||||
inputValidationErrorForeground: this.inputValidationErrorForeground,
|
||||
inputValidationErrorBorder: this.inputValidationErrorBorder,
|
||||
history,
|
||||
flexibleHeight,
|
||||
flexibleWidth,
|
||||
flexibleMaxHeight
|
||||
}));
|
||||
|
||||
this.preserveCase = this._register(new PreserveCaseCheckbox({
|
||||
appendTitle: appendPreserveCaseLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: this.inputActiveOptionBackground,
|
||||
}));
|
||||
this._register(this.preserveCase.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.preserveCase.onKeyDown(e => {
|
||||
this._onPreserveCaseKeyDown.fire(e);
|
||||
}));
|
||||
|
||||
if (this._showOptionButtons) {
|
||||
this.cachedOptionsWidth = this.preserveCase.width();
|
||||
} else {
|
||||
this.cachedOptionsWidth = 0;
|
||||
}
|
||||
|
||||
// Arrow-Key support to navigate between options
|
||||
let indexes = [this.preserveCase.domNode];
|
||||
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
|
||||
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) {
|
||||
let index = indexes.indexOf(<HTMLElement>document.activeElement);
|
||||
if (index >= 0) {
|
||||
let newIndex: number = -1;
|
||||
if (event.equals(KeyCode.RightArrow)) {
|
||||
newIndex = (index + 1) % indexes.length;
|
||||
} else if (event.equals(KeyCode.LeftArrow)) {
|
||||
if (index === 0) {
|
||||
newIndex = indexes.length - 1;
|
||||
} else {
|
||||
newIndex = index - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.equals(KeyCode.Escape)) {
|
||||
indexes[index].blur();
|
||||
this.inputBox.focus();
|
||||
} else if (newIndex >= 0) {
|
||||
indexes[newIndex].focus();
|
||||
}
|
||||
|
||||
dom.EventHelper.stop(event, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let controls = document.createElement('div');
|
||||
controls.className = 'controls';
|
||||
controls.style.display = this._showOptionButtons ? 'block' : 'none';
|
||||
controls.appendChild(this.preserveCase.domNode);
|
||||
|
||||
this.domNode.appendChild(controls);
|
||||
|
||||
if (parent) {
|
||||
parent.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
|
||||
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
|
||||
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
|
||||
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
this.domNode.classList.remove('disabled');
|
||||
this.inputBox.enable();
|
||||
this.preserveCase.enable();
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
this.domNode.classList.add('disabled');
|
||||
this.inputBox.disable();
|
||||
this.preserveCase.disable();
|
||||
}
|
||||
|
||||
public setFocusInputOnOptionClick(value: boolean): void {
|
||||
this.fixFocusOnOptionClickEnabled = value;
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.clearValidation();
|
||||
this.setValue('');
|
||||
this.focus();
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputBox.value;
|
||||
}
|
||||
|
||||
public setValue(value: string): void {
|
||||
if (this.inputBox.value !== value) {
|
||||
this.inputBox.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public onSearchSubmit(): void {
|
||||
this.inputBox.addToHistory();
|
||||
}
|
||||
|
||||
public style(styles: IReplaceInputStyles): void {
|
||||
this.inputActiveOptionBorder = styles.inputActiveOptionBorder;
|
||||
this.inputActiveOptionForeground = styles.inputActiveOptionForeground;
|
||||
this.inputActiveOptionBackground = styles.inputActiveOptionBackground;
|
||||
this.inputBackground = styles.inputBackground;
|
||||
this.inputForeground = styles.inputForeground;
|
||||
this.inputBorder = styles.inputBorder;
|
||||
|
||||
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
|
||||
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
|
||||
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
|
||||
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
|
||||
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
|
||||
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
|
||||
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
|
||||
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
if (this.domNode) {
|
||||
const checkBoxStyles: ICheckboxStyles = {
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: this.inputActiveOptionBackground,
|
||||
};
|
||||
this.preserveCase.style(checkBoxStyles);
|
||||
|
||||
const inputBoxStyles: IInputBoxStyles = {
|
||||
inputBackground: this.inputBackground,
|
||||
inputForeground: this.inputForeground,
|
||||
inputBorder: this.inputBorder,
|
||||
inputValidationInfoBackground: this.inputValidationInfoBackground,
|
||||
inputValidationInfoForeground: this.inputValidationInfoForeground,
|
||||
inputValidationInfoBorder: this.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.inputValidationWarningBackground,
|
||||
inputValidationWarningForeground: this.inputValidationWarningForeground,
|
||||
inputValidationWarningBorder: this.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.inputValidationErrorBackground,
|
||||
inputValidationErrorForeground: this.inputValidationErrorForeground,
|
||||
inputValidationErrorBorder: this.inputValidationErrorBorder
|
||||
};
|
||||
this.inputBox.style(inputBoxStyles);
|
||||
}
|
||||
}
|
||||
|
||||
public select(): void {
|
||||
this.inputBox.select();
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
|
||||
public getPreserveCase(): boolean {
|
||||
return this.preserveCase.checked;
|
||||
}
|
||||
|
||||
public setPreserveCase(value: boolean): void {
|
||||
this.preserveCase.checked = value;
|
||||
}
|
||||
|
||||
public focusOnPreserve(): void {
|
||||
this.preserveCase.focus();
|
||||
}
|
||||
|
||||
private _lastHighlightFindOptions: number = 0;
|
||||
public highlightFindOptions(): void {
|
||||
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
|
||||
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
|
||||
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
|
||||
}
|
||||
|
||||
public validate(): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.validate();
|
||||
}
|
||||
}
|
||||
|
||||
public showMessage(message: InputBoxMessage): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.showMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public clearMessage(): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.hideMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private clearValidation(): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.hideMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public set width(newWidth: number) {
|
||||
this.inputBox.paddingRight = this.cachedOptionsWidth;
|
||||
this.inputBox.width = newWidth;
|
||||
this.domNode.style.width = newWidth + 'px';
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
682
lib/vscode/src/vs/base/browser/ui/grid/grid.ts
Normal file
682
lib/vscode/src/vs/base/browser/ui/grid/grid.ts
Normal file
@@ -0,0 +1,682 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./gridview';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { tail2 as tail, equals } from 'vs/base/common/arrays';
|
||||
import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, IGridViewOptions, IBoundarySashes } from './gridview';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export { Orientation, IViewSize, orthogonal, LayoutPriority } from './gridview';
|
||||
|
||||
export const enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
function oppositeDirection(direction: Direction): Direction {
|
||||
switch (direction) {
|
||||
case Direction.Up: return Direction.Down;
|
||||
case Direction.Down: return Direction.Up;
|
||||
case Direction.Left: return Direction.Right;
|
||||
case Direction.Right: return Direction.Left;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IView extends IGridViewView {
|
||||
readonly preferredHeight?: number;
|
||||
readonly preferredWidth?: number;
|
||||
}
|
||||
|
||||
export interface GridLeafNode<T extends IView> {
|
||||
readonly view: T;
|
||||
readonly box: Box;
|
||||
readonly cachedVisibleSize: number | undefined;
|
||||
}
|
||||
|
||||
export interface GridBranchNode<T extends IView> {
|
||||
readonly children: GridNode<T>[];
|
||||
readonly box: Box;
|
||||
}
|
||||
|
||||
export type GridNode<T extends IView> = GridLeafNode<T> | GridBranchNode<T>;
|
||||
|
||||
export function isGridBranchNode<T extends IView>(node: GridNode<T>): node is GridBranchNode<T> {
|
||||
return !!(node as any).children;
|
||||
}
|
||||
|
||||
function getGridNode<T extends IView>(node: GridNode<T>, location: number[]): GridNode<T> {
|
||||
if (location.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (!isGridBranchNode(node)) {
|
||||
throw new Error('Invalid location');
|
||||
}
|
||||
|
||||
const [index, ...rest] = location;
|
||||
return getGridNode(node.children[index], rest);
|
||||
}
|
||||
|
||||
interface Range {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
function intersects(one: Range, other: Range): boolean {
|
||||
return !(one.start >= other.end || other.start >= one.end);
|
||||
}
|
||||
|
||||
interface Boundary {
|
||||
readonly offset: number;
|
||||
readonly range: Range;
|
||||
}
|
||||
|
||||
function getBoxBoundary(box: Box, direction: Direction): Boundary {
|
||||
const orientation = getDirectionOrientation(direction);
|
||||
const offset = direction === Direction.Up ? box.top :
|
||||
direction === Direction.Right ? box.left + box.width :
|
||||
direction === Direction.Down ? box.top + box.height :
|
||||
box.left;
|
||||
|
||||
const range = {
|
||||
start: orientation === Orientation.HORIZONTAL ? box.top : box.left,
|
||||
end: orientation === Orientation.HORIZONTAL ? box.top + box.height : box.left + box.width
|
||||
};
|
||||
|
||||
return { offset, range };
|
||||
}
|
||||
|
||||
function findAdjacentBoxLeafNodes<T extends IView>(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): GridLeafNode<T>[] {
|
||||
const result: GridLeafNode<T>[] = [];
|
||||
|
||||
function _(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): void {
|
||||
if (isGridBranchNode(boxNode)) {
|
||||
for (const child of boxNode.children) {
|
||||
_(child, direction, boundary);
|
||||
}
|
||||
} else {
|
||||
const { offset, range } = getBoxBoundary(boxNode.box, direction);
|
||||
|
||||
if (offset === boundary.offset && intersects(range, boundary.range)) {
|
||||
result.push(boxNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_(boxNode, direction, boundary);
|
||||
return result;
|
||||
}
|
||||
|
||||
function getLocationOrientation(rootOrientation: Orientation, location: number[]): Orientation {
|
||||
return location.length % 2 === 0 ? orthogonal(rootOrientation) : rootOrientation;
|
||||
}
|
||||
|
||||
function getDirectionOrientation(direction: Direction): Orientation {
|
||||
return direction === Direction.Up || direction === Direction.Down ? Orientation.VERTICAL : Orientation.HORIZONTAL;
|
||||
}
|
||||
|
||||
export function getRelativeLocation(rootOrientation: Orientation, location: number[], direction: Direction): number[] {
|
||||
const orientation = getLocationOrientation(rootOrientation, location);
|
||||
const directionOrientation = getDirectionOrientation(direction);
|
||||
|
||||
if (orientation === directionOrientation) {
|
||||
let [rest, index] = tail(location);
|
||||
|
||||
if (direction === Direction.Right || direction === Direction.Down) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return [...rest, index];
|
||||
} else {
|
||||
const index = (direction === Direction.Right || direction === Direction.Down) ? 1 : 0;
|
||||
return [...location, index];
|
||||
}
|
||||
}
|
||||
|
||||
function indexInParent(element: HTMLElement): number {
|
||||
const parentElement = element.parentElement;
|
||||
|
||||
if (!parentElement) {
|
||||
throw new Error('Invalid grid element');
|
||||
}
|
||||
|
||||
let el = parentElement.firstElementChild;
|
||||
let index = 0;
|
||||
|
||||
while (el !== element && el !== parentElement.lastElementChild && el) {
|
||||
el = el.nextElementSibling;
|
||||
index++;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the grid location of a specific DOM element by traversing the parent
|
||||
* chain and finding each child index on the way.
|
||||
*
|
||||
* This will break as soon as DOM structures of the Splitview or Gridview change.
|
||||
*/
|
||||
function getGridLocation(element: HTMLElement): number[] {
|
||||
const parentElement = element.parentElement;
|
||||
|
||||
if (!parentElement) {
|
||||
throw new Error('Invalid grid element');
|
||||
}
|
||||
|
||||
if (/\bmonaco-grid-view\b/.test(parentElement.className)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const index = indexInParent(parentElement);
|
||||
const ancestor = parentElement.parentElement!.parentElement!.parentElement!;
|
||||
return [...getGridLocation(ancestor), index];
|
||||
}
|
||||
|
||||
export type DistributeSizing = { type: 'distribute' };
|
||||
export type SplitSizing = { type: 'split' };
|
||||
export type InvisibleSizing = { type: 'invisible', cachedVisibleSize: number };
|
||||
export type Sizing = DistributeSizing | SplitSizing | InvisibleSizing;
|
||||
|
||||
export namespace Sizing {
|
||||
export const Distribute: DistributeSizing = { type: 'distribute' };
|
||||
export const Split: SplitSizing = { type: 'split' };
|
||||
export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; }
|
||||
}
|
||||
|
||||
export interface IGridStyles extends IGridViewStyles { }
|
||||
|
||||
export interface IGridOptions extends IGridViewOptions {
|
||||
readonly firstViewVisibleCachedSize?: number;
|
||||
}
|
||||
|
||||
export class Grid<T extends IView = IView> extends Disposable {
|
||||
|
||||
protected gridview: GridView;
|
||||
private views = new Map<T, HTMLElement>();
|
||||
get orientation(): Orientation { return this.gridview.orientation; }
|
||||
set orientation(orientation: Orientation) { this.gridview.orientation = orientation; }
|
||||
|
||||
get width(): number { return this.gridview.width; }
|
||||
get height(): number { return this.gridview.height; }
|
||||
|
||||
get minimumWidth(): number { return this.gridview.minimumWidth; }
|
||||
get minimumHeight(): number { return this.gridview.minimumHeight; }
|
||||
get maximumWidth(): number { return this.gridview.maximumWidth; }
|
||||
get maximumHeight(): number { return this.gridview.maximumHeight; }
|
||||
get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; }
|
||||
|
||||
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
|
||||
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
|
||||
|
||||
get element(): HTMLElement { return this.gridview.element; }
|
||||
|
||||
private didLayout = false;
|
||||
|
||||
constructor(gridview: GridView, options?: IGridOptions);
|
||||
constructor(view: T, options?: IGridOptions);
|
||||
constructor(view: T | GridView, options: IGridOptions = {}) {
|
||||
super();
|
||||
|
||||
if (view instanceof GridView) {
|
||||
this.gridview = view;
|
||||
this.gridview.getViewMap(this.views);
|
||||
} else {
|
||||
this.gridview = new GridView(options);
|
||||
}
|
||||
this._register(this.gridview);
|
||||
|
||||
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
|
||||
|
||||
const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number'
|
||||
? GridViewSizing.Invisible(options.firstViewVisibleCachedSize)
|
||||
: 0;
|
||||
|
||||
if (!(view instanceof GridView)) {
|
||||
this._addView(view, size, [0]);
|
||||
}
|
||||
}
|
||||
|
||||
style(styles: IGridStyles): void {
|
||||
this.gridview.style(styles);
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
this.gridview.layout(width, height);
|
||||
this.didLayout = true;
|
||||
}
|
||||
|
||||
hasView(view: T): boolean {
|
||||
return this.views.has(view);
|
||||
}
|
||||
|
||||
addView(newView: T, size: number | Sizing, referenceView: T, direction: Direction): void {
|
||||
if (this.views.has(newView)) {
|
||||
throw new Error('Can\'t add same view twice');
|
||||
}
|
||||
|
||||
const orientation = getDirectionOrientation(direction);
|
||||
|
||||
if (this.views.size === 1 && this.orientation !== orientation) {
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
const referenceLocation = this.getViewLocation(referenceView);
|
||||
const location = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
|
||||
|
||||
let viewSize: number | GridViewSizing;
|
||||
|
||||
if (typeof size === 'number') {
|
||||
viewSize = size;
|
||||
} else if (size.type === 'split') {
|
||||
const [, index] = tail(referenceLocation);
|
||||
viewSize = GridViewSizing.Split(index);
|
||||
} else if (size.type === 'distribute') {
|
||||
viewSize = GridViewSizing.Distribute;
|
||||
} else {
|
||||
viewSize = size;
|
||||
}
|
||||
|
||||
this._addView(newView, viewSize, location);
|
||||
}
|
||||
|
||||
addViewAt(newView: T, size: number | DistributeSizing | InvisibleSizing, location: number[]): void {
|
||||
if (this.views.has(newView)) {
|
||||
throw new Error('Can\'t add same view twice');
|
||||
}
|
||||
|
||||
let viewSize: number | GridViewSizing;
|
||||
|
||||
if (typeof size === 'number') {
|
||||
viewSize = size;
|
||||
} else if (size.type === 'distribute') {
|
||||
viewSize = GridViewSizing.Distribute;
|
||||
} else {
|
||||
viewSize = size;
|
||||
}
|
||||
|
||||
this._addView(newView, viewSize, location);
|
||||
}
|
||||
|
||||
protected _addView(newView: T, size: number | GridViewSizing, location: number[]): void {
|
||||
this.views.set(newView, newView.element);
|
||||
this.gridview.addView(newView, size, location);
|
||||
}
|
||||
|
||||
removeView(view: T, sizing?: Sizing): void {
|
||||
if (this.views.size === 1) {
|
||||
throw new Error('Can\'t remove last view');
|
||||
}
|
||||
|
||||
const location = this.getViewLocation(view);
|
||||
this.gridview.removeView(location, (sizing && sizing.type === 'distribute') ? GridViewSizing.Distribute : undefined);
|
||||
this.views.delete(view);
|
||||
}
|
||||
|
||||
moveView(view: T, sizing: number | Sizing, referenceView: T, direction: Direction): void {
|
||||
const sourceLocation = this.getViewLocation(view);
|
||||
const [sourceParentLocation, from] = tail(sourceLocation);
|
||||
|
||||
const referenceLocation = this.getViewLocation(referenceView);
|
||||
const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
|
||||
const [targetParentLocation, to] = tail(targetLocation);
|
||||
|
||||
if (equals(sourceParentLocation, targetParentLocation)) {
|
||||
this.gridview.moveView(sourceParentLocation, from, to);
|
||||
} else {
|
||||
this.removeView(view, typeof sizing === 'number' ? undefined : sizing);
|
||||
this.addView(view, sizing, referenceView, direction);
|
||||
}
|
||||
}
|
||||
|
||||
moveViewTo(view: T, location: number[]): void {
|
||||
const sourceLocation = this.getViewLocation(view);
|
||||
const [sourceParentLocation, from] = tail(sourceLocation);
|
||||
const [targetParentLocation, to] = tail(location);
|
||||
|
||||
if (equals(sourceParentLocation, targetParentLocation)) {
|
||||
this.gridview.moveView(sourceParentLocation, from, to);
|
||||
} else {
|
||||
const size = this.getViewSize(view);
|
||||
const orientation = getLocationOrientation(this.gridview.orientation, sourceLocation);
|
||||
const cachedViewSize = this.getViewCachedVisibleSize(view);
|
||||
const sizing = typeof cachedViewSize === 'undefined'
|
||||
? (orientation === Orientation.HORIZONTAL ? size.width : size.height)
|
||||
: Sizing.Invisible(cachedViewSize);
|
||||
|
||||
this.removeView(view);
|
||||
this.addViewAt(view, sizing, location);
|
||||
}
|
||||
}
|
||||
|
||||
swapViews(from: T, to: T): void {
|
||||
const fromLocation = this.getViewLocation(from);
|
||||
const toLocation = this.getViewLocation(to);
|
||||
return this.gridview.swapViews(fromLocation, toLocation);
|
||||
}
|
||||
|
||||
resizeView(view: T, size: IViewSize): void {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.resizeView(location, size);
|
||||
}
|
||||
|
||||
getViewSize(view?: T): IViewSize {
|
||||
if (!view) {
|
||||
return this.gridview.getViewSize();
|
||||
}
|
||||
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.getViewSize(location);
|
||||
}
|
||||
|
||||
getViewCachedVisibleSize(view: T): number | undefined {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.getViewCachedVisibleSize(location);
|
||||
}
|
||||
|
||||
maximizeViewSize(view: T): void {
|
||||
const location = this.getViewLocation(view);
|
||||
this.gridview.maximizeViewSize(location);
|
||||
}
|
||||
|
||||
distributeViewSizes(): void {
|
||||
this.gridview.distributeViewSizes();
|
||||
}
|
||||
|
||||
isViewVisible(view: T): boolean {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.isViewVisible(location);
|
||||
}
|
||||
|
||||
setViewVisible(view: T, visible: boolean): void {
|
||||
const location = this.getViewLocation(view);
|
||||
this.gridview.setViewVisible(location, visible);
|
||||
}
|
||||
|
||||
getViews(): GridBranchNode<T> {
|
||||
return this.gridview.getView() as GridBranchNode<T>;
|
||||
}
|
||||
|
||||
getNeighborViews(view: T, direction: Direction, wrap: boolean = false): T[] {
|
||||
if (!this.didLayout) {
|
||||
throw new Error('Can\'t call getNeighborViews before first layout');
|
||||
}
|
||||
|
||||
const location = this.getViewLocation(view);
|
||||
const root = this.getViews();
|
||||
const node = getGridNode(root, location);
|
||||
let boundary = getBoxBoundary(node.box, direction);
|
||||
|
||||
if (wrap) {
|
||||
if (direction === Direction.Up && node.box.top === 0) {
|
||||
boundary = { offset: root.box.top + root.box.height, range: boundary.range };
|
||||
} else if (direction === Direction.Right && node.box.left + node.box.width === root.box.width) {
|
||||
boundary = { offset: 0, range: boundary.range };
|
||||
} else if (direction === Direction.Down && node.box.top + node.box.height === root.box.height) {
|
||||
boundary = { offset: 0, range: boundary.range };
|
||||
} else if (direction === Direction.Left && node.box.left === 0) {
|
||||
boundary = { offset: root.box.left + root.box.width, range: boundary.range };
|
||||
}
|
||||
}
|
||||
|
||||
return findAdjacentBoxLeafNodes(root, oppositeDirection(direction), boundary)
|
||||
.map(node => node.view);
|
||||
}
|
||||
|
||||
getViewLocation(view: T): number[] {
|
||||
const element = this.views.get(view);
|
||||
|
||||
if (!element) {
|
||||
throw new Error('View not found');
|
||||
}
|
||||
|
||||
return getGridLocation(element);
|
||||
}
|
||||
|
||||
private onDidSashReset(location: number[]): void {
|
||||
const resizeToPreferredSize = (location: number[]): boolean => {
|
||||
const node = this.gridview.getView(location) as GridNode<T>;
|
||||
|
||||
if (isGridBranchNode(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const direction = getLocationOrientation(this.orientation, location);
|
||||
const size = direction === Orientation.HORIZONTAL ? node.view.preferredWidth : node.view.preferredHeight;
|
||||
|
||||
if (typeof size !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const viewSize = direction === Orientation.HORIZONTAL ? { width: Math.round(size) } : { height: Math.round(size) };
|
||||
this.gridview.resizeView(location, viewSize);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (resizeToPreferredSize(location)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [parentLocation, index] = tail(location);
|
||||
|
||||
if (resizeToPreferredSize([...parentLocation, index + 1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gridview.distributeViewSizes(parentLocation);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISerializableView extends IView {
|
||||
toJSON(): object;
|
||||
}
|
||||
|
||||
export interface IViewDeserializer<T extends ISerializableView> {
|
||||
fromJSON(json: any): T;
|
||||
}
|
||||
|
||||
export interface ISerializedLeafNode {
|
||||
type: 'leaf';
|
||||
data: any;
|
||||
size: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface ISerializedBranchNode {
|
||||
type: 'branch';
|
||||
data: ISerializedNode[];
|
||||
size: number;
|
||||
}
|
||||
|
||||
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
|
||||
|
||||
export interface ISerializedGrid {
|
||||
root: ISerializedNode;
|
||||
orientation: Orientation;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
|
||||
|
||||
private static serializeNode<T extends ISerializableView>(node: GridNode<T>, orientation: Orientation): ISerializedNode {
|
||||
const size = orientation === Orientation.VERTICAL ? node.box.width : node.box.height;
|
||||
|
||||
if (!isGridBranchNode(node)) {
|
||||
if (typeof node.cachedVisibleSize === 'number') {
|
||||
return { type: 'leaf', data: node.view.toJSON(), size: node.cachedVisibleSize, visible: false };
|
||||
}
|
||||
|
||||
return { type: 'leaf', data: node.view.toJSON(), size };
|
||||
}
|
||||
|
||||
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.');
|
||||
} else if (typeof json.width !== 'number') {
|
||||
throw new Error('Invalid JSON: \'width\' property must be a number.');
|
||||
} else if (typeof json.height !== 'number') {
|
||||
throw new Error('Invalid JSON: \'height\' property must be a number.');
|
||||
}
|
||||
|
||||
const gridview = GridView.deserialize(json, deserializer, options);
|
||||
const result = new SerializableGrid<T>(gridview, options);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful information in order to proportionally restore view sizes
|
||||
* upon the very first layout call.
|
||||
*/
|
||||
private initialLayoutContext: boolean = true;
|
||||
|
||||
serialize(): ISerializedGrid {
|
||||
return {
|
||||
root: SerializableGrid.serializeNode(this.getViews(), this.orientation),
|
||||
orientation: this.orientation,
|
||||
width: this.width,
|
||||
height: this.height
|
||||
};
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
super.layout(width, height);
|
||||
|
||||
if (this.initialLayoutContext) {
|
||||
this.initialLayoutContext = false;
|
||||
this.gridview.trySet2x2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type GridNodeDescriptor = { size?: number, groups?: GridNodeDescriptor[] };
|
||||
export type GridDescriptor = { orientation: Orientation, groups?: GridNodeDescriptor[] };
|
||||
|
||||
export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor, rootNode: boolean): void {
|
||||
if (!rootNode && nodeDescriptor.groups && nodeDescriptor.groups.length <= 1) {
|
||||
nodeDescriptor.groups = undefined;
|
||||
}
|
||||
|
||||
if (!nodeDescriptor.groups) {
|
||||
return;
|
||||
}
|
||||
|
||||
let totalDefinedSize = 0;
|
||||
let totalDefinedSizeCount = 0;
|
||||
|
||||
for (const child of nodeDescriptor.groups) {
|
||||
sanitizeGridNodeDescriptor(child, false);
|
||||
|
||||
if (child.size) {
|
||||
totalDefinedSize += child.size;
|
||||
totalDefinedSizeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const totalUndefinedSize = totalDefinedSizeCount > 0 ? totalDefinedSize : 1;
|
||||
const totalUndefinedSizeCount = nodeDescriptor.groups.length - totalDefinedSizeCount;
|
||||
const eachUndefinedSize = totalUndefinedSize / totalUndefinedSizeCount;
|
||||
|
||||
for (const child of nodeDescriptor.groups) {
|
||||
if (!child.size) {
|
||||
child.size = eachUndefinedSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createSerializedNode(nodeDescriptor: GridNodeDescriptor): ISerializedNode {
|
||||
if (nodeDescriptor.groups) {
|
||||
return { type: 'branch', data: nodeDescriptor.groups.map(c => createSerializedNode(c)), size: nodeDescriptor.size! };
|
||||
} else {
|
||||
return { type: 'leaf', data: null, size: nodeDescriptor.size! };
|
||||
}
|
||||
}
|
||||
|
||||
function getDimensions(node: ISerializedNode, orientation: Orientation): { width?: number, height?: number } {
|
||||
if (node.type === 'branch') {
|
||||
const childrenDimensions = node.data.map(c => getDimensions(c, orthogonal(orientation)));
|
||||
|
||||
if (orientation === Orientation.VERTICAL) {
|
||||
const width = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.width || 0)));
|
||||
const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.height || 0), 0);
|
||||
return { width, height };
|
||||
} else {
|
||||
const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.width || 0), 0);
|
||||
const height = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.height || 0)));
|
||||
return { width, height };
|
||||
}
|
||||
} else {
|
||||
const width = orientation === Orientation.VERTICAL ? node.size : undefined;
|
||||
const height = orientation === Orientation.VERTICAL ? undefined : node.size;
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
|
||||
export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerializedGrid {
|
||||
sanitizeGridNodeDescriptor(gridDescriptor, true);
|
||||
|
||||
const root = createSerializedNode(gridDescriptor);
|
||||
const { width, height } = getDimensions(root, gridDescriptor.orientation);
|
||||
|
||||
return {
|
||||
root,
|
||||
orientation: gridDescriptor.orientation,
|
||||
width: width || 1,
|
||||
height: height || 1
|
||||
};
|
||||
}
|
||||
16
lib/vscode/src/vs/base/browser/ui/grid/gridview.css
Normal file
16
lib/vscode/src/vs/base/browser/ui/grid/gridview.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-grid-view {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-grid-branch-node {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
1281
lib/vscode/src/vs/base/browser/ui/grid/gridview.ts
Normal file
1281
lib/vscode/src/vs/base/browser/ui/grid/gridview.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
|
||||
export interface IHighlight {
|
||||
start: number;
|
||||
end: number;
|
||||
extraClasses?: string;
|
||||
}
|
||||
|
||||
export class HighlightedLabel {
|
||||
|
||||
private readonly domNode: HTMLElement;
|
||||
private text: string = '';
|
||||
private title: string = '';
|
||||
private highlights: IHighlight[] = [];
|
||||
private didEverRender: boolean = false;
|
||||
|
||||
constructor(container: HTMLElement, private supportCodicons: boolean) {
|
||||
this.domNode = document.createElement('span');
|
||||
this.domNode.className = 'monaco-highlighted-label';
|
||||
|
||||
container.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
set(text: string | undefined, highlights: IHighlight[] = [], title: string = '', escapeNewLines?: boolean) {
|
||||
if (!text) {
|
||||
text = '';
|
||||
}
|
||||
if (escapeNewLines) {
|
||||
// adjusts highlights inplace
|
||||
text = HighlightedLabel.escapeNewLines(text, highlights);
|
||||
}
|
||||
if (this.didEverRender && this.text === text && this.title === title && objects.equals(this.highlights, highlights)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.text = text;
|
||||
this.title = title;
|
||||
this.highlights = highlights;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
|
||||
const children: HTMLSpanElement[] = [];
|
||||
let pos = 0;
|
||||
|
||||
for (const highlight of this.highlights) {
|
||||
if (highlight.end === highlight.start) {
|
||||
continue;
|
||||
}
|
||||
if (pos < highlight.start) {
|
||||
const substring = this.text.substring(pos, highlight.start);
|
||||
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
|
||||
pos = highlight.end;
|
||||
}
|
||||
|
||||
const substring = this.text.substring(highlight.start, highlight.end);
|
||||
const element = dom.$('span.highlight', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]);
|
||||
if (highlight.extraClasses) {
|
||||
element.classList.add(highlight.extraClasses);
|
||||
}
|
||||
children.push(element);
|
||||
pos = highlight.end;
|
||||
}
|
||||
|
||||
if (pos < this.text.length) {
|
||||
const substring = this.text.substring(pos,);
|
||||
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
|
||||
}
|
||||
|
||||
dom.reset(this.domNode, ...children);
|
||||
if (this.title) {
|
||||
this.domNode.title = this.title;
|
||||
} else {
|
||||
this.domNode.removeAttribute('title');
|
||||
}
|
||||
this.didEverRender = true;
|
||||
}
|
||||
|
||||
static escapeNewLines(text: string, highlights: IHighlight[]): string {
|
||||
|
||||
let total = 0;
|
||||
let extra = 0;
|
||||
|
||||
return text.replace(/\r\n|\r|\n/g, (match, offset) => {
|
||||
extra = match === '\r\n' ? -1 : 0;
|
||||
offset += total;
|
||||
|
||||
for (const highlight of highlights) {
|
||||
if (highlight.end <= offset) {
|
||||
continue;
|
||||
}
|
||||
if (highlight.start >= offset) {
|
||||
highlight.start += extra;
|
||||
}
|
||||
if (highlight.end >= offset) {
|
||||
highlight.end += extra;
|
||||
}
|
||||
}
|
||||
|
||||
total += extra;
|
||||
return '\u23CE';
|
||||
});
|
||||
}
|
||||
}
|
||||
139
lib/vscode/src/vs/base/browser/ui/hover/hover.css
Normal file
139
lib/vscode/src/vs/base/browser/ui/hover/hover.css
Normal file
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-hover {
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
z-index: 50;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
box-sizing: initial;
|
||||
animation: fadein 100ms linear;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.monaco-hover.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) {
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.monaco-hover p,
|
||||
.monaco-hover .code,
|
||||
.monaco-hover ul {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.monaco-hover code {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
|
||||
.monaco-hover hr {
|
||||
margin-top: 4px;
|
||||
margin-bottom: -4px;
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.monaco-hover p:first-child,
|
||||
.monaco-hover .code:first-child,
|
||||
.monaco-hover ul:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-hover p:last-child,
|
||||
.monaco-hover .code:last-child,
|
||||
.monaco-hover ul:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* MarkupContent Layout */
|
||||
.monaco-hover ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.monaco-hover ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-hover li > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-hover li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-hover code {
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
.monaco-hover .monaco-tokenized-source {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar {
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions {
|
||||
display: flex;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions .action-container {
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions .action-container .action .icon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-hover .markdown-hover .hover-contents .codicon {
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link:before {
|
||||
content: '(';
|
||||
}
|
||||
.monaco-hover .hover-contents a.code-link:after {
|
||||
content: ')';
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link {
|
||||
color: inherit;
|
||||
}
|
||||
.monaco-hover .hover-contents a.code-link > span {
|
||||
text-decoration: underline;
|
||||
/** Hack to force underline to show **/
|
||||
border-bottom: 1px solid transparent;
|
||||
text-underline-position: under;
|
||||
}
|
||||
|
||||
/** Spans in markdown hovers need a margin-bottom to avoid looking cramped: https://github.com/microsoft/vscode/issues/101496 **/
|
||||
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents) span {
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
54
lib/vscode/src/vs/base/browser/ui/hover/hoverWidget.ts
Normal file
54
lib/vscode/src/vs/base/browser/ui/hover/hoverWidget.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./hover';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class HoverWidget extends Disposable {
|
||||
|
||||
public readonly containerDomNode: HTMLElement;
|
||||
public readonly contentsDomNode: HTMLElement;
|
||||
private readonly _scrollbar: DomScrollableElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.containerDomNode = document.createElement('div');
|
||||
this.containerDomNode.className = 'monaco-hover';
|
||||
this.containerDomNode.tabIndex = 0;
|
||||
this.containerDomNode.setAttribute('role', 'tooltip');
|
||||
|
||||
this.contentsDomNode = document.createElement('div');
|
||||
this.contentsDomNode.className = 'monaco-hover-content';
|
||||
|
||||
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {}));
|
||||
this.containerDomNode.appendChild(this._scrollbar.getDomNode());
|
||||
}
|
||||
|
||||
public onContentsChanged(): void {
|
||||
this._scrollbar.scanDomNode();
|
||||
}
|
||||
}
|
||||
|
||||
export function renderHoverAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }, keybindingLabel: string | null): IDisposable {
|
||||
const actionContainer = dom.append(parent, $('div.action-container'));
|
||||
const action = dom.append(actionContainer, $('a.action'));
|
||||
action.setAttribute('href', '#');
|
||||
action.setAttribute('role', 'button');
|
||||
if (actionOptions.iconClass) {
|
||||
dom.append(action, $(`span.icon.${actionOptions.iconClass}`));
|
||||
}
|
||||
const label = dom.append(action, $('span'));
|
||||
label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
|
||||
return dom.addDisposableListener(actionContainer, dom.EventType.CLICK, e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
actionOptions.run(actionContainer);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface IHoverDelegateTarget extends IDisposable {
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
x?: number;
|
||||
}
|
||||
|
||||
export interface IHoverDelegateOptions {
|
||||
text: IMarkdownString | string;
|
||||
target: IHoverDelegateTarget | HTMLElement;
|
||||
anchorPosition?: AnchorPosition;
|
||||
}
|
||||
|
||||
export interface IHoverDelegate {
|
||||
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
|
||||
}
|
||||
350
lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabel.ts
Normal file
350
lib/vscode/src/vs/base/browser/ui/iconLabel/iconLabel.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./iconlabel';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
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 { isString } from 'vs/base/common/types';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
|
||||
export interface IIconLabelCreationOptions {
|
||||
supportHighlights?: boolean;
|
||||
supportDescriptionHighlights?: boolean;
|
||||
supportCodicons?: boolean;
|
||||
hoverDelegate?: IHoverDelegate;
|
||||
}
|
||||
|
||||
export interface IIconLabelValueOptions {
|
||||
title?: string | IMarkdownString | Promise<IMarkdownString | string | undefined>;
|
||||
descriptionTitle?: string;
|
||||
hideIcon?: boolean;
|
||||
extraClasses?: string[];
|
||||
italic?: boolean;
|
||||
strikethrough?: boolean;
|
||||
matches?: IMatch[];
|
||||
labelEscapeNewLines?: boolean;
|
||||
descriptionMatches?: IMatch[];
|
||||
readonly separator?: string;
|
||||
readonly domId?: string;
|
||||
}
|
||||
|
||||
class FastLabelNode {
|
||||
private disposed: boolean | undefined;
|
||||
private _textContent: string | undefined;
|
||||
private _className: string | undefined;
|
||||
private _empty: boolean | undefined;
|
||||
|
||||
constructor(private _element: HTMLElement) {
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
set textContent(content: string) {
|
||||
if (this.disposed || content === this._textContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._textContent = content;
|
||||
this._element.textContent = content;
|
||||
}
|
||||
|
||||
set className(className: string) {
|
||||
if (this.disposed || className === this._className) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._className = className;
|
||||
this._element.className = className;
|
||||
}
|
||||
|
||||
set empty(empty: boolean) {
|
||||
if (this.disposed || empty === this._empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._empty = empty;
|
||||
this._element.style.marginLeft = empty ? '0' : '';
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
export class IconLabel extends Disposable {
|
||||
|
||||
private domNode: FastLabelNode;
|
||||
|
||||
private nameNode: Label | LabelWithHighlights;
|
||||
|
||||
private descriptionContainer: FastLabelNode;
|
||||
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
|
||||
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
|
||||
|
||||
private hoverDelegate: IHoverDelegate | undefined = undefined;
|
||||
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
|
||||
|
||||
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
|
||||
super();
|
||||
|
||||
this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));
|
||||
|
||||
const labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));
|
||||
|
||||
const nameContainer = dom.append(labelContainer, dom.$('span.monaco-icon-name-container'));
|
||||
this.descriptionContainer = this._register(new FastLabelNode(dom.append(labelContainer, dom.$('span.monaco-icon-description-container'))));
|
||||
|
||||
if (options?.supportHighlights) {
|
||||
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
|
||||
} else {
|
||||
this.nameNode = new Label(nameContainer);
|
||||
}
|
||||
|
||||
if (options?.supportDescriptionHighlights) {
|
||||
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons);
|
||||
} else {
|
||||
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
|
||||
}
|
||||
|
||||
if (options?.hoverDelegate) {
|
||||
this.hoverDelegate = options.hoverDelegate;
|
||||
}
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this.domNode.element;
|
||||
}
|
||||
|
||||
setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {
|
||||
const classes = ['monaco-icon-label'];
|
||||
if (options) {
|
||||
if (options.extraClasses) {
|
||||
classes.push(...options.extraClasses);
|
||||
}
|
||||
|
||||
if (options.italic) {
|
||||
classes.push('italic');
|
||||
}
|
||||
|
||||
if (options.strikethrough) {
|
||||
classes.push('strikethrough');
|
||||
}
|
||||
}
|
||||
|
||||
this.domNode.className = classes.join(' ');
|
||||
this.setupHover(this.domNode.element, options?.title);
|
||||
|
||||
this.nameNode.setLabel(label, options);
|
||||
|
||||
if (description || this.descriptionNode) {
|
||||
if (!this.descriptionNode) {
|
||||
this.descriptionNode = this.descriptionNodeFactory(); // description node is created lazily on demand
|
||||
}
|
||||
|
||||
if (this.descriptionNode instanceof HighlightedLabel) {
|
||||
this.descriptionNode.set(description || '', options ? options.descriptionMatches : undefined);
|
||||
this.setupHover(this.descriptionNode.element, options?.descriptionTitle);
|
||||
} else {
|
||||
this.descriptionNode.textContent = description || '';
|
||||
this.setupHover(this.descriptionNode.element, options?.descriptionTitle || '');
|
||||
this.descriptionNode.empty = !description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupHover(htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise<IMarkdownString | string | undefined> | undefined): void {
|
||||
const previousCustomHover = this.customHovers.get(htmlElement);
|
||||
if (previousCustomHover) {
|
||||
previousCustomHover.dispose();
|
||||
this.customHovers.delete(htmlElement);
|
||||
}
|
||||
|
||||
if (!tooltip) {
|
||||
htmlElement.removeAttribute('title');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hoverDelegate) {
|
||||
return this.setupNativeHover(htmlElement, tooltip);
|
||||
} else {
|
||||
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise<IMarkdownString | string | undefined> | undefined): void {
|
||||
htmlElement.removeAttribute('title');
|
||||
// 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;
|
||||
function mouseOver(this: HTMLElement, e: MouseEvent): any {
|
||||
let isHovering = true;
|
||||
function mouseMove(this: HTMLElement, e: MouseEvent): any {
|
||||
mouseX = e.x;
|
||||
}
|
||||
function mouseLeave(this: HTMLElement, e: MouseEvent): any {
|
||||
isHovering = false;
|
||||
}
|
||||
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeave.bind(htmlElement));
|
||||
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
|
||||
setTimeout(async () => {
|
||||
if (isHovering && tooltip) {
|
||||
// Re-use the already computed hover options if they exist.
|
||||
if (!hoverOptions) {
|
||||
const target: IHoverDelegateTarget = {
|
||||
targetElements: [this],
|
||||
dispose: () => { }
|
||||
};
|
||||
const resolvedTooltip = await tooltip;
|
||||
if (resolvedTooltip) {
|
||||
hoverOptions = {
|
||||
text: resolvedTooltip,
|
||||
target,
|
||||
anchorPosition: AnchorPosition.BELOW
|
||||
};
|
||||
}
|
||||
}
|
||||
if (hoverOptions) {
|
||||
if (mouseX !== undefined) {
|
||||
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
|
||||
}
|
||||
hoverDelegate.showHover(hoverOptions);
|
||||
}
|
||||
}
|
||||
mouseMoveDisposable.dispose();
|
||||
mouseLeaveDisposable.dispose();
|
||||
}, hoverDelay);
|
||||
}
|
||||
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
|
||||
this.customHovers.set(htmlElement, mouseOverDisposable);
|
||||
}
|
||||
|
||||
private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise<IMarkdownString | string | undefined> | undefined): void {
|
||||
htmlElement.title = isString(tooltip) ? tooltip : '';
|
||||
}
|
||||
}
|
||||
|
||||
class Label {
|
||||
|
||||
private label: string | string[] | undefined = undefined;
|
||||
private singleLabel: HTMLElement | undefined = undefined;
|
||||
private options: IIconLabelValueOptions | undefined;
|
||||
|
||||
constructor(private container: HTMLElement) { }
|
||||
|
||||
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
|
||||
if (this.label === label && equals(this.options, options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.label = label;
|
||||
this.options = options;
|
||||
|
||||
if (typeof label === 'string') {
|
||||
if (!this.singleLabel) {
|
||||
this.container.innerText = '';
|
||||
this.container.classList.remove('multiple');
|
||||
this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));
|
||||
}
|
||||
|
||||
this.singleLabel.textContent = label;
|
||||
} else {
|
||||
this.container.innerText = '';
|
||||
this.container.classList.add('multiple');
|
||||
this.singleLabel = undefined;
|
||||
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
const l = label[i];
|
||||
const id = options?.domId && `${options?.domId}_${i}`;
|
||||
|
||||
dom.append(this.container, dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));
|
||||
|
||||
if (i < label.length - 1) {
|
||||
dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function splitMatches(labels: string[], separator: string, matches: IMatch[] | undefined): IMatch[][] | undefined {
|
||||
if (!matches) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let labelStart = 0;
|
||||
|
||||
return labels.map(label => {
|
||||
const labelRange = { start: labelStart, end: labelStart + label.length };
|
||||
|
||||
const result = matches
|
||||
.map(match => Range.intersect(labelRange, match))
|
||||
.filter(range => !Range.isEmpty(range))
|
||||
.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));
|
||||
|
||||
labelStart = labelRange.end + separator.length;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
class LabelWithHighlights {
|
||||
|
||||
private label: string | string[] | undefined = undefined;
|
||||
private singleLabel: HighlightedLabel | undefined = undefined;
|
||||
private options: IIconLabelValueOptions | undefined;
|
||||
|
||||
constructor(private container: HTMLElement, private supportCodicons: boolean) { }
|
||||
|
||||
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
|
||||
if (this.label === label && equals(this.options, options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.label = label;
|
||||
this.options = options;
|
||||
|
||||
if (typeof label === 'string') {
|
||||
if (!this.singleLabel) {
|
||||
this.container.innerText = '';
|
||||
this.container.classList.remove('multiple');
|
||||
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons);
|
||||
}
|
||||
|
||||
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
|
||||
} else {
|
||||
this.container.innerText = '';
|
||||
this.container.classList.add('multiple');
|
||||
this.singleLabel = undefined;
|
||||
|
||||
const separator = options?.separator || '/';
|
||||
const matches = splitMatches(label, separator, options?.matches);
|
||||
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
const l = label[i];
|
||||
const m = matches ? matches[i] : undefined;
|
||||
const id = options?.domId && `${options?.domId}_${i}`;
|
||||
|
||||
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
|
||||
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons);
|
||||
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
|
||||
|
||||
if (i < label.length - 1) {
|
||||
dom.append(name, dom.$('span.label-separator', undefined, separator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
lib/vscode/src/vs/base/browser/ui/iconLabel/iconlabel.css
Normal file
90
lib/vscode/src/vs/base/browser/ui/iconLabel/iconlabel.css
Normal file
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* ---------- Icon label ---------- */
|
||||
|
||||
.monaco-icon-label {
|
||||
display: flex; /* required for icons support :before rule */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-icon-label::before {
|
||||
|
||||
/* svg icons rendered as background image */
|
||||
background-size: 16px;
|
||||
background-position: left center;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 6px;
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
line-height: inherit !important;
|
||||
display: inline-block;
|
||||
|
||||
/* fonts icons */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
vertical-align: top;
|
||||
|
||||
flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name {
|
||||
color: inherit;
|
||||
white-space: pre; /* enable to show labels that include multiple whitespaces */
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name > .label-separator {
|
||||
margin: 0 2px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
opacity: .7;
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.9em;
|
||||
white-space: pre; /* enable to show labels that include multiple whitespaces */
|
||||
}
|
||||
|
||||
.vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.italic > .monaco-icon-description-container > .label-description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-description-container > .label-description {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.monaco-icon-label::after {
|
||||
opacity: 0.75;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
padding: 0 16px 0 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* make sure selection color wins when a label is being selected */
|
||||
.monaco-list:focus .selected .monaco-icon-label, /* list */
|
||||
.monaco-list:focus .selected .monaco-icon-label::after
|
||||
{
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-list-row.focused.selected .label-description,
|
||||
.monaco-list-row.selected .label-description {
|
||||
opacity: .8;
|
||||
}
|
||||
112
lib/vscode/src/vs/base/browser/ui/inputbox/inputBox.css
Normal file
112
lib/vscode/src/vs/base/browser/ui/inputbox/inputBox.css
Normal file
@@ -0,0 +1,112 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-inputbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Customizable */
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.monaco-inputbox.idle {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > .input,
|
||||
.monaco-inputbox > .wrapper > .mirror {
|
||||
|
||||
/* Customizable */
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > .input {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
resize: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > input {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > textarea.input {
|
||||
display: block;
|
||||
-ms-overflow-style: none; /* IE 10+: hide scrollbars */
|
||||
scrollbar-width: none; /* Firefox: hide scrollbars */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
|
||||
display: none; /* Chrome + Safari: hide scrollbar */
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > textarea.input.empty {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .wrapper > .mirror {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
visibility: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Context view */
|
||||
|
||||
.monaco-inputbox-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.monaco-inputbox-container .monaco-inputbox-message {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.4em;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
min-height: 34px;
|
||||
margin-top: -1px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Action bar support */
|
||||
.monaco-inputbox .monaco-action-bar {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.monaco-inputbox .monaco-action-bar .action-item {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.monaco-inputbox .monaco-action-bar .action-item .codicon {
|
||||
background-repeat: no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
687
lib/vscode/src/vs/base/browser/ui/inputbox/inputBox.ts
Normal file
687
lib/vscode/src/vs/base/browser/ui/inputbox/inputBox.ts
Normal file
@@ -0,0 +1,687 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./inputBox';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer';
|
||||
import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer';
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IContextViewProvider, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { HistoryNavigator } from 'vs/base/common/history';
|
||||
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
|
||||
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export interface IInputOptions extends IInputBoxStyles {
|
||||
readonly placeholder?: string;
|
||||
readonly ariaLabel?: string;
|
||||
readonly type?: string;
|
||||
readonly validationOptions?: IInputValidationOptions;
|
||||
readonly flexibleHeight?: boolean;
|
||||
readonly flexibleWidth?: boolean;
|
||||
readonly flexibleMaxHeight?: number;
|
||||
readonly actions?: ReadonlyArray<IAction>;
|
||||
}
|
||||
|
||||
export interface IInputBoxStyles {
|
||||
readonly inputBackground?: Color;
|
||||
readonly inputForeground?: Color;
|
||||
readonly inputBorder?: Color;
|
||||
readonly inputValidationInfoBorder?: Color;
|
||||
readonly inputValidationInfoBackground?: Color;
|
||||
readonly inputValidationInfoForeground?: Color;
|
||||
readonly inputValidationWarningBorder?: Color;
|
||||
readonly inputValidationWarningBackground?: Color;
|
||||
readonly inputValidationWarningForeground?: Color;
|
||||
readonly inputValidationErrorBorder?: Color;
|
||||
readonly inputValidationErrorBackground?: Color;
|
||||
readonly inputValidationErrorForeground?: Color;
|
||||
}
|
||||
|
||||
export interface IInputValidator {
|
||||
(value: string): IMessage | null;
|
||||
}
|
||||
|
||||
export interface IMessage {
|
||||
readonly content: string;
|
||||
readonly formatContent?: boolean; // defaults to false
|
||||
readonly type?: MessageType;
|
||||
}
|
||||
|
||||
export interface IInputValidationOptions {
|
||||
validation?: IInputValidator;
|
||||
}
|
||||
|
||||
export const enum MessageType {
|
||||
INFO = 1,
|
||||
WARNING = 2,
|
||||
ERROR = 3
|
||||
}
|
||||
|
||||
export interface IRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const defaultOpts = {
|
||||
inputBackground: Color.fromHex('#3C3C3C'),
|
||||
inputForeground: Color.fromHex('#CCCCCC'),
|
||||
inputValidationInfoBorder: Color.fromHex('#55AAFF'),
|
||||
inputValidationInfoBackground: Color.fromHex('#063B49'),
|
||||
inputValidationWarningBorder: Color.fromHex('#B89500'),
|
||||
inputValidationWarningBackground: Color.fromHex('#352A05'),
|
||||
inputValidationErrorBorder: Color.fromHex('#BE1100'),
|
||||
inputValidationErrorBackground: Color.fromHex('#5A1D1D')
|
||||
};
|
||||
|
||||
export class InputBox extends Widget {
|
||||
private contextViewProvider?: IContextViewProvider;
|
||||
element: HTMLElement;
|
||||
private input: HTMLInputElement;
|
||||
private actionbar?: ActionBar;
|
||||
private options: IInputOptions;
|
||||
private message: IMessage | null;
|
||||
private placeholder: string;
|
||||
private ariaLabel: string;
|
||||
private validation?: IInputValidator;
|
||||
private state: 'idle' | 'open' | 'closed' = 'idle';
|
||||
|
||||
private mirror: HTMLElement | undefined;
|
||||
private cachedHeight: number | undefined;
|
||||
private cachedContentHeight: number | undefined;
|
||||
private maxHeight: number = Number.POSITIVE_INFINITY;
|
||||
private scrollableElement: ScrollableElement | undefined;
|
||||
|
||||
private inputBackground?: Color;
|
||||
private inputForeground?: Color;
|
||||
private inputBorder?: Color;
|
||||
|
||||
private inputValidationInfoBorder?: Color;
|
||||
private inputValidationInfoBackground?: Color;
|
||||
private inputValidationInfoForeground?: Color;
|
||||
private inputValidationWarningBorder?: Color;
|
||||
private inputValidationWarningBackground?: Color;
|
||||
private inputValidationWarningForeground?: Color;
|
||||
private inputValidationErrorBorder?: Color;
|
||||
private inputValidationErrorBackground?: Color;
|
||||
private inputValidationErrorForeground?: Color;
|
||||
|
||||
private _onDidChange = this._register(new Emitter<string>());
|
||||
public readonly onDidChange: Event<string> = this._onDidChange.event;
|
||||
|
||||
private _onDidHeightChange = this._register(new Emitter<number>());
|
||||
public readonly onDidHeightChange: Event<number> = this._onDidHeightChange.event;
|
||||
|
||||
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options?: IInputOptions) {
|
||||
super();
|
||||
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.options = options || Object.create(null);
|
||||
mixin(this.options, defaultOpts, false);
|
||||
this.message = null;
|
||||
this.placeholder = this.options.placeholder || '';
|
||||
this.ariaLabel = this.options.ariaLabel || '';
|
||||
|
||||
this.inputBackground = this.options.inputBackground;
|
||||
this.inputForeground = this.options.inputForeground;
|
||||
this.inputBorder = this.options.inputBorder;
|
||||
|
||||
this.inputValidationInfoBorder = this.options.inputValidationInfoBorder;
|
||||
this.inputValidationInfoBackground = this.options.inputValidationInfoBackground;
|
||||
this.inputValidationInfoForeground = this.options.inputValidationInfoForeground;
|
||||
this.inputValidationWarningBorder = this.options.inputValidationWarningBorder;
|
||||
this.inputValidationWarningBackground = this.options.inputValidationWarningBackground;
|
||||
this.inputValidationWarningForeground = this.options.inputValidationWarningForeground;
|
||||
this.inputValidationErrorBorder = this.options.inputValidationErrorBorder;
|
||||
this.inputValidationErrorBackground = this.options.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = this.options.inputValidationErrorForeground;
|
||||
|
||||
if (this.options.validationOptions) {
|
||||
this.validation = this.options.validationOptions.validation;
|
||||
}
|
||||
|
||||
this.element = dom.append(container, $('.monaco-inputbox.idle'));
|
||||
|
||||
let tagName = this.options.flexibleHeight ? 'textarea' : 'input';
|
||||
|
||||
let wrapper = dom.append(this.element, $('.wrapper'));
|
||||
this.input = dom.append(wrapper, $(tagName + '.input.empty'));
|
||||
this.input.setAttribute('autocorrect', 'off');
|
||||
this.input.setAttribute('autocapitalize', 'off');
|
||||
this.input.setAttribute('spellcheck', 'false');
|
||||
|
||||
this.onfocus(this.input, () => this.element.classList.add('synthetic-focus'));
|
||||
this.onblur(this.input, () => this.element.classList.remove('synthetic-focus'));
|
||||
|
||||
if (this.options.flexibleHeight) {
|
||||
this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
|
||||
|
||||
this.mirror = dom.append(wrapper, $('div.mirror'));
|
||||
this.mirror.innerText = '\u00a0';
|
||||
|
||||
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
|
||||
|
||||
if (this.options.flexibleWidth) {
|
||||
this.input.setAttribute('wrap', 'off');
|
||||
this.mirror.style.whiteSpace = 'pre';
|
||||
this.mirror.style.wordWrap = 'initial';
|
||||
}
|
||||
|
||||
dom.append(container, this.scrollableElement.getDomNode());
|
||||
this._register(this.scrollableElement);
|
||||
|
||||
// from ScrollableElement to DOM
|
||||
this._register(this.scrollableElement.onScroll(e => this.input.scrollTop = e.scrollTop));
|
||||
|
||||
const onSelectionChange = Event.filter(domEvent(document, 'selectionchange'), () => {
|
||||
const selection = document.getSelection();
|
||||
return selection?.anchorNode === wrapper;
|
||||
});
|
||||
|
||||
// from DOM to ScrollableElement
|
||||
this._register(onSelectionChange(this.updateScrollDimensions, this));
|
||||
this._register(this.onDidHeightChange(this.updateScrollDimensions, this));
|
||||
} else {
|
||||
this.input.type = this.options.type || 'text';
|
||||
this.input.setAttribute('wrap', 'off');
|
||||
}
|
||||
|
||||
if (this.ariaLabel) {
|
||||
this.input.setAttribute('aria-label', this.ariaLabel);
|
||||
}
|
||||
|
||||
if (this.placeholder) {
|
||||
this.setPlaceHolder(this.placeholder);
|
||||
}
|
||||
|
||||
this.oninput(this.input, () => this.onValueChange());
|
||||
this.onblur(this.input, () => this.onBlur());
|
||||
this.onfocus(this.input, () => this.onFocus());
|
||||
|
||||
this.ignoreGesture(this.input);
|
||||
|
||||
setTimeout(() => this.updateMirror(), 0);
|
||||
|
||||
// Support actions
|
||||
if (this.options.actions) {
|
||||
this.actionbar = this._register(new ActionBar(this.element));
|
||||
this.actionbar.push(this.options.actions, { icon: true, label: false });
|
||||
}
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private onBlur(): void {
|
||||
this._hideMessage();
|
||||
}
|
||||
|
||||
private onFocus(): void {
|
||||
this._showMessage();
|
||||
}
|
||||
|
||||
public setPlaceHolder(placeHolder: string): void {
|
||||
this.placeholder = placeHolder;
|
||||
this.input.setAttribute('placeholder', placeHolder);
|
||||
this.input.title = placeHolder;
|
||||
}
|
||||
|
||||
public setAriaLabel(label: string): void {
|
||||
this.ariaLabel = label;
|
||||
|
||||
if (label) {
|
||||
this.input.setAttribute('aria-label', this.ariaLabel);
|
||||
} else {
|
||||
this.input.removeAttribute('aria-label');
|
||||
}
|
||||
}
|
||||
|
||||
public getAriaLabel(): string {
|
||||
return this.ariaLabel;
|
||||
}
|
||||
|
||||
public get mirrorElement(): HTMLElement | undefined {
|
||||
return this.mirror;
|
||||
}
|
||||
|
||||
public get inputElement(): HTMLInputElement {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
public get value(): string {
|
||||
return this.input.value;
|
||||
}
|
||||
|
||||
public set value(newValue: string) {
|
||||
if (this.input.value !== newValue) {
|
||||
this.input.value = newValue;
|
||||
this.onValueChange();
|
||||
}
|
||||
}
|
||||
|
||||
public get height(): number {
|
||||
return typeof this.cachedHeight === 'number' ? this.cachedHeight : dom.getTotalHeight(this.element);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
public hasFocus(): boolean {
|
||||
return document.activeElement === this.input;
|
||||
}
|
||||
|
||||
public select(range: IRange | null = null): void {
|
||||
this.input.select();
|
||||
|
||||
if (range) {
|
||||
this.input.setSelectionRange(range.start, range.end);
|
||||
}
|
||||
}
|
||||
|
||||
public isSelectionAtEnd(): boolean {
|
||||
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
this.input.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
this.blur();
|
||||
this.input.disabled = true;
|
||||
this._hideMessage();
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
public get width(): number {
|
||||
return dom.getTotalWidth(this.input);
|
||||
}
|
||||
|
||||
public set width(width: number) {
|
||||
if (this.options.flexibleHeight && this.options.flexibleWidth) {
|
||||
// textarea with horizontal scrolling
|
||||
let horizontalPadding = 0;
|
||||
if (this.mirror) {
|
||||
const paddingLeft = parseFloat(this.mirror.style.paddingLeft || '') || 0;
|
||||
const paddingRight = parseFloat(this.mirror.style.paddingRight || '') || 0;
|
||||
horizontalPadding = paddingLeft + paddingRight;
|
||||
}
|
||||
this.input.style.width = (width - horizontalPadding) + 'px';
|
||||
} else {
|
||||
this.input.style.width = width + 'px';
|
||||
}
|
||||
|
||||
if (this.mirror) {
|
||||
this.mirror.style.width = width + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
public set paddingRight(paddingRight: number) {
|
||||
if (this.options.flexibleHeight && this.options.flexibleWidth) {
|
||||
this.input.style.width = `calc(100% - ${paddingRight}px)`;
|
||||
} else {
|
||||
this.input.style.paddingRight = paddingRight + 'px';
|
||||
}
|
||||
|
||||
if (this.mirror) {
|
||||
this.mirror.style.paddingRight = paddingRight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
private updateScrollDimensions(): void {
|
||||
if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number' || !this.scrollableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollHeight = this.cachedContentHeight;
|
||||
const height = this.cachedHeight;
|
||||
const scrollTop = this.input.scrollTop;
|
||||
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight, height });
|
||||
this.scrollableElement.setScrollPosition({ scrollTop });
|
||||
}
|
||||
|
||||
public showMessage(message: IMessage, force?: boolean): void {
|
||||
this.message = message;
|
||||
|
||||
this.element.classList.remove('idle');
|
||||
this.element.classList.remove('info');
|
||||
this.element.classList.remove('warning');
|
||||
this.element.classList.remove('error');
|
||||
this.element.classList.add(this.classForType(message.type));
|
||||
|
||||
const styles = this.stylesForType(this.message.type);
|
||||
this.element.style.border = styles.border ? `1px solid ${styles.border}` : '';
|
||||
|
||||
if (this.hasFocus() || force) {
|
||||
this._showMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public hideMessage(): void {
|
||||
this.message = null;
|
||||
|
||||
this.element.classList.remove('info');
|
||||
this.element.classList.remove('warning');
|
||||
this.element.classList.remove('error');
|
||||
this.element.classList.add('idle');
|
||||
|
||||
this._hideMessage();
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
public isInputValid(): boolean {
|
||||
return !!this.validation && !this.validation(this.value);
|
||||
}
|
||||
|
||||
public validate(): boolean {
|
||||
let errorMsg: IMessage | null = null;
|
||||
|
||||
if (this.validation) {
|
||||
errorMsg = this.validation(this.value);
|
||||
|
||||
if (errorMsg) {
|
||||
this.inputElement.setAttribute('aria-invalid', 'true');
|
||||
this.showMessage(errorMsg);
|
||||
}
|
||||
else if (this.inputElement.hasAttribute('aria-invalid')) {
|
||||
this.inputElement.removeAttribute('aria-invalid');
|
||||
this.hideMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return !errorMsg;
|
||||
}
|
||||
|
||||
public stylesForType(type: MessageType | undefined): { border: Color | undefined; background: Color | undefined; foreground: Color | undefined } {
|
||||
switch (type) {
|
||||
case MessageType.INFO: return { border: this.inputValidationInfoBorder, background: this.inputValidationInfoBackground, foreground: this.inputValidationInfoForeground };
|
||||
case MessageType.WARNING: return { border: this.inputValidationWarningBorder, background: this.inputValidationWarningBackground, foreground: this.inputValidationWarningForeground };
|
||||
default: return { border: this.inputValidationErrorBorder, background: this.inputValidationErrorBackground, foreground: this.inputValidationErrorForeground };
|
||||
}
|
||||
}
|
||||
|
||||
private classForType(type: MessageType | undefined): string {
|
||||
switch (type) {
|
||||
case MessageType.INFO: return 'info';
|
||||
case MessageType.WARNING: return 'warning';
|
||||
default: return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
private _showMessage(): void {
|
||||
if (!this.contextViewProvider || !this.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
let div: HTMLElement;
|
||||
let layout = () => div.style.width = dom.getTotalWidth(this.element) + 'px';
|
||||
|
||||
this.contextViewProvider.showContextView({
|
||||
getAnchor: () => this.element,
|
||||
anchorAlignment: AnchorAlignment.RIGHT,
|
||||
render: (container: HTMLElement) => {
|
||||
if (!this.message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
div = dom.append(container, $('.monaco-inputbox-container'));
|
||||
layout();
|
||||
|
||||
const renderOptions: MarkdownRenderOptions = {
|
||||
inline: true,
|
||||
className: 'monaco-inputbox-message'
|
||||
};
|
||||
|
||||
const spanElement = (this.message.formatContent
|
||||
? renderFormattedText(this.message.content, renderOptions)
|
||||
: renderText(this.message.content, renderOptions));
|
||||
spanElement.classList.add(this.classForType(this.message.type));
|
||||
|
||||
const styles = this.stylesForType(this.message.type);
|
||||
spanElement.style.backgroundColor = styles.background ? styles.background.toString() : '';
|
||||
spanElement.style.color = styles.foreground ? styles.foreground.toString() : '';
|
||||
spanElement.style.border = styles.border ? `1px solid ${styles.border}` : '';
|
||||
|
||||
dom.append(div, spanElement);
|
||||
|
||||
return null;
|
||||
},
|
||||
onHide: () => {
|
||||
this.state = 'closed';
|
||||
},
|
||||
layout: layout
|
||||
});
|
||||
|
||||
// ARIA Support
|
||||
let alertText: string;
|
||||
if (this.message.type === MessageType.ERROR) {
|
||||
alertText = nls.localize('alertErrorMessage', "Error: {0}", this.message.content);
|
||||
} else if (this.message.type === MessageType.WARNING) {
|
||||
alertText = nls.localize('alertWarningMessage', "Warning: {0}", this.message.content);
|
||||
} else {
|
||||
alertText = nls.localize('alertInfoMessage', "Info: {0}", this.message.content);
|
||||
}
|
||||
|
||||
aria.alert(alertText);
|
||||
|
||||
this.state = 'open';
|
||||
}
|
||||
|
||||
private _hideMessage(): void {
|
||||
if (!this.contextViewProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state === 'open') {
|
||||
this.contextViewProvider.hideContextView();
|
||||
}
|
||||
|
||||
this.state = 'idle';
|
||||
}
|
||||
|
||||
private onValueChange(): void {
|
||||
this._onDidChange.fire(this.value);
|
||||
|
||||
this.validate();
|
||||
this.updateMirror();
|
||||
this.input.classList.toggle('empty', !this.value);
|
||||
|
||||
if (this.state === 'open' && this.contextViewProvider) {
|
||||
this.contextViewProvider.layout();
|
||||
}
|
||||
}
|
||||
|
||||
private updateMirror(): void {
|
||||
if (!this.mirror) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.value;
|
||||
const lastCharCode = value.charCodeAt(value.length - 1);
|
||||
const suffix = lastCharCode === 10 ? ' ' : '';
|
||||
const mirrorTextContent = value + suffix;
|
||||
|
||||
if (mirrorTextContent) {
|
||||
this.mirror.textContent = value + suffix;
|
||||
} else {
|
||||
this.mirror.innerText = '\u00a0';
|
||||
}
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
public style(styles: IInputBoxStyles): void {
|
||||
this.inputBackground = styles.inputBackground;
|
||||
this.inputForeground = styles.inputForeground;
|
||||
this.inputBorder = styles.inputBorder;
|
||||
|
||||
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
|
||||
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
|
||||
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
|
||||
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
|
||||
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
|
||||
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
|
||||
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
|
||||
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
const background = this.inputBackground ? this.inputBackground.toString() : '';
|
||||
const foreground = this.inputForeground ? this.inputForeground.toString() : '';
|
||||
const border = this.inputBorder ? this.inputBorder.toString() : '';
|
||||
|
||||
this.element.style.backgroundColor = background;
|
||||
this.element.style.color = foreground;
|
||||
this.input.style.backgroundColor = 'inherit';
|
||||
this.input.style.color = foreground;
|
||||
|
||||
this.element.style.borderWidth = border ? '1px' : '';
|
||||
this.element.style.borderStyle = border ? 'solid' : '';
|
||||
this.element.style.borderColor = border;
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
if (!this.mirror) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousHeight = this.cachedContentHeight;
|
||||
this.cachedContentHeight = dom.getTotalHeight(this.mirror);
|
||||
|
||||
if (previousHeight !== this.cachedContentHeight) {
|
||||
this.cachedHeight = Math.min(this.cachedContentHeight, this.maxHeight);
|
||||
this.input.style.height = this.cachedHeight + 'px';
|
||||
this._onDidHeightChange.fire(this.cachedContentHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public insertAtCursor(text: string): void {
|
||||
const inputElement = this.inputElement;
|
||||
const start = inputElement.selectionStart;
|
||||
const end = inputElement.selectionEnd;
|
||||
const content = inputElement.value;
|
||||
|
||||
if (start !== null && end !== null) {
|
||||
this.value = content.substr(0, start) + text + content.substr(end);
|
||||
inputElement.setSelectionRange(start + 1, start + 1);
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._hideMessage();
|
||||
|
||||
this.message = null;
|
||||
|
||||
if (this.actionbar) {
|
||||
this.actionbar.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IHistoryInputOptions extends IInputOptions {
|
||||
history: string[];
|
||||
}
|
||||
|
||||
export class HistoryInputBox extends InputBox implements IHistoryNavigationWidget {
|
||||
|
||||
private readonly history: HistoryNavigator<string>;
|
||||
|
||||
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IHistoryInputOptions) {
|
||||
super(container, contextViewProvider, options);
|
||||
this.history = new HistoryNavigator<string>(options.history, 100);
|
||||
}
|
||||
|
||||
public addToHistory(): void {
|
||||
if (this.value && this.value !== this.getCurrentValue()) {
|
||||
this.history.add(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
public getHistory(): string[] {
|
||||
return this.history.getHistory();
|
||||
}
|
||||
|
||||
public showNextValue(): void {
|
||||
if (!this.history.has(this.value)) {
|
||||
this.addToHistory();
|
||||
}
|
||||
|
||||
let next = this.getNextValue();
|
||||
if (next) {
|
||||
next = next === this.value ? this.getNextValue() : next;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
this.value = next;
|
||||
aria.status(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
public showPreviousValue(): void {
|
||||
if (!this.history.has(this.value)) {
|
||||
this.addToHistory();
|
||||
}
|
||||
|
||||
let previous = this.getPreviousValue();
|
||||
if (previous) {
|
||||
previous = previous === this.value ? this.getPreviousValue() : previous;
|
||||
}
|
||||
|
||||
if (previous) {
|
||||
this.value = previous;
|
||||
aria.status(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
public clearHistory(): void {
|
||||
this.history.clear();
|
||||
}
|
||||
|
||||
private getCurrentValue(): string | null {
|
||||
let currentValue = this.history.current();
|
||||
if (!currentValue) {
|
||||
currentValue = this.history.last();
|
||||
this.history.next();
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
private getPreviousValue(): string | null {
|
||||
return this.history.previous() || this.history.first();
|
||||
}
|
||||
|
||||
private getNextValue(): string | null {
|
||||
return this.history.next() || this.history.last();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-keybinding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key:last-child {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./keybindingLabel';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
|
||||
import { UILabelProvider } from 'vs/base/common/keybindingLabels';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export interface PartMatches {
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
keyCode?: boolean;
|
||||
}
|
||||
|
||||
export interface Matches {
|
||||
firstPart: PartMatches;
|
||||
chordPart: PartMatches;
|
||||
}
|
||||
|
||||
export interface KeybindingLabelOptions {
|
||||
renderUnboundKeybindings: boolean;
|
||||
}
|
||||
|
||||
export class KeybindingLabel {
|
||||
|
||||
private domNode: HTMLElement;
|
||||
private keybinding: ResolvedKeybinding | undefined;
|
||||
private matches: Matches | undefined;
|
||||
private didEverRender: boolean;
|
||||
|
||||
constructor(container: HTMLElement, private os: OperatingSystem, private options?: KeybindingLabelOptions) {
|
||||
this.domNode = dom.append(container, $('.monaco-keybinding'));
|
||||
this.didEverRender = false;
|
||||
container.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
set(keybinding: ResolvedKeybinding | undefined, matches?: Matches) {
|
||||
if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keybinding = keybinding;
|
||||
this.matches = matches;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
dom.clearNode(this.domNode);
|
||||
|
||||
if (this.keybinding) {
|
||||
let [firstPart, chordPart] = this.keybinding.getParts();
|
||||
if (firstPart) {
|
||||
this.renderPart(this.domNode, firstPart, this.matches ? this.matches.firstPart : null);
|
||||
}
|
||||
if (chordPart) {
|
||||
dom.append(this.domNode, $('span.monaco-keybinding-key-chord-separator', undefined, ' '));
|
||||
this.renderPart(this.domNode, chordPart, this.matches ? this.matches.chordPart : null);
|
||||
}
|
||||
this.domNode.title = this.keybinding.getAriaLabel() || '';
|
||||
} else if (this.options && this.options.renderUnboundKeybindings) {
|
||||
this.renderUnbound(this.domNode);
|
||||
}
|
||||
|
||||
this.didEverRender = true;
|
||||
}
|
||||
|
||||
private renderPart(parent: HTMLElement, part: ResolvedKeybindingPart, match: PartMatches | null) {
|
||||
const modifierLabels = UILabelProvider.modifierLabels[this.os];
|
||||
if (part.ctrlKey) {
|
||||
this.renderKey(parent, modifierLabels.ctrlKey, Boolean(match?.ctrlKey), modifierLabels.separator);
|
||||
}
|
||||
if (part.shiftKey) {
|
||||
this.renderKey(parent, modifierLabels.shiftKey, Boolean(match?.shiftKey), modifierLabels.separator);
|
||||
}
|
||||
if (part.altKey) {
|
||||
this.renderKey(parent, modifierLabels.altKey, Boolean(match?.altKey), modifierLabels.separator);
|
||||
}
|
||||
if (part.metaKey) {
|
||||
this.renderKey(parent, modifierLabels.metaKey, Boolean(match?.metaKey), modifierLabels.separator);
|
||||
}
|
||||
const keyLabel = part.keyLabel;
|
||||
if (keyLabel) {
|
||||
this.renderKey(parent, keyLabel, Boolean(match?.keyCode), '');
|
||||
}
|
||||
}
|
||||
|
||||
private renderKey(parent: HTMLElement, label: string, highlight: boolean, separator: string): void {
|
||||
dom.append(parent, $('span.monaco-keybinding-key' + (highlight ? '.highlight' : ''), undefined, label));
|
||||
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")));
|
||||
}
|
||||
|
||||
private static areSame(a: Matches | undefined, b: Matches | undefined): boolean {
|
||||
if (a === b || (!a && !b)) {
|
||||
return true;
|
||||
}
|
||||
return !!a && !!b && equals(a.firstPart, b.firstPart) && equals(a.chordPart, b.chordPart);
|
||||
}
|
||||
}
|
||||
162
lib/vscode/src/vs/base/browser/ui/list/list.css
Normal file
162
lib/vscode/src/vs/base/browser/ui/list/list.css
Normal file
@@ -0,0 +1,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-list {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-list.mouse-support {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.monaco-list > .monaco-scrollable-element {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-list-rows {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-list.horizontal-scrolling .monaco-list-rows {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-list-row {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-list.mouse-support .monaco-list-row {
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* for OS X ballistic scrolling */
|
||||
.monaco-list-row.scrolling {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
.monaco-list.element-focused, .monaco-list.selection-single, .monaco-list.selection-multiple {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
.monaco-list:focus .monaco-list-row.selected .codicon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Dnd */
|
||||
.monaco-drag-image {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Type filter */
|
||||
|
||||
.monaco-list-type-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
border-radius: 2px;
|
||||
padding: 0px 3px;
|
||||
max-width: calc(100% - 10px);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
box-sizing: border-box;
|
||||
cursor: all-scroll;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
height: 20px;
|
||||
z-index: 1;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter.dragging {
|
||||
transition: top 0.2s, left 0.2s;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter.ne {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter.nw {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter > .controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
transition: width 0.2s;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter.dragging > .controls,
|
||||
.monaco-list-type-filter:hover > .controls {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter > .controls > * {
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter > .controls > .filter {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter-message {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 40px 1em 1em 1em;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter-message:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Electron */
|
||||
|
||||
.monaco-list-type-filter {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
128
lib/vscode/src/vs/base/browser/ui/list/list.ts
Normal file
128
lib/vscode/src/vs/base/browser/ui/list/list.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { GestureEvent } from 'vs/base/browser/touch';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
|
||||
export interface IListVirtualDelegate<T> {
|
||||
getHeight(element: T): number;
|
||||
getTemplateId(element: T): string;
|
||||
hasDynamicHeight?(element: T): boolean;
|
||||
setDynamicHeight?(element: T, height: number): void;
|
||||
}
|
||||
|
||||
export interface IListRenderer<T, TTemplateData> {
|
||||
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;
|
||||
disposeTemplate(templateData: TTemplateData): void;
|
||||
}
|
||||
|
||||
export interface IListEvent<T> {
|
||||
elements: T[];
|
||||
indexes: number[];
|
||||
browserEvent?: UIEvent;
|
||||
}
|
||||
|
||||
export interface IListMouseEvent<T> {
|
||||
browserEvent: MouseEvent;
|
||||
element: T | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
export interface IListTouchEvent<T> {
|
||||
browserEvent: TouchEvent;
|
||||
element: T | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
export interface IListGestureEvent<T> {
|
||||
browserEvent: GestureEvent;
|
||||
element: T | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
export interface IListDragEvent<T> {
|
||||
browserEvent: DragEvent;
|
||||
element: T | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
export interface IListContextMenuEvent<T> {
|
||||
browserEvent: UIEvent;
|
||||
element: T | undefined;
|
||||
index: number | undefined;
|
||||
anchor: HTMLElement | { x: number; y: number; };
|
||||
}
|
||||
|
||||
export interface IIdentityProvider<T> {
|
||||
getId(element: T): { toString(): string; };
|
||||
}
|
||||
|
||||
export interface IKeyboardNavigationLabelProvider<T> {
|
||||
|
||||
/**
|
||||
* Return a keyboard navigation label(s) which will be used by
|
||||
* the list for filtering/navigating. Return `undefined` to make
|
||||
* an element always match.
|
||||
*/
|
||||
getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined;
|
||||
}
|
||||
|
||||
export interface IKeyboardNavigationDelegate {
|
||||
mightProducePrintableCharacter(event: IKeyboardEvent): boolean;
|
||||
}
|
||||
|
||||
export const enum ListDragOverEffect {
|
||||
Copy,
|
||||
Move
|
||||
}
|
||||
|
||||
export interface IListDragOverReaction {
|
||||
accept: boolean;
|
||||
effect?: ListDragOverEffect;
|
||||
feedback?: number[]; // use -1 for entire list
|
||||
}
|
||||
|
||||
export const ListDragOverReactions = {
|
||||
reject(): IListDragOverReaction { return { accept: false }; },
|
||||
accept(): IListDragOverReaction { return { accept: true }; },
|
||||
};
|
||||
|
||||
export interface IListDragAndDrop<T> {
|
||||
getDragURI(element: T): string | null;
|
||||
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined;
|
||||
onDragStart?(data: IDragAndDropData, originalEvent: DragEvent): void;
|
||||
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
|
||||
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
|
||||
onDragEnd?(originalEvent: DragEvent): void;
|
||||
}
|
||||
|
||||
export class ListError extends Error {
|
||||
|
||||
constructor(user: string, message: string) {
|
||||
super(`ListError [${user}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class CachedListVirtualDelegate<T extends object> implements IListVirtualDelegate<T> {
|
||||
|
||||
private cache = new WeakMap<T, number>();
|
||||
|
||||
getHeight(element: T): number {
|
||||
return this.cache.get(element) ?? this.estimateHeight(element);
|
||||
}
|
||||
|
||||
protected abstract estimateHeight(element: T): number;
|
||||
abstract getTemplateId(element: T): string;
|
||||
|
||||
setDynamicHeight(element: T, height: number): void {
|
||||
if (height > 0) {
|
||||
this.cache.set(element, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
280
lib/vscode/src/vs/base/browser/ui/list/listPaging.ts
Normal file
280
lib/vscode/src/vs/base/browser/ui/list/listPaging.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./list';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent } from './list';
|
||||
import { List, IListStyles, IListOptions, IListAccessibilityProvider, IListOptionsUpdate } from './listWidget';
|
||||
import { IPagedModel } from 'vs/base/common/paging';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { IThemable } from 'vs/base/common/styler';
|
||||
|
||||
export interface IPagedRenderer<TElement, TTemplateData> extends IListRenderer<TElement, TTemplateData> {
|
||||
renderPlaceholder(index: number, templateData: TTemplateData): void;
|
||||
}
|
||||
|
||||
export interface ITemplateData<T> {
|
||||
data?: T;
|
||||
disposable?: IDisposable;
|
||||
}
|
||||
|
||||
class PagedRenderer<TElement, TTemplateData> implements IListRenderer<number, ITemplateData<TTemplateData>> {
|
||||
|
||||
get templateId(): string { return this.renderer.templateId; }
|
||||
|
||||
constructor(
|
||||
private renderer: IPagedRenderer<TElement, TTemplateData>,
|
||||
private modelProvider: () => IPagedModel<TElement>
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): ITemplateData<TTemplateData> {
|
||||
const data = this.renderer.renderTemplate(container);
|
||||
return { data, disposable: Disposable.None };
|
||||
}
|
||||
|
||||
renderElement(index: number, _: number, data: ITemplateData<TTemplateData>, height: number | undefined): void {
|
||||
if (data.disposable) {
|
||||
data.disposable.dispose();
|
||||
}
|
||||
|
||||
if (!data.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this.modelProvider();
|
||||
|
||||
if (model.isResolved(index)) {
|
||||
return this.renderer.renderElement(model.get(index), index, data.data, height);
|
||||
}
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = model.resolve(index, cts.token);
|
||||
data.disposable = { dispose: () => cts.cancel() };
|
||||
|
||||
this.renderer.renderPlaceholder(index, data.data);
|
||||
promise.then(entry => this.renderer.renderElement(entry, index, data.data!, height));
|
||||
}
|
||||
|
||||
disposeTemplate(data: ITemplateData<TTemplateData>): void {
|
||||
if (data.disposable) {
|
||||
data.disposable.dispose();
|
||||
data.disposable = undefined;
|
||||
}
|
||||
if (data.data) {
|
||||
this.renderer.disposeTemplate(data.data);
|
||||
data.data = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PagedAccessibilityProvider<T> implements IListAccessibilityProvider<number> {
|
||||
|
||||
constructor(
|
||||
private modelProvider: () => IPagedModel<T>,
|
||||
private accessibilityProvider: IListAccessibilityProvider<T>
|
||||
) { }
|
||||
|
||||
getWidgetAriaLabel(): string {
|
||||
return this.accessibilityProvider.getWidgetAriaLabel();
|
||||
}
|
||||
|
||||
getAriaLabel(index: number): string | null {
|
||||
const model = this.modelProvider();
|
||||
|
||||
if (!model.isResolved(index)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.accessibilityProvider.getAriaLabel(model.get(index));
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPagedListOptions<T> {
|
||||
readonly enableKeyboardNavigation?: boolean;
|
||||
readonly automaticKeyboardNavigation?: boolean;
|
||||
readonly ariaLabel?: string;
|
||||
readonly keyboardSupport?: boolean;
|
||||
readonly multipleSelectionSupport?: boolean;
|
||||
readonly accessibilityProvider?: IListAccessibilityProvider<T>;
|
||||
|
||||
// list view options
|
||||
readonly useShadows?: boolean;
|
||||
readonly verticalScrollMode?: ScrollbarVisibility;
|
||||
readonly setRowLineHeight?: boolean;
|
||||
readonly setRowHeight?: boolean;
|
||||
readonly supportDynamicHeights?: boolean;
|
||||
readonly mouseSupport?: boolean;
|
||||
readonly horizontalScrolling?: boolean;
|
||||
readonly additionalScrollHeight?: number;
|
||||
}
|
||||
|
||||
function fromPagedListOptions<T>(modelProvider: () => IPagedModel<T>, options: IPagedListOptions<T>): IListOptions<number> {
|
||||
return {
|
||||
...options,
|
||||
accessibilityProvider: options.accessibilityProvider && new PagedAccessibilityProvider(modelProvider, options.accessibilityProvider)
|
||||
};
|
||||
}
|
||||
|
||||
export class PagedList<T> implements IThemable, IDisposable {
|
||||
|
||||
private list: List<number>;
|
||||
private _model!: IPagedModel<T>;
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
container: HTMLElement,
|
||||
virtualDelegate: IListVirtualDelegate<number>,
|
||||
renderers: IPagedRenderer<T, any>[],
|
||||
options: IPagedListOptions<T> = {}
|
||||
) {
|
||||
const modelProvider = () => this.model;
|
||||
const pagedRenderers = renderers.map(r => new PagedRenderer<T, ITemplateData<T>>(r, modelProvider));
|
||||
this.list = new List(user, container, virtualDelegate, pagedRenderers, fromPagedListOptions(modelProvider, options));
|
||||
}
|
||||
|
||||
updateOptions(options: IListOptionsUpdate) {
|
||||
this.list.updateOptions(options);
|
||||
}
|
||||
|
||||
getHTMLElement(): HTMLElement {
|
||||
return this.list.getHTMLElement();
|
||||
}
|
||||
|
||||
isDOMFocused(): boolean {
|
||||
return this.list.getHTMLElement() === document.activeElement;
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.list.domFocus();
|
||||
}
|
||||
|
||||
get onDidFocus(): Event<void> {
|
||||
return this.list.onDidFocus;
|
||||
}
|
||||
|
||||
get onDidBlur(): Event<void> {
|
||||
return this.list.onDidBlur;
|
||||
}
|
||||
|
||||
get widget(): List<number> {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
get onDidDispose(): Event<void> {
|
||||
return this.list.onDidDispose;
|
||||
}
|
||||
|
||||
get onMouseClick(): Event<IListMouseEvent<T>> {
|
||||
return Event.map(this.list.onMouseClick, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
|
||||
}
|
||||
|
||||
get onMouseDblClick(): Event<IListMouseEvent<T>> {
|
||||
return Event.map(this.list.onMouseDblClick, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
|
||||
}
|
||||
|
||||
get onTap(): Event<IListMouseEvent<T>> {
|
||||
return Event.map(this.list.onTap, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
|
||||
}
|
||||
|
||||
get onPointer(): Event<IListMouseEvent<T>> {
|
||||
return Event.map(this.list.onPointer, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
|
||||
}
|
||||
|
||||
get onDidChangeFocus(): Event<IListEvent<T>> {
|
||||
return Event.map(this.list.onDidChangeFocus, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
|
||||
}
|
||||
|
||||
get onDidChangeSelection(): Event<IListEvent<T>> {
|
||||
return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
|
||||
}
|
||||
|
||||
get onContextMenu(): Event<IListContextMenuEvent<T>> {
|
||||
return Event.map(this.list.onContextMenu, ({ element, index, anchor, browserEvent }) => (typeof element === 'undefined' ? { element, index, anchor, browserEvent } : { element: this._model.get(element), index, anchor, browserEvent }));
|
||||
}
|
||||
|
||||
get model(): IPagedModel<T> {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
set model(model: IPagedModel<T>) {
|
||||
this._model = model;
|
||||
this.list.splice(0, this.list.length, range(model.length));
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setFocus(indexes: number[]): void {
|
||||
this.list.setFocus(indexes);
|
||||
}
|
||||
|
||||
focusNext(n?: number, loop?: boolean): void {
|
||||
this.list.focusNext(n, loop);
|
||||
}
|
||||
|
||||
focusPrevious(n?: number, loop?: boolean): void {
|
||||
this.list.focusPrevious(n, loop);
|
||||
}
|
||||
|
||||
focusNextPage(): void {
|
||||
this.list.focusNextPage();
|
||||
}
|
||||
|
||||
focusPreviousPage(): void {
|
||||
this.list.focusPreviousPage();
|
||||
}
|
||||
|
||||
getFocus(): number[] {
|
||||
return this.list.getFocus();
|
||||
}
|
||||
|
||||
setSelection(indexes: number[], browserEvent?: UIEvent): void {
|
||||
this.list.setSelection(indexes, browserEvent);
|
||||
}
|
||||
|
||||
getSelection(): number[] {
|
||||
return this.list.getSelection();
|
||||
}
|
||||
|
||||
layout(height?: number, width?: number): void {
|
||||
this.list.layout(height, width);
|
||||
}
|
||||
|
||||
toggleKeyboardNavigation(): void {
|
||||
this.list.toggleKeyboardNavigation();
|
||||
}
|
||||
|
||||
reveal(index: number, relativeTop?: number): void {
|
||||
this.list.reveal(index, relativeTop);
|
||||
}
|
||||
|
||||
style(styles: IListStyles): void {
|
||||
this.list.style(styles);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.list.dispose();
|
||||
}
|
||||
}
|
||||
1340
lib/vscode/src/vs/base/browser/ui/list/listView.ts
Normal file
1340
lib/vscode/src/vs/base/browser/ui/list/listView.ts
Normal file
File diff suppressed because it is too large
Load Diff
1670
lib/vscode/src/vs/base/browser/ui/list/listWidget.ts
Normal file
1670
lib/vscode/src/vs/base/browser/ui/list/listWidget.ts
Normal file
File diff suppressed because it is too large
Load Diff
189
lib/vscode/src/vs/base/browser/ui/list/rangeMap.ts
Normal file
189
lib/vscode/src/vs/base/browser/ui/list/rangeMap.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRange, Range } from 'vs/base/common/range';
|
||||
|
||||
export interface IItem {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface IRangedGroup {
|
||||
range: IRange;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intersection between a ranged group and a range.
|
||||
* Returns `[]` if the intersection is empty.
|
||||
*/
|
||||
export function groupIntersect(range: IRange, groups: IRangedGroup[]): IRangedGroup[] {
|
||||
const result: IRangedGroup[] = [];
|
||||
|
||||
for (let r of groups) {
|
||||
if (range.start >= r.range.end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (range.end < r.range.start) {
|
||||
break;
|
||||
}
|
||||
|
||||
const intersection = Range.intersect(range, r.range);
|
||||
|
||||
if (Range.isEmpty(intersection)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
range: intersection,
|
||||
size: r.size
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts a range by that `much`.
|
||||
*/
|
||||
export function shift({ start, end }: IRange, much: number): IRange {
|
||||
return { start: start + much, end: end + much };
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidates a collection of ranged groups.
|
||||
*
|
||||
* Consolidation is the process of merging consecutive ranged groups
|
||||
* that share the same `size`.
|
||||
*/
|
||||
export function consolidate(groups: IRangedGroup[]): IRangedGroup[] {
|
||||
const result: IRangedGroup[] = [];
|
||||
let previousGroup: IRangedGroup | null = null;
|
||||
|
||||
for (let group of groups) {
|
||||
const start = group.range.start;
|
||||
const end = group.range.end;
|
||||
const size = group.size;
|
||||
|
||||
if (previousGroup && size === previousGroup.size) {
|
||||
previousGroup.range.end = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
previousGroup = { range: { start, end }, size };
|
||||
result.push(previousGroup);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates several collections of ranged groups into a single
|
||||
* collection.
|
||||
*/
|
||||
function concat(...groups: IRangedGroup[][]): IRangedGroup[] {
|
||||
return consolidate(groups.reduce((r, g) => r.concat(g), []));
|
||||
}
|
||||
|
||||
export class RangeMap {
|
||||
|
||||
private groups: IRangedGroup[] = [];
|
||||
private _size = 0;
|
||||
|
||||
splice(index: number, deleteCount: number, items: IItem[] = []): void {
|
||||
const diff = items.length - deleteCount;
|
||||
const before = groupIntersect({ start: 0, end: index }, this.groups);
|
||||
const after = groupIntersect({ start: index + deleteCount, end: Number.POSITIVE_INFINITY }, this.groups)
|
||||
.map<IRangedGroup>(g => ({ range: shift(g.range, diff), size: g.size }));
|
||||
|
||||
const middle = items.map<IRangedGroup>((item, i) => ({
|
||||
range: { start: index + i, end: index + i + 1 },
|
||||
size: item.size
|
||||
}));
|
||||
|
||||
this.groups = concat(before, middle, after);
|
||||
this._size = this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of items in the range map.
|
||||
*/
|
||||
get count(): number {
|
||||
const len = this.groups.length;
|
||||
|
||||
if (!len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.groups[len - 1].range.end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of the sizes of all items in the range map.
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the item at the given position.
|
||||
*/
|
||||
indexAt(position: number): number {
|
||||
if (position < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
let size = 0;
|
||||
|
||||
for (let group of this.groups) {
|
||||
const count = group.range.end - group.range.start;
|
||||
const newSize = size + (count * group.size);
|
||||
|
||||
if (position < newSize) {
|
||||
return index + Math.floor((position - size) / group.size);
|
||||
}
|
||||
|
||||
index += count;
|
||||
size = newSize;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the item right after the item at the
|
||||
* index of the given position.
|
||||
*/
|
||||
indexAfter(position: number): number {
|
||||
return Math.min(this.indexAt(position) + 1, this.count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start position of the item at the given index.
|
||||
*/
|
||||
positionAt(index: number): number {
|
||||
if (index < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let position = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let group of this.groups) {
|
||||
const groupCount = group.range.end - group.range.start;
|
||||
const newCount = count + groupCount;
|
||||
|
||||
if (index < newCount) {
|
||||
return position + ((index - count) * group.size);
|
||||
}
|
||||
|
||||
position += groupCount * group.size;
|
||||
count = newCount;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
102
lib/vscode/src/vs/base/browser/ui/list/rowCache.ts
Normal file
102
lib/vscode/src/vs/base/browser/ui/list/rowCache.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IListRenderer } from './list';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
|
||||
export interface IRow {
|
||||
domNode: HTMLElement | null;
|
||||
templateId: string;
|
||||
templateData: any;
|
||||
}
|
||||
|
||||
function removeFromParent(element: HTMLElement): void {
|
||||
try {
|
||||
if (element.parentElement) {
|
||||
element.parentElement.removeChild(element);
|
||||
}
|
||||
} catch (e) {
|
||||
// this will throw if this happens due to a blur event, nasty business
|
||||
}
|
||||
}
|
||||
|
||||
export class RowCache<T> implements IDisposable {
|
||||
|
||||
private cache = new Map<string, IRow[]>();
|
||||
|
||||
constructor(private renderers: Map<string, IListRenderer<T, any>>) { }
|
||||
|
||||
/**
|
||||
* Returns a row either by creating a new one or reusing
|
||||
* a previously released row which shares the same templateId.
|
||||
*/
|
||||
alloc(templateId: string): IRow {
|
||||
let result = this.getTemplateCache(templateId).pop();
|
||||
|
||||
if (!result) {
|
||||
const domNode = $('.monaco-list-row');
|
||||
const renderer = this.getRenderer(templateId);
|
||||
const templateData = renderer.renderTemplate(domNode);
|
||||
result = { domNode, templateId, templateData };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the row for eventual reuse.
|
||||
*/
|
||||
release(row: IRow): void {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.releaseRow(row);
|
||||
}
|
||||
|
||||
private releaseRow(row: IRow): void {
|
||||
const { domNode, templateId } = row;
|
||||
if (domNode) {
|
||||
domNode.classList.remove('scrolling');
|
||||
removeFromParent(domNode);
|
||||
}
|
||||
|
||||
const cache = this.getTemplateCache(templateId);
|
||||
cache.push(row);
|
||||
}
|
||||
|
||||
private getTemplateCache(templateId: string): IRow[] {
|
||||
let result = this.cache.get(templateId);
|
||||
|
||||
if (!result) {
|
||||
result = [];
|
||||
this.cache.set(templateId, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cache.forEach((cachedRows, templateId) => {
|
||||
for (const cachedRow of cachedRows) {
|
||||
const renderer = this.getRenderer(templateId);
|
||||
renderer.disposeTemplate(cachedRow.templateData);
|
||||
cachedRow.domNode = null;
|
||||
cachedRow.templateData = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
private getRenderer(templateId: string): IListRenderer<T, any> {
|
||||
const renderer = this.renderers.get(templateId);
|
||||
if (!renderer) {
|
||||
throw new Error(`No renderer found for ${templateId}`);
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
19
lib/vscode/src/vs/base/browser/ui/list/splice.ts
Normal file
19
lib/vscode/src/vs/base/browser/ui/list/splice.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
|
||||
export interface ISpreadSpliceable<T> {
|
||||
splice(start: number, deleteCount: number, ...elements: T[]): void;
|
||||
}
|
||||
|
||||
export class CombinedSpliceable<T> implements ISpliceable<T> {
|
||||
|
||||
constructor(private spliceables: ISpliceable<T>[]) { }
|
||||
|
||||
splice(start: number, deleteCount: number, elements: T[]): void {
|
||||
this.spliceables.forEach(s => s.splice(start, deleteCount, elements));
|
||||
}
|
||||
}
|
||||
1367
lib/vscode/src/vs/base/browser/ui/menu/menu.ts
Normal file
1367
lib/vscode/src/vs/base/browser/ui/menu/menu.ts
Normal file
File diff suppressed because it is too large
Load Diff
87
lib/vscode/src/vs/base/browser/ui/menu/menubar.css
Normal file
87
lib/vscode/src/vs/base/browser/ui/menu/menubar.css
Normal file
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Menubar styles */
|
||||
|
||||
.menubar {
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
box-sizing: border-box;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fullscreen .menubar:not(.compact) {
|
||||
margin: 0px;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.menubar > .menubar-menu-button {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 8px;
|
||||
cursor: default;
|
||||
-webkit-app-region: no-drag;
|
||||
zoom: 1;
|
||||
white-space: nowrap;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.menubar.compact {
|
||||
flex-shrink: 0;
|
||||
overflow: visible; /* to avoid the compact menu to be repositioned when clicking */
|
||||
}
|
||||
|
||||
.menubar.compact > .menubar-menu-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
opacity: 1;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.menubar.compact .menubar-menu-items-holder {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder.monaco-menu-container {
|
||||
outline: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder.monaco-menu-container :focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.menubar .toolbar-toggle-more {
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menubar.compact .toolbar-toggle-more {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menubar .toolbar-toggle-more {
|
||||
padding: 0;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.menubar.compact .toolbar-toggle-more::before {
|
||||
content: "\eb94" !important;
|
||||
}
|
||||
997
lib/vscode/src/vs/base/browser/ui/menu/menubar.ts
Normal file
997
lib/vscode/src/vs/base/browser/ui/menu/menubar.ts
Normal file
@@ -0,0 +1,997 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./menubar';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as nls from 'vs/nls';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
|
||||
import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu';
|
||||
import { ActionRunner, IAction, IActionRunner, SubmenuAction, Separator } from 'vs/base/common/actions';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode, ResolvedKeybinding, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { asArray } from 'vs/base/common/arrays';
|
||||
import { ScanCodeUtils, ScanCode } from 'vs/base/common/scanCode';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
|
||||
const $ = DOM.$;
|
||||
|
||||
const menuBarMoreIcon = registerIcon('menubar-more', Codicon.more);
|
||||
|
||||
export interface IMenuBarOptions {
|
||||
enableMnemonics?: boolean;
|
||||
disableAltFocus?: boolean;
|
||||
visibility?: string;
|
||||
getKeybinding?: (action: IAction) => ResolvedKeybinding | undefined;
|
||||
alwaysOnMnemonics?: boolean;
|
||||
compactMode?: Direction;
|
||||
getCompactMenuActions?: () => IAction[]
|
||||
}
|
||||
|
||||
export interface MenuBarMenu {
|
||||
actions: IAction[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
enum MenubarState {
|
||||
HIDDEN,
|
||||
VISIBLE,
|
||||
FOCUSED,
|
||||
OPEN
|
||||
}
|
||||
|
||||
export class MenuBar extends Disposable {
|
||||
|
||||
static readonly OVERFLOW_INDEX: number = -1;
|
||||
|
||||
private menuCache: {
|
||||
buttonElement: HTMLElement;
|
||||
titleElement: HTMLElement;
|
||||
label: string;
|
||||
actions?: IAction[];
|
||||
}[];
|
||||
|
||||
private overflowMenu!: {
|
||||
buttonElement: HTMLElement;
|
||||
titleElement: HTMLElement;
|
||||
label: string;
|
||||
actions?: IAction[];
|
||||
};
|
||||
|
||||
private focusedMenu: {
|
||||
index: number;
|
||||
holder?: HTMLElement;
|
||||
widget?: Menu;
|
||||
} | undefined;
|
||||
|
||||
private focusToReturn: HTMLElement | undefined;
|
||||
private menuUpdater: RunOnceScheduler;
|
||||
|
||||
// Input-related
|
||||
private _mnemonicsInUse: boolean = false;
|
||||
private openedViaKeyboard: boolean = false;
|
||||
private awaitingAltRelease: boolean = false;
|
||||
private ignoreNextMouseUp: boolean = false;
|
||||
private mnemonics: Map<string, number>;
|
||||
|
||||
private updatePending: boolean = false;
|
||||
private _focusState: MenubarState;
|
||||
private actionRunner: IActionRunner;
|
||||
|
||||
private readonly _onVisibilityChange: Emitter<boolean>;
|
||||
private readonly _onFocusStateChange: Emitter<boolean>;
|
||||
|
||||
private numMenusShown: number = 0;
|
||||
private menuStyle: IMenuStyles | undefined;
|
||||
private overflowLayoutScheduled: IDisposable | undefined = undefined;
|
||||
|
||||
constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) {
|
||||
super();
|
||||
|
||||
this.container.setAttribute('role', 'menubar');
|
||||
if (this.options.compactMode !== undefined) {
|
||||
this.container.classList.add('compact');
|
||||
}
|
||||
|
||||
this.menuCache = [];
|
||||
this.mnemonics = new Map<string, number>();
|
||||
|
||||
this._focusState = MenubarState.VISIBLE;
|
||||
|
||||
this._onVisibilityChange = this._register(new Emitter<boolean>());
|
||||
this._onFocusStateChange = this._register(new Emitter<boolean>());
|
||||
|
||||
this.createOverflowMenu();
|
||||
|
||||
this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200));
|
||||
|
||||
this.actionRunner = this._register(new ActionRunner());
|
||||
this._register(this.actionRunner.onDidBeforeRun(() => {
|
||||
this.setUnfocusedState();
|
||||
}));
|
||||
|
||||
this._register(DOM.ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
|
||||
|
||||
if (event.equals(KeyCode.LeftArrow) || (isMacintosh && event.equals(KeyCode.Tab | KeyMod.Shift))) {
|
||||
this.focusPrevious();
|
||||
} else if (event.equals(KeyCode.RightArrow) || (isMacintosh && event.equals(KeyCode.Tab))) {
|
||||
this.focusNext();
|
||||
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
|
||||
this.setUnfocusedState();
|
||||
} else if (!this.isOpen && !event.ctrlKey && this.options.enableMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) {
|
||||
const menuIndex = this.mnemonics.get(key)!;
|
||||
this.onMenuTriggered(menuIndex, false);
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
// Never allow default tab behavior when not compact
|
||||
if (this.options.compactMode === undefined && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => {
|
||||
// This mouse event is outside the menubar so it counts as a focus out
|
||||
if (this.isFocused) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => {
|
||||
let event = e as FocusEvent;
|
||||
|
||||
if (event.relatedTarget) {
|
||||
if (!this.container.contains(event.relatedTarget as HTMLElement)) {
|
||||
this.focusToReturn = event.relatedTarget as HTMLElement;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => {
|
||||
let event = e as FocusEvent;
|
||||
|
||||
// We are losing focus and there is no related target, e.g. webview case
|
||||
if (!event.relatedTarget) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
// We are losing focus and there is a target, reset focusToReturn value as not to redirect
|
||||
else if (event.relatedTarget && !this.container.contains(event.relatedTarget as HTMLElement)) {
|
||||
this.focusToReturn = undefined;
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
if (!this.options.enableMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = e.key.toLocaleLowerCase();
|
||||
if (!this.mnemonics.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mnemonicsInUse = true;
|
||||
this.updateMnemonicVisibility(true);
|
||||
|
||||
const menuIndex = this.mnemonics.get(key)!;
|
||||
this.onMenuTriggered(menuIndex, false);
|
||||
}));
|
||||
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
|
||||
push(arg: MenuBarMenu | MenuBarMenu[]): void {
|
||||
const menus: MenuBarMenu[] = asArray(arg);
|
||||
|
||||
menus.forEach((menuBarMenu) => {
|
||||
const menuIndex = this.menuCache.length;
|
||||
const cleanMenuLabel = cleanMnemonic(menuBarMenu.label);
|
||||
|
||||
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });
|
||||
const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });
|
||||
|
||||
buttonElement.appendChild(titleElement);
|
||||
this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);
|
||||
|
||||
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label);
|
||||
|
||||
// Register mnemonics
|
||||
if (mnemonicMatches) {
|
||||
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];
|
||||
|
||||
this.registerMnemonic(this.menuCache.length, mnemonic);
|
||||
}
|
||||
|
||||
this.updateLabels(titleElement, buttonElement, menuBarMenu.label);
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
|
||||
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
|
||||
this.focusedMenu = { index: menuIndex };
|
||||
this.openedViaKeyboard = true;
|
||||
this.focusState = MenubarState.OPEN;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(Gesture.addTarget(buttonElement));
|
||||
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
|
||||
// Ignore this touch if the menu is touched
|
||||
if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ignoreNextMouseUp = false;
|
||||
this.onMenuTriggered(menuIndex, true);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
|
||||
// Ignore non-left-click
|
||||
const mouseEvent = new StandardMouseEvent(e);
|
||||
if (!mouseEvent.leftButton) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isOpen) {
|
||||
// Open the menu with mouse down and ignore the following mouse up event
|
||||
this.ignoreNextMouseUp = true;
|
||||
this.onMenuTriggered(menuIndex, true);
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
|
||||
if (e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.ignoreNextMouseUp) {
|
||||
if (this.isFocused) {
|
||||
this.onMenuTriggered(menuIndex, true);
|
||||
}
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
|
||||
if (this.isOpen && !this.isCurrentMenu(menuIndex)) {
|
||||
this.menuCache[menuIndex].buttonElement.focus();
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(menuIndex, false);
|
||||
} else if (this.isFocused && !this.isOpen) {
|
||||
this.focusedMenu = { index: menuIndex };
|
||||
buttonElement.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
this.menuCache.push({
|
||||
label: menuBarMenu.label,
|
||||
actions: menuBarMenu.actions,
|
||||
buttonElement: buttonElement,
|
||||
titleElement: titleElement
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createOverflowMenu(): void {
|
||||
const label = this.options.compactMode !== undefined ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More');
|
||||
const title = this.options.compactMode !== undefined ? label : undefined;
|
||||
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.options.compactMode !== undefined ? 0 : -1, 'aria-label': label, 'title': title, 'aria-haspopup': true });
|
||||
const titleElement = $('div.menubar-menu-title.toolbar-toggle-more' + menuBarMoreIcon.cssSelector, { 'role': 'none', 'aria-hidden': true });
|
||||
|
||||
buttonElement.appendChild(titleElement);
|
||||
this.container.appendChild(buttonElement);
|
||||
buttonElement.style.visibility = 'hidden';
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
|
||||
const triggerKeys = [KeyCode.Enter];
|
||||
if (this.options.compactMode === undefined) {
|
||||
triggerKeys.push(KeyCode.DownArrow);
|
||||
} else {
|
||||
triggerKeys.push(KeyCode.Space);
|
||||
triggerKeys.push(this.options.compactMode === Direction.Right ? KeyCode.RightArrow : KeyCode.LeftArrow);
|
||||
}
|
||||
|
||||
if ((triggerKeys.some(k => event.equals(k)) && !this.isOpen)) {
|
||||
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
|
||||
this.openedViaKeyboard = true;
|
||||
this.focusState = MenubarState.OPEN;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(Gesture.addTarget(buttonElement));
|
||||
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
|
||||
// Ignore this touch if the menu is touched
|
||||
if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ignoreNextMouseUp = false;
|
||||
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
|
||||
// Ignore non-left-click
|
||||
const mouseEvent = new StandardMouseEvent(e);
|
||||
if (!mouseEvent.leftButton) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isOpen) {
|
||||
// Open the menu with mouse down and ignore the following mouse up event
|
||||
this.ignoreNextMouseUp = true;
|
||||
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
|
||||
if (e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.ignoreNextMouseUp) {
|
||||
if (this.isFocused) {
|
||||
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
|
||||
}
|
||||
} else {
|
||||
this.ignoreNextMouseUp = false;
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
|
||||
if (this.isOpen && !this.isCurrentMenu(MenuBar.OVERFLOW_INDEX)) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(MenuBar.OVERFLOW_INDEX, false);
|
||||
} else if (this.isFocused && !this.isOpen) {
|
||||
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
|
||||
buttonElement.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
this.overflowMenu = {
|
||||
buttonElement: buttonElement,
|
||||
titleElement: titleElement,
|
||||
label: 'More'
|
||||
};
|
||||
}
|
||||
|
||||
updateMenu(menu: MenuBarMenu): void {
|
||||
const menuToUpdate = this.menuCache.filter(menuBarMenu => menuBarMenu.label === menu.label);
|
||||
if (menuToUpdate && menuToUpdate.length) {
|
||||
menuToUpdate[0].actions = menu.actions;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
menuBarMenu.titleElement.remove();
|
||||
menuBarMenu.buttonElement.remove();
|
||||
});
|
||||
|
||||
this.overflowMenu.titleElement.remove();
|
||||
this.overflowMenu.buttonElement.remove();
|
||||
|
||||
dispose(this.overflowLayoutScheduled);
|
||||
this.overflowLayoutScheduled = undefined;
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
if (this.menuCache) {
|
||||
const left = this.menuCache[0].buttonElement.getBoundingClientRect().left;
|
||||
const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menuCache[this.menuCache.length - 1].buttonElement.getBoundingClientRect().right;
|
||||
return right - left;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this.container.clientHeight;
|
||||
}
|
||||
|
||||
toggleFocus(): void {
|
||||
if (!this.isFocused && this.options.visibility !== 'hidden') {
|
||||
this.mnemonicsInUse = true;
|
||||
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
|
||||
this.focusState = MenubarState.FOCUSED;
|
||||
} else if (!this.isOpen) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}
|
||||
|
||||
private updateOverflowAction(): void {
|
||||
if (!this.menuCache || !this.menuCache.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sizeAvailable = this.container.offsetWidth;
|
||||
let currentSize = 0;
|
||||
let full = this.options.compactMode !== undefined;
|
||||
const prevNumMenusShown = this.numMenusShown;
|
||||
this.numMenusShown = 0;
|
||||
for (let menuBarMenu of this.menuCache) {
|
||||
if (!full) {
|
||||
const size = menuBarMenu.buttonElement.offsetWidth;
|
||||
if (currentSize + size > sizeAvailable) {
|
||||
full = true;
|
||||
} else {
|
||||
currentSize += size;
|
||||
this.numMenusShown++;
|
||||
if (this.numMenusShown > prevNumMenusShown) {
|
||||
menuBarMenu.buttonElement.style.visibility = 'visible';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (full) {
|
||||
menuBarMenu.buttonElement.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Overflow
|
||||
if (full) {
|
||||
// Can't fit the more button, need to remove more menus
|
||||
while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {
|
||||
this.numMenusShown--;
|
||||
const size = this.menuCache[this.numMenusShown].buttonElement.offsetWidth;
|
||||
this.menuCache[this.numMenusShown].buttonElement.style.visibility = 'hidden';
|
||||
currentSize -= size;
|
||||
}
|
||||
|
||||
this.overflowMenu.actions = [];
|
||||
for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) {
|
||||
this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menuCache[idx].label}`, this.menuCache[idx].label, this.menuCache[idx].actions || []));
|
||||
}
|
||||
|
||||
if (this.overflowMenu.buttonElement.nextElementSibling !== this.menuCache[this.numMenusShown].buttonElement) {
|
||||
this.overflowMenu.buttonElement.remove();
|
||||
this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement);
|
||||
this.overflowMenu.buttonElement.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
const compactMenuActions = this.options.getCompactMenuActions?.();
|
||||
if (compactMenuActions && compactMenuActions.length) {
|
||||
this.overflowMenu.actions.push(new Separator());
|
||||
this.overflowMenu.actions.push(...compactMenuActions);
|
||||
}
|
||||
} else {
|
||||
this.overflowMenu.buttonElement.remove();
|
||||
this.container.appendChild(this.overflowMenu.buttonElement);
|
||||
this.overflowMenu.buttonElement.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void {
|
||||
const cleanMenuLabel = cleanMnemonic(label);
|
||||
|
||||
// Update the button label to reflect mnemonics
|
||||
|
||||
if (this.options.enableMnemonics) {
|
||||
let cleanLabel = strings.escape(label);
|
||||
|
||||
// This is global so reset it
|
||||
MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0;
|
||||
let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel);
|
||||
|
||||
// We can't use negative lookbehind so we match our negative and skip
|
||||
while (escMatch && escMatch[1]) {
|
||||
escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel);
|
||||
}
|
||||
|
||||
const replaceDoubleEscapes = (str: string) => str.replace(/&&/g, '&');
|
||||
|
||||
if (escMatch) {
|
||||
titleElement.innerText = '';
|
||||
titleElement.append(
|
||||
strings.ltrim(replaceDoubleEscapes(cleanLabel.substr(0, escMatch.index)), ' '),
|
||||
$('mnemonic', { 'aria-hidden': 'true' }, escMatch[3]),
|
||||
strings.rtrim(replaceDoubleEscapes(cleanLabel.substr(escMatch.index + escMatch[0].length)), ' ')
|
||||
);
|
||||
} else {
|
||||
titleElement.innerText = replaceDoubleEscapes(cleanLabel).trim();
|
||||
}
|
||||
} else {
|
||||
titleElement.innerText = cleanMenuLabel.replace(/&&/g, '&');
|
||||
}
|
||||
|
||||
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
|
||||
// Register mnemonics
|
||||
if (mnemonicMatches) {
|
||||
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];
|
||||
|
||||
if (this.options.enableMnemonics) {
|
||||
buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase());
|
||||
} else {
|
||||
buttonElement.removeAttribute('aria-keyshortcuts');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style(style: IMenuStyles): void {
|
||||
this.menuStyle = style;
|
||||
}
|
||||
|
||||
update(options?: IMenuBarOptions): void {
|
||||
if (options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
// Don't update while using the menu
|
||||
if (this.isFocused) {
|
||||
this.updatePending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label);
|
||||
});
|
||||
|
||||
if (!this.overflowLayoutScheduled) {
|
||||
this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this.updateOverflowAction();
|
||||
this.overflowLayoutScheduled = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
|
||||
private registerMnemonic(menuIndex: number, mnemonic: string): void {
|
||||
this.mnemonics.set(mnemonic.toLocaleLowerCase(), menuIndex);
|
||||
}
|
||||
|
||||
private hideMenubar(): void {
|
||||
if (this.container.style.display !== 'none') {
|
||||
this.container.style.display = 'none';
|
||||
this._onVisibilityChange.fire(false);
|
||||
}
|
||||
}
|
||||
|
||||
private showMenubar(): void {
|
||||
if (this.container.style.display !== 'flex') {
|
||||
this.container.style.display = 'flex';
|
||||
this._onVisibilityChange.fire(true);
|
||||
|
||||
this.updateOverflowAction();
|
||||
}
|
||||
}
|
||||
|
||||
private get focusState(): MenubarState {
|
||||
return this._focusState;
|
||||
}
|
||||
|
||||
private set focusState(value: MenubarState) {
|
||||
if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) {
|
||||
// Losing focus, update the menu if needed
|
||||
|
||||
if (this.updatePending) {
|
||||
this.menuUpdater.schedule();
|
||||
this.updatePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (value === this._focusState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isVisible = this.isVisible;
|
||||
const isOpen = this.isOpen;
|
||||
const isFocused = this.isFocused;
|
||||
|
||||
this._focusState = value;
|
||||
|
||||
switch (value) {
|
||||
case MenubarState.HIDDEN:
|
||||
if (isVisible) {
|
||||
this.hideMenubar();
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
this.focusedMenu = undefined;
|
||||
|
||||
if (this.focusToReturn) {
|
||||
this.focusToReturn.focus();
|
||||
this.focusToReturn = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
case MenubarState.VISIBLE:
|
||||
if (!isVisible) {
|
||||
this.showMenubar();
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
if (this.focusedMenu) {
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.blur();
|
||||
} else {
|
||||
this.menuCache[this.focusedMenu.index].buttonElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
this.focusedMenu = undefined;
|
||||
|
||||
if (this.focusToReturn) {
|
||||
this.focusToReturn.focus();
|
||||
this.focusToReturn = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case MenubarState.FOCUSED:
|
||||
if (!isVisible) {
|
||||
this.showMenubar();
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
}
|
||||
|
||||
if (this.focusedMenu) {
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[this.focusedMenu.index].buttonElement.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MenubarState.OPEN:
|
||||
if (!isVisible) {
|
||||
this.showMenubar();
|
||||
}
|
||||
|
||||
if (this.focusedMenu) {
|
||||
this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this._focusState = value;
|
||||
this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED);
|
||||
}
|
||||
|
||||
private get isVisible(): boolean {
|
||||
return this.focusState >= MenubarState.VISIBLE;
|
||||
}
|
||||
|
||||
private get isFocused(): boolean {
|
||||
return this.focusState >= MenubarState.FOCUSED;
|
||||
}
|
||||
|
||||
private get isOpen(): boolean {
|
||||
return this.focusState >= MenubarState.OPEN;
|
||||
}
|
||||
|
||||
private get hasOverflow(): boolean {
|
||||
return this.numMenusShown < this.menuCache.length;
|
||||
}
|
||||
|
||||
private setUnfocusedState(): void {
|
||||
if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {
|
||||
this.focusState = MenubarState.HIDDEN;
|
||||
} else if (this.options.visibility === 'default' && browser.isFullscreen()) {
|
||||
this.focusState = MenubarState.HIDDEN;
|
||||
} else {
|
||||
this.focusState = MenubarState.VISIBLE;
|
||||
}
|
||||
|
||||
this.ignoreNextMouseUp = false;
|
||||
this.mnemonicsInUse = false;
|
||||
this.updateMnemonicVisibility(false);
|
||||
}
|
||||
|
||||
private focusPrevious(): void {
|
||||
|
||||
if (!this.focusedMenu || this.numMenusShown === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let newFocusedIndex = (this.focusedMenu.index - 1 + this.numMenusShown) % this.numMenusShown;
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
newFocusedIndex = this.numMenusShown - 1;
|
||||
} else if (this.focusedMenu.index === 0 && this.hasOverflow) {
|
||||
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
|
||||
}
|
||||
|
||||
if (newFocusedIndex === this.focusedMenu.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(newFocusedIndex);
|
||||
} else if (this.isFocused) {
|
||||
this.focusedMenu.index = newFocusedIndex;
|
||||
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[newFocusedIndex].buttonElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private focusNext(): void {
|
||||
if (!this.focusedMenu || this.numMenusShown === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newFocusedIndex = (this.focusedMenu.index + 1) % this.numMenusShown;
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
newFocusedIndex = 0;
|
||||
} else if (this.focusedMenu.index === this.numMenusShown - 1) {
|
||||
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
|
||||
}
|
||||
|
||||
if (newFocusedIndex === this.focusedMenu.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen) {
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(newFocusedIndex);
|
||||
} else if (this.isFocused) {
|
||||
this.focusedMenu.index = newFocusedIndex;
|
||||
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[newFocusedIndex].buttonElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateMnemonicVisibility(visible: boolean): void {
|
||||
if (this.menuCache) {
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
if (menuBarMenu.titleElement.children.length) {
|
||||
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
|
||||
if (child) {
|
||||
child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private get mnemonicsInUse(): boolean {
|
||||
return this._mnemonicsInUse;
|
||||
}
|
||||
|
||||
private set mnemonicsInUse(value: boolean) {
|
||||
this._mnemonicsInUse = value;
|
||||
}
|
||||
|
||||
public get onVisibilityChange(): Event<boolean> {
|
||||
return this._onVisibilityChange.event;
|
||||
}
|
||||
|
||||
public get onFocusStateChange(): Event<boolean> {
|
||||
return this._onFocusStateChange.event;
|
||||
}
|
||||
|
||||
private onMenuTriggered(menuIndex: number, clicked: boolean) {
|
||||
if (this.isOpen) {
|
||||
if (this.isCurrentMenu(menuIndex)) {
|
||||
this.setUnfocusedState();
|
||||
} else {
|
||||
this.cleanupCustomMenu();
|
||||
this.showCustomMenu(menuIndex, this.openedViaKeyboard);
|
||||
}
|
||||
} else {
|
||||
this.focusedMenu = { index: menuIndex };
|
||||
this.openedViaKeyboard = !clicked;
|
||||
this.focusState = MenubarState.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
private onModifierKeyToggled(modifierKeyStatus: DOM.IModifierKeyStatus): void {
|
||||
const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey && !modifierKeyStatus.metaKey;
|
||||
|
||||
if (this.options.visibility === 'hidden') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent alt-key default if the menu is not hidden and we use alt to focus
|
||||
if (modifierKeyStatus.event && !this.options.disableAltFocus) {
|
||||
if (ScanCodeUtils.toEnum(modifierKeyStatus.event.code) === ScanCode.AltLeft) {
|
||||
modifierKeyStatus.event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Alt key pressed while menu is focused. This should return focus away from the menubar
|
||||
if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) {
|
||||
this.setUnfocusedState();
|
||||
this.mnemonicsInUse = false;
|
||||
this.awaitingAltRelease = true;
|
||||
}
|
||||
|
||||
// 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')) {
|
||||
this.mnemonicsInUse = true;
|
||||
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
|
||||
this.focusState = MenubarState.FOCUSED;
|
||||
} else if (!this.isOpen) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alt key released
|
||||
if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') {
|
||||
this.awaitingAltRelease = false;
|
||||
}
|
||||
|
||||
if (this.options.enableMnemonics && this.menuCache && !this.isOpen) {
|
||||
this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse);
|
||||
}
|
||||
}
|
||||
|
||||
private isCurrentMenu(menuIndex: number): boolean {
|
||||
if (!this.focusedMenu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.focusedMenu.index === menuIndex;
|
||||
}
|
||||
|
||||
private cleanupCustomMenu(): void {
|
||||
if (this.focusedMenu) {
|
||||
// Remove focus from the menus first
|
||||
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
|
||||
this.overflowMenu.buttonElement.focus();
|
||||
} else {
|
||||
this.menuCache[this.focusedMenu.index].buttonElement.focus();
|
||||
}
|
||||
|
||||
if (this.focusedMenu.holder) {
|
||||
if (this.focusedMenu.holder.parentElement) {
|
||||
this.focusedMenu.holder.parentElement.classList.remove('open');
|
||||
}
|
||||
|
||||
this.focusedMenu.holder.remove();
|
||||
}
|
||||
|
||||
if (this.focusedMenu.widget) {
|
||||
this.focusedMenu.widget.dispose();
|
||||
}
|
||||
|
||||
this.focusedMenu = { index: this.focusedMenu.index };
|
||||
}
|
||||
}
|
||||
|
||||
private showCustomMenu(menuIndex: number, selectFirst = true): void {
|
||||
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
|
||||
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menuCache[actualMenuIndex];
|
||||
|
||||
if (!customMenu.actions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuHolder = $('div.menubar-menu-items-holder', { 'title': '' });
|
||||
|
||||
customMenu.buttonElement.classList.add('open');
|
||||
|
||||
const buttonBoundingRect = customMenu.buttonElement.getBoundingClientRect();
|
||||
|
||||
if (this.options.compactMode === Direction.Right) {
|
||||
menuHolder.style.top = `${buttonBoundingRect.top}px`;
|
||||
menuHolder.style.left = `${buttonBoundingRect.left + this.container.clientWidth}px`;
|
||||
} else if (this.options.compactMode === Direction.Left) {
|
||||
menuHolder.style.top = `${buttonBoundingRect.top}px`;
|
||||
menuHolder.style.right = `${this.container.clientWidth}px`;
|
||||
menuHolder.style.left = 'auto';
|
||||
} else {
|
||||
menuHolder.style.top = `${this.container.clientHeight}px`;
|
||||
menuHolder.style.left = `${buttonBoundingRect.left}px`;
|
||||
}
|
||||
|
||||
customMenu.buttonElement.appendChild(menuHolder);
|
||||
|
||||
let menuOptions: IMenuOptions = {
|
||||
getKeyBinding: this.options.getKeybinding,
|
||||
actionRunner: this.actionRunner,
|
||||
enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics),
|
||||
ariaLabel: withNullAsUndefined(customMenu.buttonElement.getAttribute('aria-label')),
|
||||
expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right,
|
||||
useEventAsContext: true
|
||||
};
|
||||
|
||||
let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions));
|
||||
if (this.menuStyle) {
|
||||
menuWidget.style(this.menuStyle);
|
||||
}
|
||||
|
||||
this._register(menuWidget.onDidCancel(() => {
|
||||
this.focusState = MenubarState.FOCUSED;
|
||||
}));
|
||||
|
||||
if (actualMenuIndex !== menuIndex) {
|
||||
menuWidget.trigger(menuIndex - this.numMenusShown);
|
||||
} else {
|
||||
menuWidget.focus(selectFirst);
|
||||
}
|
||||
|
||||
this.focusedMenu = {
|
||||
index: actualMenuIndex,
|
||||
holder: menuHolder,
|
||||
widget: menuWidget
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-mouse-cursor-text {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* The following selector looks a bit funny, but that is needed to cover all the workbench and the editor!! */
|
||||
.vs-dark .mac .monaco-mouse-cursor-text, .hc-black .mac .monaco-mouse-cursor-text,
|
||||
.vs-dark.mac .monaco-mouse-cursor-text, .hc-black.mac .monaco-mouse-cursor-text {
|
||||
cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./mouseCursor';
|
||||
|
||||
export const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`;
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-progress-container {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
overflow: hidden; /* keep progress bit in bounds */
|
||||
}
|
||||
|
||||
.monaco-progress-container .progress-bit {
|
||||
width: 2%;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-progress-container.active .progress-bit {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.monaco-progress-container.discrete .progress-bit {
|
||||
left: 0;
|
||||
transition: width 100ms linear;
|
||||
}
|
||||
|
||||
.monaco-progress-container.discrete.done .progress-bit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-progress-container.infinite .progress-bit {
|
||||
animation-name: progress;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
|
||||
/**
|
||||
* The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of
|
||||
* that container. Since translateX is relative to the progress bit size, we have to multiple it with
|
||||
* its relative size to the parent container:
|
||||
* 50%: 50 * 50 = 2500%
|
||||
* 100%: 50 * 100 - 50 (do not overflow): 4950%
|
||||
*/
|
||||
@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } }
|
||||
217
lib/vscode/src/vs/base/browser/ui/progressbar/progressbar.ts
Normal file
217
lib/vscode/src/vs/base/browser/ui/progressbar/progressbar.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./progressbar';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { hide, show } from 'vs/base/browser/dom';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
|
||||
const CSS_DONE = 'done';
|
||||
const CSS_ACTIVE = 'active';
|
||||
const CSS_INFINITE = 'infinite';
|
||||
const CSS_DISCRETE = 'discrete';
|
||||
|
||||
export interface IProgressBarOptions extends IProgressBarStyles {
|
||||
}
|
||||
|
||||
export interface IProgressBarStyles {
|
||||
progressBarBackground?: Color;
|
||||
}
|
||||
|
||||
const defaultOpts = {
|
||||
progressBarBackground: Color.fromHex('#0E70C0')
|
||||
};
|
||||
|
||||
/**
|
||||
* A progress bar with support for infinite or discrete progress.
|
||||
*/
|
||||
export class ProgressBar extends Disposable {
|
||||
private options: IProgressBarOptions;
|
||||
private workedVal: number;
|
||||
private element!: HTMLElement;
|
||||
private bit!: HTMLElement;
|
||||
private totalWork: number | undefined;
|
||||
private progressBarBackground: Color | undefined;
|
||||
private showDelayedScheduler: RunOnceScheduler;
|
||||
|
||||
constructor(container: HTMLElement, options?: IProgressBarOptions) {
|
||||
super();
|
||||
|
||||
this.options = options || Object.create(null);
|
||||
mixin(this.options, defaultOpts, false);
|
||||
|
||||
this.workedVal = 0;
|
||||
|
||||
this.progressBarBackground = this.options.progressBarBackground;
|
||||
|
||||
this._register(this.showDelayedScheduler = new RunOnceScheduler(() => show(this.element), 0));
|
||||
|
||||
this.create(container);
|
||||
}
|
||||
|
||||
private create(container: HTMLElement): void {
|
||||
this.element = document.createElement('div');
|
||||
this.element.classList.add('monaco-progress-container');
|
||||
this.element.setAttribute('role', 'progressbar');
|
||||
container.appendChild(this.element);
|
||||
|
||||
this.bit = document.createElement('div');
|
||||
this.bit.classList.add('progress-bit');
|
||||
this.element.appendChild(this.bit);
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private off(): void {
|
||||
this.bit.style.width = 'inherit';
|
||||
this.bit.style.opacity = '1';
|
||||
this.element.classList.remove(CSS_ACTIVE, CSS_INFINITE, CSS_DISCRETE);
|
||||
|
||||
this.workedVal = 0;
|
||||
this.totalWork = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to the progress bar that all work is done.
|
||||
*/
|
||||
done(): ProgressBar {
|
||||
return this.doDone(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the progressbar from showing any progress instantly without fading out.
|
||||
*/
|
||||
stop(): ProgressBar {
|
||||
return this.doDone(false);
|
||||
}
|
||||
|
||||
private doDone(delayed: boolean): ProgressBar {
|
||||
this.element.classList.add(CSS_DONE);
|
||||
|
||||
// let it grow to 100% width and hide afterwards
|
||||
if (!this.element.classList.contains(CSS_INFINITE)) {
|
||||
this.bit.style.width = 'inherit';
|
||||
|
||||
if (delayed) {
|
||||
setTimeout(() => this.off(), 200);
|
||||
} else {
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
|
||||
// let it fade out and hide afterwards
|
||||
else {
|
||||
this.bit.style.opacity = '0';
|
||||
if (delayed) {
|
||||
setTimeout(() => this.off(), 200);
|
||||
} else {
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this mode to indicate progress that has no total number of work units.
|
||||
*/
|
||||
infinite(): ProgressBar {
|
||||
this.bit.style.width = '2%';
|
||||
this.bit.style.opacity = '1';
|
||||
|
||||
this.element.classList.remove(CSS_DISCRETE, CSS_DONE);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_INFINITE);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the progress bar the total number of work. Use in combination with workedVal() to let
|
||||
* the progress bar show the actual progress based on the work that is done.
|
||||
*/
|
||||
total(value: number): ProgressBar {
|
||||
this.workedVal = 0;
|
||||
this.totalWork = value;
|
||||
this.element.setAttribute('aria-valuemax', value.toString());
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if this progress bar is configured with total work
|
||||
*/
|
||||
hasTotal(): boolean {
|
||||
return isNumber(this.totalWork);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the progress bar that an increment of work has been completed.
|
||||
*/
|
||||
worked(value: number): ProgressBar {
|
||||
value = Math.max(1, Number(value));
|
||||
|
||||
return this.doSetWorked(this.workedVal + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the progress bar the total amount of work that has been completed.
|
||||
*/
|
||||
setWorked(value: number): ProgressBar {
|
||||
value = Math.max(1, Number(value));
|
||||
|
||||
return this.doSetWorked(value);
|
||||
}
|
||||
|
||||
private doSetWorked(value: number): ProgressBar {
|
||||
const totalWork = this.totalWork || 100;
|
||||
|
||||
this.workedVal = value;
|
||||
this.workedVal = Math.min(totalWork, this.workedVal);
|
||||
|
||||
this.element.classList.remove(CSS_INFINITE, CSS_DONE);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_DISCRETE);
|
||||
this.element.setAttribute('aria-valuenow', value.toString());
|
||||
|
||||
this.bit.style.width = 100 * (this.workedVal / (totalWork)) + '%';
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
show(delay?: number): void {
|
||||
this.showDelayedScheduler.cancel();
|
||||
|
||||
if (typeof delay === 'number') {
|
||||
this.showDelayedScheduler.schedule(delay);
|
||||
} else {
|
||||
show(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
hide(this.element);
|
||||
this.showDelayedScheduler.cancel();
|
||||
}
|
||||
|
||||
style(styles: IProgressBarStyles): void {
|
||||
this.progressBarBackground = styles.progressBarBackground;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
if (this.bit) {
|
||||
const background = this.progressBarBackground ? this.progressBarBackground.toString() : '';
|
||||
|
||||
this.bit.style.backgroundColor = background;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
lib/vscode/src/vs/base/browser/ui/sash/sash.css
Normal file
90
lib/vscode/src/vs/base/browser/ui/sash/sash.css
Normal file
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
:root {
|
||||
--sash-size: 4px;
|
||||
}
|
||||
|
||||
.monaco-sash {
|
||||
position: absolute;
|
||||
z-index: 35;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.monaco-sash.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-sash.mac.vertical {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical.minimum {
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical.maximum {
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.mac.horizontal {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.minimum {
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.maximum {
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.disabled {
|
||||
cursor: default !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical {
|
||||
cursor: ew-resize;
|
||||
top: 0;
|
||||
width: var(--sash-size);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal {
|
||||
cursor: ns-resize;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--sash-size);
|
||||
}
|
||||
|
||||
.monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after {
|
||||
content: ' ';
|
||||
height: calc(var(--sash-size) * 2);
|
||||
width: calc(var(--sash-size) * 2);
|
||||
z-index: 100;
|
||||
display: block;
|
||||
cursor: all-scroll; position: absolute;
|
||||
}
|
||||
|
||||
.monaco-sash.orthogonal-start.vertical::before { left: -calc(var(--sash-size) / 2); top: calc(var(--sash-size) * -1); }
|
||||
.monaco-sash.orthogonal-end.vertical::after { left: -calc(var(--sash-size) / 2); bottom: calc(var(--sash-size) * -1); }
|
||||
.monaco-sash.orthogonal-start.horizontal::before { top: -calc(var(--sash-size) / 2); left: calc(var(--sash-size) * -1); }
|
||||
.monaco-sash.orthogonal-end.horizontal::after { top: -calc(var(--sash-size) / 2); right: calc(var(--sash-size) * -1); }
|
||||
|
||||
/** Debug **/
|
||||
|
||||
.monaco-sash.debug {
|
||||
background: cyan;
|
||||
}
|
||||
|
||||
.monaco-sash.debug.disabled {
|
||||
background: rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.monaco-sash.debug:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash.debug:not(.disabled).orthogonal-end::after {
|
||||
background: red;
|
||||
}
|
||||
428
lib/vscode/src/vs/base/browser/ui/sash/sash.ts
Normal file
428
lib/vscode/src/vs/base/browser/ui/sash/sash.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./sash';
|
||||
import { IDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { 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';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
export interface ISashLayoutProvider { }
|
||||
|
||||
export interface IVerticalSashLayoutProvider extends ISashLayoutProvider {
|
||||
getVerticalSashLeft(sash: Sash): number;
|
||||
getVerticalSashTop?(sash: Sash): number;
|
||||
getVerticalSashHeight?(sash: Sash): number;
|
||||
}
|
||||
|
||||
export interface IHorizontalSashLayoutProvider extends ISashLayoutProvider {
|
||||
getHorizontalSashTop(sash: Sash): number;
|
||||
getHorizontalSashLeft?(sash: Sash): number;
|
||||
getHorizontalSashWidth?(sash: Sash): number;
|
||||
}
|
||||
|
||||
export interface ISashEvent {
|
||||
startX: number;
|
||||
currentX: number;
|
||||
startY: number;
|
||||
currentY: number;
|
||||
altKey: boolean;
|
||||
}
|
||||
|
||||
export interface ISashOptions {
|
||||
readonly orientation: Orientation;
|
||||
readonly orthogonalStartSash?: Sash;
|
||||
readonly orthogonalEndSash?: Sash;
|
||||
readonly size?: number;
|
||||
}
|
||||
|
||||
export interface IVerticalSashOptions extends ISashOptions {
|
||||
readonly orientation: Orientation.VERTICAL;
|
||||
}
|
||||
|
||||
export interface IHorizontalSashOptions extends ISashOptions {
|
||||
readonly orientation: Orientation.HORIZONTAL;
|
||||
}
|
||||
|
||||
export const enum Orientation {
|
||||
VERTICAL,
|
||||
HORIZONTAL
|
||||
}
|
||||
|
||||
export const enum SashState {
|
||||
Disabled,
|
||||
Minimum,
|
||||
Maximum,
|
||||
Enabled
|
||||
}
|
||||
|
||||
let globalSize = 4;
|
||||
const onDidChangeGlobalSize = new Emitter<number>();
|
||||
export function setGlobalSashSize(size: number): void {
|
||||
globalSize = size;
|
||||
onDidChangeGlobalSize.fire(size);
|
||||
}
|
||||
|
||||
export class Sash extends Disposable {
|
||||
|
||||
private el: HTMLElement;
|
||||
private layoutProvider: ISashLayoutProvider;
|
||||
private hidden: boolean;
|
||||
private orientation!: Orientation;
|
||||
private size: number;
|
||||
|
||||
private _state: SashState = SashState.Enabled;
|
||||
get state(): SashState { return this._state; }
|
||||
set state(state: SashState) {
|
||||
if (this._state === state) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.classList.toggle('disabled', state === SashState.Disabled);
|
||||
this.el.classList.toggle('minimum', state === SashState.Minimum);
|
||||
this.el.classList.toggle('maximum', state === SashState.Maximum);
|
||||
|
||||
this._state = state;
|
||||
this._onDidEnablementChange.fire(state);
|
||||
}
|
||||
|
||||
private readonly _onDidEnablementChange = this._register(new Emitter<SashState>());
|
||||
readonly onDidEnablementChange: Event<SashState> = this._onDidEnablementChange.event;
|
||||
|
||||
private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
|
||||
readonly onDidStart: Event<ISashEvent> = this._onDidStart.event;
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
|
||||
readonly onDidChange: Event<ISashEvent> = this._onDidChange.event;
|
||||
|
||||
private readonly _onDidReset = this._register(new Emitter<void>());
|
||||
readonly onDidReset: Event<void> = this._onDidReset.event;
|
||||
|
||||
private readonly _onDidEnd = this._register(new Emitter<void>());
|
||||
readonly onDidEnd: Event<void> = this._onDidEnd.event;
|
||||
|
||||
linkedSash: Sash | undefined = undefined;
|
||||
|
||||
private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalStartSash: Sash | undefined;
|
||||
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
|
||||
set orthogonalStartSash(sash: Sash | undefined) {
|
||||
this.orthogonalStartSashDisposables.clear();
|
||||
|
||||
if (sash) {
|
||||
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalStartSashEnablementChange, this));
|
||||
this.onOrthogonalStartSashEnablementChange(sash.state);
|
||||
} else {
|
||||
this.onOrthogonalStartSashEnablementChange(SashState.Disabled);
|
||||
}
|
||||
|
||||
this._orthogonalStartSash = sash;
|
||||
}
|
||||
|
||||
private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalEndSash: Sash | undefined;
|
||||
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
|
||||
set orthogonalEndSash(sash: Sash | undefined) {
|
||||
this.orthogonalEndSashDisposables.clear();
|
||||
|
||||
if (sash) {
|
||||
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalEndSashEnablementChange, this));
|
||||
this.onOrthogonalEndSashEnablementChange(sash.state);
|
||||
} else {
|
||||
this.onOrthogonalEndSashEnablementChange(SashState.Disabled);
|
||||
}
|
||||
|
||||
this._orthogonalEndSash = sash;
|
||||
}
|
||||
|
||||
constructor(container: HTMLElement, layoutProvider: IVerticalSashLayoutProvider, options: ISashOptions);
|
||||
constructor(container: HTMLElement, layoutProvider: IHorizontalSashLayoutProvider, options: ISashOptions);
|
||||
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) {
|
||||
super();
|
||||
|
||||
this.el = append(container, $('.monaco-sash'));
|
||||
|
||||
if (isMacintosh) {
|
||||
this.el.classList.add('mac');
|
||||
}
|
||||
|
||||
this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
|
||||
this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
|
||||
|
||||
this._register(Gesture.addTarget(this.el));
|
||||
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
|
||||
|
||||
if (typeof options.size === 'number') {
|
||||
this.size = options.size;
|
||||
|
||||
if (options.orientation === Orientation.VERTICAL) {
|
||||
this.el.style.width = `${this.size}px`;
|
||||
} else {
|
||||
this.el.style.height = `${this.size}px`;
|
||||
}
|
||||
} else {
|
||||
this.size = globalSize;
|
||||
this._register(onDidChangeGlobalSize.event(size => {
|
||||
this.size = size;
|
||||
this.layout();
|
||||
}));
|
||||
}
|
||||
|
||||
this.hidden = false;
|
||||
this.layoutProvider = layoutProvider;
|
||||
|
||||
this.orthogonalStartSash = options.orthogonalStartSash;
|
||||
this.orthogonalEndSash = options.orthogonalEndSash;
|
||||
|
||||
this.orientation = options.orientation || Orientation.VERTICAL;
|
||||
|
||||
if (this.orientation === Orientation.HORIZONTAL) {
|
||||
this.el.classList.add('horizontal');
|
||||
this.el.classList.remove('vertical');
|
||||
} else {
|
||||
this.el.classList.remove('horizontal');
|
||||
this.el.classList.add('vertical');
|
||||
}
|
||||
|
||||
this.el.classList.toggle('debug', DEBUG);
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
EventHelper.stop(e, false);
|
||||
|
||||
let isMultisashResize = false;
|
||||
|
||||
if (!(e as any).__orthogonalSashEvent) {
|
||||
const orthogonalSash = this.getOrthogonalSash(e);
|
||||
|
||||
if (orthogonalSash) {
|
||||
isMultisashResize = true;
|
||||
(e as any).__orthogonalSashEvent = true;
|
||||
orthogonalSash.onMouseDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.linkedSash && !(e as any).__linkedSashEvent) {
|
||||
(e as any).__linkedSashEvent = true;
|
||||
this.linkedSash.onMouseDown(e);
|
||||
}
|
||||
|
||||
if (!this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select both iframes and webviews; internally Electron nests an iframe
|
||||
// in its <webview> component, but this isn't queryable.
|
||||
const iframes = [
|
||||
...getElementsByTagName('iframe'),
|
||||
...getElementsByTagName('webview'),
|
||||
];
|
||||
|
||||
for (const iframe of iframes) {
|
||||
iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash
|
||||
}
|
||||
|
||||
const mouseDownEvent = new StandardMouseEvent(e);
|
||||
const startX = mouseDownEvent.posx;
|
||||
const startY = mouseDownEvent.posy;
|
||||
const altKey = mouseDownEvent.altKey;
|
||||
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
|
||||
|
||||
this.el.classList.add('active');
|
||||
this._onDidStart.fire(startEvent);
|
||||
|
||||
// fix https://github.com/microsoft/vscode/issues/21675
|
||||
const style = createStyleSheet(this.el);
|
||||
const updateStyle = () => {
|
||||
let cursor = '';
|
||||
|
||||
if (isMultisashResize) {
|
||||
cursor = 'all-scroll';
|
||||
} else if (this.orientation === Orientation.HORIZONTAL) {
|
||||
if (this.state === SashState.Minimum) {
|
||||
cursor = 's-resize';
|
||||
} else if (this.state === SashState.Maximum) {
|
||||
cursor = 'n-resize';
|
||||
} else {
|
||||
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
|
||||
}
|
||||
} else {
|
||||
if (this.state === SashState.Minimum) {
|
||||
cursor = 'e-resize';
|
||||
} else if (this.state === SashState.Maximum) {
|
||||
cursor = 'w-resize';
|
||||
} else {
|
||||
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
|
||||
}
|
||||
}
|
||||
|
||||
style.textContent = `* { cursor: ${cursor} !important; }`;
|
||||
};
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
updateStyle();
|
||||
|
||||
if (!isMultisashResize) {
|
||||
this.onDidEnablementChange(updateStyle, null, disposables);
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
EventHelper.stop(e, false);
|
||||
const mouseMoveEvent = new StandardMouseEvent(e);
|
||||
const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
|
||||
|
||||
this._onDidChange.fire(event);
|
||||
};
|
||||
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
EventHelper.stop(e, false);
|
||||
|
||||
this.el.removeChild(style);
|
||||
|
||||
this.el.classList.remove('active');
|
||||
this._onDidEnd.fire();
|
||||
|
||||
disposables.dispose();
|
||||
|
||||
for (const iframe of iframes) {
|
||||
iframe.style.pointerEvents = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
domEvent(window, 'mousemove')(onMouseMove, null, disposables);
|
||||
domEvent(window, 'mouseup')(onMouseUp, null, disposables);
|
||||
}
|
||||
|
||||
private onMouseDoubleClick(e: MouseEvent): void {
|
||||
const orthogonalSash = this.getOrthogonalSash(e);
|
||||
|
||||
if (orthogonalSash) {
|
||||
orthogonalSash._onDidReset.fire();
|
||||
}
|
||||
|
||||
if (this.linkedSash) {
|
||||
this.linkedSash._onDidReset.fire();
|
||||
}
|
||||
|
||||
this._onDidReset.fire();
|
||||
}
|
||||
|
||||
private onTouchStart(event: GestureEvent): void {
|
||||
EventHelper.stop(event);
|
||||
|
||||
const listeners: IDisposable[] = [];
|
||||
|
||||
const startX = event.pageX;
|
||||
const startY = event.pageY;
|
||||
const altKey = event.altKey;
|
||||
|
||||
this._onDidStart.fire({
|
||||
startX: startX,
|
||||
currentX: startX,
|
||||
startY: startY,
|
||||
currentY: startY,
|
||||
altKey
|
||||
});
|
||||
|
||||
listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => {
|
||||
if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) {
|
||||
this._onDidChange.fire({
|
||||
startX: startX,
|
||||
currentX: event.pageX,
|
||||
startY: startY,
|
||||
currentY: event.pageY,
|
||||
altKey
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
listeners.push(addDisposableListener(this.el, EventType.End, (event: GestureEvent) => {
|
||||
this._onDidEnd.fire();
|
||||
dispose(listeners);
|
||||
}));
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
|
||||
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
|
||||
|
||||
if (verticalProvider.getVerticalSashTop) {
|
||||
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
|
||||
}
|
||||
|
||||
if (verticalProvider.getVerticalSashHeight) {
|
||||
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
|
||||
}
|
||||
} else {
|
||||
const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
|
||||
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
|
||||
|
||||
if (horizontalProvider.getHorizontalSashLeft) {
|
||||
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
|
||||
}
|
||||
|
||||
if (horizontalProvider.getHorizontalSashWidth) {
|
||||
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.hidden = false;
|
||||
this.el.style.removeProperty('display');
|
||||
this.el.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.hidden = true;
|
||||
this.el.style.display = 'none';
|
||||
this.el.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
isHidden(): boolean {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.el.remove();
|
||||
}
|
||||
}
|
||||
282
lib/vscode/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts
Normal file
282
lib/vscode/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
|
||||
import { IMouseEvent, StandardWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { ScrollbarArrow, ScrollbarArrowOptions } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
|
||||
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
|
||||
import { ScrollbarVisibilityController } from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
|
||||
/**
|
||||
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
|
||||
*/
|
||||
const MOUSE_DRAG_RESET_DISTANCE = 140;
|
||||
|
||||
export interface ISimplifiedMouseEvent {
|
||||
buttons: number;
|
||||
posx: number;
|
||||
posy: number;
|
||||
}
|
||||
|
||||
export interface ScrollbarHost {
|
||||
onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
|
||||
onDragStart(): void;
|
||||
onDragEnd(): void;
|
||||
}
|
||||
|
||||
export interface AbstractScrollbarOptions {
|
||||
lazyRender: boolean;
|
||||
host: ScrollbarHost;
|
||||
scrollbarState: ScrollbarState;
|
||||
visibility: ScrollbarVisibility;
|
||||
extraScrollbarClassName: string;
|
||||
scrollable: Scrollable;
|
||||
}
|
||||
|
||||
export abstract class AbstractScrollbar extends Widget {
|
||||
|
||||
protected _host: ScrollbarHost;
|
||||
protected _scrollable: Scrollable;
|
||||
private _lazyRender: boolean;
|
||||
protected _scrollbarState: ScrollbarState;
|
||||
private _visibilityController: ScrollbarVisibilityController;
|
||||
private _mouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
public slider!: FastDomNode<HTMLElement>;
|
||||
|
||||
protected _shouldRender: boolean;
|
||||
|
||||
constructor(opts: AbstractScrollbarOptions) {
|
||||
super();
|
||||
this._lazyRender = opts.lazyRender;
|
||||
this._host = opts.host;
|
||||
this._scrollable = opts.scrollable;
|
||||
this._scrollbarState = opts.scrollbarState;
|
||||
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>());
|
||||
this._shouldRender = true;
|
||||
this.domNode = createFastDomNode(document.createElement('div'));
|
||||
this.domNode.setAttribute('role', 'presentation');
|
||||
this.domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this._visibilityController.setDomNode(this.domNode);
|
||||
this.domNode.setPosition('absolute');
|
||||
|
||||
this.onmousedown(this.domNode.domNode, (e) => this._domNodeMouseDown(e));
|
||||
}
|
||||
|
||||
// ----------------- creation
|
||||
|
||||
/**
|
||||
* Creates the dom node for an arrow & adds it to the container
|
||||
*/
|
||||
protected _createArrow(opts: ScrollbarArrowOptions): void {
|
||||
let arrow = this._register(new ScrollbarArrow(opts));
|
||||
this.domNode.domNode.appendChild(arrow.bgDomNode);
|
||||
this.domNode.domNode.appendChild(arrow.domNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the slider dom node, adds it to the container & hooks up the events
|
||||
*/
|
||||
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
|
||||
this.slider = createFastDomNode(document.createElement('div'));
|
||||
this.slider.setClassName('slider');
|
||||
this.slider.setPosition('absolute');
|
||||
this.slider.setTop(top);
|
||||
this.slider.setLeft(left);
|
||||
if (typeof width === 'number') {
|
||||
this.slider.setWidth(width);
|
||||
}
|
||||
if (typeof height === 'number') {
|
||||
this.slider.setHeight(height);
|
||||
}
|
||||
this.slider.setLayerHinting(true);
|
||||
this.slider.setContain('strict');
|
||||
|
||||
this.domNode.domNode.appendChild(this.slider.domNode);
|
||||
|
||||
this.onmousedown(this.slider.domNode, (e) => {
|
||||
if (e.leftButton) {
|
||||
e.preventDefault();
|
||||
this._sliderMouseDown(e, () => { /*nothing to do*/ });
|
||||
}
|
||||
});
|
||||
|
||||
this.onclick(this.slider.domNode, e => {
|
||||
if (e.leftButton) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------- Update state
|
||||
|
||||
protected _onElementSize(visibleSize: number): boolean {
|
||||
if (this._scrollbarState.setVisibleSize(visibleSize)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _onElementScrollSize(elementScrollSize: number): boolean {
|
||||
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _onElementScrollPosition(elementScrollPosition: number): boolean {
|
||||
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
// ----------------- rendering
|
||||
|
||||
public beginReveal(): void {
|
||||
this._visibilityController.setShouldBeVisible(true);
|
||||
}
|
||||
|
||||
public beginHide(): void {
|
||||
this._visibilityController.setShouldBeVisible(false);
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
if (!this._shouldRender) {
|
||||
return;
|
||||
}
|
||||
this._shouldRender = false;
|
||||
|
||||
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
|
||||
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
|
||||
}
|
||||
// ----------------- DOM events
|
||||
|
||||
private _domNodeMouseDown(e: IMouseEvent): void {
|
||||
if (e.target !== this.domNode.domNode) {
|
||||
return;
|
||||
}
|
||||
this._onMouseDown(e);
|
||||
}
|
||||
|
||||
public delegateMouseDown(e: IMouseEvent): void {
|
||||
let domTop = this.domNode.domNode.getClientRects()[0].top;
|
||||
let sliderStart = domTop + this._scrollbarState.getSliderPosition();
|
||||
let sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
|
||||
let mousePos = this._sliderMousePosition(e);
|
||||
if (sliderStart <= mousePos && mousePos <= sliderStop) {
|
||||
// Act as if it was a mouse down on the slider
|
||||
if (e.leftButton) {
|
||||
e.preventDefault();
|
||||
this._sliderMouseDown(e, () => { /*nothing to do*/ });
|
||||
}
|
||||
} else {
|
||||
// Act as if it was a mouse down on the scrollbar
|
||||
this._onMouseDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseDown(e: IMouseEvent): void {
|
||||
let offsetX: number;
|
||||
let offsetY: number;
|
||||
if (e.target === this.domNode.domNode && typeof e.browserEvent.offsetX === 'number' && typeof e.browserEvent.offsetY === 'number') {
|
||||
offsetX = e.browserEvent.offsetX;
|
||||
offsetY = e.browserEvent.offsetY;
|
||||
} else {
|
||||
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
|
||||
offsetX = e.posx - domNodePosition.left;
|
||||
offsetY = e.posy - domNodePosition.top;
|
||||
}
|
||||
this._setDesiredScrollPositionNow(this._scrollbarState.getDesiredScrollPositionFromOffset(this._mouseDownRelativePosition(offsetX, offsetY)));
|
||||
if (e.leftButton) {
|
||||
e.preventDefault();
|
||||
this._sliderMouseDown(e, () => { /*nothing to do*/ });
|
||||
}
|
||||
}
|
||||
|
||||
private _sliderMouseDown(e: IMouseEvent, onDragFinished: () => void): void {
|
||||
const initialMousePosition = this._sliderMousePosition(e);
|
||||
const initialMouseOrthogonalPosition = this._sliderOrthogonalMousePosition(e);
|
||||
const initialScrollbarState = this._scrollbarState.clone();
|
||||
this.slider.toggleClassName('active', true);
|
||||
|
||||
this._mouseMoveMonitor.startMonitoring(
|
||||
e.target,
|
||||
e.buttons,
|
||||
standardMouseMoveMerger,
|
||||
(mouseMoveData: IStandardMouseMoveEventData) => {
|
||||
const mouseOrthogonalPosition = this._sliderOrthogonalMousePosition(mouseMoveData);
|
||||
const mouseOrthogonalDelta = Math.abs(mouseOrthogonalPosition - initialMouseOrthogonalPosition);
|
||||
|
||||
if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
|
||||
// The mouse has wondered away from the scrollbar => reset dragging
|
||||
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
|
||||
return;
|
||||
}
|
||||
|
||||
const mousePosition = this._sliderMousePosition(mouseMoveData);
|
||||
const mouseDelta = mousePosition - initialMousePosition;
|
||||
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(mouseDelta));
|
||||
},
|
||||
() => {
|
||||
this.slider.toggleClassName('active', false);
|
||||
this._host.onDragEnd();
|
||||
onDragFinished();
|
||||
}
|
||||
);
|
||||
|
||||
this._host.onDragStart();
|
||||
}
|
||||
|
||||
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
|
||||
|
||||
let desiredScrollPosition: INewScrollPosition = {};
|
||||
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
|
||||
|
||||
this._scrollable.setScrollPositionNow(desiredScrollPosition);
|
||||
}
|
||||
|
||||
public updateScrollbarSize(scrollbarSize: number): void {
|
||||
this._updateScrollbarSize(scrollbarSize);
|
||||
this._scrollbarState.setScrollbarSize(scrollbarSize);
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Overwrite these
|
||||
|
||||
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
|
||||
protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
|
||||
|
||||
protected abstract _mouseDownRelativePosition(offsetX: number, offsetY: number): number;
|
||||
protected abstract _sliderMousePosition(e: ISimplifiedMouseEvent): number;
|
||||
protected abstract _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number;
|
||||
protected abstract _updateScrollbarSize(size: number): void;
|
||||
|
||||
public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
|
||||
import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
|
||||
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
|
||||
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
|
||||
|
||||
const scrollbarButtonLeftIcon = registerIcon('scrollbar-button-left', Codicon.triangleLeft);
|
||||
const scrollbarButtonRightIcon = registerIcon('scrollbar-button-right', Codicon.triangleRight);
|
||||
|
||||
export class HorizontalScrollbar extends AbstractScrollbar {
|
||||
|
||||
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
|
||||
const scrollDimensions = scrollable.getScrollDimensions();
|
||||
const scrollPosition = scrollable.getCurrentScrollPosition();
|
||||
super({
|
||||
lazyRender: options.lazyRender,
|
||||
host: host,
|
||||
scrollbarState: new ScrollbarState(
|
||||
(options.horizontalHasArrows ? options.arrowSize : 0),
|
||||
(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize),
|
||||
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
|
||||
scrollDimensions.width,
|
||||
scrollDimensions.scrollWidth,
|
||||
scrollPosition.scrollLeft
|
||||
),
|
||||
visibility: options.horizontal,
|
||||
extraScrollbarClassName: 'horizontal',
|
||||
scrollable: scrollable
|
||||
});
|
||||
|
||||
if (options.horizontalHasArrows) {
|
||||
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
|
||||
let scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
|
||||
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: scrollbarButtonLeftIcon,
|
||||
top: scrollbarDelta,
|
||||
left: arrowDelta,
|
||||
bottom: undefined,
|
||||
right: undefined,
|
||||
bgWidth: options.arrowSize,
|
||||
bgHeight: options.horizontalScrollbarSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 1, 0)),
|
||||
});
|
||||
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: scrollbarButtonRightIcon,
|
||||
top: scrollbarDelta,
|
||||
left: undefined,
|
||||
bottom: undefined,
|
||||
right: arrowDelta,
|
||||
bgWidth: options.arrowSize,
|
||||
bgHeight: options.horizontalScrollbarSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, -1, 0)),
|
||||
});
|
||||
}
|
||||
|
||||
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
|
||||
}
|
||||
|
||||
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
|
||||
this.slider.setWidth(sliderSize);
|
||||
this.slider.setLeft(sliderPosition);
|
||||
}
|
||||
|
||||
protected _renderDomNode(largeSize: number, smallSize: number): void {
|
||||
this.domNode.setWidth(largeSize);
|
||||
this.domNode.setHeight(smallSize);
|
||||
this.domNode.setLeft(0);
|
||||
this.domNode.setBottom(0);
|
||||
}
|
||||
|
||||
public onDidScroll(e: ScrollEvent): boolean {
|
||||
this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
|
||||
this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
|
||||
this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _mouseDownRelativePosition(offsetX: number, offsetY: number): number {
|
||||
return offsetX;
|
||||
}
|
||||
|
||||
protected _sliderMousePosition(e: ISimplifiedMouseEvent): number {
|
||||
return e.posx;
|
||||
}
|
||||
|
||||
protected _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number {
|
||||
return e.posy;
|
||||
}
|
||||
|
||||
protected _updateScrollbarSize(size: number): void {
|
||||
this.slider.setHeight(size);
|
||||
}
|
||||
|
||||
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
|
||||
target.scrollLeft = scrollPosition;
|
||||
}
|
||||
}
|
||||
111
lib/vscode/src/vs/base/browser/ui/scrollbar/media/scrollbars.css
Normal file
111
lib/vscode/src/vs/base/browser/ui/scrollbar/media/scrollbars.css
Normal file
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Arrows */
|
||||
.monaco-scrollable-element > .scrollbar > .scra {
|
||||
cursor: pointer;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .visible {
|
||||
opacity: 1;
|
||||
|
||||
/* Background rule added for IE9 - to allow clicks on dom node */
|
||||
background:rgba(0,0,0,0);
|
||||
|
||||
transition: opacity 100ms linear;
|
||||
}
|
||||
.monaco-scrollable-element > .invisible {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.monaco-scrollable-element > .invisible.fade {
|
||||
transition: opacity 800ms linear;
|
||||
}
|
||||
|
||||
/* Scrollable Content Inset Shadow */
|
||||
.monaco-scrollable-element > .shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top {
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 3px;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
box-shadow: #DDD 0 6px 6px -6px inset;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.left {
|
||||
display: block;
|
||||
top: 3px;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
box-shadow: #DDD 6px 0 6px -6px inset;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top-left-corner {
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top.left {
|
||||
box-shadow: #DDD 6px 6px 6px -6px inset;
|
||||
}
|
||||
|
||||
/* ---------- Default Style ---------- */
|
||||
|
||||
.vs .monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: rgba(100, 100, 100, .4);
|
||||
}
|
||||
.vs-dark .monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: rgba(121, 121, 121, .4);
|
||||
}
|
||||
.hc-black .monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: rgba(111, 195, 223, .6);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider:hover {
|
||||
background: rgba(100, 100, 100, .7);
|
||||
}
|
||||
.hc-black .monaco-scrollable-element > .scrollbar > .slider:hover {
|
||||
background: rgba(111, 195, 223, .8);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: rgba(0, 0, 0, .6);
|
||||
}
|
||||
.vs-dark .monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: rgba(191, 191, 191, .4);
|
||||
}
|
||||
.hc-black .monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: rgba(111, 195, 223, 1);
|
||||
}
|
||||
|
||||
.vs-dark .monaco-scrollable-element .shadow.top {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-scrollable-element .shadow.left {
|
||||
box-shadow: #000 6px 0 6px -6px inset;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-scrollable-element .shadow.top.left {
|
||||
box-shadow: #000 6px 6px 6px -6px inset;
|
||||
}
|
||||
|
||||
.hc-black .monaco-scrollable-element .shadow.top {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hc-black .monaco-scrollable-element .shadow.left {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hc-black .monaco-scrollable-element .shadow.top.left {
|
||||
box-shadow: none;
|
||||
}
|
||||
629
lib/vscode/src/vs/base/browser/ui/scrollbar/scrollableElement.ts
Normal file
629
lib/vscode/src/vs/base/browser/ui/scrollbar/scrollableElement.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./media/scrollbars';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { IMouseEvent, StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
|
||||
import { HorizontalScrollbar } from 'vs/base/browser/ui/scrollbar/horizontalScrollbar';
|
||||
import { ScrollableElementChangeOptions, ScrollableElementCreationOptions, ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||
import { VerticalScrollbar } from 'vs/base/browser/ui/scrollbar/verticalScrollbar';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { TimeoutTimer } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { getZoomFactor } from 'vs/base/browser/browser';
|
||||
|
||||
const HIDE_TIMEOUT = 500;
|
||||
const SCROLL_WHEEL_SENSITIVITY = 50;
|
||||
const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
|
||||
|
||||
export interface IOverviewRulerLayoutInfo {
|
||||
parent: HTMLElement;
|
||||
insertBefore: HTMLElement;
|
||||
}
|
||||
|
||||
class MouseWheelClassifierItem {
|
||||
public timestamp: number;
|
||||
public deltaX: number;
|
||||
public deltaY: number;
|
||||
public score: number;
|
||||
|
||||
constructor(timestamp: number, deltaX: number, deltaY: number) {
|
||||
this.timestamp = timestamp;
|
||||
this.deltaX = deltaX;
|
||||
this.deltaY = deltaY;
|
||||
this.score = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseWheelClassifier {
|
||||
|
||||
public static readonly INSTANCE = new MouseWheelClassifier();
|
||||
|
||||
private readonly _capacity: number;
|
||||
private _memory: MouseWheelClassifierItem[];
|
||||
private _front: number;
|
||||
private _rear: number;
|
||||
|
||||
constructor() {
|
||||
this._capacity = 5;
|
||||
this._memory = [];
|
||||
this._front = -1;
|
||||
this._rear = -1;
|
||||
}
|
||||
|
||||
public isPhysicalMouseWheel(): boolean {
|
||||
if (this._front === -1 && this._rear === -1) {
|
||||
// no elements
|
||||
return false;
|
||||
}
|
||||
|
||||
// 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
|
||||
let remainingInfluence = 1;
|
||||
let score = 0;
|
||||
let iteration = 1;
|
||||
|
||||
let index = this._rear;
|
||||
do {
|
||||
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
|
||||
remainingInfluence -= influence;
|
||||
score += this._memory[index].score * influence;
|
||||
|
||||
if (index === this._front) {
|
||||
break;
|
||||
}
|
||||
|
||||
index = (this._capacity + index - 1) % this._capacity;
|
||||
iteration++;
|
||||
} while (true);
|
||||
|
||||
return (score <= 0.5);
|
||||
}
|
||||
|
||||
public accept(timestamp: number, deltaX: number, deltaY: number): void {
|
||||
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
|
||||
item.score = this._computeScore(item);
|
||||
|
||||
if (this._front === -1 && this._rear === -1) {
|
||||
this._memory[0] = item;
|
||||
this._front = 0;
|
||||
this._rear = 0;
|
||||
} else {
|
||||
this._rear = (this._rear + 1) % this._capacity;
|
||||
if (this._rear === this._front) {
|
||||
// Drop oldest
|
||||
this._front = (this._front + 1) % this._capacity;
|
||||
}
|
||||
this._memory[this._rear] = item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A score between 0 and 1 for `item`.
|
||||
* - a score towards 0 indicates that the source appears to be a physical mouse wheel
|
||||
* - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
|
||||
*/
|
||||
private _computeScore(item: MouseWheelClassifierItem): number {
|
||||
|
||||
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
|
||||
// both axes exercised => definitely not a physical mouse wheel
|
||||
return 1;
|
||||
}
|
||||
|
||||
let score: number = 0.5;
|
||||
const prev = (this._front === -1 && this._rear === -1 ? null : this._memory[this._rear]);
|
||||
if (prev) {
|
||||
// const deltaT = item.timestamp - prev.timestamp;
|
||||
// if (deltaT < 1000 / 30) {
|
||||
// // sooner than X times per second => indicator that this is not a physical mouse wheel
|
||||
// score += 0.25;
|
||||
// }
|
||||
|
||||
// if (item.deltaX === prev.deltaX && item.deltaY === prev.deltaY) {
|
||||
// // equal amplitude => indicator that this is a physical mouse wheel
|
||||
// score -= 0.25;
|
||||
// }
|
||||
}
|
||||
|
||||
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
|
||||
// non-integer deltas => indicator that this is not a physical mouse wheel
|
||||
score += 0.25;
|
||||
}
|
||||
|
||||
return Math.min(Math.max(score, 0), 1);
|
||||
}
|
||||
|
||||
private _isAlmostInt(value: number): boolean {
|
||||
const delta = Math.abs(Math.round(value) - value);
|
||||
return (delta < 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractScrollableElement extends Widget {
|
||||
|
||||
private readonly _options: ScrollableElementResolvedOptions;
|
||||
protected readonly _scrollable: Scrollable;
|
||||
private readonly _verticalScrollbar: VerticalScrollbar;
|
||||
private readonly _horizontalScrollbar: HorizontalScrollbar;
|
||||
private readonly _domNode: HTMLElement;
|
||||
|
||||
private readonly _leftShadowDomNode: FastDomNode<HTMLElement> | null;
|
||||
private readonly _topShadowDomNode: FastDomNode<HTMLElement> | null;
|
||||
private readonly _topLeftShadowDomNode: FastDomNode<HTMLElement> | null;
|
||||
|
||||
private readonly _listenOnDomNode: HTMLElement;
|
||||
|
||||
private _mouseWheelToDispose: IDisposable[];
|
||||
|
||||
private _isDragging: boolean;
|
||||
private _mouseIsOver: boolean;
|
||||
|
||||
private readonly _hideTimeout: TimeoutTimer;
|
||||
private _shouldRender: boolean;
|
||||
|
||||
private _revealOnScroll: boolean;
|
||||
|
||||
private readonly _onScroll = this._register(new Emitter<ScrollEvent>());
|
||||
public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
|
||||
|
||||
private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
|
||||
public readonly onWillScroll: Event<ScrollEvent> = this._onWillScroll.event;
|
||||
|
||||
protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
|
||||
super();
|
||||
element.style.overflow = 'hidden';
|
||||
this._options = resolveOptions(options);
|
||||
this._scrollable = scrollable;
|
||||
|
||||
this._register(this._scrollable.onScroll((e) => {
|
||||
this._onWillScroll.fire(e);
|
||||
this._onDidScroll(e);
|
||||
this._onScroll.fire(e);
|
||||
}));
|
||||
|
||||
let scrollbarHost: ScrollbarHost = {
|
||||
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
|
||||
onDragStart: () => this._onDragStart(),
|
||||
onDragEnd: () => this._onDragEnd(),
|
||||
};
|
||||
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
|
||||
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.style.position = 'relative';
|
||||
this._domNode.style.overflow = 'hidden';
|
||||
this._domNode.appendChild(element);
|
||||
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
|
||||
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
|
||||
|
||||
if (this._options.useShadows) {
|
||||
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._leftShadowDomNode.setClassName('shadow');
|
||||
this._domNode.appendChild(this._leftShadowDomNode.domNode);
|
||||
|
||||
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._topShadowDomNode.setClassName('shadow');
|
||||
this._domNode.appendChild(this._topShadowDomNode.domNode);
|
||||
|
||||
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._topLeftShadowDomNode.setClassName('shadow top-left-corner');
|
||||
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
|
||||
} else {
|
||||
this._leftShadowDomNode = null;
|
||||
this._topShadowDomNode = null;
|
||||
this._topLeftShadowDomNode = null;
|
||||
}
|
||||
|
||||
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
|
||||
|
||||
this._mouseWheelToDispose = [];
|
||||
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
||||
|
||||
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
|
||||
this.onnonbubblingmouseout(this._listenOnDomNode, (e) => this._onMouseOut(e));
|
||||
|
||||
this._hideTimeout = this._register(new TimeoutTimer());
|
||||
this._isDragging = false;
|
||||
this._mouseIsOver = false;
|
||||
|
||||
this._shouldRender = true;
|
||||
|
||||
this._revealOnScroll = true;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the generated 'scrollable' dom node
|
||||
*/
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
|
||||
return {
|
||||
parent: this._domNode,
|
||||
insertBefore: this._verticalScrollbar.domNode.domNode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate a mouse down event to the vertical scrollbar.
|
||||
* This is to help with clicking somewhere else and having the scrollbar react.
|
||||
*/
|
||||
public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void {
|
||||
this._verticalScrollbar.delegateMouseDown(browserEvent);
|
||||
}
|
||||
|
||||
public getScrollDimensions(): IScrollDimensions {
|
||||
return this._scrollable.getScrollDimensions();
|
||||
}
|
||||
|
||||
public setScrollDimensions(dimensions: INewScrollDimensions): void {
|
||||
this._scrollable.setScrollDimensions(dimensions, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the class name of the scrollable element.
|
||||
*/
|
||||
public updateClassName(newClassName: string): void {
|
||||
this._options.className = newClassName;
|
||||
// Defaults are different on Macs
|
||||
if (platform.isMacintosh) {
|
||||
this._options.className += ' mac';
|
||||
}
|
||||
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration options for the scrollbar.
|
||||
* Really this is Editor.IEditorScrollbarOptions, but base shouldn't
|
||||
* depend on Editor.
|
||||
*/
|
||||
public updateOptions(newOptions: ScrollableElementChangeOptions): void {
|
||||
if (typeof newOptions.handleMouseWheel !== 'undefined') {
|
||||
this._options.handleMouseWheel = newOptions.handleMouseWheel;
|
||||
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
||||
}
|
||||
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
|
||||
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
|
||||
}
|
||||
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
|
||||
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
|
||||
}
|
||||
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
|
||||
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
|
||||
}
|
||||
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
|
||||
this._horizontalScrollbar.updateScrollbarSize(newOptions.horizontalScrollbarSize);
|
||||
}
|
||||
|
||||
if (!this._options.lazyRender) {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
public setRevealOnScroll(value: boolean) {
|
||||
this._revealOnScroll = value;
|
||||
}
|
||||
|
||||
public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
|
||||
this._onMouseWheel(new StandardWheelEvent(browserEvent));
|
||||
}
|
||||
|
||||
// -------------------- mouse wheel scrolling --------------------
|
||||
|
||||
private _setListeningToMouseWheel(shouldListen: boolean): void {
|
||||
let isListening = (this._mouseWheelToDispose.length > 0);
|
||||
|
||||
if (isListening === shouldListen) {
|
||||
// No change
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop listening (if necessary)
|
||||
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
|
||||
|
||||
// Start listening (if necessary)
|
||||
if (shouldListen) {
|
||||
let onMouseWheel = (browserEvent: IMouseWheelEvent) => {
|
||||
this._onMouseWheel(new StandardWheelEvent(browserEvent));
|
||||
};
|
||||
|
||||
this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseWheel(e: StandardWheelEvent): void {
|
||||
|
||||
const classifier = MouseWheelClassifier.INSTANCE;
|
||||
if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
|
||||
const osZoomFactor = window.devicePixelRatio / getZoomFactor();
|
||||
if (platform.isWindows || platform.isLinux) {
|
||||
// On Windows and Linux, the incoming delta events are multiplied with the OS zoom factor.
|
||||
// The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
|
||||
classifier.accept(Date.now(), e.deltaX / osZoomFactor, e.deltaY / osZoomFactor);
|
||||
} else {
|
||||
classifier.accept(Date.now(), e.deltaX, e.deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
|
||||
|
||||
if (e.deltaY || e.deltaX) {
|
||||
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
|
||||
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
|
||||
|
||||
if (this._options.scrollPredominantAxis) {
|
||||
if (Math.abs(deltaY) >= Math.abs(deltaX)) {
|
||||
deltaX = 0;
|
||||
} else {
|
||||
deltaY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._options.flipAxes) {
|
||||
[deltaY, deltaX] = [deltaX, deltaY];
|
||||
}
|
||||
|
||||
// Convert vertical scrolling to horizontal if shift is held, this
|
||||
// is handled at a higher level on Mac
|
||||
const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
|
||||
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
|
||||
deltaX = deltaY;
|
||||
deltaY = 0;
|
||||
}
|
||||
|
||||
if (e.browserEvent && e.browserEvent.altKey) {
|
||||
// fastScrolling
|
||||
deltaX = deltaX * this._options.fastScrollSensitivity;
|
||||
deltaY = deltaY * this._options.fastScrollSensitivity;
|
||||
}
|
||||
|
||||
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
|
||||
|
||||
let desiredScrollPosition: INewScrollPosition = {};
|
||||
if (deltaY) {
|
||||
const desiredScrollTop = futureScrollPosition.scrollTop - SCROLL_WHEEL_SENSITIVITY * deltaY;
|
||||
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
|
||||
}
|
||||
if (deltaX) {
|
||||
const desiredScrollLeft = futureScrollPosition.scrollLeft - SCROLL_WHEEL_SENSITIVITY * deltaX;
|
||||
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
|
||||
}
|
||||
|
||||
// Check that we are scrolling towards a location which is valid
|
||||
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
|
||||
|
||||
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
|
||||
|
||||
const canPerformSmoothScroll = (
|
||||
SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED
|
||||
&& this._options.mouseWheelSmoothScroll
|
||||
&& classifier.isPhysicalMouseWheel()
|
||||
);
|
||||
|
||||
if (canPerformSmoothScroll) {
|
||||
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
|
||||
} else {
|
||||
this._scrollable.setScrollPositionNow(desiredScrollPosition);
|
||||
}
|
||||
this._shouldRender = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._options.alwaysConsumeMouseWheel || this._shouldRender) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _onDidScroll(e: ScrollEvent): void {
|
||||
this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
|
||||
this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
|
||||
|
||||
if (this._options.useShadows) {
|
||||
this._shouldRender = true;
|
||||
}
|
||||
|
||||
if (this._revealOnScroll) {
|
||||
this._reveal();
|
||||
}
|
||||
|
||||
if (!this._options.lazyRender) {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render / mutate the DOM now.
|
||||
* Should be used together with the ctor option `lazyRender`.
|
||||
*/
|
||||
public renderNow(): void {
|
||||
if (!this._options.lazyRender) {
|
||||
throw new Error('Please use `lazyRender` together with `renderNow`!');
|
||||
}
|
||||
|
||||
this._render();
|
||||
}
|
||||
|
||||
private _render(): void {
|
||||
if (!this._shouldRender) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._shouldRender = false;
|
||||
|
||||
this._horizontalScrollbar.render();
|
||||
this._verticalScrollbar.render();
|
||||
|
||||
if (this._options.useShadows) {
|
||||
const scrollState = this._scrollable.getCurrentScrollPosition();
|
||||
let enableTop = scrollState.scrollTop > 0;
|
||||
let 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' : ''));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- fade in / fade out --------------------
|
||||
|
||||
private _onDragStart(): void {
|
||||
this._isDragging = true;
|
||||
this._reveal();
|
||||
}
|
||||
|
||||
private _onDragEnd(): void {
|
||||
this._isDragging = false;
|
||||
this._hide();
|
||||
}
|
||||
|
||||
private _onMouseOut(e: IMouseEvent): void {
|
||||
this._mouseIsOver = false;
|
||||
this._hide();
|
||||
}
|
||||
|
||||
private _onMouseOver(e: IMouseEvent): void {
|
||||
this._mouseIsOver = true;
|
||||
this._reveal();
|
||||
}
|
||||
|
||||
private _reveal(): void {
|
||||
this._verticalScrollbar.beginReveal();
|
||||
this._horizontalScrollbar.beginReveal();
|
||||
this._scheduleHide();
|
||||
}
|
||||
|
||||
private _hide(): void {
|
||||
if (!this._mouseIsOver && !this._isDragging) {
|
||||
this._verticalScrollbar.beginHide();
|
||||
this._horizontalScrollbar.beginHide();
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleHide(): void {
|
||||
if (!this._mouseIsOver && !this._isDragging) {
|
||||
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ScrollableElement extends AbstractScrollableElement {
|
||||
|
||||
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
|
||||
options = options || {};
|
||||
options.mouseWheelSmoothScroll = false;
|
||||
const scrollable = new Scrollable(0, (callback) => dom.scheduleAtNextAnimationFrame(callback));
|
||||
super(element, options, scrollable);
|
||||
this._register(scrollable);
|
||||
}
|
||||
|
||||
public setScrollPosition(update: INewScrollPosition): void {
|
||||
this._scrollable.setScrollPositionNow(update);
|
||||
}
|
||||
|
||||
public getScrollPosition(): IScrollPosition {
|
||||
return this._scrollable.getCurrentScrollPosition();
|
||||
}
|
||||
}
|
||||
|
||||
export class SmoothScrollableElement extends AbstractScrollableElement {
|
||||
|
||||
constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
|
||||
super(element, options, scrollable);
|
||||
}
|
||||
|
||||
public setScrollPosition(update: INewScrollPosition): void {
|
||||
this._scrollable.setScrollPositionNow(update);
|
||||
}
|
||||
|
||||
public getScrollPosition(): IScrollPosition {
|
||||
return this._scrollable.getCurrentScrollPosition();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DomScrollableElement extends ScrollableElement {
|
||||
|
||||
private _element: HTMLElement;
|
||||
|
||||
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
|
||||
super(element, options);
|
||||
this._element = element;
|
||||
this.onScroll((e) => {
|
||||
if (e.scrollTopChanged) {
|
||||
this._element.scrollTop = e.scrollTop;
|
||||
}
|
||||
if (e.scrollLeftChanged) {
|
||||
this._element.scrollLeft = e.scrollLeft;
|
||||
}
|
||||
});
|
||||
this.scanDomNode();
|
||||
}
|
||||
|
||||
public scanDomNode(): void {
|
||||
// width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
|
||||
this.setScrollDimensions({
|
||||
width: this._element.clientWidth,
|
||||
scrollWidth: this._element.scrollWidth,
|
||||
height: this._element.clientHeight,
|
||||
scrollHeight: this._element.scrollHeight
|
||||
});
|
||||
this.setScrollPosition({
|
||||
scrollLeft: this._element.scrollLeft,
|
||||
scrollTop: this._element.scrollTop,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
|
||||
let result: ScrollableElementResolvedOptions = {
|
||||
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
|
||||
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
|
||||
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
|
||||
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
|
||||
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
|
||||
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
|
||||
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
|
||||
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
|
||||
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
|
||||
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
|
||||
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
|
||||
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
|
||||
|
||||
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
|
||||
|
||||
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : ScrollbarVisibility.Auto),
|
||||
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
|
||||
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
|
||||
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
|
||||
|
||||
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto),
|
||||
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
|
||||
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
|
||||
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0)
|
||||
};
|
||||
|
||||
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
|
||||
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
|
||||
|
||||
// Defaults are different on Macs
|
||||
if (platform.isMacintosh) {
|
||||
result.className += ' mac';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
|
||||
export interface ScrollableElementCreationOptions {
|
||||
/**
|
||||
* The scrollable element should not do any DOM mutations until renderNow() is called.
|
||||
* Defaults to false.
|
||||
*/
|
||||
lazyRender?: boolean;
|
||||
/**
|
||||
* CSS Class name for the scrollable element.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Drop subtle horizontal and vertical shadows.
|
||||
* Defaults to false.
|
||||
*/
|
||||
useShadows?: boolean;
|
||||
/**
|
||||
* Handle mouse wheel (listen to mouse wheel scrolling).
|
||||
* Defaults to true
|
||||
*/
|
||||
handleMouseWheel?: boolean;
|
||||
/**
|
||||
* If mouse wheel is handled, make mouse wheel scrolling smooth.
|
||||
* Defaults to true.
|
||||
*/
|
||||
mouseWheelSmoothScroll?: boolean;
|
||||
/**
|
||||
* Flip axes. Treat vertical scrolling like horizontal and vice-versa.
|
||||
* Defaults to false.
|
||||
*/
|
||||
flipAxes?: boolean;
|
||||
/**
|
||||
* If enabled, will scroll horizontally when scrolling vertical.
|
||||
* Defaults to false.
|
||||
*/
|
||||
scrollYToX?: boolean;
|
||||
/**
|
||||
* Always consume mouse wheel events, even when scrolling is no longer possible.
|
||||
* Defaults to false.
|
||||
*/
|
||||
alwaysConsumeMouseWheel?: boolean;
|
||||
/**
|
||||
* A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.
|
||||
* Defaults to 1.
|
||||
*/
|
||||
mouseWheelScrollSensitivity?: number;
|
||||
/**
|
||||
* FastScrolling mulitplier speed when pressing `Alt`
|
||||
* Defaults to 5.
|
||||
*/
|
||||
fastScrollSensitivity?: number;
|
||||
/**
|
||||
* Whether the scrollable will only scroll along the predominant axis when scrolling both
|
||||
* vertically and horizontally at the same time.
|
||||
* Prevents horizontal drift when scrolling vertically on a trackpad.
|
||||
* Defaults to true.
|
||||
*/
|
||||
scrollPredominantAxis?: boolean;
|
||||
/**
|
||||
* Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right).
|
||||
* Defaults to 11.
|
||||
*/
|
||||
arrowSize?: number;
|
||||
/**
|
||||
* The dom node events should be bound to.
|
||||
* If no listenOnDomNode is provided, the dom node passed to the constructor will be used for event listening.
|
||||
*/
|
||||
listenOnDomNode?: HTMLElement;
|
||||
/**
|
||||
* Control the visibility of the horizontal scrollbar.
|
||||
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
|
||||
* Defaults to 'auto'.
|
||||
*/
|
||||
horizontal?: ScrollbarVisibility;
|
||||
/**
|
||||
* Height (in px) of the horizontal scrollbar.
|
||||
* Defaults to 10.
|
||||
*/
|
||||
horizontalScrollbarSize?: number;
|
||||
/**
|
||||
* Height (in px) of the horizontal scrollbar slider.
|
||||
* Defaults to `horizontalScrollbarSize`
|
||||
*/
|
||||
horizontalSliderSize?: number;
|
||||
/**
|
||||
* Render arrows (left/right) for the horizontal scrollbar.
|
||||
* Defaults to false.
|
||||
*/
|
||||
horizontalHasArrows?: boolean;
|
||||
/**
|
||||
* Control the visibility of the vertical scrollbar.
|
||||
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
|
||||
* Defaults to 'auto'.
|
||||
*/
|
||||
vertical?: ScrollbarVisibility;
|
||||
/**
|
||||
* Width (in px) of the vertical scrollbar.
|
||||
* Defaults to 10.
|
||||
*/
|
||||
verticalScrollbarSize?: number;
|
||||
/**
|
||||
* Width (in px) of the vertical scrollbar slider.
|
||||
* Defaults to `verticalScrollbarSize`
|
||||
*/
|
||||
verticalSliderSize?: number;
|
||||
/**
|
||||
* Render arrows (top/bottom) for the vertical scrollbar.
|
||||
* Defaults to false.
|
||||
*/
|
||||
verticalHasArrows?: boolean;
|
||||
}
|
||||
|
||||
export interface ScrollableElementChangeOptions {
|
||||
handleMouseWheel?: boolean;
|
||||
mouseWheelScrollSensitivity?: number;
|
||||
fastScrollSensitivity?: number;
|
||||
scrollPredominantAxis?: boolean;
|
||||
horizontalScrollbarSize?: number;
|
||||
}
|
||||
|
||||
export interface ScrollableElementResolvedOptions {
|
||||
lazyRender: boolean;
|
||||
className: string;
|
||||
useShadows: boolean;
|
||||
handleMouseWheel: boolean;
|
||||
flipAxes: boolean;
|
||||
scrollYToX: boolean;
|
||||
alwaysConsumeMouseWheel: boolean;
|
||||
mouseWheelScrollSensitivity: number;
|
||||
fastScrollSensitivity: number;
|
||||
scrollPredominantAxis: boolean;
|
||||
mouseWheelSmoothScroll: boolean;
|
||||
arrowSize: number;
|
||||
listenOnDomNode: HTMLElement | null;
|
||||
horizontal: ScrollbarVisibility;
|
||||
horizontalScrollbarSize: number;
|
||||
horizontalSliderSize: number;
|
||||
horizontalHasArrows: boolean;
|
||||
vertical: ScrollbarVisibility;
|
||||
verticalScrollbarSize: number;
|
||||
verticalSliderSize: number;
|
||||
verticalHasArrows: boolean;
|
||||
}
|
||||
114
lib/vscode/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts
Normal file
114
lib/vscode/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
/**
|
||||
* The arrow image size.
|
||||
*/
|
||||
export const ARROW_IMG_SIZE = 11;
|
||||
|
||||
export interface ScrollbarArrowOptions {
|
||||
onActivate: () => void;
|
||||
className: string;
|
||||
icon: Codicon;
|
||||
|
||||
bgWidth: number;
|
||||
bgHeight: number;
|
||||
|
||||
top?: number;
|
||||
left?: number;
|
||||
bottom?: number;
|
||||
right?: number;
|
||||
}
|
||||
|
||||
export class ScrollbarArrow extends Widget {
|
||||
|
||||
private _onActivate: () => void;
|
||||
public bgDomNode: HTMLElement;
|
||||
public domNode: HTMLElement;
|
||||
private _mousedownRepeatTimer: IntervalTimer;
|
||||
private _mousedownScheduleRepeatTimer: TimeoutTimer;
|
||||
private _mouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
|
||||
|
||||
constructor(opts: ScrollbarArrowOptions) {
|
||||
super();
|
||||
this._onActivate = opts.onActivate;
|
||||
|
||||
this.bgDomNode = document.createElement('div');
|
||||
this.bgDomNode.className = 'arrow-background';
|
||||
this.bgDomNode.style.position = 'absolute';
|
||||
this.bgDomNode.style.width = opts.bgWidth + 'px';
|
||||
this.bgDomNode.style.height = opts.bgHeight + 'px';
|
||||
if (typeof opts.top !== 'undefined') {
|
||||
this.bgDomNode.style.top = '0px';
|
||||
}
|
||||
if (typeof opts.left !== 'undefined') {
|
||||
this.bgDomNode.style.left = '0px';
|
||||
}
|
||||
if (typeof opts.bottom !== 'undefined') {
|
||||
this.bgDomNode.style.bottom = '0px';
|
||||
}
|
||||
if (typeof opts.right !== 'undefined') {
|
||||
this.bgDomNode.style.right = '0px';
|
||||
}
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = opts.className;
|
||||
this.domNode.classList.add(...opts.icon.classNamesArray);
|
||||
|
||||
this.domNode.style.position = 'absolute';
|
||||
this.domNode.style.width = ARROW_IMG_SIZE + 'px';
|
||||
this.domNode.style.height = ARROW_IMG_SIZE + 'px';
|
||||
if (typeof opts.top !== 'undefined') {
|
||||
this.domNode.style.top = opts.top + 'px';
|
||||
}
|
||||
if (typeof opts.left !== 'undefined') {
|
||||
this.domNode.style.left = opts.left + 'px';
|
||||
}
|
||||
if (typeof opts.bottom !== 'undefined') {
|
||||
this.domNode.style.bottom = opts.bottom + 'px';
|
||||
}
|
||||
if (typeof opts.right !== 'undefined') {
|
||||
this.domNode.style.right = opts.right + 'px';
|
||||
}
|
||||
|
||||
this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>());
|
||||
this.onmousedown(this.bgDomNode, (e) => this._arrowMouseDown(e));
|
||||
this.onmousedown(this.domNode, (e) => this._arrowMouseDown(e));
|
||||
|
||||
this._mousedownRepeatTimer = this._register(new IntervalTimer());
|
||||
this._mousedownScheduleRepeatTimer = this._register(new TimeoutTimer());
|
||||
}
|
||||
|
||||
private _arrowMouseDown(e: IMouseEvent): void {
|
||||
let scheduleRepeater = () => {
|
||||
this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24);
|
||||
};
|
||||
|
||||
this._onActivate();
|
||||
this._mousedownRepeatTimer.cancel();
|
||||
this._mousedownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
|
||||
|
||||
this._mouseMoveMonitor.startMonitoring(
|
||||
e.target,
|
||||
e.buttons,
|
||||
standardMouseMoveMerger,
|
||||
(mouseMoveData: IStandardMouseMoveEventData) => {
|
||||
/* Intentional empty */
|
||||
},
|
||||
() => {
|
||||
this._mousedownRepeatTimer.cancel();
|
||||
this._mousedownScheduleRepeatTimer.cancel();
|
||||
}
|
||||
);
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
217
lib/vscode/src/vs/base/browser/ui/scrollbar/scrollbarState.ts
Normal file
217
lib/vscode/src/vs/base/browser/ui/scrollbar/scrollbarState.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.
|
||||
*/
|
||||
const MINIMUM_SLIDER_SIZE = 20;
|
||||
|
||||
export class ScrollbarState {
|
||||
|
||||
/**
|
||||
* For the vertical scrollbar: the width.
|
||||
* For the horizontal scrollbar: the height.
|
||||
*/
|
||||
private _scrollbarSize: number;
|
||||
|
||||
/**
|
||||
* For the vertical scrollbar: the height of the pair horizontal scrollbar.
|
||||
* For the horizontal scrollbar: the width of the pair vertical scrollbar.
|
||||
*/
|
||||
private readonly _oppositeScrollbarSize: number;
|
||||
|
||||
/**
|
||||
* For the vertical scrollbar: the height of the scrollbar's arrows.
|
||||
* For the horizontal scrollbar: the width of the scrollbar's arrows.
|
||||
*/
|
||||
private readonly _arrowSize: number;
|
||||
|
||||
// --- variables
|
||||
/**
|
||||
* For the vertical scrollbar: the viewport height.
|
||||
* For the horizontal scrollbar: the viewport width.
|
||||
*/
|
||||
private _visibleSize: number;
|
||||
|
||||
/**
|
||||
* For the vertical scrollbar: the scroll height.
|
||||
* For the horizontal scrollbar: the scroll width.
|
||||
*/
|
||||
private _scrollSize: number;
|
||||
|
||||
/**
|
||||
* For the vertical scrollbar: the scroll top.
|
||||
* For the horizontal scrollbar: the scroll left.
|
||||
*/
|
||||
private _scrollPosition: number;
|
||||
|
||||
// --- computed variables
|
||||
|
||||
/**
|
||||
* `visibleSize` - `oppositeScrollbarSize`
|
||||
*/
|
||||
private _computedAvailableSize: number;
|
||||
/**
|
||||
* (`scrollSize` > 0 && `scrollSize` > `visibleSize`)
|
||||
*/
|
||||
private _computedIsNeeded: boolean;
|
||||
|
||||
private _computedSliderSize: number;
|
||||
private _computedSliderRatio: number;
|
||||
private _computedSliderPosition: number;
|
||||
|
||||
constructor(arrowSize: number, scrollbarSize: number, oppositeScrollbarSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
|
||||
this._scrollbarSize = Math.round(scrollbarSize);
|
||||
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
|
||||
this._arrowSize = Math.round(arrowSize);
|
||||
|
||||
this._visibleSize = visibleSize;
|
||||
this._scrollSize = scrollSize;
|
||||
this._scrollPosition = scrollPosition;
|
||||
|
||||
this._computedAvailableSize = 0;
|
||||
this._computedIsNeeded = false;
|
||||
this._computedSliderSize = 0;
|
||||
this._computedSliderRatio = 0;
|
||||
this._computedSliderPosition = 0;
|
||||
|
||||
this._refreshComputedValues();
|
||||
}
|
||||
|
||||
public clone(): ScrollbarState {
|
||||
return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
|
||||
}
|
||||
|
||||
public setVisibleSize(visibleSize: number): boolean {
|
||||
let iVisibleSize = Math.round(visibleSize);
|
||||
if (this._visibleSize !== iVisibleSize) {
|
||||
this._visibleSize = iVisibleSize;
|
||||
this._refreshComputedValues();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public setScrollSize(scrollSize: number): boolean {
|
||||
let iScrollSize = Math.round(scrollSize);
|
||||
if (this._scrollSize !== iScrollSize) {
|
||||
this._scrollSize = iScrollSize;
|
||||
this._refreshComputedValues();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public setScrollPosition(scrollPosition: number): boolean {
|
||||
let iScrollPosition = Math.round(scrollPosition);
|
||||
if (this._scrollPosition !== iScrollPosition) {
|
||||
this._scrollPosition = iScrollPosition;
|
||||
this._refreshComputedValues();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public setScrollbarSize(scrollbarSize: number): void {
|
||||
this._scrollbarSize = scrollbarSize;
|
||||
}
|
||||
|
||||
private static _computeValues(oppositeScrollbarSize: number, arrowSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
|
||||
const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
|
||||
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
|
||||
const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
|
||||
|
||||
if (!computedIsNeeded) {
|
||||
// There is no need for a slider
|
||||
return {
|
||||
computedAvailableSize: Math.round(computedAvailableSize),
|
||||
computedIsNeeded: computedIsNeeded,
|
||||
computedSliderSize: Math.round(computedRepresentableSize),
|
||||
computedSliderRatio: 0,
|
||||
computedSliderPosition: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// We must artificially increase the size of the slider if needed, since the slider would be too small to grab with the mouse otherwise
|
||||
const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
|
||||
|
||||
// The slider can move from 0 to `computedRepresentableSize` - `computedSliderSize`
|
||||
// in the same way `scrollPosition` can move from 0 to `scrollSize` - `visibleSize`.
|
||||
const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
|
||||
const computedSliderPosition = (scrollPosition * computedSliderRatio);
|
||||
|
||||
return {
|
||||
computedAvailableSize: Math.round(computedAvailableSize),
|
||||
computedIsNeeded: computedIsNeeded,
|
||||
computedSliderSize: Math.round(computedSliderSize),
|
||||
computedSliderRatio: computedSliderRatio,
|
||||
computedSliderPosition: Math.round(computedSliderPosition),
|
||||
};
|
||||
}
|
||||
|
||||
private _refreshComputedValues(): void {
|
||||
const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
|
||||
this._computedAvailableSize = r.computedAvailableSize;
|
||||
this._computedIsNeeded = r.computedIsNeeded;
|
||||
this._computedSliderSize = r.computedSliderSize;
|
||||
this._computedSliderRatio = r.computedSliderRatio;
|
||||
this._computedSliderPosition = r.computedSliderPosition;
|
||||
}
|
||||
|
||||
public getArrowSize(): number {
|
||||
return this._arrowSize;
|
||||
}
|
||||
|
||||
public getScrollPosition(): number {
|
||||
return this._scrollPosition;
|
||||
}
|
||||
|
||||
public getRectangleLargeSize(): number {
|
||||
return this._computedAvailableSize;
|
||||
}
|
||||
|
||||
public getRectangleSmallSize(): number {
|
||||
return this._scrollbarSize;
|
||||
}
|
||||
|
||||
public isNeeded(): boolean {
|
||||
return this._computedIsNeeded;
|
||||
}
|
||||
|
||||
public getSliderSize(): number {
|
||||
return this._computedSliderSize;
|
||||
}
|
||||
|
||||
public getSliderPosition(): number {
|
||||
return this._computedSliderPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a desired `scrollPosition` such that `offset` ends up in the center of the slider.
|
||||
* `offset` is based on the same coordinate system as the `sliderPosition`.
|
||||
*/
|
||||
public getDesiredScrollPositionFromOffset(offset: number): number {
|
||||
if (!this._computedIsNeeded) {
|
||||
// no need for a slider
|
||||
return 0;
|
||||
}
|
||||
|
||||
let desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
|
||||
return Math.round(desiredSliderPosition / this._computedSliderRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
|
||||
*/
|
||||
public getDesiredScrollPositionFromDelta(delta: number): number {
|
||||
if (!this._computedIsNeeded) {
|
||||
// no need for a slider
|
||||
return 0;
|
||||
}
|
||||
|
||||
let desiredSliderPosition = this._computedSliderPosition + delta;
|
||||
return Math.round(desiredSliderPosition / this._computedSliderRatio);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { FastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { TimeoutTimer } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
|
||||
export class ScrollbarVisibilityController extends Disposable {
|
||||
private _visibility: ScrollbarVisibility;
|
||||
private _visibleClassName: string;
|
||||
private _invisibleClassName: string;
|
||||
private _domNode: FastDomNode<HTMLElement> | null;
|
||||
private _shouldBeVisible: boolean;
|
||||
private _isNeeded: boolean;
|
||||
private _isVisible: boolean;
|
||||
private _revealTimer: TimeoutTimer;
|
||||
|
||||
constructor(visibility: ScrollbarVisibility, visibleClassName: string, invisibleClassName: string) {
|
||||
super();
|
||||
this._visibility = visibility;
|
||||
this._visibleClassName = visibleClassName;
|
||||
this._invisibleClassName = invisibleClassName;
|
||||
this._domNode = null;
|
||||
this._isVisible = false;
|
||||
this._isNeeded = false;
|
||||
this._shouldBeVisible = false;
|
||||
this._revealTimer = this._register(new TimeoutTimer());
|
||||
}
|
||||
|
||||
// ----------------- Hide / Reveal
|
||||
|
||||
private applyVisibilitySetting(shouldBeVisible: boolean): boolean {
|
||||
if (this._visibility === ScrollbarVisibility.Hidden) {
|
||||
return false;
|
||||
}
|
||||
if (this._visibility === ScrollbarVisibility.Visible) {
|
||||
return true;
|
||||
}
|
||||
return shouldBeVisible;
|
||||
}
|
||||
|
||||
public setShouldBeVisible(rawShouldBeVisible: boolean): void {
|
||||
let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible);
|
||||
|
||||
if (this._shouldBeVisible !== shouldBeVisible) {
|
||||
this._shouldBeVisible = shouldBeVisible;
|
||||
this.ensureVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public setIsNeeded(isNeeded: boolean): void {
|
||||
if (this._isNeeded !== isNeeded) {
|
||||
this._isNeeded = isNeeded;
|
||||
this.ensureVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public setDomNode(domNode: FastDomNode<HTMLElement>): void {
|
||||
this._domNode = domNode;
|
||||
this._domNode.setClassName(this._invisibleClassName);
|
||||
|
||||
// Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration
|
||||
this.setShouldBeVisible(false);
|
||||
}
|
||||
|
||||
public ensureVisibility(): void {
|
||||
|
||||
if (!this._isNeeded) {
|
||||
// Nothing to be rendered
|
||||
this._hide(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._shouldBeVisible) {
|
||||
this._reveal();
|
||||
} else {
|
||||
this._hide(true);
|
||||
}
|
||||
}
|
||||
|
||||
private _reveal(): void {
|
||||
if (this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = true;
|
||||
|
||||
// The CSS animation doesn't play otherwise
|
||||
this._revealTimer.setIfNotSet(() => {
|
||||
if (this._domNode) {
|
||||
this._domNode.setClassName(this._visibleClassName);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _hide(withFadeAway: boolean): void {
|
||||
this._revealTimer.cancel();
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = false;
|
||||
if (this._domNode) {
|
||||
this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
109
lib/vscode/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts
Normal file
109
lib/vscode/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
|
||||
import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
|
||||
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
|
||||
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
|
||||
const scrollbarButtonUpIcon = registerIcon('scrollbar-button-up', Codicon.triangleUp);
|
||||
const scrollbarButtonDownIcon = registerIcon('scrollbar-button-down', Codicon.triangleDown);
|
||||
|
||||
export class VerticalScrollbar extends AbstractScrollbar {
|
||||
|
||||
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
|
||||
const scrollDimensions = scrollable.getScrollDimensions();
|
||||
const scrollPosition = scrollable.getCurrentScrollPosition();
|
||||
super({
|
||||
lazyRender: options.lazyRender,
|
||||
host: host,
|
||||
scrollbarState: new ScrollbarState(
|
||||
(options.verticalHasArrows ? options.arrowSize : 0),
|
||||
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
|
||||
// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
|
||||
0,
|
||||
scrollDimensions.height,
|
||||
scrollDimensions.scrollHeight,
|
||||
scrollPosition.scrollTop
|
||||
),
|
||||
visibility: options.vertical,
|
||||
extraScrollbarClassName: 'vertical',
|
||||
scrollable: scrollable
|
||||
});
|
||||
|
||||
if (options.verticalHasArrows) {
|
||||
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
|
||||
let scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
|
||||
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: scrollbarButtonUpIcon,
|
||||
top: arrowDelta,
|
||||
left: scrollbarDelta,
|
||||
bottom: undefined,
|
||||
right: undefined,
|
||||
bgWidth: options.verticalScrollbarSize,
|
||||
bgHeight: options.arrowSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, 1)),
|
||||
});
|
||||
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: scrollbarButtonDownIcon,
|
||||
top: undefined,
|
||||
left: scrollbarDelta,
|
||||
bottom: arrowDelta,
|
||||
right: undefined,
|
||||
bgWidth: options.verticalScrollbarSize,
|
||||
bgHeight: options.arrowSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, -1)),
|
||||
});
|
||||
}
|
||||
|
||||
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
|
||||
}
|
||||
|
||||
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
|
||||
this.slider.setHeight(sliderSize);
|
||||
this.slider.setTop(sliderPosition);
|
||||
}
|
||||
|
||||
protected _renderDomNode(largeSize: number, smallSize: number): void {
|
||||
this.domNode.setWidth(smallSize);
|
||||
this.domNode.setHeight(largeSize);
|
||||
this.domNode.setRight(0);
|
||||
this.domNode.setTop(0);
|
||||
}
|
||||
|
||||
public onDidScroll(e: ScrollEvent): boolean {
|
||||
this._shouldRender = this._onElementScrollSize(e.scrollHeight) || this._shouldRender;
|
||||
this._shouldRender = this._onElementScrollPosition(e.scrollTop) || this._shouldRender;
|
||||
this._shouldRender = this._onElementSize(e.height) || this._shouldRender;
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _mouseDownRelativePosition(offsetX: number, offsetY: number): number {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
protected _sliderMousePosition(e: ISimplifiedMouseEvent): number {
|
||||
return e.posy;
|
||||
}
|
||||
|
||||
protected _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number {
|
||||
return e.posx;
|
||||
}
|
||||
|
||||
protected _updateScrollbarSize(size: number): void {
|
||||
this.slider.setWidth(size);
|
||||
}
|
||||
|
||||
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
|
||||
target.scrollTop = scrollPosition;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user