diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5528a7e6630b6..b970ebb0320c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@
## 1.50.0
+- [plugin] Support WindowState active API [#13718](https://github.com/eclipse-theia/theia/pull/13718) - contributed on behalf of STMicroelectronics
+
[Breaking Changes:](#breaking_changes_1.50.0)
- [core] Classes implementing the `Saveable` interface no longer need to implement the `autoSave` field. However, a new `onContentChanged` event has been added instead.
diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts
index ccdfb6ad1e6c8..c63e8ca5d0309 100644
--- a/packages/plugin-ext/src/common/plugin-api-rpc.ts
+++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts
@@ -893,7 +893,8 @@ export interface WindowMain {
}
export interface WindowStateExt {
- $onWindowStateChanged(focus: boolean): void;
+ $onDidChangeWindowFocus(focused: boolean): void;
+ $onDidChangeWindowActive(active: boolean): void;
}
export interface NotificationExt {
diff --git a/packages/plugin-ext/src/main/browser/window-activity-tracker.ts b/packages/plugin-ext/src/main/browser/window-activity-tracker.ts
new file mode 100644
index 0000000000000..239f0f7fa1ebc
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/window-activity-tracker.ts
@@ -0,0 +1,96 @@
+// *****************************************************************************
+// Copyright (C) 2024 STMicroelectronics and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { Disposable, Emitter, Event } from '@theia/core';
+
+const CHECK_INACTIVITY_LIMIT = 30;
+const CHECK_INACTIVITY_INTERVAL = 1000;
+
+const eventListenerOptions: AddEventListenerOptions = {
+ passive: true,
+ capture: true
+};
+export class WindowActivityTracker implements Disposable {
+
+ private inactivityCounter = 0; // number of times inactivity was checked since last reset
+ private readonly inactivityLimit = CHECK_INACTIVITY_LIMIT; // number of inactivity checks done before sending inactive signal
+ private readonly checkInactivityInterval = CHECK_INACTIVITY_INTERVAL; // check interval in milliseconds
+ private interval: NodeJS.Timeout | undefined;
+
+ protected readonly onDidChangeActiveStateEmitter = new Emitter();
+ private _activeState: boolean = true;
+
+ constructor(readonly win: Window) {
+ this.initializeListeners(this.win);
+ }
+
+ get onDidChangeActiveState(): Event {
+ return this.onDidChangeActiveStateEmitter.event;
+ }
+
+ private set activeState(newState: boolean) {
+ if (this._activeState !== newState) {
+ this._activeState = newState;
+ this.onDidChangeActiveStateEmitter.fire(this._activeState);
+ }
+ }
+
+ private initializeListeners(win: Window): void {
+ // currently assumes activity based on key/mouse/touch pressed, not on mouse move or scrolling.
+ win.addEventListener('mousedown', this.resetInactivity, eventListenerOptions);
+ win.addEventListener('keydown', this.resetInactivity, eventListenerOptions);
+ win.addEventListener('touchstart', this.resetInactivity, eventListenerOptions);
+ }
+
+ dispose(): void {
+ this.stopTracking();
+ this.win.removeEventListener('mousedown', this.resetInactivity);
+ this.win.removeEventListener('keydown', this.resetInactivity);
+ this.win.removeEventListener('touchstart', this.resetInactivity);
+
+ }
+
+ // Reset inactivity time
+ private resetInactivity = (): void => {
+ this.inactivityCounter = 0;
+ if (!this.interval) {
+ // it was not active. Set as active and restart tracking inactivity
+ this.activeState = true;
+ this.startTracking();
+ }
+ };
+
+ // Check inactivity status
+ private checkInactivity = (): void => {
+ this.inactivityCounter++;
+ if (this.inactivityCounter >= this.inactivityLimit) {
+ this.activeState = false;
+ this.stopTracking();
+ }
+ };
+
+ public startTracking(): void {
+ this.stopTracking();
+ this.interval = setInterval(this.checkInactivity, this.checkInactivityInterval);
+ }
+
+ public stopTracking(): void {
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = undefined;
+ }
+ }
+}
diff --git a/packages/plugin-ext/src/main/browser/window-state-main.ts b/packages/plugin-ext/src/main/browser/window-state-main.ts
index c7e6800855ffc..1a48ea1f50e56 100644
--- a/packages/plugin-ext/src/main/browser/window-state-main.ts
+++ b/packages/plugin-ext/src/main/browser/window-state-main.ts
@@ -23,6 +23,7 @@ import { UriComponents } from '../../common/uri-components';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { ExternalUriService } from '@theia/core/lib/browser/external-uri-service';
+import { WindowActivityTracker } from './window-activity-tracker';
export class WindowStateMain implements WindowMain, Disposable {
@@ -46,6 +47,10 @@ export class WindowStateMain implements WindowMain, Disposable {
const fireDidBlur = () => this.onFocusChanged(false);
window.addEventListener('blur', fireDidBlur);
this.toDispose.push(Disposable.create(() => window.removeEventListener('blur', fireDidBlur)));
+
+ const tracker = new WindowActivityTracker(window);
+ this.toDispose.push(tracker.onDidChangeActiveState(isActive => this.onActiveStateChanged(isActive)));
+ this.toDispose.push(tracker);
}
dispose(): void {
@@ -53,7 +58,11 @@ export class WindowStateMain implements WindowMain, Disposable {
}
private onFocusChanged(focused: boolean): void {
- this.proxy.$onWindowStateChanged(focused);
+ this.proxy.$onDidChangeWindowFocus(focused);
+ }
+
+ private onActiveStateChanged(isActive: boolean): void {
+ this.proxy.$onDidChangeWindowActive(isActive);
}
async $openUri(uriComponent: UriComponents): Promise {
diff --git a/packages/plugin-ext/src/plugin/window-state.ts b/packages/plugin-ext/src/plugin/window-state.ts
index 4b4e58aac3bbd..ed6d4b9cbe208 100644
--- a/packages/plugin-ext/src/plugin/window-state.ts
+++ b/packages/plugin-ext/src/plugin/window-state.ts
@@ -31,21 +31,28 @@ export class WindowStateExtImpl implements WindowStateExt {
constructor(rpc: RPCProtocol) {
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.WINDOW_MAIN);
- this.windowStateCached = { focused: true }; // supposed tab is active on start
+ this.windowStateCached = { focused: true, active: true }; // supposed tab is active on start
}
getWindowState(): WindowState {
return this.windowStateCached;
}
- $onWindowStateChanged(focused: boolean): void {
- const state = { focused: focused };
- if (state === this.windowStateCached) {
+ $onDidChangeWindowFocus(focused: boolean): void {
+ this.onDidChangeWindowProperty('focused', focused);
+ }
+
+ $onDidChangeWindowActive(active: boolean): void {
+ this.onDidChangeWindowProperty('active', active);
+ }
+
+ onDidChangeWindowProperty(property: keyof WindowState, value: boolean): void {
+ if (value === this.windowStateCached[property]) {
return;
}
- this.windowStateCached = state;
- this.windowStateChangedEmitter.fire(state);
+ this.windowStateCached = { ...this.windowStateCached, [property]: value };
+ this.windowStateChangedEmitter.fire(this.windowStateCached);
}
openUri(uri: URI): Promise {
diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts
index d5cc70bd07896..dd2d6cd7f2275 100644
--- a/packages/plugin/src/theia.d.ts
+++ b/packages/plugin/src/theia.d.ts
@@ -2741,6 +2741,12 @@ export module '@theia/plugin' {
* Whether the current window is focused.
*/
readonly focused: boolean;
+
+ /**
+ * Whether the window has been interacted with recently. This will change
+ * immediately on activity, or after a short time of user inactivity.
+ */
+ readonly active: boolean;
}
/**