sash.ts 11.8 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./sash';
7
import { IDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
8 9
import { isIPad } from 'vs/base/browser/browser';
import { isMacintosh } from 'vs/base/common/platform';
10
import * as types from 'vs/base/common/types';
11
import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
J
Johannes Rieken 已提交
12
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
13
import { Event, Emitter } from 'vs/base/common/event';
14
import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
15
import { domEvent } from 'vs/base/browser/event';
E
Erich Gamma 已提交
16

J
Joao Moreno 已提交
17
const DEBUG = false;
J
Joao Moreno 已提交
18

E
Erich Gamma 已提交
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
export interface ISashLayoutProvider { }

export interface IVerticalSashLayoutProvider extends ISashLayoutProvider {
	getVerticalSashLeft(sash: Sash): number;
	getVerticalSashTop?(sash: Sash): number;
	getVerticalSashHeight?(sash: Sash): number;
}

export interface IHorizontalSashLayoutProvider extends ISashLayoutProvider {
	getHorizontalSashTop(sash: Sash): number;
	getHorizontalSashLeft?(sash: Sash): number;
	getHorizontalSashWidth?(sash: Sash): number;
}

export interface ISashEvent {
	startX: number;
	currentX: number;
	startY: number;
	currentY: number;
38
	altKey: boolean;
E
Erich Gamma 已提交
39 40 41 42
}

export interface ISashOptions {
	orientation?: Orientation;
43 44
	orthogonalStartSash?: Sash;
	orthogonalEndSash?: Sash;
E
Erich Gamma 已提交
45 46
}

47
export const enum Orientation {
E
Erich Gamma 已提交
48 49 50 51
	VERTICAL,
	HORIZONTAL
}

52
export const enum SashState {
J
Joao Moreno 已提交
53 54 55 56 57 58
	Disabled,
	Minimum,
	Maximum,
	Enabled
}

59
export class Sash extends Disposable {
E
Erich Gamma 已提交
60

61
	private el: HTMLElement;
E
Erich Gamma 已提交
62 63
	private layoutProvider: ISashLayoutProvider;
	private hidden: boolean;
J
Joao Moreno 已提交
64
	private orientation!: Orientation;
E
Erich Gamma 已提交
65

J
Joao Moreno 已提交
66 67 68
	private _state: SashState = SashState.Enabled;
	get state(): SashState { return this._state; }
	set state(state: SashState) {
69 70 71
		if (this._state === state) {
			return;
		}
72

J
Joao Moreno 已提交
73 74 75
		toggleClass(this.el, 'disabled', state === SashState.Disabled);
		toggleClass(this.el, 'minimum', state === SashState.Minimum);
		toggleClass(this.el, 'maximum', state === SashState.Maximum);
76

77
		this._state = state;
J
Joao Moreno 已提交
78
		this._onDidEnablementChange.fire(state);
79 80
	}

81
	private readonly _onDidEnablementChange = this._register(new Emitter<SashState>());
J
Joao Moreno 已提交
82
	readonly onDidEnablementChange: Event<SashState> = this._onDidEnablementChange.event;
83

84
	private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
85 86
	readonly onDidStart: Event<ISashEvent> = this._onDidStart.event;

87
	private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
88 89
	readonly onDidChange: Event<ISashEvent> = this._onDidChange.event;

90
	private readonly _onDidReset = this._register(new Emitter<void>());
91 92
	readonly onDidReset: Event<void> = this._onDidReset.event;

93
	private readonly _onDidEnd = this._register(new Emitter<void>());
94 95
	readonly onDidEnd: Event<void> = this._onDidEnd.event;

J
Joao Moreno 已提交
96 97
	linkedSash: Sash | undefined = undefined;

98
	private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
99 100 101
	private _orthogonalStartSash: Sash | undefined;
	get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
	set orthogonalStartSash(sash: Sash | undefined) {
102
		this.orthogonalStartSashDisposables.clear();
103

104
		if (sash) {
105
			this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalStartSashEnablementChange, this));
106
			this.onOrthogonalStartSashEnablementChange(sash.state);
107
		} else {
108
			this.onOrthogonalStartSashEnablementChange(SashState.Disabled);
109 110
		}

111
		this._orthogonalStartSash = sash;
112 113
	}

