// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- import Atk from 'gi://Atk'; import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Meta from 'gi://Meta'; import St from 'gi://St'; import * as Main from '../main.js'; import * as PanelMenu from '../panelMenu.js'; import {SystemIndicator} from '../quickSettings.js'; // Minimum amount of time the shared indicator is visible (in micro seconds) const MIN_SHARED_INDICATOR_VISIBLE_TIME_US = 5 * GLib.TIME_SPAN_SECOND; export const RemoteAccessApplet = GObject.registerClass( class RemoteAccessApplet extends SystemIndicator { _init() { super._init(); let controller = global.backend.get_remote_access_controller(); if (!controller) return; this._handles = new Set(); this._indicator = this._addIndicator(); this._indicator.icon_name = 'media-record-symbolic'; this._indicator.add_style_class_name('privacy-indicator'); controller.connect('new-handle', (o, handle) => { this._onNewHandle(handle); }); this._sync(); } _isRecording() { // Screenshot UI screencasts have their own panel, so don't show this // indicator if there's only a screenshot UI screencast. if (Main.screenshotUI.screencast_in_progress) return this._handles.size > 1; return this._handles.size > 0; } _sync() { this._indicator.visible = this._isRecording(); } _onStopped(handle) { this._handles.delete(handle); this._sync(); } _onNewHandle(handle) { if (!handle.is_recording) return; this._handles.add(handle); handle.connect('stopped', this._onStopped.bind(this)); this._sync(); } }); export const ScreenRecordingIndicator = GObject.registerClass({ Signals: {'menu-set': {}}, }, class ScreenRecordingIndicator extends PanelMenu.ButtonBox { _init() { super._init({ reactive: true, can_focus: true, track_hover: true, accessible_name: _('Stop Screencast'), accessible_role: Atk.Role.PUSH_BUTTON, }); this.add_style_class_name('screen-recording-indicator'); this._box = new St.BoxLayout(); this.add_child(this._box); this._label = new St.Label({ text: '0:00', y_align: Clutter.ActorAlign.CENTER, }); this._box.add_child(this._label); this._icon = new St.Icon({icon_name: 'screencast-stop-symbolic'}); this._box.add_child(this._icon); this.hide(); Main.screenshotUI.connect( 'notify::screencast-in-progress', this._onScreencastInProgressChanged.bind(this)); } vfunc_event(event) { if (event.type() === Clutter.EventType.TOUCH_BEGIN || event.type() === Clutter.EventType.BUTTON_PRESS) Main.screenshotUI.stopScreencast(); return Clutter.EVENT_PROPAGATE; } _updateLabel() { const minutes = this._secondsPassed / 60; const seconds = this._secondsPassed % 60; this._label.text = '%d:%02d'.format(minutes, seconds); } _onScreencastInProgressChanged() { if (Main.screenshotUI.screencast_in_progress) { this.show(); this._secondsPassed = 0; this._updateLabel(); this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { this._secondsPassed += 1; this._updateLabel(); return GLib.SOURCE_CONTINUE; }); GLib.Source.set_name_by_id( this._timeoutId, '[gnome-shell] screen recording indicator tick'); } else { this.hide(); GLib.source_remove(this._timeoutId); delete this._timeoutId; delete this._secondsPassed; } } }); export const ScreenSharingIndicator = GObject.registerClass({ Signals: {'menu-set': {}}, }, class ScreenSharingIndicator extends PanelMenu.ButtonBox { _init() { super._init({ reactive: true, can_focus: true, track_hover: true, accessible_name: _('Stop Screen Sharing'), accessible_role: Atk.Role.PUSH_BUTTON, }); this.add_style_class_name('screen-sharing-indicator'); this._box = new St.BoxLayout(); this.add_child(this._box); let icon = new St.Icon({icon_name: 'screen-shared-symbolic'}); this._box.add_child(icon); icon = new St.Icon({icon_name: 'screencast-stop-symbolic'}); this._box.add_child(icon); this._controller = global.backend.get_remote_access_controller(); this._handles = new Set(); this._controller?.connect('new-handle', (o, handle) => this._onNewHandle(handle)); this._sync(); } _onNewHandle(handle) { // We can't possibly know about all types of screen sharing on X11, so // showing these controls on X11 might give a false sense of security. // Thus, only enable these controls when using Wayland, where we are // in control of sharing. if (!Meta.is_wayland_compositor()) return; if (handle.isRecording) return; this._handles.add(handle); handle.connect('stopped', () => { this._handles.delete(handle); this._sync(); }); this._sync(); } vfunc_event(event) { if (event.type() === Clutter.EventType.TOUCH_BEGIN || event.type() === Clutter.EventType.BUTTON_PRESS) this._stopSharing(); return Clutter.EVENT_PROPAGATE; } _stopSharing() { for (const handle of this._handles) handle.stop(); } _hideIndicator() { this.hide(); delete this._hideIndicatorId; return GLib.SOURCE_REMOVE; } _sync() { if (this._hideIndicatorId) { GLib.source_remove(this._hideIndicatorId); delete this._hideIndicatorId; } if (this._handles.size > 0) { if (!this.visible) this._visibleTimeUs = GLib.get_monotonic_time(); this.show(); } else if (this.visible) { const currentTimeUs = GLib.get_monotonic_time(); const timeSinceVisibleUs = currentTimeUs - this._visibleTimeUs; if (timeSinceVisibleUs >= MIN_SHARED_INDICATOR_VISIBLE_TIME_US) { this._hideIndicator(); } else { const timeUntilHideUs = MIN_SHARED_INDICATOR_VISIBLE_TIME_US - timeSinceVisibleUs; this._hideIndicatorId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeUntilHideUs / GLib.TIME_SPAN_MILLISECOND, () => this._hideIndicator()); } } } });