UNPKG

16.3 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2017 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { inject, injectable, named } from 'inversify';
18import { ContributionProvider, CommandRegistry, MenuModelRegistry, isOSX, BackendStopwatch, LogLevel, Stopwatch, isObject } from '../common';
19import { MaybePromise } from '../common/types';
20import { KeybindingRegistry } from './keybinding';
21import { Widget } from './widgets';
22import { ApplicationShell } from './shell/application-shell';
23import { ShellLayoutRestorer, ApplicationShellLayoutMigrationError } from './shell/shell-layout-restorer';
24import { FrontendApplicationStateService } from './frontend-application-state';
25import { preventNavigation, parseCssTime, animationFrame } from './browser';
26import { CorePreferences } from './core-preferences';
27import { WindowService } from './window/window-service';
28import { TooltipService } from './tooltip-service';
29import { StopReason } from '../common/frontend-application-state';
30
31/**
32 * Clients can implement to get a callback for contributing widgets to a shell on start.
33 */
34export const FrontendApplicationContribution = Symbol('FrontendApplicationContribution');
35export interface FrontendApplicationContribution {
36
37 /**
38 * Called on application startup before configure is called.
39 */
40 initialize?(): void;
41
42 /**
43 * Called before commands, key bindings and menus are initialized.
44 * Should return a promise if it runs asynchronously.
45 */
46 configure?(app: FrontendApplication): MaybePromise<void>;
47
48 /**
49 * Called when the application is started. The application shell is not attached yet when this method runs.
50 * Should return a promise if it runs asynchronously.
51 */
52 onStart?(app: FrontendApplication): MaybePromise<void>;
53
54 /**
55 * Called on `beforeunload` event, right before the window closes.
56 * Return `true` or an OnWillStopAction in order to prevent exit.
57 * Note: No async code allowed, this function has to run on one tick.
58 */
59 onWillStop?(app: FrontendApplication): boolean | undefined | OnWillStopAction<unknown>;
60
61 /**
62 * Called when an application is stopped or unloaded.
63 *
64 * Note that this is implemented using `window.beforeunload` which doesn't allow any asynchronous code anymore.
65 * I.e. this is the last tick.
66 */
67 onStop?(app: FrontendApplication): void;
68
69 /**
70 * Called after the application shell has been attached in case there is no previous workbench layout state.
71 * Should return a promise if it runs asynchronously.
72 */
73 initializeLayout?(app: FrontendApplication): MaybePromise<void>;
74
75 /**
76 * An event is emitted when a layout is initialized, but before the shell is attached.
77 */
78 onDidInitializeLayout?(app: FrontendApplication): MaybePromise<void>;
79}
80
81export interface OnWillStopAction<T = unknown> {
82 /**
83 * @resolves to a prepared value to be passed into the `action` function.
84 */
85 prepare?: (stopReason?: StopReason) => MaybePromise<T>;
86 /**
87 * @resolves to `true` if it is safe to close the application; `false` otherwise.
88 */
89 action: (prepared: T, stopReason?: StopReason) => MaybePromise<boolean>;
90 /**
91 * A descriptive string for the reason preventing close.
92 */
93 reason: string;
94 /**
95 * A number representing priority. Higher priority items are run later.
96 * High priority implies that some options of this check will have negative impacts if
97 * the user subsequently cancels the shutdown.
98 */
99 priority?: number;
100}
101
102export namespace OnWillStopAction {
103 export function is(candidate: unknown): candidate is OnWillStopAction {
104 return isObject(candidate) && 'action' in candidate && 'reason' in candidate;
105 }
106}
107
108const TIMER_WARNING_THRESHOLD = 100;
109
110/**
111 * Default frontend contribution that can be extended by clients if they do not want to implement any of the
112 * methods from the interface but still want to contribute to the frontend application.
113 */
114@injectable()
115export abstract class DefaultFrontendApplicationContribution implements FrontendApplicationContribution {
116
117 initialize(): void {
118 // NOOP
119 }
120
121}
122
123@injectable()
124export class FrontendApplication {
125
126 @inject(CorePreferences)
127 protected readonly corePreferences: CorePreferences;
128
129 @inject(WindowService)
130 protected readonly windowsService: WindowService;
131
132 @inject(TooltipService)
133 protected readonly tooltipService: TooltipService;
134
135 @inject(Stopwatch)
136 protected readonly stopwatch: Stopwatch;
137
138 @inject(BackendStopwatch)
139 protected readonly backendStopwatch: BackendStopwatch;
140
141 constructor(
142 @inject(CommandRegistry) protected readonly commands: CommandRegistry,
143 @inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry,
144 @inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry,
145 @inject(ShellLayoutRestorer) protected readonly layoutRestorer: ShellLayoutRestorer,
146 @inject(ContributionProvider) @named(FrontendApplicationContribution)
147 protected readonly contributions: ContributionProvider<FrontendApplicationContribution>,
148 @inject(ApplicationShell) protected readonly _shell: ApplicationShell,
149 @inject(FrontendApplicationStateService) protected readonly stateService: FrontendApplicationStateService
150 ) { }
151
152 get shell(): ApplicationShell {
153 return this._shell;
154 }
155
156 /**
157 * Start the frontend application.
158 *
159 * Start up consists of the following steps:
160 * - start frontend contributions
161 * - attach the application shell to the host element
162 * - initialize the application shell layout
163 * - reveal the application shell if it was hidden by a startup indicator
164 */
165 async start(): Promise<void> {
166 const startup = this.backendStopwatch.start('frontend');
167
168 await this.measure('startContributions', () => this.startContributions(), 'Start frontend contributions', false);
169 this.stateService.state = 'started_contributions';
170
171 const host = await this.getHost();
172 this.attachShell(host);
173 this.attachTooltip(host);
174 await animationFrame();
175 this.stateService.state = 'attached_shell';
176
177 await this.measure('initializeLayout', () => this.initializeLayout(), 'Initialize the workbench layout', false);
178 this.stateService.state = 'initialized_layout';
179 await this.fireOnDidInitializeLayout();
180
181 await this.measure('revealShell', () => this.revealShell(host), 'Replace loading indicator with ready workbench UI (animation)', false);
182 this.registerEventListeners();
183 this.stateService.state = 'ready';
184
185 startup.then(idToken => this.backendStopwatch.stop(idToken, 'Frontend application start', []));
186 }
187
188 /**
189 * Return a promise to the host element to which the application shell is attached.
190 */
191 protected getHost(): Promise<HTMLElement> {
192 if (document.body) {
193 return Promise.resolve(document.body);
194 }
195 return new Promise<HTMLElement>(resolve =>
196 window.addEventListener('load', () => resolve(document.body), { once: true })
197 );
198 }
199
200 /**
201 * Return an HTML element that indicates the startup phase, e.g. with an animation or a splash screen.
202 */
203 protected getStartupIndicator(host: HTMLElement): HTMLElement | undefined {
204 const startupElements = host.getElementsByClassName('theia-preload');
205 return startupElements.length === 0 ? undefined : startupElements[0] as HTMLElement;
206 }
207
208 /**
209 * Register global event listeners.
210 */
211 protected registerEventListeners(): void {
212 this.windowsService.onUnload(() => {
213 this.stateService.state = 'closing_window';
214 this.layoutRestorer.storeLayout(this);
215 this.stopContributions();
216 });
217 window.addEventListener('resize', () => this.shell.update());
218
219 this.keybindings.registerEventListeners(window);
220
221 document.addEventListener('touchmove', event => { event.preventDefault(); }, { passive: false });
222 // Prevent forward/back navigation by scrolling in OS X
223 if (isOSX) {
224 document.body.addEventListener('wheel', preventNavigation, { passive: false });
225 }
226 // Prevent the default browser behavior when dragging and dropping files into the window.
227 document.addEventListener('dragenter', event => {
228 if (event.dataTransfer) {
229 event.dataTransfer.dropEffect = 'none';
230 }
231 event.preventDefault();
232 }, false);
233 document.addEventListener('dragover', event => {
234 if (event.dataTransfer) {
235 event.dataTransfer.dropEffect = 'none';
236 } event.preventDefault();
237 }, false);
238 document.addEventListener('drop', event => {
239 event.preventDefault();
240 }, false);
241
242 }
243
244 /**
245 * Attach the application shell to the host element. If a startup indicator is present, the shell is
246 * inserted before that indicator so it is not visible yet.
247 */
248 protected attachShell(host: HTMLElement): void {
249 const ref = this.getStartupIndicator(host);
250 Widget.attach(this.shell, host, ref);
251 }
252
253 /**
254 * Attach the tooltip container to the host element.
255 */
256 protected attachTooltip(host: HTMLElement): void {
257 this.tooltipService.attachTo(host);
258 }
259
260 /**
261 * If a startup indicator is present, it is first hidden with the `theia-hidden` CSS class and then
262 * removed after a while. The delay until removal is taken from the CSS transition duration.
263 */
264 protected revealShell(host: HTMLElement): Promise<void> {
265 const startupElem = this.getStartupIndicator(host);
266 if (startupElem) {
267 return new Promise(resolve => {
268 window.requestAnimationFrame(() => {
269 startupElem.classList.add('theia-hidden');
270 const preloadStyle = window.getComputedStyle(startupElem);
271 const transitionDuration = parseCssTime(preloadStyle.transitionDuration, 0);
272 window.setTimeout(() => {
273 const parent = startupElem.parentElement;
274 if (parent) {
275 parent.removeChild(startupElem);
276 }
277 resolve();
278 }, transitionDuration);
279 });
280 });
281 } else {
282 return Promise.resolve();
283 }
284 }
285
286 /**
287 * Initialize the shell layout either using the layout restorer service or, if no layout has
288 * been stored, by creating the default layout.
289 */
290 protected async initializeLayout(): Promise<void> {
291 if (!await this.restoreLayout()) {
292 // Fallback: Create the default shell layout
293 await this.createDefaultLayout();
294 }
295 await this.shell.pendingUpdates;
296 }
297
298 /**
299 * Try to restore the shell layout from the storage service. Resolves to `true` if successful.
300 */
301 protected async restoreLayout(): Promise<boolean> {
302 try {
303 return await this.layoutRestorer.restoreLayout(this);
304 } catch (error) {
305 if (ApplicationShellLayoutMigrationError.is(error)) {
306 console.warn(error.message);
307 console.info('Initializing the default layout instead...');
308 } else {
309 console.error('Could not restore layout', error);
310 }
311 return false;
312 }
313 }
314
315 /**
316 * Let the frontend application contributions initialize the shell layout. Override this
317 * method in order to create an application-specific custom layout.
318 */
319 protected async createDefaultLayout(): Promise<void> {
320 for (const contribution of this.contributions.getContributions()) {
321 if (contribution.initializeLayout) {
322 await this.measure(contribution.constructor.name + '.initializeLayout',
323 () => contribution.initializeLayout!(this)
324 );
325 }
326 }
327 }
328
329 protected async fireOnDidInitializeLayout(): Promise<void> {
330 for (const contribution of this.contributions.getContributions()) {
331 if (contribution.onDidInitializeLayout) {
332 await this.measure(contribution.constructor.name + '.onDidInitializeLayout',
333 () => contribution.onDidInitializeLayout!(this)
334 );
335 }
336 }
337 }
338
339 /**
340 * Initialize and start the frontend application contributions.
341 */
342 protected async startContributions(): Promise<void> {
343 for (const contribution of this.contributions.getContributions()) {
344 if (contribution.initialize) {
345 try {
346 await this.measure(contribution.constructor.name + '.initialize',
347 () => contribution.initialize!()
348 );
349 } catch (error) {
350 console.error('Could not initialize contribution', error);
351 }
352 }
353 }
354
355 for (const contribution of this.contributions.getContributions()) {
356 if (contribution.configure) {
357 try {
358 await this.measure(contribution.constructor.name + '.configure',
359 () => contribution.configure!(this)
360 );
361 } catch (error) {
362 console.error('Could not configure contribution', error);
363 }
364 }
365 }
366
367 /**
368 * FIXME:
369 * - decouple commands & menus
370 * - consider treat commands, keybindings and menus as frontend application contributions
371 */
372 await this.measure('commands.onStart',
373 () => this.commands.onStart()
374 );
375 await this.measure('keybindings.onStart',
376 () => this.keybindings.onStart()
377 );
378 await this.measure('menus.onStart',
379 () => this.menus.onStart()
380 );
381 for (const contribution of this.contributions.getContributions()) {
382 if (contribution.onStart) {
383 try {
384 await this.measure(contribution.constructor.name + '.onStart',
385 () => contribution.onStart!(this)
386 );
387 } catch (error) {
388 console.error('Could not start contribution', error);
389 }
390 }
391 }
392 }
393
394 /**
395 * Stop the frontend application contributions. This is called when the window is unloaded.
396 */
397 protected stopContributions(): void {
398 console.info('>>> Stopping frontend contributions...');
399 for (const contribution of this.contributions.getContributions()) {
400 if (contribution.onStop) {
401 try {
402 contribution.onStop(this);
403 } catch (error) {
404 console.error('Could not stop contribution', error);
405 }
406 }
407 }
408 console.info('<<< All frontend contributions have been stopped.');
409 }
410
411 protected async measure<T>(name: string, fn: () => MaybePromise<T>, message = `Frontend ${name}`, threshold = true): Promise<T> {
412 return this.stopwatch.startAsync(name, message, fn,
413 threshold ? { thresholdMillis: TIMER_WARNING_THRESHOLD, defaultLogLevel: LogLevel.DEBUG } : {});
414 }
415
416}
417
\No newline at end of file