114
	private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
115 116 117
	private _orthogonalEndSash: Sash | undefined;
	get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
	set orthogonalEndSash(sash: Sash | undefined) {
118
		this.orthogonalEndSashDisposables.clear();
119

120
		if (sash) {
121
			this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalEndSashEnablementChange, this));
122
			this.onOrthogonalEndSashEnablementChange(sash.state);
123
		} else {
124
			this.onOrthogonalEndSashEnablementChange(SashState.Disabled);
125 126
		}

127
		this._orthogonalEndSash = sash;
128
	}
J
Joao Moreno 已提交
129

E
Erich Gamma 已提交
130
	constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions = {}) {
131 132
		super();

133
		this.el = append(container, $('.monaco-sash'));
E
Erich Gamma 已提交
134

B
Benjamin Pasero 已提交
135
		if (isMacintosh) {
136
			addClass(this.el, 'mac');
B
Benjamin Pasero 已提交
137 138
		}

139 140
		this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
		this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
J
Joao Moreno 已提交
141

142
		Gesture.addTarget(this.el);
143
		this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
E
Erich Gamma 已提交
144

B
Benjamin Pasero 已提交
145
		if (isIPad) {
146 147
			// see also http://ux.stackexchange.com/questions/39023/what-is-the-optimum-button-size-of-touch-screen-applications
			addClass(this.el, 'touch');
E
Erich Gamma 已提交
148 149
		}

B
Benjamin Pasero 已提交
150
		this.setOrientation(options.orientation || Orientation.VERTICAL);
E
Erich Gamma 已提交
151 152 153

		this.hidden = false;
		this.layoutProvider = layoutProvider;
J
Joao Moreno 已提交
154

155 156
		this.orthogonalStartSash = options.orthogonalStartSash;
		this.orthogonalEndSash = options.orthogonalEndSash;
J
Joao Moreno 已提交
157 158

		toggleClass(this.el, 'debug', DEBUG);
E
Erich Gamma 已提交
159 160
	}

161
	setOrientation(orientation: Orientation): void {
B
Benjamin Pasero 已提交
162 163 164
		this.orientation = orientation;

		if (this.orientation === Orientation.HORIZONTAL) {
165 166
			addClass(this.el, 'horizontal');
			removeClass(this.el, 'vertical');
B
Benjamin Pasero 已提交
167
		} else {
168 169
			removeClass(this.el, 'horizontal');
			addClass(this.el, 'vertical');
B
Benjamin Pasero 已提交
170 171 172 173 174 175 176
		}

		if (this.layoutProvider) {
			this.layout();
		}
	}

