Merge pull request 'Screenshot tools features' (#141) from screenshot-picker-improvements into main
Reviewed-on: #141 Reviewed-by: Inorishio <inorishio@gmail.com>
This commit was merged in pull request #141.
This commit is contained in:
@@ -35,7 +35,7 @@ CustomRect {
|
|||||||
readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor
|
readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor
|
||||||
property real padding
|
property real padding
|
||||||
readonly property alias pressed: stateLayer.pressed
|
readonly property alias pressed: stateLayer.pressed
|
||||||
property real pressedRadius: Appearance.rounding.small
|
property real pressedRadius: Appearance.rounding.smallest
|
||||||
readonly property alias radiusAnim: radiusAnim
|
readonly property alias radiusAnim: radiusAnim
|
||||||
property bool radiusMorph: true
|
property bool radiusMorph: true
|
||||||
property alias shapeMorph: stateLayer.shapeMorph
|
property alias shapeMorph: stateLayer.shapeMorph
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ JsonObject {
|
|||||||
property string mode: "manual"
|
property string mode: "manual"
|
||||||
property real radius: 12.0
|
property real radius: 12.0
|
||||||
property bool rounding: false
|
property bool rounding: false
|
||||||
|
property bool saveOnCopy: false
|
||||||
property bool shadow: true
|
property bool shadow: true
|
||||||
property real shadow_blur: 22.0
|
property real shadow_blur: 22.0
|
||||||
property list<int> shadow_color: [0, 0, 0, 160]
|
property list<int> shadow_color: [0, 0, 0, 160]
|
||||||
|
|||||||
Regular → Executable
+162
-61
@@ -7,9 +7,10 @@ import QtQuick
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Helpers
|
import qs.Helpers.Picker
|
||||||
|
import qs.Paths
|
||||||
|
|
||||||
MouseArea {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property list<var> clients: {
|
property list<var> clients: {
|
||||||
@@ -29,9 +30,11 @@ MouseArea {
|
|||||||
readonly property int cornerRadius: Hypr.options.decoration.rounding
|
readonly property int cornerRadius: Hypr.options.decoration.rounding
|
||||||
property real ex: screen.width
|
property real ex: screen.width
|
||||||
property real ey: screen.height
|
property real ey: screen.height
|
||||||
|
property color inkColor: "#ff3b30"
|
||||||
required property LazyLoader loader
|
required property LazyLoader loader
|
||||||
|
property string mode: "select"
|
||||||
property bool onClient
|
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 realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0
|
||||||
property real rsx: Math.min(sx, ex)
|
property real rsx: Math.min(sx, ex)
|
||||||
property real rsy: Math.min(sy, ey)
|
property real rsy: Math.min(sy, ey)
|
||||||
@@ -48,6 +51,12 @@ MouseArea {
|
|||||||
property real sw: Math.abs(sx - ex)
|
property real sw: Math.abs(sx - ex)
|
||||||
property real sx: 0
|
property real sx: 0
|
||||||
property real sy: 0
|
property real sy: 0
|
||||||
|
property string tool: "move"
|
||||||
|
|
||||||
|
function cancel(): void {
|
||||||
|
annotations.clearAll();
|
||||||
|
closeAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
function checkClientRects(x: real, y: real): void {
|
function checkClientRects(x: real, y: real): void {
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
@@ -71,24 +80,49 @@ MouseArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(): void {
|
function enterEditMode(): void {
|
||||||
const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`);
|
mode = "edit";
|
||||||
|
tool = "move";
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(copyToClipboard: bool): void {
|
||||||
const rounding = root.cornerRadius > 0;
|
const rounding = root.cornerRadius > 0;
|
||||||
const shadow_blur = root.shadowRange / root.shadowRenderPower;
|
const shadow_blur = root.shadowRange / root.shadowRenderPower;
|
||||||
const r = Math.floor(root.shadowColor.r * 256);
|
const r = Math.floor(root.shadowColor.r * 256);
|
||||||
const g = Math.floor(root.shadowColor.g * 256);
|
const g = Math.floor(root.shadowColor.g * 256);
|
||||||
const b = Math.floor(root.shadowColor.b * 256);
|
const b = Math.floor(root.shadowColor.b * 256);
|
||||||
const a = Math.floor(root.shadowColor.a * 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 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"];
|
const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
|
||||||
ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached([...cmd, path]));
|
|
||||||
|
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();
|
closeAnim.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.CrossCursor
|
|
||||||
focus: true
|
focus: true
|
||||||
hoverEnabled: true
|
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -97,25 +131,25 @@ MouseArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on rsx {
|
Behavior on rsx {
|
||||||
enabled: !root.pressed
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on rsy {
|
Behavior on rsy {
|
||||||
enabled: !root.pressed
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on sh {
|
Behavior on sh {
|
||||||
enabled: !root.pressed
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on sw {
|
Behavior on sw {
|
||||||
enabled: !root.pressed
|
enabled: !selectArea.pressed && root.mode === "select"
|
||||||
|
|
||||||
ExAnim {
|
ExAnim {
|
||||||
}
|
}
|
||||||
@@ -123,8 +157,7 @@ MouseArea {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Hypr.extras.refreshOptions();
|
Hypr.extras.refreshOptions();
|
||||||
if (loader.freeze)
|
screencopy.active = true;
|
||||||
clients = clients;
|
|
||||||
|
|
||||||
opacity = 1;
|
opacity = 1;
|
||||||
|
|
||||||
@@ -144,48 +177,49 @@ MouseArea {
|
|||||||
ey = screen.height / 2 + 100;
|
ey = screen.height / 2 + 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Keys.onEscapePressed: closeAnim.start()
|
Keys.onEscapePressed: root.mode === "edit" ? cancel() : closeAnim.start()
|
||||||
onClientsChanged: checkClientRects(mouseX, mouseY)
|
Keys.onPressed: e => {
|
||||||
onPositionChanged: event => {
|
if ((e.key == Qt.Key_C) && (e.modifiers & Qt.ControlModifier)) {
|
||||||
const x = event.x;
|
save(true);
|
||||||
const y = event.y;
|
e.accepted = true;
|
||||||
|
} else if ((e.key == Qt.Key_S) && (e.modifiers & Qt.ControlModifier)) {
|
||||||
if (pressed) {
|
save(false);
|
||||||
onClient = false;
|
e.accepted = true;
|
||||||
sx = ssx;
|
|
||||||
sy = ssy;
|
|
||||||
ex = x;
|
|
||||||
ey = y;
|
|
||||||
} else if (!saveTimer.running) {
|
|
||||||
checkClientRects(x, y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onPressed: event => {
|
onClientsChanged: {
|
||||||
ssx = event.x;
|
if (root.mode === "select")
|
||||||
ssy = event.y;
|
checkClientRects(selectArea.mouseX, selectArea.mouseY);
|
||||||
}
|
}
|
||||||
onReleased: {
|
|
||||||
if (closeAnim.running)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (root.loader.freeze) {
|
MouseArea {
|
||||||
saveTimer.start();
|
id: selectArea
|
||||||
} else {
|
|
||||||
overlay.visible = border.visible = false;
|
anchors.fill: parent
|
||||||
screencopy.visible = false;
|
cursorShape: Qt.CrossCursor
|
||||||
screencopy.active = true;
|
enabled: root.mode === "select"
|
||||||
saveTimer.start();
|
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;
|
||||||
Timer {
|
root.ssy = event.y;
|
||||||
id: saveTimer
|
}
|
||||||
|
onReleased: root.enterEditMode()
|
||||||
interval: 25
|
|
||||||
repeat: false
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onTriggered: root.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
@@ -231,16 +265,38 @@ MouseArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Item {
|
||||||
id: screencopy
|
id: captureLayer
|
||||||
|
|
||||||
active: root.loader.freeze
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: ScreencopyView {
|
Loader {
|
||||||
captureSource: root.screen
|
id: screencopy
|
||||||
paintCursor: false
|
|
||||||
|
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 +308,7 @@ MouseArea {
|
|||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
opacity: 0.3
|
opacity: 0.3
|
||||||
radius: root.realRounding
|
radius: root.realRounding
|
||||||
visible: screencopy.item.hasContent || !root.loader.freeze
|
visible: screencopy.item?.hasContent ?? false
|
||||||
|
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
maskEnabled: true
|
maskEnabled: true
|
||||||
@@ -290,7 +346,7 @@ MouseArea {
|
|||||||
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
|
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
|
||||||
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
|
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
|
||||||
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
|
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
|
x: selectionRect.x - root.realBorderWidth
|
||||||
y: selectionRect.y - root.realBorderWidth
|
y: selectionRect.y - root.realBorderWidth
|
||||||
|
|
||||||
@@ -300,6 +356,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 {
|
component ExAnim: Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
duration: MaterialEasing.expressiveEffectsTime
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Internal
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property color inkColor: "#ff3b30"
|
||||||
|
property real inkWidth: 4
|
||||||
|
property string tool: "none"
|
||||||
|
|
||||||
|
signal changed
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
committedCanvas.clear();
|
||||||
|
previewCanvas.clear();
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sampleShape(tool, x1, y1, x2, y2) {
|
||||||
|
const pts = [];
|
||||||
|
switch (tool) {
|
||||||
|
case "line":
|
||||||
|
pts.push([x1, y1], [x2, y2]);
|
||||||
|
break;
|
||||||
|
case "arrow":
|
||||||
|
{
|
||||||
|
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||||
|
const headLen = 14 + root.inkWidth * 2;
|
||||||
|
pts.push([x1, y1], [x2, y2]);
|
||||||
|
pts.push([x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6)]);
|
||||||
|
pts.push([x2, y2]);
|
||||||
|
pts.push([x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6)]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "rect":
|
||||||
|
pts.push([x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]);
|
||||||
|
break;
|
||||||
|
case "ellipse":
|
||||||
|
{
|
||||||
|
const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
|
||||||
|
const rx = Math.abs(x2 - x1) / 2, ry = Math.abs(y2 - y1) / 2;
|
||||||
|
const perimeter = Math.PI * (3 * (rx + ry) - Math.sqrt((3 * rx + ry) * (rx + 3 * ry)));
|
||||||
|
const steps = Math.max(48, Math.min(256, Math.ceil(perimeter / 4)));
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const a = (i / steps) * Math.PI * 2;
|
||||||
|
pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
committedCanvas.undoLastGroup();
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
StrokeCanvas {
|
||||||
|
id: committedCanvas
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
penColor: root.inkColor
|
||||||
|
penWidth: root.inkWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
StrokeCanvas {
|
||||||
|
id: previewCanvas
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
penColor: root.inkColor
|
||||||
|
penWidth: root.inkWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
PointHandler {
|
||||||
|
id: drawHandler
|
||||||
|
|
||||||
|
property real startX
|
||||||
|
property real startY
|
||||||
|
property bool started: false
|
||||||
|
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
enabled: root.tool !== "none"
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (root.tool === "pen") {
|
||||||
|
committedCanvas.endStroke();
|
||||||
|
} else if (started) {
|
||||||
|
const pts = root.sampleShape(root.tool, startX, startY, point.position.x, point.position.y);
|
||||||
|
if (pts.length > 0) {
|
||||||
|
committedCanvas.beginStroke(pts[0][0], pts[0][1], true);
|
||||||
|
for (let i = 1; i < pts.length; i++)
|
||||||
|
committedCanvas.appendPoint(pts[i][0], pts[i][1]);
|
||||||
|
committedCanvas.endStroke();
|
||||||
|
}
|
||||||
|
previewCanvas.clear();
|
||||||
|
}
|
||||||
|
started = false;
|
||||||
|
root.changed();
|
||||||
|
}
|
||||||
|
onPointChanged: {
|
||||||
|
if (!active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (point.pressedButtons & Qt.RightButton) {
|
||||||
|
root.clearAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = point.position.x;
|
||||||
|
const y = point.position.y;
|
||||||
|
|
||||||
|
if (root.tool === "pen") {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
committedCanvas.beginStroke(x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
committedCanvas.appendPoint(x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
startX = point.pressPosition.x;
|
||||||
|
startY = point.pressPosition.y;
|
||||||
|
}
|
||||||
|
const pts = root.sampleShape(root.tool, startX, startY, x, y);
|
||||||
|
if (pts.length > 0) {
|
||||||
|
previewCanvas.beginStroke(pts[0][0], pts[0][1], true);
|
||||||
|
for (let i = 1; i < pts.length; i++)
|
||||||
|
previewCanvas.appendPoint(pts[i][0], pts[i][1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+184
@@ -0,0 +1,184 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import ZShell.Components
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Components
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var colorPalette: ["#ff3b30", "#ff9500", "#ffcc00", "#34c759", "#0a84ff", "#af52de", "#ffffff", "#000000"]
|
||||||
|
property color inkColor: "#ff3b30"
|
||||||
|
property string tool: "move"
|
||||||
|
readonly property var toolList: [
|
||||||
|
{
|
||||||
|
id: "move",
|
||||||
|
glyph: "arrows_output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pen",
|
||||||
|
glyph: "draw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "line",
|
||||||
|
glyph: "diagonal_line"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "arrow",
|
||||||
|
glyph: "arrow_outward"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rect",
|
||||||
|
glyph: "rectangle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ellipse",
|
||||||
|
glyph: "circle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
signal cancelRequested
|
||||||
|
signal clearRequested
|
||||||
|
signal colorSelected(color c)
|
||||||
|
signal copyRequested
|
||||||
|
signal saveRequested
|
||||||
|
signal toolSelected(string tool)
|
||||||
|
signal undoRequested
|
||||||
|
|
||||||
|
color: DynamicColors.palette.m3surface
|
||||||
|
height: row.implicitHeight + Appearance.padding.normal * 2
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
width: row.implicitWidth + Appearance.padding.normal * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
ButtonRow {
|
||||||
|
id: toolRow
|
||||||
|
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.toolList
|
||||||
|
|
||||||
|
delegate: IconButton {
|
||||||
|
id: tool
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
color: root.tool === modelData.id ? "#3584e4" : "transparent"
|
||||||
|
icon: tool.modelData.glyph
|
||||||
|
inactiveColor: root.tool === modelData.id ? DynamicColors.palette.m3primary : "transparent"
|
||||||
|
inactiveOnColor: root.tool === modelData.id ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: shapeMorph && pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.toolSelected(tool.modelData.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
color: DynamicColors.palette.m3outlineVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonRow {
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.colorPalette
|
||||||
|
|
||||||
|
delegate: IconButton {
|
||||||
|
readonly property real buttonSize: toolRow.implicitHeight - Appearance.padding.normal
|
||||||
|
required property color modelData
|
||||||
|
|
||||||
|
fillWidth: false
|
||||||
|
font.pointSize: 0
|
||||||
|
icon: ""
|
||||||
|
implicitHeight: buttonSize
|
||||||
|
implicitWidth: buttonSize
|
||||||
|
inactiveColor: modelData
|
||||||
|
inactiveOnColor: DynamicColors.on(modelData)
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.colorSelected(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
color: DynamicColors.palette.m3outlineVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonRow {
|
||||||
|
spacing: Appearance.spacing.extraSmall
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "undo"
|
||||||
|
inactiveColor: "transparent"
|
||||||
|
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.undoRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "delete_history"
|
||||||
|
inactiveColor: "transparent"
|
||||||
|
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.clearRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "content_copy"
|
||||||
|
inactiveColor: "transparent"
|
||||||
|
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.copyRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "delete_forever"
|
||||||
|
inactiveColor: "transparent"
|
||||||
|
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.cancelRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "check"
|
||||||
|
inactiveColor: "transparent"
|
||||||
|
inactiveOnColor: DynamicColors.palette.m3onSurface
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
shapeMorphExpansion: pressed ? 12 : 0
|
||||||
|
|
||||||
|
onClicked: root.saveRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+183
@@ -0,0 +1,183 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property color handleBorder: "#3584e4"
|
||||||
|
property color handleFill: "white"
|
||||||
|
property real hs: 12
|
||||||
|
property real minSize: 24
|
||||||
|
required property real selH
|
||||||
|
required property real selW
|
||||||
|
required property real selX
|
||||||
|
required property real selY
|
||||||
|
|
||||||
|
signal boundsChanged(x: real, y: real, w: real, h: real)
|
||||||
|
signal dragFinished
|
||||||
|
signal moveBy(dx: real, dy: real)
|
||||||
|
|
||||||
|
height: selH
|
||||||
|
width: selW
|
||||||
|
x: selX
|
||||||
|
y: selY
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: moveHandler
|
||||||
|
|
||||||
|
property real accumX: 0
|
||||||
|
property real accumY: 0
|
||||||
|
|
||||||
|
target: null
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
accumX = 0;
|
||||||
|
accumY = 0;
|
||||||
|
if (!active)
|
||||||
|
root.dragFinished();
|
||||||
|
}
|
||||||
|
onTranslationChanged: {
|
||||||
|
const t = moveHandler.translation;
|
||||||
|
root.moveBy(t.x - accumX, t.y - accumY);
|
||||||
|
accumX = t.x;
|
||||||
|
accumY = t.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "tl"
|
||||||
|
x: -root.hs / 2
|
||||||
|
y: -root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "t"
|
||||||
|
x: root.width / 2 - root.hs / 2
|
||||||
|
y: -root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "tr"
|
||||||
|
x: root.width - root.hs / 2
|
||||||
|
y: -root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "r"
|
||||||
|
x: root.width - root.hs / 2
|
||||||
|
y: root.height / 2 - root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "br"
|
||||||
|
x: root.width - root.hs / 2
|
||||||
|
y: root.height - root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "b"
|
||||||
|
x: root.width / 2 - root.hs / 2
|
||||||
|
y: root.height - root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "bl"
|
||||||
|
x: -root.hs / 2
|
||||||
|
y: root.height - root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
edge: "l"
|
||||||
|
x: -root.hs / 2
|
||||||
|
y: root.height / 2 - root.hs / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
component Handle: Rectangle {
|
||||||
|
id: handle
|
||||||
|
|
||||||
|
required property string edge
|
||||||
|
|
||||||
|
border.color: root.handleBorder
|
||||||
|
border.width: 2
|
||||||
|
color: root.handleFill
|
||||||
|
height: root.hs
|
||||||
|
radius: root.hs / 2
|
||||||
|
width: root.hs
|
||||||
|
z: 10
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
cursorShape: {
|
||||||
|
switch (handle.edge) {
|
||||||
|
case "tl":
|
||||||
|
case "br":
|
||||||
|
return Qt.SizeFDiagCursor;
|
||||||
|
case "tr":
|
||||||
|
case "bl":
|
||||||
|
return Qt.SizeBDiagCursor;
|
||||||
|
case "t":
|
||||||
|
case "b":
|
||||||
|
return Qt.SizeVerCursor;
|
||||||
|
default:
|
||||||
|
return Qt.SizeHorCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: dh
|
||||||
|
|
||||||
|
property real startH
|
||||||
|
property real startW
|
||||||
|
property real startX
|
||||||
|
property real startY
|
||||||
|
|
||||||
|
grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden
|
||||||
|
target: null
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
startX = root.x;
|
||||||
|
startY = root.y;
|
||||||
|
startW = root.width;
|
||||||
|
startH = root.height;
|
||||||
|
} else {
|
||||||
|
root.dragFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTranslationChanged: {
|
||||||
|
const delta = dh.translation;
|
||||||
|
const e = handle.edge;
|
||||||
|
let nx = startX, ny = startY, nw = startW, nh = startH;
|
||||||
|
|
||||||
|
if (e.includes("l")) {
|
||||||
|
nx = startX + delta.x;
|
||||||
|
nw = startW - delta.x;
|
||||||
|
}
|
||||||
|
if (e.includes("r")) {
|
||||||
|
nw = startW + delta.x;
|
||||||
|
}
|
||||||
|
if (e.includes("t")) {
|
||||||
|
ny = startY + delta.y;
|
||||||
|
nh = startH - delta.y;
|
||||||
|
}
|
||||||
|
if (e.includes("b")) {
|
||||||
|
nh = startH + delta.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nw < root.minSize) {
|
||||||
|
if (e.includes("l"))
|
||||||
|
nx = startX + startW - root.minSize;
|
||||||
|
nw = root.minSize;
|
||||||
|
}
|
||||||
|
if (nh < root.minSize) {
|
||||||
|
if (e.includes("t"))
|
||||||
|
ny = startY + startH - root.minSize;
|
||||||
|
nh = root.minSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.boundsChanged(nx, ny, nw, nh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,9 @@ Singleton {
|
|||||||
category: "appearance"
|
category: "appearance"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: qsTr("Screenshot"),
|
name: qsTr("Screenshots"),
|
||||||
icon: "screenshot_region",
|
icon: "screenshot_region",
|
||||||
description: qsTr("Set screenshot effects"),
|
description: qsTr("Set screenshot options and effects"),
|
||||||
category: "appearance"
|
category: "appearance"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import qs.Modules.Settings.Common
|
|||||||
PageBase {
|
PageBase {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
title: qsTr("Screenshot effects")
|
title: qsTr("Screenshots")
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -22,6 +22,21 @@ PageBase {
|
|||||||
|
|
||||||
SectionHeader {
|
SectionHeader {
|
||||||
first: true
|
first: true
|
||||||
|
text: qsTr("Behavior")
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
checked: Config.screenshot.saveOnCopy
|
||||||
|
first: true
|
||||||
|
last: true
|
||||||
|
settingAnchor: "screenshot-save-on-copy"
|
||||||
|
subtext: qsTr("Also save screenshot to disk when copying to clipboard")
|
||||||
|
text: qsTr("Save on copy")
|
||||||
|
|
||||||
|
onToggled: Config.screenshot.saveOnCopy = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
text: qsTr("Effects")
|
text: qsTr("Effects")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Singleton {
|
|||||||
readonly property string notifimagecache: `${imagecache}/notifs`
|
readonly property string notifimagecache: `${imagecache}/notifs`
|
||||||
readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures`
|
readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures`
|
||||||
readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings`
|
readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings`
|
||||||
|
readonly property string screenshots: Quickshell.env("ZSHELL_SCREENSHOTS_DIR") || `${home}/Pictures/Screenshots`
|
||||||
readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell`
|
readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell`
|
||||||
readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos`
|
readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos`
|
||||||
readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.general.wallpaperPath)
|
readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.general.wallpaperPath)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct Stroke {
|
|||||||
qreal width;
|
qreal width;
|
||||||
int groupId = -1;
|
int groupId = -1;
|
||||||
bool isSinglePoint = false;
|
bool isSinglePoint = false;
|
||||||
|
bool isPolyline = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
}; // namespace ZShell::internal
|
}; // namespace ZShell::internal
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ static bool shouldAddPoint(
|
|||||||
return QPointF::dotProduct(delta, delta) >= minDistance * minDistance;
|
return QPointF::dotProduct(delta, delta) >= minDistance * minDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width) {
|
static QCanvasPath buildStrokePath(
|
||||||
|
const QVector<QPointF>& points, qreal width, bool polyline) {
|
||||||
QCanvasPath path;
|
QCanvasPath path;
|
||||||
|
|
||||||
if (points.size() == 1) {
|
if (points.size() == 1) {
|
||||||
@@ -33,6 +34,13 @@ static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width)
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (polyline) {
|
||||||
|
path.moveTo(points[0]);
|
||||||
|
for (int i = 1; i < points.size(); i++)
|
||||||
|
path.lineTo(points[i]);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
auto catmullToBezier = [](const QPointF& p0,
|
auto catmullToBezier = [](const QPointF& p0,
|
||||||
const QPointF& p1,
|
const QPointF& p1,
|
||||||
const QPointF& p2,
|
const QPointF& p2,
|
||||||
@@ -61,6 +69,16 @@ static QCanvasPath buildStrokePath(const QVector<QPointF>& points, qreal width)
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StrokeCanvasItem::undoLastGroup() {
|
||||||
|
if (m_strokes.isEmpty()) return;
|
||||||
|
const int lastGroup = m_strokes.last().groupId;
|
||||||
|
for (int i = m_strokes.size() - 1;
|
||||||
|
i >= 0 && m_strokes[i].groupId == lastGroup;
|
||||||
|
--i)
|
||||||
|
m_strokes.remove(i);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
void StrokeCanvasItem::setPenColor(const QColor& color) {
|
void StrokeCanvasItem::setPenColor(const QColor& color) {
|
||||||
if (m_penColor == color) return;
|
if (m_penColor == color) return;
|
||||||
|
|
||||||
@@ -121,13 +139,14 @@ void StrokeCanvasItem::setPenWidth(qreal width) {
|
|||||||
emit penWidthChanged();
|
emit penWidthChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StrokeCanvasItem::beginStroke(qreal x, qreal y) {
|
void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) {
|
||||||
m_isDrawing = true;
|
m_isDrawing = true;
|
||||||
m_currentStroke.points.clear();
|
m_currentStroke.points.clear();
|
||||||
m_currentStroke.points.append({x, y});
|
m_currentStroke.points.append({x, y});
|
||||||
|
|
||||||
m_currentStroke.color = m_penColor;
|
m_currentStroke.color = m_penColor;
|
||||||
m_currentStroke.width = m_penWidth;
|
m_currentStroke.width = m_penWidth;
|
||||||
|
m_currentStroke.isPolyline = polyline;
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@@ -135,6 +154,12 @@ void StrokeCanvasItem::beginStroke(qreal x, qreal y) {
|
|||||||
void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
|
void StrokeCanvasItem::appendPoint(qreal x, qreal y) {
|
||||||
const QPointF incoming{x, y};
|
const QPointF incoming{x, y};
|
||||||
|
|
||||||
|
if (m_currentStroke.isPolyline) {
|
||||||
|
m_currentStroke.points.append(incoming);
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) return;
|
if (!shouldAddPoint(m_currentStroke.points, incoming, 2.0)) return;
|
||||||
|
|
||||||
QPointF smoothed;
|
QPointF smoothed;
|
||||||
@@ -166,8 +191,10 @@ void StrokeCanvasItem::endStroke() {
|
|||||||
if (m_currentStroke.points.isEmpty()) return;
|
if (m_currentStroke.points.isEmpty()) return;
|
||||||
|
|
||||||
m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1);
|
m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1);
|
||||||
m_currentStroke.path =
|
m_currentStroke.path = buildStrokePath(
|
||||||
buildStrokePath(m_currentStroke.points, m_currentStroke.width);
|
m_currentStroke.points,
|
||||||
|
m_currentStroke.width,
|
||||||
|
m_currentStroke.isPolyline);
|
||||||
m_currentStroke.groupId = m_nextGroupId++;
|
m_currentStroke.groupId = m_nextGroupId++;
|
||||||
m_currentStroke.points.clear();
|
m_currentStroke.points.clear();
|
||||||
m_strokes.append(m_currentStroke);
|
m_strokes.append(m_currentStroke);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class StrokeCanvasItem : public QCanvasPainterItem {
|
|||||||
|
|
||||||
Q_INVOKABLE void showHover(qreal x, qreal y);
|
Q_INVOKABLE void showHover(qreal x, qreal y);
|
||||||
Q_INVOKABLE void hideHover();
|
Q_INVOKABLE void hideHover();
|
||||||
|
Q_INVOKABLE void undoLastGroup();
|
||||||
|
|
||||||
[[nodiscard]] QColor penColor() const { return m_penColor; }
|
[[nodiscard]] QColor penColor() const { return m_penColor; }
|
||||||
[[nodiscard]] qreal penWidth() const { return m_penWidth; }
|
[[nodiscard]] qreal penWidth() const { return m_penWidth; }
|
||||||
@@ -48,7 +49,7 @@ class StrokeCanvasItem : public QCanvasPainterItem {
|
|||||||
|
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
|
|
||||||
Q_INVOKABLE void beginStroke(qreal x, qreal y);
|
Q_INVOKABLE void beginStroke(qreal x, qreal y, bool polyline = false);
|
||||||
Q_INVOKABLE void appendPoint(qreal x, qreal y);
|
Q_INVOKABLE void appendPoint(qreal x, qreal y);
|
||||||
Q_INVOKABLE void endStroke();
|
Q_INVOKABLE void endStroke();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ static void drawStroke(
|
|||||||
QCanvasPainter* painter,
|
QCanvasPainter* painter,
|
||||||
const QVector<QPointF>& points,
|
const QVector<QPointF>& points,
|
||||||
const QColor& color,
|
const QColor& color,
|
||||||
qreal width) {
|
qreal width,
|
||||||
|
bool polyline) {
|
||||||
if (points.isEmpty()) return;
|
if (points.isEmpty()) return;
|
||||||
|
|
||||||
painter->setStrokeStyle(color);
|
painter->setStrokeStyle(color);
|
||||||
@@ -17,6 +18,15 @@ static void drawStroke(
|
|||||||
painter->setLineCap(QCanvasPainter::LineCap::Round);
|
painter->setLineCap(QCanvasPainter::LineCap::Round);
|
||||||
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
|
painter->setLineJoin(QCanvasPainter::LineJoin::Round);
|
||||||
|
|
||||||
|
if (polyline) {
|
||||||
|
painter->beginPath();
|
||||||
|
painter->moveTo(points[0]);
|
||||||
|
for (int i = 1; i < points.size(); i++)
|
||||||
|
painter->lineTo(points[i]);
|
||||||
|
painter->stroke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (points.size() == 1) {
|
if (points.size() == 1) {
|
||||||
painter->beginPath();
|
painter->beginPath();
|
||||||
painter->circle(points.front(), width * 0.5);
|
painter->circle(points.front(), width * 0.5);
|
||||||
@@ -180,7 +190,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
|
|||||||
painter,
|
painter,
|
||||||
m_currentStroke.points,
|
m_currentStroke.points,
|
||||||
m_currentStroke.color,
|
m_currentStroke.color,
|
||||||
m_currentStroke.width);
|
m_currentStroke.width,
|
||||||
|
m_currentStroke.isPolyline);
|
||||||
|
|
||||||
if (m_hoverVisible)
|
if (m_hoverVisible)
|
||||||
drawHoverCursor(
|
drawHoverCursor(
|
||||||
|
|||||||
Reference in New Issue
Block a user