├── prepare.js ├── src ├── typings │ ├── css.d.ts │ ├── thenable.d.ts │ └── titlebar.d.ts ├── index.ts ├── mnemonic.ts ├── accelerators.ts ├── api.ts ├── vs │ └── base │ │ ├── common │ │ ├── functional.ts │ │ ├── process.ts │ │ ├── uint.ts │ │ ├── sequence.ts │ │ ├── iterator.ts │ │ ├── linkedList.ts │ │ ├── network.ts │ │ ├── cancellation.ts │ │ ├── decorators.ts │ │ ├── errors.ts │ │ ├── platform.ts │ │ ├── lifecycle.ts │ │ ├── keyCodes.ts │ │ └── arrays.ts │ │ └── browser │ │ ├── __mocks__ │ │ └── dom.js │ │ ├── event.ts │ │ ├── canIUse.ts │ │ ├── iframe.ts │ │ ├── browser.ts │ │ ├── mouseEvent.ts │ │ ├── keyboardEvent.ts │ │ └── touch.ts ├── specs │ ├── index.spec.js │ └── menuitem.spec.js ├── themebar.ts └── menuitem.ts ├── __mocks__ ├── @treverix │ └── remote.js ├── navigatorMock.js └── electron.js ├── example ├── images │ └── icon.png ├── renderer.js ├── index.html ├── preload.js └── main.js ├── screenshots ├── window_1.png ├── window_2.png └── window_3.png ├── dev-example ├── images │ └── icon.png ├── renderer.js ├── index.html ├── preload.js └── main.js ├── .idea ├── vcs.xml ├── modules.xml ├── custom-electron-titlebar.iml └── misc.xml ├── README-MAC ├── jest.config.js ├── .babelrc ├── .npmignore ├── .github └── ISSUE_TEMPLATE │ └── issue-report.md ├── tsconfig.json ├── .gitignore ├── LICENSE ├── package.json ├── static └── theme │ ├── win.css │ ├── mac.css │ └── common.css └── README.md /prepare.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/typings/css.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const value: string; 3 | export = value 4 | } 5 | -------------------------------------------------------------------------------- /__mocks__/@treverix/remote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Menu: { 3 | getApplicationMenu: () => {} 4 | } 5 | } -------------------------------------------------------------------------------- /example/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treverix/custom-electron-titlebar/HEAD/example/images/icon.png -------------------------------------------------------------------------------- /screenshots/window_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treverix/custom-electron-titlebar/HEAD/screenshots/window_1.png -------------------------------------------------------------------------------- /screenshots/window_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treverix/custom-electron-titlebar/HEAD/screenshots/window_2.png -------------------------------------------------------------------------------- /screenshots/window_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treverix/custom-electron-titlebar/HEAD/screenshots/window_3.png -------------------------------------------------------------------------------- /dev-example/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Treverix/custom-electron-titlebar/HEAD/dev-example/images/icon.png -------------------------------------------------------------------------------- /__mocks__/navigatorMock.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(global, 'navigator', { 2 | writable: true, 3 | value: { 4 | userAgent: 'Chrome', 5 | } 6 | }) -------------------------------------------------------------------------------- /__mocks__/electron.js: -------------------------------------------------------------------------------- 1 | const browserWindow = { 2 | id: 42 3 | } 4 | 5 | module.exports = 6 | { 7 | remote: { 8 | getCurrentWindow: jest.fn(() => browserWindow) 9 | } 10 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/renderer.js: -------------------------------------------------------------------------------- 1 | // This file is required by the index.html file and will 2 | // be executed in the renderer process for that window. 3 | // No Node.js APIs are available in this process because 4 | // `nodeIntegration` is turned off. Use `preload.js` to 5 | // selectively enable features needed in the rendering 6 | // process. 7 | -------------------------------------------------------------------------------- /dev-example/renderer.js: -------------------------------------------------------------------------------- 1 | // This file is required by the index.html file and will 2 | // be executed in the renderer process for that window. 3 | // No Node.js APIs are available in this process because 4 | // `nodeIntegration` is turned off. Use `preload.js` to 5 | // selectively enable features needed in the rendering 6 | // process. 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README-MAC: -------------------------------------------------------------------------------- 1 | To maintain this library on MacOS 2 | 3 | 1. open platform.ts 4 | 2. uncomment lines 109, 110 - comment line 111, 112 5 | 6 | Reason: the title bar is not visible on MacOS by default. This hack makes it visible and so we can test and maintain it 7 | 8 | WARNING!! 9 | 10 | remove the change before releasing, clear the dist folder and rebuild. 11 | -------------------------------------------------------------------------------- /.idea/custom-electron-titlebar.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-babel', 3 | testEnvironment: 'jsdom', 4 | setupFilesAfterEnv: ['/prepare.js'], 5 | moduleNameMapper: { 6 | '^vs/(.*)$': '/src/vs/$1', 7 | '^static/(.*)$': '/static/$1' 8 | }, 9 | globals: { 10 | 'ts-jest': { 11 | babelConfig: true, 12 | } 13 | }, 14 | }; -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2018 Alex Torres 3 | * Licensed under the MIT License. See License in the project root for license information. 4 | *------------------------------------------------------------------------------------------*/ 5 | 6 | export * from './titlebar'; 7 | export * from './themebar'; 8 | export * from './vs/base/common/color'; 9 | export * from './vs/base/common/platform'; 10 | -------------------------------------------------------------------------------- /src/mnemonic.ts: -------------------------------------------------------------------------------- 1 | export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/; 2 | export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; 3 | 4 | export function cleanMnemonic(label: string): string { 5 | const regex = MENU_MNEMONIC_REGEX; 6 | 7 | const matches = regex.exec(label); 8 | if (!matches) { 9 | return label; 10 | } 11 | 12 | const mnemonicInText = !matches[1]; 13 | 14 | return label.replace(regex, mnemonicInText ? '$2$3' : '').trim(); 15 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "rewire", 4 | [ 5 | "module-resolver", 6 | { 7 | "root": [ 8 | "./dist" 9 | ], 10 | "alias": { 11 | "vs": "./dist/vs", 12 | "static": "./static" 13 | }, 14 | "extensions": [ 15 | ".js" 16 | ] 17 | } 18 | ], 19 | [ 20 | "import-require-as-string", 21 | { 22 | "extensions": [ 23 | ".css" 24 | ] 25 | } 26 | ] 27 | ] 28 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | 3 | # for WebStorm 4 | .idea/ 5 | 6 | # for npm 7 | node_modules/ 8 | npm-shrinkwrap.json 9 | 10 | # for grunt 11 | typings/ 12 | .tscache/ 13 | espowered/ 14 | 15 | # for https://github.com/Microsoft/TypeScript/issues/4667 16 | lib/**/*.ts 17 | !lib/**/*.d.ts 18 | 19 | # codes 20 | test/ 21 | 22 | # misc 23 | example/ 24 | screenshots/ 25 | dev-example/ 26 | src/ 27 | .gitignore 28 | .editorconfig 29 | circle.yml 30 | setup.sh 31 | Gruntfile.js 32 | tslint.json 33 | tsconfig.json 34 | 35 | npm-debug.log -------------------------------------------------------------------------------- /src/accelerators.ts: -------------------------------------------------------------------------------- 1 | import {Accelerator} from "electron"; 2 | import {isMacintosh} from "vs/base/common/platform"; 3 | 4 | export const convertAccelerator = (electronAccelerator: Accelerator): string => { 5 | let vsAccelerator; 6 | if (!isMacintosh) { 7 | vsAccelerator = electronAccelerator.replace(/(Cmd)|(Command)/gi, ''); 8 | } else { 9 | vsAccelerator = electronAccelerator.replace(/(Ctrl)|(Control)/gi, ''); 10 | } 11 | 12 | vsAccelerator = vsAccelerator.replace(/(Or)/gi, ''); 13 | 14 | return vsAccelerator; 15 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello World! 7 | 12 | 13 | 14 | 15 |

Hello World!

16 | We are using Node.js , 17 | Chromium , 18 | and Electron . 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /dev-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello World! 7 | 12 | 13 | 14 | 15 |

Hello World!

16 | We are using Node.js , 17 | Chromium , 18 | and Electron . 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import {Color} from "vs/base/common/color"; 2 | 3 | export interface IMenuItem { 4 | render(element: HTMLElement): void; 5 | 6 | isEnabled(): boolean; 7 | 8 | isSeparator(): boolean; 9 | 10 | focus(): void; 11 | 12 | blur(): void; 13 | 14 | dispose(): void; 15 | } 16 | 17 | export interface IMenuOptions { 18 | icon?: boolean; 19 | label?: boolean; 20 | ariaLabel?: string; 21 | enableMnemonics?: boolean; 22 | } 23 | 24 | export interface IMenuStyle { 25 | foregroundColor?: Color; 26 | backgroundColor?: Color; 27 | selectionForegroundColor?: Color; 28 | selectionBackgroundColor?: Color; 29 | separatorColor?: Color; 30 | } -------------------------------------------------------------------------------- /src/vs/base/common/functional.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function once(this: unknown, fn: T): T { 7 | const _this = this; 8 | let didCall = false; 9 | let result: unknown; 10 | 11 | return function () { 12 | if (didCall) { 13 | return result; 14 | } 15 | 16 | didCall = true; 17 | result = fn.apply(_this, arguments); 18 | 19 | return result; 20 | } as unknown as T; 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Electron version [e.g. 4.0.0] 29 | - Node Version [e.g. 2.2] 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ES2020", "DOM" 5 | ], 6 | "baseUrl": "./src", 7 | "paths": { 8 | "vs/*": [ 9 | "./vs/*" 10 | ], 11 | "static/*": [ 12 | "../static/*" 13 | ] 14 | }, 15 | "module": "commonjs", 16 | // "noEmit": true, 17 | "target": "es2019", 18 | "declaration": true, 19 | "outDir": "./dist", 20 | "moduleResolution": "node", 21 | "strict": true, 22 | "noImplicitAny": true, 23 | "esModuleInterop": true, 24 | "skipLibCheck": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "experimentalDecorators": true 27 | }, 28 | "include": [ 29 | "src/**/*" 30 | ], 31 | "exclude": [ 32 | "./node_modules", 33 | "**/node_modules/*" 34 | ] 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | coverage.* 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # IDE 38 | .idea/ 39 | 40 | lib/ 41 | dist/ 42 | *.map 43 | package-lock.json 44 | NOTES.md -------------------------------------------------------------------------------- /src/vs/base/browser/__mocks__/dom.js: -------------------------------------------------------------------------------- 1 | // import { 2 | // EventType, 3 | // addDisposableListener, 4 | // addClass, 5 | // removeClass, 6 | // removeNode, 7 | // append, 8 | // $, 9 | // hasClass, 10 | // EventHelper, 11 | // EventLike 12 | // } from "vs/base/browser/dom"; 13 | console.log('mocking...') 14 | module.exports = { 15 | EventType : {}, 16 | EventHelper : {}, 17 | EventLike : {}, 18 | addDisposableListener : jest.fn(), 19 | addClass : jest.fn(), 20 | removeClass : jest.fn(), 21 | removeNode : jest.fn(), 22 | append : jest.fn(), 23 | $ : jest.fn(), 24 | hasClass : jest.fn(), 25 | } 26 | 27 | 28 | export const addDisposableListener = jest.fn(); 29 | export const addClass = jest.fn(); 30 | export const removeClass = jest.fn(); 31 | export const removeNode = jest.fn(); 32 | export const append = jest.fn(); 33 | export const $ = jest.fn(); 34 | export const hasClass = jest.fn(); 35 | -------------------------------------------------------------------------------- /src/specs/index.spec.js: -------------------------------------------------------------------------------- 1 | const customElectronTitlebar = require('../index'); 2 | 3 | jest.mock('@treverix/remote'); 4 | 5 | describe('custom-electron-titlebar', () => { 6 | it('should export the Titlebar class', () => { 7 | expect (typeof customElectronTitlebar.Titlebar).toBe('function'); 8 | }) 9 | 10 | it('should export the Themebar class', () => { 11 | expect (typeof customElectronTitlebar.Themebar).toBe('function'); 12 | }) 13 | 14 | it('should export the RGBA class', () => { 15 | expect (typeof customElectronTitlebar.RGBA).toBe('function'); 16 | }) 17 | 18 | it('should export the HSLA class', () => { 19 | expect (typeof customElectronTitlebar.HSLA).toBe('function'); 20 | }) 21 | 22 | it('should export the HSVA class', () => { 23 | expect (typeof customElectronTitlebar.HSLA).toBe('function'); 24 | }) 25 | 26 | it('should export the Color class', () => { 27 | expect (typeof customElectronTitlebar.HSLA).toBe('function'); 28 | }) 29 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Torres, 2020 Andreas Dolk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/vs/base/common/process.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { isWindows, isMacintosh, setImmediate, IProcessEnvironment } from 'vs/base/common/platform'; 7 | 8 | interface IProcess { 9 | platform: string; 10 | env: IProcessEnvironment; 11 | 12 | cwd(): string; 13 | nextTick(callback: (...args: any[]) => void): void; 14 | } 15 | 16 | declare const process: IProcess; 17 | const safeProcess: IProcess = (typeof process === 'undefined') ? { 18 | cwd(): string { return '/'; }, 19 | env: Object.create(null), 20 | get platform(): string { return isWindows ? 'win32' : isMacintosh ? 'darwin' : 'linux'; }, 21 | nextTick(callback: (...args: any[]) => void): void { return setImmediate(callback); } 22 | } : process; 23 | 24 | export const cwd = safeProcess.cwd; 25 | export const env = safeProcess.env; 26 | export const platform = safeProcess.platform; 27 | export const nextTick = safeProcess.nextTick; 28 | -------------------------------------------------------------------------------- /src/typings/thenable.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise, 8 | * and others. This API makes no assumption about what promise library is being used which 9 | * enables reusing existing code without migrating to a specific promise implementation. Still, 10 | * we recommend the use of native promises which are available in VS Code. 11 | */ 12 | interface Thenable { 13 | /** 14 | * Attaches callbacks for the resolution and/or rejection of the Promise. 15 | * @param onfulfilled The callback to execute when the Promise is resolved. 16 | * @param onrejected The callback to execute when the Promise is rejected. 17 | * @returns A Promise for the completion of which ever callback is executed. 18 | */ 19 | then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; 20 | then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; 21 | } -------------------------------------------------------------------------------- /src/vs/base/browser/event.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Event as BaseEvent, Emitter } from 'vs/base/common/event'; 7 | 8 | export type EventHandler = HTMLElement | HTMLDocument | Window; 9 | 10 | export interface IDomEvent { 11 | (element: EventHandler, type: K, useCapture?: boolean): BaseEvent; 12 | (element: EventHandler, type: string, useCapture?: boolean): BaseEvent; 13 | } 14 | 15 | export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => { 16 | const fn = (e: Event) => emitter.fire(e); 17 | const emitter = new Emitter({ 18 | onFirstListenerAdd: () => { 19 | element.addEventListener(type, fn, useCapture); 20 | }, 21 | onLastListenerRemove: () => { 22 | element.removeEventListener(type, fn, useCapture); 23 | } 24 | }); 25 | 26 | return emitter.event; 27 | }; 28 | 29 | export interface CancellableEvent { 30 | preventDefault(): void; 31 | stopPropagation(): void; 32 | } 33 | 34 | export function stop(event: BaseEvent): BaseEvent { 35 | return BaseEvent.map(event, e => { 36 | e.preventDefault(); 37 | e.stopPropagation(); 38 | return e; 39 | }); 40 | } -------------------------------------------------------------------------------- /src/vs/base/common/uint.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export const enum Constants { 7 | /** 8 | * MAX SMI (SMall Integer) as defined in v8. 9 | * one bit is lost for boxing/unboxing flag. 10 | * one bit is lost for sign flag. 11 | * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values 12 | */ 13 | MAX_SAFE_SMALL_INTEGER = 1 << 30, 14 | 15 | /** 16 | * MIN SMI (SMall Integer) as defined in v8. 17 | * one bit is lost for boxing/unboxing flag. 18 | * one bit is lost for sign flag. 19 | * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values 20 | */ 21 | MIN_SAFE_SMALL_INTEGER = -(1 << 30), 22 | 23 | /** 24 | * Max unsigned integer that fits on 8 bits. 25 | */ 26 | MAX_UINT_8 = 255, // 2^8 - 1 27 | 28 | /** 29 | * Max unsigned integer that fits on 16 bits. 30 | */ 31 | MAX_UINT_16 = 65535, // 2^16 - 1 32 | 33 | /** 34 | * Max unsigned integer that fits on 32 bits. 35 | */ 36 | MAX_UINT_32 = 4294967295, // 2^32 - 1 37 | 38 | UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000 39 | } 40 | 41 | export function toUint8(v: number): number { 42 | if (v < 0) { 43 | return 0; 44 | } 45 | if (v > Constants.MAX_UINT_8) { 46 | return Constants.MAX_UINT_8; 47 | } 48 | return v | 0; 49 | } 50 | 51 | export function toUint32(v: number): number { 52 | if (v < 0) { 53 | return 0; 54 | } 55 | if (v > Constants.MAX_UINT_32) { 56 | return Constants.MAX_UINT_32; 57 | } 58 | return v | 0; 59 | } -------------------------------------------------------------------------------- /src/typings/titlebar.d.ts: -------------------------------------------------------------------------------- 1 | import {MenubarOptions} from "../menubar"; 2 | import {Color} from "vs/base/common/color"; 3 | import {Theme} from "../themebar"; 4 | 5 | interface TitlebarOptions extends MenubarOptions { 6 | /** 7 | * The background color of titlebar. 8 | */ 9 | backgroundColor: Color; 10 | /** 11 | * The icon shown on the left side of titlebar. 12 | */ 13 | icon?: string; 14 | /** 15 | * Style of the icons of titlebar. 16 | * You can create your custom style using [`Theme`](https://github.com/AlexTorresSk/custom-electron-titlebar/THEMES.md) 17 | */ 18 | iconsTheme: Theme; 19 | /** 20 | * The shadow color of titlebar. 21 | */ 22 | shadow: boolean; 23 | /** 24 | * Define if the minimize window button is displayed. 25 | * *The default is true* 26 | */ 27 | minimizable?: boolean; 28 | /** 29 | * Define if the maximize and restore window buttons are displayed. 30 | * *The default is true* 31 | */ 32 | maximizable?: boolean; 33 | /** 34 | * Define if the close window button is displayed. 35 | * *The default is true* 36 | */ 37 | closeable?: boolean; 38 | /** 39 | * When the close button is clicked, the window is hidden instead of closed. 40 | * *The default is false* 41 | */ 42 | hideWhenClickingClose: boolean; 43 | /** 44 | * Enables or disables the blur option in titlebar. 45 | * *The default is true* 46 | */ 47 | unfocusEffect?: boolean; 48 | /** 49 | * Set the order of the elements on the title bar. You can use `inverted`, `first-buttons` or don't add for. 50 | * *The default is normal* 51 | */ 52 | order?: "inverted" | "first-buttons"; 53 | /** 54 | * Set horizontal alignment of the window title. 55 | * *The default value is center* 56 | */ 57 | titleHorizontalAlignment: "left" | "center" | "right"; 58 | /** 59 | * Sets the value for the overflow of the window. 60 | * *The default value is auto* 61 | */ 62 | overflow: "auto" | "hidden" | "visible"; 63 | } 64 | -------------------------------------------------------------------------------- /src/vs/base/browser/canIUse.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as browser from 'vs/base/browser/browser'; 7 | import * as platform from 'vs/base/common/platform'; 8 | 9 | export const enum KeyboardSupport { 10 | Always, 11 | FullScreen, 12 | None 13 | } 14 | 15 | /** 16 | * Browser feature we can support in current platform, browser and environment. 17 | */ 18 | export const BrowserFeatures = { 19 | clipboard: { 20 | writeText: ( 21 | platform.isNative 22 | || (document.queryCommandSupported && document.queryCommandSupported('copy')) 23 | || !!(navigator && navigator.clipboard && navigator.clipboard.writeText) 24 | ), 25 | readText: ( 26 | platform.isNative 27 | || !!(navigator && navigator.clipboard && navigator.clipboard.readText) 28 | ), 29 | richText: (() => { 30 | if (browser.isEdge) { 31 | let index = navigator.userAgent.indexOf('Edge/'); 32 | let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10); 33 | 34 | if (!version || (version >= 12 && version <= 16)) { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | })() 41 | }, 42 | keyboard: (() => { 43 | if (platform.isNative || browser.isStandalone) { 44 | return KeyboardSupport.Always; 45 | } 46 | 47 | if ((navigator).keyboard || browser.isSafari) { 48 | return KeyboardSupport.FullScreen; 49 | } 50 | 51 | return KeyboardSupport.None; 52 | })(), 53 | 54 | // 'ontouchstart' in window always evaluates to true with typescript's modern typings. This causes `window` to be 55 | // `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast 56 | touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0, 57 | pointerEvents: window.PointerEvent && ('ontouchstart' in window || (window as Window).navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0) 58 | }; 59 | -------------------------------------------------------------------------------- /src/vs/base/common/sequence.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Event, Emitter } from 'vs/base/common/event'; 7 | import { IDisposable } from 'vs/base/common/lifecycle'; 8 | 9 | export interface ISplice { 10 | readonly start: number; 11 | readonly deleteCount: number; 12 | readonly toInsert: T[]; 13 | } 14 | 15 | export interface ISpliceable { 16 | splice(start: number, deleteCount: number, toInsert: T[]): void; 17 | } 18 | 19 | export interface ISequence { 20 | readonly elements: T[]; 21 | readonly onDidSplice: Event>; 22 | } 23 | 24 | export class Sequence implements ISequence, ISpliceable { 25 | 26 | readonly elements: T[] = []; 27 | 28 | private readonly _onDidSplice = new Emitter>(); 29 | readonly onDidSplice: Event> = this._onDidSplice.event; 30 | 31 | splice(start: number, deleteCount: number, toInsert: T[] = []): void { 32 | this.elements.splice(start, deleteCount, ...toInsert); 33 | this._onDidSplice.fire({ start, deleteCount, toInsert }); 34 | } 35 | } 36 | 37 | export class SimpleSequence implements ISequence { 38 | 39 | private _elements: T[]; 40 | get elements(): T[] { return this._elements; } 41 | 42 | readonly onDidSplice: Event>; 43 | private disposable: IDisposable; 44 | 45 | constructor(elements: T[], onDidAdd: Event, onDidRemove: Event) { 46 | this._elements = [...elements]; 47 | this.onDidSplice = Event.any( 48 | Event.map(onDidAdd, e => ({ start: this.elements.length, deleteCount: 0, toInsert: [e] })), 49 | Event.map(Event.filter(Event.map(onDidRemove, e => this.elements.indexOf(e)), i => i > -1), i => ({ start: i, deleteCount: 1, toInsert: [] })) 50 | ); 51 | 52 | this.disposable = this.onDidSplice(({ start, deleteCount, toInsert }) => this._elements.splice(start, deleteCount, ...toInsert)); 53 | } 54 | 55 | dispose(): void { 56 | this.disposable.dispose(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /dev-example/preload.js: -------------------------------------------------------------------------------- 1 | // All of the Node.js APIs are available in the preload process. 2 | // It has the same sandbox as a Chrome extension. 3 | 4 | const path = require('path'); 5 | const url = require('url'); 6 | const {isMacintosh} = require('../dist') 7 | const {Menu, MenuItem} = require('@treverix/remote') 8 | 9 | window.addEventListener('DOMContentLoaded', () => { 10 | 11 | // It does not make sense to use the custom titlebar on macOS where 12 | // it only tries to simulate what we get with the normal behavior anyway. 13 | if (!isMacintosh) { 14 | const customTitlebar = require('../dist'); // for local library development and testing only 15 | 16 | // add a menu 17 | const menu = new Menu(); 18 | menu.append(new MenuItem({ 19 | label: 'Item &1', 20 | click: () => console.log('Click on submenu'), 21 | submenu: [ 22 | { 23 | label: '&Subitem 1', 24 | click: () => console.log('Click on subitem 1') 25 | }, 26 | { 27 | type: 'separator' 28 | } 29 | ] 30 | })); 31 | 32 | menu.append(new MenuItem({ 33 | label: 'Item &2', 34 | submenu: [ 35 | { 36 | label: 'Subitem &checkbox', 37 | type: 'checkbox', 38 | checked: true 39 | }, 40 | { 41 | type: 'separator' 42 | }, 43 | { 44 | label: 'Subitem with &submenu', 45 | submenu: [ 46 | { 47 | label: 'Submenu &item 2', 48 | accelerator: 'Ctrl+T', 49 | click: () => console.log('Click on subitem 2') 50 | } 51 | ] 52 | } 53 | ] 54 | })); 55 | 56 | const titlebar = new customTitlebar.Titlebar({ 57 | backgroundColor: customTitlebar.Color.fromHex('#2f3241'), 58 | icon: url.format(path.join(__dirname, '/images', '/icon.png')), 59 | menu, 60 | }); 61 | 62 | titlebar.updateMenu(menu); 63 | } 64 | 65 | const replaceText = (selector, text) => { 66 | const element = document.getElementById(selector) 67 | if (element) element.innerText = text 68 | } 69 | 70 | for (const type of ['chrome', 'node', 'electron']) { 71 | replaceText(`${type}-version`, process.versions[type]) 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /example/preload.js: -------------------------------------------------------------------------------- 1 | // All of the Node.js APIs are available in the preload process. 2 | // It has the same sandbox as a Chrome extension. 3 | 4 | const path = require('path'); 5 | const url = require('url'); 6 | const {isMacintosh} = require('@treverix/custom-electron-titlebar') 7 | const {Menu, MenuItem} = require('@electron/remote') 8 | 9 | window.addEventListener('DOMContentLoaded', () => { 10 | 11 | // It does not make sense to use the custom titlebar on macOS where 12 | // it only tries to simulate what we get with the normal behavior anyway. 13 | if (!isMacintosh) { 14 | const customTitlebar = require('@treverix/custom-electron-titlebar'); 15 | 16 | // add a menu 17 | const menu = new Menu(); 18 | menu.append(new MenuItem({ 19 | label: 'Item &1', 20 | click: () => console.log('Click on submenu'), 21 | submenu: [ 22 | { 23 | label: '&Subitem 1', 24 | click: () => console.log('Click on subitem 1') 25 | }, 26 | { 27 | type: 'separator' 28 | } 29 | ] 30 | })); 31 | 32 | menu.append(new MenuItem({ 33 | label: 'Item &2', 34 | submenu: [ 35 | { 36 | label: 'Subitem &checkbox', 37 | type: 'checkbox', 38 | checked: true 39 | }, 40 | { 41 | type: 'separator' 42 | }, 43 | { 44 | label: 'Subitem with &submenu', 45 | submenu: [ 46 | { 47 | label: 'Submenu &item 2', 48 | accelerator: 'Ctrl+T', 49 | click: () => console.log('Click on subitem 2') 50 | } 51 | ] 52 | } 53 | ] 54 | })); 55 | 56 | const titlebar = new customTitlebar.Titlebar({ 57 | backgroundColor: customTitlebar.Color.fromHex('#2f3241'), 58 | icon: url.format(path.join(__dirname, '/images', '/icon.png')), 59 | menu, 60 | }); 61 | 62 | titlebar.updateMenu(menu); 63 | } 64 | 65 | const replaceText = (selector, text) => { 66 | const element = document.getElementById(selector) 67 | if (element) element.innerText = text 68 | } 69 | 70 | for (const type of ['chrome', 'node', 'electron']) { 71 | replaceText(`${type}-version`, process.versions[type]) 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@treverix/custom-electron-titlebar", 3 | "version": "4.2.0", 4 | "description": "Custom Electron Titlebar is a library for electron 10+ that allows you to configure a fully customizable title bar.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "prepublishOnly": "npm run build", 9 | "build:test": "tsc && babel ./dist --out-dir ./dist --extensions \".js\" & ncp ./src ./dist --filter=\"^.*\\.js$\"", 10 | "build": "tsc && babel ./dist --out-dir ./dist --extensions \".js\"", 11 | "dev-example": "electron dev-example/main.js", 12 | "example": "electron example/main.js", 13 | "start": "npm run build && npm run dev-example", 14 | "test": "jest" 15 | }, 16 | "author": "Andreas Dolk ", 17 | "contributors": [ 18 | "Alex Torres " 19 | ], 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/Treverix/custom-electron-titlebar.git" 24 | }, 25 | "keywords": [ 26 | "typescript", 27 | "electron", 28 | "titlebar", 29 | "menubar", 30 | "windows", 31 | "linux" 32 | ], 33 | "bugs": { 34 | "url": "https://github.com/Treverix/custom-electron-titlebar/issues" 35 | }, 36 | "homepage": "https://github.com/Treverix/custom-electron-titlebar#readme", 37 | "directories": { 38 | "example": "example", 39 | "lib": "lib" 40 | }, 41 | "dependencies": { 42 | "@electron/remote": "^2.0.1" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.15.7", 46 | "@babel/core": "^7.15.5", 47 | "@babel/plugin-proposal-class-properties": "^7.14.5", 48 | "@babel/plugin-proposal-object-rest-spread": "^7.15.6", 49 | "@babel/preset-env": "^7.15.6", 50 | "@babel/preset-typescript": "^7.15.0", 51 | "@types/jest": "^27.0.2", 52 | "@types/rewire": "^2.5.28", 53 | "babel-plugin-const-enum": "^1.1.0", 54 | "babel-plugin-import-require-as-string": "^1.0.2", 55 | "babel-plugin-module-resolver": "^4.1.0", 56 | "electron": "^14.0.2", 57 | "typescript": "^4.4.3", 58 | "babel-plugin-rewire": "^1.2.0", 59 | "jest": "^27.2.4", 60 | "ncp": "^2.0.0", 61 | "rewire": "^5.0.0", 62 | "ts-jest": "^27.0.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | // Modules to control application life and create native browser window 2 | const { app, BrowserWindow } = require('electron') 3 | const path = require('path') 4 | 5 | // initialize the remote module 6 | require('@electron/remote/main').initialize() 7 | 8 | // Keep a global reference of the window object, if you don't, the window will 9 | // be closed automatically when the JavaScript object is garbage collected. 10 | let mainWindow 11 | 12 | function createWindow() { 13 | // Create the browser window. 14 | mainWindow = new BrowserWindow({ 15 | width: 800, 16 | height: 600, 17 | // the custom title bar works best on frameless windows. But on macos, the custom titlebar is usually useless. 18 | frame: process.platform === 'darwin', 19 | webPreferences: { 20 | enableRemoteModule: true, 21 | preload: path.join(__dirname, 'preload.js'), 22 | nodeIntegration: true, 23 | } 24 | }) 25 | 26 | // and load the index.html of the app. 27 | mainWindow.loadFile('index.html') 28 | 29 | // Open the DevTools. 30 | // mainWindow.webContents.openDevTools() 31 | 32 | // Emitted when the window is closed. 33 | mainWindow.on('closed', function () { 34 | // Dereference the window object, usually you would store windows 35 | // in an array if your app supports multi windows, this is the time 36 | // when you should delete the corresponding element. 37 | mainWindow = null 38 | }) 39 | } 40 | 41 | // This method will be called when Electron has finished 42 | // initialization and is ready to create browser windows. 43 | // Some APIs can only be used after this event occurs. 44 | app.on('ready', createWindow) 45 | 46 | // Quit when all windows are closed. 47 | app.on('window-all-closed', function () { 48 | // On macOS it is common for applications and their menu bar 49 | // to stay active until the user quits explicitly with Cmd + Q 50 | if (process.platform !== 'darwin') app.quit() 51 | }) 52 | 53 | app.on('activate', function () { 54 | // On macOS it's common to re-create a window in the app when the 55 | // dock icon is clicked and there are no other windows open. 56 | if (mainWindow === null) createWindow() 57 | }) 58 | 59 | // In this file you can include the rest of your app's specific main process 60 | // code. You can also put them in separate files and require them here. 61 | -------------------------------------------------------------------------------- /dev-example/main.js: -------------------------------------------------------------------------------- 1 | // Modules to control application life and create native browser window 2 | const { app, BrowserWindow } = require('electron') 3 | const path = require('path') 4 | 5 | // initialize the remote module 6 | require('@treverix/remote/main').initialize() 7 | 8 | // Keep a global reference of the window object, if you don't, the window will 9 | // be closed automatically when the JavaScript object is garbage collected. 10 | let mainWindow 11 | 12 | function createWindow() { 13 | // Create the browser window. 14 | mainWindow = new BrowserWindow({ 15 | width: 800, 16 | height: 600, 17 | // the custom title bar works best on frameless windows. But on macos, the custom titlebar is usually useless. 18 | frame: process.platform === 'darwin', 19 | webPreferences: { 20 | enableRemoteModule: true, 21 | preload: path.join(__dirname, 'preload.js'), 22 | nodeIntegration: true, 23 | } 24 | }) 25 | 26 | // and load the index.html of the app. 27 | mainWindow.loadFile('index.html') 28 | 29 | // Open the DevTools. 30 | // mainWindow.webContents.openDevTools() 31 | 32 | // Emitted when the window is closed. 33 | mainWindow.on('closed', function () { 34 | // Dereference the window object, usually you would store windows 35 | // in an array if your app supports multi windows, this is the time 36 | // when you should delete the corresponding element. 37 | mainWindow = null 38 | }) 39 | } 40 | 41 | // This method will be called when Electron has finished 42 | // initialization and is ready to create browser windows. 43 | // Some APIs can only be used after this event occurs. 44 | app.on('ready', createWindow) 45 | 46 | // Quit when all windows are closed. 47 | app.on('window-all-closed', function () { 48 | // On macOS it is common for applications and their menu bar 49 | // to stay active until the user quits explicitly with Cmd + Q 50 | if (process.platform !== 'darwin') app.quit() 51 | }) 52 | 53 | app.on('activate', function () { 54 | // On macOS it's common to re-create a window in the app when the 55 | // dock icon is clicked and there are no other windows open. 56 | if (mainWindow === null) createWindow() 57 | }) 58 | 59 | // In this file you can include the rest of your app's specific main process 60 | // code. You can also put them in separate files and require them here. 61 | -------------------------------------------------------------------------------- /src/vs/base/common/iterator.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export namespace Iterable { 7 | 8 | export function is(thing: any): thing is IterableIterator { 9 | return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; 10 | } 11 | 12 | const _empty: Iterable = Object.freeze([]); 13 | export function empty(): Iterable { 14 | return _empty; 15 | } 16 | 17 | export function* single(element: T): Iterable { 18 | yield element; 19 | } 20 | 21 | export function from(iterable: Iterable | undefined | null): Iterable { 22 | return iterable || _empty; 23 | } 24 | 25 | export function first(iterable: Iterable): T | undefined { 26 | return iterable[Symbol.iterator]().next().value; 27 | } 28 | 29 | export function some(iterable: Iterable, predicate: (t: T) => boolean): boolean { 30 | for (const element of iterable) { 31 | if (predicate(element)) { 32 | return true; 33 | } 34 | } 35 | return false; 36 | } 37 | 38 | export function* filter(iterable: Iterable, predicate: (t: T) => boolean): Iterable { 39 | for (const element of iterable) { 40 | if (predicate(element)) { 41 | yield element; 42 | } 43 | } 44 | } 45 | 46 | export function* map(iterable: Iterable, fn: (t: T) => R): Iterable { 47 | for (const element of iterable) { 48 | yield fn(element); 49 | } 50 | } 51 | 52 | export function* concat(...iterables: Iterable[]): Iterable { 53 | for (const iterable of iterables) { 54 | for (const element of iterable) { 55 | yield element; 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Consumes `atMost` elements from iterable and returns the consumed elements, 62 | * and an iterable for the rest of the elements. 63 | */ 64 | export function consume(iterable: Iterable, atMost: number = Number.POSITIVE_INFINITY): [T[], Iterable] { 65 | const consumed: T[] = []; 66 | 67 | if (atMost === 0) { 68 | return [consumed, iterable]; 69 | } 70 | 71 | const iterator = iterable[Symbol.iterator](); 72 | 73 | for (let i = 0; i < atMost; i++) { 74 | const next = iterator.next(); 75 | 76 | if (next.done) { 77 | return [consumed, Iterable.empty()]; 78 | } 79 | 80 | consumed.push(next.value); 81 | } 82 | 83 | return [consumed, { [Symbol.iterator]() { return iterator; } }]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/themebar.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2018 Alex Torres 3 | * Licensed under the MIT License. See License in the project root for license information. 4 | * 5 | * This file has parts of one or more project files (VS Code) from Microsoft 6 | * You can check your respective license and the original file in https://github.com/Microsoft/vscode/ 7 | *-------------------------------------------------------------------------------------------------------*/ 8 | 9 | import {toDisposable, IDisposable, Disposable} from "vs/base/common/lifecycle"; 10 | const commonTheme: string = require('static/theme/common.css'); 11 | const macTheme: string = require('static/theme/mac.css'); 12 | const winTheme: string = require('static/theme/win.css'); 13 | 14 | class ThemingRegistry extends Disposable { 15 | private readonly theming: Theme[] = []; 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.theming = []; 21 | } 22 | 23 | protected onThemeChange(theme: Theme): IDisposable { 24 | this.theming.push(theme); 25 | return toDisposable(() => { 26 | const idx = this.theming.indexOf(theme); 27 | this.theming.splice(idx, 1); 28 | }); 29 | } 30 | 31 | protected getTheming(): Theme[] { 32 | return this.theming; 33 | } 34 | } 35 | 36 | export class Themebar extends ThemingRegistry { 37 | 38 | constructor() { 39 | super(); 40 | 41 | this.registerTheme((collector: CssStyle) => { 42 | collector.addRule(commonTheme); 43 | }); 44 | } 45 | 46 | protected registerTheme(theme: Theme) { 47 | this.onThemeChange(theme); 48 | 49 | let cssRules: string[] = []; 50 | let hasRule: { [rule: string]: boolean } = {}; 51 | let ruleCollector = { 52 | addRule: (rule: string) => { 53 | if (!hasRule[rule]) { 54 | cssRules.push(rule); 55 | hasRule[rule] = true; 56 | } 57 | } 58 | }; 59 | 60 | this.getTheming().forEach(p => p(ruleCollector)); 61 | 62 | _applyRules(cssRules.join('\n'), 'titlebar-style'); 63 | } 64 | 65 | static get win(): Theme { 66 | return ((collector: CssStyle) => { 67 | collector.addRule(winTheme); 68 | }); 69 | } 70 | 71 | static get mac(): Theme { 72 | return ((collector: CssStyle) => { 73 | collector.addRule(macTheme); 74 | }); 75 | } 76 | 77 | } 78 | 79 | export interface CssStyle { 80 | addRule(rule: string): void; 81 | } 82 | 83 | export interface Theme { 84 | (collector: CssStyle): void; 85 | } 86 | 87 | function _applyRules(styleSheetContent: string, rulesClassName: string) { 88 | let themeStyles = document.head.getElementsByClassName(rulesClassName); 89 | 90 | if (themeStyles.length === 0) { 91 | let styleElement = document.createElement('style'); 92 | styleElement.className = rulesClassName; 93 | styleElement.innerHTML = styleSheetContent; 94 | document.head.appendChild(styleElement); 95 | } else { 96 | (themeStyles[0]).innerHTML = styleSheetContent; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/vs/base/common/linkedList.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | class Node { 7 | 8 | static readonly Undefined = new Node(undefined); 9 | 10 | element: E; 11 | next: Node; 12 | prev: Node; 13 | 14 | constructor(element: E) { 15 | this.element = element; 16 | this.next = Node.Undefined; 17 | this.prev = Node.Undefined; 18 | } 19 | } 20 | 21 | export class LinkedList { 22 | 23 | private _first: Node = Node.Undefined; 24 | private _last: Node = Node.Undefined; 25 | private _size: number = 0; 26 | 27 | get size(): number { 28 | return this._size; 29 | } 30 | 31 | isEmpty(): boolean { 32 | return this._first === Node.Undefined; 33 | } 34 | 35 | clear(): void { 36 | this._first = Node.Undefined; 37 | this._last = Node.Undefined; 38 | this._size = 0; 39 | } 40 | 41 | unshift(element: E): () => void { 42 | return this._insert(element, false); 43 | } 44 | 45 | push(element: E): () => void { 46 | return this._insert(element, true); 47 | } 48 | 49 | private _insert(element: E, atTheEnd: boolean): () => void { 50 | const newNode = new Node(element); 51 | if (this._first === Node.Undefined) { 52 | this._first = newNode; 53 | this._last = newNode; 54 | 55 | } else if (atTheEnd) { 56 | // push 57 | const oldLast = this._last!; 58 | this._last = newNode; 59 | newNode.prev = oldLast; 60 | oldLast.next = newNode; 61 | 62 | } else { 63 | // unshift 64 | const oldFirst = this._first; 65 | this._first = newNode; 66 | newNode.next = oldFirst; 67 | oldFirst.prev = newNode; 68 | } 69 | this._size += 1; 70 | 71 | let didRemove = false; 72 | return () => { 73 | if (!didRemove) { 74 | didRemove = true; 75 | this._remove(newNode); 76 | } 77 | }; 78 | } 79 | 80 | shift(): E | undefined { 81 | if (this._first === Node.Undefined) { 82 | return undefined; 83 | } else { 84 | const res = this._first.element; 85 | this._remove(this._first); 86 | return res; 87 | } 88 | } 89 | 90 | pop(): E | undefined { 91 | if (this._last === Node.Undefined) { 92 | return undefined; 93 | } else { 94 | const res = this._last.element; 95 | this._remove(this._last); 96 | return res; 97 | } 98 | } 99 | 100 | private _remove(node: Node): void { 101 | if (node.prev !== Node.Undefined && node.next !== Node.Undefined) { 102 | // middle 103 | const anchor = node.prev; 104 | anchor.next = node.next; 105 | node.next.prev = anchor; 106 | 107 | } else if (node.prev === Node.Undefined && node.next === Node.Undefined) { 108 | // only node 109 | this._first = Node.Undefined; 110 | this._last = Node.Undefined; 111 | 112 | } else if (node.next === Node.Undefined) { 113 | // last 114 | this._last = this._last!.prev!; 115 | this._last.next = Node.Undefined; 116 | 117 | } else if (node.prev === Node.Undefined) { 118 | // first 119 | this._first = this._first!.next!; 120 | this._first.prev = Node.Undefined; 121 | } 122 | 123 | // done 124 | this._size -= 1; 125 | } 126 | 127 | *[Symbol.iterator](): Iterator { 128 | let node = this._first; 129 | while (node !== Node.Undefined) { 130 | yield node.element; 131 | node = node.next; 132 | } 133 | } 134 | 135 | toArray(): E[] { 136 | const result: E[] = []; 137 | for (let node = this._first; node !== Node.Undefined; node = node.next) { 138 | result.push(node.element); 139 | } 140 | return result; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /static/theme/win.css: -------------------------------------------------------------------------------- 1 | .titlebar .window-controls-container .window-icon-bg { 2 | display: inline-block; 3 | -webkit-app-region: no-drag; 4 | height: 100%; 5 | width: 46px; 6 | } 7 | 8 | .titlebar .window-controls-container .window-icon-bg .window-icon { 9 | height: 100%; 10 | width: 100%; 11 | -webkit-mask-size: 23.1%; 12 | mask-size: 23.1%; 13 | } 14 | 15 | .titlebar .window-controls-container .window-icon-bg .window-icon.window-close { 16 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 17 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 18 | } 19 | 20 | .titlebar .window-controls-container .window-icon-bg .window-icon.window-close:hover { 21 | background-color: #ffffff!important; 22 | } 23 | 24 | .titlebar .window-controls-container .window-icon-bg .window-icon.window-unmaximize { 25 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 8.798H8.798V11H0V2.202h2.202V0H11v8.798zm-3.298-5.5h-6.6v6.6h6.6v-6.6zM9.9 1.1H3.298v1.101h5.5v5.5h1.1v-6.6z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 26 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 8.798H8.798V11H0V2.202h2.202V0H11v8.798zm-3.298-5.5h-6.6v6.6h6.6v-6.6zM9.9 1.1H3.298v1.101h5.5v5.5h1.1v-6.6z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 27 | } 28 | 29 | .titlebar .window-controls-container .window-icon-bg .window-icon.window-maximize { 30 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 31 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 32 | } 33 | 34 | .titlebar .window-controls-container .window-icon-bg .window-icon.window-minimize { 35 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 36 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 37 | } 38 | 39 | .titlebar .window-controls-container .window-icon-bg.window-close-bg:hover { 40 | background-color: rgba(232, 17, 35, 0.9)!important; 41 | } 42 | 43 | .titlebar .window-controls-container .window-icon-bg.inactive { 44 | background-color: transparent!important; 45 | } 46 | 47 | .titlebar .window-controls-container .window-icon-bg.inactive .window-icon { 48 | opacity: .4; 49 | } 50 | 51 | .titlebar .window-controls-container .window-icon { 52 | background-color: #eeeeee; 53 | } 54 | 55 | .titlebar.light .window-controls-container .window-icon { 56 | background-color: #333333; 57 | } 58 | 59 | .titlebar.inactive .window-controls-container .window-icon { 60 | opacity: .7; 61 | } 62 | 63 | .titlebar .window-controls-container .window-icon-bg:not(.inactive):hover { 64 | background-color: rgba(255, 255, 255, .1); 65 | } 66 | 67 | .titlebar.light .window-controls-container .window-icon-bg:not(.inactive):hover { 68 | background-color: rgba(0, 0, 0, .1); 69 | } -------------------------------------------------------------------------------- /src/vs/base/common/network.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { URI } from 'vs/base/common/uri'; 7 | import * as platform from 'vs/base/common/platform'; 8 | 9 | export namespace Schemas { 10 | 11 | /** 12 | * A schema that is used for models that exist in memory 13 | * only and that have no correspondence on a server or such. 14 | */ 15 | export const inMemory = 'inmemory'; 16 | 17 | /** 18 | * A schema that is used for setting files 19 | */ 20 | export const vscode = 'vscode'; 21 | 22 | /** 23 | * A schema that is used for internal private files 24 | */ 25 | export const internal = 'private'; 26 | 27 | /** 28 | * A walk-through document. 29 | */ 30 | export const walkThrough = 'walkThrough'; 31 | 32 | /** 33 | * An embedded code snippet. 34 | */ 35 | export const walkThroughSnippet = 'walkThroughSnippet'; 36 | 37 | export const http = 'http'; 38 | 39 | export const https = 'https'; 40 | 41 | export const file = 'file'; 42 | 43 | export const mailto = 'mailto'; 44 | 45 | export const untitled = 'untitled'; 46 | 47 | export const data = 'data'; 48 | 49 | export const command = 'command'; 50 | 51 | export const vscodeRemote = 'vscode-remote'; 52 | 53 | export const vscodeRemoteResource = 'vscode-remote-resource'; 54 | 55 | export const userData = 'vscode-userdata'; 56 | 57 | export const vscodeCustomEditor = 'vscode-custom-editor'; 58 | 59 | export const vscodeNotebook = 'vscode-notebook'; 60 | 61 | export const vscodeNotebookCell = 'vscode-notebook-cell'; 62 | 63 | export const vscodeSettings = 'vscode-settings'; 64 | 65 | export const webviewPanel = 'webview-panel'; 66 | 67 | /** 68 | * Scheme used for loading the wrapper html and script in webviews. 69 | */ 70 | export const vscodeWebview = 'vscode-webview'; 71 | 72 | /** 73 | * Scheme used for loading resources inside of webviews. 74 | */ 75 | export const vscodeWebviewResource = 'vscode-webview-resource'; 76 | 77 | /** 78 | * Scheme used for extension pages 79 | */ 80 | export const extension = 'extension'; 81 | } 82 | 83 | class RemoteAuthoritiesImpl { 84 | private readonly _hosts: { [authority: string]: string | undefined; } = Object.create(null); 85 | private readonly _ports: { [authority: string]: number | undefined; } = Object.create(null); 86 | private readonly _connectionTokens: { [authority: string]: string | undefined; } = Object.create(null); 87 | private _preferredWebSchema: 'http' | 'https' = 'http'; 88 | private _delegate: ((uri: URI) => URI) | null = null; 89 | 90 | setPreferredWebSchema(schema: 'http' | 'https') { 91 | this._preferredWebSchema = schema; 92 | } 93 | 94 | setDelegate(delegate: (uri: URI) => URI): void { 95 | this._delegate = delegate; 96 | } 97 | 98 | set(authority: string, host: string, port: number): void { 99 | this._hosts[authority] = host; 100 | this._ports[authority] = port; 101 | } 102 | 103 | setConnectionToken(authority: string, connectionToken: string): void { 104 | this._connectionTokens[authority] = connectionToken; 105 | } 106 | 107 | rewrite(uri: URI): URI { 108 | if (this._delegate) { 109 | return this._delegate(uri); 110 | } 111 | const authority = uri.authority; 112 | let host = this._hosts[authority]; 113 | if (host && host.indexOf(':') !== -1) { 114 | host = `[${host}]`; 115 | } 116 | const port = this._ports[authority]; 117 | const connectionToken = this._connectionTokens[authority]; 118 | let query = `path=${encodeURIComponent(uri.path)}`; 119 | if (typeof connectionToken === 'string') { 120 | query += `&tkn=${encodeURIComponent(connectionToken)}`; 121 | } 122 | return URI.from({ 123 | scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, 124 | authority: `${host}:${port}`, 125 | path: `/vscode-remote-resource`, 126 | query 127 | }); 128 | } 129 | } 130 | 131 | export const RemoteAuthorities = new RemoteAuthoritiesImpl(); 132 | -------------------------------------------------------------------------------- /static/theme/mac.css: -------------------------------------------------------------------------------- 1 | .titlebar.cet-mac { 2 | padding: 0; 3 | } 4 | 5 | .titlebar .window-controls-container .window-icon-bg { 6 | display: inline-block; 7 | -webkit-app-region: no-drag; 8 | height: 12px; 9 | width: 12px; 10 | margin: 7.5px 4px; 11 | border-radius: 50%; 12 | overflow: hidden; 13 | } 14 | 15 | .titlebar .window-controls-container .window-icon-bg.inactive { 16 | background-color: #cdcdcd; 17 | } 18 | 19 | .titlebar .window-controls-container .window-icon-bg:not(.inactive) .window-icon { 20 | height: 100%; 21 | width: 100%; 22 | background-color: transparent; 23 | -webkit-mask-size: 100% !important; 24 | mask-size: 100% !important; 25 | } 26 | 27 | .titlebar .window-controls-container .window-icon-bg:not(.inactive) .window-icon:hover { 28 | background-color: rgba(0, 0, 0, 0.4); 29 | } 30 | 31 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-minimize-bg { 32 | background-color: #febc28; 33 | } 34 | 35 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-minimize-bg:hover { 36 | background-color: #feb30a; 37 | } 38 | 39 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-restore-bg { 40 | background-color: #01cc4e; 41 | } 42 | 43 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-restore-bg:hover { 44 | background-color: #01ae42; 45 | } 46 | 47 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-close-bg { 48 | background-color: #ff5b5d; 49 | } 50 | 51 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-close-bg:hover { 52 | background-color: #ff3c3f; 53 | } 54 | 55 | .titlebar .window-controls-container .window-icon-bg:not(.inactive).window-close-bg .window-close { 56 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 57 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 58 | } 59 | 60 | .titlebar .window-controls-container .window-icon-bg:not(.inactive) .window-maximize { 61 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.17,12L15,8.83L16.41,7.41L21,12L16.41,16.58L15,15.17L18.17,12M5.83,12L9,15.17L7.59,16.59L3,12L7.59,7.42L9,8.83L5.83,12Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 62 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.17,12L15,8.83L16.41,7.41L21,12L16.41,16.58L15,15.17L18.17,12M5.83,12L9,15.17L7.59,16.59L3,12L7.59,7.42L9,8.83L5.83,12Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 63 | transform: rotate(-45deg); 64 | } 65 | 66 | .titlebar .window-controls-container .window-icon-bg:not(.inactive) .window-unmaximize { 67 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.41,7.41L10,12L5.41,16.59L4,15.17L7.17,12L4,8.83L5.41,7.41M18.59,16.59L14,12L18.59,7.42L20,8.83L16.83,12L20,15.17L18.59,16.59Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 68 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.41,7.41L10,12L5.41,16.59L4,15.17L7.17,12L4,8.83L5.41,7.41M18.59,16.59L14,12L18.59,7.42L20,8.83L16.83,12L20,15.17L18.59,16.59Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 69 | transform: rotate(-45deg); 70 | } 71 | 72 | .titlebar .window-controls-container .window-icon-bg:not(.inactive) .window-minimize { 73 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19,13H5V11H19V13Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 74 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19,13H5V11H19V13Z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%; 75 | } -------------------------------------------------------------------------------- /src/vs/base/common/cancellation.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Emitter, Event } from 'vs/base/common/event'; 7 | import { IDisposable } from 'vs/base/common/lifecycle'; 8 | 9 | export interface CancellationToken { 10 | 11 | /** 12 | * A flag signalling is cancellation has been requested. 13 | */ 14 | readonly isCancellationRequested: boolean; 15 | 16 | /** 17 | * An event which fires when cancellation is requested. This event 18 | * only ever fires `once` as cancellation can only happen once. Listeners 19 | * that are registered after cancellation will be called (next event loop run), 20 | * but also only once. 21 | * 22 | * @event 23 | */ 24 | readonly onCancellationRequested: (listener: (e: any) => any, thisArgs?: any, disposables?: IDisposable[]) => IDisposable; 25 | } 26 | 27 | const shortcutEvent: Event = Object.freeze(function (callback, context?): IDisposable { 28 | const handle = setTimeout(callback.bind(context), 0); 29 | return { dispose() { clearTimeout(handle); } }; 30 | }); 31 | 32 | export namespace CancellationToken { 33 | 34 | export function isCancellationToken(thing: unknown): thing is CancellationToken { 35 | if (thing === CancellationToken.None || thing === CancellationToken.Cancelled) { 36 | return true; 37 | } 38 | if (thing instanceof MutableToken) { 39 | return true; 40 | } 41 | if (!thing || typeof thing !== 'object') { 42 | return false; 43 | } 44 | return typeof (thing as CancellationToken).isCancellationRequested === 'boolean' 45 | && typeof (thing as CancellationToken).onCancellationRequested === 'function'; 46 | } 47 | 48 | 49 | export const None: CancellationToken = Object.freeze({ 50 | isCancellationRequested: false, 51 | onCancellationRequested: Event.None 52 | }); 53 | 54 | export const Cancelled: CancellationToken = Object.freeze({ 55 | isCancellationRequested: true, 56 | onCancellationRequested: shortcutEvent 57 | }); 58 | } 59 | 60 | class MutableToken implements CancellationToken { 61 | 62 | private _isCancelled: boolean = false; 63 | private _emitter: Emitter | null = null; 64 | 65 | public cancel() { 66 | if (!this._isCancelled) { 67 | this._isCancelled = true; 68 | if (this._emitter) { 69 | this._emitter.fire(undefined); 70 | this.dispose(); 71 | } 72 | } 73 | } 74 | 75 | get isCancellationRequested(): boolean { 76 | return this._isCancelled; 77 | } 78 | 79 | get onCancellationRequested(): Event { 80 | if (this._isCancelled) { 81 | return shortcutEvent; 82 | } 83 | if (!this._emitter) { 84 | this._emitter = new Emitter(); 85 | } 86 | return this._emitter.event; 87 | } 88 | 89 | public dispose(): void { 90 | if (this._emitter) { 91 | this._emitter.dispose(); 92 | this._emitter = null; 93 | } 94 | } 95 | } 96 | 97 | export class CancellationTokenSource { 98 | 99 | private _token?: CancellationToken = undefined; 100 | private _parentListener?: IDisposable = undefined; 101 | 102 | constructor(parent?: CancellationToken) { 103 | this._parentListener = parent && parent.onCancellationRequested(this.cancel, this); 104 | } 105 | 106 | get token(): CancellationToken { 107 | if (!this._token) { 108 | // be lazy and create the token only when 109 | // actually needed 110 | this._token = new MutableToken(); 111 | } 112 | return this._token; 113 | } 114 | 115 | cancel(): void { 116 | if (!this._token) { 117 | // save an object by returning the default 118 | // cancelled token when cancellation happens 119 | // before someone asks for the token 120 | this._token = CancellationToken.Cancelled; 121 | 122 | } else if (this._token instanceof MutableToken) { 123 | // actually cancel 124 | this._token.cancel(); 125 | } 126 | } 127 | 128 | dispose(cancel: boolean = false): void { 129 | if (cancel) { 130 | this.cancel(); 131 | } 132 | if (this._parentListener) { 133 | this._parentListener.dispose(); 134 | } 135 | if (!this._token) { 136 | // ensure to initialize with an empty token if we had none 137 | this._token = CancellationToken.None; 138 | 139 | } else if (this._token instanceof MutableToken) { 140 | // actually dispose 141 | this._token.dispose(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/vs/base/browser/iframe.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * Represents a window in a possible chain of iframes 8 | */ 9 | export interface IWindowChainElement { 10 | /** 11 | * The window object for it 12 | */ 13 | window: Window; 14 | /** 15 | * The iframe element inside the window.parent corresponding to window 16 | */ 17 | iframeElement: HTMLIFrameElement | null; 18 | } 19 | 20 | let hasDifferentOriginAncestorFlag: boolean = false; 21 | let sameOriginWindowChainCache: IWindowChainElement[] | null = null; 22 | 23 | function getParentWindowIfSameOrigin(w: Window): Window | null { 24 | if (!w.parent || w.parent === w) { 25 | return null; 26 | } 27 | 28 | // Cannot really tell if we have access to the parent window unless we try to access something in it 29 | try { 30 | let location = w.location; 31 | let parentLocation = w.parent.location; 32 | if (location.protocol !== parentLocation.protocol || location.hostname !== parentLocation.hostname || location.port !== parentLocation.port) { 33 | hasDifferentOriginAncestorFlag = true; 34 | return null; 35 | } 36 | } catch (e) { 37 | hasDifferentOriginAncestorFlag = true; 38 | return null; 39 | } 40 | 41 | return w.parent; 42 | } 43 | 44 | function findIframeElementInParentWindow(parentWindow: Window, childWindow: Window): HTMLIFrameElement | null { 45 | let parentWindowIframes = parentWindow.document.getElementsByTagName('iframe'); 46 | let iframe: HTMLIFrameElement; 47 | for (let i = 0, len = parentWindowIframes.length; i < len; i++) { 48 | iframe = parentWindowIframes[i]; 49 | if (iframe.contentWindow === childWindow) { 50 | return iframe; 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | export class IframeUtils { 57 | 58 | /** 59 | * Returns a chain of embedded windows with the same origin (which can be accessed programmatically). 60 | * 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. 61 | * To distinguish if at one point the current execution environment is running inside a window with a different origin, see hasDifferentOriginAncestor() 62 | */ 63 | public static getSameOriginWindowChain(): IWindowChainElement[] { 64 | if (!sameOriginWindowChainCache) { 65 | sameOriginWindowChainCache = []; 66 | let w: Window | null = window; 67 | let parent: Window | null; 68 | do { 69 | parent = getParentWindowIfSameOrigin(w); 70 | if (parent) { 71 | sameOriginWindowChainCache.push({ 72 | window: w, 73 | iframeElement: findIframeElementInParentWindow(parent, w) 74 | }); 75 | } else { 76 | sameOriginWindowChainCache.push({ 77 | window: w, 78 | iframeElement: null 79 | }); 80 | } 81 | w = parent; 82 | } while (w); 83 | } 84 | return sameOriginWindowChainCache.slice(0); 85 | } 86 | 87 | /** 88 | * 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. 89 | * Returns false if the current execution environment is not running inside an iframe or if the entire chain of iframes have the same origin. 90 | */ 91 | public static hasDifferentOriginAncestor(): boolean { 92 | if (!sameOriginWindowChainCache) { 93 | this.getSameOriginWindowChain(); 94 | } 95 | return hasDifferentOriginAncestorFlag; 96 | } 97 | 98 | /** 99 | * Returns the position of `childWindow` relative to `ancestorWindow` 100 | */ 101 | public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) { 102 | 103 | if (!ancestorWindow || childWindow === ancestorWindow) { 104 | return { 105 | top: 0, 106 | left: 0 107 | }; 108 | } 109 | 110 | let top = 0, left = 0; 111 | 112 | let windowChain = this.getSameOriginWindowChain(); 113 | 114 | for (const windowChainEl of windowChain) { 115 | 116 | if (windowChainEl.window === ancestorWindow) { 117 | break; 118 | } 119 | 120 | if (!windowChainEl.iframeElement) { 121 | break; 122 | } 123 | 124 | let boundingRect = windowChainEl.iframeElement.getBoundingClientRect(); 125 | top += boundingRect.top; 126 | left += boundingRect.left; 127 | } 128 | 129 | return { 130 | top: top, 131 | left: left 132 | }; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/vs/base/browser/browser.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Emitter, Event } from 'vs/base/common/event'; 7 | import { IDisposable } from 'vs/base/common/lifecycle'; 8 | 9 | class WindowManager { 10 | 11 | public static readonly INSTANCE = new WindowManager(); 12 | 13 | // --- Zoom Level 14 | private _zoomLevel: number = 0; 15 | private _lastZoomLevelChangeTime: number = 0; 16 | private readonly _onDidChangeZoomLevel = new Emitter(); 17 | 18 | public readonly onDidChangeZoomLevel: Event = this._onDidChangeZoomLevel.event; 19 | public getZoomLevel(): number { 20 | return this._zoomLevel; 21 | } 22 | public getTimeSinceLastZoomLevelChanged(): number { 23 | return Date.now() - this._lastZoomLevelChangeTime; 24 | } 25 | public setZoomLevel(zoomLevel: number, isTrusted: boolean): void { 26 | if (this._zoomLevel === zoomLevel) { 27 | return; 28 | } 29 | 30 | this._zoomLevel = zoomLevel; 31 | // See https://github.com/Microsoft/vscode/issues/26151 32 | this._lastZoomLevelChangeTime = isTrusted ? 0 : Date.now(); 33 | this._onDidChangeZoomLevel.fire(this._zoomLevel); 34 | } 35 | 36 | // --- Zoom Factor 37 | private _zoomFactor: number = 1; 38 | 39 | public getZoomFactor(): number { 40 | return this._zoomFactor; 41 | } 42 | public setZoomFactor(zoomFactor: number): void { 43 | this._zoomFactor = zoomFactor; 44 | } 45 | 46 | // --- Pixel Ratio 47 | public getPixelRatio(): number { 48 | let ctx: any = document.createElement('canvas').getContext('2d'); 49 | let dpr = window.devicePixelRatio || 1; 50 | let bsr = ctx.webkitBackingStorePixelRatio || 51 | ctx.mozBackingStorePixelRatio || 52 | ctx.msBackingStorePixelRatio || 53 | ctx.oBackingStorePixelRatio || 54 | ctx.backingStorePixelRatio || 1; 55 | return dpr / bsr; 56 | } 57 | 58 | // --- Fullscreen 59 | private _fullscreen: boolean = false; 60 | private readonly _onDidChangeFullscreen = new Emitter(); 61 | 62 | public readonly onDidChangeFullscreen: Event = this._onDidChangeFullscreen.event; 63 | public setFullscreen(fullscreen: boolean): void { 64 | if (this._fullscreen === fullscreen) { 65 | return; 66 | } 67 | 68 | this._fullscreen = fullscreen; 69 | this._onDidChangeFullscreen.fire(); 70 | } 71 | public isFullscreen(): boolean { 72 | return this._fullscreen; 73 | } 74 | } 75 | 76 | /** A zoom index, e.g. 1, 2, 3 */ 77 | export function setZoomLevel(zoomLevel: number, isTrusted: boolean): void { 78 | WindowManager.INSTANCE.setZoomLevel(zoomLevel, isTrusted); 79 | } 80 | export function getZoomLevel(): number { 81 | return WindowManager.INSTANCE.getZoomLevel(); 82 | } 83 | /** Returns the time (in ms) since the zoom level was changed */ 84 | export function getTimeSinceLastZoomLevelChanged(): number { 85 | return WindowManager.INSTANCE.getTimeSinceLastZoomLevelChanged(); 86 | } 87 | export function onDidChangeZoomLevel(callback: (zoomLevel: number) => void): IDisposable { 88 | return WindowManager.INSTANCE.onDidChangeZoomLevel(callback); 89 | } 90 | 91 | /** The zoom scale for an index, e.g. 1, 1.2, 1.4 */ 92 | export function getZoomFactor(): number { 93 | return WindowManager.INSTANCE.getZoomFactor(); 94 | } 95 | export function setZoomFactor(zoomFactor: number): void { 96 | WindowManager.INSTANCE.setZoomFactor(zoomFactor); 97 | } 98 | 99 | export function getPixelRatio(): number { 100 | return WindowManager.INSTANCE.getPixelRatio(); 101 | } 102 | 103 | export function setFullscreen(fullscreen: boolean): void { 104 | WindowManager.INSTANCE.setFullscreen(fullscreen); 105 | } 106 | export function isFullscreen(): boolean { 107 | return WindowManager.INSTANCE.isFullscreen(); 108 | } 109 | export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen; 110 | 111 | const userAgent = navigator.userAgent; 112 | 113 | export const isEdge = (userAgent.indexOf('Edge/') >= 0); 114 | export const isOpera = (userAgent.indexOf('Opera') >= 0); 115 | export const isFirefox = (userAgent.indexOf('Firefox') >= 0); 116 | export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0); 117 | export const isChrome = (userAgent.indexOf('Chrome') >= 0); 118 | export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0)); 119 | export const isWebkitWebView = (!isChrome && !isSafari && isWebKit); 120 | export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0)); 121 | export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0); 122 | export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches); 123 | -------------------------------------------------------------------------------- /src/specs/menuitem.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | const {KeyCodeUtils} = require("../../dist/vs/base/common/keyCodes"); 5 | const {CETMenuItem} = require('../menuitem'); 6 | const {Color} = require('..'); 7 | 8 | describe('Menuitem creation tests', () => { 9 | 10 | describe('Constructor tests', () => { 11 | 12 | it('it stores the item internally', () => { 13 | const item = {label: 'foo'} 14 | const menuItem = new CETMenuItem(item); 15 | 16 | expect(menuItem.item).toEqual(item); 17 | }) 18 | 19 | it('it stores the options internally', () => { 20 | const options = {label: 'foo'} 21 | const menuItem = new CETMenuItem(undefined, options); 22 | 23 | expect(menuItem.options).toEqual(options); 24 | }) 25 | 26 | it('it stores the current browser window correctly', () => { 27 | const menuItem = new CETMenuItem(undefined); 28 | 29 | expect(menuItem.currentWindow.id).toEqual(42); // value, see __mock__/electron.js 30 | }) 31 | 32 | it('it stores the current browser window correctly', () => { 33 | const closeSubMenu = () => {} 34 | const menuItem = new CETMenuItem(undefined, undefined, closeSubMenu); 35 | 36 | expect(menuItem.closeSubMenu).toEqual(closeSubMenu); 37 | }) 38 | }) 39 | 40 | describe('Menuitem Mnemonic Tests', () => { 41 | 42 | it('it finds a mnemonic inside the label', () => { 43 | const menuItem = new CETMenuItem( 44 | { 45 | label: 'a&bc' 46 | }, 47 | { 48 | enableMnemonics: true 49 | } 50 | ); 51 | 52 | expect(menuItem.getMnemonic()).toEqual(KeyCodeUtils.fromString('b')); 53 | }) 54 | 55 | it('it finds a mnemonic at the beginning of the label', () => { 56 | const menuItem = new CETMenuItem( 57 | { 58 | label: '&abc' 59 | }, 60 | { 61 | enableMnemonics: true 62 | } 63 | ); 64 | 65 | expect(menuItem.getMnemonic()).toEqual(KeyCodeUtils.fromString('a')); 66 | }) 67 | 68 | it('it does not create a mnemonic if the feature is turned off', () => { 69 | const menuItem = new CETMenuItem( 70 | { 71 | label: '&abc' 72 | }, 73 | { 74 | enableMnemonics: false 75 | } 76 | ); 77 | 78 | expect(menuItem.getMnemonic()).toBeFalsy(); 79 | }) 80 | 81 | it('it does not create a mnemonic if there is no label', () => { 82 | const menuItem = new CETMenuItem( 83 | {}, 84 | { 85 | enableMnemonics: true 86 | } 87 | ); 88 | 89 | expect(menuItem.getMnemonic()).toBeFalsy(); 90 | }) 91 | 92 | }) 93 | 94 | describe('Menuitem styling Tests', () => { 95 | 96 | let menuItem; 97 | let hasFocusedClass = false; 98 | 99 | beforeEach(() => { 100 | menuItem = new CETMenuItem({}, {}, () => {}); 101 | menuItem.container = {classList: {contains: jest.fn(c => hasFocusedClass)}}; 102 | menuItem.checkElement = {style: {removeProperty: jest.fn(c => {})}}; 103 | menuItem.itemElement = {style: {removeProperty: jest.fn(c => {})}}; 104 | 105 | }) 106 | 107 | it('Stores the applied style internally', () => { 108 | const style = {}; 109 | menuItem.style(style); 110 | expect(menuItem.menuStyle).toEqual(style); 111 | }) 112 | 113 | it('Stores null or undefined styles but does not crash.', () => { 114 | const containsMock = menuItem.container.classList.contains.mock; 115 | menuItem.menuStyle = {}; 116 | const style = null; 117 | menuItem.style(style); 118 | 119 | expect(menuItem.menuStyle).toBeFalsy(); 120 | expect(containsMock.calls.length).toBe(0); 121 | }) 122 | 123 | it('Checks if the menuitem container has the style class "focused"', () => { 124 | menuItem.style({}); 125 | const containsMock = menuItem.container.classList.contains.mock; 126 | 127 | expect(containsMock.calls.length).toBe(1); 128 | expect(containsMock.calls[0][0]).toBe('focused'); 129 | }) 130 | 131 | it('Correctly sets the foreground color', () => { 132 | hasFocusedClass = true; 133 | menuItem.style({ 134 | selectionForegroundColor: undefined, 135 | foregroundColor: Color.black 136 | }); 137 | 138 | expect(menuItem.itemElement.style.color).toEqual(Color.black.toString()); 139 | expect(menuItem.checkElement.style.backgroundColor).toEqual(Color.black.toString()); 140 | }); 141 | 142 | it('Correctly sets the foreground color to selection foreground color', () => { 143 | hasFocusedClass = true; 144 | menuItem.style({ 145 | selectionForegroundColor: Color.red, 146 | foregroundColor: Color.black 147 | }); 148 | 149 | expect(menuItem.itemElement.style.color).toEqual(Color.red.toString()); 150 | expect(menuItem.checkElement.style.backgroundColor).toEqual(Color.red.toString()); 151 | }); 152 | 153 | it('Correctly sets the background color', () => { 154 | hasFocusedClass = true; 155 | menuItem.style({ 156 | selectionBackgroundColor: undefined, 157 | backgroundColor: Color.black 158 | }); 159 | 160 | expect(menuItem.itemElement.style.backgroundColor).toEqual(Color.black.toString()); 161 | }); 162 | 163 | it('Correctly sets the background color to selection background color', () => { 164 | hasFocusedClass = true; 165 | menuItem.style({ 166 | selectionBackgroundColor: Color.red, 167 | backgroundColor: Color.black 168 | }); 169 | 170 | expect(menuItem.itemElement.style.backgroundColor).toEqual(Color.red.toString()); 171 | }); 172 | 173 | it('Does not crash when itemElement or checkElement are falsy', () => { 174 | menuItem.itemElement = null; 175 | menuItem.checkElement = null; 176 | menuItem.style({}); 177 | }); 178 | 179 | }) 180 | }) 181 | 182 | -------------------------------------------------------------------------------- /src/vs/base/common/decorators.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function createDecorator(mapFn: (fn: Function, key: string) => Function): Function { 7 | return (target: any, key: string, descriptor: any) => { 8 | let fnKey: string | null = null; 9 | let fn: Function | null = null; 10 | 11 | if (typeof descriptor.value === 'function') { 12 | fnKey = 'value'; 13 | fn = descriptor.value; 14 | } else if (typeof descriptor.get === 'function') { 15 | fnKey = 'get'; 16 | fn = descriptor.get; 17 | } 18 | 19 | if (!fn) { 20 | throw new Error('not supported'); 21 | } 22 | 23 | descriptor[fnKey!] = mapFn(fn, key); 24 | }; 25 | } 26 | 27 | let memoizeId = 0; 28 | export function createMemoizer() { 29 | const memoizeKeyPrefix = `$memoize${memoizeId++}`; 30 | let self: any = undefined; 31 | 32 | const result = function memoize(target: any, key: string, descriptor: any) { 33 | let fnKey: string | null = null; 34 | let fn: Function | null = null; 35 | 36 | if (typeof descriptor.value === 'function') { 37 | fnKey = 'value'; 38 | fn = descriptor.value; 39 | 40 | if (fn!.length !== 0) { 41 | console.warn('Memoize should only be used in functions with zero parameters'); 42 | } 43 | } else if (typeof descriptor.get === 'function') { 44 | fnKey = 'get'; 45 | fn = descriptor.get; 46 | } 47 | 48 | if (!fn) { 49 | throw new Error('not supported'); 50 | } 51 | 52 | const memoizeKey = `${memoizeKeyPrefix}:${key}`; 53 | descriptor[fnKey!] = function (...args: any[]) { 54 | self = this; 55 | 56 | if (!this.hasOwnProperty(memoizeKey)) { 57 | Object.defineProperty(this, memoizeKey, { 58 | configurable: true, 59 | enumerable: false, 60 | writable: true, 61 | value: fn!.apply(this, args) 62 | }); 63 | } 64 | 65 | return this[memoizeKey]; 66 | }; 67 | }; 68 | 69 | result.clear = () => { 70 | if (typeof self === 'undefined') { 71 | return; 72 | } 73 | Object.getOwnPropertyNames(self).forEach(property => { 74 | if (property.indexOf(memoizeKeyPrefix) === 0) { 75 | delete self[property]; 76 | } 77 | }); 78 | }; 79 | 80 | return result; 81 | } 82 | 83 | export function memoize(target: any, key: string, descriptor: any) { 84 | return createMemoizer()(target, key, descriptor); 85 | } 86 | 87 | export interface IDebounceReducer { 88 | (previousValue: T, ...args: any[]): T; 89 | } 90 | 91 | export function debounce(delay: number, reducer?: IDebounceReducer, initialValueProvider?: () => T): Function { 92 | return createDecorator((fn, key) => { 93 | const timerKey = `$debounce$${key}`; 94 | const resultKey = `$debounce$result$${key}`; 95 | 96 | return function (this: any, ...args: any[]) { 97 | if (!this[resultKey]) { 98 | this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; 99 | } 100 | 101 | clearTimeout(this[timerKey]); 102 | 103 | if (reducer) { 104 | this[resultKey] = reducer(this[resultKey], ...args); 105 | args = [this[resultKey]]; 106 | } 107 | 108 | this[timerKey] = setTimeout(() => { 109 | fn.apply(this, args); 110 | this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; 111 | }, delay); 112 | }; 113 | }); 114 | } 115 | 116 | export function throttle(delay: number, reducer?: IDebounceReducer, initialValueProvider?: () => T): Function { 117 | return createDecorator((fn, key) => { 118 | const timerKey = `$throttle$timer$${key}`; 119 | const resultKey = `$throttle$result$${key}`; 120 | const lastRunKey = `$throttle$lastRun$${key}`; 121 | const pendingKey = `$throttle$pending$${key}`; 122 | 123 | return function (this: any, ...args: any[]) { 124 | if (!this[resultKey]) { 125 | this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; 126 | } 127 | if (this[lastRunKey] === null || this[lastRunKey] === undefined) { 128 | this[lastRunKey] = -Number.MAX_VALUE; 129 | } 130 | 131 | if (reducer) { 132 | this[resultKey] = reducer(this[resultKey], ...args); 133 | } 134 | 135 | if (this[pendingKey]) { 136 | return; 137 | } 138 | 139 | const nextTime = this[lastRunKey] + delay; 140 | if (nextTime <= Date.now()) { 141 | this[lastRunKey] = Date.now(); 142 | fn.apply(this, [this[resultKey]]); 143 | this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; 144 | } else { 145 | this[pendingKey] = true; 146 | this[timerKey] = setTimeout(() => { 147 | this[pendingKey] = false; 148 | this[lastRunKey] = Date.now(); 149 | fn.apply(this, [this[resultKey]]); 150 | this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; 151 | }, nextTime - Date.now()); 152 | } 153 | }; 154 | }); 155 | } -------------------------------------------------------------------------------- /src/vs/base/common/errors.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface ErrorListenerCallback { 7 | (error: any): void; 8 | } 9 | 10 | export interface ErrorListenerUnbind { 11 | (): void; 12 | } 13 | 14 | // Avoid circular dependency on EventEmitter by implementing a subset of the interface. 15 | export class ErrorHandler { 16 | private unexpectedErrorHandler: (e: any) => void; 17 | private listeners: ErrorListenerCallback[]; 18 | 19 | constructor() { 20 | 21 | this.listeners = []; 22 | 23 | this.unexpectedErrorHandler = function (e: any) { 24 | setTimeout(() => { 25 | if (e.stack) { 26 | throw new Error(e.message + '\n\n' + e.stack); 27 | } 28 | 29 | throw e; 30 | }, 0); 31 | }; 32 | } 33 | 34 | addListener(listener: ErrorListenerCallback): ErrorListenerUnbind { 35 | this.listeners.push(listener); 36 | 37 | return () => { 38 | this._removeListener(listener); 39 | }; 40 | } 41 | 42 | private emit(e: any): void { 43 | this.listeners.forEach((listener) => { 44 | listener(e); 45 | }); 46 | } 47 | 48 | private _removeListener(listener: ErrorListenerCallback): void { 49 | this.listeners.splice(this.listeners.indexOf(listener), 1); 50 | } 51 | 52 | setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void { 53 | this.unexpectedErrorHandler = newUnexpectedErrorHandler; 54 | } 55 | 56 | getUnexpectedErrorHandler(): (e: any) => void { 57 | return this.unexpectedErrorHandler; 58 | } 59 | 60 | onUnexpectedError(e: any): void { 61 | this.unexpectedErrorHandler(e); 62 | this.emit(e); 63 | } 64 | 65 | // For external errors, we don't want the listeners to be called 66 | onUnexpectedExternalError(e: any): void { 67 | this.unexpectedErrorHandler(e); 68 | } 69 | } 70 | 71 | export const errorHandler = new ErrorHandler(); 72 | 73 | export function setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void { 74 | errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler); 75 | } 76 | 77 | export function onUnexpectedError(e: any): undefined { 78 | // ignore errors from cancelled promises 79 | if (!isPromiseCanceledError(e)) { 80 | errorHandler.onUnexpectedError(e); 81 | } 82 | return undefined; 83 | } 84 | 85 | export function onUnexpectedExternalError(e: any): undefined { 86 | // ignore errors from cancelled promises 87 | if (!isPromiseCanceledError(e)) { 88 | errorHandler.onUnexpectedExternalError(e); 89 | } 90 | return undefined; 91 | } 92 | 93 | export interface SerializedError { 94 | readonly $isError: true; 95 | readonly name: string; 96 | readonly message: string; 97 | readonly stack: string; 98 | } 99 | 100 | export function transformErrorForSerialization(error: Error): SerializedError; 101 | export function transformErrorForSerialization(error: any): any; 102 | export function transformErrorForSerialization(error: any): any { 103 | if (error instanceof Error) { 104 | let { name, message } = error; 105 | const stack: string = (error).stacktrace || (error).stack; 106 | return { 107 | $isError: true, 108 | name, 109 | message, 110 | stack 111 | }; 112 | } 113 | 114 | // return as is 115 | return error; 116 | } 117 | 118 | // see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces 119 | export interface V8CallSite { 120 | getThis(): any; 121 | getTypeName(): string; 122 | getFunction(): string; 123 | getFunctionName(): string; 124 | getMethodName(): string; 125 | getFileName(): string; 126 | getLineNumber(): number; 127 | getColumnNumber(): number; 128 | getEvalOrigin(): string; 129 | isToplevel(): boolean; 130 | isEval(): boolean; 131 | isNative(): boolean; 132 | isConstructor(): boolean; 133 | toString(): string; 134 | } 135 | 136 | const canceledName = 'Canceled'; 137 | 138 | /** 139 | * Checks if the given error is a promise in canceled state 140 | */ 141 | export function isPromiseCanceledError(error: any): boolean { 142 | return error instanceof Error && error.name === canceledName && error.message === canceledName; 143 | } 144 | 145 | /** 146 | * Returns an error that signals cancellation. 147 | */ 148 | export function canceled(): Error { 149 | const error = new Error(canceledName); 150 | error.name = error.message; 151 | return error; 152 | } 153 | 154 | export function illegalArgument(name?: string): Error { 155 | if (name) { 156 | return new Error(`Illegal argument: ${name}`); 157 | } else { 158 | return new Error('Illegal argument'); 159 | } 160 | } 161 | 162 | export function illegalState(name?: string): Error { 163 | if (name) { 164 | return new Error(`Illegal state: ${name}`); 165 | } else { 166 | return new Error('Illegal state'); 167 | } 168 | } 169 | 170 | export function readonly(name?: string): Error { 171 | return name 172 | ? new Error(`readonly property '${name} cannot be changed'`) 173 | : new Error('readonly property cannot be changed'); 174 | } 175 | 176 | export function disposed(what: string): Error { 177 | const result = new Error(`${what} has been disposed`); 178 | result.name = 'DISPOSED'; 179 | return result; 180 | } 181 | 182 | export function getErrorMessage(err: any): string { 183 | if (!err) { 184 | return 'Error'; 185 | } 186 | 187 | if (err.message) { 188 | return err.message; 189 | } 190 | 191 | if (err.stack) { 192 | return err.stack.split('\n')[0]; 193 | } 194 | 195 | return String(err); 196 | } 197 | 198 | export class NotImplementedError extends Error { 199 | constructor(message?: string) { 200 | super('NotImplemented'); 201 | if (message) { 202 | this.message = message; 203 | } 204 | } 205 | } 206 | 207 | export class NotSupportedError extends Error { 208 | constructor(message?: string) { 209 | super('NotSupported'); 210 | if (message) { 211 | this.message = message; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/vs/base/browser/mouseEvent.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as browser from 'vs/base/browser/browser'; 7 | import { IframeUtils } from 'vs/base/browser/iframe'; 8 | import * as platform from 'vs/base/common/platform'; 9 | 10 | export interface IMouseEvent { 11 | readonly browserEvent: MouseEvent; 12 | readonly leftButton: boolean; 13 | readonly middleButton: boolean; 14 | readonly rightButton: boolean; 15 | readonly buttons: number; 16 | readonly target: HTMLElement; 17 | readonly detail: number; 18 | readonly posx: number; 19 | readonly posy: number; 20 | readonly ctrlKey: boolean; 21 | readonly shiftKey: boolean; 22 | readonly altKey: boolean; 23 | readonly metaKey: boolean; 24 | readonly timestamp: number; 25 | 26 | preventDefault(): void; 27 | stopPropagation(): void; 28 | } 29 | 30 | export class StandardMouseEvent implements IMouseEvent { 31 | 32 | public readonly browserEvent: MouseEvent; 33 | 34 | public readonly leftButton: boolean; 35 | public readonly middleButton: boolean; 36 | public readonly rightButton: boolean; 37 | public readonly buttons: number; 38 | public readonly target: HTMLElement; 39 | public detail: number; 40 | public readonly posx: number; 41 | public readonly posy: number; 42 | public readonly ctrlKey: boolean; 43 | public readonly shiftKey: boolean; 44 | public readonly altKey: boolean; 45 | public readonly metaKey: boolean; 46 | public readonly timestamp: number; 47 | 48 | constructor(e: MouseEvent) { 49 | this.timestamp = Date.now(); 50 | this.browserEvent = e; 51 | this.leftButton = e.button === 0; 52 | this.middleButton = e.button === 1; 53 | this.rightButton = e.button === 2; 54 | this.buttons = e.buttons; 55 | 56 | this.target = e.target; 57 | 58 | this.detail = e.detail || 1; 59 | if (e.type === 'dblclick') { 60 | this.detail = 2; 61 | } 62 | this.ctrlKey = e.ctrlKey; 63 | this.shiftKey = e.shiftKey; 64 | this.altKey = e.altKey; 65 | this.metaKey = e.metaKey; 66 | 67 | if (typeof e.pageX === 'number') { 68 | this.posx = e.pageX; 69 | this.posy = e.pageY; 70 | } else { 71 | // Probably hit by MSGestureEvent 72 | this.posx = e.clientX + document.body.scrollLeft + document.documentElement!.scrollLeft; 73 | this.posy = e.clientY + document.body.scrollTop + document.documentElement!.scrollTop; 74 | } 75 | 76 | // Find the position of the iframe this code is executing in relative to the iframe where the event was captured. 77 | let iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(self, e.view); 78 | this.posx -= iframeOffsets.left; 79 | this.posy -= iframeOffsets.top; 80 | } 81 | 82 | public preventDefault(): void { 83 | this.browserEvent.preventDefault(); 84 | } 85 | 86 | public stopPropagation(): void { 87 | this.browserEvent.stopPropagation(); 88 | } 89 | } 90 | 91 | export interface IDataTransfer { 92 | dropEffect: string; 93 | effectAllowed: string; 94 | types: any[]; 95 | files: any[]; 96 | 97 | setData(type: string, data: string): void; 98 | setDragImage(image: any, x: number, y: number): void; 99 | 100 | getData(type: string): string; 101 | clearData(types?: string[]): void; 102 | } 103 | 104 | export class DragMouseEvent extends StandardMouseEvent { 105 | 106 | public readonly dataTransfer: IDataTransfer; 107 | 108 | constructor(e: MouseEvent) { 109 | super(e); 110 | this.dataTransfer = (e).dataTransfer; 111 | } 112 | 113 | } 114 | 115 | export interface IMouseWheelEvent extends MouseEvent { 116 | readonly wheelDelta: number; 117 | readonly wheelDeltaX: number; 118 | readonly wheelDeltaY: number; 119 | 120 | readonly deltaX: number; 121 | readonly deltaY: number; 122 | readonly deltaZ: number; 123 | readonly deltaMode: number; 124 | } 125 | 126 | interface IWebKitMouseWheelEvent { 127 | wheelDeltaY: number; 128 | wheelDeltaX: number; 129 | } 130 | 131 | interface IGeckoMouseWheelEvent { 132 | HORIZONTAL_AXIS: number; 133 | VERTICAL_AXIS: number; 134 | axis: number; 135 | detail: number; 136 | } 137 | 138 | export class StandardWheelEvent { 139 | 140 | public readonly browserEvent: IMouseWheelEvent | null; 141 | public readonly deltaY: number; 142 | public readonly deltaX: number; 143 | public readonly target: Node; 144 | 145 | constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) { 146 | 147 | this.browserEvent = e || null; 148 | this.target = e ? (e.target || (e).targetNode || e.srcElement) : null; 149 | 150 | this.deltaY = deltaY; 151 | this.deltaX = deltaX; 152 | 153 | if (e) { 154 | // Old (deprecated) wheel events 155 | let e1 = e; 156 | let e2 = e; 157 | 158 | // vertical delta scroll 159 | if (typeof e1.wheelDeltaY !== 'undefined') { 160 | this.deltaY = e1.wheelDeltaY / 120; 161 | } else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) { 162 | this.deltaY = -e2.detail / 3; 163 | } else if (e.type === 'wheel') { 164 | // Modern wheel event 165 | // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent 166 | const ev = e; 167 | 168 | if (ev.deltaMode === ev.DOM_DELTA_LINE) { 169 | // the deltas are expressed in lines 170 | this.deltaY = -e.deltaY; 171 | } else { 172 | this.deltaY = -e.deltaY / 40; 173 | } 174 | } 175 | 176 | // horizontal delta scroll 177 | if (typeof e1.wheelDeltaX !== 'undefined') { 178 | if (browser.isSafari && platform.isWindows) { 179 | this.deltaX = - (e1.wheelDeltaX / 120); 180 | } else { 181 | this.deltaX = e1.wheelDeltaX / 120; 182 | } 183 | } else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) { 184 | this.deltaX = -e.detail / 3; 185 | } else if (e.type === 'wheel') { 186 | // Modern wheel event 187 | // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent 188 | const ev = e; 189 | 190 | if (ev.deltaMode === ev.DOM_DELTA_LINE) { 191 | // the deltas are expressed in lines 192 | this.deltaX = -e.deltaX; 193 | } else { 194 | this.deltaX = -e.deltaX / 40; 195 | } 196 | } 197 | 198 | // Assume a vertical scroll if nothing else worked 199 | if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) { 200 | this.deltaY = e.wheelDelta / 120; 201 | } 202 | } 203 | } 204 | 205 | public preventDefault(): void { 206 | if (this.browserEvent) { 207 | this.browserEvent.preventDefault(); 208 | } 209 | } 210 | 211 | public stopPropagation(): void { 212 | if (this.browserEvent) { 213 | this.browserEvent.stopPropagation(); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/vs/base/common/platform.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | const LANGUAGE_DEFAULT = 'en'; 7 | 8 | let _isWindows = false; 9 | let _isMacintosh = false; 10 | let _isLinux = false; 11 | let _isNative = false; 12 | let _isWeb = false; 13 | let _isIOS = false; 14 | let _locale: string | undefined = undefined; 15 | let _language: string = LANGUAGE_DEFAULT; 16 | let _translationsConfigFile: string | undefined = undefined; 17 | let _userAgent: string | undefined = undefined; 18 | 19 | interface NLSConfig { 20 | locale: string; 21 | availableLanguages: { [key: string]: string; }; 22 | _translationsConfigFile: string; 23 | } 24 | 25 | export interface IProcessEnvironment { 26 | [key: string]: string; 27 | } 28 | 29 | interface INodeProcess { 30 | platform: string; 31 | env: IProcessEnvironment; 32 | getuid(): number; 33 | nextTick: Function; 34 | versions?: { 35 | electron?: string; 36 | }; 37 | type?: string; 38 | } 39 | declare const process: INodeProcess; 40 | declare const global: any; 41 | 42 | interface INavigator { 43 | userAgent: string; 44 | language: string; 45 | maxTouchPoints?: number; 46 | } 47 | declare const navigator: INavigator; 48 | declare const self: any; 49 | 50 | const isElectronRenderer = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'renderer'); 51 | 52 | // OS detection 53 | if (typeof navigator === 'object' && !isElectronRenderer) { 54 | _userAgent = navigator.userAgent; 55 | _isWindows = _userAgent.indexOf('Windows') >= 0; 56 | _isMacintosh = _userAgent.indexOf('Macintosh') >= 0; 57 | _isIOS = (_userAgent.indexOf('Macintosh') >= 0 || _userAgent.indexOf('iPad') >= 0 || _userAgent.indexOf('iPhone') >= 0) && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 0; 58 | _isLinux = _userAgent.indexOf('Linux') >= 0; 59 | _isWeb = true; 60 | _locale = navigator.language; 61 | _language = _locale; 62 | } else if (typeof process === 'object') { 63 | // _isWindows = (process.platform === 'darwin'); // uncomment this and comment the next 2 lines to test it on macos 64 | _isWindows = (process.platform === 'win32'); 65 | _isMacintosh = (process.platform === 'darwin'); 66 | _isLinux = (process.platform === 'linux'); 67 | _locale = LANGUAGE_DEFAULT; 68 | _language = LANGUAGE_DEFAULT; 69 | const rawNlsConfig = process.env['VSCODE_NLS_CONFIG']; 70 | if (rawNlsConfig) { 71 | try { 72 | const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); 73 | const resolved = nlsConfig.availableLanguages['*']; 74 | _locale = nlsConfig.locale; 75 | // VSCode's default language is 'en' 76 | _language = resolved ? resolved : LANGUAGE_DEFAULT; 77 | _translationsConfigFile = nlsConfig._translationsConfigFile; 78 | } catch (e) { 79 | } 80 | } 81 | _isNative = true; 82 | } 83 | 84 | export const enum Platform { 85 | Web, 86 | Mac, 87 | Linux, 88 | Windows 89 | } 90 | export function PlatformToString(platform: Platform) { 91 | switch (platform) { 92 | case Platform.Web: return 'Web'; 93 | case Platform.Mac: return 'Mac'; 94 | case Platform.Linux: return 'Linux'; 95 | case Platform.Windows: return 'Windows'; 96 | } 97 | } 98 | 99 | let _platform: Platform = Platform.Web; 100 | if (_isMacintosh) { 101 | _platform = Platform.Mac; 102 | } else if (_isWindows) { 103 | _platform = Platform.Windows; 104 | } else if (_isLinux) { 105 | _platform = Platform.Linux; 106 | } 107 | 108 | // use the first two lines for development on MacOS only. 109 | // export const isWindows = true || _isWindows; 110 | // export const isMacintosh = false && _isMacintosh; 111 | export const isWindows = _isWindows; 112 | export const isMacintosh = _isMacintosh; 113 | 114 | export const isLinux = _isLinux; 115 | export const isNative = _isNative; 116 | export const isWeb = _isWeb; 117 | export const isIOS = _isIOS; 118 | export const platform = _platform; 119 | export const userAgent = _userAgent; 120 | 121 | export function isRootUser(): boolean { 122 | return _isNative && !_isWindows && (process.getuid() === 0); 123 | } 124 | 125 | /** 126 | * The language used for the user interface. The format of 127 | * the string is all lower case (e.g. zh-tw for Traditional 128 | * Chinese) 129 | */ 130 | export const language = _language; 131 | 132 | export namespace Language { 133 | 134 | export function value(): string { 135 | return language; 136 | } 137 | 138 | export function isDefaultVariant(): boolean { 139 | if (language.length === 2) { 140 | return language === 'en'; 141 | } else if (language.length >= 3) { 142 | return language[0] === 'e' && language[1] === 'n' && language[2] === '-'; 143 | } else { 144 | return false; 145 | } 146 | } 147 | 148 | export function isDefault(): boolean { 149 | return language === 'en'; 150 | } 151 | } 152 | 153 | /** 154 | * The OS locale or the locale specified by --locale. The format of 155 | * the string is all lower case (e.g. zh-tw for Traditional 156 | * Chinese). The UI is not necessarily shown in the provided locale. 157 | */ 158 | export const locale = _locale; 159 | 160 | /** 161 | * The translatios that are available through language packs. 162 | */ 163 | export const translationsConfigFile = _translationsConfigFile; 164 | 165 | const _globals = (typeof self === 'object' ? self : typeof global === 'object' ? global : {} as any); 166 | export const globals: any = _globals; 167 | 168 | interface ISetImmediate { 169 | (callback: (...args: any[]) => void): void; 170 | } 171 | 172 | export const setImmediate: ISetImmediate = (function defineSetImmediate() { 173 | if (globals.setImmediate) { 174 | return globals.setImmediate.bind(globals); 175 | } 176 | if (typeof globals.postMessage === 'function' && !globals.importScripts) { 177 | interface IQueueElement { 178 | id: number; 179 | callback: () => void; 180 | } 181 | let pending: IQueueElement[] = []; 182 | globals.addEventListener('message', (e: MessageEvent) => { 183 | if (e.data && e.data.vscodeSetImmediateId) { 184 | for (let i = 0, len = pending.length; i < len; i++) { 185 | const candidate = pending[i]; 186 | if (candidate.id === e.data.vscodeSetImmediateId) { 187 | pending.splice(i, 1); 188 | candidate.callback(); 189 | return; 190 | } 191 | } 192 | } 193 | }); 194 | let lastId = 0; 195 | return (callback: () => void) => { 196 | const myId = ++lastId; 197 | pending.push({ 198 | id: myId, 199 | callback: callback 200 | }); 201 | globals.postMessage({ vscodeSetImmediateId: myId }, '*'); 202 | }; 203 | } 204 | if (typeof process !== 'undefined' && typeof process.nextTick === 'function') { 205 | return process.nextTick.bind(process); 206 | } 207 | const _promise = Promise.resolve(); 208 | return (callback: (...args: any[]) => void) => _promise.then(callback); 209 | })(); 210 | 211 | export const enum OperatingSystem { 212 | Windows = 1, 213 | Macintosh = 2, 214 | Linux = 3 215 | } 216 | export const OS = (_isMacintosh || _isIOS ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux)); 217 | 218 | let _isLittleEndian = true; 219 | let _isLittleEndianComputed = false; 220 | export function isLittleEndian(): boolean { 221 | if (!_isLittleEndianComputed) { 222 | _isLittleEndianComputed = true; 223 | const test = new Uint8Array(2); 224 | test[0] = 1; 225 | test[1] = 2; 226 | const view = new Uint16Array(test.buffer); 227 | _isLittleEndian = (view[0] === (2 << 8) + 1); 228 | } 229 | return _isLittleEndian; 230 | } 231 | -------------------------------------------------------------------------------- /src/vs/base/common/lifecycle.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { once } from 'vs/base/common/functional'; 7 | import { Iterable } from 'vs/base/common/iterator'; 8 | 9 | /** 10 | * Enables logging of potentially leaked disposables. 11 | * 12 | * A disposable is considered leaked if it is not disposed or not registered as the child of 13 | * another disposable. This tracking is very simple an only works for classes that either 14 | * extend Disposable or use a DisposableStore. This means there are a lot of false positives. 15 | */ 16 | const TRACK_DISPOSABLES = false; 17 | 18 | const __is_disposable_tracked__ = '__is_disposable_tracked__'; 19 | 20 | function markTracked(x: T): void { 21 | if (!TRACK_DISPOSABLES) { 22 | return; 23 | } 24 | 25 | if (x && x !== Disposable.None) { 26 | try { 27 | (x as any)[__is_disposable_tracked__] = true; 28 | } catch { 29 | // noop 30 | } 31 | } 32 | } 33 | 34 | function trackDisposable(x: T): T { 35 | if (!TRACK_DISPOSABLES) { 36 | return x; 37 | } 38 | 39 | const stack = new Error('Potentially leaked disposable').stack!; 40 | setTimeout(() => { 41 | if (!(x as any)[__is_disposable_tracked__]) { 42 | console.log(stack); 43 | } 44 | }, 3000); 45 | return x; 46 | } 47 | 48 | export class MultiDisposeError extends Error { 49 | constructor( 50 | public readonly errors: any[] 51 | ) { 52 | super(`Encounter errors while disposing of store. Errors: [${errors.join(', ')}]`); 53 | } 54 | } 55 | 56 | export interface IDisposable { 57 | dispose(): void; 58 | } 59 | 60 | export function isDisposable(thing: E): thing is E & IDisposable { 61 | return typeof (thing).dispose === 'function' && (thing).dispose.length === 0; 62 | } 63 | 64 | export function dispose(disposable: T): T; 65 | export function dispose(disposable: T | undefined): T | undefined; 66 | export function dispose = IterableIterator>(disposables: IterableIterator): A; 67 | export function dispose(disposables: Array): Array; 68 | export function dispose(disposables: ReadonlyArray): ReadonlyArray; 69 | export function dispose(arg: T | IterableIterator | undefined): any { 70 | if (Iterable.is(arg)) { 71 | let errors: any[] = []; 72 | 73 | for (const d of arg) { 74 | if (d) { 75 | markTracked(d); 76 | try { 77 | d.dispose(); 78 | } catch (e) { 79 | errors.push(e); 80 | } 81 | } 82 | } 83 | 84 | if (errors.length === 1) { 85 | throw errors[0]; 86 | } else if (errors.length > 1) { 87 | throw new MultiDisposeError(errors); 88 | } 89 | 90 | return Array.isArray(arg) ? [] : arg; 91 | } else if (arg) { 92 | markTracked(arg); 93 | arg.dispose(); 94 | return arg; 95 | } 96 | } 97 | 98 | 99 | export function combinedDisposable(...disposables: IDisposable[]): IDisposable { 100 | disposables.forEach(markTracked); 101 | return trackDisposable({ dispose: () => dispose(disposables) }); 102 | } 103 | 104 | export function toDisposable(fn: () => void): IDisposable { 105 | const self = trackDisposable({ 106 | dispose: () => { 107 | markTracked(self); 108 | fn(); 109 | } 110 | }); 111 | return self; 112 | } 113 | 114 | export class DisposableStore implements IDisposable { 115 | 116 | static DISABLE_DISPOSED_WARNING = false; 117 | 118 | private _toDispose = new Set(); 119 | private _isDisposed = false; 120 | 121 | /** 122 | * Dispose of all registered disposables and mark this object as disposed. 123 | * 124 | * Any future disposables added to this object will be disposed of on `add`. 125 | */ 126 | public dispose(): void { 127 | if (this._isDisposed) { 128 | return; 129 | } 130 | 131 | markTracked(this); 132 | this._isDisposed = true; 133 | this.clear(); 134 | } 135 | 136 | /** 137 | * Dispose of all registered disposables but do not mark this object as disposed. 138 | */ 139 | public clear(): void { 140 | try { 141 | dispose(this._toDispose.values()); 142 | } finally { 143 | this._toDispose.clear(); 144 | } 145 | } 146 | 147 | public add(t: T): T { 148 | if (!t) { 149 | return t; 150 | } 151 | if ((t as unknown as DisposableStore) === this) { 152 | throw new Error('Cannot register a disposable on itself!'); 153 | } 154 | 155 | markTracked(t); 156 | if (this._isDisposed) { 157 | if (!DisposableStore.DISABLE_DISPOSED_WARNING) { 158 | console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack); 159 | } 160 | } else { 161 | this._toDispose.add(t); 162 | } 163 | 164 | return t; 165 | } 166 | } 167 | 168 | export abstract class Disposable implements IDisposable { 169 | 170 | static readonly None = Object.freeze({ dispose() { } }); 171 | 172 | private readonly _store = new DisposableStore(); 173 | 174 | constructor() { 175 | trackDisposable(this); 176 | } 177 | 178 | public dispose(): void { 179 | markTracked(this); 180 | 181 | this._store.dispose(); 182 | } 183 | 184 | protected _register(t: T): T { 185 | if ((t as unknown as Disposable) === this) { 186 | throw new Error('Cannot register a disposable on itself!'); 187 | } 188 | return this._store.add(t); 189 | } 190 | } 191 | 192 | /** 193 | * Manages the lifecycle of a disposable value that may be changed. 194 | * 195 | * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can 196 | * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. 197 | */ 198 | export class MutableDisposable implements IDisposable { 199 | private _value?: T; 200 | private _isDisposed = false; 201 | 202 | constructor() { 203 | trackDisposable(this); 204 | } 205 | 206 | get value(): T | undefined { 207 | return this._isDisposed ? undefined : this._value; 208 | } 209 | 210 | set value(value: T | undefined) { 211 | if (this._isDisposed || value === this._value) { 212 | return; 213 | } 214 | 215 | if (this._value) { 216 | this._value.dispose(); 217 | } 218 | if (value) { 219 | markTracked(value); 220 | } 221 | this._value = value; 222 | } 223 | 224 | clear() { 225 | this.value = undefined; 226 | } 227 | 228 | dispose(): void { 229 | this._isDisposed = true; 230 | markTracked(this); 231 | if (this._value) { 232 | this._value.dispose(); 233 | } 234 | this._value = undefined; 235 | } 236 | } 237 | 238 | export interface IReference extends IDisposable { 239 | readonly object: T; 240 | } 241 | 242 | export abstract class ReferenceCollection { 243 | 244 | private readonly references: Map = new Map(); 245 | 246 | acquire(key: string, ...args: any[]): IReference { 247 | let reference = this.references.get(key); 248 | 249 | if (!reference) { 250 | reference = { counter: 0, object: this.createReferencedObject(key, ...args) }; 251 | this.references.set(key, reference); 252 | } 253 | 254 | const { object } = reference; 255 | const dispose = once(() => { 256 | if (--reference!.counter === 0) { 257 | this.destroyReferencedObject(key, reference!.object); 258 | this.references.delete(key); 259 | } 260 | }); 261 | 262 | reference.counter++; 263 | 264 | return { object, dispose }; 265 | } 266 | 267 | protected abstract createReferencedObject(key: string, ...args: any[]): T; 268 | protected abstract destroyReferencedObject(key: string, object: T): void; 269 | } 270 | 271 | export class ImmortalReference implements IReference { 272 | constructor(public object: T) { } 273 | dispose(): void { /* noop */ } 274 | } 275 | -------------------------------------------------------------------------------- /static/theme/common.css: -------------------------------------------------------------------------------- 1 | .titlebar { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | box-sizing: border-box; 7 | width: 100%; 8 | font-size: 13px; 9 | padding: 0 16px; 10 | overflow: hidden; 11 | flex-shrink: 0; 12 | align-items: center; 13 | justify-content: center; 14 | user-select: none; 15 | zoom: 1; 16 | line-height: 22px; 17 | height: 22px; 18 | display: flex; 19 | z-index: 99999; 20 | } 21 | 22 | .titlebar.cet-windows, .titlebar.cet-linux { 23 | padding: 0; 24 | height: 30px; 25 | line-height: 30px; 26 | justify-content: left; 27 | overflow: visible; 28 | } 29 | 30 | .titlebar.inverted, .titlebar.inverted .menubar, 31 | .titlebar.inverted .window-controls-container { 32 | flex-direction: row-reverse; 33 | } 34 | 35 | .titlebar.inverted .window-controls-container { 36 | margin: 0 5px 0 0; 37 | } 38 | 39 | .titlebar.first-buttons .window-controls-container { 40 | order: -1; 41 | margin: 0 5px 0 0; 42 | } 43 | /* Drag region */ 44 | 45 | .titlebar .titlebar-drag-region { 46 | top: 0; 47 | left: 0; 48 | display: block; 49 | position: absolute; 50 | width: 100%; 51 | height: 100%; 52 | z-index: -1; 53 | -webkit-app-region: drag; 54 | } 55 | 56 | /* icon app */ 57 | 58 | .titlebar > .window-appicon { 59 | width: 35px; 60 | height: 30px; 61 | position: relative; 62 | z-index: 99; 63 | background-repeat: no-repeat; 64 | background-position: center center; 65 | background-size: 16px; 66 | flex-shrink: 0; 67 | } 68 | 69 | /* Menu bar */ 70 | 71 | .menubar { 72 | display: flex; 73 | flex-shrink: 1; 74 | box-sizing: border-box; 75 | height: 30px; 76 | overflow: hidden; 77 | flex-wrap: wrap; 78 | } 79 | 80 | .menubar.bottom { 81 | order: 1; 82 | width: 100%; 83 | padding: 0 5px; 84 | } 85 | 86 | .menubar .menubar-menu-button { 87 | align-items: center; 88 | box-sizing: border-box; 89 | padding: 0px 8px; 90 | cursor: default; 91 | -webkit-app-region: no-drag; 92 | zoom: 1; 93 | white-space: nowrap; 94 | outline: 0; 95 | } 96 | 97 | .menubar .menubar-menu-button.disabled { 98 | opacity: 0.4; 99 | } 100 | 101 | .menubar .menubar-menu-button:not(.disabled):focus, 102 | .menubar .menubar-menu-button:not(.disabled).open, 103 | .menubar .menubar-menu-button:not(.disabled):hover { 104 | background-color: rgba(255, 255, 255, .1); 105 | } 106 | 107 | .titlebar.light .menubar .menubar-menu-button:focus, 108 | .titlebar.light .menubar .menubar-menu-button.open, 109 | .titlebar.light .menubar .menubar-menu-button:hover { 110 | background-color: rgba(0, 0, 0, .1); 111 | } 112 | 113 | .menubar-menu-container { 114 | position: absolute; 115 | display: block; 116 | left: 0px; 117 | opacity: 1; 118 | outline: 0; 119 | border: none; 120 | text-align: left; 121 | margin: 0 auto; 122 | padding: .5em 0; 123 | margin-left: 0; 124 | overflow: visible; 125 | justify-content: flex-end; 126 | white-space: nowrap; 127 | box-shadow: 0 5px 5px -3px rgba(0,0,0,.2), 0 8px 10px 1px rgba(0,0,0,.14), 0 3px 14px 2px rgba(0,0,0,.12); 128 | z-index: 99999; 129 | } 130 | 131 | .menubar-menu-container:focus { 132 | outline: 0; 133 | } 134 | 135 | .menubar-menu-container .action-item { 136 | padding: 0; 137 | transform: none; 138 | display: -ms-flexbox; 139 | display: flex; 140 | outline: none; 141 | } 142 | 143 | .menubar-menu-container .action-item.active { 144 | transform: none; 145 | } 146 | 147 | .menubar-menu-container .action-menu-item { 148 | -ms-flex: 1 1 auto; 149 | flex: 1 1 auto; 150 | display: -ms-flexbox; 151 | display: flex; 152 | height: 2.5em; 153 | align-items: center; 154 | position: relative; 155 | } 156 | 157 | .menubar-menu-container .action-label { 158 | -ms-flex: 1 1 auto; 159 | flex: 1 1 auto; 160 | text-decoration: none; 161 | padding: 0 1em; 162 | background: none; 163 | font-size: 12px; 164 | line-height: 1; 165 | } 166 | 167 | .menubar-menu-container .action-label:not(.separator), 168 | .menubar-menu-container .keybinding { 169 | padding: 0 2em 0 1em; 170 | } 171 | 172 | .menubar-menu-container .keybinding, 173 | .menubar-menu-container .submenu-indicator { 174 | display: inline-block; 175 | -ms-flex: 2 1 auto; 176 | flex: 2 1 auto; 177 | padding: 0 2em 0 1em; 178 | text-align: right; 179 | font-size: 12px; 180 | line-height: 1; 181 | } 182 | 183 | .menubar-menu-container .submenu-indicator { 184 | height: 100%; 185 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.52 12.364L9.879 7 4.52 1.636l.615-.615L11.122 7l-5.986 5.98-.615-.616z' fill='%23000'/%3E%3C/svg%3E") no-repeat right center/13px 13px; 186 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.52 12.364L9.879 7 4.52 1.636l.615-.615L11.122 7l-5.986 5.98-.615-.616z' fill='%23000'/%3E%3C/svg%3E") no-repeat right center/13px 13px; 187 | font-size: 60%; 188 | margin: 0 2em 0 1em; 189 | } 190 | 191 | .menubar-menu-container .action-item.disabled .action-menu-item, 192 | .menubar-menu-container .action-label.separator { 193 | opacity: 0.4; 194 | } 195 | 196 | .menubar-menu-container .action-label:not(.separator) { 197 | display: inline-block; 198 | -webkit-box-sizing: border-box; 199 | -o-box-sizing: border-box; 200 | -moz-box-sizing: border-box; 201 | -ms-box-sizing: border-box; 202 | box-sizing: border-box; 203 | margin: 0; 204 | } 205 | 206 | .menubar-menu-container .action-item .submenu { 207 | position: absolute; 208 | } 209 | 210 | .menubar-menu-container .action-label.separator { 211 | font-size: inherit; 212 | padding: .2em 0 0; 213 | margin-left: .8em; 214 | margin-right: .8em; 215 | margin-bottom: .2em; 216 | width: 100%; 217 | border-bottom: 1px solid transparent; 218 | } 219 | 220 | .menubar-menu-container .action-label.separator.text { 221 | padding: 0.7em 1em 0.1em 1em; 222 | font-weight: bold; 223 | opacity: 1; 224 | } 225 | 226 | .menubar-menu-container .action-label:hover { 227 | color: inherit; 228 | } 229 | 230 | .menubar-menu-container .menu-item-check { 231 | position: absolute; 232 | visibility: hidden; 233 | -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='-2 -2 16 16'%3E%3Cpath fill='%23424242' d='M9 0L4.5 9 3 6H0l3 6h3l6-12z'/%3E%3C/svg%3E") no-repeat 50% 56%/15px 15px; 234 | mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='-2 -2 16 16'%3E%3Cpath fill='%23424242' d='M9 0L4.5 9 3 6H0l3 6h3l6-12z'/%3E%3C/svg%3E") no-repeat 50% 56%/15px 15px; 235 | width: 2em; 236 | height: 2em; 237 | margin: 0 0 0 0.7em; 238 | } 239 | 240 | .menubar-menu-container .menu-item-icon { 241 | width: 18px; 242 | height: 18px; 243 | margin: 0 0 0 1.1em; 244 | } 245 | 246 | .menubar-menu-container .menu-item-icon img { 247 | display: inherit; 248 | width: 100%; 249 | height: 100%; 250 | } 251 | 252 | .menubar-menu-container .action-menu-item.checked .menu-item-icon { 253 | visibility: hidden; 254 | } 255 | 256 | .menubar-menu-container .action-menu-item.checked .menu-item-check { 257 | visibility: visible; 258 | } 259 | 260 | /* Title */ 261 | 262 | .titlebar .window-title { 263 | flex: 0 1 auto; 264 | font-size: 12px; 265 | overflow: hidden; 266 | white-space: nowrap; 267 | text-overflow: ellipsis; 268 | margin: 0 auto; 269 | zoom: 1; 270 | } 271 | 272 | /* Window controls */ 273 | 274 | .titlebar .window-controls-container { 275 | display: flex; 276 | flex-grow: 0; 277 | flex-shrink: 0; 278 | text-align: center; 279 | position: relative; 280 | z-index: 99; 281 | -webkit-app-region: no-drag; 282 | height: 30px; 283 | margin-left: 5px; 284 | } 285 | 286 | /* Resizer */ 287 | 288 | .titlebar.cet-windows .resizer, .titlebar.cet-linux .resizer { 289 | -webkit-app-region: no-drag; 290 | position: absolute; 291 | } 292 | 293 | .titlebar.cet-windows .resizer.top, .titlebar.cet-linux .resizer.top { 294 | top: 0; 295 | width: 100%; 296 | height: 6px; 297 | } 298 | 299 | .titlebar.cet-windows .resizer.left, .titlebar.cet-linux .resizer.left { 300 | top: 0; 301 | left: 0; 302 | width: 6px; 303 | height: 100%; 304 | } -------------------------------------------------------------------------------- /src/vs/base/browser/keyboardEvent.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as browser from 'vs/base/browser/browser'; 7 | import { KeyCode, KeyCodeUtils, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes'; 8 | import * as platform from 'vs/base/common/platform'; 9 | 10 | let KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230); 11 | let INVERSE_KEY_CODE_MAP: KeyCode[] = new Array(KeyCode.MAX_VALUE); 12 | 13 | (function () { 14 | for (let i = 0; i < INVERSE_KEY_CODE_MAP.length; i++) { 15 | INVERSE_KEY_CODE_MAP[i] = -1; 16 | } 17 | 18 | function define(code: number, keyCode: KeyCode): void { 19 | KEY_CODE_MAP[code] = keyCode; 20 | INVERSE_KEY_CODE_MAP[keyCode] = code; 21 | } 22 | 23 | define(3, KeyCode.PauseBreak); // VK_CANCEL 0x03 Control-break processing 24 | define(8, KeyCode.Backspace); 25 | define(9, KeyCode.Tab); 26 | define(13, KeyCode.Enter); 27 | define(16, KeyCode.Shift); 28 | define(17, KeyCode.Ctrl); 29 | define(18, KeyCode.Alt); 30 | define(19, KeyCode.PauseBreak); 31 | define(20, KeyCode.CapsLock); 32 | define(27, KeyCode.Escape); 33 | define(32, KeyCode.Space); 34 | define(33, KeyCode.PageUp); 35 | define(34, KeyCode.PageDown); 36 | define(35, KeyCode.End); 37 | define(36, KeyCode.Home); 38 | define(37, KeyCode.LeftArrow); 39 | define(38, KeyCode.UpArrow); 40 | define(39, KeyCode.RightArrow); 41 | define(40, KeyCode.DownArrow); 42 | define(45, KeyCode.Insert); 43 | define(46, KeyCode.Delete); 44 | 45 | define(48, KeyCode.KEY_0); 46 | define(49, KeyCode.KEY_1); 47 | define(50, KeyCode.KEY_2); 48 | define(51, KeyCode.KEY_3); 49 | define(52, KeyCode.KEY_4); 50 | define(53, KeyCode.KEY_5); 51 | define(54, KeyCode.KEY_6); 52 | define(55, KeyCode.KEY_7); 53 | define(56, KeyCode.KEY_8); 54 | define(57, KeyCode.KEY_9); 55 | 56 | define(65, KeyCode.KEY_A); 57 | define(66, KeyCode.KEY_B); 58 | define(67, KeyCode.KEY_C); 59 | define(68, KeyCode.KEY_D); 60 | define(69, KeyCode.KEY_E); 61 | define(70, KeyCode.KEY_F); 62 | define(71, KeyCode.KEY_G); 63 | define(72, KeyCode.KEY_H); 64 | define(73, KeyCode.KEY_I); 65 | define(74, KeyCode.KEY_J); 66 | define(75, KeyCode.KEY_K); 67 | define(76, KeyCode.KEY_L); 68 | define(77, KeyCode.KEY_M); 69 | define(78, KeyCode.KEY_N); 70 | define(79, KeyCode.KEY_O); 71 | define(80, KeyCode.KEY_P); 72 | define(81, KeyCode.KEY_Q); 73 | define(82, KeyCode.KEY_R); 74 | define(83, KeyCode.KEY_S); 75 | define(84, KeyCode.KEY_T); 76 | define(85, KeyCode.KEY_U); 77 | define(86, KeyCode.KEY_V); 78 | define(87, KeyCode.KEY_W); 79 | define(88, KeyCode.KEY_X); 80 | define(89, KeyCode.KEY_Y); 81 | define(90, KeyCode.KEY_Z); 82 | 83 | define(93, KeyCode.ContextMenu); 84 | 85 | define(96, KeyCode.NUMPAD_0); 86 | define(97, KeyCode.NUMPAD_1); 87 | define(98, KeyCode.NUMPAD_2); 88 | define(99, KeyCode.NUMPAD_3); 89 | define(100, KeyCode.NUMPAD_4); 90 | define(101, KeyCode.NUMPAD_5); 91 | define(102, KeyCode.NUMPAD_6); 92 | define(103, KeyCode.NUMPAD_7); 93 | define(104, KeyCode.NUMPAD_8); 94 | define(105, KeyCode.NUMPAD_9); 95 | define(106, KeyCode.NUMPAD_MULTIPLY); 96 | define(107, KeyCode.NUMPAD_ADD); 97 | define(108, KeyCode.NUMPAD_SEPARATOR); 98 | define(109, KeyCode.NUMPAD_SUBTRACT); 99 | define(110, KeyCode.NUMPAD_DECIMAL); 100 | define(111, KeyCode.NUMPAD_DIVIDE); 101 | 102 | define(112, KeyCode.F1); 103 | define(113, KeyCode.F2); 104 | define(114, KeyCode.F3); 105 | define(115, KeyCode.F4); 106 | define(116, KeyCode.F5); 107 | define(117, KeyCode.F6); 108 | define(118, KeyCode.F7); 109 | define(119, KeyCode.F8); 110 | define(120, KeyCode.F9); 111 | define(121, KeyCode.F10); 112 | define(122, KeyCode.F11); 113 | define(123, KeyCode.F12); 114 | define(124, KeyCode.F13); 115 | define(125, KeyCode.F14); 116 | define(126, KeyCode.F15); 117 | define(127, KeyCode.F16); 118 | define(128, KeyCode.F17); 119 | define(129, KeyCode.F18); 120 | define(130, KeyCode.F19); 121 | 122 | define(144, KeyCode.NumLock); 123 | define(145, KeyCode.ScrollLock); 124 | 125 | define(186, KeyCode.US_SEMICOLON); 126 | define(187, KeyCode.US_EQUAL); 127 | define(188, KeyCode.US_COMMA); 128 | define(189, KeyCode.US_MINUS); 129 | define(190, KeyCode.US_DOT); 130 | define(191, KeyCode.US_SLASH); 131 | define(192, KeyCode.US_BACKTICK); 132 | define(193, KeyCode.ABNT_C1); 133 | define(194, KeyCode.ABNT_C2); 134 | define(219, KeyCode.US_OPEN_SQUARE_BRACKET); 135 | define(220, KeyCode.US_BACKSLASH); 136 | define(221, KeyCode.US_CLOSE_SQUARE_BRACKET); 137 | define(222, KeyCode.US_QUOTE); 138 | define(223, KeyCode.OEM_8); 139 | 140 | define(226, KeyCode.OEM_102); 141 | 142 | /** 143 | * https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html 144 | * If an Input Method Editor is processing key input and the event is keydown, return 229. 145 | */ 146 | define(229, KeyCode.KEY_IN_COMPOSITION); 147 | 148 | if (browser.isFirefox) { 149 | define(59, KeyCode.US_SEMICOLON); 150 | define(107, KeyCode.US_EQUAL); 151 | define(109, KeyCode.US_MINUS); 152 | if (platform.isMacintosh) { 153 | define(224, KeyCode.Meta); 154 | } 155 | } else if (browser.isWebKit) { 156 | define(91, KeyCode.Meta); 157 | if (platform.isMacintosh) { 158 | // the two meta keys in the Mac have different key codes (91 and 93) 159 | define(93, KeyCode.Meta); 160 | } else { 161 | define(92, KeyCode.Meta); 162 | } 163 | } 164 | })(); 165 | 166 | function extractKeyCode(e: KeyboardEvent): KeyCode { 167 | if (e.charCode) { 168 | // "keypress" events mostly 169 | let char = String.fromCharCode(e.charCode).toUpperCase(); 170 | return KeyCodeUtils.fromString(char); 171 | } 172 | return KEY_CODE_MAP[e.keyCode] || KeyCode.Unknown; 173 | } 174 | 175 | export function getCodeForKeyCode(keyCode: KeyCode): number { 176 | return INVERSE_KEY_CODE_MAP[keyCode]; 177 | } 178 | 179 | export interface IKeyboardEvent { 180 | 181 | readonly _standardKeyboardEventBrand: true; 182 | 183 | readonly browserEvent: KeyboardEvent; 184 | readonly target: HTMLElement; 185 | 186 | readonly ctrlKey: boolean; 187 | readonly shiftKey: boolean; 188 | readonly altKey: boolean; 189 | readonly metaKey: boolean; 190 | readonly keyCode: KeyCode; 191 | readonly code: string; 192 | 193 | /** 194 | * @internal 195 | */ 196 | toKeybinding(): SimpleKeybinding; 197 | equals(keybinding: number): boolean; 198 | 199 | preventDefault(): void; 200 | stopPropagation(): void; 201 | } 202 | 203 | const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd); 204 | const altKeyMod = KeyMod.Alt; 205 | const shiftKeyMod = KeyMod.Shift; 206 | const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl); 207 | 208 | export class StandardKeyboardEvent implements IKeyboardEvent { 209 | 210 | readonly _standardKeyboardEventBrand = true; 211 | 212 | public readonly browserEvent: KeyboardEvent; 213 | public readonly target: HTMLElement; 214 | 215 | public readonly ctrlKey: boolean; 216 | public readonly shiftKey: boolean; 217 | public readonly altKey: boolean; 218 | public readonly metaKey: boolean; 219 | public readonly keyCode: KeyCode; 220 | public readonly code: string; 221 | 222 | private _asKeybinding: number; 223 | private _asRuntimeKeybinding: SimpleKeybinding; 224 | 225 | constructor(source: KeyboardEvent) { 226 | let e = source; 227 | 228 | this.browserEvent = e; 229 | this.target = e.target; 230 | 231 | this.ctrlKey = e.ctrlKey; 232 | this.shiftKey = e.shiftKey; 233 | this.altKey = e.altKey; 234 | this.metaKey = e.metaKey; 235 | this.keyCode = extractKeyCode(e); 236 | this.code = e.code; 237 | 238 | // console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]); 239 | 240 | this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl; 241 | this.altKey = this.altKey || this.keyCode === KeyCode.Alt; 242 | this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift; 243 | this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta; 244 | 245 | this._asKeybinding = this._computeKeybinding(); 246 | this._asRuntimeKeybinding = this._computeRuntimeKeybinding(); 247 | 248 | // console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`); 249 | } 250 | 251 | public preventDefault(): void { 252 | if (this.browserEvent && this.browserEvent.preventDefault) { 253 | this.browserEvent.preventDefault(); 254 | } 255 | } 256 | 257 | public stopPropagation(): void { 258 | if (this.browserEvent && this.browserEvent.stopPropagation) { 259 | this.browserEvent.stopPropagation(); 260 | } 261 | } 262 | 263 | public toKeybinding(): SimpleKeybinding { 264 | return this._asRuntimeKeybinding; 265 | } 266 | 267 | public equals(other: number): boolean { 268 | return this._asKeybinding === other; 269 | } 270 | 271 | private _computeKeybinding(): number { 272 | let key = KeyCode.Unknown; 273 | if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) { 274 | key = this.keyCode; 275 | } 276 | 277 | let result = 0; 278 | if (this.ctrlKey) { 279 | result |= ctrlKeyMod; 280 | } 281 | if (this.altKey) { 282 | result |= altKeyMod; 283 | } 284 | if (this.shiftKey) { 285 | result |= shiftKeyMod; 286 | } 287 | if (this.metaKey) { 288 | result |= metaKeyMod; 289 | } 290 | result |= key; 291 | 292 | return result; 293 | } 294 | 295 | private _computeRuntimeKeybinding(): SimpleKeybinding { 296 | let key = KeyCode.Unknown; 297 | if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) { 298 | key = this.keyCode; 299 | } 300 | return new SimpleKeybinding(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Electron Titlebar 2 | 3 | This project is a typescript library for electron that allows you to configure a fully customizable title bar. 4 | The titlebar is based on the solution from Visual Studio Code and uses 5 | some of their code. 6 | 7 | **This library is for electron 10+, it cannot be used on a basic website or with previous versions of electron.** 8 | 9 | [![LICENSE](https://img.shields.io/github/license/AlexTorresSk/custom-electron-titlebar.svg)](https://github.com/AlexTorresSk/custom-electron-titlebar/blob/master/LICENSE) 10 | 11 | ![Preview 1](screenshots/window_1.png) 12 | 13 | ![Preview 2](screenshots/window_2.png) 14 | 15 | ![Preview 3](screenshots/window_3.png) 16 | 17 | ## Install 18 | 19 | ``` 20 | npm i custom-electron-titlebar 21 | ``` 22 | 23 | or use example folder to init basic electron project with this titlebar. 24 | 25 | ## Usage 26 | 27 | #### Step 1 28 | In your **preload** file add: 29 | 30 | ```js 31 | const customTitlebar = require('custom-electron-titlebar'); 32 | 33 | window.addEventListener('DOMContentLoaded', () => { 34 | new customTitlebar.Titlebar({ 35 | backgroundColor: customTitlebar.Color.fromHex('#ECECEC') 36 | }); 37 | 38 | // ... 39 | }) 40 | ``` 41 | 42 | or, if you are using _typescript_ 43 | ```ts 44 | import { Titlebar, Color } from 'custom-electron-titlebar' 45 | 46 | window.addEventListener('DOMContentLoaded', () => { 47 | new Titlebar({ 48 | backgroundColor: Color.fromHex('#ECECEC') 49 | }); 50 | 51 | // ... 52 | }) 53 | ``` 54 | 55 | The parameter `backgroundColor: Color` is required, this should be `Color` type. 56 | (View [Update Background](#update-background) for more details). 57 | 58 | #### Step 2 59 | Update the code that launches browser window 60 | 61 | Add the following line to your `main.js` file: 62 | 63 | ```js 64 | require('@treverix/remote/main').initialize() 65 | ``` 66 | 67 | Then configure the browser window like this. We need 68 | nodeIntegration and we need to enable the remote module for 69 | electron 10+. Further, we assume that the : 70 | 71 | ```js 72 | var mainWindow = new BrowserWindow({ 73 | width: 1000, 74 | height: 600, 75 | frame: false, 76 | webPreferences: { 77 | nodeIntegration: true, 78 | enableRemoteModule: true, 79 | preload: path.join(__dirname, 'preload.js'), 80 | } 81 | }); 82 | ``` 83 | 84 | #### MacOS / Windows 85 | 86 | Custom Electron Titlebar is not really useful on MacOS platforms 87 | where it only tries to simulate the normal MacOS window 88 | design to some extent and it is recommended to exclude 89 | the custom titlebar when your application is run on 90 | a MacOS platform. 91 | 92 | To switch it of for MacOS platforms, you should configure 93 | your browser window like so 94 | 95 | ```js 96 | var mainWindow = new BrowserWindow({ 97 | // ... 98 | frame: process.platform === 'darwin', 99 | // ... 100 | }); 101 | ``` 102 | 103 | and similiar on the preload: 104 | 105 | ```js 106 | 107 | window.addEventListener('DOMContentLoaded', () => { 108 | if (process.platform !== 'darwin') { 109 | const customTitlebar = require('custom-electron-titlebar'); 110 | new customTitlebar.Titlebar({ 111 | backgroundColor: customTitlebar.Color.fromHex('#ECECEC') 112 | }); 113 | } 114 | 115 | // ... 116 | }) 117 | ``` 118 | 119 | ## Options 120 | 121 | The interface [`TitleBarOptions`] is managed, which has the following configurable options for the title bar. Some parameters are optional. 122 | 123 | | Parameter | Type | Description | Default | 124 | | ------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | 125 | | backgroundColor **(required)** | Color | The background color of the titlebar. | #444444 | 126 | | icon | string | The icon shown on the left side of the title bar. | null | 127 | | iconsTheme | Theme | Style of the icons. | Themebar.win | 128 | | shadow | boolean | The shadow of the titlebar. | false | 129 | | drag | boolean | Define whether or not you can drag the window by holding the click on the title bar. | true | 130 | | minimizable | boolean | Enables or disables the option to minimize the window by clicking on the corresponding button in the title bar. | true | 131 | | maximizable | boolean | Enables or disables the option to maximize and un-maximize the window by clicking on the corresponding button in the title bar. | true | 132 | | closeable | boolean | Enables or disables the option of the close window by clicking on the corresponding button in the title bar. | true | 133 | | order | string | Set the order of the elements on the title bar. (`inverted`, `first-buttons`) | null | 134 | | titleHorizontalAlignment | string | Set horizontal alignment of the window title. (`left`, `center`, `right`) | center | 135 | | menu | Electron.Menu | The menu to show in the title bar. | Menu.getApplicationMenu() | 136 | | menuPosition | string | The position of menubar on titlebar. | left | 137 | | enableMnemonics | boolean | Enable the mnemonics on menubar and menu items. | true | 138 | | itemBackgroundColor | Color | The background color when the mouse is over the item. | rgba(0, 0, 0, .14) | 139 | | hideWhenClickingClose | boolean | When the close button is clicked, the window is hidden instead of closed. | false | 140 | | overflow | string | The overflow of the container (`auto`, `visible`, `hidden`) | auto | 141 | | unfocusEffect | boolean | Enables or disables the blur option in the title bar. | false | 142 | 143 | ## Methods 144 | 145 | ### Update Background 146 | 147 | This change the color of titlebar and it's checked whether the color is light or dark, so that the color of the icons adapts to the background of the title bar. 148 | 149 | ```js 150 | titlebar.updateBackground(new Color(new RGBA(0, 0, 0, .7))); 151 | ``` 152 | 153 | To assign colors you can use the following options: `Color.fromHex()`, `new Color(new RGBA(r, g, b, a))`, `new Color(new HSLA(h, s, l, a))`, `new Color(new HSVA(h, s, v, a))` or `Color.BLUE`, `Color.RED`, etc. 154 | 155 | ### Update Items Background Color 156 | 157 | This method change background color on hover of items of menubar. 158 | 159 | ```js 160 | titlebar.updateItemBGColor(new Color(new RGBA(0, 0, 0, .7))); 161 | ``` 162 | 163 | To assign colors you can use the following options: `Color.fromHex()`, `new Color(new RGBA(r, g, b, a))`, `new Color(new HSLA(h, s, l, a))`, `new Color(new HSVA(h, s, v, a))` or `Color.BLUE`, `Color.RED`, etc. 164 | 165 | ### Update Title 166 | 167 | This method updated the title of the title bar, If you change the content of the `title` tag, you should call this method for update the title. 168 | 169 | ```js 170 | document.title = 'My new title'; 171 | titlebar.updateTitle(); 172 | 173 | // Or you can do as follows and avoid writing document.title 174 | titlebar.updateTitle('New Title'); 175 | ``` 176 | 177 | if this method is called and the title parameter is added, the title of the document is changed to that of the parameter. 178 | 179 | ### Update Icon 180 | 181 | With this method you can update the icon. This method receives the url of the image _(it is advisable to use transparent image formats)_ 182 | 183 | ```js 184 | titlebar.updateIcon('./images/my-icon.svg'); 185 | ``` 186 | 187 | ### Update Menu 188 | 189 | This method updates or creates the menu, to create the menu use remote.Menu and remote.MenuItem. 190 | 191 | ```js 192 | const menu = new Menu(); 193 | menu.append(new MenuItem({ 194 | label: 'Item 1', 195 | submenu: [ 196 | { 197 | label: 'Subitem 1', 198 | click: () => console.log('Click on subitem 1') 199 | }, 200 | { 201 | type: 'separator' 202 | } 203 | ] 204 | })); 205 | 206 | menu.append(new MenuItem({ 207 | label: 'Item 2', 208 | submenu: [ 209 | { 210 | label: 'Subitem checkbox', 211 | type: 'checkbox', 212 | checked: true 213 | }, 214 | { 215 | type: 'separator' 216 | }, 217 | { 218 | label: 'Subitem with submenu', 219 | submenu: [ 220 | { 221 | label: 'Submenu &item 1', 222 | accelerator: 'Ctrl+T' 223 | } 224 | ] 225 | } 226 | ] 227 | })); 228 | 229 | titlebar.updateMenu(menu); 230 | ``` 231 | 232 | ### Update Menu Position 233 | 234 | You can change the position of the menu bar. `left` and `bottom` are allowed. 235 | 236 | ```js 237 | titlebar.updateMenuPosition('bottom'); 238 | ``` 239 | 240 | ### Set Horizontal Alignment 241 | 242 | > setHorizontalAlignment method was contributed by [@MairwunNx](https://github.com/MairwunNx) :punch: 243 | 244 | `left`, `center` and `right` are allowed 245 | 246 | ```js 247 | titlebar.setHorizontalAlignment('right'); 248 | ``` 249 | 250 | ### Dispose 251 | 252 | This method removes the title bar completely and all recorded events. 253 | 254 | ```js 255 | titlebar.dispose(); 256 | ``` 257 | 258 | ## CSS Classes 259 | The following CSS classes exist and can be used to customize the titlebar 260 | 261 | | Class name | Description | 262 | | --------------------------- | ----------------------------------------------- | 263 | | .titlebar | Styles the titlebar. | 264 | | .window-appicon | Styles the app icon on the titlebar. | 265 | | .window-title | Styles the window title. (Example: font-size) | 266 | | .window-controls-container | Styles the window controls section. | 267 | | .resizer top | Description missing | 268 | | .resizer left | Description missing | 269 | | .menubar | Description missing | 270 | | .menubar-menu-button | Styles the main menu elements. (Example: color) | 271 | | .menubar-menu-button open | Description missing | 272 | | .menubar-menu-title | Description missing | 273 | | .action-item | Description missing | 274 | | .action-menu-item | Styles action menu elements. (Example: color) | 275 | 276 | ## Goal 277 | 278 | Electron is deprecating the remote module. It is deactivated by default and 279 | will be removed with electron 12 in the near future. The great custom-electron-titlebar 280 | by AlexTorresSk has been archived recently and is not maintained anymore. 281 | This fork is a version for electron 10+ and uses the new userland implementation 282 | of the remote module [@treverix/remote](https://www.npmjs.com/package/@electron/remote). 283 | 284 | ## Contributing 285 | 286 | Many thanks to all the people who supported Alex' project through issues and pull request. 287 | If you want to contribute with this electron 10 fork, all the issues and pull request are welcome. 288 | 289 | You can also buy us a coffee: 290 | 291 | | Andreas (Treverix) | Alex | 292 | |--------------------|------| 293 | |Buy Me A Coffee | Buy Me A Coffee 294 | | 295 | 296 | 297 | ## License 298 | 299 | This fork of the archived [custom-electron-titlebar project by AlexTorresSk](https://github.com/AlexTorresSk/custom-electron-titlebar) is under the [MIT](https://github.com/Treverix/custom-electron-titlebar/blob/master/LICENSE) license. 300 | 301 | It uses Code from the [Visual Studio Code Project](https://github.com/microsoft/vscode) which is also under the [MIT](https://github.com/Treverix/custom-electron-titlebar/blob/master/LICENSE) license. 302 | -------------------------------------------------------------------------------- /src/vs/base/browser/touch.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as arrays from 'vs/base/common/arrays'; 7 | import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; 8 | import * as DomUtils from 'vs/base/browser/dom'; 9 | import { memoize } from 'vs/base/common/decorators'; 10 | 11 | export namespace EventType { 12 | export const Tap = '-monaco-gesturetap'; 13 | export const Change = '-monaco-gesturechange'; 14 | export const Start = '-monaco-gesturestart'; 15 | export const End = '-monaco-gesturesend'; 16 | export const Contextmenu = '-monaco-gesturecontextmenu'; 17 | } 18 | 19 | interface TouchData { 20 | id: number; 21 | initialTarget: EventTarget; 22 | initialTimeStamp: number; 23 | initialPageX: number; 24 | initialPageY: number; 25 | rollingTimestamps: number[]; 26 | rollingPageX: number[]; 27 | rollingPageY: number[]; 28 | } 29 | 30 | export interface GestureEvent extends MouseEvent { 31 | initialTarget: EventTarget | undefined; 32 | translationX: number; 33 | translationY: number; 34 | pageX: number; 35 | pageY: number; 36 | tapCount: number; 37 | } 38 | 39 | interface Touch { 40 | identifier: number; 41 | screenX: number; 42 | screenY: number; 43 | clientX: number; 44 | clientY: number; 45 | pageX: number; 46 | pageY: number; 47 | radiusX: number; 48 | radiusY: number; 49 | rotationAngle: number; 50 | force: number; 51 | target: Element; 52 | } 53 | 54 | interface TouchList { 55 | [i: number]: Touch; 56 | length: number; 57 | item(index: number): Touch; 58 | identifiedTouch(id: number): Touch; 59 | } 60 | 61 | interface TouchEvent extends Event { 62 | touches: TouchList; 63 | targetTouches: TouchList; 64 | changedTouches: TouchList; 65 | } 66 | 67 | export class Gesture extends Disposable { 68 | 69 | private static readonly SCROLL_FRICTION = -0.005; 70 | private static INSTANCE: Gesture; 71 | private static readonly HOLD_DELAY = 700; 72 | 73 | private dispatched = false; 74 | private targets: HTMLElement[]; 75 | private ignoreTargets: HTMLElement[]; 76 | private handle: IDisposable | null; 77 | 78 | private activeTouches: { [id: number]: TouchData; }; 79 | 80 | private _lastSetTapCountTime: number; 81 | 82 | private static readonly CLEAR_TAP_COUNT_TIME = 400; // ms 83 | 84 | 85 | private constructor() { 86 | super(); 87 | 88 | this.activeTouches = {}; 89 | this.handle = null; 90 | this.targets = []; 91 | this.ignoreTargets = []; 92 | this._lastSetTapCountTime = 0; 93 | this._register(DomUtils.addDisposableListener(document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e), { passive: false })); 94 | this._register(DomUtils.addDisposableListener(document, 'touchend', (e: TouchEvent) => this.onTouchEnd(e))); 95 | this._register(DomUtils.addDisposableListener(document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e), { passive: false })); 96 | } 97 | 98 | public static addTarget(element: HTMLElement): IDisposable { 99 | if (!Gesture.isTouchDevice()) { 100 | return Disposable.None; 101 | } 102 | if (!Gesture.INSTANCE) { 103 | Gesture.INSTANCE = new Gesture(); 104 | } 105 | 106 | Gesture.INSTANCE.targets.push(element); 107 | 108 | return { 109 | dispose: () => { 110 | Gesture.INSTANCE.targets = Gesture.INSTANCE.targets.filter(t => t !== element); 111 | } 112 | }; 113 | } 114 | 115 | public static ignoreTarget(element: HTMLElement): IDisposable { 116 | if (!Gesture.isTouchDevice()) { 117 | return Disposable.None; 118 | } 119 | if (!Gesture.INSTANCE) { 120 | Gesture.INSTANCE = new Gesture(); 121 | } 122 | 123 | Gesture.INSTANCE.ignoreTargets.push(element); 124 | 125 | return { 126 | dispose: () => { 127 | Gesture.INSTANCE.ignoreTargets = Gesture.INSTANCE.ignoreTargets.filter(t => t !== element); 128 | } 129 | }; 130 | } 131 | 132 | @memoize 133 | private static isTouchDevice(): boolean { 134 | // `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be 135 | // `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast 136 | return 'ontouchstart' in window || navigator.maxTouchPoints > 0; 137 | } 138 | 139 | public dispose(): void { 140 | if (this.handle) { 141 | this.handle.dispose(); 142 | this.handle = null; 143 | } 144 | 145 | super.dispose(); 146 | } 147 | 148 | private onTouchStart(e: TouchEvent): void { 149 | let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based. 150 | 151 | if (this.handle) { 152 | this.handle.dispose(); 153 | this.handle = null; 154 | } 155 | 156 | for (let i = 0, len = e.targetTouches.length; i < len; i++) { 157 | let touch = e.targetTouches.item(i); 158 | 159 | this.activeTouches[touch.identifier] = { 160 | id: touch.identifier, 161 | initialTarget: touch.target, 162 | initialTimeStamp: timestamp, 163 | initialPageX: touch.pageX, 164 | initialPageY: touch.pageY, 165 | rollingTimestamps: [timestamp], 166 | rollingPageX: [touch.pageX], 167 | rollingPageY: [touch.pageY] 168 | }; 169 | 170 | let evt = this.newGestureEvent(EventType.Start, touch.target); 171 | evt.pageX = touch.pageX; 172 | evt.pageY = touch.pageY; 173 | this.dispatchEvent(evt); 174 | } 175 | 176 | if (this.dispatched) { 177 | e.preventDefault(); 178 | e.stopPropagation(); 179 | this.dispatched = false; 180 | } 181 | } 182 | 183 | private onTouchEnd(e: TouchEvent): void { 184 | let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based. 185 | 186 | let activeTouchCount = Object.keys(this.activeTouches).length; 187 | 188 | for (let i = 0, len = e.changedTouches.length; i < len; i++) { 189 | 190 | let touch = e.changedTouches.item(i); 191 | 192 | if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) { 193 | console.warn('move of an UNKNOWN touch', touch); 194 | continue; 195 | } 196 | 197 | let data = this.activeTouches[touch.identifier], 198 | holdTime = Date.now() - data.initialTimeStamp; 199 | 200 | if (holdTime < Gesture.HOLD_DELAY 201 | && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30 202 | && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) { 203 | 204 | let evt = this.newGestureEvent(EventType.Tap, data.initialTarget); 205 | evt.pageX = arrays.tail(data.rollingPageX); 206 | evt.pageY = arrays.tail(data.rollingPageY); 207 | this.dispatchEvent(evt); 208 | 209 | } else if (holdTime >= Gesture.HOLD_DELAY 210 | && Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30 211 | && Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) { 212 | 213 | let evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget); 214 | evt.pageX = arrays.tail(data.rollingPageX); 215 | evt.pageY = arrays.tail(data.rollingPageY); 216 | this.dispatchEvent(evt); 217 | 218 | } else if (activeTouchCount === 1) { 219 | let finalX = arrays.tail(data.rollingPageX); 220 | let finalY = arrays.tail(data.rollingPageY); 221 | 222 | let deltaT = arrays.tail(data.rollingTimestamps) - data.rollingTimestamps[0]; 223 | let deltaX = finalX - data.rollingPageX[0]; 224 | let deltaY = finalY - data.rollingPageY[0]; 225 | 226 | // We need to get all the dispatch targets on the start of the inertia event 227 | const dispatchTo = this.targets.filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget)); 228 | this.inertia(dispatchTo, timestamp, // time now 229 | Math.abs(deltaX) / deltaT, // speed 230 | deltaX > 0 ? 1 : -1, // x direction 231 | finalX, // x now 232 | Math.abs(deltaY) / deltaT, // y speed 233 | deltaY > 0 ? 1 : -1, // y direction 234 | finalY // y now 235 | ); 236 | } 237 | 238 | 239 | this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget)); 240 | // forget about this touch 241 | delete this.activeTouches[touch.identifier]; 242 | } 243 | 244 | if (this.dispatched) { 245 | e.preventDefault(); 246 | e.stopPropagation(); 247 | this.dispatched = false; 248 | } 249 | } 250 | 251 | private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent { 252 | let event = document.createEvent('CustomEvent') as unknown as GestureEvent; 253 | event.initEvent(type, false, true); 254 | event.initialTarget = initialTarget; 255 | event.tapCount = 0; 256 | return event; 257 | } 258 | 259 | private dispatchEvent(event: GestureEvent): void { 260 | if (event.type === EventType.Tap) { 261 | const currentTime = (new Date()).getTime(); 262 | let setTapCount = 0; 263 | if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) { 264 | setTapCount = 1; 265 | } else { 266 | setTapCount = 2; 267 | } 268 | 269 | this._lastSetTapCountTime = currentTime; 270 | event.tapCount = setTapCount; 271 | } else if (event.type === EventType.Change || event.type === EventType.Contextmenu) { 272 | // tap is canceled by scrolling or context menu 273 | this._lastSetTapCountTime = 0; 274 | } 275 | 276 | for (let i = 0; i < this.ignoreTargets.length; i++) { 277 | if (event.initialTarget instanceof Node && this.ignoreTargets[i].contains(event.initialTarget)) { 278 | return; 279 | } 280 | } 281 | 282 | this.targets.forEach(target => { 283 | if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) { 284 | target.dispatchEvent(event); 285 | this.dispatched = true; 286 | } 287 | }); 288 | } 289 | 290 | private inertia(dispatchTo: EventTarget[], t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void { 291 | this.handle = DomUtils.scheduleAtNextAnimationFrame(() => { 292 | let now = Date.now(); 293 | 294 | // velocity: old speed + accel_over_time 295 | let deltaT = now - t1, 296 | delta_pos_x = 0, delta_pos_y = 0, 297 | stopped = true; 298 | 299 | vX += Gesture.SCROLL_FRICTION * deltaT; 300 | vY += Gesture.SCROLL_FRICTION * deltaT; 301 | 302 | if (vX > 0) { 303 | stopped = false; 304 | delta_pos_x = dirX * vX * deltaT; 305 | } 306 | 307 | if (vY > 0) { 308 | stopped = false; 309 | delta_pos_y = dirY * vY * deltaT; 310 | } 311 | 312 | // dispatch translation event 313 | let evt = this.newGestureEvent(EventType.Change); 314 | evt.translationX = delta_pos_x; 315 | evt.translationY = delta_pos_y; 316 | dispatchTo.forEach(d => d.dispatchEvent(evt)); 317 | 318 | if (!stopped) { 319 | this.inertia(dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y); 320 | } 321 | }); 322 | } 323 | 324 | private onTouchMove(e: TouchEvent): void { 325 | let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based. 326 | 327 | for (let i = 0, len = e.changedTouches.length; i < len; i++) { 328 | 329 | let touch = e.changedTouches.item(i); 330 | 331 | if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) { 332 | console.warn('end of an UNKNOWN touch', touch); 333 | continue; 334 | } 335 | 336 | let data = this.activeTouches[touch.identifier]; 337 | 338 | let evt = this.newGestureEvent(EventType.Change, data.initialTarget); 339 | evt.translationX = touch.pageX - arrays.tail(data.rollingPageX); 340 | evt.translationY = touch.pageY - arrays.tail(data.rollingPageY); 341 | evt.pageX = touch.pageX; 342 | evt.pageY = touch.pageY; 343 | this.dispatchEvent(evt); 344 | 345 | // only keep a few data points, to average the final speed 346 | if (data.rollingPageX.length > 3) { 347 | data.rollingPageX.shift(); 348 | data.rollingPageY.shift(); 349 | data.rollingTimestamps.shift(); 350 | } 351 | 352 | data.rollingPageX.push(touch.pageX); 353 | data.rollingPageY.push(touch.pageY); 354 | data.rollingTimestamps.push(timestamp); 355 | } 356 | 357 | if (this.dispatched) { 358 | e.preventDefault(); 359 | e.stopPropagation(); 360 | this.dispatched = false; 361 | } 362 | } 363 | } -------------------------------------------------------------------------------- /src/menuitem.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------- 2 | * This file has been modified by @AlexTorresSk (http://github.com/AlexTorresSk) 3 | * to work in custom-electron-titlebar. 4 | * 5 | * The original copy of this file and its respective license are in https://github.com/Microsoft/vscode/ 6 | * 7 | * Copyright (c) 2018 Alex Torres 8 | * Licensed under the MIT License. See License in the project root for license information. 9 | *-------------------------------------------------------------------------------------------------------*/ 10 | 11 | import { 12 | EventType, 13 | addDisposableListener, 14 | addClass, 15 | removeClass, 16 | removeNode, 17 | append, 18 | $, 19 | hasClass, 20 | EventHelper, 21 | EventLike 22 | } from "vs/base/browser/dom"; 23 | import {BrowserWindow, Accelerator, NativeImage, MenuItem} from "electron"; 24 | import {getCurrentWindow} from '@electron/remote'; 25 | import {MENU_MNEMONIC_REGEX, cleanMnemonic, MENU_ESCAPED_MNEMONIC_REGEX} from "./mnemonic"; 26 | import {KeyCode, KeyCodeUtils} from "vs/base/common/keyCodes"; 27 | import {Disposable} from "vs/base/common/lifecycle"; 28 | import {isMacintosh} from "vs/base/common/platform"; 29 | import {IMenuItem, IMenuStyle, IMenuOptions} from './api' 30 | import * as strings from 'vs/base/common/strings'; 31 | import {EventType as TouchEventType} from 'vs/base/browser/touch'; 32 | 33 | let menuItemId = 0; 34 | 35 | export class CETMenuItem extends Disposable implements IMenuItem { 36 | 37 | protected options: IMenuOptions; 38 | protected menuStyle: IMenuStyle | undefined; 39 | protected container: HTMLElement | undefined; 40 | protected itemElement: HTMLElement | undefined; 41 | 42 | private readonly item: MenuItem; 43 | private labelElement: HTMLElement | undefined; 44 | private checkElement: HTMLElement | undefined; 45 | private iconElement: HTMLElement | undefined; 46 | private readonly mnemonic: KeyCode | undefined; 47 | protected readonly closeSubMenu: () => void; 48 | 49 | private event: Electron.Event | undefined; 50 | private readonly currentWindow: BrowserWindow; 51 | 52 | constructor(item: MenuItem, options: IMenuOptions = {}, closeSubMenu = () => { 53 | }) { 54 | super(); 55 | 56 | this.item = item; 57 | this.options = options; 58 | this.options.icon = options.icon !== undefined ? options.icon : false; 59 | this.options.label = options.label !== undefined ? options.label : true; 60 | 61 | this.currentWindow = getCurrentWindow(); 62 | this.closeSubMenu = closeSubMenu; 63 | 64 | // Set mnemonic 65 | if (this.options.label && options.enableMnemonics) { 66 | let label = this.item.label; 67 | if (label) { 68 | const matches = MENU_MNEMONIC_REGEX.exec(label); 69 | if (matches) { 70 | this.mnemonic = KeyCodeUtils.fromString((!!matches[1] ? matches[1] : matches[3]).toLocaleUpperCase()); 71 | } 72 | } 73 | } 74 | 75 | //this.item._id = menuItemId++; 76 | } 77 | 78 | getContainer() { 79 | return this.container; 80 | } 81 | 82 | isEnabled(): boolean { 83 | return this.item.enabled; 84 | } 85 | 86 | isSeparator(): boolean { 87 | return this.item.type === 'separator'; 88 | } 89 | 90 | render(container: HTMLElement): void { 91 | if (!container) return; 92 | 93 | this.container = container; 94 | 95 | this._register(addDisposableListener(this.container!, TouchEventType.Tap, e => this.onClick(e))); 96 | 97 | this._register(addDisposableListener(this.container!, EventType.MOUSE_DOWN, e => { 98 | if (this.item.enabled && e.button === 0) { 99 | this.container!.classList.add('active'); 100 | } 101 | })); 102 | 103 | this._register(addDisposableListener(this.container, EventType.CLICK, e => { 104 | if (this.item.enabled) { 105 | this.onClick(e); 106 | } 107 | })); 108 | 109 | this._register(addDisposableListener(this.container, EventType.DBLCLICK, e => { 110 | EventHelper.stop(e, true); 111 | })); 112 | 113 | [EventType.MOUSE_UP, EventType.MOUSE_OUT].forEach(event => { 114 | this._register(addDisposableListener(this.container!, event, e => { 115 | EventHelper.stop(e); 116 | this.container!.classList.remove('active'); 117 | })); 118 | }); 119 | 120 | this.itemElement = append(this.container, $('a.action-menu-item')); 121 | this.itemElement.setAttribute('role', 'menuitem'); 122 | 123 | if (this.mnemonic) { 124 | this.itemElement.setAttribute('aria-keyshortcuts', `${this.mnemonic}`); 125 | } 126 | 127 | this.checkElement = append(this.itemElement, $('span.menu-item-check')); 128 | this.checkElement.setAttribute('role', 'none'); 129 | 130 | this.iconElement = append(this.itemElement, $('span.menu-item-icon')); 131 | this.iconElement.setAttribute('role', 'none'); 132 | 133 | this.labelElement = append(this.itemElement, $('span.action-label')); 134 | 135 | this.setAccelerator(); 136 | this.updateLabel(); 137 | this.updateIcon(); 138 | this.updateTooltip(); 139 | this.updateEnabled(); 140 | this.updateChecked(); 141 | this.updateVisibility(); 142 | } 143 | 144 | onClick(event: EventLike) { 145 | EventHelper.stop(event, true); 146 | 147 | if (this.item.click) { 148 | this.item.click(this.item as MenuItem, this.currentWindow, this.event); 149 | } 150 | 151 | if (this.item.type === 'checkbox') { 152 | this.item.checked = !this.item.checked; 153 | this.updateChecked(); 154 | } 155 | 156 | this.closeSubMenu(); 157 | } 158 | 159 | focus(): void { 160 | if (this.container) { 161 | this.container.focus(); 162 | addClass(this.container, 'focused'); 163 | } 164 | 165 | this.applyStyle(); 166 | } 167 | 168 | blur(): void { 169 | if (this.container) { 170 | this.container.blur(); 171 | removeClass(this.container, 'focused'); 172 | } 173 | 174 | this.applyStyle(); 175 | } 176 | 177 | setAccelerator(): void { 178 | let accelerator = null; 179 | if (this.item.role) { 180 | switch (this.item.role.toLocaleLowerCase()) { 181 | case 'undo': 182 | accelerator = 'CtrlOrCmd+Z'; 183 | break; 184 | case 'redo': 185 | accelerator = 'CtrlOrCmd+Y'; 186 | break; 187 | case 'cut': 188 | accelerator = 'CtrlOrCmd+X'; 189 | break; 190 | case 'copy': 191 | accelerator = 'CtrlOrCmd+C'; 192 | break; 193 | case 'paste': 194 | accelerator = 'CtrlOrCmd+V'; 195 | break; 196 | case 'selectall': 197 | accelerator = 'CtrlOrCmd+A'; 198 | break; 199 | case 'minimize': 200 | accelerator = 'CtrlOrCmd+M'; 201 | break; 202 | case 'close': 203 | accelerator = 'CtrlOrCmd+W'; 204 | break; 205 | case 'reload': 206 | accelerator = 'CtrlOrCmd+R'; 207 | break; 208 | case 'forcereload': 209 | accelerator = 'CtrlOrCmd+Shift+R'; 210 | break; 211 | case 'toggledevtools': 212 | accelerator = 'CtrlOrCmd+Shift+I'; 213 | break; 214 | case 'togglefullscreen': 215 | accelerator = 'F11'; 216 | break; 217 | case 'resetzoom': 218 | accelerator = 'CtrlOrCmd+0'; 219 | break; 220 | case 'zoomin': 221 | accelerator = 'CtrlOrCmd+Shift+='; 222 | break; 223 | case 'zoomout': 224 | accelerator = 'CtrlOrCmd+-'; 225 | break; 226 | } 227 | } 228 | 229 | if (this.item.label && this.item.accelerator) { 230 | accelerator = this.item.accelerator; 231 | } 232 | 233 | if (this.itemElement && accelerator !== null) { 234 | append(this.itemElement, $('span.keybinding')).textContent = parseAccelerator(accelerator); 235 | } 236 | } 237 | 238 | updateLabel(): void { 239 | if (this.item.label) { 240 | let label = this.item.label; 241 | 242 | if (label) { 243 | const cleanLabel = cleanMnemonic(label); 244 | 245 | if (!this.options.enableMnemonics) { 246 | label = cleanLabel; 247 | } 248 | 249 | if (this.labelElement) { 250 | this.labelElement.setAttribute('aria-label', cleanLabel.replace(/&&/g, '&')); 251 | } 252 | 253 | const matches = MENU_MNEMONIC_REGEX.exec(label); 254 | 255 | if (matches) { 256 | label = strings.escape(label); 257 | 258 | // This is global, reset it 259 | MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0; 260 | let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(label); 261 | 262 | // We can't use negative lookbehind so if we match our negative and skip 263 | while (escMatch && escMatch[1]) { 264 | escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(label); 265 | } 266 | 267 | const replaceDoubleEscapes = (str: string) => str.replace(/&&/g, '&'); 268 | 269 | if (escMatch) { 270 | label = `${label.substr(0, escMatch.index)}${label.substr(escMatch.index + escMatch[0].length)}`; 271 | } else { 272 | 273 | } 274 | 275 | label = label.replace(/&&/g, '&'); 276 | if (this.itemElement) { 277 | this.itemElement.setAttribute('aria-keyshortcuts', (!!matches[1] ? matches[1] : matches[3]).toLocaleLowerCase()); 278 | } 279 | } else { 280 | label = label.replace(/&&/g, '&'); 281 | } 282 | } 283 | 284 | if (this.labelElement) { 285 | this.labelElement.innerHTML = label.trim(); 286 | } 287 | } 288 | } 289 | 290 | updateIcon(): void { 291 | let icon: string | NativeImage | null = null; 292 | 293 | if (this.item.icon) { 294 | icon = this.item.icon; 295 | } 296 | 297 | if (this.iconElement && icon) { 298 | const iconE = append(this.iconElement, $('img')); 299 | iconE.setAttribute('src', icon.toString()); 300 | } 301 | } 302 | 303 | updateTooltip(): void { 304 | let title: string | null = null; 305 | 306 | if (this.item.sublabel) { 307 | title = this.item.sublabel; 308 | } else if (!this.item.label && this.item.label && this.item.icon) { 309 | title = this.item.label; 310 | 311 | if (this.item.accelerator) { 312 | title = parseAccelerator(this.item.accelerator); 313 | } 314 | } 315 | 316 | if (this.itemElement && title) { 317 | this.itemElement.title = title; 318 | } 319 | } 320 | 321 | updateEnabled(): void { 322 | if (!this.container) return; 323 | 324 | if (this.item.enabled && this.item.type !== 'separator') { 325 | removeClass(this.container, 'disabled'); 326 | this.container.tabIndex = 0; 327 | } else { 328 | addClass(this.container, 'disabled'); 329 | } 330 | } 331 | 332 | updateVisibility(): void { 333 | if (!this.item.visible && this.itemElement) { 334 | this.itemElement.remove(); 335 | } 336 | } 337 | 338 | updateChecked(): void { 339 | if (!this.itemElement) return 340 | 341 | if (this.item.checked) { 342 | this.itemElement.classList.add('checked') 343 | this.itemElement.setAttribute('role', 'menuitemcheckbox'); 344 | this.itemElement.setAttribute('aria-checked', 'true'); 345 | } else { 346 | this.itemElement.classList.remove('checked') 347 | this.itemElement.setAttribute('role', 'menuitem'); 348 | this.itemElement.setAttribute('aria-checked', 'false'); 349 | } 350 | } 351 | 352 | dispose(): void { 353 | if (this.itemElement) { 354 | this.itemElement.remove(); 355 | this.itemElement = undefined; 356 | } 357 | 358 | super.dispose(); 359 | } 360 | 361 | getMnemonic(): KeyCode | undefined { 362 | return this.mnemonic; 363 | } 364 | 365 | protected applyStyle() { 366 | if (!this.menuStyle) { 367 | return; 368 | } 369 | 370 | const isSelected = this.container && this.container.classList.contains('focused'); 371 | const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor; 372 | const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : this.menuStyle.backgroundColor; 373 | 374 | if (!this.checkElement || !this.itemElement) { 375 | return; 376 | } 377 | 378 | if (fgColor) { 379 | this.itemElement.style.color = fgColor.toString(); 380 | this.checkElement.style.backgroundColor = fgColor.toString(); 381 | } else { 382 | this.itemElement.style.removeProperty('color'); 383 | this.checkElement.style.removeProperty('background-color'); 384 | } 385 | 386 | if (bgColor) { 387 | this.itemElement.style.backgroundColor = bgColor.toString(); 388 | } else { 389 | this.itemElement.style.removeProperty('background-color'); 390 | } 391 | } 392 | 393 | style(style: IMenuStyle): void { 394 | this.menuStyle = style; 395 | this.applyStyle(); 396 | } 397 | } 398 | 399 | function parseAccelerator(a: Accelerator): string { 400 | let accelerator; 401 | if (!isMacintosh) { 402 | accelerator = a.replace(/(Cmd)|(Command)/gi, ''); 403 | } else { 404 | accelerator = a.replace(/(Ctrl)|(Control)/gi, ''); 405 | } 406 | 407 | accelerator = accelerator.replace(/(Or)/gi, ''); 408 | 409 | return accelerator; 410 | } 411 | -------------------------------------------------------------------------------- /src/vs/base/common/keyCodes.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { OperatingSystem } from 'vs/base/common/platform'; 7 | import { illegalArgument } from 'vs/base/common/errors'; 8 | 9 | /** 10 | * Virtual Key Codes, the value does not hold any inherent meaning. 11 | * Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 12 | * But these are "more general", as they should work across browsers & OS`s. 13 | */ 14 | export const enum KeyCode { 15 | /** 16 | * Placed first to cover the 0 value of the enum. 17 | */ 18 | Unknown = 0, 19 | 20 | Backspace = 1, 21 | Tab = 2, 22 | Enter = 3, 23 | Shift = 4, 24 | Ctrl = 5, 25 | Alt = 6, 26 | PauseBreak = 7, 27 | CapsLock = 8, 28 | Escape = 9, 29 | Space = 10, 30 | PageUp = 11, 31 | PageDown = 12, 32 | End = 13, 33 | Home = 14, 34 | LeftArrow = 15, 35 | UpArrow = 16, 36 | RightArrow = 17, 37 | DownArrow = 18, 38 | Insert = 19, 39 | Delete = 20, 40 | 41 | KEY_0 = 21, 42 | KEY_1 = 22, 43 | KEY_2 = 23, 44 | KEY_3 = 24, 45 | KEY_4 = 25, 46 | KEY_5 = 26, 47 | KEY_6 = 27, 48 | KEY_7 = 28, 49 | KEY_8 = 29, 50 | KEY_9 = 30, 51 | 52 | KEY_A = 31, 53 | KEY_B = 32, 54 | KEY_C = 33, 55 | KEY_D = 34, 56 | KEY_E = 35, 57 | KEY_F = 36, 58 | KEY_G = 37, 59 | KEY_H = 38, 60 | KEY_I = 39, 61 | KEY_J = 40, 62 | KEY_K = 41, 63 | KEY_L = 42, 64 | KEY_M = 43, 65 | KEY_N = 44, 66 | KEY_O = 45, 67 | KEY_P = 46, 68 | KEY_Q = 47, 69 | KEY_R = 48, 70 | KEY_S = 49, 71 | KEY_T = 50, 72 | KEY_U = 51, 73 | KEY_V = 52, 74 | KEY_W = 53, 75 | KEY_X = 54, 76 | KEY_Y = 55, 77 | KEY_Z = 56, 78 | 79 | Meta = 57, 80 | ContextMenu = 58, 81 | 82 | F1 = 59, 83 | F2 = 60, 84 | F3 = 61, 85 | F4 = 62, 86 | F5 = 63, 87 | F6 = 64, 88 | F7 = 65, 89 | F8 = 66, 90 | F9 = 67, 91 | F10 = 68, 92 | F11 = 69, 93 | F12 = 70, 94 | F13 = 71, 95 | F14 = 72, 96 | F15 = 73, 97 | F16 = 74, 98 | F17 = 75, 99 | F18 = 76, 100 | F19 = 77, 101 | 102 | NumLock = 78, 103 | ScrollLock = 79, 104 | 105 | /** 106 | * Used for miscellaneous characters; it can vary by keyboard. 107 | * For the US standard keyboard, the ';:' key 108 | */ 109 | US_SEMICOLON = 80, 110 | /** 111 | * For any country/region, the '+' key 112 | * For the US standard keyboard, the '=+' key 113 | */ 114 | US_EQUAL = 81, 115 | /** 116 | * For any country/region, the ',' key 117 | * For the US standard keyboard, the ',<' key 118 | */ 119 | US_COMMA = 82, 120 | /** 121 | * For any country/region, the '-' key 122 | * For the US standard keyboard, the '-_' key 123 | */ 124 | US_MINUS = 83, 125 | /** 126 | * For any country/region, the '.' key 127 | * For the US standard keyboard, the '.>' key 128 | */ 129 | US_DOT = 84, 130 | /** 131 | * Used for miscellaneous characters; it can vary by keyboard. 132 | * For the US standard keyboard, the '/?' key 133 | */ 134 | US_SLASH = 85, 135 | /** 136 | * Used for miscellaneous characters; it can vary by keyboard. 137 | * For the US standard keyboard, the '`~' key 138 | */ 139 | US_BACKTICK = 86, 140 | /** 141 | * Used for miscellaneous characters; it can vary by keyboard. 142 | * For the US standard keyboard, the '[{' key 143 | */ 144 | US_OPEN_SQUARE_BRACKET = 87, 145 | /** 146 | * Used for miscellaneous characters; it can vary by keyboard. 147 | * For the US standard keyboard, the '\|' key 148 | */ 149 | US_BACKSLASH = 88, 150 | /** 151 | * Used for miscellaneous characters; it can vary by keyboard. 152 | * For the US standard keyboard, the ']}' key 153 | */ 154 | US_CLOSE_SQUARE_BRACKET = 89, 155 | /** 156 | * Used for miscellaneous characters; it can vary by keyboard. 157 | * For the US standard keyboard, the ''"' key 158 | */ 159 | US_QUOTE = 90, 160 | /** 161 | * Used for miscellaneous characters; it can vary by keyboard. 162 | */ 163 | OEM_8 = 91, 164 | /** 165 | * Either the angle bracket key or the backslash key on the RT 102-key keyboard. 166 | */ 167 | OEM_102 = 92, 168 | 169 | NUMPAD_0 = 93, // VK_NUMPAD0, 0x60, Numeric keypad 0 key 170 | NUMPAD_1 = 94, // VK_NUMPAD1, 0x61, Numeric keypad 1 key 171 | NUMPAD_2 = 95, // VK_NUMPAD2, 0x62, Numeric keypad 2 key 172 | NUMPAD_3 = 96, // VK_NUMPAD3, 0x63, Numeric keypad 3 key 173 | NUMPAD_4 = 97, // VK_NUMPAD4, 0x64, Numeric keypad 4 key 174 | NUMPAD_5 = 98, // VK_NUMPAD5, 0x65, Numeric keypad 5 key 175 | NUMPAD_6 = 99, // VK_NUMPAD6, 0x66, Numeric keypad 6 key 176 | NUMPAD_7 = 100, // VK_NUMPAD7, 0x67, Numeric keypad 7 key 177 | NUMPAD_8 = 101, // VK_NUMPAD8, 0x68, Numeric keypad 8 key 178 | NUMPAD_9 = 102, // VK_NUMPAD9, 0x69, Numeric keypad 9 key 179 | 180 | NUMPAD_MULTIPLY = 103, // VK_MULTIPLY, 0x6A, Multiply key 181 | NUMPAD_ADD = 104, // VK_ADD, 0x6B, Add key 182 | NUMPAD_SEPARATOR = 105, // VK_SEPARATOR, 0x6C, Separator key 183 | NUMPAD_SUBTRACT = 106, // VK_SUBTRACT, 0x6D, Subtract key 184 | NUMPAD_DECIMAL = 107, // VK_DECIMAL, 0x6E, Decimal key 185 | NUMPAD_DIVIDE = 108, // VK_DIVIDE, 0x6F, 186 | 187 | /** 188 | * Cover all key codes when IME is processing input. 189 | */ 190 | KEY_IN_COMPOSITION = 109, 191 | 192 | ABNT_C1 = 110, // Brazilian (ABNT) Keyboard 193 | ABNT_C2 = 111, // Brazilian (ABNT) Keyboard 194 | 195 | /** 196 | * Placed last to cover the length of the enum. 197 | * Please do not depend on this value! 198 | */ 199 | MAX_VALUE 200 | } 201 | 202 | class KeyCodeStrMap { 203 | 204 | private _keyCodeToStr: string[]; 205 | private _strToKeyCode: { [str: string]: KeyCode; }; 206 | 207 | constructor() { 208 | this._keyCodeToStr = []; 209 | this._strToKeyCode = Object.create(null); 210 | } 211 | 212 | define(keyCode: KeyCode, str: string): void { 213 | this._keyCodeToStr[keyCode] = str; 214 | this._strToKeyCode[str.toLowerCase()] = keyCode; 215 | } 216 | 217 | keyCodeToStr(keyCode: KeyCode): string { 218 | return this._keyCodeToStr[keyCode]; 219 | } 220 | 221 | strToKeyCode(str: string): KeyCode { 222 | return this._strToKeyCode[str.toLowerCase()] || KeyCode.Unknown; 223 | } 224 | } 225 | 226 | const uiMap = new KeyCodeStrMap(); 227 | const userSettingsUSMap = new KeyCodeStrMap(); 228 | const userSettingsGeneralMap = new KeyCodeStrMap(); 229 | 230 | (function () { 231 | 232 | function define(keyCode: KeyCode, uiLabel: string, usUserSettingsLabel: string = uiLabel, generalUserSettingsLabel: string = usUserSettingsLabel): void { 233 | uiMap.define(keyCode, uiLabel); 234 | userSettingsUSMap.define(keyCode, usUserSettingsLabel); 235 | userSettingsGeneralMap.define(keyCode, generalUserSettingsLabel); 236 | } 237 | 238 | define(KeyCode.Unknown, 'unknown'); 239 | 240 | define(KeyCode.Backspace, 'Backspace'); 241 | define(KeyCode.Tab, 'Tab'); 242 | define(KeyCode.Enter, 'Enter'); 243 | define(KeyCode.Shift, 'Shift'); 244 | define(KeyCode.Ctrl, 'Ctrl'); 245 | define(KeyCode.Alt, 'Alt'); 246 | define(KeyCode.PauseBreak, 'PauseBreak'); 247 | define(KeyCode.CapsLock, 'CapsLock'); 248 | define(KeyCode.Escape, 'Escape'); 249 | define(KeyCode.Space, 'Space'); 250 | define(KeyCode.PageUp, 'PageUp'); 251 | define(KeyCode.PageDown, 'PageDown'); 252 | define(KeyCode.End, 'End'); 253 | define(KeyCode.Home, 'Home'); 254 | 255 | define(KeyCode.LeftArrow, 'LeftArrow', 'Left'); 256 | define(KeyCode.UpArrow, 'UpArrow', 'Up'); 257 | define(KeyCode.RightArrow, 'RightArrow', 'Right'); 258 | define(KeyCode.DownArrow, 'DownArrow', 'Down'); 259 | define(KeyCode.Insert, 'Insert'); 260 | define(KeyCode.Delete, 'Delete'); 261 | 262 | define(KeyCode.KEY_0, '0'); 263 | define(KeyCode.KEY_1, '1'); 264 | define(KeyCode.KEY_2, '2'); 265 | define(KeyCode.KEY_3, '3'); 266 | define(KeyCode.KEY_4, '4'); 267 | define(KeyCode.KEY_5, '5'); 268 | define(KeyCode.KEY_6, '6'); 269 | define(KeyCode.KEY_7, '7'); 270 | define(KeyCode.KEY_8, '8'); 271 | define(KeyCode.KEY_9, '9'); 272 | 273 | define(KeyCode.KEY_A, 'A'); 274 | define(KeyCode.KEY_B, 'B'); 275 | define(KeyCode.KEY_C, 'C'); 276 | define(KeyCode.KEY_D, 'D'); 277 | define(KeyCode.KEY_E, 'E'); 278 | define(KeyCode.KEY_F, 'F'); 279 | define(KeyCode.KEY_G, 'G'); 280 | define(KeyCode.KEY_H, 'H'); 281 | define(KeyCode.KEY_I, 'I'); 282 | define(KeyCode.KEY_J, 'J'); 283 | define(KeyCode.KEY_K, 'K'); 284 | define(KeyCode.KEY_L, 'L'); 285 | define(KeyCode.KEY_M, 'M'); 286 | define(KeyCode.KEY_N, 'N'); 287 | define(KeyCode.KEY_O, 'O'); 288 | define(KeyCode.KEY_P, 'P'); 289 | define(KeyCode.KEY_Q, 'Q'); 290 | define(KeyCode.KEY_R, 'R'); 291 | define(KeyCode.KEY_S, 'S'); 292 | define(KeyCode.KEY_T, 'T'); 293 | define(KeyCode.KEY_U, 'U'); 294 | define(KeyCode.KEY_V, 'V'); 295 | define(KeyCode.KEY_W, 'W'); 296 | define(KeyCode.KEY_X, 'X'); 297 | define(KeyCode.KEY_Y, 'Y'); 298 | define(KeyCode.KEY_Z, 'Z'); 299 | 300 | define(KeyCode.Meta, 'Meta'); 301 | define(KeyCode.ContextMenu, 'ContextMenu'); 302 | 303 | define(KeyCode.F1, 'F1'); 304 | define(KeyCode.F2, 'F2'); 305 | define(KeyCode.F3, 'F3'); 306 | define(KeyCode.F4, 'F4'); 307 | define(KeyCode.F5, 'F5'); 308 | define(KeyCode.F6, 'F6'); 309 | define(KeyCode.F7, 'F7'); 310 | define(KeyCode.F8, 'F8'); 311 | define(KeyCode.F9, 'F9'); 312 | define(KeyCode.F10, 'F10'); 313 | define(KeyCode.F11, 'F11'); 314 | define(KeyCode.F12, 'F12'); 315 | define(KeyCode.F13, 'F13'); 316 | define(KeyCode.F14, 'F14'); 317 | define(KeyCode.F15, 'F15'); 318 | define(KeyCode.F16, 'F16'); 319 | define(KeyCode.F17, 'F17'); 320 | define(KeyCode.F18, 'F18'); 321 | define(KeyCode.F19, 'F19'); 322 | 323 | define(KeyCode.NumLock, 'NumLock'); 324 | define(KeyCode.ScrollLock, 'ScrollLock'); 325 | 326 | define(KeyCode.US_SEMICOLON, ';', ';', 'OEM_1'); 327 | define(KeyCode.US_EQUAL, '=', '=', 'OEM_PLUS'); 328 | define(KeyCode.US_COMMA, ',', ',', 'OEM_COMMA'); 329 | define(KeyCode.US_MINUS, '-', '-', 'OEM_MINUS'); 330 | define(KeyCode.US_DOT, '.', '.', 'OEM_PERIOD'); 331 | define(KeyCode.US_SLASH, '/', '/', 'OEM_2'); 332 | define(KeyCode.US_BACKTICK, '`', '`', 'OEM_3'); 333 | define(KeyCode.ABNT_C1, 'ABNT_C1'); 334 | define(KeyCode.ABNT_C2, 'ABNT_C2'); 335 | define(KeyCode.US_OPEN_SQUARE_BRACKET, '[', '[', 'OEM_4'); 336 | define(KeyCode.US_BACKSLASH, '\\', '\\', 'OEM_5'); 337 | define(KeyCode.US_CLOSE_SQUARE_BRACKET, ']', ']', 'OEM_6'); 338 | define(KeyCode.US_QUOTE, '\'', '\'', 'OEM_7'); 339 | define(KeyCode.OEM_8, 'OEM_8'); 340 | define(KeyCode.OEM_102, 'OEM_102'); 341 | 342 | define(KeyCode.NUMPAD_0, 'NumPad0'); 343 | define(KeyCode.NUMPAD_1, 'NumPad1'); 344 | define(KeyCode.NUMPAD_2, 'NumPad2'); 345 | define(KeyCode.NUMPAD_3, 'NumPad3'); 346 | define(KeyCode.NUMPAD_4, 'NumPad4'); 347 | define(KeyCode.NUMPAD_5, 'NumPad5'); 348 | define(KeyCode.NUMPAD_6, 'NumPad6'); 349 | define(KeyCode.NUMPAD_7, 'NumPad7'); 350 | define(KeyCode.NUMPAD_8, 'NumPad8'); 351 | define(KeyCode.NUMPAD_9, 'NumPad9'); 352 | 353 | define(KeyCode.NUMPAD_MULTIPLY, 'NumPad_Multiply'); 354 | define(KeyCode.NUMPAD_ADD, 'NumPad_Add'); 355 | define(KeyCode.NUMPAD_SEPARATOR, 'NumPad_Separator'); 356 | define(KeyCode.NUMPAD_SUBTRACT, 'NumPad_Subtract'); 357 | define(KeyCode.NUMPAD_DECIMAL, 'NumPad_Decimal'); 358 | define(KeyCode.NUMPAD_DIVIDE, 'NumPad_Divide'); 359 | 360 | })(); 361 | 362 | export namespace KeyCodeUtils { 363 | export function toString(keyCode: KeyCode): string { 364 | return uiMap.keyCodeToStr(keyCode); 365 | } 366 | export function fromString(key: string): KeyCode { 367 | return uiMap.strToKeyCode(key); 368 | } 369 | 370 | export function toUserSettingsUS(keyCode: KeyCode): string { 371 | return userSettingsUSMap.keyCodeToStr(keyCode); 372 | } 373 | export function toUserSettingsGeneral(keyCode: KeyCode): string { 374 | return userSettingsGeneralMap.keyCodeToStr(keyCode); 375 | } 376 | export function fromUserSettings(key: string): KeyCode { 377 | return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key); 378 | } 379 | } 380 | 381 | /** 382 | * Binary encoding strategy: 383 | * ``` 384 | * 1111 11 385 | * 5432 1098 7654 3210 386 | * ---- CSAW KKKK KKKK 387 | * C = bit 11 = ctrlCmd flag 388 | * S = bit 10 = shift flag 389 | * A = bit 9 = alt flag 390 | * W = bit 8 = winCtrl flag 391 | * K = bits 0-7 = key code 392 | * ``` 393 | */ 394 | const enum BinaryKeybindingsMask { 395 | CtrlCmd = (1 << 11) >>> 0, 396 | Shift = (1 << 10) >>> 0, 397 | Alt = (1 << 9) >>> 0, 398 | WinCtrl = (1 << 8) >>> 0, 399 | KeyCode = 0x000000FF 400 | } 401 | 402 | export const enum KeyMod { 403 | CtrlCmd = (1 << 11) >>> 0, 404 | Shift = (1 << 10) >>> 0, 405 | Alt = (1 << 9) >>> 0, 406 | WinCtrl = (1 << 8) >>> 0, 407 | } 408 | 409 | export function KeyChord(firstPart: number, secondPart: number): number { 410 | const chordPart = ((secondPart & 0x0000FFFF) << 16) >>> 0; 411 | return (firstPart | chordPart) >>> 0; 412 | } 413 | 414 | export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null { 415 | if (keybinding === 0) { 416 | return null; 417 | } 418 | const firstPart = (keybinding & 0x0000FFFF) >>> 0; 419 | const chordPart = (keybinding & 0xFFFF0000) >>> 16; 420 | if (chordPart !== 0) { 421 | return new ChordKeybinding([ 422 | createSimpleKeybinding(firstPart, OS), 423 | createSimpleKeybinding(chordPart, OS) 424 | ]); 425 | } 426 | return new ChordKeybinding([createSimpleKeybinding(firstPart, OS)]); 427 | } 428 | 429 | export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): SimpleKeybinding { 430 | 431 | const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false); 432 | const winCtrl = (keybinding & BinaryKeybindingsMask.WinCtrl ? true : false); 433 | 434 | const ctrlKey = (OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd); 435 | const shiftKey = (keybinding & BinaryKeybindingsMask.Shift ? true : false); 436 | const altKey = (keybinding & BinaryKeybindingsMask.Alt ? true : false); 437 | const metaKey = (OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl); 438 | const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode); 439 | 440 | return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, keyCode); 441 | } 442 | 443 | export class SimpleKeybinding { 444 | public readonly ctrlKey: boolean; 445 | public readonly shiftKey: boolean; 446 | public readonly altKey: boolean; 447 | public readonly metaKey: boolean; 448 | public readonly keyCode: KeyCode; 449 | 450 | constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, keyCode: KeyCode) { 451 | this.ctrlKey = ctrlKey; 452 | this.shiftKey = shiftKey; 453 | this.altKey = altKey; 454 | this.metaKey = metaKey; 455 | this.keyCode = keyCode; 456 | } 457 | 458 | public equals(other: SimpleKeybinding): boolean { 459 | return ( 460 | this.ctrlKey === other.ctrlKey 461 | && this.shiftKey === other.shiftKey 462 | && this.altKey === other.altKey 463 | && this.metaKey === other.metaKey 464 | && this.keyCode === other.keyCode 465 | ); 466 | } 467 | 468 | public getHashCode(): string { 469 | const ctrl = this.ctrlKey ? '1' : '0'; 470 | const shift = this.shiftKey ? '1' : '0'; 471 | const alt = this.altKey ? '1' : '0'; 472 | const meta = this.metaKey ? '1' : '0'; 473 | return `${ctrl}${shift}${alt}${meta}${this.keyCode}`; 474 | } 475 | 476 | public isModifierKey(): boolean { 477 | return ( 478 | this.keyCode === KeyCode.Unknown 479 | || this.keyCode === KeyCode.Ctrl 480 | || this.keyCode === KeyCode.Meta 481 | || this.keyCode === KeyCode.Alt 482 | || this.keyCode === KeyCode.Shift 483 | ); 484 | } 485 | 486 | public toChord(): ChordKeybinding { 487 | return new ChordKeybinding([this]); 488 | } 489 | 490 | /** 491 | * Does this keybinding refer to the key code of a modifier and it also has the modifier flag? 492 | */ 493 | public isDuplicateModifierCase(): boolean { 494 | return ( 495 | (this.ctrlKey && this.keyCode === KeyCode.Ctrl) 496 | || (this.shiftKey && this.keyCode === KeyCode.Shift) 497 | || (this.altKey && this.keyCode === KeyCode.Alt) 498 | || (this.metaKey && this.keyCode === KeyCode.Meta) 499 | ); 500 | } 501 | } 502 | 503 | export class ChordKeybinding { 504 | public readonly parts: SimpleKeybinding[]; 505 | 506 | constructor(parts: SimpleKeybinding[]) { 507 | if (parts.length === 0) { 508 | throw illegalArgument(`parts`); 509 | } 510 | this.parts = parts; 511 | } 512 | 513 | public getHashCode(): string { 514 | let result = ''; 515 | for (let i = 0, len = this.parts.length; i < len; i++) { 516 | if (i !== 0) { 517 | result += ';'; 518 | } 519 | result += this.parts[i].getHashCode(); 520 | } 521 | return result; 522 | } 523 | 524 | public equals(other: ChordKeybinding | null): boolean { 525 | if (other === null) { 526 | return false; 527 | } 528 | if (this.parts.length !== other.parts.length) { 529 | return false; 530 | } 531 | for (let i = 0; i < this.parts.length; i++) { 532 | if (!this.parts[i].equals(other.parts[i])) { 533 | return false; 534 | } 535 | } 536 | return true; 537 | } 538 | } 539 | 540 | export type Keybinding = ChordKeybinding; 541 | 542 | export class ResolvedKeybindingPart { 543 | readonly ctrlKey: boolean; 544 | readonly shiftKey: boolean; 545 | readonly altKey: boolean; 546 | readonly metaKey: boolean; 547 | 548 | readonly keyLabel: string | null; 549 | readonly keyAriaLabel: string | null; 550 | 551 | constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, kbLabel: string | null, kbAriaLabel: string | null) { 552 | this.ctrlKey = ctrlKey; 553 | this.shiftKey = shiftKey; 554 | this.altKey = altKey; 555 | this.metaKey = metaKey; 556 | this.keyLabel = kbLabel; 557 | this.keyAriaLabel = kbAriaLabel; 558 | } 559 | } 560 | 561 | /** 562 | * A resolved keybinding. Can be a simple keybinding or a chord keybinding. 563 | */ 564 | export abstract class ResolvedKeybinding { 565 | /** 566 | * This prints the binding in a format suitable for displaying in the UI. 567 | */ 568 | public abstract getLabel(): string | null; 569 | /** 570 | * This prints the binding in a format suitable for ARIA. 571 | */ 572 | public abstract getAriaLabel(): string | null; 573 | /** 574 | * This prints the binding in a format suitable for electron's accelerators. 575 | * See https://github.com/electron/electron/blob/master/docs/api/accelerator.md 576 | */ 577 | public abstract getElectronAccelerator(): string | null; 578 | /** 579 | * This prints the binding in a format suitable for user settings. 580 | */ 581 | public abstract getUserSettingsLabel(): string | null; 582 | /** 583 | * Is the user settings label reflecting the label? 584 | */ 585 | public abstract isWYSIWYG(): boolean; 586 | 587 | /** 588 | * Is the binding a chord? 589 | */ 590 | public abstract isChord(): boolean; 591 | 592 | /** 593 | * Returns the parts that comprise of the keybinding. 594 | * Simple keybindings return one element. 595 | */ 596 | public abstract getParts(): ResolvedKeybindingPart[]; 597 | 598 | /** 599 | * Returns the parts that should be used for dispatching. 600 | */ 601 | public abstract getDispatchParts(): (string | null)[]; 602 | } 603 | -------------------------------------------------------------------------------- /src/vs/base/common/arrays.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CancellationToken } from 'vs/base/common/cancellation'; 7 | import { canceled } from 'vs/base/common/errors'; 8 | import { ISplice } from 'vs/base/common/sequence'; 9 | 10 | /** 11 | * Returns the last element of an array. 12 | * @param array The array. 13 | * @param n Which element from the end (default is zero). 14 | */ 15 | export function tail(array: ArrayLike, n: number = 0): T { 16 | return array[array.length - (1 + n)]; 17 | } 18 | 19 | export function tail2(arr: T[]): [T[], T] { 20 | if (arr.length === 0) { 21 | throw new Error('Invalid tail call'); 22 | } 23 | 24 | return [arr.slice(0, arr.length - 1), arr[arr.length - 1]]; 25 | } 26 | 27 | export function equals(one: ReadonlyArray | undefined, other: ReadonlyArray | undefined, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { 28 | if (one === other) { 29 | return true; 30 | } 31 | 32 | if (!one || !other) { 33 | return false; 34 | } 35 | 36 | if (one.length !== other.length) { 37 | return false; 38 | } 39 | 40 | for (let i = 0, len = one.length; i < len; i++) { 41 | if (!itemEquals(one[i], other[i])) { 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | } 48 | 49 | export function binarySearch(array: ReadonlyArray, key: T, comparator: (op1: T, op2: T) => number): number { 50 | let low = 0, 51 | high = array.length - 1; 52 | 53 | while (low <= high) { 54 | const mid = ((low + high) / 2) | 0; 55 | const comp = comparator(array[mid], key); 56 | if (comp < 0) { 57 | low = mid + 1; 58 | } else if (comp > 0) { 59 | high = mid - 1; 60 | } else { 61 | return mid; 62 | } 63 | } 64 | return -(low + 1); 65 | } 66 | 67 | /** 68 | * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false 69 | * are located before all elements where p(x) is true. 70 | * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. 71 | */ 72 | export function findFirstInSorted(array: ReadonlyArray, p: (x: T) => boolean): number { 73 | let low = 0, high = array.length; 74 | if (high === 0) { 75 | return 0; // no children 76 | } 77 | while (low < high) { 78 | const mid = Math.floor((low + high) / 2); 79 | if (p(array[mid])) { 80 | high = mid; 81 | } else { 82 | low = mid + 1; 83 | } 84 | } 85 | return low; 86 | } 87 | 88 | type Compare = (a: T, b: T) => number; 89 | 90 | /** 91 | * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` 92 | * so only use this when actually needing stable sort. 93 | */ 94 | export function mergeSort(data: T[], compare: Compare): T[] { 95 | _sort(data, compare, 0, data.length - 1, []); 96 | return data; 97 | } 98 | 99 | function _merge(a: T[], compare: Compare, lo: number, mid: number, hi: number, aux: T[]): void { 100 | let leftIdx = lo, rightIdx = mid + 1; 101 | for (let i = lo; i <= hi; i++) { 102 | aux[i] = a[i]; 103 | } 104 | for (let i = lo; i <= hi; i++) { 105 | if (leftIdx > mid) { 106 | // left side consumed 107 | a[i] = aux[rightIdx++]; 108 | } else if (rightIdx > hi) { 109 | // right side consumed 110 | a[i] = aux[leftIdx++]; 111 | } else if (compare(aux[rightIdx], aux[leftIdx]) < 0) { 112 | // right element is less -> comes first 113 | a[i] = aux[rightIdx++]; 114 | } else { 115 | // left element comes first (less or equal) 116 | a[i] = aux[leftIdx++]; 117 | } 118 | } 119 | } 120 | 121 | function _sort(a: T[], compare: Compare, lo: number, hi: number, aux: T[]) { 122 | if (hi <= lo) { 123 | return; 124 | } 125 | const mid = lo + ((hi - lo) / 2) | 0; 126 | _sort(a, compare, lo, mid, aux); 127 | _sort(a, compare, mid + 1, hi, aux); 128 | if (compare(a[mid], a[mid + 1]) <= 0) { 129 | // left and right are sorted and if the last-left element is less 130 | // or equals than the first-right element there is nothing else 131 | // to do 132 | return; 133 | } 134 | _merge(a, compare, lo, mid, hi, aux); 135 | } 136 | 137 | 138 | export function groupBy(data: ReadonlyArray, compare: (a: T, b: T) => number): T[][] { 139 | const result: T[][] = []; 140 | let currentGroup: T[] | undefined = undefined; 141 | for (const element of mergeSort(data.slice(0), compare)) { 142 | if (!currentGroup || compare(currentGroup[0], element) !== 0) { 143 | currentGroup = [element]; 144 | result.push(currentGroup); 145 | } else { 146 | currentGroup.push(element); 147 | } 148 | } 149 | return result; 150 | } 151 | 152 | interface IMutableSplice extends ISplice { 153 | deleteCount: number; 154 | } 155 | 156 | /** 157 | * Diffs two *sorted* arrays and computes the splices which apply the diff. 158 | */ 159 | export function sortedDiff(before: ReadonlyArray, after: ReadonlyArray, compare: (a: T, b: T) => number): ISplice[] { 160 | const result: IMutableSplice[] = []; 161 | 162 | function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { 163 | if (deleteCount === 0 && toInsert.length === 0) { 164 | return; 165 | } 166 | 167 | const latest = result[result.length - 1]; 168 | 169 | if (latest && latest.start + latest.deleteCount === start) { 170 | latest.deleteCount += deleteCount; 171 | latest.toInsert.push(...toInsert); 172 | } else { 173 | result.push({ start, deleteCount, toInsert }); 174 | } 175 | } 176 | 177 | let beforeIdx = 0; 178 | let afterIdx = 0; 179 | 180 | while (true) { 181 | if (beforeIdx === before.length) { 182 | pushSplice(beforeIdx, 0, after.slice(afterIdx)); 183 | break; 184 | } 185 | if (afterIdx === after.length) { 186 | pushSplice(beforeIdx, before.length - beforeIdx, []); 187 | break; 188 | } 189 | 190 | const beforeElement = before[beforeIdx]; 191 | const afterElement = after[afterIdx]; 192 | const n = compare(beforeElement, afterElement); 193 | if (n === 0) { 194 | // equal 195 | beforeIdx += 1; 196 | afterIdx += 1; 197 | } else if (n < 0) { 198 | // beforeElement is smaller -> before element removed 199 | pushSplice(beforeIdx, 1, []); 200 | beforeIdx += 1; 201 | } else if (n > 0) { 202 | // beforeElement is greater -> after element added 203 | pushSplice(beforeIdx, 0, [afterElement]); 204 | afterIdx += 1; 205 | } 206 | } 207 | 208 | return result; 209 | } 210 | 211 | /** 212 | * Takes two *sorted* arrays and computes their delta (removed, added elements). 213 | * Finishes in `Math.min(before.length, after.length)` steps. 214 | */ 215 | export function delta(before: ReadonlyArray, after: ReadonlyArray, compare: (a: T, b: T) => number): { removed: T[], added: T[] } { 216 | const splices = sortedDiff(before, after, compare); 217 | const removed: T[] = []; 218 | const added: T[] = []; 219 | 220 | for (const splice of splices) { 221 | removed.push(...before.slice(splice.start, splice.start + splice.deleteCount)); 222 | added.push(...splice.toInsert); 223 | } 224 | 225 | return { removed, added }; 226 | } 227 | 228 | /** 229 | * Returns the top N elements from the array. 230 | * 231 | * Faster than sorting the entire array when the array is a lot larger than N. 232 | * 233 | * @param array The unsorted array. 234 | * @param compare A sort function for the elements. 235 | * @param n The number of elements to return. 236 | * @return The first n elemnts from array when sorted with compare. 237 | */ 238 | export function top(array: ReadonlyArray, compare: (a: T, b: T) => number, n: number): T[] { 239 | if (n === 0) { 240 | return []; 241 | } 242 | const result = array.slice(0, n).sort(compare); 243 | topStep(array, compare, result, n, array.length); 244 | return result; 245 | } 246 | 247 | /** 248 | * Asynchronous variant of `top()` allowing for splitting up work in batches between which the event loop can run. 249 | * 250 | * Returns the top N elements from the array. 251 | * 252 | * Faster than sorting the entire array when the array is a lot larger than N. 253 | * 254 | * @param array The unsorted array. 255 | * @param compare A sort function for the elements. 256 | * @param n The number of elements to return. 257 | * @param batch The number of elements to examine before yielding to the event loop. 258 | * @return The first n elemnts from array when sorted with compare. 259 | */ 260 | export function topAsync(array: T[], compare: (a: T, b: T) => number, n: number, batch: number, token?: CancellationToken): Promise { 261 | if (n === 0) { 262 | return Promise.resolve([]); 263 | } 264 | 265 | return new Promise((resolve, reject) => { 266 | (async () => { 267 | const o = array.length; 268 | const result = array.slice(0, n).sort(compare); 269 | for (let i = n, m = Math.min(n + batch, o); i < o; i = m, m = Math.min(m + batch, o)) { 270 | if (i > n) { 271 | await new Promise(resolve => setTimeout(resolve)); // nextTick() would starve I/O. 272 | } 273 | if (token && token.isCancellationRequested) { 274 | throw canceled(); 275 | } 276 | topStep(array, compare, result, i, m); 277 | } 278 | return result; 279 | })() 280 | .then(resolve, reject); 281 | }); 282 | } 283 | 284 | function topStep(array: ReadonlyArray, compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { 285 | for (const n = result.length; i < m; i++) { 286 | const element = array[i]; 287 | if (compare(element, result[n - 1]) < 0) { 288 | result.pop(); 289 | const j = findFirstInSorted(result, e => compare(element, e) < 0); 290 | result.splice(j, 0, element); 291 | } 292 | } 293 | } 294 | 295 | /** 296 | * @returns New array with all falsy values removed. The original array IS NOT modified. 297 | */ 298 | export function coalesce(array: ReadonlyArray): T[] { 299 | return array.filter(e => !!e); 300 | } 301 | 302 | /** 303 | * Remove all falsey values from `array`. The original array IS modified. 304 | */ 305 | export function coalesceInPlace(array: Array): void { 306 | let to = 0; 307 | for (let i = 0; i < array.length; i++) { 308 | if (!!array[i]) { 309 | array[to] = array[i]; 310 | to += 1; 311 | } 312 | } 313 | array.length = to; 314 | } 315 | 316 | /** 317 | * Moves the element in the array for the provided positions. 318 | */ 319 | export function move(array: any[], from: number, to: number): void { 320 | array.splice(to, 0, array.splice(from, 1)[0]); 321 | } 322 | 323 | /** 324 | * @returns false if the provided object is an array and not empty. 325 | */ 326 | export function isFalsyOrEmpty(obj: any): boolean { 327 | return !Array.isArray(obj) || obj.length === 0; 328 | } 329 | 330 | /** 331 | * @returns True if the provided object is an array and has at least one element. 332 | */ 333 | export function isNonEmptyArray(obj: T[] | undefined | null): obj is T[]; 334 | export function isNonEmptyArray(obj: readonly T[] | undefined | null): obj is readonly T[]; 335 | export function isNonEmptyArray(obj: T[] | readonly T[] | undefined | null): obj is T[] | readonly T[] { 336 | return Array.isArray(obj) && obj.length > 0; 337 | } 338 | 339 | /** 340 | * Removes duplicates from the given array. The optional keyFn allows to specify 341 | * how elements are checked for equalness by returning a unique string for each. 342 | */ 343 | export function distinct(array: ReadonlyArray, keyFn?: (t: T) => string): T[] { 344 | if (!keyFn) { 345 | return array.filter((element, position) => { 346 | return array.indexOf(element) === position; 347 | }); 348 | } 349 | 350 | const seen: { [key: string]: boolean; } = Object.create(null); 351 | return array.filter((elem) => { 352 | const key = keyFn(elem); 353 | if (seen[key]) { 354 | return false; 355 | } 356 | 357 | seen[key] = true; 358 | 359 | return true; 360 | }); 361 | } 362 | 363 | export function distinctES6(array: ReadonlyArray): T[] { 364 | const seen = new Set(); 365 | return array.filter(element => { 366 | if (seen.has(element)) { 367 | return false; 368 | } 369 | 370 | seen.add(element); 371 | return true; 372 | }); 373 | } 374 | 375 | export function uniqueFilter(keyFn: (t: T) => string): (t: T) => boolean { 376 | const seen: { [key: string]: boolean; } = Object.create(null); 377 | 378 | return element => { 379 | const key = keyFn(element); 380 | 381 | if (seen[key]) { 382 | return false; 383 | } 384 | 385 | seen[key] = true; 386 | return true; 387 | }; 388 | } 389 | 390 | export function lastIndex(array: ReadonlyArray, fn: (item: T) => boolean): number { 391 | for (let i = array.length - 1; i >= 0; i--) { 392 | const element = array[i]; 393 | 394 | if (fn(element)) { 395 | return i; 396 | } 397 | } 398 | 399 | return -1; 400 | } 401 | 402 | /** 403 | * @deprecated ES6: use `Array.findIndex` 404 | */ 405 | export function firstIndex(array: ReadonlyArray, fn: (item: T) => boolean): number { 406 | for (let i = 0; i < array.length; i++) { 407 | const element = array[i]; 408 | 409 | if (fn(element)) { 410 | return i; 411 | } 412 | } 413 | 414 | return -1; 415 | } 416 | 417 | 418 | /** 419 | * @deprecated ES6: use `Array.find` 420 | */ 421 | export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T): T; 422 | export function first(array: ReadonlyArray, fn: (item: T) => boolean): T | undefined; 423 | export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T | undefined = undefined): T | undefined { 424 | const index = firstIndex(array, fn); 425 | return index < 0 ? notFoundValue : array[index]; 426 | } 427 | 428 | export function firstOrDefault(array: ReadonlyArray, notFoundValue: NotFound): T | NotFound; 429 | export function firstOrDefault(array: ReadonlyArray): T | undefined; 430 | export function firstOrDefault(array: ReadonlyArray, notFoundValue?: NotFound): T | NotFound | undefined { 431 | return array.length > 0 ? array[0] : notFoundValue; 432 | } 433 | 434 | export function commonPrefixLength(one: ReadonlyArray, other: ReadonlyArray, equals: (a: T, b: T) => boolean = (a, b) => a === b): number { 435 | let result = 0; 436 | 437 | for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) { 438 | result++; 439 | } 440 | 441 | return result; 442 | } 443 | 444 | export function flatten(arr: T[][]): T[] { 445 | return ([]).concat(...arr); 446 | } 447 | 448 | export function range(to: number): number[]; 449 | export function range(from: number, to: number): number[]; 450 | export function range(arg: number, to?: number): number[] { 451 | let from = typeof to === 'number' ? arg : 0; 452 | 453 | if (typeof to === 'number') { 454 | from = arg; 455 | } else { 456 | from = 0; 457 | to = arg; 458 | } 459 | 460 | const result: number[] = []; 461 | 462 | if (from <= to) { 463 | for (let i = from; i < to; i++) { 464 | result.push(i); 465 | } 466 | } else { 467 | for (let i = from; i > to; i--) { 468 | result.push(i); 469 | } 470 | } 471 | 472 | return result; 473 | } 474 | 475 | export function index(array: ReadonlyArray, indexer: (t: T) => string): { [key: string]: T; }; 476 | export function index(array: ReadonlyArray, indexer: (t: T) => string, mapper: (t: T) => R): { [key: string]: R; }; 477 | export function index(array: ReadonlyArray, indexer: (t: T) => string, mapper?: (t: T) => R): { [key: string]: R; } { 478 | return array.reduce((r, t) => { 479 | r[indexer(t)] = mapper ? mapper(t) : t; 480 | return r; 481 | }, Object.create(null)); 482 | } 483 | 484 | /** 485 | * Inserts an element into an array. Returns a function which, when 486 | * called, will remove that element from the array. 487 | */ 488 | export function insert(array: T[], element: T): () => void { 489 | array.push(element); 490 | 491 | return () => remove(array, element); 492 | } 493 | 494 | /** 495 | * Removes an element from an array if it can be found. 496 | */ 497 | export function remove(array: T[], element: T): T | undefined { 498 | const index = array.indexOf(element); 499 | if (index > -1) { 500 | array.splice(index, 1); 501 | 502 | return element; 503 | } 504 | 505 | return undefined; 506 | } 507 | 508 | /** 509 | * Insert `insertArr` inside `target` at `insertIndex`. 510 | * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array 511 | */ 512 | export function arrayInsert(target: T[], insertIndex: number, insertArr: T[]): T[] { 513 | const before = target.slice(0, insertIndex); 514 | const after = target.slice(insertIndex); 515 | return before.concat(insertArr, after); 516 | } 517 | 518 | /** 519 | * Uses Fisher-Yates shuffle to shuffle the given array 520 | */ 521 | export function shuffle(array: T[], _seed?: number): void { 522 | let rand: () => number; 523 | 524 | if (typeof _seed === 'number') { 525 | let seed = _seed; 526 | // Seeded random number generator in JS. Modified from: 527 | // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript 528 | rand = () => { 529 | const x = Math.sin(seed++) * 179426549; // throw away most significant digits and reduce any potential bias 530 | return x - Math.floor(x); 531 | }; 532 | } else { 533 | rand = Math.random; 534 | } 535 | 536 | for (let i = array.length - 1; i > 0; i -= 1) { 537 | const j = Math.floor(rand() * (i + 1)); 538 | const temp = array[i]; 539 | array[i] = array[j]; 540 | array[j] = temp; 541 | } 542 | } 543 | 544 | /** 545 | * Pushes an element to the start of the array, if found. 546 | */ 547 | export function pushToStart(arr: T[], value: T): void { 548 | const index = arr.indexOf(value); 549 | 550 | if (index > -1) { 551 | arr.splice(index, 1); 552 | arr.unshift(value); 553 | } 554 | } 555 | 556 | /** 557 | * Pushes an element to the end of the array, if found. 558 | */ 559 | export function pushToEnd(arr: T[], value: T): void { 560 | const index = arr.indexOf(value); 561 | 562 | if (index > -1) { 563 | arr.splice(index, 1); 564 | arr.push(value); 565 | } 566 | } 567 | 568 | 569 | /** 570 | * @deprecated ES6: use `Array.find` 571 | */ 572 | export function find(arr: ArrayLike, predicate: (value: T, index: number, arr: ArrayLike) => any): T | undefined { 573 | for (let i = 0; i < arr.length; i++) { 574 | const element = arr[i]; 575 | if (predicate(element, i, arr)) { 576 | return element; 577 | } 578 | } 579 | 580 | return undefined; 581 | } 582 | 583 | export function mapArrayOrNot(items: T | T[], fn: (_: T) => U): U | U[] { 584 | return Array.isArray(items) ? 585 | items.map(fn) : 586 | fn(items); 587 | } 588 | 589 | export function asArray(x: T | T[]): T[] { 590 | return Array.isArray(x) ? x : [x]; 591 | } 592 | 593 | /** 594 | * @deprecated Use `Array.from` or `[...iter]` 595 | */ 596 | export function toArray(iterable: IterableIterator): T[] { 597 | const result: T[] = []; 598 | for (let element of iterable) { 599 | result.push(element); 600 | } 601 | return result; 602 | } 603 | 604 | export function getRandomElement(arr: T[]): T | undefined { 605 | return arr[Math.floor(Math.random() * arr.length)]; 606 | } 607 | --------------------------------------------------------------------------------