C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 20s
JS/TS / lint (pull_request) Successful in 22s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m2s
C++ / build (pull_request) Successful in 2m10s
Rust / build (pull_request) Successful in 1m50s
Rust / clippy (pull_request) Successful in 1m25s
Python / verify (pull_request) Successful in 2m58s
C++ / clang-tidy (pull_request) Successful in 7m8s
408 lines
9.5 KiB
QML
Executable File
408 lines
9.5 KiB
QML
Executable File
pragma ComponentBehavior: Bound
|
|
|
|
import ZShell
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Effects
|
|
import qs.Components
|
|
import ZShell.Config
|
|
import qs.Helpers.Picker
|
|
import qs.Paths
|
|
import qs.Services
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property list<var> clients: {
|
|
const mon = Hypr.monitorFor(screen);
|
|
if (!mon)
|
|
return [];
|
|
|
|
const special = mon.lastIpcObject.specialWorkspace;
|
|
const wsId = special.name ? special.id : mon.activeWorkspace.id;
|
|
|
|
return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => {
|
|
const ac = a.lastIpcObject;
|
|
const bc = b.lastIpcObject;
|
|
return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating);
|
|
});
|
|
}
|
|
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: 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)
|
|
readonly property real scaleRatio: Hypr.monitorFor(screen).scale
|
|
required property ShellScreen screen
|
|
property real sh: Math.abs(sy - ey)
|
|
readonly property color shadowColor: Hypr.options.decoration.shadow.color
|
|
readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled
|
|
readonly property var shadowOffset: Hypr.options.decoration.shadow.offset
|
|
readonly property int shadowRange: Hypr.options.decoration.shadow.range
|
|
readonly property int shadowRenderPower: Hypr.options.decoration.shadow.render_power
|
|
property real ssx
|
|
property real ssy
|
|
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) {
|
|
if (!client)
|
|
continue;
|
|
|
|
let {
|
|
at: [cx, cy],
|
|
size: [cw, ch]
|
|
} = client.lastIpcObject;
|
|
cx -= screen.x;
|
|
cy -= screen.y;
|
|
if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) {
|
|
onClient = true;
|
|
sx = cx;
|
|
sy = cy;
|
|
ex = cx + cw;
|
|
ey = cy + ch;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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, `'\\''`)}'`]);
|
|
}
|
|
if (!copyToClipboard || Config.screenshot.saveOnCopy) {
|
|
const dest = `${Paths.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
|
|
focus: true
|
|
opacity: 0
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
duration: 300
|
|
}
|
|
}
|
|
Behavior on rsx {
|
|
enabled: !selectArea.pressed && root.mode === "select"
|
|
|
|
ExAnim {
|
|
}
|
|
}
|
|
Behavior on rsy {
|
|
enabled: !selectArea.pressed && root.mode === "select"
|
|
|
|
ExAnim {
|
|
}
|
|
}
|
|
Behavior on sh {
|
|
enabled: !selectArea.pressed && root.mode === "select"
|
|
|
|
ExAnim {
|
|
}
|
|
}
|
|
Behavior on sw {
|
|
enabled: !selectArea.pressed && root.mode === "select"
|
|
|
|
ExAnim {
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
Hypr.extras.refreshOptions();
|
|
screencopy.active = true;
|
|
|
|
opacity = 1;
|
|
|
|
const c = clients[0];
|
|
if (c) {
|
|
const cx = c.lastIpcObject.at[0] - screen.x;
|
|
const cy = c.lastIpcObject.at[1] - screen.y;
|
|
onClient = true;
|
|
sx = cx;
|
|
sy = cy;
|
|
ex = cx + c.lastIpcObject.size[0];
|
|
ey = cy + c.lastIpcObject.size[1];
|
|
} else {
|
|
sx = screen.width / 2 - 100;
|
|
sy = screen.height / 2 - 100;
|
|
ex = screen.width / 2 + 100;
|
|
ey = screen.height / 2 + 100;
|
|
}
|
|
}
|
|
Keys.onEscapePressed: root.mode === "edit" ? cancel() : closeAnim.start()
|
|
Keys.onPressed: e => {
|
|
if ((e.key == Qt.Key_C) && (e.modifiers & Qt.ControlModifier)) {
|
|
save(true);
|
|
e.accepted = true;
|
|
} else if ((e.key == Qt.Key_S) && (e.modifiers & Qt.ControlModifier)) {
|
|
save(false);
|
|
e.accepted = true;
|
|
}
|
|
}
|
|
onClientsChanged: {
|
|
if (root.mode === "select")
|
|
checkClientRects(selectArea.mouseX, selectArea.mouseY);
|
|
}
|
|
|
|
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 => {
|
|
root.ssx = event.x;
|
|
root.ssy = event.y;
|
|
}
|
|
onReleased: root.enterEditMode()
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: closeAnim
|
|
|
|
PropertyAction {
|
|
property: "closing"
|
|
target: root.loader
|
|
value: true
|
|
}
|
|
|
|
ParallelAnimation {
|
|
Anim {
|
|
duration: 300
|
|
property: "opacity"
|
|
target: root
|
|
to: 0
|
|
}
|
|
|
|
ExAnim {
|
|
properties: "rsx,rsy"
|
|
target: root
|
|
to: 0
|
|
}
|
|
|
|
ExAnim {
|
|
property: "sw"
|
|
target: root
|
|
to: root.screen.width
|
|
}
|
|
|
|
ExAnim {
|
|
property: "sh"
|
|
target: root
|
|
to: root.screen.height
|
|
}
|
|
}
|
|
|
|
PropertyAction {
|
|
property: "activeAsync"
|
|
target: root.loader
|
|
value: false
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: captureLayer
|
|
|
|
anchors.fill: parent
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: overlay
|
|
|
|
anchors.fill: parent
|
|
color: "white"
|
|
layer.enabled: true
|
|
opacity: 0.3
|
|
radius: root.realRounding
|
|
visible: screencopy.item?.hasContent ?? false
|
|
|
|
layer.effect: MultiEffect {
|
|
maskEnabled: true
|
|
maskInverted: true
|
|
maskSource: selectionWrapper
|
|
maskSpreadAtMin: 1
|
|
maskThresholdMin: 0.5
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: selectionWrapper
|
|
|
|
anchors.fill: parent
|
|
layer.enabled: true
|
|
visible: false
|
|
|
|
Rectangle {
|
|
id: selectionRect
|
|
|
|
implicitHeight: root.sh
|
|
implicitWidth: root.sw
|
|
radius: root.realRounding
|
|
x: root.rsx
|
|
y: root.rsy
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: border
|
|
|
|
border.color: Colors.palette.m3primary
|
|
border.width: root.realBorderWidth
|
|
color: "transparent"
|
|
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 ?? false
|
|
x: selectionRect.x - root.realBorderWidth
|
|
y: selectionRect.y - root.realBorderWidth
|
|
|
|
Behavior on border.color {
|
|
Anim {
|
|
}
|
|
}
|
|
}
|
|
|
|
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 + Tokens.padding.small <= root.screen.height) ? root.rsy + root.sh + Tokens.padding.small : (root.rsy - height - Tokens.padding.small <= 0) ? root.rsy + root.sh - height - Tokens.padding.small : root.rsy - height - Tokens.padding.small
|
|
|
|
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 {
|
|
}
|
|
}
|