Skip to content
Source
Sign inSign up

captureFeedback.js

166 lines5.0 KB

1import Clutter from "gi://Clutter";
2import Gio from "gi://Gio";
3import GLib from "gi://GLib";
4import Meta from "gi://Meta";
5import St from "gi://St";
6import * as Main from "resource:///org/gnome/shell/ui/main.js";
7import { captureDestinationFrame, findCaptureDestination } from "./feedbackGeometry.js";
8​
9export class CaptureFeedback {
10 begin(sender, pid, snapshot, options) {
11 this.dispose();
12 const session = { sender, pid };
13 this._session = session;
14 this._watch = Gio.bus_watch_name_on_connection(
15 Gio.DBus.session,
16 sender,
17 Gio.BusNameWatcherFlags.NONE,
18 null,
19 () => {
20 if (this._session === session) this.dispose();
21 },
22 );
23 this._timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 6000, () => {
24 this._timeout = 0;
25 this.dispose();
26 return GLib.SOURCE_REMOVE;
27 });
28 try {
29 if (options.animate && snapshot.content && St.Settings.get().enable_animations) {
30 this._actor = new Clutter.Actor({
31 ...snapshot.bufferBounds,
32 content: snapshot.content,
33 reactive: false,
34 });
35 Main.uiGroup.add_child(this._actor);
36 }
37 if (options.flash) {
38 this._flash = new St.Widget({
39 ...snapshot.bounds,
40 reactive: false,
41 style: "background-color: white;",
42 opacity: 24,
43 });
44 Main.uiGroup.add_child(this._flash);
45 this._flash.ease({
46 opacity: 0,
47 duration: 180,
48 mode: Clutter.AnimationMode.EASE_OUT_QUAD,
49 onComplete: () => {
50 this._flash?.destroy();
51 this._flash = null;
52 },
53 });
54 }
55 } catch (error) {
56 this._clearActors();
57 console.warn(`T3 capture effects unavailable: ${error.message}`);
58 }
59 return Boolean(this._actor);
60 }
61​
62 _owned(sender) {
63 if (
64 !this._session ||
65 this._session.sender !== sender ||
66 Main.sessionMode.isLocked ||
67 Main.sessionMode.isGreeter
68 ) {
69 throw new Error("No active capture belongs to this caller.");
70 }
71 return this._session;
72 }
73​
74 async activate(sender, title) {
75 const session = this._owned(sender);
76 if (session.target) return;
77 const find = () =>
78 findCaptureDestination(
79 global
80 .get_window_actors()
81 .map((actor) => actor.meta_window)
82 .filter((window) => window.get_window_type() === Meta.WindowType.NORMAL),
83 session.pid,
84 title,
85 );
86 // A command-palette capture temporarily unmaps T3. Wait for its new surface, not a sleep.
87 const target =
88 find() ??
89 (await new Promise((resolve) => {
90 const check = () => {
91 const window = find();
92 if (window) finish(window);
93 };
94 let timeout;
95 const mapped = global.window_manager.connect("map", check);
96 const finish = (window) => {
97 global.window_manager.disconnect(mapped);
98 if (timeout) GLib.source_remove(timeout);
99 this._cancelActivation = null;
100 resolve(window);
101 };
102 this._cancelActivation = () => finish(undefined);
103 timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1500, () => {
104 timeout = 0;
105 finish(undefined);
106 return GLib.SOURCE_REMOVE;
107 });
108 check();
109 }));
110 if (this._owned(sender) !== session || !target)
111 throw new Error("T3 Code's window is not available for activation.");
112 session.target = target;
113 Main.activateWindow(target, global.get_current_time());
114 }
115​
116 async animate(sender, relative) {
117 const session = this._owned(sender);
118 if (this._flight) return this._flight;
119 const actor = this._actor;
120 if (!actor || !session.target || !St.Settings.get().enable_animations) {
121 this._clearActors();
122 return;
123 }
124 const window = session.target;
125 const bounds = window.frame_rect_to_client_rect(window.get_frame_rect());
126 const frame = captureDestinationFrame(relative, bounds);
127 const distance = Math.hypot(frame.x - actor.x, frame.y - actor.y);
128 this._flight = new Promise((resolve) => {
129 this._land = resolve;
130 actor.ease({
131 x: frame.x,
132 y: frame.y,
133 scale_x: frame.width / actor.width,
134 scale_y: frame.height / actor.height,
135 duration: Math.round(680 - 400 * Math.exp(-distance / 2000)),
136 mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
137 onStopped: () => {
138 this._land?.();
139 this._land = null;
140 },
141 });
142 });
143 await this._flight;
144 // Keep the landed image until the renderer acknowledges the attachment (connection closes).
145 }
146​
147 _clearActors() {
148 this._actor?.destroy();
149 this._flash?.destroy();
150 this._actor = null;
151 this._flash = null;
152 this._land?.();
153 this._land = null;
154 }
155​
156 dispose() {
157 this._session = null;
158 this._cancelActivation?.();
159 this._clearActors();
160 this._flight = null;
161 if (this._watch) Gio.bus_unwatch_name(this._watch);
162 if (this._timeout) GLib.source_remove(this._timeout);
163 this._watch = 0;
164 this._timeout = 0;
165 }