@theia/core 1.39.0-next.1 → 1.39.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +6 -6
  2. package/lib/browser/dialogs.d.ts +15 -9
  3. package/lib/browser/dialogs.d.ts.map +1 -1
  4. package/lib/browser/dialogs.js +67 -54
  5. package/lib/browser/dialogs.js.map +1 -1
  6. package/lib/browser/saveable.d.ts.map +1 -1
  7. package/lib/browser/saveable.js +4 -2
  8. package/lib/browser/saveable.js.map +1 -1
  9. package/lib/browser/secondary-window-handler.d.ts +2 -2
  10. package/lib/browser/secondary-window-handler.d.ts.map +1 -1
  11. package/lib/browser/secondary-window-handler.js +8 -34
  12. package/lib/browser/secondary-window-handler.js.map +1 -1
  13. package/lib/browser/styling-service.d.ts +5 -2
  14. package/lib/browser/styling-service.d.ts.map +1 -1
  15. package/lib/browser/styling-service.js +15 -5
  16. package/lib/browser/styling-service.js.map +1 -1
  17. package/lib/browser/widgets/widget.d.ts +1 -0
  18. package/lib/browser/widgets/widget.d.ts.map +1 -1
  19. package/lib/browser/widgets/widget.js +7 -3
  20. package/lib/browser/widgets/widget.js.map +1 -1
  21. package/lib/browser/window/default-secondary-window-service.d.ts +5 -3
  22. package/lib/browser/window/default-secondary-window-service.d.ts.map +1 -1
  23. package/lib/browser/window/default-secondary-window-service.js +62 -18
  24. package/lib/browser/window/default-secondary-window-service.js.map +1 -1
  25. package/lib/browser/window/secondary-window-service.d.ts +3 -1
  26. package/lib/browser/window/secondary-window-service.d.ts.map +1 -1
  27. package/lib/browser/window/secondary-window-service.js.map +1 -1
  28. package/lib/common/message-rpc/rpc-message-encoder.d.ts +3 -3
  29. package/lib/common/message-rpc/rpc-message-encoder.d.ts.map +1 -1
  30. package/lib/common/message-rpc/rpc-message-encoder.js +2 -2
  31. package/lib/common/message-rpc/rpc-message-encoder.js.map +1 -1
  32. package/lib/common/message-rpc/rpc-protocol.d.ts +1 -1
  33. package/lib/common/message-rpc/rpc-protocol.d.ts.map +1 -1
  34. package/lib/common/message-rpc/rpc-protocol.js +8 -4
  35. package/lib/common/message-rpc/rpc-protocol.js.map +1 -1
  36. package/lib/common/messaging/proxy-factory.d.ts +6 -6
  37. package/lib/common/messaging/proxy-factory.d.ts.map +1 -1
  38. package/lib/common/messaging/proxy-factory.js +11 -10
  39. package/lib/common/messaging/proxy-factory.js.map +1 -1
  40. package/lib/electron-browser/preload.d.ts.map +1 -1
  41. package/lib/electron-browser/preload.js +20 -15
  42. package/lib/electron-browser/preload.js.map +1 -1
  43. package/lib/electron-browser/window/electron-secondary-window-service.d.ts +3 -1
  44. package/lib/electron-browser/window/electron-secondary-window-service.d.ts.map +1 -1
  45. package/lib/electron-browser/window/electron-secondary-window-service.js +7 -2
  46. package/lib/electron-browser/window/electron-secondary-window-service.js.map +1 -1
  47. package/lib/electron-common/electron-api.d.ts +2 -0
  48. package/lib/electron-common/electron-api.d.ts.map +1 -1
  49. package/lib/electron-common/electron-api.js +2 -1
  50. package/lib/electron-common/electron-api.js.map +1 -1
  51. package/lib/electron-main/electron-api-main.d.ts +1 -0
  52. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  53. package/lib/electron-main/electron-api-main.js +16 -0
  54. package/lib/electron-main/electron-api-main.js.map +1 -1
  55. package/lib/electron-main/theia-electron-window.d.ts +1 -0
  56. package/lib/electron-main/theia-electron-window.d.ts.map +1 -1
  57. package/lib/electron-main/theia-electron-window.js +32 -0
  58. package/lib/electron-main/theia-electron-window.js.map +1 -1
  59. package/package.json +4 -4
  60. package/src/browser/dialogs.ts +69 -52
  61. package/src/browser/saveable.ts +4 -2
  62. package/src/browser/secondary-window-handler.ts +7 -38
  63. package/src/browser/styling-service.ts +17 -6
  64. package/src/browser/widgets/widget.ts +4 -0
  65. package/src/browser/window/default-secondary-window-service.ts +67 -18
  66. package/src/browser/window/secondary-window-service.ts +4 -1
  67. package/src/common/message-rpc/rpc-message-encoder.ts +4 -4
  68. package/src/common/message-rpc/rpc-protocol.ts +8 -4
  69. package/src/common/messaging/proxy-factory.ts +13 -15
  70. package/src/electron-browser/preload.ts +21 -1
  71. package/src/electron-browser/window/electron-secondary-window-service.ts +8 -2
  72. package/src/electron-common/electron-api.ts +4 -0
  73. package/src/electron-main/electron-api-main.ts +19 -1
  74. package/src/electron-main/theia-electron-window.ts +30 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theia/core",
