diff --git a/Components/ButtonBase.qml b/Components/ButtonBase.qml index 5eb4bb7..6373f39 100644 --- a/Components/ButtonBase.qml +++ b/Components/ButtonBase.qml @@ -35,7 +35,7 @@ CustomRect { readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor property real padding readonly property alias pressed: stateLayer.pressed - property real pressedRadius: Appearance.rounding.small + property real pressedRadius: Appearance.rounding.smallest readonly property alias radiusAnim: radiusAnim property bool radiusMorph: true property alias shapeMorph: stateLayer.shapeMorph diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml old mode 100644 new mode 100755 index f48951f..b59dc8a --- a/Helpers/Picker.qml +++ b/Helpers/Picker.qml @@ -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 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 diff --git a/Helpers/Picker/AnnotationCanvas.qml b/Helpers/Picker/AnnotationCanvas.qml new file mode 100755 index 0000000..7ff98b0 --- /dev/null +++ b/Helpers/Picker/AnnotationCanvas.qml @@ -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]); + } + } + } +} diff --git a/Helpers/Picker/AnnotationToolbar.qml b/Helpers/Picker/AnnotationToolbar.qml new file mode 100755 index 0000000..758e58d --- /dev/null +++ b/Helpers/Picker/AnnotationToolbar.qml @@ -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() + } + } + } +} diff --git a/Helpers/Picker/SelectionHandles.qml b/Helpers/Picker/SelectionHandles.qml new file mode 100755 index 0000000..eca4b0f --- /dev/null +++ b/Helpers/Picker/SelectionHandles.qml @@ -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); + } + } + } +} diff --git a/Plugins/ZShell/Internal/stroke.hpp b/Plugins/ZShell/Internal/stroke.hpp index aba955b..200ae4f 100644 --- a/Plugins/ZShell/Internal/stroke.hpp +++ b/Plugins/ZShell/Internal/stroke.hpp @@ -14,6 +14,7 @@ struct Stroke { qreal width; int groupId = -1; bool isSinglePoint = false; + bool isPolyline = false; }; }; // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/strokecanvasitem.cpp b/Plugins/ZShell/Internal/strokecanvasitem.cpp index 4f8d615..dcb7e47 100644 --- a/Plugins/ZShell/Internal/strokecanvasitem.cpp +++ b/Plugins/ZShell/Internal/strokecanvasitem.cpp @@ -25,7 +25,8 @@ static bool shouldAddPoint( return QPointF::dotProduct(delta, delta) >= minDistance * minDistance; } -static QCanvasPath buildStrokePath(const QVector& points, qreal width) { +static QCanvasPath buildStrokePath( + const QVector& points, qreal width, bool polyline) { QCanvasPath path; if (points.size() == 1) { @@ -33,6 +34,13 @@ static QCanvasPath buildStrokePath(const QVector& points, qreal width) 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, const QPointF& p1, const QPointF& p2, @@ -61,6 +69,16 @@ static QCanvasPath buildStrokePath(const QVector& points, qreal width) 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) { if (m_penColor == color) return; @@ -121,13 +139,14 @@ void StrokeCanvasItem::setPenWidth(qreal width) { emit penWidthChanged(); } -void StrokeCanvasItem::beginStroke(qreal x, qreal y) { +void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) { m_isDrawing = true; m_currentStroke.points.clear(); m_currentStroke.points.append({x, y}); m_currentStroke.color = m_penColor; m_currentStroke.width = m_penWidth; + m_currentStroke.isPolyline = polyline; update(); } @@ -135,6 +154,12 @@ void StrokeCanvasItem::beginStroke(qreal x, qreal y) { void StrokeCanvasItem::appendPoint(qreal x, qreal 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; QPointF smoothed; @@ -166,8 +191,10 @@ void StrokeCanvasItem::endStroke() { if (m_currentStroke.points.isEmpty()) return; m_currentStroke.isSinglePoint = (m_currentStroke.points.size() == 1); - m_currentStroke.path = - buildStrokePath(m_currentStroke.points, m_currentStroke.width); + m_currentStroke.path = buildStrokePath( + m_currentStroke.points, + m_currentStroke.width, + m_currentStroke.isPolyline); m_currentStroke.groupId = m_nextGroupId++; m_currentStroke.points.clear(); m_strokes.append(m_currentStroke); diff --git a/Plugins/ZShell/Internal/strokecanvasitem.hpp b/Plugins/ZShell/Internal/strokecanvasitem.hpp index 67a75fd..7f1e9a0 100644 --- a/Plugins/ZShell/Internal/strokecanvasitem.hpp +++ b/Plugins/ZShell/Internal/strokecanvasitem.hpp @@ -39,6 +39,7 @@ class StrokeCanvasItem : public QCanvasPainterItem { Q_INVOKABLE void showHover(qreal x, qreal y); Q_INVOKABLE void hideHover(); + Q_INVOKABLE void undoLastGroup(); [[nodiscard]] QColor penColor() const { return m_penColor; } [[nodiscard]] qreal penWidth() const { return m_penWidth; } @@ -48,7 +49,7 @@ class StrokeCanvasItem : public QCanvasPainterItem { 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 endStroke(); diff --git a/Plugins/ZShell/Internal/strokecanvasrenderer.cpp b/Plugins/ZShell/Internal/strokecanvasrenderer.cpp index 704fcd4..3a77d76 100644 --- a/Plugins/ZShell/Internal/strokecanvasrenderer.cpp +++ b/Plugins/ZShell/Internal/strokecanvasrenderer.cpp @@ -8,7 +8,8 @@ static void drawStroke( QCanvasPainter* painter, const QVector& points, const QColor& color, - qreal width) { + qreal width, + bool polyline) { if (points.isEmpty()) return; painter->setStrokeStyle(color); @@ -17,6 +18,15 @@ static void drawStroke( painter->setLineCap(QCanvasPainter::LineCap::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) { painter->beginPath(); painter->circle(points.front(), width * 0.5); @@ -180,7 +190,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) { painter, m_currentStroke.points, m_currentStroke.color, - m_currentStroke.width); + m_currentStroke.width, + m_currentStroke.isPolyline); if (m_hoverVisible) drawHoverCursor(