diff --git a/CMakeLists.txt b/CMakeLists.txt index d9b81d0..e8f999c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,10 @@ set(SOURCES src/instance_activation.cpp src/instance_activation.h src/singleton.h + src/widgets/sticky_pin_widget.cpp + src/widgets/sticky_pin_widget.h + src/widgets/sticky_canvas_widget.cpp + src/widgets/sticky_canvas_widget.h # Model src/model/desktop_snapshot.cpp @@ -104,6 +108,8 @@ set(SOURCES src/paint_board/shape/two_point_shape.h src/paint_board/shape/arrow_shape.cpp src/paint_board/shape/arrow_shape.h + src/paint_board/shape/blur_shape.cpp + src/paint_board/shape/blur_shape.h src/paint_board/shape/ellipse_shape.cpp src/paint_board/shape/ellipse_shape.h src/paint_board/shape/mosaic_shape.cpp diff --git a/i18n/app_en.ts b/i18n/app_en.ts index 868a7f7..4781bad 100644 --- a/i18n/app_en.ts +++ b/i18n/app_en.ts @@ -93,37 +93,37 @@ TZChat - + AI 图像编辑 AI Image Edit - + 当前贴图 Current Image - + 未关联贴图 No Image Linked - + 发送后将对此图执行 AI 编辑 AI edit will be applied to this image after sending - + 描述您想要的修改... Describe the changes you want... - + 取消 Cancel - + 发送 Send @@ -189,7 +189,7 @@ TZGifRecordBar - + 正在生成 GIF... @@ -266,52 +266,52 @@ If it conflicts with system or other app shortcuts, change to a different combin TZMarkToolInfo - + 文本 - + 细 Thin - + 中 Medium - + 粗 Thick - + 特粗 Extra Thick - + 输入文字 - + 背景 - + 模糊 - + 自动递增 - + 数字 @@ -319,32 +319,32 @@ If it conflicts with system or other app shortcuts, change to a different combin TZOcrResult - + OCR 识别结果 OCR Results - + 正在识别中… Recognizing… - + 识别结果将显示在这里… Recognition results will appear here… - + 复制全部 Copy All - + 关闭 Close - + 已复制! Copied! @@ -378,46 +378,46 @@ If it conflicts with system or other app shortcuts, change to a different combin TZStickyWindow - + 当前透明度 %1% Opacity: %1% - - + + 覆盖贴图(合并标注) Overwrite Image (Merge Annotations) - - + + 显示工具栏 Show Toolbar - + 透明度 +5% Opacity +5% - + 透明度 -5% Opacity -5% - + 旋转 90° Rotate 90° - + 重置位置 Reset Position - - - + + + AI 编辑 AI Edit @@ -426,120 +426,120 @@ If it conflicts with system or other app shortcuts, change to a different combin Save Image - - - + + + 复制贴图 Copy Image - - - + + + 关闭窗口 Close Window - + 保存原图 Save Original Image - - + + 另存为(合并标注) Save As (Merge Annotations) - + 常用项置顶 Pin Common Items - + 覆盖贴图 Overwrite Image - + 显示/隐藏工具栏 Show/Hide Toolbar - + 选择保存位置 Choose Save Location - + Pencil Pencil - + Rectangle Rectangle - + Circle Circle - + Arrow Arrow - - + + Mosaic Mosaic - - + + 缩小 Zoom Out - - + + 放大 Zoom In - + 旋转90° Rotate 90° - - + + 镜像 Mirror - - + + 恢复原始大小 Reset Size - + 完成并复制 Apply and Copy - + 撤销 Undo - - - - + + + + 隐藏工具栏 Hide Toolbar @@ -560,8 +560,8 @@ If it conflicts with system or other app shortcuts, change to a different combin Hide Toolbar - - + + OCR OCR @@ -573,7 +573,7 @@ If it conflicts with system or other app shortcuts, change to a different combin Confirm - + 取消 Cancel @@ -598,67 +598,67 @@ If it conflicts with system or other app shortcuts, change to a different combin Arrow - + 完成 Done - + 录制GIF Record GIF - + 保存 Save - + 贴图 Pin - + 矩形 Rectangle - + 圆形 Circle - + 箭头 Arrow - + 画笔 Pen - + 高亮 Highlight - + 马赛克 Mosaic - + 文字 Text - + 序号 Number - + 撤销 Undo @@ -667,7 +667,7 @@ If it conflicts with system or other app shortcuts, change to a different combin Number - + OCR OCR @@ -703,7 +703,7 @@ If it conflicts with system or other app shortcuts, change to a different combin Failed to save screenshot file - + %1 * %2 %1 * %2 @@ -724,57 +724,57 @@ If it conflicts with system or other app shortcuts, change to a different combin Failed to copy screenshot to clipboard - + Capture done Capture done - + Copied to clipboard Copied to clipboard - + Capture failed Capture failed - + Failed to copy image Failed to copy image - + Select save location Select save location - + Save success Save success - + Screenshot saved to file Screenshot saved to file - + Save failed Save failed - + Failed to save screenshot file Failed to save screenshot file - + Cancel Cancel - + OK OK diff --git a/i18n/app_zh_CN.ts b/i18n/app_zh_CN.ts index 9f7ee39..0c9165f 100644 --- a/i18n/app_zh_CN.ts +++ b/i18n/app_zh_CN.ts @@ -82,37 +82,37 @@ TZChat - + AI 图像编辑 - + 当前贴图 - + 未关联贴图 - + 发送后将对此图执行 AI 编辑 - + 描述您想要的修改... - + 取消 - + 发送 @@ -178,7 +178,7 @@ TZGifRecordBar - + 正在生成 GIF... @@ -254,52 +254,52 @@ TZMarkToolInfo - + 文本 - + 细 - + 中 - + 粗 - + 特粗 - + 输入文字 - + 背景 - + 模糊 - + 自动递增 - + 数字 @@ -307,32 +307,32 @@ TZOcrResult - + OCR 识别结果 - + 正在识别中… - + 识别结果将显示在这里… - + 复制全部 - + 关闭 - + 已复制! @@ -366,170 +366,170 @@ TZStickyWindow - - + + OCR - + Pencil - + Rectangle - + Circle - + Arrow - - + + Mosaic - - + + 缩小 - - + + 放大 - + 旋转90° - - + + 镜像 - - + + 恢复原始大小 - + 完成并复制 - + 撤销 - - - - + + + + 隐藏工具栏 - + 当前透明度 %1% - - + + 覆盖贴图(合并标注) - - - + + + 复制贴图 - - - + + + AI 编辑 - - + + 显示工具栏 - - - + + + 关闭窗口 - + 透明度 +5% - + 透明度 -5% - + 旋转 90° - + 重置位置 - + 保存原图 - - + + 另存为(合并标注) - + 常用项置顶 - + 覆盖贴图 - + 显示/隐藏工具栏 - + 选择保存位置 @@ -537,77 +537,77 @@ TZTool - + 取消 - + 完成 - + OCR - + 录制GIF - + 保存 - + 贴图 - + 矩形 - + 圆形 - + 箭头 - + 画笔 - + 高亮 - + 马赛克 - + 文字 - + 序号 - + 撤销 @@ -615,62 +615,62 @@ TZWindow - + Capture done - + Copied to clipboard - + Capture failed - + Failed to copy image - + Select save location - + Save success - + Screenshot saved to file - + Save failed - + Failed to save screenshot file - + %1 * %2 - + Cancel - + OK diff --git a/qml.qrc b/qml.qrc index ed8f88d..b86b9ca 100644 --- a/qml.qrc +++ b/qml.qrc @@ -19,16 +19,13 @@ qml/Capture/TZMagnifyingGlass.qml qml/Capture/TZGifRecordBar.qml qml/Windows/TZStickyWindow.qml - qml/Windows/TZStickyExtraToolBar.qml qml/Windows/TZOcrResult.qml qml/Windows/TZChat.qml - qml/Windows/TZLoadingSpinner.qml qml/Setting/TZSettingWin.qml qml/Setting/TZGeneralSettings.qml qml/Setting/TZRightPanel.qml qml/TZControls/qmldir qml/TZControls/TZComboBox.qml - qml/TZStickyContextMenu.qml qml/Setting/TZHotkeySettings.qml qml/Setting/TZAboutWin.qml resource/img/lc_check.svg @@ -39,6 +36,7 @@ resource/img/lc_pencil.svg resource/img/lc_circle.svg resource/img/lc_arrow.svg + resource/img/lc_blur.svg resource/img/lc_mosaic.svg resource/img/lc_text.svg resource/img/lc_list_ordered.svg @@ -47,8 +45,6 @@ resource/img/tray_icon.svg resource/img/lc_close.svg resource/img/lc_chat.svg - resource/img/lc_emoji.svg - resource/img/lc_file.svg resource/img/lc_send.svg qml/LongCapture/TZLongCaptureBar.qml qml/LongCapture/TZLongCapturePreview.qml @@ -57,5 +53,6 @@ resource/img/lc_gif_stop.svg resource/img/lc_ocr.svg resource/img/lc_longshot.svg + resource/qss/sticky_pin_widget.qss diff --git a/qml/Capture/TZTool.qml b/qml/Capture/TZTool.qml index 5d6552b..f117cab 100644 --- a/qml/Capture/TZTool.qml +++ b/qml/Capture/TZTool.qml @@ -1,17 +1,14 @@ -import QtQuick 2.12 +import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.5 Rectangle { id: tool - radius: 10 color: "#FFFFFF" visible: false border.color: "#E2E8F0" border.width: 1 layer.enabled: true - implicitWidth: toolRow.implicitWidth + 12 - implicitHeight: 44 signal signalScreenshot signal signalCancel @@ -42,6 +39,18 @@ Rectangle { property bool showMosaicButton: true property bool showTextButton: true property bool showNumberButton: true + readonly property int buttonSize: 32 + readonly property int buttonRadius: 7 + readonly property int iconSize: 18 + readonly property int dividerHeight: 20 + readonly property int outerRadius: 10 + readonly property int outerMargin: 6 + readonly property int bubbleRadius: 6 + readonly property int bubbleHeight: 24 + readonly property int bubblePadding: 14 + readonly property int bubbleGap: 6 + readonly property int tipFontSize: 11 + readonly property int rowSpacing: 2 readonly property int shapeType: { switch (activeTool) { @@ -82,161 +91,165 @@ Rectangle { hoverTipText = "" } + radius: outerRadius + implicitWidth: toolRow.implicitWidth + outerMargin * 2 + implicitHeight: Math.max(buttonSize + outerMargin * 2, 44) + RowLayout { id: toolRow anchors.fill: parent - anchors.margins: 6 + anchors.margins: tool.outerMargin layoutDirection: Qt.RightToLeft - spacing: 2 + spacing: tool.rowSpacing Rectangle { visible: tool.showScreenshotButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: checkArea.containsMouse ? "#DCFCE7" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: checkArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("完成"), parent); onExited: tool.hideTip(qsTr("完成")); onPressed: signalScreenshot() } } Rectangle { visible: tool.showCancelButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: closeArea.containsMouse ? "#FEE2E2" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: closeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("取消"), parent); onExited: tool.hideTip(qsTr("取消")); onPressed: signalCancel() } } - Rectangle { visible: tool.showScreenshotButton || tool.showCancelButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } + Rectangle { visible: tool.showScreenshotButton || tool.showCancelButton; width: 1; height: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } Rectangle { visible: tool.showOcrButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: ocrArea.containsMouse ? "#F0FDF4" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: ocrArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("OCR"), parent); onExited: tool.hideTip(qsTr("OCR")); onPressed: signalOcr() } } Rectangle { id: gifRecordBtn visible: tool.showGifRecordButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: gifArea.containsMouse ? "#FFF7ED" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: gifArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("录制GIF"), parent); onExited: tool.hideTip(qsTr("录制GIF")); onPressed: signalGifRecord() } } Rectangle { visible: tool.showLongScreenshotButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: longShotArea.containsMouse ? "#FFF7ED" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_longshot.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_longshot.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: longShotArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("长截图"), parent); onExited: tool.hideTip(qsTr("长截图")); onPressed: signalLongScreenshot() } } - Rectangle { visible: tool.showOcrButton || tool.showGifRecordButton || tool.showLongScreenshotButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } + Rectangle { visible: tool.showOcrButton || tool.showGifRecordButton || tool.showLongScreenshotButton; width: 1; height: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } Rectangle { visible: tool.showSaveButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: saveArea.containsMouse ? "#EFF6FF" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: saveArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("保存"), parent); onExited: tool.hideTip(qsTr("保存")); onPressed: signalSave() } } Rectangle { visible: tool.showStickyButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: fixArea.containsMouse ? "#EFF6FF" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: fixArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("贴图"), parent); onExited: tool.hideTip(qsTr("贴图")); onPressed: signalSticky() } } - Rectangle { visible: tool.showSaveButton || tool.showStickyButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } + Rectangle { visible: tool.showSaveButton || tool.showStickyButton; width: 1; height: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } Rectangle { visible: tool.showRectButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "rect") ? "#EFF6FF" : (rectArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "rect") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: rectArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("矩形"), parent); onExited: tool.hideTip(qsTr("矩形")); onPressed: tool.setActiveTool("rect") } } Rectangle { visible: tool.showCircleButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "circle") ? "#EFF6FF" : (circleArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "circle") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: circleArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("圆形"), parent); onExited: tool.hideTip(qsTr("圆形")); onPressed: tool.setActiveTool("circle") } } Rectangle { visible: tool.showArrowButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "arrow") ? "#EFF6FF" : (arrowArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "arrow") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: arrowArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("箭头"), parent); onExited: tool.hideTip(qsTr("箭头")); onPressed: tool.setActiveTool("arrow") } } Rectangle { visible: tool.showPencilButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "pencil") ? "#EFF6FF" : (pencilArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "pencil") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: pencilArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("画笔"), parent); onExited: tool.hideTip(qsTr("画笔")); onPressed: tool.setActiveTool("pencil") } } Rectangle { visible: tool.showHighlightButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "highlight") ? "#EFF6FF" : (highlightArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "highlight") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_highlighter.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_highlighter.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: highlightArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("高亮"), parent); onExited: tool.hideTip(qsTr("高亮")); onPressed: tool.setActiveTool("highlight") } } Rectangle { visible: tool.showMosaicButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "mosaic") ? "#EFF6FF" : (mosaicArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "mosaic") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: mosaicArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("马赛克"), parent); onExited: tool.hideTip(qsTr("马赛克")); onPressed: tool.setActiveTool("mosaic") } } Rectangle { visible: tool.showTextButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "text") ? "#EFF6FF" : (textArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "text") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_text.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_text.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: textArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("文字"), parent); onExited: tool.hideTip(qsTr("文字")); onPressed: tool.setActiveTool("text") } } Rectangle { visible: tool.showNumberButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: (tool.activeTool === "number") ? "#EFF6FF" : (numberArea.containsMouse ? "#F1F5F9" : "transparent") border.color: (tool.activeTool === "number") ? "#3B82F6" : "transparent" border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_list_ordered.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_list_ordered.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: numberArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("序号"), parent); onExited: tool.hideTip(qsTr("序号")); onPressed: tool.setActiveTool("number") } } Rectangle { visible: tool.showUndoButton - width: 32; height: 32; radius: 7 + width: tool.buttonSize; height: tool.buttonSize; radius: tool.buttonRadius color: undoArea.containsMouse ? "#EFF6FF" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: 18; height: 18 } + Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: tool.iconSize; height: tool.iconSize } MouseArea { id: undoArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("撤销"), parent); onExited: tool.hideTip(qsTr("撤销")); onPressed: signalUndo() } } } @@ -246,20 +259,20 @@ Rectangle { visible: tool.visible && tool.hoverTipText.length > 0 z: 100 parent: tool.parent - width: Math.max(44, tipText.implicitWidth + 14) - height: 24 - radius: 6 + width: Math.max(36, tipText.implicitWidth + tool.bubblePadding) + height: tool.bubbleHeight + radius: tool.bubbleRadius color: "#0F172A" opacity: 0.92 x: Math.max(tool.x, Math.min(tool.x + tool.width - width, tool.x + tool.hoverTipCenterX - width / 2)) - y: tool.y + tool.height + 6 + y: tool.y + tool.height + tool.bubbleGap Text { id: tipText anchors.centerIn: parent text: tool.hoverTipText color: "#FFFFFF" - font.pixelSize: 11 + font.pixelSize: tool.tipFontSize } } } diff --git a/qml/Capture/TZWindow.qml b/qml/Capture/TZWindow.qml index 1f603f2..02ea925 100644 --- a/qml/Capture/TZWindow.qml +++ b/qml/Capture/TZWindow.qml @@ -233,7 +233,6 @@ Window { ) function createStickyWindow(imageUrl, imgRect) { - console.log("imgRect-->",imgRect) return windowFactory.createStickyWindow(imageUrl, imgRect) } @@ -550,7 +549,6 @@ Window { Qt.callLater(function() { var captureRect = normalizedScreenshotArea var url = O_ScreenCapture.captureRectToStickyUrl(paintBoard, captureRect) - console.log("[Sticky] imageUrl:", url) O_StickyViewModel.requestSticky(url, captureRect) resetArea() }) diff --git a/qml/TZChat.qml b/qml/TZChat.qml deleted file mode 100644 index ac6aa6e..0000000 --- a/qml/TZChat.qml +++ /dev/null @@ -1,264 +0,0 @@ -import QtQuick -import QtQuick.Window -import QtQuick.Layouts -import QtQuick.Controls.Basic - -QtObject { - id: chatPanel - - // 对应贴图的图片 URL,由外部绑定 - property string imageUrl: "" - - signal signalSendPrompt(var text) - - // 公开方法 - function open(px, py) { - win.x = px - win.y = py - win.visible = true - win.raise() - win.requestActivate() - } - - function close() { - win.visible = false - } - - // ── 内部 Window ───────────────────────────────────── - property var win: Window { - id: win - flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool - color: "transparent" - width: 420 - height: 380 - visible: false - - Rectangle { - anchors.fill: parent - color: "#ffffff" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - // ── 顶部标题栏 ──────────────────────────────────── - Rectangle { - Layout.fillWidth: true - height: 36 - color: "transparent" - - // 拖动区域(z:-1 让关闭按钮的点击不被拦截) - MouseArea { - id: dragHandle - anchors.fill: parent - z: -1 - property point startPos - - onPressed: startPos = Qt.point(mouseX, mouseY) - onPositionChanged: { - if (pressed) { - win.x += mouseX - startPos.x - win.y += mouseY - startPos.y - } - } - cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor - } - - RowLayout { - anchors.fill: parent - spacing: 8 - - Image { - source: "qrc:/resource/img/lc_chat.svg" - width: 20 - height: 20 - fillMode: Image.PreserveAspectFit - } - - Text { - text: qsTr("AI 图像编辑") - font.pixelSize: 14 - font.weight: Font.Medium - color: "#1E293B" - } - - Item { Layout.fillWidth: true } - - // 关闭按钮 - Rectangle { - width: 28 - height: 28 - radius: 6 - color: clearMouse.containsMouse ? "#FEE2E2" : "transparent" - - Image { - anchors.centerIn: parent - source: "qrc:/resource/img/lc_close.svg" - width: 14 - height: 14 - fillMode: Image.PreserveAspectFit - } - - MouseArea { - id: clearMouse - anchors.fill: parent - hoverEnabled: true - onClicked: chatPanel.close() - } - } - } - - // 标题栏底部分割线 - Rectangle { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - height: 1 - color: "#E2E8F0" - } - } - - // ── 贴图缩略图预览 ──────────────────────────────── - Rectangle { - Layout.fillWidth: true - height: 64 - radius: 6 - color: "#F1F5F9" - border.color: "#E2E8F0" - border.width: 1 - clip: true - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 10 - - // 缩略图 - Rectangle { - width: 48 - height: 48 - radius: 4 - color: "#E2E8F0" - clip: true - - Image { - anchors.fill: parent - source: chatPanel.imageUrl - fillMode: Image.PreserveAspectCrop - cache: false - } - } - - // 文字说明 - Column { - Layout.fillWidth: true - spacing: 3 - - Text { - text: qsTr("当前贴图") - font.pixelSize: 11 - color: "#94A3B8" - } - - Text { - width: parent.width - text: chatPanel.imageUrl !== "" - ? chatPanel.imageUrl.replace(/^image:\/\/[^/]+\//, "ID: ") - : qsTr("未关联贴图") - font.pixelSize: 12 - color: "#475569" - elide: Text.ElideRight - } - - Text { - text: qsTr("发送后将对此图执行 AI 编辑") - font.pixelSize: 10 - color: "#94A3B8" - } - } - } - } - - // ── 输入区域 ────────────────────────────────────── - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: "#F8FAFC" - radius: 8 - border.color: "#E2E8F0" - border.width: 1 - - TextArea { - id: input - anchors.fill: parent - anchors.margins: 12 - font.family: "Segoe UI" - font.pixelSize: 13 - color: "#1E293B" - wrapMode: Text.Wrap - placeholderText: qsTr("描述您想要的修改...") - background: Rectangle { color: "transparent" } - selectByMouse: true - focus: true - } - } - - // ── 底部工具栏 ──────────────────────────────────── - Rectangle { - Layout.fillWidth: true - height: 40 - color: "transparent" - - RowLayout { - anchors.fill: parent - spacing: 8 - - Item { Layout.fillWidth: true } - - // 取消按钮 - Rectangle { - width: 70; height: 32; radius: 6 - color: cancelMouse.containsMouse ? "#F1F5F9" : "#F8FAFC" - border.color: "#E2E8F0"; border.width: 1 - - Text { anchors.centerIn: parent; text: qsTr("取消"); font.pixelSize: 13; color: "#64748B" } - - MouseArea { - id: cancelMouse - anchors.fill: parent; hoverEnabled: true - onClicked: chatPanel.close() - } - } - - // 发送按钮 - Rectangle { - width: 70; height: 32; radius: 6 - color: sendMouse.pressed ? "#0066cc" : (sendMouse.containsMouse ? "#3399ff" : "#0088ff") - - RowLayout { - anchors.centerIn: parent; spacing: 4 - Image { source: "qrc:/resource/img/lc_send.svg"; width: 14; height: 14 } - Text { text: qsTr("发送"); font.pixelSize: 13; color: "white" } - } - - MouseArea { - id: sendMouse - anchors.fill: parent; hoverEnabled: true - onClicked: { - if (input.text.trim() !== "") { - chatPanel.signalSendPrompt(input.text) - chatPanel.close() - } - } - } - } - } - } - } - } - } -} diff --git a/qml/TZGifRecordBar.qml b/qml/TZGifRecordBar.qml deleted file mode 100644 index f5b4003..0000000 --- a/qml/TZGifRecordBar.qml +++ /dev/null @@ -1,194 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.5 - -// GIF 录制控制栏:在截图选区确定后显示 -Rectangle { - id: gifBar - radius: 10 - color: "#FFFFFF" - border.color: "#E2E8F0" - border.width: 1 - layer.enabled: true - - // 外部传入的录制区域 - property rect captureRect: Qt.rect(0, 0, 0, 0) - - signal signalCancel - signal signalRecordFinished - - readonly property bool isRecording: O_GifRecordVM.isRecording - readonly property bool isEncoding: O_GifRecordVM.isEncoding - readonly property int elapsedSecs: O_GifRecordVM.elapsedSecs - readonly property int frameCount: O_GifRecordVM.frameCount - - Connections { - target: O_GifRecordVM - - function onEncodingFinished(savedPath) { - gifBar.signalRecordFinished() - } - - function onEncodingFailed(error) { - console.warn("[GifRecordBar] 录制失败:", error) - gifBar.signalRecordFinished() - } - } - - SequentialAnimation { - id: blinkAnim - running: gifBar.isRecording - loops: Animation.Infinite - PropertyAnimation { target: recDot; property: "opacity"; to: 0.2; duration: 500 } - PropertyAnimation { target: recDot; property: "opacity"; to: 1.0; duration: 500 } - } - - RowLayout { - anchors.fill: parent - anchors.margins: 6 - layoutDirection: Qt.RightToLeft - spacing: 4 - - // 取消按钮 - Rectangle { - width: 32; height: 32; radius: 7 - visible: !gifBar.isEncoding - color: cancelArea.containsMouse ? "#FEE2E2" : "transparent" - Behavior on color { ColorAnimation { duration: 100 } } - Image { - anchors.centerIn: parent - source: "qrc:/resource/img/lc_x.svg" - width: 18; height: 18 - fillMode: Image.PreserveAspectFit - smooth: true - } - MouseArea { - id: cancelArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - O_GifRecordVM.cancelRecording() - gifBar.signalCancel() - } - } - } - - // 停止录制按钮 - Rectangle { - width: 32; height: 32; radius: 7 - visible: gifBar.isRecording && !gifBar.isEncoding - color: stopArea.containsMouse ? "#FEF3C7" : "transparent" - Behavior on color { ColorAnimation { duration: 100 } } - Image { - anchors.centerIn: parent - source: "qrc:/resource/img/lc_gif_stop.svg" - width: 18; height: 18 - fillMode: Image.PreserveAspectFit - smooth: true - layer.enabled: true - layer.effect: null - } - MouseArea { - id: stopArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: O_GifRecordVM.stopRecording() - } - } - - Rectangle { - width: 1; height: 20 - visible: gifBar.isRecording && !gifBar.isEncoding - color: "#E2E8F0" - Layout.alignment: Qt.AlignVCenter - } - - // 录制中:时间 + 帧数 - Item { - visible: gifBar.isRecording && !gifBar.isEncoding - Layout.fillWidth: true - height: 32 - implicitWidth: recRow.implicitWidth + 8 - - Row { - id: recRow - anchors.centerIn: parent - spacing: 6 - - Rectangle { - id: recDot - width: 8; height: 8; radius: 4 - color: "#EF4444" - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: { - var m = Math.floor(gifBar.elapsedSecs / 60) - var s = gifBar.elapsedSecs % 60 - return (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s) - } - font.pixelSize: 13 - font.family: "monospace" - color: "#1E293B" - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: gifBar.frameCount + "f" - font.pixelSize: 11 - color: "#94A3B8" - anchors.verticalCenter: parent.verticalCenter - } - } - } - - // 编码中:进度提示 - Item { - visible: gifBar.isEncoding - Layout.fillWidth: true - height: 32 - implicitWidth: encodingRow.implicitWidth + 16 - - Row { - id: encodingRow - anchors.centerIn: parent - spacing: 8 - - Rectangle { - width: 16; height: 16 - color: "transparent" - anchors.verticalCenter: parent.verticalCenter - Rectangle { - width: 16; height: 16 - radius: 8 - color: "transparent" - border.color: "#3B82F6" - border.width: 2 - Rectangle { - width: 6; height: 6 - color: "#3B82F6" - radius: 3 - x: 5; y: -1 - } - RotationAnimation on rotation { - running: gifBar.isEncoding - from: 0; to: 360 - duration: 800 - loops: Animation.Infinite - } - } - } - - Text { - text: qsTr("正在生成 GIF...") - font.pixelSize: 13 - color: "#1E293B" - anchors.verticalCenter: parent.verticalCenter - } - } - } - } -} diff --git a/qml/TZLable.qml b/qml/TZLable.qml deleted file mode 100644 index 0863d50..0000000 --- a/qml/TZLable.qml +++ /dev/null @@ -1,26 +0,0 @@ -import QtQuick - -// 显示当前截图大小 -Rectangle { - property string lableText: "" - - radius: 6 - color: "#1E293B" - opacity: 0.85 - border.color: "#334155" - border.width: 1 - - implicitWidth: labelContent.implicitWidth + 16 - implicitHeight: labelContent.implicitHeight + 8 - - Text { - id: labelContent - anchors.centerIn: parent - font.pixelSize: 12 - font.family: "Microsoft YaHei" - color: "#F1F5F9" - text: lableText - renderType: Text.NativeRendering - } -} - diff --git a/qml/TZLoadingSpinner.qml b/qml/TZLoadingSpinner.qml deleted file mode 100644 index e4df73f..0000000 --- a/qml/TZLoadingSpinner.qml +++ /dev/null @@ -1,52 +0,0 @@ -import QtQuick - -// ── AI 加载动画层 ───────────────────────────────────── -Rectangle { - - width: 100 - height: 100 - radius: 2 - color: "#99000000" // 半透明黑色遮罩 - - - // 旋转圆弧 - Item { - id: spinnerRoot - anchors.centerIn: parent - width: 48 - height: 48 - - RotationAnimator { - target: spinnerRoot - from: 0; to: 360 - duration: 900 - loops: Animation.Infinite - running: O_AIViewModel.isLoading - } - - // 四个小圆点模拟旋转圆弧 - Repeater { - model: 8 - Rectangle { - width: 6 - height: 6 - radius: 3 - color: "white" - opacity: (index + 1) / 8 - x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2 - y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2 - } - } - } - - // 提示文字 - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: spinnerRoot.bottom - anchors.topMargin: 10 - text: "AI 生成中..." - color: "white" - font.pixelSize: 13 - } -} - diff --git a/qml/TZLongCaptureBar.qml b/qml/TZLongCaptureBar.qml deleted file mode 100644 index d77bc11..0000000 --- a/qml/TZLongCaptureBar.qml +++ /dev/null @@ -1,109 +0,0 @@ -import QtQuick -import QtQuick.Window -import QtQuick.Layouts - -Window { - id: longBar - width: 500 - height: 56 - visible: false - color: "transparent" - flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowDoesNotAcceptFocus - - signal startRequested() - signal stopRequested() - - property bool running: false - property int frameCount: 0 - property string message: qsTr("点击开始后,滑动内容自动拼接") - - Rectangle { - anchors.fill: parent - radius: 12 - color: "#111827" - border.color: "#1F2937" - border.width: 1 - - RowLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 10 - - Rectangle { - width: 26 - height: 26 - radius: 13 - color: "#F97316" - Layout.alignment: Qt.AlignVCenter - Text { - anchors.centerIn: parent - text: "长" - color: "#FFFFFF" - font.pixelSize: 14 - font.bold: true - } - } - - Text { - Layout.fillWidth: true - text: longBar.message - color: "#E5E7EB" - font.pixelSize: 13 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - Text { - text: qsTr("%1 帧").arg(longBar.frameCount) - color: "#D1D5DB" - font.pixelSize: 12 - verticalAlignment: Text.AlignVCenter - } - - Rectangle { - width: 72 - height: 30 - radius: 6 - visible: !longBar.running - color: startArea.containsMouse ? "#1D4ED8" : "#2563EB" - Layout.alignment: Qt.AlignVCenter - Behavior on color { ColorAnimation { duration: 100 } } - Text { - anchors.centerIn: parent - text: qsTr("开始") - color: "#FFFFFF" - font.pixelSize: 13 - } - MouseArea { - id: startArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: longBar.startRequested() - } - } - - Rectangle { - width: 72 - height: 30 - radius: 6 - color: stopArea.containsMouse ? "#DC2626" : "#EF4444" - Layout.alignment: Qt.AlignVCenter - Behavior on color { ColorAnimation { duration: 100 } } - Text { - anchors.centerIn: parent - text: longBar.running ? qsTr("完成") : qsTr("关闭") - color: "#FFFFFF" - font.pixelSize: 13 - } - MouseArea { - id: stopArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: longBar.stopRequested() - } - } - } - } -} diff --git a/qml/TZLongCaptureFrame.qml b/qml/TZLongCaptureFrame.qml deleted file mode 100644 index 24e2739..0000000 --- a/qml/TZLongCaptureFrame.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick -import QtQuick.Window - -Window { - id: longFrame - visible: false - color: "transparent" - flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowTransparentForInput - - property rect captureRect: Qt.rect(0, 0, 0, 0) - - x: captureRect.x - 2 - y: captureRect.y - 2 - width: Math.max(0, captureRect.width) + 4 - height: Math.max(0, captureRect.height) + 4 - - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: "#0EA5E9" - border.width: 2 - radius: 2 - - Rectangle { - x: 0 - y: 0 - width: 6 - height: 6 - color: "#0EA5E9" - } - Rectangle { - x: parent.width - width - y: 0 - width: 6 - height: 6 - color: "#0EA5E9" - } - Rectangle { - x: 0 - y: parent.height - height - width: 6 - height: 6 - color: "#0EA5E9" - } - Rectangle { - x: parent.width - width - y: parent.height - height - width: 6 - height: 6 - color: "#0EA5E9" - } - } -} diff --git a/qml/TZLongCapturePreview.qml b/qml/TZLongCapturePreview.qml deleted file mode 100644 index dcead86..0000000 --- a/qml/TZLongCapturePreview.qml +++ /dev/null @@ -1,82 +0,0 @@ -import QtQuick -import QtQuick.Window - -Window { - id: longPreview - width: 260 - height: 420 - visible: false - color: "transparent" - flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowDoesNotAcceptFocus - - property string previewUrl: "" - property string statusText: "" - property int frameCount: 0 - - Rectangle { - anchors.fill: parent - radius: 12 - color: "#111827" - border.color: "#1F2937" - border.width: 1 - - Column { - anchors.fill: parent - anchors.margins: 10 - spacing: 8 - - Row { - width: parent.width - spacing: 8 - - Text { - text: qsTr("长截图预览") - color: "#F9FAFB" - font.pixelSize: 13 - font.bold: true - } - - Text { - text: qsTr("%1 帧").arg(longPreview.frameCount) - color: "#9CA3AF" - font.pixelSize: 12 - } - } - - Rectangle { - width: parent.width - height: parent.height - 58 - radius: 8 - color: "#0B1220" - border.color: "#334155" - border.width: 1 - clip: true - - Image { - anchors.fill: parent - anchors.margins: 6 - source: longPreview.previewUrl - fillMode: Image.PreserveAspectFit - smooth: true - cache: false - } - - Text { - anchors.centerIn: parent - visible: longPreview.previewUrl.length === 0 - text: qsTr("暂无预览") - color: "#64748B" - font.pixelSize: 12 - } - } - - Text { - width: parent.width - text: longPreview.statusText - color: "#CBD5E1" - font.pixelSize: 12 - elide: Text.ElideRight - } - } - } -} diff --git a/qml/TZMagnifyingGlass.qml b/qml/TZMagnifyingGlass.qml deleted file mode 100644 index 5e92039..0000000 --- a/qml/TZMagnifyingGlass.qml +++ /dev/null @@ -1,116 +0,0 @@ -import QtQuick -import QtQuick.Window - -// ── 放大镜 ─────────────────────────────────────────────── -Rectangle { - id: magnifier - width: 130 - height: 170 // 额外 40px 供颜色信息显示 - z: 10 - color: "transparent" - visible: root.visibility === Window.FullScreen - - // 跟随鼠标移动,始终显示(不随截图状态切换) - property int magnifierMouseX: 0 - property int magnifierMouseY: 0 - property int magnifierAbsX: 0 - property int magnifierAbsY: 0 - property color magnifierPixelColor: "#000000" - - // 跟随鼠标,偏移 +15px,防止被遮挡;超出屏幕时翻转 - x: { - var px = magnifierMouseX + 15 - if (px + width > root.width) px = magnifierMouseX - width - 15 - return px - } - y: { - var py = magnifierMouseY + 15 - if (py + height > root.height) py = magnifierMouseY - height - 15 - return py - } - - // 外框 - Rectangle { - id: magnifierFrame - anchors.top: parent.top - width: 130 - height: 130 - radius: 4 - color: "#1a1a1a" - border.color: "#ffffff" - border.width: 1 - clip: true - - // 放大图像(来自 ImageProvider) - Image { - id: magnifierImage - anchors.centerIn: parent - width: 110 - height: 110 - // cache: false 确保每次坐标变化时重新请求图像 - cache: false - // source 用时间戳 +坐标,强制 QML 重新加载 - source: root.visibility === Window.FullScreen - ? ("image://magnifier/" + magnifierAbsX + "," + magnifierAbsY + "?t=" + Date.now()) - : "" - fillMode: Image.Stretch - } - - // 中心像素颜色色块(8×8,叠在图像中心上方,不遮挡准线) - Rectangle { - anchors.centerIn: parent - width: 8 - height: 8 - color: magnifierPixelColor - border.color: "#ffffff" - border.width: 1 - visible: false // 色块已在 RGB 文本行体现,避免遮挡准线 - } - } - - // 颜色信息栏 - Rectangle { - id: colorInfoBar - anchors.top: magnifierFrame.bottom - anchors.topMargin: 2 - width: 130 - height: 38 - radius: 4 - color: "#cc1a1a1a" - border.color: "#555555" - border.width: 1 - - Row { - anchors.centerIn: parent - spacing: 6 - - // 色块 - Rectangle { - width: 16 - height: 16 - radius: 2 - color: magnifierPixelColor - border.color: "#ffffff" - border.width: 1 - anchors.verticalCenter: parent.verticalCenter - } - - // RGB 文本 - Text { - id: rgbText - anchors.verticalCenter: parent.verticalCenter - color: "#ffffff" - font.pixelSize: 11 - font.family: "monospace" - text: { - var c = magnifierPixelColor - var r = Math.round(c.r * 255) - var g = Math.round(c.g * 255) - var b = Math.round(c.b * 255) - return "R:" + r + " G:" + g + " B:" + b - } - } - } - } -} -// ── 放大镜 end ─────────────────────────────────────────── diff --git a/qml/TZMarkToolInfo.qml b/qml/TZMarkToolInfo.qml deleted file mode 100644 index 5c35099..0000000 --- a/qml/TZMarkToolInfo.qml +++ /dev/null @@ -1,257 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls - -Rectangle { - id: panel - - property color currentColor: "#F43F5E" - property int currentSize: 6 - property bool showColorRow: true - property string toolName: "" - - property string annotationText: qsTr("文本") - property bool textBackgroundEnabled: true - property int mosaicBlurLevel: 2 - property bool numberAutoIncrement: true - property int numberValue: 1 - - width: 260 - height: toolName === "text" ? 128 - : (toolName === "number" ? 132 - : ((toolName === "mosaic") ? 92 : (showColorRow ? 84 : 50))) - radius: 10 - color: "#FFFFFF" - border.color: "#E2E8F0" - border.width: 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 8 - - RowLayout { - spacing: 6 - Layout.fillWidth: true - visible: panel.showColorRow - height: panel.showColorRow ? implicitHeight : 0 - - Repeater { - model: ["#F43F5E","#EF4444","#F97316","#EAB308", - "#22C55E","#3B82F6","#8B5CF6","#000000","#FFFFFF"] - delegate: Rectangle { - width: 18; height: 18; radius: 9 - color: modelData - border.color: panel.currentColor === modelData ? "#3B82F6" : "#CBD5E1" - border.width: panel.currentColor === modelData ? 2 : 1 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onPressed: panel.currentColor = modelData - } - } - } - } - - RowLayout { - spacing: 6 - Layout.fillWidth: true - - Repeater { - model: [ - { size: 2, label: qsTr("细") }, - { size: 4, label: qsTr("中") }, - { size: 6, label: qsTr("粗") }, - { size: 10, label: qsTr("特粗") } - ] - delegate: Rectangle { - width: 48; height: 28 - radius: 6 - color: panel.currentSize === modelData.size ? "#EFF6FF" : (sizeArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: panel.currentSize === modelData.size ? "#3B82F6" : "#E2E8F0" - border.width: 1 - - Column { - anchors.centerIn: parent - spacing: 3 - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - width: 24 - height: Math.min(modelData.size, 6) - radius: height / 2 - color: panel.currentSize === modelData.size ? "#3B82F6" : "#94A3B8" - } - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.label - font.pixelSize: 10 - color: panel.currentSize === modelData.size ? "#3B82F6" : "#64748B" - } - } - - MouseArea { - id: sizeArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: panel.currentSize = modelData.size - } - } - } - } - - RowLayout { - visible: panel.toolName === "text" - Layout.fillWidth: true - spacing: 8 - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 34 - radius: 6 - color: "#F8FAFC" - border.color: textInput.activeFocus ? "#3B82F6" : "#E2E8F0" - border.width: 1 - - TextInput { - id: textInput - anchors.fill: parent - anchors.margins: 8 - verticalAlignment: TextInput.AlignVCenter - color: "#0F172A" - font.pixelSize: 13 - selectByMouse: true - text: panel.annotationText - onTextChanged: panel.annotationText = text - } - - Text { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 8 - visible: textInput.text.length === 0 && !textInput.activeFocus - text: qsTr("输入文字") - color: "#94A3B8" - font.pixelSize: 13 - } - } - - CheckBox { - text: qsTr("背景") - checked: panel.textBackgroundEnabled - onToggled: panel.textBackgroundEnabled = checked - } - } - - RowLayout { - visible: panel.toolName === "mosaic" - Layout.fillWidth: true - spacing: 8 - - Text { - text: qsTr("模糊") - color: "#334155" - } - - Slider { - Layout.fillWidth: true - from: 1 - to: 4 - stepSize: 1 - value: panel.mosaicBlurLevel - onMoved: panel.mosaicBlurLevel = Math.round(value) - } - - Text { - text: panel.mosaicBlurLevel - color: "#64748B" - } - } - - ColumnLayout { - visible: panel.toolName === "number" - Layout.fillWidth: true - spacing: 8 - - CheckBox { - text: qsTr("自动递增") - checked: panel.numberAutoIncrement - onToggled: panel.numberAutoIncrement = checked - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Text { - text: qsTr("数字") - color: "#334155" - } - - Rectangle { - width: 28 - height: 28 - radius: 6 - color: minusArea.containsMouse ? "#F1F5F9" : "transparent" - border.color: "#CBD5E1" - border.width: 1 - - Text { - anchors.centerIn: parent - text: "-" - color: "#334155" - font.pixelSize: 16 - } - - MouseArea { - id: minusArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: panel.numberValue = Math.max(1, panel.numberValue - 1) - } - } - - Rectangle { - Layout.fillWidth: true - height: 28 - radius: 6 - color: "#F8FAFC" - border.color: "#E2E8F0" - border.width: 1 - - Text { - anchors.centerIn: parent - text: panel.numberValue - color: "#0F172A" - font.pixelSize: 13 - } - } - - Rectangle { - width: 28 - height: 28 - radius: 6 - color: plusArea.containsMouse ? "#F1F5F9" : "transparent" - border.color: "#CBD5E1" - border.width: 1 - - Text { - anchors.centerIn: parent - text: "+" - color: "#334155" - font.pixelSize: 16 - } - - MouseArea { - id: plusArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: panel.numberValue = Math.min(9999, panel.numberValue + 1) - } - } - } - } - } -} diff --git a/qml/TZOcrResult.qml b/qml/TZOcrResult.qml deleted file mode 100644 index 624766b..0000000 --- a/qml/TZOcrResult.qml +++ /dev/null @@ -1,328 +0,0 @@ -import QtQuick -import QtQuick.Window -import QtQuick.Controls -import QtQuick.Layouts - -// TZOcrResult -// ----------- -// 无边框悬浮窗,展示 OCR 识别结果。 -// 由 TZWindow.qml 通过 Qt.createComponent 动态创建并显示。 - -Window { - id: ocrWin - flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool - color: "transparent" - width: 420 - height: 320 - - // 初始居中(创建后由调用方设置位置,此处给默认值) - x: (Screen.width - width) / 2 - y: (Screen.height - height) / 2 - - Component.onCompleted: { - ocrWin.raise() - ocrWin.requestActivate() - } - - Shortcut { - sequence: "Esc" - onActivated: ocrWin.close() - } - - // ── 阴影容器 ────────────────────────────────────────── - Rectangle { - id: shadow - anchors.fill: parent - anchors.margins: 10 - radius: 12 - color: "white" - layer.enabled: true - border.color: "#D1D5DB" - border.width: 1 - - // ── 标题栏 ───────────────────────────────────────── - Rectangle { - id: titleBar - anchors.top: shadow.top - anchors.left: shadow.left - anchors.right: shadow.right - height: 40 - radius: 12 - color: "#F8FAFC" - - // 圆角只在上半部分(下方盖住) - Rectangle { - anchors.bottom: titleBar.bottom - anchors.left: titleBar.left - anchors.right: titleBar.right - height: titleBar.radius - color: titleBar.color - } - - // 标题文字 - Text { - anchors.verticalCenter: titleBar.verticalCenter - anchors.left: titleBar.left - anchors.leftMargin: 14 - text: qsTr("OCR 识别结果") - font.pixelSize: 14 - font.weight: Font.Medium - color: "#1E293B" - } - - // 关闭按钮 - Rectangle { - id: closeBtn - anchors.right: titleBar.right - anchors.rightMargin: 8 - anchors.verticalCenter: titleBar.verticalCenter - width: 26; height: 26; radius: 5 - color: closeArea.containsMouse ? "#FEE2E2" : "transparent" - Behavior on color { ColorAnimation { duration: 80 } } - - Text { - anchors.centerIn: parent - text: "✕" - color: closeArea.containsMouse ? "#DC2626" : "#94A3B8" - font.pixelSize: 13 - } - MouseArea { - id: closeArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: ocrWin.close() - } - } - - // 拖动标题栏移动窗口 - MouseArea { - anchors.fill: titleBar - anchors.rightMargin: 40 - property point pressPos - onPressed: function(mouse) { - pressPos = Qt.point(mouse.x, mouse.y) - } - onPositionChanged: function(mouse) { - if (pressed) { - ocrWin.x += mouse.x - pressPos.x - ocrWin.y += mouse.y - pressPos.y - } - } - } - } - - // ── 分割线 ───────────────────────────────────────── - Rectangle { - id: divider - anchors.top: titleBar.bottom - anchors.left: shadow.left - anchors.right: shadow.right - height: 1 - color: "#E2E8F0" - } - - // ── 内容区 ───────────────────────────────────────── - Item { - anchors.top: divider.bottom - anchors.left: shadow.left - anchors.right: shadow.right - anchors.bottom: bottomBar.top - - // Loading 遮罩 - Rectangle { - anchors.fill: parent - color: "#F8FAFC" - visible: O_OcrVM.isRecognizing - radius: 6 - - Item { - id: spinnerRoot - anchors.centerIn: parent - width: 48; height: 48 - - RotationAnimator { - target: spinnerRoot - from: 0; to: 360 - duration: 900 - loops: Animation.Infinite - running: O_OcrVM.isRecognizing - } - Repeater { - model: 8 - Rectangle { - width: 6; height: 6; radius: 3 - color: "#3B82F6" - opacity: (index + 1) / 8 - x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2 - y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2 - } - } - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: spinnerRoot.bottom - anchors.topMargin: 12 - text: qsTr("正在识别中…") - color: "#64748B" - font.pixelSize: 13 - } - } - - // 文本区域(识别完成后显示) - ScrollView { - anchors.fill: parent - anchors.margins: 8 - visible: !O_OcrVM.isRecognizing - clip: true - - TextArea { - id: resultArea - text: O_OcrVM.resultText - readOnly: false // 允许用户二次编辑 - wrapMode: TextEdit.Wrap - font.pixelSize: 14 - color: "#1E293B" - background: null - padding: 4 - selectByMouse: true - placeholderText: qsTr("识别结果将显示在这里…") - placeholderTextColor: "#94A3B8" - } - } - } - - // ── 底部操作栏 ───────────────────────────────────── - Rectangle { - id: bottomBar - anchors.bottom: shadow.bottom - anchors.left: shadow.left - anchors.right: shadow.right - height: 44 - radius: 12 - color: "#F8FAFC" - - // 圆角只在下半部分 - Rectangle { - anchors.top: bottomBar.top - anchors.left: bottomBar.left - anchors.right: bottomBar.right - height: bottomBar.radius - color: bottomBar.color - } - - // 分割线 - Rectangle { - anchors.top: bottomBar.top - anchors.left: bottomBar.left - anchors.right: bottomBar.right - height: 1 - color: "#E2E8F0" - } - - Row { - anchors.centerIn: parent - spacing: 10 - - // 一键复制 - Rectangle { - id: copyBtn - width: 100; height: 30; radius: 6 - color: copyArea.containsMouse ? "#DBEAFE" : "#EFF6FF" - border.color: "#3B82F6" - border.width: 1 - Behavior on color { ColorAnimation { duration: 80 } } - - Text { - anchors.centerIn: parent - text: qsTr("复制全部") - color: "#3B82F6" - font.pixelSize: 13 - } - MouseArea { - id: copyArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - O_OcrVM.copyResultToClipboard() - copyFeedbackTimer.start() - } - } - } - - // 关闭 - Rectangle { - width: 70; height: 30; radius: 6 - color: closeBtn2Area.containsMouse ? "#F1F5F9" : "transparent" - border.color: "#CBD5E1" - border.width: 1 - Behavior on color { ColorAnimation { duration: 80 } } - - Text { - anchors.centerIn: parent - text: qsTr("关闭") - color: "#64748B" - font.pixelSize: 13 - } - MouseArea { - id: closeBtn2Area - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: ocrWin.close() - } - } - } - } - - // 复制成功短暂提示 - Rectangle { - id: copyFeedback - anchors.horizontalCenter: shadow.horizontalCenter - anchors.bottom: bottomBar.top - anchors.bottomMargin: 6 - width: 100; height: 28; radius: 6 - color: "#22C55E" - visible: false - opacity: 0 - - Text { - anchors.centerIn: parent - text: qsTr("已复制!") - color: "white" - font.pixelSize: 12 - } - - Timer { - id: copyFeedbackTimer - interval: 1200 - onTriggered: copyFeedback.visible = false - } - - onVisibleChanged: { - if (visible) opacity = 1 - } - - Behavior on opacity { - NumberAnimation { duration: 300 } - } - } - - Connections { - target: copyFeedbackTimer - function onTriggered() { - copyFeedback.opacity = 0 - } - } - } - - // 启动计时后立即显示反馈 - Connections { - target: copyArea - function onClicked() { - copyFeedback.visible = true - } - } -} diff --git a/qml/TZResizeHandle.qml b/qml/TZResizeHandle.qml deleted file mode 100644 index 91c8ab1..0000000 --- a/qml/TZResizeHandle.qml +++ /dev/null @@ -1,14 +0,0 @@ -import QtQuick - -Rectangle { -// id:leftTop -// x: screenshotArea.x - handleSize/2 -// y: screenshotArea.y - handleSize/2 - property int handleSize: 8 - width: handleSize - height: handleSize - radius: handleSize/2 - color: "#0088FF" - border.color: "white" - border.width: 1 -} diff --git a/qml/TZResizeHandleGroup.qml b/qml/TZResizeHandleGroup.qml deleted file mode 100644 index 088fe77..0000000 --- a/qml/TZResizeHandleGroup.qml +++ /dev/null @@ -1,296 +0,0 @@ -import QtQuick -import QtQuick.Window - - -Rectangle{ - id:handleGroup - property rect area: Qt.rect(0,0,0,0) - property rect initialArea: Qt.rect(0,0,0,0) - property var mousePointStart: Qt.point(0,0) - x:area.x - y:area.y - width: area.width - height: area.height - color: "transparent" - //左上角控制点 - TZResizeHandle - { - id:leftTop - x: - handleSize/2 - y: - handleSize/2 - // 仅当选区宽度/高度非零时显示 - visible: area.width !== 0 || area.height !== 0 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeFDiagCursor - onPressed: { - let globalPos = leftTop.mapToItem(null, mouseX, mouseY); - mousePointStart.x = globalPos.x - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - // 重置所有临时变量 - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = leftTop.mapToItem(null, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - let dy = globalPos.y - mousePointStart.y - area.x = initialArea.x + dx - area.y = initialArea.y + dy - area.width = initialArea.width - dx - area.height = initialArea.height - dy - } - } - } - } - - //正上方控制点 - TZResizeHandle - { - id: top - x: (area.width - handleSize/2)/2 - y: -handleSize/2 - // 仅当选区宽度/高度非零时显示 - visible: area.width !== 0 || area.height !== 0 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeVerCursor - onPressed: { - let globalPos = top.mapToItem(null, mouseX, mouseY); - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - // 重置所有临时变量 - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = top.mapToItem(null, mouseX, mouseY); - let dy = globalPos.y - mousePointStart.y - area.y = initialArea.y + dy - area.height = initialArea.height - dy - } - } - } - } - - //右上角控制点 - TZResizeHandle - { - id:rightTop - x: area.width - handleSize/2 - y: -handleSize/2 - visible: area.width !== 0 || area.height !== 0 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeBDiagCursor - onPressed: { - let globalPos = rightTop.mapToItem(null, mouseX, mouseY); - mousePointStart.x = globalPos.x - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = rightTop.mapToItem(null, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - let dy = globalPos.y - mousePointStart.y - area.y = initialArea.y + dy - area.width = initialArea.width + dx - area.height = initialArea.height - dy - } - } - } - - - } - - //右边控制点 - TZResizeHandle - { - id: right - x: area.width - handleSize/2 - y: (area.height -handleSize/2)/2 - visible: area.width !== 0 || area.height !== 0 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - onPressed: { - let globalPos = right.mapToItem(null, mouseX, mouseY); - mousePointStart.x = globalPos.x - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = right.mapToItem(null, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - area.width = initialArea.width + dx - } - } - } - - - } - - //右下角控制点 - TZResizeHandle - { - id:rightButtom - x: area.width - handleSize/2 - y: area.height - handleSize/2 - visible: area.width !== 0 || area.height !== 0 - z:2 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeFDiagCursor - onPressed: { - let globalPos = rightButtom.mapToItem(null, mouseX, mouseY); - mousePointStart.x = globalPos.x - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = rightButtom.mapToItem(null, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - let dy = globalPos.y - mousePointStart.y - area.width = initialArea.width + dx - area.height = initialArea.height + dy - } - } - } - } - - //正下方控制点 - TZResizeHandle - { - id:buttom - x: (area.width - handleSize/2)/2 - y: area.height - handleSize/2 - visible: area.width !== 0 || area.height !== 0 - z:2 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeVerCursor - onPressed: { - let globalPos = buttom.mapToItem(null, mouseX, mouseY); - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = buttom.mapToItem(null, mouseX, mouseY); - let dy = globalPos.y - mousePointStart.y - area.height = initialArea.height + dy - } - } - } - } - - //左下角控制点 - TZResizeHandle - { - id:leftBottom - x: -handleSize/2 - y: area.height - handleSize/2 - visible: area.width !== 0 || area.height !== 0 - z:2 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeBDiagCursor - onPressed: { - let globalPos = leftBottom.mapToItem(null, mouseX, mouseY); - mousePointStart.x = globalPos.x - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = leftBottom.mapToItem(null, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - let dy = globalPos.y - mousePointStart.y - area.x = initialArea.x + dx - area.width = initialArea.width - dx - area.height = initialArea.height + dy - } - } - } - - } - - //左边控制点 - TZResizeHandle - { - id:left - x: -handleSize/2 - y: (area.height - handleSize/2)/2 - visible: area.width !== 0 || area.height !== 0 - z:2 - MouseArea { - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - onPressed: { - let globalPos = left.mapToItem(null, mouseX, mouseY); - mousePointStart.x = globalPos.x - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = left.mapToItem(null, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - area.x = initialArea.x + dx - area.width = initialArea.width - dx - } - } - } - - } - -} - diff --git a/qml/TZStickyContextMenu.qml b/qml/TZStickyContextMenu.qml deleted file mode 100644 index c374786..0000000 --- a/qml/TZStickyContextMenu.qml +++ /dev/null @@ -1,124 +0,0 @@ -import QtQuick -import Qt.labs.platform 1.0 as Platform -import QtCore -import QtQml - -Item { - id: root - visible: false - - required property var menuPrefs - required property string imageUrl - required property real imgOpacity - required property real zoomFactor - required property bool toolbarVisible - required property url saveFolder - - required property var overwriteWithEdits - required property var copyImageToClipboard - required property var openChatPanel - required property var hideOrToggleToolbar - required property var setOpacityValue - required property var setZoomValue - required property var rotateCurrent - required property var mirrorCurrent - required property var resetDisplaySize - required property var resetWindowPosition - required property var saveOriginalImage - required property var saveMergedTo - required property var closeWindow - - property bool contextMenuOpen: false - - function open() { - contextMenu.open() - } - - Platform.Menu { - id: contextMenu - onAboutToShow: root.contextMenuOpen = true - onAboutToHide: root.contextMenuOpen = false - - Platform.MenuItem { text: qsTr("当前透明度 %1%").arg(Math.round(root.imgOpacity * 100)); enabled: false } - Platform.MenuSeparator {} - - Platform.MenuItem { visible: root.menuPrefs.pinSaveMerged; text: qsTr("覆盖贴图(合并标注)"); onTriggered: root.overwriteWithEdits() } - Platform.MenuItem { visible: root.menuPrefs.pinCopy; text: qsTr("复制贴图"); onTriggered: root.copyImageToClipboard(root.imageUrl) } - Platform.MenuItem { visible: root.menuPrefs.pinAi; text: qsTr("AI 编辑"); onTriggered: root.openChatPanel() } - Platform.MenuItem { - visible: root.menuPrefs.pinToolbar - text: root.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏") - onTriggered: root.hideOrToggleToolbar() - } - Platform.MenuItem { visible: root.menuPrefs.pinClose; text: qsTr("关闭窗口"); onTriggered: root.closeWindow() } - - Platform.MenuSeparator {} - - Platform.MenuItem { text: qsTr("透明度 +5%"); onTriggered: root.setOpacityValue(root.imgOpacity + 0.05) } - Platform.MenuItem { text: qsTr("透明度 -5%"); onTriggered: root.setOpacityValue(root.imgOpacity - 0.05) } - - Platform.MenuSeparator {} - - Platform.MenuItem { text: qsTr("缩小"); onTriggered: root.setZoomValue(root.zoomFactor - 0.1) } - Platform.MenuItem { text: qsTr("放大"); onTriggered: root.setZoomValue(root.zoomFactor + 0.1) } - Platform.MenuItem { text: qsTr("旋转 90°"); onTriggered: root.rotateCurrent(90) } - Platform.MenuItem { text: qsTr("镜像"); onTriggered: root.mirrorCurrent() } - Platform.MenuItem { text: qsTr("恢复原始大小"); onTriggered: root.resetDisplaySize() } - - Platform.MenuSeparator {} - - Platform.MenuItem { - text: qsTr("重置位置") - onTriggered: root.resetWindowPosition() - } - - Platform.MenuSeparator {} - - Platform.MenuItem { - text: root.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏") - onTriggered: root.hideOrToggleToolbar() - } - - Platform.MenuItem { text: qsTr("AI 编辑"); onTriggered: root.openChatPanel() } - - Platform.MenuSeparator {} - - Platform.MenuItem { text: qsTr("保存原图"); onTriggered: saveDialog.open() } - Platform.MenuItem { text: qsTr("另存为(合并标注)"); onTriggered: mergedSaveDialog.open() } - Platform.MenuItem { text: qsTr("覆盖贴图(合并标注)"); onTriggered: root.overwriteWithEdits() } - Platform.MenuItem { text: qsTr("复制贴图"); onTriggered: root.copyImageToClipboard(root.imageUrl) } - - Platform.MenuSeparator {} - - Platform.Menu { - title: qsTr("常用项置顶") - Platform.MenuItem { text: qsTr("覆盖贴图"); checkable: true; checked: root.menuPrefs.pinSaveMerged; onTriggered: root.menuPrefs.pinSaveMerged = !root.menuPrefs.pinSaveMerged } - Platform.MenuItem { text: qsTr("复制贴图"); checkable: true; checked: root.menuPrefs.pinCopy; onTriggered: root.menuPrefs.pinCopy = !root.menuPrefs.pinCopy } - Platform.MenuItem { text: qsTr("AI 编辑"); checkable: true; checked: root.menuPrefs.pinAi; onTriggered: root.menuPrefs.pinAi = !root.menuPrefs.pinAi } - Platform.MenuItem { text: qsTr("显示/隐藏工具栏"); checkable: true; checked: root.menuPrefs.pinToolbar; onTriggered: root.menuPrefs.pinToolbar = !root.menuPrefs.pinToolbar } - Platform.MenuItem { text: qsTr("关闭窗口"); checkable: true; checked: root.menuPrefs.pinClose; onTriggered: root.menuPrefs.pinClose = !root.menuPrefs.pinClose } - } - - Platform.MenuSeparator {} - - Platform.MenuItem { text: qsTr("关闭窗口"); onTriggered: root.closeWindow() } - } - - Platform.FileDialog { - id: saveDialog - title: qsTr("选择保存位置") - folder: root.saveFolder - fileMode: Platform.FileDialog.SaveFile - nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"] - onAccepted: root.saveOriginalImage(root.imageUrl, saveDialog.file) - } - - Platform.FileDialog { - id: mergedSaveDialog - title: qsTr("另存为(合并标注)") - folder: root.saveFolder - fileMode: Platform.FileDialog.SaveFile - nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"] - onAccepted: root.saveMergedTo(mergedSaveDialog.file) - } -} diff --git a/qml/TZStickyWindow.qml b/qml/TZStickyWindow.qml deleted file mode 100644 index 9bd3ca7..0000000 --- a/qml/TZStickyWindow.qml +++ /dev/null @@ -1,747 +0,0 @@ -import QtQuick -import QtQuick.Window -import Qt.labs.platform 1.0 as Platform -import QtQuick.Effects -import QtQuick.Controls.Basic -import QtCore -import CustomComponents 1.0 - -Window { - id: stickyWin - flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool - color: "transparent" - - readonly property real minOpacity: 0.1 - readonly property real maxOpacity: 1.0 - readonly property real minZoomFactor: 0.3 - readonly property real maxZoomFactor: 4.0 - readonly property int toolbarButtonSize: 30 - readonly property int toolbarIconSize: 18 - readonly property color toolbarHoverColor: "#F1F5F9" - readonly property color toolbarActiveColor: "#EFF6FF" - readonly property color toolbarActiveBorderColor: "#3B82F6" - - property string imageUrl: "" - property rect imgRect: Qt.rect(0, 0, 0, 0) - property bool contentActive: true - property bool cleanedUp: false - - property real imgOpacity: 1.0 - property string activeTool: "" - property bool toolbarVisible: true - property string toolbarHoverTipText: "" - property real toolbarHoverTipCenterX: 0 - property bool contextMenuOpen: stickyContextMenu.contextMenuOpen - - property int imageRevision: 0 - property real zoomFactor: 1.0 - property real dpiScale: 1.0 - property size sourceImageSize: Qt.size(1, 1) - property int imageLogicalWidth: 1 - property int imageLogicalHeight: 1 - - readonly property int toolbarHeight: 40 - readonly property int toolbarGap: 8 - readonly property int toolbarTipGap: 6 - readonly property int toolbarTipHeight: 24 - readonly property int shadowRadius: 12 - readonly property int shadowOffset: 4 - readonly property int imageLeft: imgRect.width >= 0 ? imgRect.x : imgRect.x + imgRect.width - readonly property int imageTop: imgRect.height >= 0 ? imgRect.y : imgRect.y + imgRect.height - - readonly property int displayW: Math.max(1, Math.round(imageLogicalWidth * zoomFactor)) - readonly property int displayH: Math.max(1, Math.round(imageLogicalHeight * zoomFactor)) - readonly property int toolbarContentWidth: Math.max(560, stickyRow.implicitWidth + 16) - readonly property string displaySourceUrl: { - if (typeof stickyWin.imageUrl !== "string" || stickyWin.imageUrl.length === 0) - return "" - return stickyWin.imageUrl + "?rev=" + stickyWin.imageRevision - } - readonly property var stickyShapeMap: ({ - pencil: 0, - rect: 1, - circle: 2, - arrow: 3, - mosaic: 4 - }) - readonly property var drawToolButtons: [ - { tool: "pencil", tip: qsTr("Pencil"), icon: "qrc:/resource/img/lc_pencil.svg" }, - { tool: "rect", tip: qsTr("Rectangle"), icon: "qrc:/resource/img/lc_square.svg" }, - { tool: "circle", tip: qsTr("Circle"), icon: "qrc:/resource/img/lc_circle.svg" }, - { tool: "arrow", tip: qsTr("Arrow"), icon: "qrc:/resource/img/lc_arrow.svg" }, - { tool: "mosaic", tip: qsTr("Mosaic"), icon: "qrc:/resource/img/lc_mosaic.svg" } - ] - readonly property var zoomActionButtons: [ - { action: "zoomOut", tip: qsTr("Zoom out"), text: "-", textSize: 16 }, - { action: "zoomIn", tip: qsTr("Zoom in"), text: "+", textSize: 16 } - ] - readonly property var transformActionButtons: [ - { action: "rotate", tip: qsTr("Rotate 90°"), text: "↻", textSize: 14 }, - { action: "mirror", tip: qsTr("Mirror"), text: "⇋", textSize: 14 }, - { action: "resetSize", tip: qsTr("Reset size"), text: "1:1", textSize: 10 } - ] - readonly property var editActionButtons: [ - { action: "doneAndCopy", tip: qsTr("Done and copy"), icon: "qrc:/resource/img/lc_check.svg", hoverColor: "#E0F2FE" }, - { action: "undo", tip: qsTr("Undo"), icon: "qrc:/resource/img/lc_undo.svg", hoverColor: toolbarActiveColor }, - { action: "hideToolbar", tip: qsTr("Hide toolbar"), icon: "qrc:/resource/img/lc_x.svg", hoverColor: "#FEE2E2" } - ] - - readonly property int stickyShapeType: { - return stickyShapeMap[activeTool] !== undefined ? stickyShapeMap[activeTool] : 1 - } - - width: Math.max(displayW + shadowRadius * 2, toolbarContentWidth + 16) - height: displayH + shadowRadius * 2 + shadowOffset + toolbarGap + toolbarHeight + toolbarTipGap + toolbarTipHeight + 4 - x: imageLeft - shadowRadius - y: imageTop - shadowRadius - - Settings { - id: menuPrefs - category: "sticky_menu" - property bool pinSaveMerged: true - property bool pinCopy: true - property bool pinAi: false - property bool pinToolbar: false - property bool pinClose: false - } - - function setActiveTool(name) { - if (activeTool === name) - activeTool = "" - else - activeTool = name - } - - function clamp(value, minValue, maxValue) { - return Math.max(minValue, Math.min(maxValue, value)) - } - - function hasValidImage(image) { - return image && image.width > 0 && image.height > 0 - } - - function clearActiveTool() { - stickyWin.activeTool = "" - } - - function setToolbarVisible(visible) { - stickyWin.toolbarVisible = visible - if (!visible) - clearActiveTool() - } - - function toggleToolbarVisible() { - setToolbarVisible(!stickyWin.toolbarVisible) - } - - function showToolbarTip(text, item) { - toolbarHoverTipText = text - if (item) { - var p = stickyToolbar.mapFromItem(item, item.width / 2, item.height) - toolbarHoverTipCenterX = p.x - } else { - toolbarHoverTipCenterX = stickyToolbar.width / 2 - } - } - - function hideToolbarTip(text) { - if (text === undefined || toolbarHoverTipText === text) - toolbarHoverTipText = "" - } - - function setOpacityValue(v) { - stickyWin.imgOpacity = clamp(v, minOpacity, maxOpacity) - } - - function setZoomValue(v) { - zoomFactor = clamp(v, minZoomFactor, maxZoomFactor) - } - - function resetDisplaySize() { - zoomFactor = 1.0 - } - - function updateImageMetricsFromStore() { - var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - if (hasValidImage(img)) { - stickyPaintBoard.setBackgroundImg(img) - } else { - syncPaintboardBackgroundFromDisplay() - } - } - - function syncPaintboardBackgroundFromDisplay() { - if (!stickyWin.visible || !stickyWin.contentActive) - return - if (stickyImage.status !== Image.Ready) - return - stickyImage.grabToImage(function(result) { - if (!hasValidImage(result && result.image)) - return - stickyPaintBoard.setBackgroundImg(result.image) - }, Qt.size(Math.max(1, Math.abs(imgRect.width)), Math.max(1, Math.abs(imgRect.height)))) - } - - function refreshAfterImageChanged() { - imageRevision += 1 - stickyPaintBoard.reset() - updateImageMetricsFromStore() - } - - function renderAnnotationLayer() { - return stickyPaintBoard.renderToImage() - } - - function overwriteWithEdits() { - var layer = renderAnnotationLayer() - var ok = O_StickyViewModel.overwriteWithAnnotations(stickyWin.imageUrl, layer) - if (ok) - refreshAfterImageChanged() - } - - function rotateCurrent(degrees) { - if (O_StickyViewModel.rotateImage(stickyWin.imageUrl, degrees)) - refreshAfterImageChanged() - } - - function mirrorCurrent() { - if (O_StickyViewModel.mirrorImage(stickyWin.imageUrl, true, false)) - refreshAfterImageChanged() - } - - function saveMergedTo(pathUrl) { - var layer = renderAnnotationLayer() - O_StickyViewModel.saveMergedImage(stickyWin.imageUrl, layer, pathUrl) - } - - function completeEditsAndCopy() { - var layer = renderAnnotationLayer() - if (hasValidImage(layer)) - overwriteWithEdits() - O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl) - setToolbarVisible(false) - } - - function ensureMosaicBackground() { - var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - if (hasValidImage(img)) - stickyPaintBoard.setBackgroundImg(img) - - setActiveTool("mosaic") - if (stickyWin.activeTool !== "mosaic" || stickyPaintBoard.hasBackgroundImg()) - return - - Qt.callLater(function() { - var retryImg = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - if (hasValidImage(retryImg)) - stickyPaintBoard.setBackgroundImg(retryImg) - if (!stickyPaintBoard.hasBackgroundImg()) - stickyWin.syncPaintboardBackgroundFromDisplay() - }) - } - - function handleToolbarAction(action) { - switch (action) { - case "zoomOut": - setZoomValue(stickyWin.zoomFactor - 0.1) - break - case "zoomIn": - setZoomValue(stickyWin.zoomFactor + 0.1) - break - case "rotate": - rotateCurrent(90) - break - case "mirror": - mirrorCurrent() - break - case "resetSize": - resetDisplaySize() - break - case "doneAndCopy": - completeEditsAndCopy() - break - case "undo": - stickyPaintBoard.undo() - break - case "hideToolbar": - setToolbarVisible(false) - break - case "ocr": - var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - O_OcrVM.recognize(img) - break - } - } - - function handleDrawTool(tool) { - if (tool === "mosaic") { - ensureMosaicBackground() - return - } - setActiveTool(tool) - } - - function resetWindowPosition() { - stickyWin.x = stickyWin.imageLeft - stickyWin.shadowRadius - stickyWin.y = stickyWin.imageTop - stickyWin.shadowRadius - } - - function cleanup() { - if (cleanedUp) - return - cleanedUp = true - - stickyWin.visible = false - contentActive = false - stickyWin.releaseResources() - O_StickyViewModel.releaseImage(imageUrl) - imageUrl = "" - } - - function updateDpiScale() { - dpiScale = (screen && screen.devicePixelRatio > 0) ? screen.devicePixelRatio : 1.0 - } - - function updateImageLogicalSize() { - sourceImageSize = O_StickyViewModel.getImageSizeByUrl(stickyWin.imageUrl) - if (sourceImageSize.width > 0 && sourceImageSize.height > 0) { - imageLogicalWidth = Math.max(1, Math.round(sourceImageSize.width / dpiScale)) - imageLogicalHeight = Math.max(1, Math.round(sourceImageSize.height / dpiScale)) - } - } - - Component.onCompleted: { - updateDpiScale() - updateImageMetricsFromStore() - updateImageLogicalSize() - stickyWin.raise() - stickyWin.requestActivate() - } - - Shortcut { - sequence: "Esc" - onActivated: { - if (stickyWin.activeTool !== "") { - clearActiveTool() - return - } - if (stickyWin.toolbarVisible) { - setToolbarVisible(false) - return - } - stickyWin.close() - } - } - - onImageUrlChanged: { - imageRevision = 0 - sourceImageSize = Qt.size(1, 1) - imageLogicalWidth = 1 - imageLogicalHeight = 1 - resetDisplaySize() - refreshAfterImageChanged() - } - - onActiveToolChanged: { - if (activeTool === "mosaic") - updateImageMetricsFromStore() - } - - onScreenChanged: { - updateDpiScale() - updateImageLogicalSize() - } - - Component.onDestruction: cleanup() - - function onClosing(close) { - close.accepted = true - chatPanel.close() - cleanup() - Qt.callLater(function() { stickyWin.destroy() }) - } - - Rectangle { - id: imgContainer - visible: stickyWin.contentActive - z: 10 - x: stickyWin.shadowRadius - y: stickyWin.shadowRadius + stickyWin.shadowOffset / 2 - width: stickyWin.displayW - height: stickyWin.displayH - radius: 2 - clip: true - color: "transparent" - - Item { - id: visualStage - anchors.centerIn: parent - width: stickyWin.imageLogicalWidth - height: stickyWin.imageLogicalHeight - scale: stickyWin.zoomFactor - - Image { - id: stickyImage - anchors.fill: parent - source: stickyWin.displaySourceUrl - fillMode: Image.Stretch - opacity: stickyWin.imgOpacity - cache: false - onStatusChanged: { - if (status === Image.Ready) - stickyWin.updateImageLogicalSize() - if (status === Image.Ready && stickyWin.activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg()) - stickyWin.syncPaintboardBackgroundFromDisplay() - } - } - - PaintBoard { - id: stickyPaintBoard - anchors.fill: parent - z: 20 - visible: stickyWin.contentActive - enabled: stickyWin.activeTool !== "" - shapeType: stickyWin.stickyShapeType - penColor: stickyMarkTool.currentColor - penSize: stickyMarkTool.currentSize - mosaicBlurLevel: stickyMarkTool.mosaicBlurLevel - } - } - } - - MultiEffect { - visible: stickyWin.contentActive - z: 5 - source: stickyImage - anchors.fill: imgContainer - shadowEnabled: true - shadowColor: "#55000000" - shadowBlur: stickyWin.shadowRadius / 20.0 - shadowVerticalOffset: stickyWin.shadowOffset - shadowHorizontalOffset: 0 - opacity: stickyWin.imgOpacity - } - - TZLoadingSpinner { - anchors.centerIn: imgContainer - width: stickyWin.displayW - height: stickyWin.displayH - visible: O_AIViewModel.isLoading && stickyWin.contentActive - } - - TZMarkToolInfo { - id: stickyMarkTool - z: 32 - visible: stickyWin.toolbarVisible && stickyWin.activeTool !== "" - toolName: stickyWin.activeTool - showColorRow: stickyWin.activeTool !== "mosaic" - x: Math.max(8, stickyToolbar.x) - y: stickyToolbar.y - height - 6 - } - - Rectangle { - id: stickyToolbar - z: 30 - visible: stickyWin.toolbarVisible - width: stickyWin.toolbarContentWidth - height: stickyWin.toolbarHeight - radius: 10 - color: "#FFFFFF" - border.color: "#E2E8F0" - border.width: 1 - x: Math.max(8, Math.min(stickyWin.width - width - 8, stickyWin.shadowRadius + (stickyWin.displayW - width) / 2)) - y: stickyWin.shadowRadius + stickyWin.shadowOffset / 2 + stickyWin.displayH + stickyWin.toolbarGap - - Row { - id: stickyRow - anchors.centerIn: parent - spacing: 6 - - Rectangle { - width: stickyWin.toolbarButtonSize; height: stickyWin.toolbarButtonSize; radius: 7 - color: ocrArea.containsMouse ? "#F0FDF4" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: stickyWin.toolbarIconSize; height: stickyWin.toolbarIconSize } - MouseArea { - id: ocrArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: stickyWin.showToolbarTip(qsTr("OCR"), parent) - onExited: stickyWin.hideToolbarTip(qsTr("OCR")) - onPressed: stickyWin.handleToolbarAction("ocr") - } - } - - Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter } - - Repeater { - model: stickyWin.drawToolButtons - - delegate: Rectangle { - property var buttonData: modelData - width: stickyWin.toolbarButtonSize - height: stickyWin.toolbarButtonSize - radius: 7 - color: stickyWin.activeTool === buttonData.tool ? stickyWin.toolbarActiveColor : (hoverArea.containsMouse ? stickyWin.toolbarHoverColor : "transparent") - border.color: stickyWin.activeTool === buttonData.tool ? stickyWin.toolbarActiveBorderColor : "transparent" - border.width: 1 - - Image { - anchors.centerIn: parent - source: buttonData.icon - width: stickyWin.toolbarIconSize - height: stickyWin.toolbarIconSize - } - - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: stickyWin.showToolbarTip(parent.buttonData.tip, parent) - onExited: stickyWin.hideToolbarTip(parent.buttonData.tip) - onPressed: stickyWin.handleDrawTool(parent.buttonData.tool) - } - } - } - - Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter } - - Repeater { - model: [stickyWin.zoomActionButtons[0]] - - delegate: Rectangle { - property var buttonData: modelData - width: stickyWin.toolbarButtonSize - height: stickyWin.toolbarButtonSize - radius: 7 - color: hoverArea.containsMouse ? stickyWin.toolbarHoverColor : "transparent" - - Text { - anchors.centerIn: parent - text: parent.buttonData.text - color: "#334155" - font.pixelSize: parent.buttonData.textSize - } - - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: stickyWin.showToolbarTip(parent.buttonData.tip, parent) - onExited: stickyWin.hideToolbarTip(parent.buttonData.tip) - onPressed: stickyWin.handleToolbarAction(parent.buttonData.action) - } - } - } - - Rectangle { width: 40; height: 30; radius: 7; color: "transparent"; Text { anchors.centerIn: parent; text: Math.round(stickyWin.zoomFactor * 100) + "%"; color: "#64748B"; font.pixelSize: 11 } } - - Repeater { - model: [stickyWin.zoomActionButtons[1]].concat(stickyWin.transformActionButtons) - - delegate: Rectangle { - property var buttonData: modelData - width: stickyWin.toolbarButtonSize - height: stickyWin.toolbarButtonSize - radius: 7 - color: hoverArea.containsMouse ? stickyWin.toolbarHoverColor : "transparent" - - Text { - anchors.centerIn: parent - text: parent.buttonData.text - color: "#334155" - font.pixelSize: parent.buttonData.textSize - } - - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: stickyWin.showToolbarTip(parent.buttonData.tip, parent) - onExited: stickyWin.hideToolbarTip(parent.buttonData.tip) - onPressed: stickyWin.handleToolbarAction(parent.buttonData.action) - } - } - } - - Repeater { - model: stickyWin.editActionButtons - - delegate: Rectangle { - property var buttonData: modelData - width: stickyWin.toolbarButtonSize - height: stickyWin.toolbarButtonSize - radius: 7 - color: hoverArea.containsMouse ? buttonData.hoverColor : "transparent" - - Image { - anchors.centerIn: parent - source: buttonData.icon - width: stickyWin.toolbarIconSize - height: stickyWin.toolbarIconSize - } - - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: stickyWin.showToolbarTip(parent.buttonData.tip, parent) - onExited: stickyWin.hideToolbarTip(parent.buttonData.tip) - onPressed: stickyWin.handleToolbarAction(parent.buttonData.action) - } - } - } - } - } - - Rectangle { - id: stickyToolbarTip - parent: stickyWin.contentItem - visible: stickyWin.toolbarVisible && stickyWin.toolbarHoverTipText.length > 0 - z: 40 - width: Math.min(stickyWin.width - 8, Math.max(64, stickyToolbarTipText.implicitWidth + 16)) - height: 24 - radius: 6 - color: "#0F172A" - opacity: 0.92 - x: Math.max(4, Math.min(stickyWin.width - width - 4, stickyToolbar.x + stickyWin.toolbarHoverTipCenterX - width / 2)) - y: Math.min(stickyWin.height - height - 4, stickyToolbar.y + stickyToolbar.height + stickyWin.toolbarTipGap) - - Text { - id: stickyToolbarTipText - anchors.centerIn: parent - text: stickyWin.toolbarHoverTipText - color: "#FFFFFF" - font.pixelSize: 11 - } - } - - readonly property int chatPanelGap: 6 - - TZChat { - id: chatPanel - imageUrl: stickyWin.imageUrl - onSignalSendPrompt: function(text) { - O_AIViewModel.sendPrompt(text, stickyWin.imageUrl) - } - } - - Connections { - target: O_AIViewModel - function onSignalRequestComplete(oldImageUrl, newImageUrl) { - if (oldImageUrl !== stickyWin.imageUrl) - return - var old = oldImageUrl - stickyWin.imageUrl = newImageUrl - Qt.callLater(function() { O_StickyViewModel.releaseImage(old) }) - } - function onSignalRequestFailed(errorMsg) { - console.warn("[TZStickyWindow] AI request failed: " + errorMsg) - } - } - - function openChatPanel() { - var imgLeft = stickyWin.x + stickyWin.shadowRadius - var imgTop = stickyWin.y + stickyWin.shadowRadius - var imgBottom = imgTop + stickyWin.displayH - - var screenH = Screen.desktopAvailableHeight - var screenW = Screen.desktopAvailableWidth - - var cx = imgLeft - if (cx + chatPanel.win.width > screenW) - cx = imgLeft + stickyWin.displayW - chatPanel.win.width - - var cy - if (imgBottom + chatPanelGap + chatPanel.win.height <= screenH) - cy = imgBottom + chatPanelGap - else - cy = imgTop - chatPanelGap - chatPanel.win.height - - chatPanel.open(Math.max(0, cx), Math.max(0, cy)) - } - - TZStickyContextMenu { - id: stickyContextMenu - menuPrefs: menuPrefs - imageUrl: stickyWin.imageUrl - imgOpacity: stickyWin.imgOpacity - zoomFactor: stickyWin.zoomFactor - toolbarVisible: stickyWin.toolbarVisible - saveFolder: O_ImageSaver.savePath - overwriteWithEdits: function() { stickyWin.overwriteWithEdits() } - copyImageToClipboard: function(url) { O_StickyViewModel.copyImageToClipboard(url) } - openChatPanel: function() { stickyWin.openChatPanel() } - hideOrToggleToolbar: function() { stickyWin.toggleToolbarVisible() } - setOpacityValue: function(value) { stickyWin.setOpacityValue(value) } - setZoomValue: function(value) { stickyWin.setZoomValue(value) } - rotateCurrent: function(degrees) { stickyWin.rotateCurrent(degrees) } - mirrorCurrent: function() { stickyWin.mirrorCurrent() } - resetDisplaySize: function() { stickyWin.resetDisplaySize() } - resetWindowPosition: function() { stickyWin.resetWindowPosition() } - saveOriginalImage: function(url, pathUrl) { O_StickyViewModel.saveImage(url, pathUrl) } - saveMergedTo: function(pathUrl) { stickyWin.saveMergedTo(pathUrl) } - closeWindow: function() { stickyWin.close() } - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - enabled: stickyWin.activeTool === "" && !stickyWin.contextMenuOpen - property point dragPos: Qt.point(0, 0) - property bool leftDragActive: false - z: 0 - - function inImageArea(mx, my) { - return mx >= stickyWin.shadowRadius - && mx <= stickyWin.shadowRadius + stickyWin.displayW - && my >= stickyWin.shadowRadius + stickyWin.shadowOffset / 2 - && my <= stickyWin.shadowRadius + stickyWin.shadowOffset / 2 + stickyWin.displayH - } - - onPressed: function(mouse) { - if (mouse.button === Qt.LeftButton) { - dragPos = Qt.point(mouseX, mouseY) - leftDragActive = true - } else { - leftDragActive = false - } - } - - onPositionChanged: function(mouse) { - if (pressed && leftDragActive && (mouse.buttons & Qt.LeftButton) && stickyWin.activeTool === "" && !stickyWin.contextMenuOpen) { - stickyWin.x += mouseX - dragPos.x - stickyWin.y += mouseY - dragPos.y - } - } - - onReleased: function(mouse) { - if (!(mouse.buttons & Qt.LeftButton)) - leftDragActive = false - } - - onDoubleClicked: function(mouse) { - if (mouse.button === Qt.LeftButton && inImageArea(mouseX, mouseY)) - resetDisplaySize() - } - - onClicked: function(mouse) { - if (mouse.button === Qt.RightButton) { - leftDragActive = false - stickyContextMenu.open() - } - } - - onWheel: function(wheel) { - if (wheel.angleDelta.y > 0) - setZoomValue(stickyWin.zoomFactor + 0.1) - else if (wheel.angleDelta.y < 0) - setZoomValue(stickyWin.zoomFactor - 0.1) - wheel.accepted = true - } - } -} diff --git a/qml/TZTool.qml b/qml/TZTool.qml deleted file mode 100644 index 6482025..0000000 --- a/qml/TZTool.qml +++ /dev/null @@ -1,230 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.5 - -Rectangle { - id: tool - radius: 10 - color: "#FFFFFF" - visible: false - border.color: "#E2E8F0" - border.width: 1 - layer.enabled: true - - signal signalScreenshot - signal signalCancel - signal signalSave - signal signalSticky - signal signalUndo - signal signalGifRecord - signal signalLongScreenshot - signal signalOcr - - property string activeTool: "" - property bool markToolInfoVisible: false - property string hoverTipText: "" - property real hoverTipCenterX: width / 2 - - readonly property int shapeType: { - switch (activeTool) { - case "pencil": return 0 - case "rect": return 1 - case "circle": return 2 - case "arrow": return 3 - case "mosaic": return 4 - case "text": return 5 - case "number": return 6 - case "highlight": return 7 - default: return 1 - } - } - - function setActiveTool(name) { - if (activeTool === name) { - activeTool = "" - markToolInfoVisible = false - } else { - activeTool = name - markToolInfoVisible = true - } - } - - function showTip(text, targetItem) { - hoverTipText = text - if (targetItem) { - var p = tool.mapFromItem(targetItem, targetItem.width / 2, targetItem.height) - hoverTipCenterX = p.x - } else { - hoverTipCenterX = width / 2 - } - } - - function hideTip(text) { - if (text === undefined || hoverTipText === text) - hoverTipText = "" - } - - RowLayout { - anchors.fill: parent - anchors.margins: 6 - layoutDirection: Qt.RightToLeft - spacing: 2 - - Rectangle { - width: 32; height: 32; radius: 7 - color: checkArea.containsMouse ? "#DCFCE7" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 } - MouseArea { id: checkArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("完成"), parent); onExited: tool.hideTip(qsTr("完成")); onPressed: signalScreenshot() } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: closeArea.containsMouse ? "#FEE2E2" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 } - MouseArea { id: closeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("取消"), parent); onExited: tool.hideTip(qsTr("取消")); onPressed: signalCancel() } - } - - Rectangle { width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } - - Rectangle { - width: 32; height: 32; radius: 7 - color: ocrArea.containsMouse ? "#F0FDF4" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: 18; height: 18 } - MouseArea { id: ocrArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("OCR"), parent); onExited: tool.hideTip(qsTr("OCR")); onPressed: signalOcr() } - } - - Rectangle { - id: gifRecordBtn - width: 32; height: 32; radius: 7 - color: gifArea.containsMouse ? "#FFF7ED" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: 18; height: 18 } - MouseArea { id: gifArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("录制GIF"), parent); onExited: tool.hideTip(qsTr("录制GIF")); onPressed: signalGifRecord() } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: longShotArea.containsMouse ? "#FFF7ED" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_longshot.svg"; width: 18; height: 18 } - MouseArea { id: longShotArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("长截图"), parent); onExited: tool.hideTip(qsTr("长截图")); onPressed: signalLongScreenshot() } - } - - Rectangle { width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } - - Rectangle { - width: 32; height: 32; radius: 7 - color: saveArea.containsMouse ? "#EFF6FF" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: 18; height: 18 } - MouseArea { id: saveArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("保存"), parent); onExited: tool.hideTip(qsTr("保存")); onPressed: signalSave() } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: fixArea.containsMouse ? "#EFF6FF" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: 18; height: 18 } - MouseArea { id: fixArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("贴图"), parent); onExited: tool.hideTip(qsTr("贴图")); onPressed: signalSticky() } - } - - Rectangle { width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "rect") ? "#EFF6FF" : (rectArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "rect") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: 18; height: 18 } - MouseArea { id: rectArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("矩形"), parent); onExited: tool.hideTip(qsTr("矩形")); onPressed: tool.setActiveTool("rect") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "circle") ? "#EFF6FF" : (circleArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "circle") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: 18; height: 18 } - MouseArea { id: circleArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("圆形"), parent); onExited: tool.hideTip(qsTr("圆形")); onPressed: tool.setActiveTool("circle") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "arrow") ? "#EFF6FF" : (arrowArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "arrow") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: 18; height: 18 } - MouseArea { id: arrowArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("箭头"), parent); onExited: tool.hideTip(qsTr("箭头")); onPressed: tool.setActiveTool("arrow") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "pencil") ? "#EFF6FF" : (pencilArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "pencil") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: 18; height: 18 } - MouseArea { id: pencilArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("画笔"), parent); onExited: tool.hideTip(qsTr("画笔")); onPressed: tool.setActiveTool("pencil") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "highlight") ? "#EFF6FF" : (highlightArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "highlight") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_highlighter.svg"; width: 18; height: 18 } - MouseArea { id: highlightArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("高亮"), parent); onExited: tool.hideTip(qsTr("高亮")); onPressed: tool.setActiveTool("highlight") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "mosaic") ? "#EFF6FF" : (mosaicArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "mosaic") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: 18; height: 18 } - MouseArea { id: mosaicArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("马赛克"), parent); onExited: tool.hideTip(qsTr("马赛克")); onPressed: tool.setActiveTool("mosaic") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "text") ? "#EFF6FF" : (textArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "text") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_text.svg"; width: 18; height: 18 } - MouseArea { id: textArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("文字"), parent); onExited: tool.hideTip(qsTr("文字")); onPressed: tool.setActiveTool("text") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: (tool.activeTool === "number") ? "#EFF6FF" : (numberArea.containsMouse ? "#F1F5F9" : "transparent") - border.color: (tool.activeTool === "number") ? "#3B82F6" : "transparent" - border.width: 1 - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_list_ordered.svg"; width: 18; height: 18 } - MouseArea { id: numberArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("序号"), parent); onExited: tool.hideTip(qsTr("序号")); onPressed: tool.setActiveTool("number") } - } - - Rectangle { - width: 32; height: 32; radius: 7 - color: undoArea.containsMouse ? "#EFF6FF" : "transparent" - Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: 18; height: 18 } - MouseArea { id: undoArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("撤销"), parent); onExited: tool.hideTip(qsTr("撤销")); onPressed: signalUndo() } - } - } - - Rectangle { - id: tipBubble - visible: tool.visible && tool.hoverTipText.length > 0 - z: 100 - parent: tool.parent - width: Math.max(44, tipText.implicitWidth + 14) - height: 24 - radius: 6 - color: "#0F172A" - opacity: 0.92 - x: Math.max(tool.x, Math.min(tool.x + tool.width - width, tool.x + tool.hoverTipCenterX - width / 2)) - y: tool.y + tool.height + 6 - - Text { - id: tipText - anchors.centerIn: parent - text: tool.hoverTipText - color: "#FFFFFF" - font.pixelSize: 11 - } - } -} diff --git a/qml/TZWindow.qml b/qml/TZWindow.qml deleted file mode 100644 index 6b8a86e..0000000 --- a/qml/TZWindow.qml +++ /dev/null @@ -1,1054 +0,0 @@ -import QtQuick -import QtQuick.Window -import QtQuick.Shapes -import QtQuick.Layouts -import QtQuick.Dialogs -import CustomComponents 1.0 - -Window { - id: mainWin - color: "transparent"; - - // Virtual desktop offset injected by main.qml - property int screenVirtualX: 0 - property int screenVirtualY: 0 - - property rect initialScreenshotArea: Qt.rect(0, 0, 0, 0) - property rect screenshotArea: Qt.rect(0, 0, 0, 0) - property var offset: Qt.point(0, 0) - property string screenShotUrl: "" - - property rect highlightRect: Qt.rect(0, 0, 0, 0) - property point pendingTextPoint: Qt.point(0, 0) - property string pendingTextValue: "" - property bool captureUiReady: false - - property var stickyWinObj: null - property var stickyWinList: [] - property var stickyComponent: null - property bool gifRecordMode: false - readonly property bool gifAreaLocked: O_GifRecordVM.isRecording || O_GifRecordVM.isEncoding - property var ocrComponent: null - property var ocrWinList: [] - property bool spaceHeld: false - property bool hotkeyGestureEnabled: false - property bool hotkeyGestureArmed: false - property bool hotkeyLongPressed: false - property string hotkeyMainToken: "" - property bool textClickPending: false - property bool textPressMoved: false - property point textClickAbsPoint: Qt.point(0, 0) - property point textClickLocalPoint: Qt.point(0, 0) - property string dragMode: "none" // none | select | move - - property alias tool: tool - property alias paintBoard: paintBoard - property alias magnifier: magnifier - property alias gifBarObj: gifBar - - function hasSelectionArea() { - return Math.abs(screenshotArea.width) > 2 && Math.abs(screenshotArea.height) > 2 - } - - function parseMainKeyToken(shortcutSeq) { - if (!shortcutSeq || shortcutSeq.length === 0) - return "" - var parts = shortcutSeq.split("+") - if (parts.length === 0) - return "" - return parts[parts.length - 1].trim().toUpperCase() - } - - function keyMatchesToken(key, token) { - if (!token || token.length === 0) - return false - if (token.length === 1) { - if (token >= "A" && token <= "Z") - return key === (Qt.Key_A + (token.charCodeAt(0) - "A".charCodeAt(0))) - if (token >= "0" && token <= "9") - return key === (Qt.Key_0 + (token.charCodeAt(0) - "0".charCodeAt(0))) - } - if (token[0] === "F") { - var fn = Number(token.slice(1)) - if (fn >= 1 && fn <= 24) - return key === (Qt.Key_F1 + fn - 1) - } - if (token === "SPACE") - return key === Qt.Key_Space - if (token === "ESC" || token === "ESCAPE") - return key === Qt.Key_Escape - if (token === "ENTER" || token === "RETURN") - return key === Qt.Key_Return || key === Qt.Key_Enter - return false - } - - function beginHotkeyGesture(shortcutSeq) { - hotkeyGestureEnabled = true - hotkeyGestureArmed = false - hotkeyLongPressed = false - hotkeyMainToken = parseMainKeyToken(shortcutSeq) - hotkeyLongPressTimer.stop() - } - - function clearHotkeyGesture() { - hotkeyGestureEnabled = false - hotkeyGestureArmed = false - hotkeyLongPressed = false - hotkeyMainToken = "" - hotkeyLongPressTimer.stop() - } - - function maybeArmHotkeyGesture() { - if (!hotkeyGestureEnabled || !hasSelectionArea()) - return - hotkeyGestureArmed = true - hotkeyLongPressed = false - hotkeyLongPressTimer.restart() - } - - function selectionRectFromDrag(startX, startY, endX, endY, withShift, withAlt) { - var x = startX - var y = startY - var w = endX - startX - var h = endY - startY - - if (withAlt) { - x = startX - (endX - startX) - y = startY - (endY - startY) - w = (endX - startX) * 2 - h = (endY - startY) * 2 - } - - if (withShift) { - var absW = Math.abs(w) - var absH = Math.abs(h) - var side = Math.min(absW, absH) - w = w < 0 ? -side : side - h = h < 0 ? -side : side - } - - return Qt.rect(x, y, w, h) - } - - function confirmSelectionToClipboard() { - root.visibility = Window.Hidden - Qt.callLater(function() { - var success = O_ScreenCapture.captureRectToClipboard(paintBoard, screenshotArea) - resetArea() - if (success) - trayHelper.showMessage(qsTr("Capture done"), qsTr("Copied to clipboard")) - else - trayHelper.showMessage(qsTr("Capture failed"), qsTr("Failed to copy image")) - }) - } - - function handleEscAction() { - if (textEditor.visible) { - hideTextEditor() - return true - } - - if (ocrWinList.length > 0) { - var topWin = ocrWinList[ocrWinList.length - 1] - if (topWin) - topWin.close() - return true - } - - if (gifRecordMode) { - O_GifRecordVM.cancelRecording() - gifBar.visible = false - gifRecordMode = false - if (hasSelectionArea()) - tool.visible = true - return true - } - - if (tool.activeTool !== "") { - tool.activeTool = "" - return true - } - - if (tool.markToolInfoVisible) { - tool.markToolInfoVisible = false - return true - } - - if (tool.visible) { - tool.visible = false - return true - } - - if (hasSelectionArea()) { - resetArea() - return true - } - - return false - } - - function handleMousePressed(ax, ay) { - if (screenshotArea.width !== 0 || screenshotArea.height !== 0) { - if (isPointInRect(screenshotArea, ax, ay)) { - dragMode = "move" - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - } else { - dragMode = "select" - paintBoard.reset() - tool.activeTool = "" - tool.markToolInfoVisible = false - tool.visible = false - resizeHandleGroup.visible = false - hideTextEditor() - var winRect = O_ScreenCapture.windowAtPoint(ax, ay) - highlightRect = winRect.width > 0 - ? Qt.rect(winRect.x, winRect.y, winRect.width, winRect.height) - : Qt.rect(0, 0, 0, 0) - initialScreenshotArea = Qt.rect(ax, ay, 0, 0) - screenshotArea = Qt.rect(ax, ay, 0, 0) - } - } else { - dragMode = "select" - paintBoard.reset() - tool.activeTool = "" - initialScreenshotArea = Qt.rect(ax, ay, 0, 0) - screenshotArea = Qt.rect(ax, ay, 0, 0) - } - } - - function handleMouseMoved(ax, ay, isPressed, modifiers) { - if(isPressed) - resizeHandleGroup.visible = false; - var c = O_ScreenCapture.getPixelColor(ax, ay) - if (c !== undefined && c !== null) - magnifier.magnifierPixelColor = c - - if (!isPressed && screenshotArea.width === 0 && screenshotArea.height === 0) { - var winRect = O_ScreenCapture.windowAtPoint(ax, ay) - highlightRect = winRect.width > 0 - ? Qt.rect(winRect.x, winRect.y, winRect.width, winRect.height) - : Qt.rect(0, 0, 0, 0) - return - } - if (!isPressed) { - return - } - - if (dragMode === "move") { - let dx = ax - offset.x - let dy = ay - offset.y - screenshotArea = Qt.rect( - initialScreenshotArea.x + dx, - initialScreenshotArea.y + dy, - initialScreenshotArea.width, initialScreenshotArea.height) - return - } - - if (dragMode === "select") { - var useShift = (modifiers & Qt.ShiftModifier) !== 0 - var useAlt = (modifiers & Qt.AltModifier) !== 0 - screenshotArea = selectionRectFromDrag( - initialScreenshotArea.x, initialScreenshotArea.y, ax, ay, useShift, useAlt) - if (tool.visible) tool.visible = false - } - } - - function handleMouseReleased() { - dragMode = "none" - if (Math.abs(screenshotArea.width) <= 5 && Math.abs(screenshotArea.height) <= 5) { - if (highlightRect.width > 0 && highlightRect.height > 0) { - screenshotArea = highlightRect - initialScreenshotArea = highlightRect - resizeHandleGroup.area = localScreenshotArea - resizeHandleGroup.initialArea = Qt.rect( - initialScreenshotArea.x - screenVirtualX, - initialScreenshotArea.y - screenVirtualY, - initialScreenshotArea.width, initialScreenshotArea.height) - highlightRect = Qt.rect(0, 0, 0, 0) - Qt.callLater(function() { tool.visible = true }) - } - resizeHandleGroup.visible = (highlightRect.width > 0 && highlightRect.height > 0) - return - } - var bounds = getNormalizedRectBounds(screenshotArea) - if (bounds.isValid) { - screenshotArea = Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height) - } - initialScreenshotArea = screenshotArea - resizeHandleGroup.initialArea = Qt.rect( - initialScreenshotArea.x - screenVirtualX, - initialScreenshotArea.y - screenVirtualY, - initialScreenshotArea.width, initialScreenshotArea.height) - if (Math.abs(screenshotArea.width) > 2 && Math.abs(screenshotArea.height) > 2) { - Qt.callLater(function() { tool.visible = true }) - } - resizeHandleGroup.area = localScreenshotArea - resizeHandleGroup.visible = true; - - } - // 本地坐标 -> 虚拟桌面绝对坐标 - function toAbsX(localX) { return localX + screenVirtualX } - function toAbsY(localY) { return localY + screenVirtualY } - - // screenshotArea 在本窗口本地坐标中的等价矩形(用于绘制) - readonly property rect localScreenshotArea: Qt.rect( - screenshotArea.x - screenVirtualX, - screenshotArea.y - screenVirtualY, - screenshotArea.width, - screenshotArea.height - ) - readonly property rect localNormalizedScreenshotArea: { - let b = getNormalizedRectBounds(localScreenshotArea) - return b.isValid ? Qt.rect(b.left, b.top, b.width, b.height) : Qt.rect(0, 0, 0, 0) - } - readonly property rect localHighlightRect: Qt.rect( - highlightRect.x - screenVirtualX, - highlightRect.y - screenVirtualY, - highlightRect.width, - highlightRect.height - ) - - function getNormalizedRectBounds(rect) { - if (rect.width === 0 || rect.height === 0) { - return { left: 0, right: 0, top: 0, bottom: 0, isValid: false }; - } - let left = Math.min(rect.x, rect.x + rect.width); - let right = Math.max(rect.x, rect.x + rect.width); - let top = Math.min(rect.y, rect.y + rect.height); - let bottom = Math.max(rect.y, rect.y + rect.height); - return { left: left, right: right, top: top, bottom: bottom, - width: right - left, height: bottom - top, isValid: true }; - } - - function isPointInRect(rect, x, y) { - let bounds = getNormalizedRectBounds(rect); - if (!bounds.isValid) return false; - return (x >= bounds.left && x <= bounds.right) - && (y >= bounds.top && y <= bounds.bottom); - } - - function createStickyWindow(imageUrl, imgRect) { - if (!imageUrl || imageUrl.length === 0) { - console.error("Failed to create sticky window: invalid image URL") - return - } - if (stickyComponent === null) - stickyComponent = Qt.createComponent("qrc:/qml/TZStickyWindow.qml"); - - var newWinObj = null; - if (stickyComponent.status === Component.Ready) { - newWinObj = stickyComponent.createObject(null, { - "imageUrl": imageUrl, "imgRect": imgRect }); - } else if (stickyComponent.status === Component.Error) { - console.error("Failed to load TZStickyWindow.qml:", stickyComponent.errorString()); - } - if (newWinObj) { - newWinObj.visible = true; - newWinObj.raise() - newWinObj.requestActivate() - var newList = stickyWinList.slice(); - newList.push(newWinObj); - stickyWinList = newList; - var winRef = newWinObj; - function onWinClosing(closeEvent) { - var idx = stickyWinList.indexOf(winRef); - if (idx !== -1) { - var updatedList = stickyWinList.slice(); - updatedList.splice(idx, 1); - stickyWinList = updatedList; - } - winRef.closing.disconnect(onWinClosing); - winRef = null; - } - newWinObj.closing.connect(onWinClosing); - } else { - console.error("Failed to create sticky window object"); - } - } - - function createOcrResultWindow() { - if (ocrWinList.length > 0) { - var existing = ocrWinList[ocrWinList.length - 1] - if (existing) { - existing.show() - existing.raise() - existing.requestActivate() - return - } - } - - if (ocrComponent === null) - ocrComponent = Qt.createComponent("qrc:/qml/TZOcrResult.qml") - if (ocrComponent.status === Component.Ready) { - var win = ocrComponent.createObject(null) - if (win) { - win.show() - win.raise() - win.requestActivate() - var list = ocrWinList.slice() - list.push(win) - ocrWinList = list - var winRef = win - function onOcrClosing(closeEvent) { - var idx = ocrWinList.indexOf(winRef) - if (idx !== -1) { - var updatedList = ocrWinList.slice() - updatedList.splice(idx, 1) - ocrWinList = updatedList - } - winRef.closing.disconnect(onOcrClosing) - winRef = null - } - win.closing.connect(onOcrClosing) - } - } else if (ocrComponent.status === Component.Error) { - console.error("Failed to load TZOcrResult.qml:", ocrComponent.errorString()) - } - } - - function openTextEditor(localX, localY) { - pendingTextPoint = Qt.point(localX, localY) - pendingTextValue = markToolInfo.annotationText - textInput.text = pendingTextValue - textEditor.visible = true - textEditor.x = Math.max(0, Math.min(localX, mainWin.width - textEditor.width)) - textEditor.y = Math.max(0, Math.min(localY + 8, mainWin.height - textEditor.height)) - textInput.forceActiveFocus() - textInput.selectAll() - } - - function hideTextEditor() { - textEditor.visible = false - } - - function submitTextEditor() { - var text = textInput.text.trim() - if (text.length === 0) { - hideTextEditor() - return - } - markToolInfo.annotationText = text - paintBoard.addTextAnnotation(Math.round(pendingTextPoint.x), Math.round(pendingTextPoint.y), text) - hideTextEditor() - } - - function resetArea() { - screenshotArea = Qt.rect(0, 0, 0, 0) - initialScreenshotArea = Qt.rect(0, 0, 0, 0) - resizeHandleGroup.area = Qt.rect(0, 0, 0, 0) - resizeHandleGroup.initialArea = Qt.rect(0, 0, 0, 0) - gifResizeHandleGroup.area = Qt.rect(0, 0, 0, 0) - gifResizeHandleGroup.initialArea = Qt.rect(0, 0, 0, 0) - screenShotUrl = "" - paintBoard.reset() - tool.visible = false - tool.activeTool = "" - tool.markToolInfoVisible = false - hideTextEditor() - gifRecordMode = false - gifBar.visible = false - highlightRect = Qt.rect(0, 0, 0, 0) - textClickPending = false - textPressMoved = false - textClickAbsPoint = Qt.point(0, 0) - textClickLocalPoint = Qt.point(0, 0) - dragMode = "none" - clearHotkeyGesture() - O_ScreenCapture.releaseDesktopSnapshot() - root.visibility = Window.Hidden; - } - - Timer { - id: hotkeyLongPressTimer - interval: 280 - repeat: false - onTriggered: { - if (hotkeyGestureArmed && hasSelectionArea()) { - hotkeyLongPressed = true - tool.visible = true - } - } - } - - FileDialog { - id: saveDialog - title: qsTr("Select save location") - currentFolder: O_ImageSaver.savePath - fileMode: FileDialog.SaveFile - nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"] - onAccepted: { - var success = O_ImageSaver.saveImage("data:image/png;base64,"+screenShotUrl, saveDialog.selectedFile) - if (success) { - console.log("Save success:", saveDialog.currentFolder) - trayHelper.showMessage(qsTr("Save success"), qsTr("Screenshot saved to file")) - } else { - console.log("Save failed") - trayHelper.showMessage(qsTr("Save failed"), qsTr("Failed to save screenshot file")) - } - resetArea() - } - } - - TZGifRecordBar { - id: gifBar - width: 260 - height: 44 - visible: false - z: 20 - captureRect: screenshotArea - x: { - let areaLeft = localNormalizedScreenshotArea.x - let preferX = areaLeft + localNormalizedScreenshotArea.width - width - return Math.max(0, Math.min(preferX, mainWin.width - width)) - } - y: { - let areaTop = localNormalizedScreenshotArea.y - let belowY = areaTop + localNormalizedScreenshotArea.height + 6 - return belowY + height > mainWin.height ? areaTop - height - 6 : belowY - } - onSignalCancel: { - gifBar.visible = false - gifRecordMode = false - tool.visible = true - } - onSignalRecordFinished: { - resetArea() - } - } - - Rectangle { - id: recordingBorder - visible: O_GifRecordVM.isRecording - // Draw border outside capture rect so it won't be included in recorded frames. - x: localNormalizedScreenshotArea.x - 2 - y: localNormalizedScreenshotArea.y - 2 - width: localNormalizedScreenshotArea.width + 4 - height: localNormalizedScreenshotArea.height + 4 - color: "transparent" - border.color: "#EF4444" - border.width: 2 - z: 10 - } - - PaintBoard { - id: paintBoard - anchors.fill: parent - z: 1 - enabled: tool.activeTool !== "" && tool.activeTool !== "text" - visible: captureUiReady && screenshotArea.width !== 0 && screenshotArea.height !== 0 - shapeType: tool.shapeType - penColor: markToolInfo.currentColor - penSize: markToolInfo.currentSize - annotationText: markToolInfo.annotationText - textBackgroundEnabled: markToolInfo.textBackgroundEnabled - mosaicBlurLevel: markToolInfo.mosaicBlurLevel - numberAutoIncrement: markToolInfo.numberAutoIncrement - numberValue: markToolInfo.numberValue - } - - Connections { - target: paintBoard - function onNumberValueChanged(value) { - markToolInfo.numberValue = value - } - } - - Rectangle { - id: screenshotRect - anchors.fill: parent - color: "transparent"; - focus: true - z: 2 - visible: captureUiReady && !gifRecordMode - - Shape { - id: shape - ShapePath { - id: path - strokeWidth: -1 - fillColor: "#99000000" - startX: 0; startY: 0 - // 外框(整个窗口) - PathLine { x: mainWin.width; y: 0 } - PathLine { x: mainWin.width; y: mainWin.height } - PathLine { x: 0; y: mainWin.height } - PathLine { x: 0; y: 0 } - // 镂空(选区,本地坐标) - PathMove { - x: localNormalizedScreenshotArea.x - y: localNormalizedScreenshotArea.y - } - PathLine { - x: localNormalizedScreenshotArea.x + localNormalizedScreenshotArea.width - y: localNormalizedScreenshotArea.y - } - PathLine { - x: localNormalizedScreenshotArea.x + localNormalizedScreenshotArea.width - y: localNormalizedScreenshotArea.y + localNormalizedScreenshotArea.height - } - PathLine { - x: localNormalizedScreenshotArea.x - y: localNormalizedScreenshotArea.y + localNormalizedScreenshotArea.height - } - PathLine { - x: localNormalizedScreenshotArea.x - y: localNormalizedScreenshotArea.y - } - } - } - - TZLable { - width: 80; height: 20 - visible: screenshotArea.width !== 0 && screenshotArea.height !== 0 - x: localNormalizedScreenshotArea.x - y: localNormalizedScreenshotArea.y - 25 - lableText: qsTr("%1 * %2").arg(Math.abs(screenshotArea.width)).arg(Math.abs(screenshotArea.height)) - } - - TZTool { - id: tool - width: 656; height: 44 - z: 30 - - // Toolbar position (local coordinates) - x: { - let areaLeft = localNormalizedScreenshotArea.x - let preferX = areaLeft + localNormalizedScreenshotArea.width - width - return Math.max(0, Math.min(preferX, mainWin.width - width)) - } - y: { - let areaTop = localNormalizedScreenshotArea.y - let belowY = areaTop + localNormalizedScreenshotArea.height + 6 - return belowY + height > mainWin.height ? areaTop - height - 6 : belowY - } - - onActiveToolChanged: { - if (activeTool !== "text" && textEditor.visible) { - hideTextEditor() - } - - if (activeTool === "mosaic" && !paintBoard.hasBackgroundImg()) { - Qt.callLater(function() { - O_ScreenCapture.grabToPaintBoard( - paintBoard, - Qt.rect(screenVirtualX, screenVirtualY, mainWin.width, mainWin.height) - ) - }) - } - } - - onSignalScreenshot: { - confirmSelectionToClipboard() - } - - onSignalCancel: { resetArea() } - - onSignalSave: { - root.visibility = Window.Hidden - Qt.callLater(function() { - screenShotUrl = O_ScreenCapture.captureRectToBase64(paintBoard, screenshotArea) - root.visibility = Window.Windowed - saveDialog.open() - }) - } - - onSignalSticky: { - root.visibility = Window.Hidden - Qt.callLater(function() { - var url = O_ScreenCapture.captureRectToStickyUrl(paintBoard, screenshotArea) - O_StickyViewModel.requestSticky(url, screenshotArea) - resetArea() - }) - } - - onSignalUndo: { paintBoard.undo() } - - onSignalGifRecord: { - gifRecordMode = true - tool.visible = false - gifBar.visible = true - Qt.callLater(function() { - O_GifRecordVM.startRecording( - screenshotArea.x, screenshotArea.y, - screenshotArea.width, screenshotArea.height - ) - }) - } - - onSignalLongScreenshot: { - var captureRect = screenshotArea - root.beginLongCapture(captureRect) - } - - onSignalOcr: { - var captureRect = screenshotArea - Qt.callLater(function() { - var img = O_ScreenCapture.captureRectToImage(captureRect) - O_OcrVM.recognize(img) - }) - } - } - - TZMarkToolInfo { - id: markToolInfo - visible: tool.visible && tool.markToolInfoVisible - z: 30 - x: tool.x - y: tool.y + tool.height - toolName: tool.activeTool - showColorRow: tool.activeTool !== "mosaic" - } - - Rectangle { - id: textEditor - visible: false - width: 260 - height: 88 - radius: 8 - color: "#FFFFFF" - border.color: "#CBD5E1" - border.width: 1 - z: 40 - - Column { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - - Rectangle { - width: parent.width - height: 34 - radius: 6 - color: "#F8FAFC" - border.color: textInput.activeFocus ? "#3B82F6" : "#E2E8F0" - border.width: 1 - - TextInput { - id: textInput - anchors.fill: parent - anchors.margins: 8 - verticalAlignment: TextInput.AlignVCenter - color: "#0F172A" - font.pixelSize: 13 - selectByMouse: true - text: pendingTextValue - onAccepted: submitTextEditor() - } - } - - Row { - spacing: 8 - - Rectangle { - width: 72 - height: 28 - radius: 6 - color: cancelInputArea.containsMouse ? "#F1F5F9" : "transparent" - border.color: "#CBD5E1" - border.width: 1 - - Text { - anchors.centerIn: parent - text: qsTr("Cancel") - color: "#334155" - font.pixelSize: 12 - } - - MouseArea { - id: cancelInputArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: hideTextEditor() - } - } - - Rectangle { - width: 72 - height: 28 - radius: 6 - color: confirmInputArea.containsMouse ? "#2563EB" : "#3B82F6" - - Text { - anchors.centerIn: parent - text: qsTr("OK") - color: "#FFFFFF" - font.pixelSize: 12 - } - - MouseArea { - id: confirmInputArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: submitTextEditor() - } - } - } - } - } - - TZResizeHandleGroup { - id: resizeHandleGroup - // Handle control points use local coordinates - area: localScreenshotArea - initialArea: Qt.rect( - initialScreenshotArea.x - screenVirtualX, - initialScreenshotArea.y - screenVirtualY, - initialScreenshotArea.width, - initialScreenshotArea.height) - z: 2 - onAreaChanged: { - // 转回绝对坐标 - screenshotArea = Qt.rect( - area.x + screenVirtualX, - area.y + screenVirtualY, - area.width, area.height) - } - onInitialAreaChanged: { - initialScreenshotArea = Qt.rect( - initialArea.x + screenVirtualX, - initialArea.y + screenVirtualY, - initialArea.width, initialArea.height) - } - } - } - - TZResizeHandleGroup { - id: gifResizeHandleGroup - area: localScreenshotArea - initialArea: Qt.rect( - initialScreenshotArea.x - screenVirtualX, - initialScreenshotArea.y - screenVirtualY, - initialScreenshotArea.width, - initialScreenshotArea.height) - visible: gifRecordMode && !gifAreaLocked - enabled: !gifAreaLocked - z: 15 - onAreaChanged: { - screenshotArea = Qt.rect( - area.x + screenVirtualX, - area.y + screenVirtualY, - area.width, area.height) - O_GifRecordVM.updateCaptureRect( - screenshotArea.x, screenshotArea.y, - screenshotArea.width, screenshotArea.height - ) - } - onInitialAreaChanged: { - initialScreenshotArea = Qt.rect( - initialArea.x + screenVirtualX, - initialArea.y + screenVirtualY, - initialArea.width, initialArea.height) - } - } - - // Window highlight rectangle (local coordinates) - Rectangle { - id: windowHighlight - visible: captureUiReady && screenshotArea.width === 0 && screenshotArea.height === 0 - && highlightRect.width > 0 && highlightRect.height > 0 - x: localHighlightRect.x - y: localHighlightRect.y - width: localHighlightRect.width - height: localHighlightRect.height - color: "transparent" - border.color: "#3B82F6" - border.width: 2 - z: 5 - - Rectangle { - anchors.fill: parent - color: "#153B82F6" - } - - Repeater { - model: [ - { ax: 0, ay: 0, bx: 1, by: 0, cx: 0, cy: 1 }, - { ax: parent.width, ay: 0, bx:-1, by: 0, cx: 0, cy: 1 }, - { ax: 0, ay: parent.height, bx: 1, by: 0, cx: 0, cy:-1 }, - { ax: parent.width, ay: parent.height, bx:-1, by: 0, cx: 0, cy:-1 } - ] - delegate: Item { - x: modelData.ax - (modelData.bx > 0 ? 0 : 3) - y: modelData.ay - (modelData.cy > 0 ? 0 : 3) - Rectangle { x:0; y:0; width:12; height:3; color:"#3B82F6"; transform: Scale { xScale: modelData.bx } } - Rectangle { x:0; y:0; width:3; height:12; color:"#3B82F6"; transform: Scale { yScale: modelData.cy } } - } - } - } - - TZMagnifyingGlass { - id: magnifier - width: 130 - height: 170 - z: 3 - color: "transparent" - // Hide magnifier in GIF mode to avoid recording it - visible: captureUiReady && mainWin.visible && !tool.visible && !gifRecordMode - } - - MouseArea { - id: mouseArea - anchors.fill: parent - enabled: captureUiReady - hoverEnabled: true - focus: true - cursorShape: gifRecordMode - ? (gifAreaLocked ? Qt.ArrowCursor : Qt.SizeAllCursor) - : Qt.CrossCursor - - onPressed: function(mouse) { - var ax = toAbsX(mouseX), ay = toAbsY(mouseY) - textClickPending = false - textPressMoved = false - if (gifRecordMode) { - if (gifAreaLocked) { - return - } - if (isPointInRect(screenshotArea, ax, ay)) { - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - } - return - } - if (spaceHeld && hasSelectionArea()) { - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - return - } - if (tool.activeTool === "text" && screenshotArea.width !== 0 && screenshotArea.height !== 0 - && isPointInRect(screenshotArea, ax, ay)) { - textClickPending = true - textPressMoved = false - textClickAbsPoint = Qt.point(ax, ay) - textClickLocalPoint = Qt.point(mouseX, mouseY) - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - return - } - if (textEditor.visible) { - hideTextEditor() - } - handleMousePressed(ax, ay) - } - - onPositionChanged: function(mouse) { - var ax = toAbsX(mouseX), ay = toAbsY(mouseY) - if (pressed && textClickPending) { - var tdx = ax - textClickAbsPoint.x - var tdy = ay - textClickAbsPoint.y - if (Math.abs(tdx) > 3 || Math.abs(tdy) > 3) { - textPressMoved = true - textClickPending = false - if (textEditor.visible) - hideTextEditor() - } else { - return - } - } - if (gifRecordMode) { - if (gifAreaLocked) { - return - } - if (pressed && isPointInRect(screenshotArea, ax, ay)) { - var dx = ax - offset.x - var dy = ay - offset.y - screenshotArea = Qt.rect( - initialScreenshotArea.x + dx, - initialScreenshotArea.y + dy, - screenshotArea.width, screenshotArea.height) - O_GifRecordVM.updateCaptureRect( - screenshotArea.x, screenshotArea.y, - screenshotArea.width, screenshotArea.height - ) - } - return - } - if (pressed && spaceHeld && hasSelectionArea()) { - var sdx = ax - offset.x - var sdy = ay - offset.y - screenshotArea = Qt.rect( - initialScreenshotArea.x + sdx, - initialScreenshotArea.y + sdy, - screenshotArea.width, screenshotArea.height) - return - } - // Magnifier follows mouse in local coordinates - magnifier.magnifierMouseX = mouseX - magnifier.magnifierMouseY = mouseY - magnifier.magnifierAbsX = ax - magnifier.magnifierAbsY = ay - handleMouseMoved(ax, ay, pressed, mouse.modifiers) - } - - onReleased: function(mouse) { - if (textClickPending && !textPressMoved) { - openTextEditor(textClickLocalPoint.x, textClickLocalPoint.y) - textClickPending = false - textPressMoved = false - return - } - textClickPending = false - textPressMoved = false - if (gifRecordMode) { - if (gifAreaLocked) { - return - } - initialScreenshotArea = screenshotArea - gifResizeHandleGroup.initialArea = Qt.rect( - initialScreenshotArea.x - screenVirtualX, - initialScreenshotArea.y - screenVirtualY, - initialScreenshotArea.width, initialScreenshotArea.height) - return - } - handleMouseReleased() - } - - onWheel: function(wheel) { wheel.accepted = true } - - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Space) { - spaceHeld = true - event.accepted = true - return - } - if (event.key === Qt.Key_Escape) { - if (handleEscAction()) - event.accepted = true - return - } - } - - Keys.onReleased: function(event) { - if (event.key === Qt.Key_Space) { - spaceHeld = false - event.accepted = true - return - } - - if (hotkeyGestureArmed && keyMatchesToken(event.key, hotkeyMainToken)) { - hotkeyLongPressTimer.stop() - if (!hotkeyLongPressed && hasSelectionArea()) - confirmSelectionToClipboard() - clearHotkeyGesture() - event.accepted = true - } - } - } - onVisibilityChanged: - { - if(root.visibility === Window.Hidden) { - resizeHandleGroup.visible = false; - gifResizeHandleGroup.visible = false; - } - } - -} - - diff --git a/qml/Windows/TZChat.qml b/qml/Windows/TZChat.qml index 6833c91..52ee5ad 100644 --- a/qml/Windows/TZChat.qml +++ b/qml/Windows/TZChat.qml @@ -12,7 +12,9 @@ QtObject { signal signalSendPrompt(var text) // 公开方法 - function open(px, py) { + function open(px, py, targetScreen) { + if (targetScreen) + win.screen = targetScreen win.x = px win.y = py win.visible = true @@ -56,14 +58,10 @@ QtObject { id: dragHandle anchors.fill: parent z: -1 - property point startPos - onPressed: startPos = Qt.point(mouseX, mouseY) - onPositionChanged: { - if (pressed) { - win.x += mouseX - startPos.x - win.y += mouseY - startPos.y - } + onPressed: function(mouse) { + mouse.accepted = true + win.startSystemMove() } cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor } @@ -187,6 +185,8 @@ QtObject { Rectangle { Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: 140 + Layout.preferredHeight: 180 color: "#F8FAFC" radius: 8 border.color: "#E2E8F0" @@ -259,7 +259,53 @@ QtObject { } } } + + Rectangle { + id: resizeHandle + width: 16 + height: 16 + radius: 8 + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: 8 + anchors.bottomMargin: 8 + color: resizeMouse.pressed ? "#0369A1" : "#0EA5E9" + border.color: "white" + border.width: 1 + z: 10 + + Canvas { + anchors.fill: parent + anchors.margins: 3 + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.strokeStyle = "white" + ctx.lineWidth = 1.2 + + ctx.beginPath() + ctx.moveTo(width * 0.25, height) + ctx.lineTo(width, height * 0.25) + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(width * 0.55, height) + ctx.lineTo(width, height * 0.55) + ctx.stroke() + } + } + + MouseArea { + id: resizeMouse + anchors.fill: parent + cursorShape: Qt.SizeFDiagCursor + + onPressed: function(mouse) { + mouse.accepted = true + win.startSystemResize(Qt.RightEdge | Qt.BottomEdge) + } + } + } } } } - diff --git a/qml/Windows/TZLoadingSpinner.qml b/qml/Windows/TZLoadingSpinner.qml deleted file mode 100644 index e4df73f..0000000 --- a/qml/Windows/TZLoadingSpinner.qml +++ /dev/null @@ -1,52 +0,0 @@ -import QtQuick - -// ── AI 加载动画层 ───────────────────────────────────── -Rectangle { - - width: 100 - height: 100 - radius: 2 - color: "#99000000" // 半透明黑色遮罩 - - - // 旋转圆弧 - Item { - id: spinnerRoot - anchors.centerIn: parent - width: 48 - height: 48 - - RotationAnimator { - target: spinnerRoot - from: 0; to: 360 - duration: 900 - loops: Animation.Infinite - running: O_AIViewModel.isLoading - } - - // 四个小圆点模拟旋转圆弧 - Repeater { - model: 8 - Rectangle { - width: 6 - height: 6 - radius: 3 - color: "white" - opacity: (index + 1) / 8 - x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2 - y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2 - } - } - } - - // 提示文字 - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: spinnerRoot.bottom - anchors.topMargin: 10 - text: "AI 生成中..." - color: "white" - font.pixelSize: 13 - } -} - diff --git a/qml/Windows/TZStickyExtraToolBar.qml b/qml/Windows/TZStickyExtraToolBar.qml deleted file mode 100644 index cef7b88..0000000 --- a/qml/Windows/TZStickyExtraToolBar.qml +++ /dev/null @@ -1,40 +0,0 @@ -import QtQuick - -Rectangle { - id: bar - radius: 10 - color: "#FFFFFF" - border.color: "#E2E8F0" - border.width: 1 - - signal zoomOut() - signal zoomIn() - signal rotate90() - signal mirror() - signal resetSize() - signal doneAndCopy() - signal hideToolbar() - signal hoverEntered(string text, Item targetItem) - signal hoverExited(string text) - - property real zoomFactor: 1.0 - - width: extraRow.implicitWidth + 16 - height: 40 - - Row { - id: extraRow - anchors.centerIn: parent - spacing: 6 - - Rectangle { width: 30; height: 30; radius: 7; color: zoomOutArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "-"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomOutArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Zoom out"), parent); onExited: bar.hoverExited(qsTr("Zoom out")); onPressed: bar.zoomOut() } } - Rectangle { width: 40; height: 30; radius: 7; color: "transparent"; Text { anchors.centerIn: parent; text: Math.round(bar.zoomFactor * 100) + "%"; color: "#64748B"; font.pixelSize: 11 } } - Rectangle { width: 30; height: 30; radius: 7; color: zoomInArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "+"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomInArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Zoom in"), parent); onExited: bar.hoverExited(qsTr("Zoom in")); onPressed: bar.zoomIn() } } - Rectangle { width: 30; height: 30; radius: 7; color: rotateArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "↻"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: rotateArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Rotate 90°"), parent); onExited: bar.hoverExited(qsTr("Rotate 90°")); onPressed: bar.rotate90() } } - Rectangle { width: 30; height: 30; radius: 7; color: mirrorArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "⇋"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: mirrorArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Mirror"), parent); onExited: bar.hoverExited(qsTr("Mirror")); onPressed: bar.mirror() } } - Rectangle { width: 30; height: 30; radius: 7; color: resetSizeArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "1:1"; color: "#334155"; font.pixelSize: 10 } MouseArea { id: resetSizeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Reset size"), parent); onExited: bar.hoverExited(qsTr("Reset size")); onPressed: bar.resetSize() } } - Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter } - Rectangle { width: 30; height: 30; radius: 7; color: overwriteArea.containsMouse ? "#E0F2FE" : "transparent"; Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 } MouseArea { id: overwriteArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Done and copy"), parent); onExited: bar.hoverExited(qsTr("Done and copy")); onPressed: bar.doneAndCopy() } } - Rectangle { width: 30; height: 30; radius: 7; color: clearToolArea.containsMouse ? "#FEE2E2" : "transparent"; Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 } MouseArea { id: clearToolArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Hide toolbar"), parent); onExited: bar.hoverExited(qsTr("Hide toolbar")); onPressed: bar.hideToolbar() } } - } -} diff --git a/qml/Windows/TZStickyWindow.qml b/qml/Windows/TZStickyWindow.qml index 29c8ef7..0f215d1 100644 --- a/qml/Windows/TZStickyWindow.qml +++ b/qml/Windows/TZStickyWindow.qml @@ -1,317 +1,173 @@ import QtQuick import QtQuick.Window +import Qt5Compat.GraphicalEffects import Qt.labs.platform 1.0 as Platform -import QtQuick.Effects -import QtQuick.Controls.Basic -import QtCore -import CustomComponents 1.0 -import ".." import "../Capture" +import CustomComponents 1.0 Window { id: stickyWin flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool color: "transparent" + visible: true property string imageUrl: "" property rect imgRect: Qt.rect(0, 0, 0, 0) - property bool contentActive: true property bool cleanedUp: false - - property real imgOpacity: 1.0 - property string activeTool: "" - property bool toolbarVisible: true - property string toolbarHoverTipText: "" - property real toolbarHoverTipCenterX: 0 - property bool contextMenuOpen: stickyContextMenu.contextMenuOpen - - property int imageRevision: 0 - property real zoomFactor: 1.0 - property real dpiScale: 1.0 property size sourceImageSize: Qt.size(1, 1) - property int imageLogicalWidth: 1 - property int imageLogicalHeight: 1 - - readonly property int toolbarHeight: 40 + property bool toolbarVisible: true + property real zoomFactor: 1.0 + property int windowWidthValue: 1 + property int windowHeightValue: 1 + readonly property int shadowPadding: 12 + readonly property color shadowColor: "#2F8FFF" + readonly property int toolbarHeight: toolBar.implicitHeight readonly property int toolbarGap: 8 - readonly property int toolbarTipGap: 6 - readonly property int toolbarTipHeight: 24 - readonly property int shadowRadius: 12 - readonly property int shadowOffset: 4 - readonly property int displayW: Math.max(1, Math.round(imageLogicalWidth * zoomFactor)) - readonly property int displayH: Math.max(1, Math.round(imageLogicalHeight * zoomFactor)) - readonly property int extraToolbarWidth: stickyExtraBar.width - readonly property int toolbarContentWidth: stickyTool.implicitWidth + extraToolbarWidth + 6 - readonly property int toolbarBlockX: Math.max(8, Math.min(stickyWin.width - toolbarContentWidth - 8, imgContainer.x + (imgContainer.width - toolbarContentWidth) / 2)) - readonly property int toolbarTopY: imgContainer.y + imgContainer.height + stickyWin.toolbarGap + readonly property int toolbarTipReserve: 30 + readonly property int markToolGap: 8 + readonly property int contentX: stickyWin.shadowPadding + readonly property int contentY: stickyWin.shadowPadding + readonly property int contentWidth: Math.max(1, stickyWin.width - stickyWin.shadowPadding * 2) + readonly property int contentHeight: Math.max(1, stickyWin.height - stickyWin.shadowPadding * 2 + - (stickyWin.toolbarVisible ? (stickyWin.toolbarHeight + stickyWin.toolbarGap + stickyWin.toolbarTipReserve) : 0)) - readonly property int stickyShapeType: { - switch (activeTool) { - case "pencil": return 0 - case "rect": return 1 - case "circle": return 2 - case "arrow": return 3 - case "mosaic": return 4 - default: return 1 - } + width: windowWidthValue + height: windowHeightValue + minimumWidth: windowWidthValue + maximumWidth: windowWidthValue + minimumHeight: windowHeightValue + maximumHeight: windowHeightValue + + function updateImageSize() { + sourceImageSize = O_StickyViewModel.getImageSizeByUrl(imageUrl) + if (!sourceImageSize || sourceImageSize.width <= 0 || sourceImageSize.height <= 0) + sourceImageSize = Qt.size(1, 1) + updateWindowSize() } - width: Math.max(displayW, toolbarContentWidth + 16) - height: displayH + toolbarGap + toolbarHeight + toolbarTipGap + toolbarTipHeight + 4 - function syncInitialPosition() { - O_StickyViewModel.positionStickyWindow(stickyWin, imgRect) + function updateWindowSize() { + var imageWidth = Math.max(1, Math.floor(sourceImageSize.width * zoomFactor)) + var imageHeight = Math.max(1, Math.floor(sourceImageSize.height * zoomFactor)) + windowWidthValue = imageWidth + shadowPadding * 2 + windowHeightValue = imageHeight + shadowPadding * 2 + + (toolbarVisible ? (toolbarHeight + toolbarGap + toolbarTipReserve) : 0) + Qt.callLater(applyNativeSize) } - function updateDpiScale() { - dpiScale = (screen && screen.devicePixelRatio > 0) ? screen.devicePixelRatio : 1.0 + function applyNativeSize() { + O_StickyViewModel.resizeStickyWindow(stickyWin, windowWidthValue, windowHeightValue) } - function updateImageLogicalSize() { - sourceImageSize = O_StickyViewModel.getImageSizeByUrl(stickyWin.imageUrl) - if (sourceImageSize.width > 0 && sourceImageSize.height > 0) { - imageLogicalWidth = Math.max(1, Math.round(sourceImageSize.width / dpiScale)) - imageLogicalHeight = Math.max(1, Math.round(sourceImageSize.height / dpiScale)) - } - } - - Settings { - id: menuPrefs - category: "sticky_menu" - property bool pinSaveMerged: true - property bool pinCopy: true - property bool pinAi: false - property bool pinToolbar: false - property bool pinClose: false - } - - function showToolbarTip(text, item) { - toolbarHoverTipText = text - if (item) { - var p = stickyExtraBar.mapFromItem(item, item.width / 2, item.height) - toolbarHoverTipCenterX = p.x - } else { - toolbarHoverTipCenterX = stickyExtraBar.width / 2 - } - } - - function hideToolbarTip(text) { - if (text === undefined || toolbarHoverTipText === text) - toolbarHoverTipText = "" - } - - function displaySourceUrl() { - if (typeof stickyWin.imageUrl !== "string" || stickyWin.imageUrl.length === 0) - return "" - return stickyWin.imageUrl + "?rev=" + stickyWin.imageRevision - } - - function setOpacityValue(v) { - stickyWin.imgOpacity = Math.max(0.1, Math.min(1.0, v)) - } - - function setZoomValue(v) { - zoomFactor = Math.max(0.3, Math.min(4.0, v)) + function setZoomFactor(value) { + zoomFactor = Math.max(0.3, Math.min(4.0, value)) + updateWindowSize() } function resetDisplaySize() { - zoomFactor = 1.0 + setZoomFactor(1.0) } - function updateImageMetricsFromStore() { - var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - if (img && img.width > 0 && img.height > 0) { - stickyPaintBoard.setBackgroundImg(img) - } else { - syncPaintboardBackgroundFromDisplay() - } - } - - function syncPaintboardBackgroundFromDisplay() { - if (!stickyWin.visible || !stickyWin.contentActive) - return - if (stickyImage.status !== Image.Ready) - return - stickyImage.grabToImage(function(result) { - if (!result || !result.image || result.image.width <= 0 || result.image.height <= 0) - return - stickyPaintBoard.setBackgroundImg(result.image) - }, Qt.size(Math.max(1, Math.abs(imgRect.width)), Math.max(1, Math.abs(imgRect.height)))) - } - - function refreshAfterImageChanged() { - imageRevision += 1 - stickyPaintBoard.reset() - updateImageMetricsFromStore() - } - - function overwriteWithEdits() { - var layer = stickyPaintBoard.renderToImage() - var ok = O_StickyViewModel.overwriteWithAnnotations(stickyWin.imageUrl, layer) - if (ok) - refreshAfterImageChanged() - } - - function rotateCurrent(degrees) { - if (O_StickyViewModel.rotateImage(stickyWin.imageUrl, degrees)) - refreshAfterImageChanged() - } - - function mirrorCurrent() { - if (O_StickyViewModel.mirrorImage(stickyWin.imageUrl, true, false)) - refreshAfterImageChanged() - } - - function saveMergedTo(pathUrl) { - var layer = stickyPaintBoard.renderToImage() - O_StickyViewModel.saveMergedImage(stickyWin.imageUrl, layer, pathUrl) - } - - function completeEditsAndCopy() { - var layer = stickyPaintBoard.renderToImage() - if (layer && layer.width > 0 && layer.height > 0) - overwriteWithEdits() - O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl) - stickyTool.activeTool = "" - stickyWin.activeTool = "" - stickyWin.toolbarVisible = false + function syncInitialPosition() { + O_StickyViewModel.positionStickyWindow(stickyWin, + Qt.rect(imgRect.x - shadowPadding, + imgRect.y - shadowPadding, + imgRect.width + shadowPadding * 2, + imgRect.height + shadowPadding * 2)) } function cleanup() { if (cleanedUp) return cleanedUp = true - - stickyWin.visible = false - contentActive = false - stickyWin.releaseResources() + chatPanel.close() O_StickyViewModel.releaseImage(imageUrl) imageUrl = "" } + function syncPaintBoardBackground() { + if (!imageUrl) { + paintBoard.reset() + return + } + + var bg = O_StickyViewModel.getImageByUrl(imageUrl) + paintBoard.reset() + paintBoard.setBackgroundImg(bg) + } + + function openChatPanel() { + var targetScreen = stickyWin.screen + if (!targetScreen || !targetScreen.availableGeometry) { + chatPanel.open(stickyWin.x, stickyWin.y + stickyWin.height + 6, targetScreen) + return + } + + var available = targetScreen.availableGeometry + var panelX = stickyWin.x + var panelY = stickyWin.y + stickyWin.height + 6 + + if (panelY + chatPanel.win.height > available.y + available.height) + panelY = stickyWin.y - chatPanel.win.height - 6 + + panelX = Math.max(available.x, Math.min(panelX, available.x + available.width - chatPanel.win.width)) + panelY = Math.max(available.y, Math.min(panelY, available.y + available.height - chatPanel.win.height)) + + chatPanel.open(Math.round(panelX), Math.round(panelY), targetScreen) + } + Component.onCompleted: { - updateDpiScale() + updateImageSize() + syncPaintBoardBackground() syncInitialPosition() - updateImageMetricsFromStore() - updateImageLogicalSize() + applyNativeSize() stickyWin.raise() stickyWin.requestActivate() } + onImageUrlChanged: { - imageRevision = 0 - sourceImageSize = Qt.size(1, 1) - imageLogicalWidth = 1 - imageLogicalHeight = 1 - resetDisplaySize() - refreshAfterImageChanged() + updateImageSize() + syncPaintBoardBackground() } - onActiveToolChanged: { - if (activeTool === "mosaic") - updateImageMetricsFromStore() - } - - onScreenChanged: { - updateDpiScale() - updateImageLogicalSize() - } - - onImgRectChanged: syncInitialPosition() - - Component.onDestruction: cleanup() - function onClosing(close) { close.accepted = true - chatPanel.close() cleanup() Qt.callLater(function() { stickyWin.destroy() }) } - Rectangle { - id: imgContainer - visible: stickyWin.contentActive - z: 10 - x: 0 - y: 0 - width: stickyWin.displayW - height: stickyWin.displayH - radius: 2 - clip: true - color: "transparent" - - Item { - id: visualStage - anchors.centerIn: parent - width: stickyWin.imageLogicalWidth - height: stickyWin.imageLogicalHeight - scale: stickyWin.zoomFactor - - Image { - id: stickyImage - anchors.fill: parent - source: displaySourceUrl() - fillMode: Image.Stretch - opacity: stickyWin.imgOpacity - cache: false - onStatusChanged: { - if (status === Image.Ready) - stickyWin.updateImageLogicalSize() - if (status === Image.Ready && stickyWin.activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg()) - stickyWin.syncPaintboardBackgroundFromDisplay() - } - } - - PaintBoard { - id: stickyPaintBoard - anchors.fill: parent - z: 20 - visible: stickyWin.contentActive - enabled: stickyWin.activeTool !== "" - shapeType: stickyWin.stickyShapeType - penColor: stickyMarkTool.currentColor - penSize: stickyMarkTool.currentSize - mosaicBlurLevel: stickyMarkTool.mosaicBlurLevel - } - } + RectangularGlow { + id: stickyShadow + x: stickyWin.contentX + y: stickyWin.contentY + width: stickyWin.contentWidth + height: stickyWin.contentHeight + glowRadius: 10 + spread: 0.08 + color: stickyWin.shadowColor + cornerRadius: 2 + opacity: 0.45 + cached: true } - MultiEffect { - visible: stickyWin.contentActive + PaintBoard { + id: paintBoard + x: stickyWin.contentX + y: stickyWin.contentY + width: stickyWin.contentWidth + height: stickyWin.contentHeight z: 5 - source: stickyImage - anchors.fill: imgContainer - shadowEnabled: true - shadowColor: "#55000000" - shadowBlur: stickyWin.shadowRadius / 20.0 - shadowVerticalOffset: stickyWin.shadowOffset - shadowHorizontalOffset: 0 - opacity: stickyWin.imgOpacity - } - - TZLoadingSpinner { - anchors.centerIn: imgContainer - width: stickyWin.displayW - height: stickyWin.displayH - visible: O_AIViewModel.isLoading && stickyWin.contentActive - } - - TZMarkToolInfo { - id: stickyMarkTool - z: 32 - visible: stickyWin.toolbarVisible && stickyWin.activeTool !== "" - toolName: stickyWin.activeTool - showColorRow: stickyWin.activeTool !== "mosaic" - x: Math.max(8, toolbarBlockX) - y: stickyTool.y - height - 6 + visible: true + enabled: toolBar.activeTool !== "" + shapeType: toolBar.shapeType + penColor: markToolInfo.currentColor + penSize: markToolInfo.currentSize } TZTool { - id: stickyTool - z: 30 + id: toolBar visible: stickyWin.toolbarVisible - width: implicitWidth - height: stickyWin.toolbarHeight + 4 - x: stickyWin.toolbarBlockX - y: stickyWin.toolbarTopY + x: Math.max(0, Math.floor((stickyWin.width - width) / 2)) + y: stickyWin.contentY + stickyWin.contentHeight + stickyWin.toolbarGap showScreenshotButton: false showCancelButton: false showSaveButton: false @@ -321,76 +177,31 @@ Window { showHighlightButton: false showTextButton: false showNumberButton: false + showRectButton: false + showCircleButton: false + showArrowButton: false + showMosaicButton: false + showPencilButton: true + showUndoButton: true - onActiveToolChanged: { - stickyWin.activeTool = activeTool - if (activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg()) { - Qt.callLater(function() { - var retryImg = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - if (retryImg && retryImg.width > 0 && retryImg.height > 0) - stickyPaintBoard.setBackgroundImg(retryImg) - if (!stickyPaintBoard.hasBackgroundImg()) - stickyWin.syncPaintboardBackgroundFromDisplay() - }) - } - } - onSignalOcr: { - var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl) - O_OcrVM.recognize(img) - } - onSignalUndo: stickyPaintBoard.undo() + onSignalOcr: {} + onSignalUndo: paintBoard.undo() } - TZStickyExtraToolBar { - id: stickyExtraBar - z: 30 - visible: stickyWin.toolbarVisible - height: stickyWin.toolbarHeight - x: stickyWin.toolbarBlockX + stickyTool.width + 6 - y: stickyTool.y + 2 - zoomFactor: stickyWin.zoomFactor - onZoomOut: setZoomValue(stickyWin.zoomFactor - 0.1) - onZoomIn: setZoomValue(stickyWin.zoomFactor + 0.1) - onRotate90: rotateCurrent(90) - onMirror: mirrorCurrent() - onResetSize: resetDisplaySize() - onDoneAndCopy: completeEditsAndCopy() - onHideToolbar: { - stickyTool.activeTool = "" - stickyWin.activeTool = "" - stickyWin.toolbarVisible = false - } - onHoverEntered: function(text, targetItem) { stickyWin.showToolbarTip(text, targetItem) } - onHoverExited: function(text) { stickyWin.hideToolbarTip(text) } + TZMarkToolInfo { + id: markToolInfo + visible: toolBar.visible && toolBar.markToolInfoVisible && toolBar.activeTool === "pencil" + z: 6 + x: Math.max(stickyWin.shadowPadding, Math.min(stickyWin.width - width - stickyWin.shadowPadding, toolBar.x)) + y: Math.max(stickyWin.shadowPadding, toolBar.y - height - stickyWin.markToolGap) + toolName: toolBar.activeTool + showColorRow: true } - Rectangle { - id: stickyToolbarTip - parent: stickyWin.contentItem - visible: stickyWin.toolbarVisible && stickyWin.toolbarHoverTipText.length > 0 - z: 40 - width: Math.min(stickyWin.width - 8, Math.max(64, stickyToolbarTipText.implicitWidth + 16)) - height: 24 - radius: 6 - color: "#0F172A" - opacity: 0.92 - x: Math.max(4, Math.min(stickyWin.width - width - 4, stickyExtraBar.x + stickyWin.toolbarHoverTipCenterX - width / 2)) - y: Math.min(stickyWin.height - height - 4, stickyExtraBar.y + stickyExtraBar.height + stickyWin.toolbarTipGap) - - Text { - id: stickyToolbarTipText - anchors.centerIn: parent - text: stickyWin.toolbarHoverTipText - color: "#FFFFFF" - font.pixelSize: 11 - } - } - - readonly property int chatPanelGap: 6 - TZChat { id: chatPanel imageUrl: stickyWin.imageUrl + onSignalSendPrompt: function(text) { O_AIViewModel.sendPrompt(text, stickyWin.imageUrl) } @@ -398,123 +209,109 @@ Window { Connections { target: O_AIViewModel + function onSignalRequestComplete(oldImageUrl, newImageUrl) { if (oldImageUrl !== stickyWin.imageUrl) return var old = oldImageUrl stickyWin.imageUrl = newImageUrl + syncPaintBoardBackground() Qt.callLater(function() { O_StickyViewModel.releaseImage(old) }) } + function onSignalRequestFailed(errorMsg) { console.warn("[TZStickyWindow] AI request failed: " + errorMsg) } } - function openChatPanel() { - var imgLeft = stickyWin.x - var imgTop = stickyWin.y - var imgBottom = imgTop + stickyWin.displayH + Platform.Menu { + id: contextMenu - var screenH = Screen.desktopAvailableHeight - var screenW = Screen.desktopAvailableWidth + Platform.MenuItem { + text: qsTr("复制") + onTriggered: O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl) + } - var cx = imgLeft - if (cx + chatPanel.win.width > screenW) - cx = imgLeft + stickyWin.displayW - chatPanel.win.width + Platform.MenuItem { + text: qsTr("AI 编辑") + onTriggered: stickyWin.openChatPanel() + } - var cy - if (imgBottom + chatPanelGap + chatPanel.win.height <= screenH) - cy = imgBottom + chatPanelGap - else - cy = imgTop - chatPanelGap - chatPanel.win.height - - chatPanel.open(Math.max(0, cx), Math.max(0, cy)) - } - - TZStickyContextMenu { - id: stickyContextMenu - menuPrefs: menuPrefs - imageUrl: stickyWin.imageUrl - imgOpacity: stickyWin.imgOpacity - zoomFactor: stickyWin.zoomFactor - toolbarVisible: stickyWin.toolbarVisible - saveFolder: O_ImageSaver.savePath - overwriteWithEdits: function() { stickyWin.overwriteWithEdits() } - copyImageToClipboard: function(url) { O_StickyViewModel.copyImageToClipboard(url) } - openChatPanel: function() { stickyWin.openChatPanel() } - hideOrToggleToolbar: function() { - stickyWin.toolbarVisible = !stickyWin.toolbarVisible - if (!stickyWin.toolbarVisible) { - stickyTool.activeTool = "" - stickyWin.activeTool = "" + Platform.MenuItem { + text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏") + onTriggered: { + stickyWin.toolbarVisible = !stickyWin.toolbarVisible + updateWindowSize() } } - setOpacityValue: function(value) { stickyWin.setOpacityValue(value) } - setZoomValue: function(value) { stickyWin.setZoomValue(value) } - rotateCurrent: function(degrees) { stickyWin.rotateCurrent(degrees) } - mirrorCurrent: function() { stickyWin.mirrorCurrent() } - resetDisplaySize: function() { stickyWin.resetDisplaySize() } - resetWindowPosition: function() { stickyWin.syncInitialPosition() } - saveOriginalImage: function(url, pathUrl) { O_StickyViewModel.saveImage(url, pathUrl) } - saveMergedTo: function(pathUrl) { stickyWin.saveMergedTo(pathUrl) } - closeWindow: function() { stickyWin.close() } + + Platform.MenuItem { + text: qsTr("恢复原图大小") + onTriggered: stickyWin.resetDisplaySize() + } + + Platform.MenuSeparator {} + + Platform.MenuItem { + text: qsTr("保存原图") + onTriggered: saveDialog.open() + } + + Platform.MenuItem { + text: qsTr("关闭") + onTriggered: stickyWin.close() + } + } + + Platform.FileDialog { + id: saveDialog + title: qsTr("选择保存位置") + folder: O_ImageSaver.savePath + fileMode: Platform.FileDialog.SaveFile + nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"] + onAccepted: O_StickyViewModel.saveImage(stickyWin.imageUrl, saveDialog.file) } MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - enabled: stickyWin.activeTool === "" && !stickyWin.contextMenuOpen - property point dragPos: Qt.point(0, 0) - property bool leftDragActive: false - z: 0 - - function inImageArea(mx, my) { - return mx >= 0 - && mx <= stickyWin.displayW - && my >= 0 - && my <= stickyWin.displayH - } + id: dragArea + x: stickyWin.contentX + y: stickyWin.contentY + width: stickyWin.contentWidth + height: stickyWin.contentHeight + enabled: toolBar.activeTool === "" + acceptedButtons: Qt.LeftButton + property point pressOffset: Qt.point(0, 0) onPressed: function(mouse) { - if (mouse.button === Qt.LeftButton) { - dragPos = Qt.point(mouseX, mouseY) - leftDragActive = true - } else { - leftDragActive = false - } + pressOffset = Qt.point(mouse.x, mouse.y) } onPositionChanged: function(mouse) { - if (pressed && leftDragActive && (mouse.buttons & Qt.LeftButton) && stickyWin.activeTool === "" && !stickyWin.contextMenuOpen) { - stickyWin.x += mouseX - dragPos.x - stickyWin.y += mouseY - dragPos.y - } - } - - onReleased: function(mouse) { - if (!(mouse.buttons & Qt.LeftButton)) - leftDragActive = false - } - - onDoubleClicked: function(mouse) { - if (mouse.button === Qt.LeftButton && inImageArea(mouseX, mouseY)) - resetDisplaySize() - } - - onClicked: function(mouse) { - if (mouse.button === Qt.RightButton) { - leftDragActive = false - stickyContextMenu.open() + if (pressed && (mouse.buttons & Qt.LeftButton)) { + stickyWin.x += mouse.x - pressOffset.x + stickyWin.y += mouse.y - pressOffset.y } } onWheel: function(wheel) { if (wheel.angleDelta.y > 0) - setZoomValue(stickyWin.zoomFactor + 0.1) + stickyWin.setZoomFactor(stickyWin.zoomFactor + 0.1) else if (wheel.angleDelta.y < 0) - setZoomValue(stickyWin.zoomFactor - 0.1) + stickyWin.setZoomFactor(stickyWin.zoomFactor - 0.1) wheel.accepted = true } } + MouseArea { + x: stickyWin.contentX + y: stickyWin.contentY + width: stickyWin.contentWidth + height: stickyWin.contentHeight + (stickyWin.toolbarVisible ? (stickyWin.toolbarHeight + stickyWin.toolbarGap + stickyWin.toolbarTipReserve) : 0) + acceptedButtons: Qt.RightButton + + onClicked: function(mouse) { + if (mouse.button === Qt.RightButton) + contextMenu.open() + } + } } diff --git a/resource/img/lc_blur.svg b/resource/img/lc_blur.svg new file mode 100644 index 0000000..97f7746 --- /dev/null +++ b/resource/img/lc_blur.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resource/img/lc_emoji.svg b/resource/img/lc_emoji.svg deleted file mode 100644 index 8a511b7..0000000 --- a/resource/img/lc_emoji.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resource/img/lc_file.svg b/resource/img/lc_file.svg deleted file mode 100644 index 3146f30..0000000 --- a/resource/img/lc_file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resource/qss/sticky_pin_widget.qss b/resource/qss/sticky_pin_widget.qss new file mode 100644 index 0000000..b5c8a21 --- /dev/null +++ b/resource/qss/sticky_pin_widget.qss @@ -0,0 +1,81 @@ +#stickyToolbar { + background-color: #FFFFFF; + border: 1px solid rgba(96, 165, 250, 170); + border-radius: 10px; +} + +#stickyToolbar QToolButton { + background-color: transparent; + color: #0F172A; + border: 1px solid transparent; + border-radius: 7px; + padding: 0px; + margin: 0px; + qproperty-iconSize: 18px 18px; +} + +#stickyToolbar QToolButton:hover { + background-color: #F1F5F9; + border-color: transparent; +} + +#stickyToolbar QToolButton:pressed { + background-color: #EFF6FF; + border-color: #3B82F6; +} + +#stickyToolbar QToolButton:checked { + background-color: #EFF6FF; + border-color: #3B82F6; +} + +#stickyToolOptions { + background-color: #FFFFFF; + border: 1px solid #E2E8F0; + border-radius: 10px; +} + +#stickyToolOptions QToolButton { + background-color: transparent; + border: 1px solid #E2E8F0; + border-radius: 6px; + color: #334155; +} + +#stickyToolOptions QToolButton:checked { + background-color: #EFF6FF; + border-color: #3B82F6; + color: #3B82F6; +} + +#stickyToolOptions QToolButton:hover { + background-color: #F1F5F9; +} + +#stickyToolOptions QLineEdit, +#stickyToolOptions QSpinBox { + background-color: #F8FAFC; + border: 1px solid #E2E8F0; + border-radius: 6px; + padding: 4px 6px; + color: #0F172A; +} + +#stickyToolOptions QCheckBox { + color: #334155; +} + +#stickyInlineTextEditor { + background-color: #FFFFFF; + border: 1px solid #3B82F6; + border-radius: 6px; + padding: 4px 6px; + color: #0F172A; +} + +#stickyNumberLabel { + background-color: #F8FAFC; + border: 1px solid #E2E8F0; + border-radius: 6px; + color: #0F172A; +} diff --git a/src/main.cpp b/src/main.cpp index 6e7f460..545d45a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,9 +29,10 @@ int main(int argc, char *argv[]) { // 强制让窗口跟随系统DPI,不使用虚拟像素 - QGuiApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); - + // QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + // Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor); + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); QApplication app(argc, argv); // Single-instance activation: second launch notifies the first one and exits. @@ -55,6 +56,7 @@ int main(int argc, char *argv[]) ScreenshotViewModel screenshotViewModel(desktopSnapshot, stickyImageStore); StickyViewModel stickyViewModel(stickyImageStore); AIViewModel aiViewModel(appSettings, stickyImageStore); + stickyViewModel.setAiViewModel(&aiViewModel); StorageViewModel storageViewModel(appSettings); LanguageManager languageManager(appSettings); GifRecordViewModel gifRecordViewModel(appSettings); diff --git a/src/model/desktop_snapshot.cpp b/src/model/desktop_snapshot.cpp index 1f00baa..acb88c9 100644 --- a/src/model/desktop_snapshot.cpp +++ b/src/model/desktop_snapshot.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #ifdef Q_OS_WIN #define WIN32_LEAN_AND_MEAN @@ -15,6 +17,18 @@ namespace { +struct ScreenSortLess +{ + template + bool operator()(const T &lhs, const T &rhs) const + { + if (lhs.left() != rhs.left()) { + return lhs.left() < rhs.left(); + } + return lhs.top() < rhs.top(); + } +}; + QRect desktopGeometry() { #ifdef Q_OS_WIN @@ -78,6 +92,33 @@ QImage captureVirtualDesktopImage(const QRect& geometry) ReleaseDC(nullptr, screenDC); return image; } + +QVector enumerateMonitorRects() +{ + struct MonitorRects { + QVector rects; + } monitors; + + EnumDisplayMonitors(nullptr, + nullptr, + [](HMONITOR monitor, HDC, LPRECT, LPARAM data) -> BOOL { + auto *rects = reinterpret_cast(data); + MONITORINFOEXW info{}; + info.cbSize = sizeof(info); + if (GetMonitorInfoW(monitor, &info)) { + const RECT &rc = info.rcMonitor; + rects->rects.append(QRect(rc.left, + rc.top, + rc.right - rc.left, + rc.bottom - rc.top)); + } + return TRUE; + }, + reinterpret_cast(&monitors)); + + std::sort(monitors.rects.begin(), monitors.rects.end(), ScreenSortLess{}); + return monitors.rects; +} #endif } @@ -123,6 +164,7 @@ void DesktopSnapshot::grab() #endif m_virtualGeometry = geometry; + buildScreenEntries(); buildWindowList(); } @@ -131,6 +173,102 @@ void DesktopSnapshot::release() m_snapshot = QPixmap(); m_virtualGeometry = QRect(); m_windowRects.clear(); + m_screenEntries.clear(); +} + +void DesktopSnapshot::buildScreenEntries() +{ + m_screenEntries.clear(); + + QList screens = QGuiApplication::screens(); + if (screens.isEmpty()) { + return; + } + +#ifdef Q_OS_WIN + const QVector monitorRects = enumerateMonitorRects(); + std::sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) { + if (!lhs || !rhs) { + return lhs < rhs; + } + return ScreenSortLess{}(lhs->geometry(), rhs->geometry()); + }); + + const int pairCount = qMin(screens.size(), monitorRects.size()); + for (int i = 0; i < pairCount; ++i) { + QScreen *screen = screens.at(i); + if (!screen) { + continue; + } + ScreenEntry entry; + entry.logicalGeometry = screen->geometry(); + entry.physicalRect = monitorRects.at(i); + entry.dpr = screen->devicePixelRatio(); + if (entry.dpr <= 0.0) { + entry.dpr = 1.0; + } + m_screenEntries.append(entry); + } + + for (int i = pairCount; i < screens.size(); ++i) { + QScreen *screen = screens.at(i); + if (!screen) { + continue; + } + ScreenEntry entry; + entry.logicalGeometry = screen->geometry(); + entry.dpr = screen->devicePixelRatio(); + if (entry.dpr <= 0.0) { + entry.dpr = 1.0; + } + entry.physicalRect = QRect(qRound(entry.logicalGeometry.x() * entry.dpr), + qRound(entry.logicalGeometry.y() * entry.dpr), + qRound(entry.logicalGeometry.width() * entry.dpr), + qRound(entry.logicalGeometry.height() * entry.dpr)); + m_screenEntries.append(entry); + } +#else + for (QScreen *screen : screens) { + if (!screen) { + continue; + } + ScreenEntry entry; + entry.logicalGeometry = screen->geometry(); + entry.physicalRect = entry.logicalGeometry; + entry.dpr = screen->devicePixelRatio(); + if (entry.dpr <= 0.0) { + entry.dpr = 1.0; + } + m_screenEntries.append(entry); + } +#endif +} + +const DesktopSnapshot::ScreenEntry *DesktopSnapshot::screenEntryForPoint(const QPoint &logicalPoint) const +{ + for (const ScreenEntry &entry : m_screenEntries) { + if (entry.logicalGeometry.contains(logicalPoint)) { + return &entry; + } + } + + if (m_screenEntries.isEmpty()) { + return nullptr; + } + + const ScreenEntry *nearest = &m_screenEntries.first(); + qreal bestDistance = (std::numeric_limits::max)(); + for (const ScreenEntry &entry : m_screenEntries) { + const QPoint center = entry.logicalGeometry.center(); + const qreal dx = logicalPoint.x() - center.x(); + const qreal dy = logicalPoint.y() - center.y(); + const qreal distance = dx * dx + dy * dy; + if (distance < bestDistance) { + bestDistance = distance; + nearest = &entry; + } + } + return nearest; } QPoint DesktopSnapshot::mapLogicalPointToPhysical(const QPoint &logicalPoint) const @@ -138,7 +276,16 @@ QPoint DesktopSnapshot::mapLogicalPointToPhysical(const QPoint &logicalPoint) co if (m_virtualGeometry.isEmpty()) { return {}; } - return logicalPoint - m_virtualGeometry.topLeft(); + + const ScreenEntry *entry = screenEntryForPoint(logicalPoint); + if (!entry) { + return {}; + } + + const QPoint localPoint = logicalPoint - entry->logicalGeometry.topLeft(); + const QPoint physicalPoint(entry->physicalRect.x() + qRound(localPoint.x() * entry->dpr), + entry->physicalRect.y() + qRound(localPoint.y() * entry->dpr)); + return physicalPoint - m_virtualGeometry.topLeft(); } QRect DesktopSnapshot::mapLogicalRectToPhysical(const QRect &logicalRect) const @@ -147,7 +294,18 @@ QRect DesktopSnapshot::mapLogicalRectToPhysical(const QRect &logicalRect) const if (normalized.isEmpty() || m_virtualGeometry.isEmpty()) { return {}; } - QRect translated = normalized.translated(-m_virtualGeometry.topLeft()); + + const ScreenEntry *entry = screenEntryForPoint(normalized.center()); + if (!entry) { + return {}; + } + + const QPoint localTopLeft = normalized.topLeft() - entry->logicalGeometry.topLeft(); + const QPoint absolutePhysicalTopLeft(entry->physicalRect.x() + qRound(localTopLeft.x() * entry->dpr), + entry->physicalRect.y() + qRound(localTopLeft.y() * entry->dpr)); + QRect translated(absolutePhysicalTopLeft - m_virtualGeometry.topLeft(), + QSize(qMax(1, qRound(normalized.width() * entry->dpr)), + qMax(1, qRound(normalized.height() * entry->dpr)))); return translated.intersected(m_snapshot.rect()); } diff --git a/src/model/desktop_snapshot.h b/src/model/desktop_snapshot.h index 18f20be..3b41e2d 100644 --- a/src/model/desktop_snapshot.h +++ b/src/model/desktop_snapshot.h @@ -31,6 +31,7 @@ private: qreal dpr = 1.0; }; + void buildScreenEntries(); void buildWindowList(); const ScreenEntry* screenEntryForPoint(const QPoint &logicalPoint) const; diff --git a/src/paint_board/paint_board.cpp b/src/paint_board/paint_board.cpp index 9d32c67..0a61b56 100644 --- a/src/paint_board/paint_board.cpp +++ b/src/paint_board/paint_board.cpp @@ -1,15 +1,19 @@ -#include "paint_board.h" +#include "paint_board.h" #include "shape/mosaic_shape.h" #include PaintBoard::PaintBoard(QQuickItem *parent) : QQuickPaintedItem(parent) { + setRenderTarget(QQuickPaintedItem::FramebufferObject); setAcceptedMouseButtons(Qt::LeftButton); } void PaintBoard::setBackgroundImg(QImage img) { + // if (!img.isNull()) { + // img.setDevicePixelRatio(1.0); + // } m_background_img = img; update(); } @@ -125,7 +129,8 @@ QImage PaintBoard::renderToImage() const if (m_shapes.isEmpty()) return QImage(); - QImage img(qRound(width()), qRound(height()), QImage::Format_ARGB32); + QImage img(qRound(width() ), qRound(height()), QImage::Format_ARGB32); + img.setDevicePixelRatio(1.0); img.fill(Qt::transparent); QPainter painter(&img); @@ -140,13 +145,16 @@ QImage PaintBoard::renderToImage() const void PaintBoard::paint(QPainter *painter) { + // painter->save(); + // painter->scale(1.0 / m_canvasScale, 1.0 / m_canvasScale); drawBkImg(painter, m_background_img); drawShape(painter); + // painter->restore(); } void PaintBoard::mousePressEvent(QMouseEvent *event) { - m_startPoint = event->position().toPoint(); + m_startPoint = (event->position()).toPoint(); m_drawing = true; if (m_currentType == TEXT) { @@ -206,7 +214,7 @@ void PaintBoard::mouseMoveEvent(QMouseEvent *event) if (!m_drawing || !m_currentShape) return; - m_currentShape->setEndPoint(event->position().toPoint()); + m_currentShape->setEndPoint((event->position()).toPoint()); update(); } @@ -215,7 +223,7 @@ void PaintBoard::mouseReleaseEvent(QMouseEvent *event) if (!m_drawing || !m_currentShape) return; - m_currentShape->setEndPoint(event->position().toPoint()); + m_currentShape->setEndPoint((event->position()).toPoint()); m_shapes.append(m_currentShape); m_currentShape = nullptr; m_drawing = false; @@ -232,9 +240,7 @@ bool PaintBoard::drawBkImg(QPainter *painter, QImage img) if (!painter || img.isNull()) return false; - QRectF drawRect = img.rect(); - drawRect.moveCenter(boundingRect().center()); - painter->drawImage(drawRect, img); + painter->drawImage(QRectF(0, 0, width() , height() ), img); return true; } diff --git a/src/paint_board/shape/arrow_shape.cpp b/src/paint_board/shape/arrow_shape.cpp index 97eea06..96ea49a 100644 --- a/src/paint_board/shape/arrow_shape.cpp +++ b/src/paint_board/shape/arrow_shape.cpp @@ -12,37 +12,36 @@ void ArrowShape::draw(QPainter *painter) if (m_startPoint == m_endPoint) return; painter->save(); + const QPointF startPoint(m_startPoint); + const QPointF endPoint(m_endPoint); QPen pen(m_color, m_size); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); painter->setPen(pen); painter->setRenderHint(QPainter::Antialiasing); - // 绘制箭杆 - painter->drawLine(m_startPoint, m_endPoint); + painter->drawLine(startPoint, endPoint); // 计算箭头方向角 const double angle = std::atan2( - m_endPoint.y() - m_startPoint.y(), - m_endPoint.x() - m_startPoint.x() + endPoint.y() - startPoint.y(), + endPoint.x() - startPoint.x() ); - // 计算箭头两侧的点 - QPoint arrowP1( - m_endPoint.x() - static_cast(std::cos(angle + ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH), - m_endPoint.y() - static_cast(std::sin(angle + ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH) + const qreal headLength = ARROW_HEAD_LENGTH; + QPointF arrowP1( + endPoint.x() - std::cos(angle + ARROW_HEAD_ANGLE) * headLength, + endPoint.y() - std::sin(angle + ARROW_HEAD_ANGLE) * headLength ); - QPoint arrowP2( - m_endPoint.x() - static_cast(std::cos(angle - ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH), - m_endPoint.y() - static_cast(std::sin(angle - ARROW_HEAD_ANGLE) * ARROW_HEAD_LENGTH) + QPointF arrowP2( + endPoint.x() - std::cos(angle - ARROW_HEAD_ANGLE) * headLength, + endPoint.y() - std::sin(angle - ARROW_HEAD_ANGLE) * headLength ); - // 绘制实心箭头头部 - QPolygon arrowHead; - arrowHead << m_endPoint << arrowP1 << arrowP2; + QPolygonF arrowHead; + arrowHead << endPoint << arrowP1 << arrowP2; painter->setBrush(QBrush(m_color)); painter->drawPolygon(arrowHead); painter->restore(); } - diff --git a/src/paint_board/shape/blur_shape.cpp b/src/paint_board/shape/blur_shape.cpp new file mode 100644 index 0000000..3332a21 --- /dev/null +++ b/src/paint_board/shape/blur_shape.cpp @@ -0,0 +1,65 @@ +#include "blur_shape.h" + +#include +#include + +BlurShape::BlurShape(const QPoint &startPoint, int brushRadius, int blurRadius) + : Shape(Qt::transparent, blurRadius) + , m_brushRadius(qMax(4, brushRadius)) + , m_blurRadius(qMax(2, blurRadius)) +{ + m_points.append(startPoint); +} + +void BlurShape::setEndPoint(const QPoint &point) +{ + if (!m_points.isEmpty() && m_points.last() == point) { + return; + } + m_points.append(point); +} + +void BlurShape::draw(QPainter *painter) +{ + if (!painter || m_blurredSnapshot.isNull() || m_points.isEmpty()) { + return; + } + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + for (const QPoint &point : m_points) { + QPainterPath path; + path.addEllipse(QPointF(point), m_brushRadius, m_brushRadius); + painter->save(); + painter->setClipPath(path); + painter->drawImage(QPoint(0, 0), m_blurredSnapshot); + painter->restore(); + } + painter->restore(); +} + +void BlurShape::setCanvasSnapshot(const QImage &snapshot) +{ + if (snapshot.isNull()) { + return; + } + m_canvasSnapshot = snapshot.convertToFormat(QImage::Format_ARGB32); + m_blurredSnapshot = makeBlurredImage(m_canvasSnapshot); +} + +QImage BlurShape::makeBlurredImage(const QImage &source) const +{ + if (source.isNull()) { + return {}; + } + + const int downscale = qMax(1, m_blurRadius); + const QSize smallSize(qMax(1, source.width() / downscale), + qMax(1, source.height() / downscale)); + const QImage small = source.scaled(smallSize, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + return small.scaled(source.size(), + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); +} diff --git a/src/paint_board/shape/blur_shape.h b/src/paint_board/shape/blur_shape.h new file mode 100644 index 0000000..14f45c4 --- /dev/null +++ b/src/paint_board/shape/blur_shape.h @@ -0,0 +1,29 @@ +#ifndef BLUR_SHAPE_H +#define BLUR_SHAPE_H + +#include "shape.h" +#include +#include + +class BlurShape : public Shape +{ +public: + BlurShape(const QPoint& startPoint, + int brushRadius = 20, + int blurRadius = 8); + + void setEndPoint(const QPoint& point) override; + void draw(QPainter* painter) override; + void setCanvasSnapshot(const QImage& snapshot); + +private: + QImage makeBlurredImage(const QImage& source) const; + + QVector m_points; + QImage m_canvasSnapshot; + QImage m_blurredSnapshot; + int m_brushRadius; + int m_blurRadius; +}; + +#endif // BLUR_SHAPE_H diff --git a/src/paint_board/shape/ellipse_shape.cpp b/src/paint_board/shape/ellipse_shape.cpp index 146bb5d..46e7f74 100644 --- a/src/paint_board/shape/ellipse_shape.cpp +++ b/src/paint_board/shape/ellipse_shape.cpp @@ -12,8 +12,6 @@ void EllipseShape::draw(QPainter *painter) painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->setRenderHint(QPainter::Antialiasing); - // 用两点构造外接矩形来绘制椭圆,normalized() 处理反向拖动 painter->drawEllipse(QRect(m_startPoint, m_endPoint).normalized()); painter->restore(); } - diff --git a/src/paint_board/shape/mosaic_shape.cpp b/src/paint_board/shape/mosaic_shape.cpp index 67740bc..1d34e41 100644 --- a/src/paint_board/shape/mosaic_shape.cpp +++ b/src/paint_board/shape/mosaic_shape.cpp @@ -57,7 +57,10 @@ void MosaicShape::draw(QPainter* painter) const QPoint& blockOrigin = it.key(); const QColor& color = it.value(); - QRect blockRect(blockOrigin.x(), blockOrigin.y(), m_blockSize, m_blockSize); + QRectF blockRect(blockOrigin.x(), + blockOrigin.y(), + m_blockSize, + m_blockSize); // 无需裁剪到背景图范围,因为我们不再依赖背景图 // 但为了保证安全,可以检查坐标是否为正 diff --git a/src/paint_board/shape/number_shape.cpp b/src/paint_board/shape/number_shape.cpp index e4e3cad..1844411 100644 --- a/src/paint_board/shape/number_shape.cpp +++ b/src/paint_board/shape/number_shape.cpp @@ -25,9 +25,11 @@ void NumberShape::draw(QPainter* painter) painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); painter->setRenderHint(QPainter::TextAntialiasing, true); - const int r = qMax(10, m_size * 2 + 6); - const QRect circleRect(m_center.x() - r, m_center.y() - r, r * 2, r * 2); + const QRect circleRect(m_center.x() - r, + m_center.y() - r, + r * 2, + r * 2); painter->setPen(QPen(Qt::white, qMax(2, m_size / 2))); painter->setBrush(m_color); diff --git a/src/paint_board/shape/pen_shape.cpp b/src/paint_board/shape/pen_shape.cpp index 5dcdbf5..d1963b3 100644 --- a/src/paint_board/shape/pen_shape.cpp +++ b/src/paint_board/shape/pen_shape.cpp @@ -1,4 +1,4 @@ -#include "pen_shape.h" +#include "pen_shape.h" PenShape::PenShape(const QPoint &startPoint, const QColor &color, int size) : Shape(color, size) @@ -25,14 +25,11 @@ void PenShape::draw(QPainter *painter) return; painter->save(); - QPen pen(m_color, m_size); pen.setCapStyle(Qt::RoundCap); // 端点圆滑 pen.setJoinStyle(Qt::RoundJoin); // 转折点圆滑 painter->setPen(pen); painter->setRenderHint(QPainter::Antialiasing); - - // 将所有点连成折线 painter->drawPolyline(QPolygon(m_points)); painter->restore(); diff --git a/src/paint_board/shape/rect_shape.cpp b/src/paint_board/shape/rect_shape.cpp index 6b9fc9b..eecd757 100644 --- a/src/paint_board/shape/rect_shape.cpp +++ b/src/paint_board/shape/rect_shape.cpp @@ -12,8 +12,6 @@ void RectShape::draw(QPainter *painter) painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->setRenderHint(QPainter::Antialiasing); - // normalized() 处理从右下往左上拖动的情况 painter->drawRect(QRect(m_startPoint, m_endPoint).normalized()); painter->restore(); } - diff --git a/src/paint_board/shape/shape_type.h b/src/paint_board/shape/shape_type.h index f811c13..8cbc73e 100644 --- a/src/paint_board/shape/shape_type.h +++ b/src/paint_board/shape/shape_type.h @@ -10,7 +10,8 @@ enum Shapeype MOSAIC, TEXT, NUMBER, - HIGHLIGHT + HIGHLIGHT, + BLUR }; #endif // SHAPETYPE_H diff --git a/src/paint_board/shape/text_shape.cpp b/src/paint_board/shape/text_shape.cpp index d23c0e0..984d017 100644 --- a/src/paint_board/shape/text_shape.cpp +++ b/src/paint_board/shape/text_shape.cpp @@ -27,7 +27,6 @@ void TextShape::draw(QPainter* painter) painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); painter->setRenderHint(QPainter::TextAntialiasing, true); - QFont f = painter->font(); f.setPixelSize(qMax(12, m_size * 4)); painter->setFont(f); diff --git a/src/viewmodel/screenshot_view_model.cpp b/src/viewmodel/screenshot_view_model.cpp index d669edf..fbbdcc4 100644 --- a/src/viewmodel/screenshot_view_model.cpp +++ b/src/viewmodel/screenshot_view_model.cpp @@ -10,13 +10,32 @@ #include #include +#ifdef Q_OS_WIN +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif + namespace { qreal captureDprForRect(const QRect& rect) { #ifdef Q_OS_WIN - if (QScreen* screen = QGuiApplication::screenAt(rect.center())) { - return screen->devicePixelRatio(); + const POINT centerPoint { + rect.center().x(), + rect.center().y() + }; + if (HMONITOR monitor = MonitorFromPoint(centerPoint, MONITOR_DEFAULTTONEAREST)) { + using GetDpiForMonitorFn = HRESULT (WINAPI *)(HMONITOR, MONITOR_DPI_TYPE, UINT *, UINT *); + static const auto getDpiForMonitor = reinterpret_cast( + GetProcAddress(LoadLibraryW(L"Shcore.dll"), "GetDpiForMonitor")); + if (getDpiForMonitor) { + UINT dpiX = 0; + UINT dpiY = 0; + if (SUCCEEDED(getDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY)) && dpiX > 0) { + return qreal(dpiX) / 96.0; + } + } } #endif if (QScreen* screen = QGuiApplication::primaryScreen()) { @@ -25,6 +44,20 @@ qreal captureDprForRect(const QRect& rect) return 1.0; } +qreal effectiveCaptureDpr(QQuickItem *paintBoard, const QRect &rect) +{ + if (!rect.isEmpty()) { + return captureDprForRect(rect); + } + if (paintBoard && paintBoard->window()) { + return paintBoard->window()->devicePixelRatio(); + } + if (QScreen *screen = QGuiApplication::primaryScreen()) { + return screen->devicePixelRatio(); + } + return 1.0; +} + QRect captureRectInSnapshot(const DesktopSnapshot& snapshot, const QRect& rect, qreal dpr) { if (snapshot.isNull()) { @@ -35,6 +68,11 @@ QRect captureRectInSnapshot(const DesktopSnapshot& snapshot, const QRect& rect, return snapshot.image().rect(); } +#ifdef Q_OS_WIN + Q_UNUSED(dpr); + const QRect localRect = rect.translated(-snapshot.virtualGeometry().topLeft()); + return localRect.intersected(snapshot.image().rect()); +#else const QRect localRect = rect.translated(-snapshot.virtualGeometry().topLeft()); const QPoint topLeft = QPoint(qRound(localRect.left() * dpr), qRound(localRect.top() * dpr)); @@ -43,6 +81,7 @@ QRect captureRectInSnapshot(const DesktopSnapshot& snapshot, const QRect& rect, QRect target; target.setCoords(topLeft.x(), topLeft.y(), bottomRight.x(), bottomRight.y()); return target.intersected(snapshot.image().rect()); +#endif } } @@ -140,6 +179,11 @@ QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, cons return {}; } + const qreal dpr = effectiveCaptureDpr(paintBoard, r); + if (dpr > 0.0) { + img.setDevicePixelRatio(dpr); + } + return m_stickyStore.storeImage(img); } @@ -199,11 +243,7 @@ QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard, qreal dpr = 1.0; if (!m_snapshot.isNull()) { - if (board && board->window()) { - dpr = board->window()->devicePixelRatio(); - } else if (!rect.isEmpty()) { - dpr = captureDprForRect(rect); - } + dpr = effectiveCaptureDpr(paintBoard, rect); const QRect captureRect = captureRectInSnapshot(m_snapshot, rect, dpr); screenshot = m_snapshot.image().copy(captureRect).toImage(); } else { @@ -249,9 +289,9 @@ QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard, if (setBackground) { QImage background = screenshot; - if (board->window() && board->window()->devicePixelRatio() > 1.0) { - const QSize logicalSize(qRound(background.width() / board->window()->devicePixelRatio()), - qRound(background.height() / board->window()->devicePixelRatio())); + if (dpr > 1.0) { + const QSize logicalSize(qRound(background.width() / dpr), + qRound(background.height() / dpr)); if (logicalSize.isValid() && logicalSize != background.size()) { background = background.scaled(logicalSize, Qt::IgnoreAspectRatio, diff --git a/src/viewmodel/sticky_view_model.cpp b/src/viewmodel/sticky_view_model.cpp index 5e6394f..d87bd5e 100644 --- a/src/viewmodel/sticky_view_model.cpp +++ b/src/viewmodel/sticky_view_model.cpp @@ -1,10 +1,13 @@ -#include "sticky_view_model.h" -#include -#include +#include "sticky_view_model.h" + +#include "ai_view_model.h" +#include "widgets/sticky_pin_widget.h" + #include +#include #include +#include #include -#include #ifdef Q_OS_WIN #include @@ -16,6 +19,11 @@ StickyViewModel::StickyViewModel(StickyImageStore &store, QObject *parent) { } +void StickyViewModel::setAiViewModel(AIViewModel *aiViewModel) +{ + m_aiViewModel = aiViewModel; +} + void StickyViewModel::requestSticky(const QString &imageUrl, const QRect &imgRect) { if (imageUrl.isEmpty()) { @@ -23,16 +31,14 @@ void StickyViewModel::requestSticky(const QString &imageUrl, const QRect &imgRec } QRect targetRect = imgRect.normalized(); - QScreen *screen = QGuiApplication::screenAt(targetRect.topLeft()); - if (!screen) { - screen = QGuiApplication::screenAt(targetRect.center()); +#ifdef Q_OS_WIN + const QImage image = m_store.getImageByUrl(imageUrl); + if (!image.isNull()) { + auto *widget = new StickyPinWidget(imageUrl, targetRect, image, &m_store, m_aiViewModel); + Q_UNUSED(widget); + return; } - if (!screen) { - screen = QGuiApplication::primaryScreen(); - } - - QImage image = m_store.getImageByUrl(imageUrl); - +#endif emit stickyReady(imageUrl, targetRect); } @@ -51,22 +57,58 @@ void StickyViewModel::positionStickyWindow(QObject *windowObject, const QRect &i const QRect targetRect = imgRect.normalized(); const QPoint topLeft = targetRect.topLeft(); +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(window->winId())) { + SetWindowPos(hwnd, + HWND_TOPMOST, + topLeft.x(), + topLeft.y(), + 0, + 0, + SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + return; + } +#endif + + QScreen *screen = QGuiApplication::screenAt(topLeft); + if (!screen) { + screen = QGuiApplication::screenAt(targetRect.center()); + } + if (!screen) { + screen = QGuiApplication::primaryScreen(); + } + + if (screen && window->screen() != screen) { + window->setScreen(screen); + } + window->setPosition(topLeft); +} + +void StickyViewModel::resizeStickyWindow(QObject *windowObject, int physicalWidth, int physicalHeight) const +{ + auto *window = qobject_cast(windowObject); + if (!window) { + return; + } + + const int targetWidth = qMax(1, physicalWidth); + const int targetHeight = qMax(1, physicalHeight); #ifdef Q_OS_WIN if (HWND hwnd = reinterpret_cast(window->winId())) { - RECT rect{}; - GetWindowRect(hwnd, &rect); - qDebug() << "[StickyHWND] qt-pos:" - << topLeft - << "hwnd:" - << QRect(rect.left, - rect.top, - rect.right - rect.left, - rect.bottom - rect.top) - << "qmlPos:" << window->x() << window->y(); + SetWindowPos(hwnd, + nullptr, + 0, + 0, + targetWidth, + targetHeight, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + return; } #endif + + window->resize(targetWidth, targetHeight); } bool StickyViewModel::saveImage(const QString &imageUrl, const QUrl &targetUrl) @@ -86,7 +128,11 @@ QImage StickyViewModel::getImageByUrl(const QString &imageUrl) const QSize StickyViewModel::getImageSizeByUrl(const QString &imageUrl) const { - return m_store.getImageByUrl(imageUrl).size(); + const QImage image = m_store.getImageByUrl(imageUrl); + if (image.isNull()) { + return {}; + } + return image.size(); } QImage StickyViewModel::mergeLayers(const QString &imageUrl, const QImage &annotationLayer) const diff --git a/src/viewmodel/sticky_view_model.h b/src/viewmodel/sticky_view_model.h index afe07a6..f241190 100644 --- a/src/viewmodel/sticky_view_model.h +++ b/src/viewmodel/sticky_view_model.h @@ -7,6 +7,7 @@ #include "sticky_image_store.h" class QScreen; +class AIViewModel; // StickyViewModel // --------------- @@ -21,6 +22,7 @@ class StickyViewModel : public QObject public: explicit StickyViewModel(StickyImageStore &store, QObject *parent = nullptr); + void setAiViewModel(AIViewModel *aiViewModel); // 通知 QML 创建贴图窗口,由 ScreenshotViewModel::captureRectToStickyUrl 产生的 URL 触发 // QML 监听 stickyReady(imageUrl, imgRect) 信号后创建 TZStickyWindow @@ -33,6 +35,7 @@ public: Q_INVOKABLE QImage getImageByUrl(const QString &imageUrl) const; Q_INVOKABLE QSize getImageSizeByUrl(const QString &imageUrl) const; Q_INVOKABLE void positionStickyWindow(QObject *windowObject, const QRect &imgRect) const; + Q_INVOKABLE void resizeStickyWindow(QObject *windowObject, int physicalWidth, int physicalHeight) const; Q_INVOKABLE bool overwriteWithAnnotations(const QString &imageUrl, const QImage &annotationLayer); Q_INVOKABLE bool saveMergedImage(const QString &imageUrl, const QImage &annotationLayer, const QUrl &targetUrl); Q_INVOKABLE bool rotateImage(const QString &imageUrl, int degreesClockwise); @@ -46,6 +49,7 @@ private: QImage mergeLayers(const QString &imageUrl, const QImage &annotationLayer) const; StickyImageStore &m_store; + AIViewModel *m_aiViewModel = nullptr; }; #endif // STICKY_VIEW_MODEL_H diff --git a/src/widgets/sticky_canvas_widget.cpp b/src/widgets/sticky_canvas_widget.cpp new file mode 100644 index 0000000..292d00d --- /dev/null +++ b/src/widgets/sticky_canvas_widget.cpp @@ -0,0 +1,262 @@ +#include "widgets/sticky_canvas_widget.h" + +#include "paint_board/shape/blur_shape.h" +#include "paint_board/shape/mosaic_shape.h" + +#include +#include + +StickyCanvasWidget::StickyCanvasWidget(QWidget *parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_OpaquePaintEvent, false); + setAttribute(Qt::WA_TransparentForMouseEvents, true); + setMouseTracking(true); +} + +StickyCanvasWidget::~StickyCanvasWidget() +{ + clearShapes(); +} + +void StickyCanvasWidget::setBackgroundImage(const QImage &image) +{ + m_backgroundImage = image; + for (Shape *shape : m_shapes) { + if (auto *mosaic = dynamic_cast(shape)) { + mosaic->setCanvasSnapshot(m_backgroundImage); + } else if (auto *blur = dynamic_cast(shape)) { + blur->setCanvasSnapshot(m_backgroundImage); + } + } + update(); +} + +void StickyCanvasWidget::setViewScale(qreal scale) +{ + const qreal next = qMax(0.01, scale); + if (qFuzzyCompare(m_viewScale, next)) { + return; + } + m_viewScale = next; + update(); +} + +void StickyCanvasWidget::setActiveShapeType(Shapeype type) +{ + m_shapeType = type; +} + +void StickyCanvasWidget::setPenColor(const QColor &color) +{ + m_penColor = color; +} + +void StickyCanvasWidget::setPenSize(int size) +{ + m_penSize = qMax(1, size); +} + +void StickyCanvasWidget::setDrawingEnabled(bool enabled) +{ + m_drawingEnabled = enabled; + setAttribute(Qt::WA_TransparentForMouseEvents, !m_drawingEnabled); + if (!m_drawingEnabled) { + finishCurrentShape(); + } +} + +void StickyCanvasWidget::setAnnotationText(const QString &text) +{ + m_annotationText = text; +} + +void StickyCanvasWidget::setTextBackgroundEnabled(bool enabled) +{ + m_textBackgroundEnabled = enabled; +} + +void StickyCanvasWidget::setNumberValue(int value) +{ + m_numberValue = qMax(1, value); +} + +void StickyCanvasWidget::setNumberAutoIncrement(bool enabled) +{ + m_numberAutoIncrement = enabled; +} + +void StickyCanvasWidget::addTextAnnotation(const QPoint &point, const QString &text) +{ + const QString resolved = text.trimmed(); + if (resolved.isEmpty()) { + return; + } + + const QPoint imagePoint(qRound(point.x() / m_viewScale), + qRound(point.y() / m_viewScale)); + + Shape *textShape = m_factory.createTextShape(imagePoint, + resolved, + m_penColor, + m_penSize, + m_textBackgroundEnabled); + if (!textShape) { + return; + } + + m_shapes.append(textShape); + m_annotationText = resolved; + update(); +} + +void StickyCanvasWidget::undo() +{ + if (!m_shapes.isEmpty()) { + delete m_shapes.takeLast(); + update(); + } +} + +void StickyCanvasWidget::reset() +{ + finishCurrentShape(); + clearShapes(); + m_backgroundImage = QImage(); + update(); +} + +void StickyCanvasWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.fillRect(rect(), Qt::white); + + if (!m_backgroundImage.isNull()) { + painter.drawImage(rect(), m_backgroundImage); + } + + painter.save(); + painter.scale(m_viewScale, m_viewScale); + for (Shape *shape : m_shapes) { + if (shape) { + shape->draw(&painter); + } + } + + if (m_currentShape) { + m_currentShape->draw(&painter); + } + painter.restore(); +} + +void StickyCanvasWidget::mousePressEvent(QMouseEvent *event) +{ + if (!m_drawingEnabled || event->button() != Qt::LeftButton) { + QWidget::mousePressEvent(event); + return; + } + + const QPoint displayPoint = event->position().toPoint(); + m_startPoint = QPoint(qRound(displayPoint.x() / m_viewScale), + qRound(displayPoint.y() / m_viewScale)); + + if (m_shapeType == TEXT) { + emit textPlacementRequested(displayPoint); + return; + } + + if (m_shapeType == NUMBER) { + Shape *numberShape = m_factory.createNumberShape(m_startPoint, + m_numberValue, + m_penColor, + m_penSize); + if (numberShape) { + m_shapes.append(numberShape); + if (m_numberAutoIncrement) { + ++m_numberValue; + emit numberValueChanged(m_numberValue); + } + update(); + } + return; + } + + delete m_currentShape; + m_currentShape = nullptr; + + if (m_shapeType == MOSAIC) { + auto *mosaic = new MosaicShape(m_startPoint, + qMax(10, m_penSize * 5), + qMax(2, m_penSize)); + mosaic->setCanvasSnapshot(m_backgroundImage); + m_currentShape = mosaic; + m_draggingShape = true; + update(); + return; + } + + if (m_shapeType == BLUR) { + auto *blur = new BlurShape(m_startPoint, + qMax(10, m_penSize * 5), + qMax(2, m_penSize)); + blur->setCanvasSnapshot(m_backgroundImage); + m_currentShape = blur; + m_draggingShape = true; + update(); + return; + } + + m_currentShape = m_factory.createShape(m_shapeType, + m_startPoint, + m_startPoint, + m_penColor, + m_penSize); + m_draggingShape = (m_currentShape != nullptr); + update(); +} + +void StickyCanvasWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_draggingShape || !m_currentShape) { + QWidget::mouseMoveEvent(event); + return; + } + + const QPoint imagePoint(qRound(event->position().x() / m_viewScale), + qRound(event->position().y() / m_viewScale)); + m_currentShape->setEndPoint(imagePoint); + update(); +} + +void StickyCanvasWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (!m_draggingShape || !m_currentShape || event->button() != Qt::LeftButton) { + QWidget::mouseReleaseEvent(event); + return; + } + + const QPoint imagePoint(qRound(event->position().x() / m_viewScale), + qRound(event->position().y() / m_viewScale)); + m_currentShape->setEndPoint(imagePoint); + m_shapes.append(m_currentShape); + m_currentShape = nullptr; + m_draggingShape = false; + update(); +} + +void StickyCanvasWidget::finishCurrentShape() +{ + if (m_currentShape) { + delete m_currentShape; + m_currentShape = nullptr; + } + m_draggingShape = false; +} + +void StickyCanvasWidget::clearShapes() +{ + qDeleteAll(m_shapes); + m_shapes.clear(); +} diff --git a/src/widgets/sticky_canvas_widget.h b/src/widgets/sticky_canvas_widget.h new file mode 100644 index 0000000..fd043bc --- /dev/null +++ b/src/widgets/sticky_canvas_widget.h @@ -0,0 +1,68 @@ +#ifndef STICKY_CANVAS_WIDGET_H +#define STICKY_CANVAS_WIDGET_H + +#include +#include +#include +#include + +#include "paint_board/shape/shape.h" +#include "paint_board/shape/shape_factory.h" +#include "paint_board/shape/shape_type.h" + +class StickyCanvasWidget : public QWidget +{ + Q_OBJECT + +public: + explicit StickyCanvasWidget(QWidget *parent = nullptr); + ~StickyCanvasWidget() override; + + void setBackgroundImage(const QImage &image); + void setViewScale(qreal scale); + void setActiveShapeType(Shapeype type); + void setPenColor(const QColor &color); + void setPenSize(int size); + void setDrawingEnabled(bool enabled); + void setAnnotationText(const QString &text); + void setTextBackgroundEnabled(bool enabled); + void setNumberValue(int value); + void setNumberAutoIncrement(bool enabled); + bool drawingEnabled() const { return m_drawingEnabled; } + Shapeype activeShapeType() const { return m_shapeType; } + void addTextAnnotation(const QPoint &point, const QString &text); + void undo(); + void reset(); + +signals: + void numberValueChanged(int value); + void textPlacementRequested(const QPoint &point); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private: + void finishCurrentShape(); + void clearShapes(); + + ShapeFactory m_factory; + QList m_shapes; + Shape *m_currentShape = nullptr; + QImage m_backgroundImage; + QColor m_penColor = Qt::red; + int m_penSize = 6; + Shapeype m_shapeType = PEN; + QString m_annotationText = QStringLiteral("Text"); + bool m_textBackgroundEnabled = true; + bool m_numberAutoIncrement = true; + int m_numberValue = 1; + bool m_drawingEnabled = false; + bool m_draggingShape = false; + qreal m_viewScale = 1.0; + QPoint m_startPoint; +}; + +#endif // STICKY_CANVAS_WIDGET_H diff --git a/src/widgets/sticky_pin_widget.cpp b/src/widgets/sticky_pin_widget.cpp new file mode 100644 index 0000000..84b7547 --- /dev/null +++ b/src/widgets/sticky_pin_widget.cpp @@ -0,0 +1,1073 @@ +#include "widgets/sticky_pin_widget.h" + +#include "viewmodel/ai_view_model.h" +#include "widgets/sticky_canvas_widget.h" +#include "sticky_image_store.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif + +namespace { + +QString loadStickyPinStyleSheet() +{ + QFile file(QStringLiteral(":/resource/qss/sticky_pin_widget.qss")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + return QString::fromUtf8(file.readAll()); +} + +qreal monitorScaleForPoint(const QPoint &physicalPoint) +{ +#ifdef Q_OS_WIN + const POINT nativePoint { physicalPoint.x(), physicalPoint.y() }; + if (HMONITOR monitor = MonitorFromPoint(nativePoint, MONITOR_DEFAULTTONEAREST)) { + using GetDpiForMonitorFn = HRESULT (WINAPI *)(HMONITOR, MONITOR_DPI_TYPE, UINT *, UINT *); + static const auto getDpiForMonitor = reinterpret_cast( + GetProcAddress(LoadLibraryW(L"Shcore.dll"), "GetDpiForMonitor")); + if (getDpiForMonitor) { + UINT dpiX = 0; + UINT dpiY = 0; + if (SUCCEEDED(getDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY)) && dpiX > 0) { + return qreal(dpiX) / 96.0; + } + } + } +#endif + if (QScreen *screen = QGuiApplication::primaryScreen()) { + return screen->devicePixelRatio(); + } + return 1.0; +} + +class StickyAiDialog : public QDialog +{ +public: + explicit StickyAiDialog(const QImage &previewImage, QWidget *parent = nullptr) + : QDialog(parent) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + setAttribute(Qt::WA_DeleteOnClose, false); + setModal(true); + setFixedSize(420, 380); + setObjectName(QStringLiteral("stickyAiDialog")); + setStyleSheet(QStringLiteral( + "#stickyAiDialog { background:#FFFFFF; border:1px solid #E2E8F0; border-radius:10px; }" + "#stickyAiTitle { color:#1E293B; font-size:14px; font-weight:600; }" + "#stickyAiSubtle { color:#94A3B8; font-size:10px; }" + "#stickyAiPreview { background:#F1F5F9; border:1px solid #E2E8F0; border-radius:6px; }" + "#stickyAiInput { background:#F8FAFC; border:1px solid #E2E8F0; border-radius:8px; padding:8px; color:#1E293B; }" + "#stickyAiCancel { background:#F8FAFC; border:1px solid #E2E8F0; border-radius:6px; color:#64748B; padding:6px 14px; }" + "#stickyAiCancel:hover { background:#F1F5F9; }" + "#stickyAiSend { background:#0088FF; border:none; border-radius:6px; color:white; padding:6px 14px; }" + "#stickyAiSend:hover { background:#3399FF; }" + "#stickyAiClose { background:transparent; border:none; color:#64748B; font-size:14px; }" + "#stickyAiClose:hover { background:#FEE2E2; border-radius:6px; color:#B91C1C; }")); + + auto *root = new QVBoxLayout(this); + root->setContentsMargins(12, 12, 12, 12); + root->setSpacing(8); + + auto *titleBar = new QHBoxLayout; + titleBar->setContentsMargins(0, 0, 0, 0); + titleBar->setSpacing(8); + + auto *icon = new QLabel(this); + icon->setPixmap(QIcon(QStringLiteral(":/resource/img/lc_chat.svg")).pixmap(20, 20)); + icon->setFixedSize(20, 20); + titleBar->addWidget(icon); + + auto *title = new QLabel(tr("AI 图像编辑"), this); + title->setObjectName(QStringLiteral("stickyAiTitle")); + titleBar->addWidget(title); + titleBar->addStretch(); + + auto *closeButton = new QPushButton(QStringLiteral("×"), this); + closeButton->setObjectName(QStringLiteral("stickyAiClose")); + closeButton->setFixedSize(28, 28); + connect(closeButton, &QPushButton::clicked, this, &QDialog::reject); + titleBar->addWidget(closeButton); + root->addLayout(titleBar); + + auto *previewBox = new QFrame(this); + previewBox->setObjectName(QStringLiteral("stickyAiPreview")); + previewBox->setFixedHeight(64); + auto *previewLayout = new QHBoxLayout(previewBox); + previewLayout->setContentsMargins(8, 8, 8, 8); + previewLayout->setSpacing(10); + + auto *thumb = new QLabel(previewBox); + thumb->setFixedSize(48, 48); + thumb->setStyleSheet(QStringLiteral("background:#E2E8F0; border-radius:4px;")); + if (!previewImage.isNull()) { + thumb->setPixmap(QPixmap::fromImage(previewImage).scaled(48, 48, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + } + previewLayout->addWidget(thumb); + + auto *metaCol = new QVBoxLayout; + metaCol->setContentsMargins(0, 0, 0, 0); + metaCol->setSpacing(3); + auto *label1 = new QLabel(tr("当前贴图"), previewBox); + label1->setStyleSheet(QStringLiteral("color:#94A3B8; font-size:11px;")); + metaCol->addWidget(label1); + auto *label2 = new QLabel(tr("发送后将对此图执行 AI 编辑"), previewBox); + label2->setObjectName(QStringLiteral("stickyAiSubtle")); + metaCol->addWidget(label2); + metaCol->addStretch(); + previewLayout->addLayout(metaCol, 1); + root->addWidget(previewBox); + + m_input = new QPlainTextEdit(this); + m_input->setObjectName(QStringLiteral("stickyAiInput")); + m_input->setPlaceholderText(tr("描述您想要的修改...")); + root->addWidget(m_input, 1); + + auto *bottom = new QHBoxLayout; + bottom->setContentsMargins(0, 0, 0, 0); + bottom->addStretch(); + + auto *cancelButton = new QPushButton(tr("取消"), this); + cancelButton->setObjectName(QStringLiteral("stickyAiCancel")); + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); + bottom->addWidget(cancelButton); + + auto *sendButton = new QPushButton(tr("发送"), this); + sendButton->setObjectName(QStringLiteral("stickyAiSend")); + connect(sendButton, &QPushButton::clicked, this, [this]() { + if (!m_input->toPlainText().trimmed().isEmpty()) { + accept(); + } + }); + bottom->addWidget(sendButton); + root->addLayout(bottom); + } + + QString prompt() const + { + return m_input ? m_input->toPlainText().trimmed() : QString(); + } + +protected: + void mousePressEvent(QMouseEvent *event) override + { + if (event->button() == Qt::LeftButton) { + m_dragOffset = event->globalPosition().toPoint() - frameGeometry().topLeft(); + } + QDialog::mousePressEvent(event); + } + + void mouseMoveEvent(QMouseEvent *event) override + { + if (event->buttons() & Qt::LeftButton) { + move(event->globalPosition().toPoint() - m_dragOffset); + } + QDialog::mouseMoveEvent(event); + } + +private: + QPlainTextEdit *m_input = nullptr; + QPoint m_dragOffset; +}; + +} + +StickyPinWidget::StickyPinWidget(const QString &imageUrl, + const QRect &physicalRect, + const QImage &image, + StickyImageStore *store, + AIViewModel *aiViewModel, + QWidget *parent) + : QWidget(parent) + , m_imageUrl(imageUrl) + , m_physicalRect(physicalRect.normalized()) + , m_image(image) + , m_store(store) + , m_aiViewModel(aiViewModel) +{ + setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_TranslucentBackground); + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + setContextMenuPolicy(Qt::DefaultContextMenu); + setStyleSheet(loadStickyPinStyleSheet()); + + const qreal dpr = screenScaleForRect(m_physicalRect); + if (!m_image.isNull() && dpr > 0.0 && !qFuzzyCompare(m_image.devicePixelRatio(), dpr)) { + m_image.setDevicePixelRatio(dpr); + } + + m_imageDisplaySize = QSize(qMax(1, qRound(m_image.width() / qMax(1.0, m_image.devicePixelRatio()))), + qMax(1, qRound(m_image.height() / qMax(1.0, m_image.devicePixelRatio())))); + m_baseImageDisplaySize = m_imageDisplaySize; + + m_canvas = new StickyCanvasWidget(this); + m_canvas->setBackgroundImage(m_image); + m_canvas->setViewScale(m_zoomFactor); + m_canvas->setPenColor(m_currentPenColor); + m_canvas->setPenSize(m_currentPenSize); + m_canvas->setActiveShapeType(PEN); + m_canvas->setAnnotationText(m_annotationText); + m_canvas->setTextBackgroundEnabled(m_textBackgroundEnabled); + m_canvas->setNumberAutoIncrement(m_numberAutoIncrement); + m_canvas->setNumberValue(m_numberValue); + connect(m_canvas, &StickyCanvasWidget::numberValueChanged, this, [this](int value) { + m_numberValue = value; + if (m_numberValueSpin && m_numberValueSpin->value() != value) { + m_numberValueSpin->setValue(value); + } + if (m_numberLabel) { + m_numberLabel->setText(QString::number(value)); + } + }); + connect(m_canvas, &StickyCanvasWidget::textPlacementRequested, this, [this](const QPoint &point) { + m_pendingTextPoint = point; + if (!m_inlineTextEditor) { + return; + } + m_inlineTextEditor->setText(m_annotationText); + const QRect canvasRect = contentRect(); + const int editorW = 180; + const int editorH = 32; + int x = canvasRect.x() + point.x(); + int y = canvasRect.y() + point.y() + 6; + x = qMax(canvasRect.x(), qMin(x, canvasRect.right() - editorW + 1)); + y = qMax(canvasRect.y(), qMin(y, canvasRect.bottom() - editorH + 1)); + m_inlineTextEditor->setGeometry(x, y, editorW, editorH); + m_inlineTextEditor->show(); + m_inlineTextEditor->raise(); + m_inlineTextEditor->setFocus(); + m_inlineTextEditor->selectAll(); + }); + if (m_aiViewModel) { + connect(m_aiViewModel, &AIViewModel::signalRequestComplete, + this, &StickyPinWidget::applyAiImage); + connect(m_aiViewModel, &AIViewModel::signalRequestFailed, this, + [this](const QString &errorMsg) { + QMessageBox::warning(this, tr("AI 编辑失败"), errorMsg); + }); + } + + m_toolOptions = new QWidget(this); + m_toolOptions->setObjectName(QStringLiteral("stickyToolOptions")); + + const QVector colors { + QColor("#F43F5E"), QColor("#EF4444"), QColor("#F97316"), QColor("#EAB308"), + QColor("#22C55E"), QColor("#3B82F6"), QColor("#8B5CF6"), QColor("#000000"), QColor("#FFFFFF") + }; + for (const QColor &color : colors) { + auto *button = new QToolButton(m_toolOptions); + button->setCheckable(true); + button->setAutoRaise(false); + button->setToolButtonStyle(Qt::ToolButtonIconOnly); + button->setCursor(Qt::PointingHandCursor); + button->setStyleSheet(QStringLiteral( + "QToolButton { border: 1px solid #CBD5E1; border-radius: 9px; background-color: %1; }" + "QToolButton:checked { border: 2px solid #3B82F6; background-color: %1; }" + ).arg(color.name())); + connect(button, &QToolButton::clicked, this, [this, button, color]() { + for (QToolButton *other : m_colorButtons) { + if (other != button) { + other->setChecked(false); + } + } + button->setChecked(true); + m_currentPenColor = color; + syncCanvasToolSettings(); + }); + m_colorButtons.append(button); + } + + struct SizePreset { int value; const char *label; }; + const QVector sizePresets { + { 2, "细" }, { 4, "中" }, { 6, "粗" }, { 10, "特粗" } + }; + for (const SizePreset &preset : sizePresets) { + auto *button = new QToolButton(m_toolOptions); + button->setCheckable(true); + button->setText(QString::fromUtf8(preset.label)); + button->setCursor(Qt::PointingHandCursor); + connect(button, &QToolButton::clicked, this, [this, button, preset]() { + for (QToolButton *other : m_sizeButtons) { + if (other != button) { + other->setChecked(false); + } + } + button->setChecked(true); + m_currentPenSize = preset.value; + syncCanvasToolSettings(); + }); + m_sizeButtons.append(button); + } + + m_textInput = new QLineEdit(m_toolOptions); + m_textInput->setPlaceholderText(tr("输入文字")); + m_textInput->setText(m_annotationText); + connect(m_textInput, &QLineEdit::textChanged, this, [this](const QString &text) { + m_annotationText = text; + if (m_canvas) { + m_canvas->setAnnotationText(m_annotationText); + } + }); + + m_inlineTextEditor = new QLineEdit(this); + m_inlineTextEditor->setObjectName(QStringLiteral("stickyInlineTextEditor")); + m_inlineTextEditor->hide(); + m_inlineTextEditor->setPlaceholderText(tr("输入文字")); + connect(m_inlineTextEditor, &QLineEdit::returnPressed, this, [this]() { + const QString text = m_inlineTextEditor->text().trimmed(); + if (m_canvas && !text.isEmpty()) { + m_annotationText = text; + if (m_textInput) { + m_textInput->setText(text); + } + m_canvas->setAnnotationText(text); + m_canvas->addTextAnnotation(m_pendingTextPoint, text); + } + m_inlineTextEditor->hide(); + }); + connect(m_inlineTextEditor, &QLineEdit::editingFinished, this, [this]() { + if (m_inlineTextEditor->isVisible()) { + m_inlineTextEditor->hide(); + } + }); + + m_textBackgroundCheck = new QCheckBox(tr("背景"), m_toolOptions); + m_textBackgroundCheck->setChecked(m_textBackgroundEnabled); + connect(m_textBackgroundCheck, &QCheckBox::toggled, this, [this](bool checked) { + m_textBackgroundEnabled = checked; + if (m_canvas) { + m_canvas->setTextBackgroundEnabled(checked); + } + }); + + m_numberAutoIncrementCheck = new QCheckBox(tr("自动递增"), m_toolOptions); + m_numberAutoIncrementCheck->setChecked(m_numberAutoIncrement); + connect(m_numberAutoIncrementCheck, &QCheckBox::toggled, this, [this](bool checked) { + m_numberAutoIncrement = checked; + if (m_canvas) { + m_canvas->setNumberAutoIncrement(checked); + } + }); + + m_numberMinusButton = new QToolButton(m_toolOptions); + m_numberMinusButton->setText(QStringLiteral("-")); + m_numberMinusButton->setCursor(Qt::PointingHandCursor); + connect(m_numberMinusButton, &QToolButton::clicked, this, [this]() { + const int nextValue = qMax(1, m_numberValue - 1); + if (m_numberValueSpin) { + m_numberValueSpin->setValue(nextValue); + } + }); + + m_numberLabel = new QLabel(QString::number(m_numberValue), m_toolOptions); + m_numberLabel->setObjectName(QStringLiteral("stickyNumberLabel")); + m_numberLabel->setAlignment(Qt::AlignCenter); + + m_numberPlusButton = new QToolButton(m_toolOptions); + m_numberPlusButton->setText(QStringLiteral("+")); + m_numberPlusButton->setCursor(Qt::PointingHandCursor); + connect(m_numberPlusButton, &QToolButton::clicked, this, [this]() { + const int nextValue = qMin(9999, m_numberValue + 1); + if (m_numberValueSpin) { + m_numberValueSpin->setValue(nextValue); + } + }); + + m_numberValueSpin = new QSpinBox(m_toolOptions); + m_numberValueSpin->setRange(1, 9999); + m_numberValueSpin->setValue(m_numberValue); + connect(m_numberValueSpin, &QSpinBox::valueChanged, this, [this](int value) { + m_numberValue = value; + if (m_numberLabel) { + m_numberLabel->setText(QString::number(value)); + } + if (m_canvas) { + m_canvas->setNumberValue(value); + } + }); + + m_toolbar = new QWidget(this); + m_toolbar->setObjectName(QStringLiteral("stickyToolbar")); + + m_pencilButton = new QToolButton(m_toolbar); + m_rectButton = new QToolButton(m_toolbar); + m_circleButton = new QToolButton(m_toolbar); + m_arrowButton = new QToolButton(m_toolbar); + m_highlightButton = new QToolButton(m_toolbar); + m_mosaicButton = new QToolButton(m_toolbar); + m_blurButton = new QToolButton(m_toolbar); + m_textButton = new QToolButton(m_toolbar); + m_numberButton = new QToolButton(m_toolbar); + m_undoButton = new QToolButton(m_toolbar); + m_aiButton = new QToolButton(m_toolbar); + m_copyButton = new QToolButton(m_toolbar); + m_saveButton = new QToolButton(m_toolbar); + m_zoomLabel = new QLabel(m_toolbar); + m_toggleToolbarButton = new QToolButton(m_toolbar); + m_closeButton = new QToolButton(m_toolbar); + + auto setupButton = [](QToolButton *button, const QString &tooltip) { + button->setToolButtonStyle(Qt::ToolButtonIconOnly); + button->setAutoRaise(false); + button->setIconSize(QSize(18, 18)); + button->setToolTip(tooltip); + button->setCursor(Qt::PointingHandCursor); + button->setContentsMargins(0, 0, 0, 0); + }; + + setupButton(m_pencilButton, tr("画笔")); + setupButton(m_rectButton, tr("矩形")); + setupButton(m_circleButton, tr("圆形")); + setupButton(m_arrowButton, tr("箭头")); + setupButton(m_highlightButton, tr("高亮")); + setupButton(m_mosaicButton, tr("马赛克")); + setupButton(m_blurButton, tr("高斯模糊")); + setupButton(m_textButton, tr("文字")); + setupButton(m_numberButton, tr("序号")); + setupButton(m_undoButton, tr("撤销")); + setupButton(m_aiButton, tr("AI 编辑")); + setupButton(m_copyButton, tr("复制")); + setupButton(m_saveButton, tr("保存")); + setupButton(m_toggleToolbarButton, tr("隐藏工具栏")); + setupButton(m_closeButton, tr("关闭")); + + m_zoomLabel->setAlignment(Qt::AlignCenter); + m_zoomLabel->setMinimumWidth(52); + m_zoomLabel->setText(QStringLiteral("100%")); + m_zoomLabel->setStyleSheet(QStringLiteral( + "color:#334155;" + "background:transparent;" + "border:none;" + "font-size:12px;" + "font-weight:500;" + )); + + m_pencilButton->setCheckable(true); + m_rectButton->setCheckable(true); + m_circleButton->setCheckable(true); + m_arrowButton->setCheckable(true); + m_highlightButton->setCheckable(true); + m_mosaicButton->setCheckable(true); + m_blurButton->setCheckable(true); + m_textButton->setCheckable(true); + m_numberButton->setCheckable(true); + m_pencilButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_pencil.svg"))); + m_rectButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_square.svg"))); + m_circleButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_circle.svg"))); + m_arrowButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_arrow.svg"))); + m_highlightButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_highlighter.svg"))); + m_mosaicButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_mosaic.svg"))); + m_blurButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_blur.svg"))); + m_textButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_text.svg"))); + m_numberButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_list_ordered.svg"))); + m_undoButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_undo.svg"))); + m_aiButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_chat.svg"))); + QIcon copyIcon = QIcon::fromTheme(QStringLiteral("edit-copy")); + if (copyIcon.isNull()) { + copyIcon = style()->standardIcon(QStyle::SP_FileDialogDetailedView); + } + m_copyButton->setIcon(copyIcon); + m_saveButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_save.svg"))); + m_toggleToolbarButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_close.svg"))); + m_closeButton->setIcon(QIcon(QStringLiteral(":/resource/img/lc_x.svg"))); + + const QVector drawButtons { + m_pencilButton, m_rectButton, m_circleButton, m_arrowButton, + m_highlightButton, m_mosaicButton, m_blurButton, m_textButton, m_numberButton + }; + auto setOnlyChecked = [drawButtons](QToolButton *activeButton) { + for (QToolButton *button : drawButtons) { + if (button != activeButton) { + button->setChecked(false); + } + } + }; + + connect(m_pencilButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_pencilButton); setActiveTool(PEN, m_pencilButton); }); + connect(m_rectButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_rectButton); setActiveTool(RECTANGLE, m_rectButton); }); + connect(m_circleButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_circleButton); setActiveTool(ELLIPSE, m_circleButton); }); + connect(m_arrowButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_arrowButton); setActiveTool(ARROW, m_arrowButton); }); + connect(m_highlightButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_highlightButton); setActiveTool(HIGHLIGHT, m_highlightButton); }); + connect(m_mosaicButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_mosaicButton); setActiveTool(MOSAIC, m_mosaicButton); }); + connect(m_blurButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_blurButton); setActiveTool(BLUR, m_blurButton); }); + connect(m_textButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_textButton); setActiveTool(TEXT, m_textButton); }); + connect(m_numberButton, &QToolButton::clicked, this, [=]() { setOnlyChecked(m_numberButton); setActiveTool(NUMBER, m_numberButton); }); + connect(m_undoButton, &QToolButton::clicked, this, [this]() { + if (m_canvas) { + m_canvas->undo(); + } + }); + connect(m_aiButton, &QToolButton::clicked, this, &StickyPinWidget::openAiEditor); + connect(m_copyButton, &QToolButton::clicked, this, [this]() { + if (m_store) { + m_store->copyImageToClipboard(m_imageUrl); + } + }); + connect(m_saveButton, &QToolButton::clicked, this, [this]() { + saveCurrentImage(); + }); + connect(m_toggleToolbarButton, &QToolButton::clicked, this, [this]() { + m_toolbarVisible = !m_toolbarVisible; + updateLayoutAndSize(); + update(); + }); + connect(m_closeButton, &QToolButton::clicked, this, &QWidget::close); + + updateLayoutAndSize(); + if (!m_colorButtons.isEmpty()) { + m_colorButtons.first()->setChecked(true); + } + for (QToolButton *sizeButton : m_sizeButtons) { + if (sizeButton->text() == tr("粗")) { + sizeButton->setChecked(true); + break; + } + } + syncCanvasToolSettings(); + show(); + raise(); + activateWindow(); + applyNativePosition(m_physicalRect); +} + +StickyPinWidget::~StickyPinWidget() +{ + releaseStoredImage(); +} + +void StickyPinWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + painter.fillRect(rect(), Qt::transparent); + + const QRect imageRect = contentRect(); + if (m_shadowVisible) { + for (int i = 10; i >= 1; --i) { + const int alpha = 3 + i * 2; + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(47, 143, 255, alpha)); + painter.drawRoundedRect(imageRect.adjusted(-i, -i, i, i), 8, 8); + } + } + + painter.setPen(QPen(QColor(120, 182, 255, 80), 1)); + painter.setBrush(Qt::white); + painter.drawRoundedRect(imageRect, 4, 4); + + painter.setBrush(Qt::NoBrush); + painter.drawRoundedRect(imageRect, 4, 4); +} + +void StickyPinWidget::setActiveTool(Shapeype type, QToolButton *activeButton) +{ + const bool activate = activeButton && activeButton->isChecked(); + if (m_canvas) { + m_canvas->setDrawingEnabled(activate); + if (activate) { + m_canvas->setActiveShapeType(type); + } + } + syncCanvasToolSettings(); + updateToolOptionsPanel(); + updateLayoutAndSize(); + update(); +} + +void StickyPinWidget::syncCanvasToolSettings() +{ + if (!m_canvas) { + return; + } + + QColor penColor = m_currentPenColor; + int penSize = m_currentPenSize; + const Shapeype type = m_canvas->activeShapeType(); + if (type == HIGHLIGHT) { + penColor.setAlpha(100); + penSize = qMax(8, m_currentPenSize * 2); + } + + m_canvas->setPenColor(penColor); + m_canvas->setPenSize(penSize); + m_canvas->setAnnotationText(m_annotationText); + m_canvas->setTextBackgroundEnabled(m_textBackgroundEnabled); + m_canvas->setNumberAutoIncrement(m_numberAutoIncrement); + m_canvas->setNumberValue(m_numberValue); +} + +void StickyPinWidget::updateToolOptionsPanel() +{ + const bool visible = m_toolbarVisible && m_canvas && m_canvas->drawingEnabled(); + if (!m_toolOptions) { + return; + } + + m_toolOptions->setVisible(visible); + if (!visible) { + return; + } + + const Shapeype type = m_canvas->activeShapeType(); + const bool showTextOptions = (type == TEXT); + const bool showNumberOptions = (type == NUMBER); + const bool showColorOptions = (type != HIGHLIGHT && type != MOSAIC && type != BLUR); + + int x = 10; + int y = 10; + for (QToolButton *button : m_colorButtons) { + button->setVisible(showColorOptions); + button->setGeometry(x, y, 18, 18); + x += 24; + } + + y += 26; + x = 10; + for (QToolButton *button : m_sizeButtons) { + button->setVisible(true); + button->setGeometry(x, y, 48, 28); + x += 54; + } + + if (m_textInput && m_textBackgroundCheck) { + m_textInput->setVisible(showTextOptions); + m_textBackgroundCheck->setVisible(showTextOptions); + m_textInput->setGeometry(10, 68, 160, 32); + m_textBackgroundCheck->setGeometry(182, 72, 64, 24); + } + + if (m_numberAutoIncrementCheck && m_numberMinusButton && m_numberLabel && m_numberPlusButton) { + m_numberAutoIncrementCheck->setVisible(showNumberOptions); + m_numberMinusButton->setVisible(showNumberOptions); + m_numberLabel->setVisible(showNumberOptions); + m_numberPlusButton->setVisible(showNumberOptions); + m_numberValueSpin->setVisible(false); + m_numberAutoIncrementCheck->setGeometry(10, 68, 100, 24); + m_numberMinusButton->setGeometry(118, 66, 28, 28); + m_numberLabel->setGeometry(152, 66, 48, 28); + m_numberPlusButton->setGeometry(206, 66, 28, 28); + } +} + +void StickyPinWidget::closeEvent(QCloseEvent *event) +{ + releaseStoredImage(); + QWidget::closeEvent(event); +} + +void StickyPinWidget::contextMenuEvent(QContextMenuEvent *event) +{ + QMenu menu(this); + QAction *copyAction = menu.addAction(tr("复制")); + QAction *saveAction = menu.addAction(tr("保存原图")); + QAction *aiAction = menu.addAction(tr("AI 编辑")); + QAction *toggleAction = menu.addAction(m_toolbarVisible ? tr("隐藏工具栏") : tr("显示工具栏")); + QAction *shadowAction = menu.addAction(m_shadowVisible ? tr("隐藏阴影") : tr("显示阴影")); + menu.addSeparator(); + QAction *closeAction = menu.addAction(tr("关闭")); + QAction *chosen = menu.exec(event->globalPos()); + if (chosen == copyAction) { + if (m_store) { + m_store->copyImageToClipboard(m_imageUrl); + } + return; + } + if (chosen == saveAction) { + saveCurrentImage(); + return; + } + if (chosen == aiAction) { + openAiEditor(); + return; + } + if (chosen == toggleAction) { + m_toolbarVisible = !m_toolbarVisible; + updateLayoutAndSize(); + update(); + return; + } + if (chosen == shadowAction) { + m_shadowVisible = !m_shadowVisible; + update(); + return; + } + if (chosen == closeAction) { + close(); + } +} + +void StickyPinWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(winId())) { + ReleaseCapture(); + SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); + event->accept(); + return; + } +#endif + } + QWidget::mousePressEvent(event); +} + +void StickyPinWidget::wheelEvent(QWheelEvent *event) +{ + const QPoint angleDelta = event->angleDelta(); + if (angleDelta.y() == 0) { + QWidget::wheelEvent(event); + return; + } + + const qreal step = angleDelta.y() > 0 ? 1.1 : 0.9; + setZoomFactor(m_zoomFactor * step); + event->accept(); +} + +void StickyPinWidget::setZoomFactor(qreal value) +{ + const qreal next = qBound(0.3, value, 4.0); + if (qFuzzyCompare(m_zoomFactor, next)) { + return; + } + + m_zoomFactor = next; + m_imageDisplaySize = QSize(qMax(1, qRound(m_baseImageDisplaySize.width() * m_zoomFactor)), + qMax(1, qRound(m_baseImageDisplaySize.height() * m_zoomFactor))); + if (m_canvas) { + m_canvas->setViewScale(m_zoomFactor); + m_canvas->setBackgroundImage(m_image); + } + updateLayoutAndSize(); + update(); +} + +void StickyPinWidget::openAiEditor() +{ + if (!m_aiViewModel) { + QMessageBox::information(this, tr("AI 编辑"), tr("AI 功能当前不可用。")); + return; + } + + StickyAiDialog dialog(m_image, this); + QPoint panelPos = frameGeometry().bottomLeft() + QPoint(0, 6); + if (QScreen *screen = this->screen()) { + const QRect available = screen->availableGeometry(); + if (panelPos.y() + dialog.height() > available.bottom() + 1) { + panelPos.setY(frameGeometry().top() - dialog.height() - 6); + } + if (panelPos.x() + dialog.width() > available.right() + 1) { + panelPos.setX(available.right() - dialog.width() + 1); + } + panelPos.setX(qMax(available.left(), panelPos.x())); + panelPos.setY(qMax(available.top(), panelPos.y())); + } + dialog.move(panelPos); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + const QString prompt = dialog.prompt(); + const QString trimmed = prompt.trimmed(); + if (trimmed.isEmpty()) { + QMessageBox::information(this, tr("AI 编辑"), tr("提示词不能为空。")); + return; + } + + m_aiViewModel->sendPrompt(trimmed, m_imageUrl); +} + +void StickyPinWidget::applyAiImage(const QString &oldImageUrl, const QString &newImageUrl) +{ + if (oldImageUrl != m_imageUrl || !m_store) { + return; + } + + const QImage newImage = m_store->getImageByUrl(newImageUrl); + if (newImage.isNull()) { + return; + } + + const QString previousUrl = m_imageUrl; + m_imageUrl = newImageUrl; + m_image = newImage; + m_baseImageDisplaySize = QSize(qMax(1, qRound(m_image.width() / qMax(1.0, m_image.devicePixelRatio()))), + qMax(1, qRound(m_image.height() / qMax(1.0, m_image.devicePixelRatio())))); + m_imageDisplaySize = QSize(qMax(1, qRound(m_baseImageDisplaySize.width() * m_zoomFactor)), + qMax(1, qRound(m_baseImageDisplaySize.height() * m_zoomFactor))); + if (m_canvas) { + m_canvas->setViewScale(m_zoomFactor); + m_canvas->reset(); + m_canvas->setBackgroundImage(m_image); + syncCanvasToolSettings(); + } + updateLayoutAndSize(); + update(); + m_store->releaseImage(previousUrl); +} + +void StickyPinWidget::releaseStoredImage() +{ + if (m_released || !m_store || m_imageUrl.isEmpty()) { + return; + } + m_released = true; + m_store->releaseImage(m_imageUrl); +} + +bool StickyPinWidget::saveCurrentImage() +{ + if (m_image.isNull()) { + return false; + } + + const QString filePath = QFileDialog::getSaveFileName( + this, + tr("选择保存位置"), + QString(), + tr("PNG (*.png);;JPG (*.jpg);;BMP (*.bmp)")); + + if (filePath.isEmpty()) { + return false; + } + + return m_image.save(filePath); +} + +qreal StickyPinWidget::screenScaleForRect(const QRect &physicalRect) const +{ + if (physicalRect.isEmpty()) { + return 1.0; + } + return monitorScaleForPoint(physicalRect.center()); +} + +void StickyPinWidget::applyNativePosition(const QRect &physicalRect) +{ + const QPoint nativeTopLeft = physicalRect.topLeft() - contentRect().topLeft(); +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(winId())) { + SetWindowPos(hwnd, + HWND_TOPMOST, + nativeTopLeft.x(), + nativeTopLeft.y(), + 0, + 0, + SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + return; + } +#endif + move(nativeTopLeft); +} + +void StickyPinWidget::updateLayoutAndSize() +{ + const QPoint contentTopLeft = currentContentTopLeftPhysical(); + const int totalWidth = qMax(m_imageDisplaySize.width(), toolbarWidth()) + shadowPadding() * 2; + const int totalHeight = m_imageDisplaySize.height() + + shadowPadding() * 2 + + (m_toolbarVisible ? (toolbarGap() + toolbarHeight()) : 0); + const int optionsReserve = (m_toolOptions && m_toolOptions->isVisible()) + ? (toolOptionsGap() + toolOptionsHeight()) + : 0; + const int finalHeight = totalHeight + optionsReserve; + + setFixedSize(totalWidth, finalHeight); + + if (m_toolbar) { + const QRect barRect = toolbarRect(); + m_toolbar->setVisible(m_toolbarVisible); + m_toolbar->setGeometry(barRect); + } + + if (m_canvas) { + m_canvas->setGeometry(contentRect()); + } + + if (m_toolOptions) { + m_toolOptions->setGeometry(toolOptionsRect()); + updateToolOptionsPanel(); + } + + if (m_pencilButton && m_rectButton && m_circleButton && m_arrowButton + && m_highlightButton && m_mosaicButton && m_blurButton + && m_textButton && m_numberButton + && m_undoButton && m_aiButton && m_copyButton && m_saveButton && m_zoomLabel + && m_toggleToolbarButton && m_closeButton) { + const int buttonH = 32; + const int buttonW = 32; + const int zoomW = 52; + const int buttonY = (toolbarHeight() - buttonH) / 2; + const int gap = 2; + int x = 6; + m_pencilButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_rectButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_circleButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_arrowButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_highlightButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_mosaicButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_blurButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_textButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_numberButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_undoButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_aiButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_copyButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_saveButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_zoomLabel->setGeometry(x, buttonY, zoomW, buttonH); + m_zoomLabel->setText(QString::number(qRound(m_zoomFactor * 100)) + QStringLiteral("%")); + x += zoomW + gap; + m_toggleToolbarButton->setGeometry(x, buttonY, buttonW, buttonH); + x += buttonW + gap; + m_closeButton->setGeometry(x, buttonY, buttonW, buttonH); + m_toggleToolbarButton->setToolTip(m_toolbarVisible ? tr("隐藏工具栏") : tr("显示工具栏")); + const char *toggleIconPath = m_toolbarVisible + ? ":/resource/img/lc_close.svg" + : ":/resource/img/lc_check.svg"; + m_toggleToolbarButton->setIcon(QIcon(QString::fromLatin1(toggleIconPath))); + } + + applyNativePosition(QRect(contentTopLeft, m_imageDisplaySize)); +} + +QPoint StickyPinWidget::currentContentTopLeftPhysical() const +{ +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(const_cast(this)->winId())) { + RECT rc{}; + if (GetWindowRect(hwnd, &rc)) { + return QPoint(rc.left, rc.top) + contentRect().topLeft(); + } + } +#endif + if (isVisible()) { + return geometry().topLeft() + contentRect().topLeft(); + } + return m_physicalRect.topLeft(); +} + +QRect StickyPinWidget::contentRect() const +{ + const int x = (width() - m_imageDisplaySize.width()) / 2; + return QRect(x, shadowPadding(), m_imageDisplaySize.width(), m_imageDisplaySize.height()); +} + +QRect StickyPinWidget::toolOptionsRect() const +{ + const int w = toolOptionsWidth(); + int x = (width() - w) / 2; + if (QToolButton *anchorButton = currentActiveToolButton()) { + const int centerX = toolbarRect().x() + anchorButton->geometry().center().x(); + x = centerX - w / 2; + x = qMax(shadowPadding(), qMin(x, width() - w - shadowPadding())); + } + const int y = toolbarRect().bottom() + 1 + toolOptionsGap(); + return QRect(x, y, w, toolOptionsHeight()); +} + +QRect StickyPinWidget::toolbarRect() const +{ + const int w = toolbarWidth(); + const int x = (width() - w) / 2; + const int y = contentRect().bottom() + 1 + toolbarGap(); + return QRect(x, y, w, toolbarHeight()); +} + +int StickyPinWidget::shadowPadding() const +{ + return 12; +} + +int StickyPinWidget::toolbarGap() const +{ + return 8; +} + +int StickyPinWidget::toolOptionsGap() const +{ + return 8; +} + +int StickyPinWidget::toolOptionsHeight() const +{ + if (m_canvas && m_canvas->drawingEnabled()) { + const Shapeype type = m_canvas->activeShapeType(); + if (type == TEXT || type == NUMBER) { + return 116; + } + } + return 88; +} + +int StickyPinWidget::toolOptionsWidth() const +{ + return 260; +} + +QToolButton *StickyPinWidget::currentActiveToolButton() const +{ + const QVector drawButtons { + m_pencilButton, m_rectButton, m_circleButton, m_arrowButton, + m_highlightButton, m_mosaicButton, m_blurButton, m_textButton, m_numberButton + }; + for (QToolButton *button : drawButtons) { + if (button && button->isChecked()) { + return button; + } + } + return nullptr; +} + +int StickyPinWidget::toolbarHeight() const +{ + return 44; +} + +int StickyPinWidget::toolbarWidth() const +{ + return 32 * 15 + 52 + 2 * 15 + 12; +} diff --git a/src/widgets/sticky_pin_widget.h b/src/widgets/sticky_pin_widget.h new file mode 100644 index 0000000..813badb --- /dev/null +++ b/src/widgets/sticky_pin_widget.h @@ -0,0 +1,116 @@ +#ifndef STICKY_PIN_WIDGET_H +#define STICKY_PIN_WIDGET_H + +#include +#include +#include +#include +#include +#include + +#include "paint_board/shape/shape_type.h" + +class StickyImageStore; +class AIViewModel; +class QCheckBox; +class QLabel; +class QLineEdit; +class QSpinBox; +class QToolButton; +class StickyCanvasWidget; + +class StickyPinWidget : public QWidget +{ + Q_OBJECT + +public: + StickyPinWidget(const QString &imageUrl, + const QRect &physicalRect, + const QImage &image, + StickyImageStore *store, + AIViewModel *aiViewModel = nullptr, + QWidget *parent = nullptr); + ~StickyPinWidget() override; + +protected: + void paintEvent(QPaintEvent *event) override; + void closeEvent(QCloseEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + +private: + void setActiveTool(Shapeype type, QToolButton *activeButton); + void syncCanvasToolSettings(); + void updateToolOptionsPanel(); + void updateLayoutAndSize(); + QToolButton *currentActiveToolButton() const; + void setZoomFactor(qreal value); + void openAiEditor(); + void applyAiImage(const QString &oldImageUrl, const QString &newImageUrl); + void releaseStoredImage(); + bool saveCurrentImage(); + qreal screenScaleForRect(const QRect &physicalRect) const; + void applyNativePosition(const QRect &physicalRect); + QPoint currentContentTopLeftPhysical() const; + QRect contentRect() const; + QRect toolOptionsRect() const; + QRect toolbarRect() const; + int shadowPadding() const; + int toolbarGap() const; + int toolOptionsGap() const; + int toolOptionsHeight() const; + int toolOptionsWidth() const; + int toolbarHeight() const; + int toolbarWidth() const; + + QString m_imageUrl; + QRect m_physicalRect; + QImage m_image; + QSize m_baseImageDisplaySize; + QSize m_imageDisplaySize; + QPointer m_store; + QPointer m_aiViewModel; + StickyCanvasWidget *m_canvas = nullptr; + QWidget *m_toolOptions = nullptr; + QWidget *m_toolbar = nullptr; + QLabel *m_zoomLabel = nullptr; + QVector m_colorButtons; + QVector m_sizeButtons; + QLineEdit *m_inlineTextEditor = nullptr; + QLineEdit *m_textInput = nullptr; + QCheckBox *m_textBackgroundCheck = nullptr; + QCheckBox *m_numberAutoIncrementCheck = nullptr; + QToolButton *m_numberMinusButton = nullptr; + QLabel *m_numberLabel = nullptr; + QToolButton *m_numberPlusButton = nullptr; + QSpinBox *m_numberValueSpin = nullptr; + QToolButton *m_pencilButton = nullptr; + QToolButton *m_rectButton = nullptr; + QToolButton *m_circleButton = nullptr; + QToolButton *m_arrowButton = nullptr; + QToolButton *m_highlightButton = nullptr; + QToolButton *m_mosaicButton = nullptr; + QToolButton *m_blurButton = nullptr; + QToolButton *m_textButton = nullptr; + QToolButton *m_numberButton = nullptr; + QToolButton *m_undoButton = nullptr; + QToolButton *m_aiButton = nullptr; + QToolButton *m_copyButton = nullptr; + QToolButton *m_saveButton = nullptr; + QToolButton *m_toggleToolbarButton = nullptr; + QToolButton *m_closeButton = nullptr; + QColor m_currentPenColor = QColor("#F43F5E"); + int m_currentPenSize = 6; + QString m_annotationText = QStringLiteral("文本"); + QPoint m_pendingTextPoint; + bool m_textBackgroundEnabled = true; + bool m_numberAutoIncrement = true; + int m_numberValue = 1; + bool m_toolbarVisible = true; + bool m_shadowVisible = true; + qreal m_zoomFactor = 1.0; + bool m_released = false; +}; + +#endif // STICKY_PIN_WIDGET_H