| 1 | import QtQuick |
| 2 | import QtQuick.Window |
| 3 | import QtQml.Models |
| 4 | import org.kde.kwin |
| 5 | import "feedbackGeometry.js" as Geometry |
| 6 | |
| 7 | // Internal output-only windows do not change, activate, or intercept input to the real window. |
| 8 | Item { |
| 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 | } |