3
- "version": "1.39.0-next.1+c1a2b7b02",
3
+ "version": "1.39.0-next.11+c42adc25d",
4
4
  "description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.",
5
5
  "main": "lib/common/index.js",
6
6
  "typings": "lib/common/index.d.ts",
@@ -16,8 +16,8 @@
16
16
  "@phosphor/signaling": "1",
17
17
  "@phosphor/virtualdom": "1",
18
18
  "@phosphor/widgets": "1",
19
- "@theia/application-package": "1.39.0-next.1+c1a2b7b02",
20
- "@theia/request": "1.39.0-next.1+c1a2b7b02",
19
+ "@theia/application-package": "1.39.0-next.11+c42adc25d",
20
+ "@theia/request": "1.39.0-next.11+c42adc25d",
21
21
  "@types/body-parser": "^1.16.4",
22
22
  "@types/cookie": "^0.3.3",
23
23
  "@types/dompurify": "^2.2.2",
@@ -206,5 +206,5 @@
206
206
  "nyc": {
207
207
  "extends": "../../configs/nyc.json"
208
208
  },
209
- "gitHead": "c1a2b7b021cb67db4cc05a57cd3561bcba023ab9"
209
+ "gitHead": "c42adc25db04d38e9d300a56b9f91ba9f46601f3"
210
210
  }