E
Erich Gamma 已提交
177
	private onMouseDown(e: MouseEvent): void {
178
		EventHelper.stop(e, false);
E
Erich Gamma 已提交
179

J
Joao Moreno 已提交
180 181
		let isMultisashResize = false;

182
		if (!(e as any).__orthogonalSashEvent) {
J
Joao Moreno 已提交
183
			const orthogonalSash = this.getOrthogonalSash(e);
184

185 186 187 188
			if (orthogonalSash) {
				isMultisashResize = true;
				(e as any).__orthogonalSashEvent = true;
				orthogonalSash.onMouseDown(e);
189 190 191
			}
		}

J
Joao Moreno 已提交
192 193 194 195 196
		if (this.linkedSash && !(e as any).__linkedSashEvent) {
			(e as any).__linkedSashEvent = true;
			this.linkedSash.onMouseDown(e);
		}

J
Joao Moreno 已提交
197
		if (!this.state) {
E
Erich Gamma 已提交
198 199 200
			return;
		}

201 202 203 204 205 206 207
		// Select both iframes and webviews; internally Electron nests an iframe
		// in its <webview> component, but this isn't queryable.
		const iframes = [
			...getElementsByTagName('iframe'),
			...getElementsByTagName('webview'),
		];

208 209
		for (const iframe of iframes) {
			iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash
210
		}
211

212 213 214
		const mouseDownEvent = new StandardMouseEvent(e);
		const startX = mouseDownEvent.posx;
		const startY = mouseDownEvent.posy;
215
		const altKey = mouseDownEvent.altKey;
J
Joao Moreno 已提交
216
		const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
E
Erich Gamma 已提交
217

218
		addClass(this.el, 'active');
I
isidor 已提交
219
		this._onDidStart.fire(startEvent);
E
Erich Gamma 已提交
220

221
		// fix https://github.com/Microsoft/vscode/issues/21675
J
Joao Moreno 已提交
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
		const style = createStyleSheet(this.el);
		const updateStyle = () => {
			let cursor = '';

			if (isMultisashResize) {
				cursor = 'all-scroll';
			} else if (this.orientation === Orientation.HORIZONTAL) {
				if (this.state === SashState.Minimum) {
					cursor = 's-resize';
				} else if (this.state === SashState.Maximum) {
					cursor = 'n-resize';
				} else {
					cursor = isMacintosh ? 'row-resize' : 'ns-resize';
				}
			} else {
				if (this.state === SashState.Minimum) {
					cursor = 'e-resize';
				} else if (this.state === SashState.Maximum) {
					cursor = 'w-resize';
				} else {
					cursor = isMacintosh ? 'col-resize' : 'ew-resize';
				}
			}

			style.innerHTML = `* { cursor: ${cursor} !important; }`;
		};
E
Erich Gamma 已提交
248

M
Matt Bierner 已提交
249
		const disposables = new DisposableStore();
250

J
Joao Moreno 已提交
251 252 253 254 255 256
		updateStyle();

		if (!isMultisashResize) {
			this.onDidEnablementChange(updateStyle, null, disposables);
		}

257
		const onMouseMove = (e: MouseEvent) => {
258
			EventHelper.stop(e, false);
259
			const mouseMoveEvent = new StandardMouseEvent(e);
J
Joao Moreno 已提交
260
			const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
E
Erich Gamma 已提交
261

I
isidor 已提交
262
			this._onDidChange.fire(event);
263 264 265
		};

		const onMouseUp = (e: MouseEvent) => {
266
			EventHelper.stop(e, false);
267

J
Joao Moreno 已提交
268
			this.el.removeChild(style);
269

270
			removeClass(this.el, 'active');
I
isidor 已提交
271
			this._onDidEnd.fire();
E
Erich Gamma 已提交
272

M
Matt Bierner 已提交
273
			disposables.dispose();
274

275 276
			for (const iframe of iframes) {
				iframe.style.pointerEvents = 'auto';
277
			}
278 279 280 281 282 283
		};

		domEvent(window, 'mousemove')(onMouseMove, null, disposables);
		domEvent(window, 'mouseup')(onMouseUp, null, disposables);
	}

J
Joao Moreno 已提交
284 285 286 287 288 289 290 291 292 293 294
	private onMouseDoubleClick(e: MouseEvent): void {
		const orthogonalSash = this.getOrthogonalSash(e);

		if (orthogonalSash) {
			orthogonalSash._onDidReset.fire();
		}

		if (this.linkedSash) {
			this.linkedSash._onDidReset.fire();
		}

295
		this._onDidReset.fire();
E
Erich Gamma 已提交
296 297
	}

