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; } /**