sash.ts 11.6 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 64 65
	private layoutProvider: ISashLayoutProvider;
	private hidden: boolean;
	private orientation: Orientation;

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;

J
Joao Moreno 已提交
182 183 184 185 186
		if (this.linkedSash && !(e as any).__linkedSashEvent) {
			(e as any).__linkedSashEvent = true;
			this.linkedSash.onMouseDown(e);
		}

187 188
		if (!(e as any).__orthogonalSashEvent) {
			let orthogonalSash: Sash | undefined;
189 190

			if (this.orientation === Orientation.VERTICAL) {
J
Joao Moreno 已提交
191
				if (e.offsetY <= 4) {
192
					orthogonalSash = this.orthogonalStartSash;
J
Joao Moreno 已提交
193
				} else if (e.offsetY >= this.el.clientHeight - 4) {
194
					orthogonalSash = this.orthogonalEndSash;
195 196
				}
			} else {
J
Joao Moreno 已提交
197
				if (e.offsetX <= 4) {
198
					orthogonalSash = this.orthogonalStartSash;
J
Joao Moreno 已提交
199
				} else if (e.offsetX >= this.el.clientWidth - 4) {
200
					orthogonalSash = this.orthogonalEndSash;
201 202 203
				}
			}

204 205 206 207
			if (orthogonalSash) {
				isMultisashResize = true;
				(e as any).__orthogonalSashEvent = true;
				orthogonalSash.onMouseDown(e);
208 209 210
			}
		}

J
Joao Moreno 已提交
211
		if (!this.state) {
E
Erich Gamma 已提交
212 213 214
			return;
		}

215 216 217 218 219 220 221
		// 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'),
		];

222 223
		for (const iframe of iframes) {
			iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash
224
		}
225

226 227 228
		const mouseDownEvent = new StandardMouseEvent(e);
		const startX = mouseDownEvent.posx;
		const startY = mouseDownEvent.posy;
229
		const altKey = mouseDownEvent.altKey;
J
Joao Moreno 已提交
230
		const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
E
Erich Gamma 已提交
231

232
		addClass(this.el, 'active');
I
isidor 已提交
233
		this._onDidStart.fire(startEvent);
E
Erich Gamma 已提交
234

235
		// fix https://github.com/Microsoft/vscode/issues/21675
J
Joao Moreno 已提交
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
		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 已提交
262

M
Matt Bierner 已提交
263
		const disposables = new DisposableStore();
264

J
Joao Moreno 已提交
265 266 267 268 269 270
		updateStyle();

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

271
		const onMouseMove = (e: MouseEvent) => {
272
			EventHelper.stop(e, false);
273
			const mouseMoveEvent = new StandardMouseEvent(e);
J
Joao Moreno 已提交
274
			const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
E
Erich Gamma 已提交
275

I
isidor 已提交
276
			this._onDidChange.fire(event);
277 278 279
		};

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

J
Joao Moreno 已提交
282
			this.el.removeChild(style);
283

284
			removeClass(this.el, 'active');
I
isidor 已提交
285
			this._onDidEnd.fire();
E
Erich Gamma 已提交
286

M
Matt Bierner 已提交
287
			disposables.dispose();
288

289 290
			for (const iframe of iframes) {
				iframe.style.pointerEvents = 'auto';
291
			}
292 293 294 295 296 297 298 299
		};

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

	private onMouseDoubleClick(event: MouseEvent): void {
		this._onDidReset.fire();
E
Erich Gamma 已提交
300 301
	}

B
Benjamin Pasero 已提交
302
	private onTouchStart(event: GestureEvent): void {
303
		EventHelper.stop(event);
E
Erich Gamma 已提交
304

305
		const listeners: IDisposable[] = [];
E
Erich Gamma 已提交
306

307 308
		const startX = event.pageX;
		const startY = event.pageY;
309 310
		const altKey = event.altKey;

I
isidor 已提交
311
		this._onDidStart.fire({
E
Erich Gamma 已提交
312 313 314
			startX: startX,
			currentX: startX,
			startY: startY,
315 316
			currentY: startY,
			altKey
E
Erich Gamma 已提交
317 318
		});

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

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

337 338
	layout(): void {
		const size = isIPad ? 20 : 4;
J
Joao Moreno 已提交
339

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

			if (verticalProvider.getVerticalSashTop) {
345
				this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
E
Erich Gamma 已提交
346 347 348
			}

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

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

			if (horizontalProvider.getHorizontalSashWidth) {
360
				this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
E
Erich Gamma 已提交
361 362 363 364
			}
		}
	}

365
	show(): void {
E
Erich Gamma 已提交
366
		this.hidden = false;
367 368
		this.el.style.removeProperty('display');
		this.el.setAttribute('aria-hidden', 'false');
E
Erich Gamma 已提交
369 370
	}

371
	hide(): void {
E
Erich Gamma 已提交
372
		this.hidden = true;
373 374
		this.el.style.display = 'none';
		this.el.setAttribute('aria-hidden', 'true');
E
Erich Gamma 已提交
375 376
	}

377
	isHidden(): boolean {
E
Erich Gamma 已提交
378 379 380
		return this.hidden;
	}

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

385 386
	private onOrthogonalEndSashEnablementChange(state: SashState): void {
		toggleClass(this.el, 'orthogonal-end', state !== SashState.Disabled);
J
Joao Moreno 已提交
387 388
	}

389
	dispose(): void {
390 391
		super.dispose();

392 393
		if (this.el && this.el.parentElement) {
			this.el.parentElement.removeChild(this.el);
E
Erich Gamma 已提交
394
		}
395

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