B
Benjamin Pasero 已提交
298
	private onTouchStart(event: GestureEvent): void {
299
		EventHelper.stop(event);
E
Erich Gamma 已提交
300

301
		const listeners: IDisposable[] = [];
E
Erich Gamma 已提交
302

303 304
		const startX = event.pageX;
		const startY = event.pageY;
305 306
		const altKey = event.altKey;

I
isidor 已提交
307
		this._onDidStart.fire({
E
Erich Gamma 已提交
308 309 310
			startX: startX,
			currentX: startX,
			startY: startY,
311 312
			currentY: startY,
			altKey
E
Erich Gamma 已提交
313 314
		});

315
		listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => {
B
Benjamin Pasero 已提交
316
			if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) {
I
isidor 已提交
317
				this._onDidChange.fire({
E
Erich Gamma 已提交
318 319 320
					startX: startX,
					currentX: event.pageX,
					startY: startY,
321 322
					currentY: event.pageY,
					altKey
E
Erich Gamma 已提交
323 324 325 326
				});
			}
		}));

327
		listeners.push(addDisposableListener(this.el, EventType.End, (event: GestureEvent) => {
I
isidor 已提交
328
			this._onDidEnd.fire();
J
Joao Moreno 已提交
329
			dispose(listeners);
E
Erich Gamma 已提交
330 331 332
		}));
	}

333 334
	layout(): void {
		const size = isIPad ? 20 : 4;
J
Joao Moreno 已提交
335

E
Erich Gamma 已提交
336
		if (this.orientation === Orientation.VERTICAL) {
337
			const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
338
			this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (size / 2) + 'px';
E
Erich Gamma 已提交
339 340

			if (verticalProvider.getVerticalSashTop) {
341
				this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
E
Erich Gamma 已提交
342 343 344
			}

			if (verticalProvider.getVerticalSashHeight) {
345
				this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
E
Erich Gamma 已提交
346 347
			}
		} else {
348
			const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
349
			this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (size / 2) + 'px';
E
Erich Gamma 已提交
350 351

			if (horizontalProvider.getHorizontalSashLeft) {
352
				this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
E
Erich Gamma 已提交
353 354 355
			}

			if (horizontalProvider.getHorizontalSashWidth) {
356
				this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
E
Erich Gamma 已提交
357 358 359 360
			}
		}
	}

361
	show(): void {
E
Erich Gamma 已提交
362
		this.hidden = false;
363 364
		this.el.style.removeProperty('display');
		this.el.setAttribute('aria-hidden', 'false');
E
Erich Gamma 已提交
365 366
	}

367
	hide(): void {
E
Erich Gamma 已提交
368
		this.hidden = true;
369 370
		this.el.style.display = 'none';
		this.el.setAttribute('aria-hidden', 'true');
E
Erich Gamma 已提交
371 372
	}

373
	isHidden(): boolean {
E
Erich Gamma 已提交
374 375 376
		return this.hidden;
	}

377 378
	private onOrthogonalStartSashEnablementChange(state: SashState): void {
		toggleClass(this.el, 'orthogonal-start', state !== SashState.Disabled);
J
Joao Moreno 已提交
379 380
	}

381 382
	private onOrthogonalEndSashEnablementChange(state: SashState): void {
		toggleClass(this.el, 'orthogonal-end', state !== SashState.Disabled);
J
Joao Moreno 已提交
383 384
	}

J
Joao Moreno 已提交
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
	private getOrthogonalSash(e: MouseEvent): Sash | undefined {
		if (this.orientation === Orientation.VERTICAL) {
			if (e.offsetY <= 4) {
				return this.orthogonalStartSash;
			} else if (e.offsetY >= this.el.clientHeight - 4) {
				return this.orthogonalEndSash;
			}
		} else {
			if (e.offsetX <= 4) {
				return this.orthogonalStartSash;
			} else if (e.offsetX >= this.el.clientWidth - 4) {
				return this.orthogonalEndSash;
			}
		}

		return undefined;
	}

403
	dispose(): void {
404 405
		super.dispose();

406 407
		if (this.el && this.el.parentElement) {
			this.el.parentElement.removeChild(this.el);
E
Erich Gamma 已提交
408
		}
409

M
Matt Bierner 已提交
410
		this.el = null!; // StrictNullOverride: nulling out ok in dispose
E
Erich Gamma 已提交
411 412
	}
}