screenshot tools baked-in: drawing uses optimized qcanvaspainter, direct to clipboard, eliminate forking to swappy
C++ / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 21s
JS/TS / fmt (pull_request) Successful in 20s
Python / lint (pull_request) Successful in 31s
Python / fmt (pull_request) Successful in 33s
Python / typecheck (pull_request) Failing after 1m4s
Python / test (pull_request) Successful in 1m7s
C++ / build (pull_request) Successful in 1m48s
Rust / fmt (pull_request) Successful in 37s
Rust / build (pull_request) Successful in 1m42s
Python / buildcheck (pull_request) Successful in 2m33s
Rust / clippy (pull_request) Successful in 1m32s
C++ / clang-tidy (pull_request) Successful in 3m35s

This commit is contained in:
2026-08-10 12:02:34 +02:00
parent 93c6ac251e
commit 47c842941f
9 changed files with 711 additions and 71 deletions
Regular → Executable
+153 -63
View File
@@ -7,9 +7,9 @@ import QtQuick
import QtQuick.Effects
import qs.Components
import qs.Config
import qs.Helpers
import qs.Helpers.Picker
MouseArea {
Item {
id: root
property list<var> clients: {
@@ -29,9 +29,11 @@ MouseArea {
readonly property int cornerRadius: Hypr.options.decoration.rounding
property real ex: screen.width
property real ey: screen.height
property color inkColor: "#ff3b30"
required property LazyLoader loader
property string mode: "select"
property bool onClient
property real realBorderWidth: onClient ? (Hypr.options.general.border_size ?? 1) : 2
property real realBorderWidth: 2
property real realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0
property real rsx: Math.min(sx, ex)
property real rsy: Math.min(sy, ey)
@@ -48,6 +50,12 @@ MouseArea {
property real sw: Math.abs(sx - ex)
property real sx: 0
property real sy: 0
property string tool: "move"
function cancel(): void {
annotations.clearAll();
closeAnim.start();
}
function checkClientRects(x: real, y: real): void {
for (const client of clients) {
@@ -71,24 +79,48 @@ MouseArea {
}
}
function save(): void {
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
function enterEditMode(): void {
mode = "edit";
tool = "move";
}
function save(copyToClipboard: bool): void {
const rounding = root.cornerRadius > 0;
const shadow_blur = root.shadowRange / root.shadowRenderPower;
const r = Math.floor(root.shadowColor.r * 256);
const g = Math.floor(root.shadowColor.g * 256);
const b = Math.floor(root.shadowColor.b * 256);
const a = Math.floor(root.shadowColor.a * 256);
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : [];
const cmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : ["swappy", "-f"];
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached([...cmd, path]));
const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
overlay.visible = border.visible = false;
handles.visible = false;
toolbar.visible = false;
ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
const finalize = finalPath => {
if (copyToClipboard) {
Quickshell.execDetached(["sh", "-c", `wl-copy --type image/png < '${finalPath.replace(/'/g, `'\\''`)}'`]);
} else {
const dest = `${Quickshell.env("HOME")}/Pictures/Screenshots/zshell-${Date.now()}.png`;
Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname '${dest}')" && cp '${path.replace(/'/g, `'\\''`)}' '${dest}'`]);
}
};
if (postCmd) {
Quickshell.execDetached(postCmd.concat([path]));
finalize(path);
} else {
finalize(path);
}
});
closeAnim.start();
}
anchors.fill: parent
cursorShape: Qt.CrossCursor
focus: true
hoverEnabled: true
opacity: 0
Behavior on opacity {
@@ -97,25 +129,25 @@ MouseArea {
}
}
Behavior on rsx {
enabled: !root.pressed
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on rsy {
enabled: !root.pressed
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on sh {
enabled: !root.pressed
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on sw {
enabled: !root.pressed
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
@@ -123,8 +155,7 @@ MouseArea {
Component.onCompleted: {
Hypr.extras.refreshOptions();
if (loader.freeze)
clients = clients;
screencopy.active = true;
opacity = 1;
@@ -144,48 +175,40 @@ MouseArea {
ey = screen.height / 2 + 100;
}
}
Keys.onEscapePressed: closeAnim.start()
onClientsChanged: checkClientRects(mouseX, mouseY)
onPositionChanged: event => {
const x = event.x;
const y = event.y;
Keys.onEscapePressed: root.mode === "edit" ? cancel() : closeAnim.start()
onClientsChanged: {
if (root.mode === "select")
checkClientRects(selectArea.mouseX, selectArea.mouseY);
}
if (pressed) {
onClient = false;
sx = ssx;
sy = ssy;
ex = x;
ey = y;
} else if (!saveTimer.running) {
checkClientRects(x, y);
MouseArea {
id: selectArea
anchors.fill: parent
cursorShape: Qt.CrossCursor
enabled: root.mode === "select"
hoverEnabled: true
visible: root.mode === "select"
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (pressed) {
root.onClient = false;
root.sx = root.ssx;
root.sy = root.ssy;
root.ex = x;
root.ey = y;
} else {
root.checkClientRects(x, y);
}
}
}
onPressed: event => {
ssx = event.x;
ssy = event.y;
}
onReleased: {
if (closeAnim.running)
return;
if (root.loader.freeze) {
saveTimer.start();
} else {
overlay.visible = border.visible = false;
screencopy.visible = false;
screencopy.active = true;
saveTimer.start();
onPressed: event => {
root.ssx = event.x;
root.ssy = event.y;
}
}
Timer {
id: saveTimer
interval: 25
repeat: false
running: false
onTriggered: root.save()
onReleased: root.enterEditMode()
}
SequentialAnimation {
@@ -231,16 +254,38 @@ MouseArea {
}
}
Loader {
id: screencopy
Item {
id: captureLayer
active: root.loader.freeze
anchors.fill: parent
asynchronous: true
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
Loader {
id: screencopy
active: false
anchors.fill: parent
asynchronous: true
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
}
}
AnnotationCanvas {
id: annotations
anchors.fill: parent
inkColor: root.inkColor
layer.enabled: true
tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none"
layer.effect: MultiEffect {
maskEnabled: true
maskSource: selectionWrapper
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
}
}
@@ -252,7 +297,7 @@ MouseArea {
layer.enabled: true
opacity: 0.3
radius: root.realRounding
visible: screencopy.item.hasContent || !root.loader.freeze
visible: screencopy.item?.hasContent ?? false
layer.effect: MultiEffect {
maskEnabled: true
@@ -290,7 +335,7 @@ MouseArea {
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: screencopy.item.hasContent || !root.loader.freeze
visible: screencopy.item?.hasContent ?? false
x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth
@@ -300,6 +345,51 @@ MouseArea {
}
}
SelectionHandles {
id: handles
selH: root.sh
selW: root.sw
selX: root.rsx
selY: root.rsy
visible: root.mode === "edit" && root.tool === "move"
onBoundsChanged: (x, y, w, h) => {
root.sx = x;
root.sy = y;
root.ex = x + w;
root.ey = y + h;
}
onMoveBy: (dx, dy) => {
const w = root.sw, h = root.sh;
let nx = root.rsx + dx, ny = root.rsy + dy;
nx = Math.max(0, Math.min(nx, root.screen.width - w));
ny = Math.max(0, Math.min(ny, root.screen.height - h));
root.sx = nx;
root.sy = ny;
root.ex = nx + w;
root.ey = ny + h;
}
}
AnnotationToolbar {
id: toolbar
inkColor: root.inkColor
tool: root.tool
visible: root.mode === "edit"
x: Math.max(8, Math.min(root.rsx + root.sw / 2 - width / 2, root.screen.width - width - 8))
y: (root.rsy + root.sh + height + Appearance.padding.normal <= root.screen.height) ? root.rsy + root.sh + Appearance.padding.normal : (root.rsy - height - Appearance.padding.normal <= 0) ? root.rsy + root.sh - height - Appearance.padding.normal : root.rsy - height - Appearance.padding.normal
onCancelRequested: root.cancel()
onClearRequested: annotations.clearAll()
onColorSelected: c => root.inkColor = c
onCopyRequested: root.save(true)
onSaveRequested: root.save(false)
onToolSelected: t => root.tool = t
onUndoRequested: annotations.undo()
}
component ExAnim: Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects