import QtQuickimport QtQuick.Windowimport QtQml.Modelsimport org.kde.kwinimport "feedbackGeometry.js" as Geometry // Internal output-only windows do not change, activate, or intercept input to the real window.Item { id: root property var options: /*OPTIONS*/ null property string bus: /*BUS*/ "" property string scriptName: /*NAME*/ "" property real motionFactor: /*FACTOR*/ 1 property rect frame: Qt.rect(options.bounds.x, options.bounds.y, options.bounds.width, options.bounds.height) property bool showing: false property bool started: false property bool closing: false property bool flying: false property bool initialized: false property bool readySent: false property real flashOpacity: 0 property var target: null property url screenshot: Qt.resolvedUrl("capture.png") function finish() { if (closing) return; closing = true; showing = false; flight.stop(); deadline.stop(); done.call(); } function begin() { if (started || closing) return; started = true; showing = true; if (options.flash) flash.start(); next.call(); } // A cached, bounded PNG is shared by each output. Nothing repaints while waiting for T3. Image { id: snapshot visible: false source: root.screenshot sourceSize.width: 2560 sourceSize.height: 1600 onStatusChanged: { if (status === Image.Ready && root.initialized) root.begin(); else if (status === Image.Error) root.finish(); } } Component.onCompleted: { initialized = true; if (snapshot.status === Image.Ready) begin(); else if (snapshot.status === Image.Error) finish(); } Instantiator { model: Workspace.screens delegate: Window { required property var modelData x: modelData.geometry.x y: modelData.geometry.y width: modelData.geometry.width height: modelData.geometry.height color: "transparent" visible: root.showing flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.BypassWindowManagerHint | Qt.WindowStaysOnTopHint | Qt.WindowTransparentForInput | Qt.WindowDoesNotAcceptFocus property bool outputOnly: true property bool kwin_shadow_enabled: false Connections { target: modelData function onGeometryChanged() { root.finish(); } } onFrameSwapped: { if (root.showing && !root.readySent) { root.readySent = true; ready.call(); } } Item { x: root.frame.x - modelData.geometry.x y: root.frame.y - modelData.geometry.y width: root.frame.width height: root.frame.height Image { anchors.fill: parent visible: root.options.animate source: snapshot.source sourceSize: snapshot.sourceSize fillMode: Image.Stretch } } Rectangle { x: root.options.bounds.x - modelData.geometry.x y: root.options.bounds.y - modelData.geometry.y width: root.options.bounds.width height: root.options.bounds.height color: "white" opacity: root.flashOpacity } } } NumberAnimation { id: flash target: root property: "flashOpacity" from: 0.10 to: 0 duration: 180 easing.type: Easing.OutQuad } PropertyAnimation { id: flight target: root property: "frame" easing.type: Easing.OutCubic onFinished: landed.call() } Timer { id: deadline; interval: 6000; running: true; onTriggered: root.finish() } Connections { target: Workspace function onScreensChanged() { root.finish(); } function onVirtualScreenGeometryChanged() { root.finish(); } function onCurrentDesktopChanged() { root.finish(); } function onCurrentActivityChanged() { root.finish(); } } Connections { target: root.target function onClientGeometryChanged() { root.finish(); } function onClosed() { root.finish(); } function onMinimizedChanged() { root.finish(); } } DBusCall { id: next service: root.bus path: "/com/t3tools/KdeCapture/Feedback" dbusInterface: "com.t3tools.KdeCapture.Feedback" method: "Next" onFailed: root.finish() onFinished: (values) => { if (root.closing) return; try { const command = JSON.parse(values[0]); if (command.command === "close") { root.finish(); return; } if (command.command !== "animate" || root.flying || !root.options.animate) throw new Error("Invalid capture feedback command"); const destination = Geometry.destination(Workspace.windows, root.options.pid, command.title, command.frame); root.target = destination.window; const frame = destination.frame; const distance = Math.hypot(frame.x - root.frame.x, frame.y - root.frame.y); flight.duration = Math.round((680 - 400 * Math.exp(-distance / 2000)) * root.motionFactor); flight.to = Qt.rect(frame.x, frame.y, frame.width, frame.height); root.flying = true; flight.start(); // Keep a pending call during the animation for cancellation and owner loss. next.call(); } catch (error) { root.finish(); } } } DBusCall { id: ready service: root.bus path: "/com/t3tools/KdeCapture/Feedback" dbusInterface: "com.t3tools.KdeCapture.Feedback" method: "Event" arguments: [JSON.stringify({event: "ready", animate: root.options.animate})] onFailed: root.finish() } DBusCall { id: landed service: root.bus path: "/com/t3tools/KdeCapture/Feedback" dbusInterface: "com.t3tools.KdeCapture.Feedback" method: "Event" arguments: ['{"event":"landed"}'] onFailed: root.finish() } DBusCall { id: done service: root.bus path: "/com/t3tools/KdeCapture/Feedback" dbusInterface: "com.t3tools.KdeCapture.Feedback" method: "Event" arguments: ['{"event":"done"}'] onFinished: unload.call() onFailed: unload.call() } // Also unload after helper loss; a hidden script would otherwise retain the screenshot. DBusCall { id: unload service: "org.kde.KWin" path: "/Scripting" dbusInterface: "org.kde.kwin.Scripting" method: "unloadScript" arguments: [root.scriptName] }}
import QtQuick
import QtQuick.Window
import QtQml.Models
import org.kde.kwin
import "feedbackGeometry.js" as Geometry
// Internal output-only windows do not change, activate, or intercept input to the real window.
Item {
id: root
property var options: /*OPTIONS*/ null
property string bus: /*BUS*/ ""
property string scriptName: /*NAME*/ ""
property real motionFactor: /*FACTOR*/ 1
property rect frame: Qt.rect(options.bounds.x, options.bounds.y, options.bounds.width, options.bounds.height)
property bool showing: false
property bool started: false
property bool closing: false
property bool flying: false
property bool initialized: false
property bool readySent: false
property real flashOpacity: 0
property var target: null
property url screenshot: Qt.resolvedUrl("capture.png")
function finish() {
if (closing) return;
closing = true;
showing = false;
flight.stop();
deadline.stop();
done.call();
}
function begin() {
if (started || closing) return;
started = true;
showing = true;
if (options.flash) flash.start();
next.call();
}
// A cached, bounded PNG is shared by each output. Nothing repaints while waiting for T3.
Image {
id: snapshot
visible: false
source: root.screenshot
sourceSize.width: 2560
sourceSize.height: 1600
onStatusChanged: {
if (status === Image.Ready && root.initialized) root.begin();
else if (status === Image.Error) root.finish();
}
}
Component.onCompleted: {
initialized = true;
if (snapshot.status === Image.Ready) begin();
else if (snapshot.status === Image.Error) finish();
}
Instantiator {
model: Workspace.screens
delegate: Window {
required property var modelData
x: modelData.geometry.x
y: modelData.geometry.y
width: modelData.geometry.width
height: modelData.geometry.height
color: "transparent"
visible: root.showing
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.BypassWindowManagerHint | Qt.WindowStaysOnTopHint | Qt.WindowTransparentForInput | Qt.WindowDoesNotAcceptFocus
property bool outputOnly: true
property bool kwin_shadow_enabled: false
Connections {
target: modelData
function onGeometryChanged() { root.finish(); }
}
onFrameSwapped: {
if (root.showing && !root.readySent) {
root.readySent = true;
ready.call();
}
}
Item {
x: root.frame.x - modelData.geometry.x
y: root.frame.y - modelData.geometry.y
width: root.frame.width
height: root.frame.height
Image {
anchors.fill: parent
visible: root.options.animate
source: snapshot.source
sourceSize: snapshot.sourceSize
fillMode: Image.Stretch
}
}
Rectangle {
x: root.options.bounds.x - modelData.geometry.x
y: root.options.bounds.y - modelData.geometry.y
width: root.options.bounds.width
height: root.options.bounds.height
color: "white"
opacity: root.flashOpacity
}
}
}
NumberAnimation {
id: flash
target: root
property: "flashOpacity"
from: 0.10
to: 0
duration: 180
easing.type: Easing.OutQuad
}
PropertyAnimation {
id: flight
target: root
property: "frame"
easing.type: Easing.OutCubic
onFinished: landed.call()
}
Timer { id: deadline; interval: 6000; running: true; onTriggered: root.finish() }
Connections {
target: Workspace
function onScreensChanged() { root.finish(); }
function onVirtualScreenGeometryChanged() { root.finish(); }
function onCurrentDesktopChanged() { root.finish(); }
function onCurrentActivityChanged() { root.finish(); }
}
Connections {
target: root.target
function onClientGeometryChanged() { root.finish(); }
function onClosed() { root.finish(); }
function onMinimizedChanged() { root.finish(); }
}
DBusCall {
id: next
service: root.bus
path: "/com/t3tools/KdeCapture/Feedback"
dbusInterface: "com.t3tools.KdeCapture.Feedback"
method: "Next"
onFailed: root.finish()
onFinished: (values) => {
if (root.closing) return;
try {
const command = JSON.parse(values[0]);
if (command.command === "close") { root.finish(); return; }
if (command.command !== "animate" || root.flying || !root.options.animate)
throw new Error("Invalid capture feedback command");
const destination = Geometry.destination(Workspace.windows, root.options.pid, command.title, command.frame);
root.target = destination.window;
const frame = destination.frame;
const distance = Math.hypot(frame.x - root.frame.x, frame.y - root.frame.y);
flight.duration = Math.round((680 - 400 * Math.exp(-distance / 2000)) * root.motionFactor);
flight.to = Qt.rect(frame.x, frame.y, frame.width, frame.height);
root.flying = true;
flight.start();
// Keep a pending call during the animation for cancellation and owner loss.
next.call();
} catch (error) { root.finish(); }
}
}
DBusCall {
id: ready
service: root.bus
path: "/com/t3tools/KdeCapture/Feedback"
dbusInterface: "com.t3tools.KdeCapture.Feedback"
method: "Event"
arguments: [JSON.stringify({event: "ready", animate: root.options.animate})]
onFailed: root.finish()
}
DBusCall {
id: landed
service: root.bus
path: "/com/t3tools/KdeCapture/Feedback"
dbusInterface: "com.t3tools.KdeCapture.Feedback"
method: "Event"
arguments: ['{"event":"landed"}']
onFailed: root.finish()
}
DBusCall {
id: done
service: root.bus
path: "/com/t3tools/KdeCapture/Feedback"
dbusInterface: "com.t3tools.KdeCapture.Feedback"
method: "Event"
arguments: ['{"event":"done"}']
onFinished: unload.call()
onFailed: unload.call()
}
// Also unload after helper loss; a hidden script would otherwise retain the screenshot.
DBusCall {
id: unload
service: "org.kde.KWin"
path: "/Scripting"
dbusInterface: "org.kde.kwin.Scripting"
method: "unloadScript"
arguments: [root.scriptName]
}
}