1import QtQuick
2import QtQuick.Window
3import QtQml.Models
4import org.kde.kwin
5import "feedbackGeometry.js" as Geometry
6
7// Internal output-only windows do not change, activate, or intercept input to the real window.
8Item {
9 id: root
10 property var options: /*OPTIONS*/ null
11 property string bus: /*BUS*/ ""
12 property string scriptName: /*NAME*/ ""
13 property real motionFactor: /*FACTOR*/ 1
14 property rect frame: Qt.rect(options.bounds.x, options.bounds.y, options.bounds.width, options.bounds.height)
15 property bool showing: false
16 property bool started: false
17 property bool closing: false
18 property bool flying: false
19 property bool initialized: false
20 property bool readySent: false
21 property real flashOpacity: 0
22 property var target: null
23 property url screenshot: Qt.resolvedUrl("capture.png")
24
25 function finish() {
26 if (closing) return;
27 closing = true;
28 showing = false;
29 flight.stop();
30 deadline.stop();
31 done.call();
32 }
33 function begin() {
34 if (started || closing) return;
35 started = true;
36 showing = true;
37 if (options.flash) flash.start();
38 next.call();
39 }
40
41 // A cached, bounded PNG is shared by each output. Nothing repaints while waiting for T3.
42 Image {
43 id: snapshot
44 visible: false
45 source: root.screenshot
46 sourceSize.width: 2560
47 sourceSize.height: 1600
48 onStatusChanged: {
49 if (status === Image.Ready && root.initialized) root.begin();
50 else if (status === Image.Error) root.finish();
51 }
52 }
53 Component.onCompleted: {
54 initialized = true;
55 if (snapshot.status === Image.Ready) begin();
56 else if (snapshot.status === Image.Error) finish();
57 }
58 Instantiator {
59 model: Workspace.screens
60 delegate: Window {
61 required property var modelData
62 x: modelData.geometry.x
63 y: modelData.geometry.y
64 width: modelData.geometry.width
65 height: modelData.geometry.height
66 color: "transparent"
67 visible: root.showing
68 flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.BypassWindowManagerHint | Qt.WindowStaysOnTopHint | Qt.WindowTransparentForInput | Qt.WindowDoesNotAcceptFocus
69 property bool outputOnly: true
70 property bool kwin_shadow_enabled: false
71 Connections {
72 target: modelData
73 function onGeometryChanged() { root.finish(); }
74 }
75 onFrameSwapped: {
76 if (root.showing && !root.readySent) {
77 root.readySent = true;
78 ready.call();
79 }
80 }
81 Item {
82 x: root.frame.x - modelData.geometry.x
83 y: root.frame.y - modelData.geometry.y
84 width: root.frame.width
85 height: root.frame.height
86 Image {
87 anchors.fill: parent
88 visible: root.options.animate
89 source: snapshot.source
90 sourceSize: snapshot.sourceSize
91 fillMode: Image.Stretch
92 }
93 }
94 Rectangle {
95 x: root.options.bounds.x - modelData.geometry.x
96 y: root.options.bounds.y - modelData.geometry.y
97 width: root.options.bounds.width
98 height: root.options.bounds.height
99 color: "white"
100 opacity: root.flashOpacity
101 }
102 }
103 }
104 NumberAnimation {
105 id: flash
106 target: root
107 property: "flashOpacity"
108 from: 0.10
109 to: 0
110 duration: 180
111 easing.type: Easing.OutQuad
112 }
113 PropertyAnimation {
114 id: flight
115 target: root
116 property: "frame"
117 easing.type: Easing.OutCubic
118 onFinished: landed.call()
119 }
120 Timer { id: deadline; interval: 6000; running: true; onTriggered: root.finish() }
121 Connections {
122 target: Workspace
123 function onScreensChanged() { root.finish(); }
124 function onVirtualScreenGeometryChanged() { root.finish(); }
125 function onCurrentDesktopChanged() { root.finish(); }
126 function onCurrentActivityChanged() { root.finish(); }
127 }
128 Connections {
129 target: root.target
130 function onClientGeometryChanged() { root.finish(); }
131 function onClosed() { root.finish(); }
132 function onMinimizedChanged() { root.finish(); }
133 }
134 DBusCall {
135 id: next
136 service: root.bus
137 path: "/com/t3tools/KdeCapture/Feedback"
138 dbusInterface: "com.t3tools.KdeCapture.Feedback"
139 method: "Next"
140 onFailed: root.finish()
141 onFinished: (values) => {
142 if (root.closing) return;
143 try {
144 const command = JSON.parse(values[0]);
145 if (command.command === "close") { root.finish(); return; }
146 if (command.command !== "animate" || root.flying || !root.options.animate)
147 throw new Error("Invalid capture feedback command");
148 const destination = Geometry.destination(Workspace.windows, root.options.pid, command.title, command.frame);
149 root.target = destination.window;
150 const frame = destination.frame;
151 const distance = Math.hypot(frame.x - root.frame.x, frame.y - root.frame.y);
152 flight.duration = Math.round((680 - 400 * Math.exp(-distance / 2000)) * root.motionFactor);
153 flight.to = Qt.rect(frame.x, frame.y, frame.width, frame.height);
154 root.flying = true;
155 flight.start();
156 // Keep a pending call during the animation for cancellation and owner loss.
157 next.call();
158 } catch (error) { root.finish(); }
159 }
160 }
161 DBusCall {
162 id: ready
163 service: root.bus
164 path: "/com/t3tools/KdeCapture/Feedback"
165 dbusInterface: "com.t3tools.KdeCapture.Feedback"
166 method: "Event"
167 arguments: [JSON.stringify({event: "ready", animate: root.options.animate})]
168 onFailed: root.finish()
169 }
170 DBusCall {
171 id: landed
172 service: root.bus
173 path: "/com/t3tools/KdeCapture/Feedback"
174 dbusInterface: "com.t3tools.KdeCapture.Feedback"
175 method: "Event"
176 arguments: ['{"event":"landed"}']
177 onFailed: root.finish()
178 }
179 DBusCall {
180 id: done
181 service: root.bus
182 path: "/com/t3tools/KdeCapture/Feedback"
183 dbusInterface: "com.t3tools.KdeCapture.Feedback"
184 method: "Event"
185 arguments: ['{"event":"done"}']
186 onFinished: unload.call()
187 onFailed: unload.call()
188 }
189 // Also unload after helper loss; a hidden script would otherwise retain the screenshot.
190 DBusCall {
191 id: unload
192 service: "org.kde.KWin"
193 path: "/Scripting"
194 dbusInterface: "org.kde.kwin.Scripting"
195 method: "unloadScript"
196 arguments: [root.scriptName]
197 }
198}