1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { inject, injectable, named } from 'inversify';
|
18 | import { ContributionProvider, CommandRegistry, MenuModelRegistry, isOSX, BackendStopwatch, LogLevel, Stopwatch, isObject } from '../common';
|
19 | import { MaybePromise } from '../common/types';
|
20 | import { KeybindingRegistry } from './keybinding';
|
21 | import { Widget } from './widgets';
|
22 | import { ApplicationShell } from './shell/application-shell';
|
23 | import { ShellLayoutRestorer, ApplicationShellLayoutMigrationError } from './shell/shell-layout-restorer';
|
24 | import { FrontendApplicationStateService } from './frontend-application-state';
|
25 | import { preventNavigation, parseCssTime, animationFrame } from './browser';
|
26 | import { CorePreferences } from './core-preferences';
|
27 | import { WindowService } from './window/window-service';
|
28 | import { TooltipService } from './tooltip-service';
|
29 | import { StopReason } from '../common/frontend-application-state';
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | export const FrontendApplicationContribution = Symbol('FrontendApplicationContribution');
|
35 | export interface FrontendApplicationContribution {
|
36 |
|
37 | |
38 |
|
39 |
|
40 | initialize?(): void;
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
46 | configure?(app: FrontendApplication): MaybePromise<void>;
|
47 |
|
48 | |
49 |
|
50 |
|
51 |
|
52 | onStart?(app: FrontendApplication): MaybePromise<void>;
|
53 |
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 | onWillStop?(app: FrontendApplication): boolean | undefined | OnWillStopAction<unknown>;
|
60 |
|
61 | |
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | onStop?(app: FrontendApplication): void;
|
68 |
|
69 | |
70 |
|
71 |
|
72 |
|
73 | initializeLayout?(app: FrontendApplication): MaybePromise<void>;
|
74 |
|
75 | |
76 |
|
77 |
|
78 | onDidInitializeLayout?(app: FrontendApplication): MaybePromise<void>;
|
79 | }
|
80 |
|
81 | export interface OnWillStopAction<T = unknown> {
|
82 | |
83 |
|
84 |
|
85 | prepare?: (stopReason?: StopReason) => MaybePromise<T>;
|
86 | |
87 |
|
88 |
|
89 | action: (prepared: T, stopReason?: StopReason) => MaybePromise<boolean>;
|
90 | |
91 |
|
92 |
|
93 | reason: string;
|
94 | |
95 |
|
96 |
|
97 |
|
98 |
|
99 | priority?: number;
|
100 | }
|
101 |
|
102 | export namespace OnWillStopAction {
|
103 | export function is(candidate: unknown): candidate is OnWillStopAction {
|
104 | return isObject(candidate) && 'action' in candidate && 'reason' in candidate;
|
105 | }
|
106 | }
|
107 |
|
108 | const TIMER_WARNING_THRESHOLD = 100;
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | @injectable()
|
115 | export abstract class DefaultFrontendApplicationContribution implements FrontendApplicationContribution {
|
116 |
|
117 | initialize(): void {
|
118 |
|
119 | }
|
120 |
|
121 | }
|
122 |
|
123 | @injectable()
|
124 | export 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 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
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 |
|
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 |
|
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 |
|
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 |
|
223 | if (isOSX) {
|
224 | document.body.addEventListener('wheel', preventNavigation, { passive: false });
|
225 | }
|
226 |
|
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 |
|
246 |
|
247 |
|
248 | protected attachShell(host: HTMLElement): void {
|
249 | const ref = this.getStartupIndicator(host);
|
250 | Widget.attach(this.shell, host, ref);
|
251 | }
|
252 |
|
253 | |
254 |
|
255 |
|
256 | protected attachTooltip(host: HTMLElement): void {
|
257 | this.tooltipService.attachTo(host);
|
258 | }
|
259 |
|
260 | |
261 |
|
262 |
|
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 |
|
288 |
|
289 |
|
290 | protected async initializeLayout(): Promise<void> {
|
291 | if (!await this.restoreLayout()) {
|
292 |
|
293 | await this.createDefaultLayout();
|
294 | }
|
295 | await this.shell.pendingUpdates;
|
296 | }
|
297 |
|
298 | |
299 |
|
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 |
|
317 |
|
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 |
|
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 |
|
369 |
|
370 |
|
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 |
|
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 |