Files
azuredatastudio/src/sql/base/browser/ui/splitview/splitview.ts
Karl Burtram 81329fa7fa Merge VS Code 1.26.1 (#2394)
* Squash merge commits for 1.26 (#1) (#2323)

* Polish tag search as per feedback (#55269)

* Polish tag search as per feedback

* Updated regex

* Allow users to opt-out of features that send online requests in the background (#55097)

* settings sweep #54690

* Minor css tweaks to enable eoverflow elipsis in more places (#55277)

* fix an issue with titlebarheight when not scaling with zoom

* Settings descriptions update #54690

* fixes #55209

* Settings editor - many padding fixes

* More space above level 2 label

* Fixing Cannot debug npm script using Yarn #55103

* Settings editor - show ellipsis when description overflows

* Settings editor - ... fix measuring around links, relayout

* Setting descriptions

* Settings editor - fix ... for some short lines, fix select container width

* Settings editor - overlay trees so scrollable shadow is full width

* Fix #54133 - missing extension settings after reload

* Settings color token description tweak

* Settings editor - disable overflow indicator temporarily, needs to be faster

* Added command to Run the selected npm script

* fixes #54452

* fixes #54929

* fixes #55248

* prefix command with extension name

* Contribute run selected to the context menu

* node-debug@1.26.6

* Allow terminal rendererType to be swapped out at runtime

Part of #53274
Fixes #55344

* Settings editor - fix not focusing search when restoring editor
setInput must be actually async. Will be fixed naturally when we aren't using winJS promises...

* Settings editor - TOC should only expand the section with a selected item

* Bump node-debug2

* Settings editor - Tree focus outlines

* Settings editor - don't blink the scrollbar when toc selection changes
And hide TOC correctly when the editor is narrow

* Settings editor - header rows should not be selectable

* fixes #54877

* change debug assignee to isi

* Settings sweep (#54690)

* workaround for #55051

* Settings sweep (#54690)

* settings sweep

#54690

* Don't try closing tags when you type > after another >

* Describe what implementation code lens does

Fixes #55370

* fix javadoc formatter setting description

* fixes #55325

* update to officical TS version

* Settings editor - Even more padding, use semibold instead of bold

* Fix #55357 - fix TOC twistie

* fixes #55288

* explorer: refresh on di change file system provider registration

fixes #53256

* Disable push to Linux repo to test standalone publisher

* New env var to notify log level to extensions #54001

* Disable snippets in extension search (when not in suggest dropdown) (#55281)

* Disable snippits in extension search (when not in suggest dropdown)

* Add monaco input contributions

* Fix bug preventing snippetSuggestions from taking effect in sub-editors

* Latest emmet helper to fix #52366

* Fix comment updates for threads within same file

* Allow extensions to log telemetry to log files #54001

* Pull latest css grammar

* files.exclude control - use same style for "add" vs "edit"

* files.exclude control - focus/keyboard behavior

* don't show menubar too early

* files.exclude - better styling

* Place cursor at end of extensions search box on autofill (#55254)

* Place cursor at end of extensions search box on autofill

* Use position instead of selection

* fix linux build issue (empty if block)

* Settings editor - fix extension category prefixes

* Settings editor - add simple ellipsis for first line that overflows, doesn't cover case when first line does not overflow but there is more text, TODO

* File/Text search provider docs

* Fixes #52655

* Include epoch (#55008)

* Fixes #53385

* Fixes #49480

*  VS Code Insiders (Users) not opening Fixes #55353

* Better handling of the case when the extension host fails to start

* Fixes #53966

*  Remove confusing Start from wordPartLeft commands ID

* vscode-xterm@3.6.0-beta12

Fixes #55488

* Initial size is set to infinity!! Fixes #55461

* Polish embeddedEditorBackground

* configuration service misses event

* Fix #55224 - fix duplicate results in multiroot workspace from splitting the diskseach query

* Select all not working in issue reporter on mac, fixes #55424

* Disable fuzzy matching for extensions autosuggest (#55498)

* Fix clipping of extensions search border in some third party themes (#55504)

* fixes #55538

* Fix bug causing an aria alert to not be shown the third time
 (and odd numbers thereafter)

* Settings editor - work around rendering glitch with webkit-line-clamp

* Settings editor - revert earlier '...' changes

* Settings editor - move enumDescription to its own div, because it disturbs -webkit-line-clamp for some reason

* Settings editor - better overflow indicator

* Don't show existing filters in autocomplete (#55495)

* Dont show existing filters in autocomplete

* Simplify

* Settings Editor: Add aria labels for input elements Fixes: #54836 (#55543)

* fixes #55223

* Update vscode-css-languageservice to 3.0.10-next.1

* Fix #55509 - settings navigation

* Fix #55519

* Fix #55520

* FIx #55524

* Fix #55556 - include wordSeparators in all search queries, so findTextInFiles can respect isWordMatch correctly

* oss updates for endgame

* Fix unit tests

* fixes #55522

* Avoid missing manifest error from bubbling up #54757

* Settings format crawl

* Search provider - Fix FileSearchProvider to return array, not progress

* Fix #55598

* Settings editor - fix NPE rendering settings with no description

* dont render inden guides in search box (#55600)

* fixes #55454

* More settings crawl

* Another change for #55598 - maxResults applies to FileSearch and TextSearch but not FileIndex

* Fix FileSearchProvider unit tests for progress change

* fixes #55561

* Settings description update for #54690

* Update setting descriptions for online services

* Minor edits

* fixes #55513

* fixes #55451

* Fix #55612 - fix findTextInFiles cancellation

* fixes #55539

* More setting description tweaks

* Setting to disable online experiments #54354

* fixes #55507

* fixes #55515

* Show online services action only in Insiders for now

* Settings editor - change toc behavior default to 'filter'

* Settings editor - nicer filter count style during search

* Fix #55617 - search viewlet icons

* Settings editor - better styling for element count indicator

* SearchProvider - fix NPE when searching extraFileResources

* Allow extends to work without json suffix

Fixes #16905

* Remove accessability options logic entirely

Follow up on #55451

* use latest version of DAP

* fixes #55490

* fixes #55122

* fixes #52332

* Avoid assumptions about git: URIs (fixes #36236)

* relative path for descriptions

* resourece: get rid of isFile context key

fixes #48275

* Register previous ids for compatibility (#53497)

* more tuning for #48275

* no need to always re-read "files explorer"

fixes #52003

* read out active composites properly

fixes #51967

* Update link colors for hc theme to meet color contrast ratio, fixes #55651

Also updated link color for `textLinkActiveForeground` to be the same as `textLinkForeground` as it wasn't properly updated

* detect 'winpty-agent.exe'; fixes #55672

* node-debug@1.26.7

* reset counter on new label

* Settings editor - fix multiple setting links in one description

* Settings editor - color code blocks in setting descriptions, fix #55532

* Settings editor - hover color in TOC

* Settings editor - fix navigation NPE

* Settings editor - fix text control width

* Settings editor - maybe fix #55684

* Fix bug causing cursor to not move on paste

* fixes #53582

* Use ctrlCmd instead of ctrl for go down from search box

* fixes #55264

* fixes #55456

* filter for spcaes before triggering search (#55611)

* Fix #55698 - don't lose filtered TOC counts when refreshing TOC

* fixes #55421

* fixes #28979

* fixes #55576

* only add check for updates to windows/linux help

* readonly files: append decoration to label

fixes #53022

* debug: do not show toolbar while initialising

fixes #55026

* Opening launch.json should not activate debug extensions

fixes #55029

* fixes #55435

* fixes #55434

* fixes #55439

* trigger menu only on altkey up

* Fix #50555 - fix settings editor memory leak

* Fix #55712 - no need to focus 'a' anymore when restoring control focus after tree render

* fixes #55335

* proper fix for readonly model

fixes #53022

* improve FoldingRangeKind spec (for #55686)

* Use class with static fields (fixes #55494)

* Fixes #53671

* fixes #54630

* [html] should disable ionic suggestions by default. Currently forces deprecated Ionic v1 suggestions in .html files while typing. Fixes #53324

* cleanup deps

* debug issues back to andre

* update electron for smoketest

* Fix #55757 - prevent settings tabs from overflowing

* Fix #53897 - revert setting menu defaults to old editor

* Add enum descriptions to `typescript.preferences.importModuleSpecifier`

* Fix #55767 - leaking style elements from settings editor

* Fix #55521 - prevent flashing when clicking in exclude control

* Update Git modified color for contrast ratio, fixes #53140

* Revert "Merge branch 'master' of github.com:Microsoft/vscode"

This reverts commit bf46b6bfbae0cab99c2863e1244a916181fa9fbc, reversing
changes made to e275a424483dfb4ed33b428c97d5e2c441d6b917.

* Revert "Revert "Merge branch 'master' of github.com:Microsoft/vscode""

This reverts commit 53949d963f39e40757557c6526332354a31d9154.

* don't ask to install an incomplete menu

* Fix NPE in terminal AccessibilityManager

Fixes #55744

* don't display fallback menu unless we've closed the last window

* fixes #55547

* Fix smoke tests for extension search box

* Update OSSREADME.json for Electron 2.0.5

* Update distro

Includes Chromium license changes

* fix #55455

* fix #55865

* fixes #55893

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation (#55805)

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation

* ONly show on @recommended or @recommended:workspace

* Make more consistant

* Fix #55911

* Understand json activity (#55926)

* Understand json file activity

* Refactoring

* adding composer.json

* Distro update for experiments

* use terminal.processId for auto-attach; fixes #55918

* Reject invalid URI with vscode.openFolder (for #55891)

* improve win32 setup system vs user detection

fixes #55840

fixes #55840

delay winreg import

related to #55840

show notification earlier

related to #55840

fix #55840

update inno setup message

related to #55840

* Fix #55593 - this code only operates on local paths, so use fsPath and Uri.file instead

* Bring back the old menu due to electron 2.0 issues (#55913)

* add the old menu back for native menus

* make menu labels match

* `vscode.openFolder`: treat missing URI schema gracefully (for #55891)

* delay EH reattach; fixes #55955

* Mark all json files under appSettingsHome as settings

* Use localized strings for telemetry opt-out

* Exception when saving file editor opened from remote file provider (fixes #55051)

* Remove terminal menu from stable

Fixes 56003

* VSCode Insiders crashes on open with TypeError: Cannot read property 'lastIndexOf' of undefined. Fixes #54933

* improve fix for #55891

* fix #55916

* Improve #55891

* increase EH debugging restart delay; fixes #55955

* Revert "Don't include non-resource entries in history quick pick"

This reverts commit 37209a838e9f7e9abe6dc53ed73cdf1e03b72060.

* Diff editor: horizontal scrollbar height is smaller (fixes #56062)

* improve openFolder uri fix (correctly treat backslashes)

* fixes #56116
repair ipc for native menubar keybindings

* Fix #56240 - Open the JSON settings editor instead of the UI editor

* Fix #55536

* uriDisplay: if no formatter is registered fall back to getPathlabel

fixes #56104

* VSCode hangs when opening python file. Fixes #56377

* VS Code Hangs When Opening Specific PowerShell File. Fixes #56430

* Fix #56433 - search extraFileResources even when no folders open

* Workaround #55649

* Fix in master #56371

* Fix tests #56371

* Fix in master #56317

* increase version to 1.26.1

* Fixes #56387: Handle SIGPIPE in extension host

* fixes #56185

* Fix merge issues (part 1)

* Fix build breaks (part 1)

* Build breaks (part 2)

* Build breaks (part 3)

* More build breaks (part 4)

* Fix build breaks (part 5)

* WIP

* Fix menus

* Render query result and message panels (#2363)

* Put back query editor hot exit changes

* Fix grid changes that broke profiler (#2365)

* Update APIs for saving query editor state

* Fix restore view state for profiler and edit data

* Updating custom default themes to support 4.5:1 contrast ratio

* Test updates

* Fix Extension Manager and Windows Setup

* Update license headers

* Add appveyor and travis files back

* Fix hidden modal dropdown issue
2018-09-04 14:55:00 -07:00

1045 lines
30 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./splitview';
import lifecycle = require('vs/base/common/lifecycle');
import ee = require('sql/base/common/eventEmitter');
import types = require('vs/base/common/types');
import dom = require('vs/base/browser/dom');
import numbers = require('vs/base/common/numbers');
import sash = require('vs/base/browser/ui/sash/sash');
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Event, Emitter } from 'vs/base/common/event';
import { Color } from 'vs/base/common/color';
import { SashState } from 'vs/base/browser/ui/sash/sash';
export enum Orientation {
VERTICAL,
HORIZONTAL
}
export enum ViewSizing {
Flexible,
Fixed
}
export interface IOptions {
orientation?: Orientation; // default Orientation.VERTICAL
canChangeOrderByDragAndDrop?: boolean;
}
export interface ISashEvent {
start: number;
current: number;
}
export interface IViewOptions {
sizing?: ViewSizing;
fixedSize?: number;
minimumSize?: number;
}
export interface IView extends ee.IEventEmitter {
preferredSize: number;
size: number;
sizing: ViewSizing;
fixedSize: number;
minimumSize: number;
maximumSize: number;
draggableElement?: HTMLElement;
draggableLabel?: string;
render(container: HTMLElement, orientation: Orientation): void;
layout(size: number, orientation: Orientation): void;
focus(): void;
}
interface IState {
start?: number;
sizes?: number[];
up?: number[];
down?: number[];
maxUp?: number;
maxDown?: number;
collapses: number[];
expands: number[];
}
export abstract class View extends ee.EventEmitter implements IView {
size: number;
protected _sizing: ViewSizing;
protected _fixedSize: number;
protected _minimumSize: number;
constructor(public preferredSize: number, opts: IViewOptions) {
super();
this.size = 0;
this._sizing = types.isUndefined(opts.sizing) ? ViewSizing.Flexible : opts.sizing;
this._fixedSize = types.isUndefined(opts.fixedSize) ? 22 : opts.fixedSize;
this._minimumSize = types.isUndefined(opts.minimumSize) ? 22 : opts.minimumSize;
}
get sizing(): ViewSizing { return this._sizing; }
get fixedSize(): number { return this._fixedSize; }
get minimumSize(): number { return this.sizing === ViewSizing.Fixed ? this.fixedSize : this._minimumSize; }
get maximumSize(): number { return this.sizing === ViewSizing.Fixed ? this.fixedSize : Number.POSITIVE_INFINITY; }
protected setFlexible(size?: number): void {
this._sizing = ViewSizing.Flexible;
this.emit('change', types.isUndefined(size) ? this._minimumSize : size);
}
protected setFixed(size?: number): void {
this._sizing = ViewSizing.Fixed;
this._fixedSize = types.isUndefined(size) ? this._fixedSize : size;
this.emit('change', this._fixedSize);
}
abstract render(container: HTMLElement, orientation: Orientation): void;
abstract focus(): void;
abstract layout(size: number, orientation: Orientation): void;
}
export interface IHeaderViewOptions extends IHeaderViewStyles, IViewOptions {
headerSize?: number;
}
export interface IHeaderViewStyles {
headerForeground?: Color;
headerBackground?: Color;
headerHighContrastBorder?: Color;
}
const headerDefaultOpts = {
headerBackground: Color.fromHex('#808080').transparent(0.2)
};
export abstract class HeaderView extends View {
private _headerSize: number;
private _showHeader: boolean;
protected header: HTMLElement;
protected body: HTMLElement;
private headerForeground: Color;
private headerBackground: Color;
private headerHighContrastBorder: Color;
constructor(initialSize: number, opts: IHeaderViewOptions) {
super(initialSize, opts);
this._headerSize = types.isUndefined(opts.headerSize) ? 22 : opts.headerSize;
this._showHeader = this._headerSize > 0;
this.headerForeground = opts.headerForeground;
this.headerBackground = opts.headerBackground || headerDefaultOpts.headerBackground;
this.headerHighContrastBorder = opts.headerHighContrastBorder;
}
style(styles: IHeaderViewStyles): void {
this.headerForeground = styles.headerForeground;
this.headerBackground = styles.headerBackground;
this.headerHighContrastBorder = styles.headerHighContrastBorder;
this.applyStyles();
}
protected get headerSize(): number {
return this._showHeader ? this._headerSize : 0;
}
protected applyStyles(): void {
if (this.header) {
const headerForegroundColor = this.headerForeground ? this.headerForeground.toString() : null;
const headerBackgroundColor = this.headerBackground ? this.headerBackground.toString() : null;
const headerHighContrastBorderColor = this.headerHighContrastBorder ? this.headerHighContrastBorder.toString() : null;
this.header.style.color = headerForegroundColor;
this.header.style.backgroundColor = headerBackgroundColor;
this.header.style.borderTop = headerHighContrastBorderColor ? `1px solid ${headerHighContrastBorderColor}` : null;
}
}
get draggableElement(): HTMLElement { return this.header; }
render(container: HTMLElement, orientation: Orientation): void {
this.header = document.createElement('div');
this.header.className = 'header';
let headerSize = this.headerSize + 'px';
if (orientation === Orientation.HORIZONTAL) {
this.header.style.width = headerSize;
} else {
this.header.style.height = headerSize;
}
if (this._showHeader) {
this.renderHeader(this.header);
container.appendChild(this.header);
}
this.body = document.createElement('div');
this.body.className = 'body';
this.layoutBodyContainer(orientation);
this.renderBody(this.body);
container.appendChild(this.body);
this.applyStyles();
}
showHeader(): boolean {
if (!this._showHeader) {
if (!this.body.parentElement.contains(this.header)) {
this.renderHeader(this.header);
this.body.parentElement.insertBefore(this.header, this.body);
}
dom.removeClass(this.header, 'hide');
this._showHeader = true;
return true;
}
return false;
}
hideHeader(): boolean {
if (this._showHeader) {
dom.addClass(this.header, 'hide');
this._showHeader = false;
return true;
}
return false;
}
layout(size: number, orientation: Orientation): void {
this.layoutBodyContainer(orientation);
this.layoutBody(size - this.headerSize);
}
private layoutBodyContainer(orientation: Orientation): void {
let size = `calc(100% - ${this.headerSize}px)`;
if (orientation === Orientation.HORIZONTAL) {
this.body.style.width = size;
} else {
this.body.style.height = size;
}
}
dispose(): void {
this.header = null;
this.body = null;
super.dispose();
}
protected abstract renderHeader(container: HTMLElement): void;
protected abstract renderBody(container: HTMLElement): void;
protected abstract layoutBody(size: number): void;
}
export interface ICollapsibleViewOptions {
sizing: ViewSizing;
ariaHeaderLabel: string;
bodySize?: number;
initialState?: CollapsibleState;
}
export enum CollapsibleState {
EXPANDED,
COLLAPSED
}
export abstract class AbstractCollapsibleView extends HeaderView {
protected state: CollapsibleState;
private ariaHeaderLabel: string;
private headerClickListener: lifecycle.IDisposable;
private headerKeyListener: lifecycle.IDisposable;
private focusTracker: dom.IFocusTracker;
private _bodySize: number;
private _previousSize: number = null;
private readonly viewSizing: ViewSizing;
constructor(initialSize: number | undefined, opts: ICollapsibleViewOptions) {
super(initialSize, opts);
this.viewSizing = opts.sizing;
this.ariaHeaderLabel = opts.ariaHeaderLabel;
this.setBodySize(types.isUndefined(opts.bodySize) ? 22 : opts.bodySize);
if (typeof this.preferredSize === 'undefined') {
this.preferredSize = this._bodySize + this.headerSize;
}
this.changeState(types.isUndefined(opts.initialState) ? CollapsibleState.EXPANDED : opts.initialState);
}
get previousSize(): number {
return this._previousSize;
}
setBodySize(bodySize: number) {
this._bodySize = bodySize;
this.updateSize();
}
private updateSize() {
if (this.viewSizing === ViewSizing.Fixed) {
this.setFixed(this.state === CollapsibleState.EXPANDED ? this._bodySize + this.headerSize : this.headerSize);
} else {
this._minimumSize = this._bodySize + this.headerSize;
this._previousSize = !this.previousSize || this._previousSize < this._minimumSize ? this._minimumSize : this._previousSize;
if (this.state === CollapsibleState.EXPANDED) {
this.setFlexible(this._previousSize || this._minimumSize);
} else {
this._previousSize = this.size || this._minimumSize;
this.setFixed(this.headerSize);
}
}
}
render(container: HTMLElement, orientation: Orientation): void {
super.render(container, orientation);
dom.addClass(this.header, 'collapsible');
dom.addClass(this.body, 'collapsible');
// Keyboard access
this.header.setAttribute('tabindex', '0');
this.header.setAttribute('role', 'toolbar');
if (this.ariaHeaderLabel) {
this.header.setAttribute('aria-label', this.ariaHeaderLabel);
}
this.header.setAttribute('aria-expanded', String(this.state === CollapsibleState.EXPANDED));
this.headerKeyListener = dom.addDisposableListener(this.header, dom.EventType.KEY_DOWN, (e) => {
let event = new StandardKeyboardEvent(e);
let eventHandled = false;
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space) || (event.equals(KeyCode.LeftArrow) && this.state === CollapsibleState.EXPANDED) || (event.equals(KeyCode.RightArrow) && this.state === CollapsibleState.COLLAPSED)) {
this.toggleExpansion();
eventHandled = true;
} else if (event.equals(KeyCode.Escape)) {
this.header.blur();
eventHandled = true;
} else if (event.equals(KeyCode.UpArrow)) {
this.emit('focusPrevious');
eventHandled = true;
} else if (event.equals(KeyCode.DownArrow)) {
this.emit('focusNext');
eventHandled = true;
}
if (eventHandled) {
dom.EventHelper.stop(event, true);
}
});
// Mouse access
this.headerClickListener = dom.addDisposableListener(this.header, dom.EventType.CLICK, () => this.toggleExpansion());
// Track state of focus in header so that other components can adjust styles based on that
// (for example show or hide actions based on the state of being focused or not)
this.focusTracker = dom.trackFocus(this.header);
this.focusTracker.onDidFocus(() => {
dom.addClass(this.header, 'focused');
});
this.focusTracker.onDidBlur(() => {
dom.removeClass(this.header, 'focused');
});
}
focus(): void {
if (this.header) {
this.header.focus();
}
}
layout(size: number, orientation: Orientation): void {
this.layoutHeader();
super.layout(size, orientation);
}
isExpanded(): boolean {
return this.state === CollapsibleState.EXPANDED;
}
expand(): void {
if (this.isExpanded()) {
return;
}
this.changeState(CollapsibleState.EXPANDED);
}
collapse(): void {
if (!this.isExpanded()) {
return;
}
this.changeState(CollapsibleState.COLLAPSED);
}
toggleExpansion(): void {
if (this.isExpanded()) {
this.collapse();
} else {
this.expand();
}
}
private layoutHeader(): void {
if (!this.header) {
return;
}
if (this.state === CollapsibleState.COLLAPSED) {
dom.addClass(this.header, 'collapsed');
} else {
dom.removeClass(this.header, 'collapsed');
}
}
protected changeState(state: CollapsibleState): void {
this.state = state;
if (this.header) {
this.header.setAttribute('aria-expanded', String(this.state === CollapsibleState.EXPANDED));
}
this.layoutHeader();
this.updateSize();
}
showHeader(): boolean {
const result = super.showHeader();
if (result) {
this.updateSize();
}
return result;
}
hideHeader(): boolean {
const result = super.hideHeader();
if (result) {
this.updateSize();
}
return result;
}
dispose(): void {
if (this.headerClickListener) {
this.headerClickListener.dispose();
this.headerClickListener = null;
}
if (this.headerKeyListener) {
this.headerKeyListener.dispose();
this.headerKeyListener = null;
}
if (this.focusTracker) {
this.focusTracker.dispose();
this.focusTracker = null;
}
super.dispose();
}
}
class PlainView extends View {
render() { }
focus() { }
layout() { }
}
class DeadView extends PlainView {
constructor(view: IView) {
super(view.size, { sizing: ViewSizing.Fixed, fixedSize: 0 });
}
}
class VoidView extends PlainView {
constructor() {
super(0, { sizing: ViewSizing.Fixed, minimumSize: 0, fixedSize: 0 });
}
setFlexible(size?: number): void {
super.setFlexible(size);
}
setFixed(size?: number): void {
super.setFixed(size);
}
}
function sum(arr: number[]): number {
return arr.reduce((a, b) => a + b);
}
export interface SplitViewStyles {
dropBackground?: Color;
}
export class SplitView extends lifecycle.Disposable implements
sash.IHorizontalSashLayoutProvider,
sash.IVerticalSashLayoutProvider {
private orientation: Orientation;
private canDragAndDrop: boolean;
private el: HTMLElement;
private size: number;
private viewElements: HTMLElement[];
private views: IView[];
private viewChangeListeners: lifecycle.IDisposable[];
private viewFocusPreviousListeners: lifecycle.IDisposable[];
private viewFocusNextListeners: lifecycle.IDisposable[];
private viewFocusListeners: lifecycle.IDisposable[];
private viewDnDListeners: lifecycle.IDisposable[][];
private sashOrientation: sash.Orientation;
private sashes: sash.Sash[];
private sashesListeners: lifecycle.IDisposable[];
private measureContainerSize: () => number;
private layoutViewElement: (viewElement: HTMLElement, size: number) => void;
private eventWrapper: (event: sash.ISashEvent) => ISashEvent;
private animationTimeout: number;
private state: IState;
private draggedView: IView;
private dropBackground: Color;
private _onFocus: Emitter<IView> = this._register(new Emitter<IView>());
readonly onFocus: Event<IView> = this._onFocus.event;
private _onDidOrderChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidOrderChange: Event<void> = this._onDidOrderChange.event;
constructor(container: HTMLElement, options?: IOptions) {
super();
options = options || {};
this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation;
this.canDragAndDrop = !!options.canChangeOrderByDragAndDrop;
this.el = document.createElement('div');
dom.addClass(this.el, 'monaco-split-view');
dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal');
container.appendChild(this.el);
this.size = null;
this.viewElements = [];
this.views = [];
this.viewChangeListeners = [];
this.viewFocusPreviousListeners = [];
this.viewFocusNextListeners = [];
this.viewFocusListeners = [];
this.viewDnDListeners = [];
this.sashes = [];
this.sashesListeners = [];
this.animationTimeout = null;
this.sashOrientation = this.orientation === Orientation.VERTICAL
? sash.Orientation.HORIZONTAL
: sash.Orientation.VERTICAL;
if (this.orientation === Orientation.VERTICAL) {
this.measureContainerSize = () => dom.getContentHeight(container);
this.layoutViewElement = (viewElement, size) => viewElement.style.height = size + 'px';
this.eventWrapper = e => { return { start: e.startY, current: e.currentY }; };
} else {
this.measureContainerSize = () => dom.getContentWidth(container);
this.layoutViewElement = (viewElement, size) => viewElement.style.width = size + 'px';
this.eventWrapper = e => { return { start: e.startX, current: e.currentX }; };
}
// The void space exists to handle the case where all other views are fixed size
this.addView(new VoidView(), 1, 0);
}
getViews<T extends IView>(): T[] {
return <T[]>this.views.slice(0, this.views.length - 1);
}
addView(view: IView, initialWeight: number = 1, index = this.views.length - 1): void {
if (initialWeight <= 0) {
throw new Error('Initial weight must be a positive number.');
}
/**
* Reset size to null. This will layout newly added views to initial weights.
*/
this.size = null;
let viewCount = this.views.length;
// Create view container
let viewElement = document.createElement('div');
dom.addClass(viewElement, 'split-view-view');
this.viewElements.splice(index, 0, viewElement);
// Create view
view.render(viewElement, this.orientation);
this.views.splice(index, 0, view);
// Render view
if (index === viewCount) {
this.el.appendChild(viewElement);
} else {
this.el.insertBefore(viewElement, this.el.children.item(index));
}
// Listen to Drag and Drop
this.viewDnDListeners[index] = this.createDnDListeners(view, viewElement);
// Add sash
if (this.views.length > 2) {
let s = new sash.Sash(this.el, this, { orientation: this.sashOrientation });
this.sashes.splice(index - 1, 0, s);
this.sashesListeners.push(s.onDidStart((e) => this.onSashStart(s, this.eventWrapper(e))));
this.sashesListeners.push(s.onDidChange((e) => this.onSashChange(s, this.eventWrapper(e))));
}
this.viewChangeListeners.splice(index, 0, view.addListener('change', size => this.onViewChange(view, size)));
this.onViewChange(view, view.minimumSize);
let viewFocusTracker = dom.trackFocus(viewElement);
this.viewFocusListeners.splice(index, 0, viewFocusTracker);
viewFocusTracker.onDidFocus(() => this._onFocus.fire(view));
this.viewFocusPreviousListeners.splice(index, 0, view.addListener('focusPrevious', () => index > 0 && this.views[index - 1].focus()));
this.viewFocusNextListeners.splice(index, 0, view.addListener('focusNext', () => index < this.views.length && this.views[index + 1].focus()));
}
removeView(view: IView): void {
let index = this.views.indexOf(view);
if (index < 0) {
return;
}
this.size = null;
let deadView = new DeadView(view);
this.views[index] = deadView;
this.onViewChange(deadView, 0);
let sashIndex = Math.max(index - 1, 0);
if (sashIndex < this.sashes.length) {
this.sashes[sashIndex].dispose();
this.sashes.splice(sashIndex, 1);
}
this.viewChangeListeners[index].dispose();
this.viewChangeListeners.splice(index, 1);
this.viewFocusPreviousListeners[index].dispose();
this.viewFocusPreviousListeners.splice(index, 1);
this.viewFocusListeners[index].dispose();
this.viewFocusListeners.splice(index, 1);
this.viewFocusNextListeners[index].dispose();
this.viewFocusNextListeners.splice(index, 1);
lifecycle.dispose(this.viewDnDListeners[index]);
this.viewDnDListeners.splice(index, 1);
this.views.splice(index, 1);
this.el.removeChild(this.viewElements[index]);
this.viewElements.splice(index, 1);
deadView.dispose();
view.dispose();
}
layout(size?: number): void {
size = size || this.measureContainerSize();
if (this.size === null) {
this.size = size;
this.initialLayout();
return;
}
size = Math.max(size, this.views.reduce((t, v) => t + v.minimumSize, 0));
let diff = Math.abs(this.size - size);
let up = numbers.countToArray(this.views.length - 1, -1);
let collapses = this.views.map(v => v.size - v.minimumSize);
let expands = this.views.map(v => v.maximumSize - v.size);
if (size < this.size) {
this.expandCollapse(Math.min(diff, sum(collapses)), collapses, expands, up, []);
} else if (size > this.size) {
this.expandCollapse(Math.min(diff, sum(expands)), collapses, expands, [], up);
}
this.size = size;
this.layoutViews();
}
style(styles: SplitViewStyles): void {
this.dropBackground = styles.dropBackground;
}
private createDnDListeners(view: IView, viewElement: HTMLElement): lifecycle.IDisposable[] {
if (!this.canDragAndDrop || view instanceof VoidView) {
return [];
}
const disposables: lifecycle.IDisposable[] = [];
// Allow to drag
if (view.draggableElement) {
view.draggableElement.draggable = true;
disposables.push(dom.addDisposableListener(view.draggableElement, dom.EventType.DRAG_START, (e: DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
const dragImage = document.createElement('div');
dragImage.className = 'monaco-tree-drag-image';
dragImage.textContent = view.draggableLabel ? view.draggableLabel : view.draggableElement.textContent;
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, -10, -10);
setTimeout(() => document.body.removeChild(dragImage), 0);
this.draggedView = view;
}));
}
// Drag enter
let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470
disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DRAG_ENTER, (e: DragEvent) => {
if (this.draggedView && this.draggedView !== view) {
counter++;
this.updateFromDragging(view, viewElement, true);
}
}));
// Drag leave
disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DRAG_LEAVE, (e: DragEvent) => {
if (this.draggedView && this.draggedView !== view) {
counter--;
if (counter === 0) {
this.updateFromDragging(view, viewElement, false);
}
}
}));
// Drag end
disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DRAG_END, (e: DragEvent) => {
if (this.draggedView) {
counter = 0;
this.updateFromDragging(view, viewElement, false);
this.draggedView = null;
}
}));
// Drop
disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DROP, (e: DragEvent) => {
dom.EventHelper.stop(e, true);
counter = 0;
this.updateFromDragging(view, viewElement, false);
if (this.draggedView && this.draggedView !== view) {
this.move(this.views.indexOf(this.draggedView), this.views.indexOf(view));
}
this.draggedView = null;
}));
return disposables;
}
private updateFromDragging(view: IView, viewElement: HTMLElement, isDragging: boolean): void {
viewElement.style.backgroundColor = isDragging && this.dropBackground ? this.dropBackground.toString() : null;
}
private move(fromIndex: number, toIndex: number): void {
if (fromIndex < 0 || toIndex > this.views.length - 2) {
return;
}
const [viewChangeListener] = this.viewChangeListeners.splice(fromIndex, 1);
this.viewChangeListeners.splice(toIndex, 0, viewChangeListener);
const [viewFocusPreviousListener] = this.viewFocusPreviousListeners.splice(fromIndex, 1);
this.viewFocusPreviousListeners.splice(toIndex, 0, viewFocusPreviousListener);
const [viewFocusListener] = this.viewFocusListeners.splice(fromIndex, 1);
this.viewFocusListeners.splice(toIndex, 0, viewFocusListener);
const [viewFocusNextListener] = this.viewFocusNextListeners.splice(fromIndex, 1);
this.viewFocusNextListeners.splice(toIndex, 0, viewFocusNextListener);
const [viewDnDListeners] = this.viewDnDListeners.splice(fromIndex, 1);
this.viewDnDListeners.splice(toIndex, 0, viewDnDListeners);
const [view] = this.views.splice(fromIndex, 1);
this.views.splice(toIndex, 0, view);
this.el.removeChild(this.viewElements[fromIndex]);
this.el.insertBefore(this.viewElements[fromIndex], this.viewElements[toIndex < fromIndex ? toIndex : toIndex + 1]);
const [viewElement] = this.viewElements.splice(fromIndex, 1);
this.viewElements.splice(toIndex, 0, viewElement);
this.layout();
this._onDidOrderChange.fire();
}
private onSashStart(sash: sash.Sash, event: ISashEvent): void {
let i = this.sashes.indexOf(sash);
let collapses = this.views.map(v => v.size - v.minimumSize);
let expands = this.views.map(v => v.maximumSize - v.size);
let up = numbers.countToArray(i, -1);
let down = numbers.countToArray(i + 1, this.views.length);
let collapsesUp = up.map(i => collapses[i]);
let collapsesDown = down.map(i => collapses[i]);
let expandsUp = up.map(i => expands[i]);
let expandsDown = down.map(i => expands[i]);
this.state = {
start: event.start,
sizes: this.views.map(v => v.size),
up: up,
down: down,
maxUp: Math.min(sum(collapsesUp), sum(expandsDown)),
maxDown: Math.min(sum(expandsUp), sum(collapsesDown)),
collapses: collapses,
expands: expands
};
}
private onSashChange(sash: sash.Sash, event: ISashEvent): void {
let diff = event.current - this.state.start;
for (let i = 0; i < this.views.length; i++) {
this.views[i].size = this.views[i].preferredSize = this.state.sizes[i];
}
if (diff < 0) {
this.expandCollapse(Math.min(-diff, this.state.maxUp), this.state.collapses, this.state.expands, this.state.up, this.state.down);
} else {
this.expandCollapse(Math.min(diff, this.state.maxDown), this.state.collapses, this.state.expands, this.state.down, this.state.up);
}
this.layoutViews();
}
// Main algorithm
private expandCollapse(collapse: number, collapses: number[], expands: number[], collapseIndexes: number[], expandIndexes: number[]): void {
let totalCollapse = collapse;
let totalExpand = totalCollapse;
collapseIndexes.forEach(i => {
let collapse = Math.min(collapses[i], totalCollapse);
totalCollapse -= collapse;
this.views[i].size -= collapse;
});
expandIndexes.forEach(i => {
let expand = Math.min(expands[i], totalExpand);
totalExpand -= expand;
this.views[i].size += expand;
});
}
private initialLayout(): void {
let totalWeight = 0;
let fixedSize = 0;
this.views.forEach((v, i) => {
if (v.sizing === ViewSizing.Flexible) {
totalWeight += v.preferredSize;
} else {
fixedSize += v.fixedSize;
}
});
let flexibleSize = this.size - fixedSize;
this.views.forEach((v, i) => {
if (v.sizing === ViewSizing.Flexible) {
if (totalWeight === 0) {
v.size = flexibleSize;
} else {
v.size = v.preferredSize * flexibleSize / totalWeight;
}
} else {
v.size = v.fixedSize;
}
});
// Leftover
let index = this.getLastFlexibleViewIndex();
if (index >= 0) {
this.views[index].size += this.size - this.views.reduce((t, v) => t + v.size, 0);
}
// Layout
this.layoutViews();
}
private getLastFlexibleViewIndex(exceptIndex: number = null): number {
for (let i = this.views.length - 1; i >= 0; i--) {
if (exceptIndex === i) {
continue;
}
if (this.views[i].sizing === ViewSizing.Flexible) {
return i;
}
}
return -1;
}
private layoutViews(): void {
for (let i = 0; i < this.views.length; i++) {
// Layout the view elements
this.layoutViewElement(this.viewElements[i], this.views[i].size);
// Layout the views themselves
this.views[i].layout(this.views[i].size, this.orientation);
}
// Layout the sashes
this.sashes.forEach(s => s.layout());
// Update sashes enablement
let previous = false;
let collapsesDown = this.views.map(v => previous = (v.size - v.minimumSize > 0) || previous);
previous = false;
let expandsDown = this.views.map(v => previous = (v.maximumSize - v.size > 0) || previous);
let reverseViews = this.views.slice().reverse();
previous = false;
let collapsesUp = reverseViews.map(v => previous = (v.size - v.minimumSize > 0) || previous).reverse();
previous = false;
let expandsUp = reverseViews.map(v => previous = (v.maximumSize - v.size > 0) || previous).reverse();
this.sashes.forEach((s, i) => {
if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) {
s.state = SashState.Enabled;
} else {
s.state = SashState.Disabled;
}
});
}
private onViewChange(view: IView, size: number): void {
if (view !== this.voidView) {
if (this.areAllViewsFixed()) {
this.voidView.setFlexible();
} else {
this.voidView.setFixed();
}
}
if (this.size === null) {
return;
}
if (size === view.size) {
return;
}
this.setupAnimation();
let index = this.views.indexOf(view);
let diff = Math.abs(size - view.size);
let up = numbers.countToArray(index - 1, -1);
let down = numbers.countToArray(index + 1, this.views.length);
let downUp = down.concat(up);
let collapses = this.views.map(v => Math.max(v.size - v.minimumSize, 0));
let expands = this.views.map(v => Math.max(v.maximumSize - v.size, 0));
let collapse: number, collapseIndexes: number[], expandIndexes: number[];
if (size < view.size) {
collapse = Math.min(downUp.reduce((t, i) => t + expands[i], 0), diff);
collapseIndexes = [index];
expandIndexes = downUp;
} else {
collapse = Math.min(downUp.reduce((t, i) => t + collapses[i], 0), diff);
collapseIndexes = downUp;
expandIndexes = [index];
}
this.expandCollapse(collapse, collapses, expands, collapseIndexes, expandIndexes);
this.layoutViews();
}
private setupAnimation(): void {
if (types.isNumber(this.animationTimeout)) {
window.clearTimeout(this.animationTimeout);
}
dom.addClass(this.el, 'animated');
this.animationTimeout = window.setTimeout(() => this.clearAnimation(), 200);
}
private clearAnimation(): void {
this.animationTimeout = null;
dom.removeClass(this.el, 'animated');
}
private get voidView(): VoidView {
return this.views[this.views.length - 1] as VoidView;
}
private areAllViewsFixed(): boolean {
return this.views.every((v, i) => v.sizing === ViewSizing.Fixed || i === this.views.length - 1);
}
getVerticalSashLeft(sash: sash.Sash): number {
return this.getSashPosition(sash);
}
getHorizontalSashTop(sash: sash.Sash): number {
return this.getSashPosition(sash);
}
private getSashPosition(sash: sash.Sash): number {
let index = this.sashes.indexOf(sash);
let position = 0;
for (let i = 0; i <= index; i++) {
position += this.views[i].size;
}
return position;
}
dispose(): void {
if (types.isNumber(this.animationTimeout)) {
window.clearTimeout(this.animationTimeout);
}
this.orientation = null;
this.size = null;
this.viewElements.forEach(e => this.el.removeChild(e));
this.el = null;
this.viewElements = [];
this.views = lifecycle.dispose(this.views);
this.sashes = lifecycle.dispose(this.sashes);
this.sashesListeners = lifecycle.dispose(this.sashesListeners);
this.measureContainerSize = null;
this.layoutViewElement = null;
this.eventWrapper = null;
this.state = null;
super.dispose();
}
}