@@ -84,10 +84,9 @@ export class DialogOverlayService implements FrontendApplicationContribution {
84
84
 
85
85
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
86
  protected readonly dialogs: AbstractDialog<any>[] = [];
87
+ protected readonly documents: Document[] = [];
87
88
 
88
89
  constructor() {
89
- addKeyListener(document.body, Key.ENTER, e => this.handleEnter(e));
90
- addKeyListener(document.body, Key.ESCAPE, e => this.handleEscape(e));
91
90
  }
92
91
 
93
92
  initialize(): void {
@@ -101,6 +100,11 @@ export class DialogOverlayService implements FrontendApplicationContribution {
101
100
 
102
101
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
102
  push(dialog: AbstractDialog<any>): Disposable {
103
+ if (this.documents.findIndex(document => document === dialog.node.ownerDocument) < 0) {
104
+ addKeyListener(dialog.node.ownerDocument.body, Key.ENTER, e => this.handleEnter(e));
105
+ addKeyListener(dialog.node.ownerDocument.body, Key.ESCAPE, e => this.handleEscape(e));
106
+ this.documents.push(dialog.node.ownerDocument);
107
+ }
104
108
  this.dialogs.unshift(dialog);
105
109
  return Disposable.create(() => {
106
110
  const index = this.dialogs.indexOf(dialog);
@@ -147,9 +151,10 @@ export abstract class AbstractDialog<T> extends BaseWidget {
147
151
  protected activeElement: HTMLElement | undefined;
148
152
 
149
153
  constructor(
150
- @inject(DialogProps) protected readonly props: DialogProps
154
+ protected readonly props: DialogProps,
155
+ options?: Widget.IOptions
151
156
  ) {
152
- super();
157
+ super(options);
153
158
  this.id = 'theia-dialog-shell';
154
159
  this.addClass('dialogOverlay');
155
160
  this.toDispose.push(Disposable.create(() => {
@@ -157,7 +162,7 @@ export abstract class AbstractDialog<T> extends BaseWidget {
157
162
  Widget.detach(this);
158
163
  }
159
164
  }));
160
- const container = document.createElement('div');
165
+ const container = this.node.ownerDocument.createElement('div');
161
166
  container.classList.add('dialogBlock');
162
167
  if (props.maxWidth === undefined) {
163
168
  container.setAttribute('style', 'max-width: none');
@@ -166,31 +171,31 @@ export abstract class AbstractDialog<T> extends BaseWidget {
166
171
  }
167
172
  this.node.appendChild(container);
168
173
 
169
- const titleContentNode = document.createElement('div');
174
+ const titleContentNode = this.node.ownerDocument.createElement('div');
170
175
  titleContentNode.classList.add('dialogTitle');
171
176
  container.appendChild(titleContentNode);
172
177
 
173
- this.titleNode = document.createElement('div');
178
+ this.titleNode = this.node.ownerDocument.createElement('div');
174
179
  this.titleNode.textContent = props.title;
175
180
  titleContentNode.appendChild(this.titleNode);
176
181
 
177
- this.closeCrossNode = document.createElement('i');
182
+ this.closeCrossNode = this.node.ownerDocument.createElement('i');
178
183
  this.closeCrossNode.classList.add(...codiconArray('close'));
179
184
  this.closeCrossNode.classList.add('closeButton');
180
185
  titleContentNode.appendChild(this.closeCrossNode);
181
186
 
182
- this.contentNode = document.createElement('div');
187
+ this.contentNode = this.node.ownerDocument.createElement('div');
183
188
  this.contentNode.classList.add('dialogContent');
184
189
  if (props.wordWrap !== undefined) {
185
190
  this.contentNode.setAttribute('style', `word-wrap: ${props.wordWrap}`);
186
191
  }
187
192
  container.appendChild(this.contentNode);
188
193
 
189
- this.controlPanel = document.createElement('div');
194
+ this.controlPanel = this.node.ownerDocument.createElement('div');
190
195
  this.controlPanel.classList.add('dialogControl');
191
196
  container.appendChild(this.controlPanel);
192
197
 
193
- this.errorMessageNode = document.createElement('div');
198
+ this.errorMessageNode = this.node.ownerDocument.createElement('div');
194
199
  this.errorMessageNode.classList.add('error');
195
200
  this.errorMessageNode.setAttribute('style', 'flex: 2');
196
201
  this.controlPanel.appendChild(this.errorMessageNode);
@@ -199,17 +204,18 @@ export abstract class AbstractDialog<T> extends BaseWidget {
199
204
  }
200
205
 
201
206
  protected appendCloseButton(text: string = Dialog.CANCEL): HTMLButtonElement {
202
- this.closeButton = this.createButton(text);
203
- this.controlPanel.appendChild(this.closeButton);
204
- this.closeButton.classList.add('secondary');
205
- return this.closeButton;
207
+ return this.closeButton = this.appendButton(text, false);
206
208
  }
207
209
 
208
210
  protected appendAcceptButton(text: string = Dialog.OK): HTMLButtonElement {
209
- this.acceptButton = this.createButton(text);
210
- this.controlPanel.appendChild(this.acceptButton);
211
- this.acceptButton.classList.add('main');
212
- return this.acceptButton;
211
+ return this.acceptButton = this.appendButton(text, true);
212
+ }
213
+
214
+ protected appendButton(text: string, primary: boolean): HTMLButtonElement {
215
+ const button = this.createButton(text);
216
+ this.controlPanel.appendChild(button);
217
+ button.classList.add(primary ? 'main' : 'secondary');
218
+ return button;
213
219
  }
214
220
 
215
221
  protected createButton(text: string): HTMLButtonElement {
@@ -254,7 +260,7 @@ export abstract class AbstractDialog<T> extends BaseWidget {
254
260
  if (this.resolve) {
255
261
  return Promise.reject(new Error('The dialog is already opened.'));
256
262
  }
257
- this.activeElement = window.document.activeElement as HTMLElement;
263
+ this.activeElement = this.node.ownerDocument.activeElement as HTMLElement;
258
264
  return new Promise<T | undefined>((resolve, reject) => {
259
265
  this.resolve = resolve;
260
266
  this.reject = reject;
@@ -263,7 +269,7 @@ export abstract class AbstractDialog<T> extends BaseWidget {
263
269
  this.reject = undefined;
264
270
  }));
265
271
 
266
- Widget.attach(this, document.body);
272
+ Widget.attach(this, this.node.ownerDocument.body);
267
273
  this.activate();
268
274
  });
269
275
  }
@@ -351,8 +357,12 @@ export abstract class AbstractDialog<T> extends BaseWidget {
351
357
  }
352
358
 
353
359
  @injectable()
354
- export class ConfirmDialogProps extends DialogProps {
360
+ export class MessageDialogProps extends DialogProps {
355
361
  readonly msg: string | HTMLElement;
362
+ }
363
+
364
+ @injectable()
365
+ export class ConfirmDialogProps extends MessageDialogProps {
356
366
  readonly cancel?: string;
357
367
  readonly ok?: string;
358
368
  }
@@ -383,7 +393,7 @@ export class ConfirmDialog extends AbstractDialog<boolean> {
383
393
 
384
394
  protected createMessageNode(msg: string | HTMLElement): HTMLElement {
385
395
  if (typeof msg === 'string') {
386
- const messageNode = document.createElement('div');
396
+ const messageNode = this.node.ownerDocument.createElement('div');
387
397
  messageNode.textContent = msg;
388
398
  return messageNode;
389
399
  }
@@ -401,48 +411,52 @@ export async function confirmExit(): Promise<boolean> {
401
411
  return safeToExit === true;
402
412
  }
403
413
 
404
- export class ConfirmSaveDialogProps extends ConfirmDialogProps {
414
+ export class ConfirmSaveDialogProps extends MessageDialogProps {
415
+ readonly cancel: string;
416
+ readonly dontSave: string;
405
417
  readonly save: string;
406
- performSave: () => Promise<void>;
407
418
  }
408
419
 
409
- export class ConfirmSaveDialog extends ConfirmDialog {
420
+ // Dialog prompting the user to confirm whether they wish to save changes or not
421
+ export class ConfirmSaveDialog extends AbstractDialog<boolean | undefined> {
422
+ protected result?: boolean = false;
410
423
 
411
- protected saveButton: HTMLButtonElement | undefined;
412
424
  constructor(
413
425
  @inject(ConfirmSaveDialogProps) protected override readonly props: ConfirmSaveDialogProps
414
426
  ) {
415
427
  super(props);
428
+ // Append message and buttons to the dialog
416
429
  this.contentNode.appendChild(this.createMessageNode(this.props.msg));
417
- // reorder buttons
418
- this.controlPanel.childNodes.forEach(child => this.controlPanel.removeChild(child));
419
- [this.acceptButton, this.closeButton].forEach(child => {
420
- if (typeof child !== 'undefined') {
421
- this.controlPanel.appendChild(child);
422
- }
423
- });
424
- this.appendSaveButton(props.save).addEventListener('click', async () => {
425
- await props.performSave();
426
- this.acceptButton?.click();
427
- });
430
+ this.closeButton = this.appendButtonAndSetResult(props.cancel, false);
431
+ this.appendButtonAndSetResult(props.dontSave, false, false);
432
+ this.acceptButton = this.appendButtonAndSetResult(props.save, true, true);
428
433
  }
429
434
 
430
- protected appendSaveButton(text: string = Dialog.OK): HTMLButtonElement {
431
- this.saveButton = this.createButton(text);
432
- this.controlPanel.appendChild(this.saveButton);
433
- this.saveButton.classList.add('main');
434
- return this.saveButton;
435
+ get value(): boolean | undefined {
436
+ return this.result;
435
437
  }
436
438
 
437
- protected override onActivateRequest(msg: Message): void {
438
- super.onActivateRequest(msg);
439
- if (this.saveButton) {
440
- this.saveButton.focus();
439
+ protected createMessageNode(msg: string | HTMLElement): HTMLElement {
440
+ if (typeof msg === 'string') {
441
+ const messageNode = document.createElement('div');
442
+ messageNode.textContent = msg;
443
+ return messageNode;
441
444
  }
445
+ return msg;
446
+ }
447
+
448
+ protected appendButtonAndSetResult(text: string, primary: boolean, result?: boolean): HTMLButtonElement {
449
+ const button = this.appendButton(text, primary);
450
+ button.addEventListener('click', () => {
451
+ this.result = result;
452
+ this.accept();
453
+ });
454
+ return button;
442
455
  }
443
456
 
444
457
  }
445
458
 
459
+ // Asks the user to confirm whether they want to exit with or without saving the changes
446
460
  export async function confirmExitWithOrWithoutSaving(captionsToSave: string[], performSave: () => Promise<void>): Promise<boolean> {
447
461
  const div: HTMLElement = document.createElement('div');
448
462
  div.innerText = nls.localizeByDefault("Your changes will be lost if you don't save them.");
@@ -458,15 +472,18 @@ export async function confirmExitWithOrWithoutSaving(captionsToSave: string[], p
458
472
  });
459
473
  span.appendChild(document.createElement('br'));
460
474
  div.appendChild(span);
461
- const safeToExit = await new ConfirmSaveDialog({
475
+ const result = await new ConfirmSaveDialog({
462
476
  title: nls.localizeByDefault('Do you want to save the changes to the following {0} files?', captionsToSave.length),
463
477
  msg: div,
464
- ok: nls.localizeByDefault("Don't Save"),
478
+ dontSave: nls.localizeByDefault("Don't Save"),
465
479
  save: nls.localizeByDefault('Save All'),
466
- cancel: Dialog.CANCEL,
467
- performSave: performSave
480
+ cancel: Dialog.CANCEL
468
481
  }).open();
469
- return safeToExit === true;
482
+
483
+ if (result) {
484
+ await performSave();
485
+ }
486
+ return result !== undefined;
470
487
  } else {
471
488
  // fallback if not passed with an empty caption-list.
472
489
  return confirmExit();
@@ -287,14 +287,16 @@ export class ShouldSaveDialog extends AbstractDialog<boolean> {
287
287
  constructor(widget: Widget) {
288
288
  super({
289
289
  title: nls.localizeByDefault('Do you want to save the changes you made to {0}?', widget.title.label || widget.title.caption)
290
+ }, {
291
+ node: widget.node.ownerDocument.createElement('div')
290
292
  });
291
293
 
292
- const messageNode = document.createElement('div');
294
+ const messageNode = this.node.ownerDocument.createElement('div');
293
295
  messageNode.textContent = nls.localizeByDefault("Your changes will be lost if you don't save them.");
294
296
  messageNode.setAttribute('style', 'flex: 1 100%; padding-bottom: calc(var(--theia-ui-padding)*3);');
295
297
  this.contentNode.appendChild(messageNode);
296
- this.dontSaveButton = this.appendDontSaveButton();
297
298
  this.appendCloseButton();
299
+ this.dontSaveButton = this.appendDontSaveButton();
298
300
  this.appendAcceptButton(nls.localizeByDefault('Save'));
299
301
  }
300
302
 
@@ -23,6 +23,7 @@ import { Emitter } from '../common/event';
23
23
  import { SecondaryWindowService } from './window/secondary-window-service';
24
24
  import { KeybindingRegistry } from './keybinding';
25
25
  import { ColorApplicationContribution } from './color-application-contribution';
26
+ import { StylingService } from './styling-service';
26
27
 
27
28
  /** Widget to be contained directly in a secondary window. */
28
29
  class SecondaryWindowRootWidget extends Widget {
@@ -50,8 +51,6 @@ class SecondaryWindowRootWidget extends Widget {
50
51
  */
51
52
  @injectable()
52
53
  export class SecondaryWindowHandler {
53
- /** List of currently open secondary windows. Window references should be removed once the window is closed. */
54
- protected readonly secondaryWindows: Window[] = [];
55
54
  /** List of widgets in secondary windows. */
56
55
  protected readonly _widgets: ExtractableWidget[] = [];
57
56
 
@@ -63,6 +62,9 @@ export class SecondaryWindowHandler {
63
62
  @inject(ColorApplicationContribution)
64
63
  protected colorAppContribution: ColorApplicationContribution;
65
64
 
65
+ @inject(StylingService)
66
+ protected stylingService: StylingService;
67
+
66
68
  protected readonly onDidAddWidgetEmitter = new Emitter<Widget>();
67
69
  /** Subscribe to get notified when a widget is added to this handler, i.e. the widget was moved to an secondary window . */
68
70
  readonly onDidAddWidget = this.onDidAddWidgetEmitter.event;
@@ -95,33 +97,6 @@ export class SecondaryWindowHandler {
95
97
  return;
96
98
  }
97
99
  this.applicationShell = shell;
98
-
99
- // Set up messaging with secondary windows
100
- window.addEventListener('message', (event: MessageEvent) => {
101
- console.trace('Message on main window', event);
102
- if (event.data.fromSecondary) {
103
- console.trace('Message comes from secondary window');
104
- return;
105
- }
106
- if (event.data.fromMain) {
107
- console.trace('Message has mainWindow marker, therefore ignore it');
108
- return;
109
- }
110
-
111
- // Filter setImmediate messages. Do not forward because these come in with very high frequency.
112
- // They are not needed in secondary windows because these messages are just a work around
113
- // to make setImmediate work in the main window: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
114
- if (typeof event.data === 'string' && event.data.startsWith('setImmediate')) {
115
- return;
116
- }
117
-
118
- console.trace('Delegate main window message to secondary windows', event);
119
- this.secondaryWindows.forEach(secondaryWindow => {
120
- if (!secondaryWindow.window.closed) {
121
- secondaryWindow.window.postMessage({ ...event.data, fromMain: true }, '*');
122
- }
123
- });
124
- });
125
100
  }
126
101
 
127
102
  /**
@@ -139,21 +114,13 @@ export class SecondaryWindowHandler {
139
114
  return;
140
115
  }
141
116
 
142
- const newWindow = this.secondaryWindowService.createSecondaryWindow(closed => {
143
- this.applicationShell.closeWidget(widget.id);
144
- const extIndex = this.secondaryWindows.indexOf(closed);
145
- if (extIndex > -1) {
146
- this.secondaryWindows.splice(extIndex, 1);
147
- }
148
- });
117
+ const newWindow = this.secondaryWindowService.createSecondaryWindow(widget, this.applicationShell);
149
118
 
150
119
  if (!newWindow) {
151
120
  this.messageService.error('The widget could not be moved to a secondary window because the window creation failed. Please make sure to allow popups.');
152
121
  return;
153
122
  }
154
123
 
155
- this.secondaryWindows.push(newWindow);
156
-
157
124
  const mainWindowTitle = document.title;
158
125
  newWindow.onload = () => {
159
126
  this.keybindings.registerEventListeners(newWindow);
@@ -168,6 +135,7 @@ export class SecondaryWindowHandler {
168
135
  return;
169
136
  }
170
137
  const unregisterWithColorContribution = this.colorAppContribution.registerWindow(newWindow);
138
+ const unregisterWithStylingService = this.stylingService.registerWindow(newWindow);
171
139
 
172
140
  widget.secondaryWindow = newWindow;
173
141
  const rootWidget = new SecondaryWindowRootWidget();
@@ -182,6 +150,7 @@ export class SecondaryWindowHandler {
182
150
  // Close the window if the widget is disposed, e.g. by a command closing all widgets.
183
151
  widget.disposed.connect(() => {
184
152
  unregisterWithColorContribution.dispose();
153
+ unregisterWithStylingService.dispose();
185
154
  this.removeWidget(widget);
186
155
  if (!newWindow.closed) {
187
156
  newWindow.close();
@@ -21,6 +21,7 @@ import { ColorRegistry } from './color-registry';
21
21
  import { DecorationStyle } from './decoration-style';
22
22
  import { FrontendApplicationContribution } from './frontend-application';
23
23
  import { ThemeService } from './theming';
24
+ import { Disposable } from '../common';
24
25
 
25
26
  export const StylingParticipant = Symbol('StylingParticipant');
26
27
 
@@ -40,8 +41,7 @@ export interface CssStyleCollector {
40
41
 
41
42
  @injectable()
42
43
  export class StylingService implements FrontendApplicationContribution {
43
-
44
- protected cssElement = DecorationStyle.createStyleElement('contributedColorTheme');
44
+ protected cssElements = new Map<Window, HTMLStyleElement>();
45
45
 
46
46
  @inject(ThemeService)
47
47
  protected readonly themeService: ThemeService;
@@ -53,11 +53,22 @@ export class StylingService implements FrontendApplicationContribution {
53
53
  protected readonly themingParticipants: ContributionProvider<StylingParticipant>;
54
54
 
55
55
  onStart(): void {
56
- this.applyStyling(this.themeService.getCurrentTheme());
57
- this.themeService.onDidColorThemeChange(e => this.applyStyling(e.newTheme));
56
+ this.registerWindow(window);
57
+ this.themeService.onDidColorThemeChange(e => this.applyStylingToWindows(e.newTheme));
58
+ }
59
+
60
+ registerWindow(win: Window): Disposable {
61
+ const cssElement = DecorationStyle.createStyleElement('contributedColorTheme', win.document.head);
62
+ this.cssElements.set(win, cssElement);
63
+ this.applyStyling(this.themeService.getCurrentTheme(), cssElement);
64
+ return Disposable.create(() => this.cssElements.delete(win));
65
+ }
66
+
67
+ protected applyStylingToWindows(theme: Theme): void {
68
+ this.cssElements.forEach(cssElement => this.applyStyling(theme, cssElement));
58
69
  }
59
70
 
60
- protected applyStyling(theme: Theme): void {
71
+ protected applyStyling(theme: Theme, cssElement: HTMLStyleElement): void {
61
72
  const rules: string[] = [];
62
73
  const colorTheme: ColorTheme = {
63
74
  type: theme.type,
@@ -71,6 +82,6 @@ export class StylingService implements FrontendApplicationContribution {
71
82
  themingParticipant.registerThemeStyle(colorTheme, styleCollector);
72
83
  }
73
84
  const fullCss = rules.join('\n');
74
- this.cssElement.innerText = fullCss;
85
+ cssElement.innerText = fullCss;
75
86
  }
76
87
  }
@@ -115,6 +115,10 @@ export class BaseWidget extends Widget {
115
115
  protected scrollBar?: PerfectScrollbar;
116
116
  protected scrollOptions?: PerfectScrollbar.Options;
117
117
 
118
+ constructor(options?: Widget.IOptions) {
119
+ super(options);
120
+ }
121
+
118
122
  override dispose(): void {
119
123
  if (this.isDisposed) {
120
124
  return;
@@ -16,6 +16,9 @@
16
16
  import { inject, injectable, postConstruct } from 'inversify';
17
17
  import { SecondaryWindowService } from './secondary-window-service';
18
18
  import { WindowService } from './window-service';
19
+ import { ExtractableWidget } from '../widgets';
20
+ import { ApplicationShell } from '../shell';
21
+ import { Saveable } from '../saveable';
19
22
 
20
23
  @injectable()
21
24
  export class DefaultSecondaryWindowService implements SecondaryWindowService {
@@ -37,6 +40,33 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
37
40
 
38
41
  @postConstruct()
39
42
  init(): void {
43
+ // Set up messaging with secondary windows
44
+ window.addEventListener('message', (event: MessageEvent) => {
45
+ console.trace('Message on main window', event);
46
+ if (event.data.fromSecondary) {
47
+ console.trace('Message comes from secondary window');
48
+ return;
49
+ }
50
+ if (event.data.fromMain) {
51
+ console.trace('Message has mainWindow marker, therefore ignore it');
52
+ return;
53
+ }
54
+
55
+ // Filter setImmediate messages. Do not forward because these come in with very high frequency.
56
+ // They are not needed in secondary windows because these messages are just a work around
57
+ // to make setImmediate work in the main window: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
58
+ if (typeof event.data === 'string' && event.data.startsWith('setImmediate')) {
59
+ return;
60
+ }
61
+
62
+ console.trace('Delegate main window message to secondary windows', event);
63
+ this.secondaryWindows.forEach(secondaryWindow => {
64
+ if (!secondaryWindow.window.closed) {
65
+ secondaryWindow.window.postMessage({ ...event.data, fromMain: true }, '*');
66
+ }
67
+ });
68
+ });
69
+
40
70
  // Close all open windows when the main window is closed.
41
71
  this.windowService.onUnload(() => {
42
72
  // Iterate backwards because calling window.close might remove the window from the array
@@ -46,33 +76,52 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
46
76
  });
47
77
  }
48
78
 
49
- createSecondaryWindow(onClose?: (closedWin: Window) => void): Window | undefined {
50
- const win = this.doCreateSecondaryWindow(onClose);
79
+ createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined {
80
+ const win = this.doCreateSecondaryWindow(widget, shell);
51
81
  if (win) {
52
82
  this.secondaryWindows.push(win);
83
+ win.addEventListener('close', () => {
84
+ const extIndex = this.secondaryWindows.indexOf(win);
85
+ if (extIndex > -1) {
86
+ this.secondaryWindows.splice(extIndex, 1);
87
+ };
88
+ });
53
89
  }
54
90
  return win;
55
91
  }
56
92
 
57
- protected doCreateSecondaryWindow(onClose?: (closedWin: Window) => void): Window | undefined {
58
- const win = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, this.nextWindowId(), 'popup');
59
- if (win) {
60
- // Add the unload listener after the dom content was loaded because otherwise the unload listener is called already on open in some browsers (e.g. Chrome).
61
- win.addEventListener('DOMContentLoaded', () => {
62
- win.addEventListener('unload', () => {
63
- this.handleWindowClosed(win, onClose);
64
- });
65
- });
93
+ protected findWindow<T>(windowName: string): Window | undefined {
94
+ for (const w of this.secondaryWindows) {
95
+ if (w.name === windowName) {
96
+ return w;
97
+ }
66
98
  }
67
- return win ?? undefined;
99
+ return undefined;
68
100
  }
69
101
 
70
- protected handleWindowClosed(win: Window, onClose?: (closedWin: Window) => void): void {
71
- const extIndex = this.secondaryWindows.indexOf(win);
72
- if (extIndex > -1) {
73
- this.secondaryWindows.splice(extIndex, 1);
74
- };
75
- onClose?.(win);
102
+ protected doCreateSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined {
103
+ const newWindow = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, this.nextWindowId(), 'popup') ?? undefined;
104
+ if (newWindow) {
105
+ newWindow.addEventListener('DOMContentLoaded', () => {
106
+ newWindow.addEventListener('beforeunload', evt => {
107
+ const saveable = Saveable.get(widget);
108
+ const wouldLoseState = !!saveable && saveable.dirty && saveable.autoSave === 'off';
109
+ if (wouldLoseState) {
110
+ evt.returnValue = '';
111
+ evt.preventDefault();
112
+ return 'non-empty';
113
+ }
114
+ }, { capture: true });
115
+
116
+ newWindow.addEventListener('close', () => {
117
+ const saveable = Saveable.get(widget);
118
+ shell.closeWidget(widget.id, {
119
+ save: !!saveable && saveable.dirty && saveable.autoSave !== 'off'
120
+ });
121
+ });
122
+ });
123
+ }
124
+ return newWindow;
76
125
  }
77
126
 
78
127
  focus(win: Window): void {
@@ -14,6 +14,9 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
+ import { ApplicationShell } from '../shell';
18
+ import { ExtractableWidget } from '../widgets';
19
+
17
20
  export const SecondaryWindowService = Symbol('SecondaryWindowService');
18
21
 
19
22
  /**
@@ -29,7 +32,7 @@ export interface SecondaryWindowService {
29
32
  * @param onClose optional callback that is invoked when the secondary window is closed
30
33
  * @returns the created window or `undefined` if it could not be created
31
34
  */
32
- createSecondaryWindow(onClose?: (win: Window) => void): Window | undefined;
35
+ createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined;
33
36
 
34
37
  /** Handles focussing the given secondary window in the browser and on Electron. */
35
38
  focus(win: Window): void;
@@ -51,7 +51,7 @@ export interface RequestMessage {
51
51
 
52
52
  export interface NotificationMessage {
53
53
  type: RpcMessageType.Notification;
54
- id: number;
54
+ id?: number;
55
55
  method: string;
56
56
  args: any[];
57
57
  }
@@ -111,7 +111,7 @@ export interface RpcMessageDecoder {
111
111
  export interface RpcMessageEncoder {
112
112
  cancel(buf: WriteBuffer, requestId: number): void;
113
113
 
114
- notification(buf: WriteBuffer, requestId: number, method: string, args: any[]): void
114
+ notification(buf: WriteBuffer, method: string, args: any[], id?: number): void
115
115
 
116
116
  request(buf: WriteBuffer, requestId: number, method: string, args: any[]): void
117
117
 
@@ -130,8 +130,8 @@ export class MsgPackMessageEncoder implements RpcMessageEncoder {
130
130
  cancel(buf: WriteBuffer, requestId: number): void {
131
131
  this.encode<CancelMessage>(buf, { type: RpcMessageType.Cancel, id: requestId });
132
132
  }
133
- notification(buf: WriteBuffer, requestId: number, method: string, args: any[]): void {
134
- this.encode<NotificationMessage>(buf, { type: RpcMessageType.Notification, id: requestId, method, args });
133
+ notification(buf: WriteBuffer, method: string, args: any[], id?: number): void {
134
+ this.encode<NotificationMessage>(buf, { type: RpcMessageType.Notification, method, args, id });
135
135
  }
136
136
  request(buf: WriteBuffer, requestId: number, method: string, args: any[]): void {
137
137
  this.encode<RequestMessage>(buf, { type: RpcMessageType.Request, id: requestId, method, args });
@@ -77,7 +77,11 @@ export class RpcProtocol {
77
77
  this.encoder = options.encoder ?? new MsgPackMessageEncoder();
78
78
  this.decoder = options.decoder ?? new MsgPackMessageDecoder();
79
79
  this.toDispose.push(this.onNotificationEmitter);
80
- channel.onClose(() => this.toDispose.dispose());
80
+ channel.onClose(event => {
81
+ this.pendingRequests.forEach(pending => pending.reject(new Error(event.reason)));
82
+ this.pendingRequests.clear();
83
+ this.toDispose.dispose();
84
+ });
81
85
  this.toDispose.push(channel.onMessage(readBuffer => this.handleMessage(this.decoder.parse(readBuffer()))));
82
86
  this.mode = options.mode ?? 'default';
83
87
 
@@ -98,7 +102,7 @@ export class RpcProtocol {
98
102
  return;
99
103
  }
100
104
  case RpcMessageType.Notification: {
101
- this.handleNotify(message.id, message.method, message.args);
105
+ this.handleNotify(message.method, message.args, message.id);
102
106
  return;
103
107
  }
104
108
  }
@@ -179,7 +183,7 @@ export class RpcProtocol {
179
183
  }
180
184
 
181
185
  const output = this.channel.getWriteBuffer();
182
- this.encoder.notification(output, this.nextMessageId++, method, args);
186
+ this.encoder.notification(output, method, args, this.nextMessageId++);
183
187
  output.commit();
184
188
  }
185
189
 
@@ -226,7 +230,7 @@ export class RpcProtocol {
226
230
  }
227
231
  }
228
232
 
229
- protected async handleNotify(id: number, method: string, args: any[]): Promise<void> {
233
+ protected async handleNotify(method: string, args: any[], id?: number): Promise<void> {
230
234
  if (this.toDispose.disposed) {
231
235
  return;
232
236
  }