From 409fdcb91e288c72b39bf0d0f1fcd40f84bf07da Mon Sep 17 00:00:00 2001 From: easyz Date: Fri, 27 Mar 2026 20:26:57 +0800 Subject: [PATCH] Migrate capture overlay to widgets --- CMakeLists.txt | 43 +- i18n/app_en.ts | 734 +---------- i18n/app_zh_CN.ts | 752 +---------- qml.qrc | 34 +- qml/Capture/TZCaptureInputController.qml | 56 - qml/Capture/TZGifRecordBar.qml | 194 --- qml/Capture/TZHotkeyGestureController.qml | 96 -- qml/Capture/TZLable.qml | 26 - qml/Capture/TZMagnifyingGlass.qml | 116 -- qml/Capture/TZMarkToolInfo.qml | 257 ---- qml/Capture/TZRectMaskOverlay.qml | 48 - qml/Capture/TZResizeHandle.qml | 14 - qml/Capture/TZResizeHandleGroup.qml | 301 ----- qml/Capture/TZSelectionInfoLabel.qml | 19 - qml/Capture/TZSelectionOverlay.qml | 197 --- qml/Capture/TZSelectionPositioner.qml | 34 - qml/Capture/TZTextAnnotationEditor.qml | 101 -- qml/Capture/TZTool.qml | 278 ----- qml/Capture/TZWindow.qml | 765 ------------ qml/Capture/TZWindowFactory.qml | 116 -- qml/Capture/TZWindowHighlightOverlay.qml | 46 - qml/LongCapture/TZLongCaptureBar.qml | 110 -- qml/LongCapture/TZLongCaptureFrame.qml | 53 - qml/LongCapture/TZLongCapturePreview.qml | 83 -- qml/Setting/TZAboutWin.qml | 128 -- qml/Setting/TZGeneralSettings.qml | 500 -------- qml/Setting/TZHotkeySettings.qml | 373 ------ qml/Setting/TZRightPanel.qml | 166 --- qml/Setting/TZSettingWin.qml | 77 -- qml/TZControls/TZComboBox.qml | 128 -- qml/TZControls/qmldir | 2 - qml/Windows/TZChat.qml | 311 ----- qml/Windows/TZOcrResult.qml | 328 ----- qml/Windows/TZStickyWindow.qml | 317 ----- qml/main.qml | 339 ----- resource/img/lc_copy.svg | 5 + resource/qss/capture_overlay_widget.qss | 186 +++ resource/qss/long_capture_controller.qss | 76 ++ src/app/tzshot_app.cpp | 90 ++ src/app/tzshot_app.h | 33 + src/app/tzshot_services.cpp | 19 + src/app/tzshot_services.h | 54 + src/app/tzshot_widget_runtime.cpp | 30 + src/app/tzshot_widget_runtime.h | 41 + src/gif_encoder/gif_frame_grabber.cpp | 139 ++- src/magnifier_provider.cpp | 92 -- src/magnifier_provider.h | 36 - src/main.cpp | 125 +- src/paint_board/paint_board.cpp | 261 ---- src/paint_board/paint_board.h | 103 -- src/paint_board/shape/mosaic_shape.h | 1 - src/sticky_image_provider_proxy.cpp | 31 - src/sticky_image_provider_proxy.h | 24 - src/sticky_image_store.h | 7 +- src/viewmodel/screenshot_view_model.cpp | 87 +- src/viewmodel/screenshot_view_model.h | 13 +- src/viewmodel/sticky_view_model.cpp | 13 + src/viewmodel/sticky_view_model.h | 12 +- src/widgets/about_dialog.cpp | 98 ++ src/widgets/about_dialog.h | 16 + src/widgets/capture_overlay_widget.cpp | 1384 +++++++++++++++++++++ src/widgets/capture_overlay_widget.h | 160 +++ src/widgets/long_capture_controller.cpp | 388 ++++++ src/widgets/long_capture_controller.h | 54 + src/widgets/magnifier_widget.cpp | 147 +++ src/widgets/magnifier_widget.h | 30 + src/widgets/selection_mask_controller.cpp | 386 ++++++ src/widgets/selection_mask_controller.h | 60 + src/widgets/sticky_canvas_widget.cpp | 48 +- src/widgets/sticky_canvas_widget.h | 4 + src/widgets/widget_window_bridge.cpp | 30 +- src/widgets/widget_window_bridge.h | 14 +- 72 files changed, 3607 insertions(+), 7832 deletions(-) delete mode 100644 qml/Capture/TZCaptureInputController.qml delete mode 100644 qml/Capture/TZGifRecordBar.qml delete mode 100644 qml/Capture/TZHotkeyGestureController.qml delete mode 100644 qml/Capture/TZLable.qml delete mode 100644 qml/Capture/TZMagnifyingGlass.qml delete mode 100644 qml/Capture/TZMarkToolInfo.qml delete mode 100644 qml/Capture/TZRectMaskOverlay.qml delete mode 100644 qml/Capture/TZResizeHandle.qml delete mode 100644 qml/Capture/TZResizeHandleGroup.qml delete mode 100644 qml/Capture/TZSelectionInfoLabel.qml delete mode 100644 qml/Capture/TZSelectionOverlay.qml delete mode 100644 qml/Capture/TZSelectionPositioner.qml delete mode 100644 qml/Capture/TZTextAnnotationEditor.qml delete mode 100644 qml/Capture/TZTool.qml delete mode 100644 qml/Capture/TZWindow.qml delete mode 100644 qml/Capture/TZWindowFactory.qml delete mode 100644 qml/Capture/TZWindowHighlightOverlay.qml delete mode 100644 qml/LongCapture/TZLongCaptureBar.qml delete mode 100644 qml/LongCapture/TZLongCaptureFrame.qml delete mode 100644 qml/LongCapture/TZLongCapturePreview.qml delete mode 100644 qml/Setting/TZAboutWin.qml delete mode 100644 qml/Setting/TZGeneralSettings.qml delete mode 100644 qml/Setting/TZHotkeySettings.qml delete mode 100644 qml/Setting/TZRightPanel.qml delete mode 100644 qml/Setting/TZSettingWin.qml delete mode 100644 qml/TZControls/TZComboBox.qml delete mode 100644 qml/TZControls/qmldir delete mode 100644 qml/Windows/TZChat.qml delete mode 100644 qml/Windows/TZOcrResult.qml delete mode 100644 qml/Windows/TZStickyWindow.qml delete mode 100644 qml/main.qml create mode 100644 resource/img/lc_copy.svg create mode 100644 resource/qss/capture_overlay_widget.qss create mode 100644 resource/qss/long_capture_controller.qss create mode 100644 src/app/tzshot_app.cpp create mode 100644 src/app/tzshot_app.h create mode 100644 src/app/tzshot_services.cpp create mode 100644 src/app/tzshot_services.h create mode 100644 src/app/tzshot_widget_runtime.cpp create mode 100644 src/app/tzshot_widget_runtime.h delete mode 100644 src/magnifier_provider.cpp delete mode 100644 src/magnifier_provider.h delete mode 100644 src/paint_board/paint_board.cpp delete mode 100644 src/paint_board/paint_board.h delete mode 100644 src/sticky_image_provider_proxy.cpp delete mode 100644 src/sticky_image_provider_proxy.h create mode 100644 src/widgets/about_dialog.cpp create mode 100644 src/widgets/about_dialog.h create mode 100644 src/widgets/capture_overlay_widget.cpp create mode 100644 src/widgets/capture_overlay_widget.h create mode 100644 src/widgets/long_capture_controller.cpp create mode 100644 src/widgets/long_capture_controller.h create mode 100644 src/widgets/magnifier_widget.cpp create mode 100644 src/widgets/magnifier_widget.h create mode 100644 src/widgets/selection_mask_controller.cpp create mode 100644 src/widgets/selection_mask_controller.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ed5c389..11c27f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,21 +15,13 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets - Quick - Qml Network Concurrent LinguistTools - QuickDialogs2 ) qt_standard_project_setup() -# Help Qt Creator's QML code model resolve local qmldir-based modules -# such as `import TZControls 1.0`. -set(QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml" CACHE STRING "Local QML import paths" FORCE) -set(QML2_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml" CACHE STRING "Local QML import paths" FORCE) - # ── Third-party build options ───────────────────────────────── option(BUILD_THIRDPART_OCR "Auto build OCR libs" ON) option(OCR_FETCH_FROM_GITHUB "Fetch Leptonica/Tesseract from GitHub before building" ON) @@ -43,23 +35,35 @@ set(TS_FILES # ── 平台无关源文件 ──────────────────────────────────────── set(SOURCES src/main.cpp + src/app/tzshot_app.cpp + src/app/tzshot_app.h + src/app/tzshot_services.cpp + src/app/tzshot_services.h + src/app/tzshot_widget_runtime.cpp + src/app/tzshot_widget_runtime.h src/tray_icon_helper.cpp src/tray_icon_helper.h src/language_manager.cpp src/language_manager.h - src/magnifier_provider.cpp - src/magnifier_provider.h src/sticky_image_store.cpp src/sticky_image_store.h - src/sticky_image_provider_proxy.cpp - src/sticky_image_provider_proxy.h src/instance_activation.cpp src/instance_activation.h src/singleton.h + src/widgets/about_dialog.cpp + src/widgets/about_dialog.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 + src/widgets/capture_overlay_widget.cpp + src/widgets/capture_overlay_widget.h + src/widgets/long_capture_controller.cpp + src/widgets/long_capture_controller.h + src/widgets/magnifier_widget.cpp + src/widgets/magnifier_widget.h + src/widgets/selection_mask_controller.cpp + src/widgets/selection_mask_controller.h src/widgets/ocr_result_widget.cpp src/widgets/ocr_result_widget.h src/widgets/settings_dialog.cpp @@ -102,9 +106,6 @@ set(SOURCES src/ocr/ocr_engine.cpp src/ocr/ocr_engine.h - # PaintBoard - src/paint_board/paint_board.cpp - src/paint_board/paint_board.h src/paint_board/shape/shape.cpp src/paint_board/shape/shape.h src/paint_board/shape/shape_type.h @@ -165,10 +166,6 @@ qt_add_executable(TZshot ${SOURCES} ) -set_target_properties(TZshot PROPERTIES - QT_QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml" -) - # ── include 路径 ────────────────────────────────────────── target_include_directories(TZshot PRIVATE src @@ -197,11 +194,8 @@ target_link_libraries(TZshot PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets - Qt6::Quick - Qt6::Qml Qt6::Network Qt6::Concurrent - Qt6::QuickDialogs2 ) # ── 平台系统库 ──────────────────────────────────────────── @@ -453,11 +447,6 @@ if(WIN32) ) endif() -# ── QML 导入路径 ────────────────────────────────────────── -set_target_properties(TZshot PROPERTIES - QT_QML_IMPORT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qml" -) - # ── 翻译 ────────────────────────────────────────────────── qt_add_translations(TZshot TS_FILES ${TS_FILES} diff --git a/i18n/app_en.ts b/i18n/app_en.ts index 4781bad..48ee156 100644 --- a/i18n/app_en.ts +++ b/i18n/app_en.ts @@ -1,4 +1,4 @@ - + GifRecordViewModel @@ -62,734 +62,4 @@ - - TZAboutWin - - - 关于 TZshot - About TZshot - - - - 版本 v1.0.0 - Version v1.0.0 - - - - 简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。 - A lightweight screenshot and annotation tool with AI editing, mosaic, and more. - - - - © 2025 TZshot. All rights reserved. - © 2025 TZshot. All rights reserved. - - - - 关闭 - Close - - - - 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 - - - - TZGeneralSettings - - - 大模型设置 - AI Model Settings - - - - 大模型选择 - Select Model - - - - 阿里云百炼 - Alibaba Cloud Bailian - - - - 火山引擎 - Volcano Engine - - - - 请输入 API Key - Enter API Key - - - - 保存 - - - - - 语言设置 - Language - - - - 界面语言 - UI Language - - - - 截图保存路径 - Screenshot Save Path - - - - 保存路径 - Save Path - - - - 浏览 - Browse - - - - TZGifRecordBar - - - 正在生成 GIF... - - - - - TZHotkeySettings - - - 编辑快捷键 · %1 - Edit Shortcut · %1 - - - - 请按下新的快捷键组合... - Press the new shortcut key combination... - - - - 取消 - Cancel - - - - 确认 - Confirm - - - - - 截图 - Screenshot - - - - - 截图并保存 - Screenshot & Save - - - - - 贴图到桌面 - Pin to Desktop - - - - - 显示/隐藏窗口 - Show/Hide Window - - - - 全局快捷键 - Global Shortcuts - - - - 编辑 - Edit - - - - 说明 - Notes - - - - 快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。 -若与系统或其他应用快捷键冲突,请修改为其他组合。 - Shortcuts work globally. Click "Edit", press the new key combination, then click "Confirm" to save. -If it conflicts with system or other app shortcuts, change to a different combination. - - - - TZMarkToolInfo - - - 文本 - - - - - 细 - Thin - - - - 中 - Medium - - - - 粗 - Thick - - - - 特粗 - Extra Thick - - - - 输入文字 - - - - - 背景 - - - - - 模糊 - - - - - 自动递增 - - - - - 数字 - - - - - TZOcrResult - - - OCR 识别结果 - OCR Results - - - - 正在识别中… - Recognizing… - - - - 识别结果将显示在这里… - Recognition results will appear here… - - - - 复制全部 - Copy All - - - - 关闭 - Close - - - - 已复制! - Copied! - - - - TZRightPanel - - - 通用设置 - General - - - - 快捷键设置 - Shortcuts - - - - TZSettingWin - - - 设置 - Settings - - - - 选择保存位置 - Choose Save Location - - - - TZStickyWindow - - - 当前透明度 %1% - Opacity: %1% - - - - - 覆盖贴图(合并标注) - Overwrite Image (Merge Annotations) - - - - - 显示工具栏 - Show Toolbar - - - - 透明度 +5% - Opacity +5% - - - - 透明度 -5% - Opacity -5% - - - - 旋转 90° - Rotate 90° - - - - 重置位置 - Reset Position - - - - - - AI 编辑 - AI Edit - - - 保存图片 - 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 - - - 序号 - Number - - - 旋转90° - Rotate 90° - - - 恢复原始大小 - Reset Size - - - 隐藏工具栏 - Hide Toolbar - - - - - OCR - OCR - - - - TZTool - - 确认截图 - Confirm - - - - 取消 - Cancel - - - 保存到文件 - Save to File - - - 贴图到桌面 - Pin to Desktop - - - 矩形标注 - Rectangle - - - 椭圆标注 - Ellipse - - - 箭头标注 - Arrow - - - - 完成 - Done - - - - 录制GIF - Record GIF - - - - 保存 - Save - - - - 贴图 - Pin - - - - 矩形 - Rectangle - - - - 圆形 - Circle - - - - 箭头 - Arrow - - - - 画笔 - Pen - - - - 高亮 - Highlight - - - - 马赛克 - Mosaic - - - - 文字 - Text - - - - 序号 - Number - - - - 撤销 - Undo - - - 序号 - Number - - - - OCR - OCR - - - 录制GIF - Record GIF - - - 马赛克 - Mosaic - - - - TZWindow - - 选择保存位置 - Choose Save Location - - - 保存成功 - Saved - - - 截图已保存到文件 - Screenshot saved to file - - - 保存失败 - Save Failed - - - 截图文件保存失败 - Failed to save screenshot file - - - - %1 * %2 - %1 * %2 - - - 截图完成 - Done - - - 已复制到剪贴板 - Copied to clipboard - - - 截图失败 - Screenshot Failed - - - 截图复制到剪贴板失败 - 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 - - - - main - - - TZshot - TZshot - - - - OCR 识别失败 - OCR Recognition Failed - - - + \ No newline at end of file diff --git a/i18n/app_zh_CN.ts b/i18n/app_zh_CN.ts index 0c9165f..c79ef72 100644 --- a/i18n/app_zh_CN.ts +++ b/i18n/app_zh_CN.ts @@ -1,691 +1,67 @@ - + - - GifRecordViewModel - - - 录制区域无效 - - - - - 未采集到任何帧 - - - - - GIF 编码失败,请检查保存路径或磁盘空间 - - - - - QObject - - - 截图 - - - - - 设置 - - - - - 关于 - - - - - 退出 - - - - - S: - - - - Don't translate the - - - - - TZAboutWin - - - 关于 TZshot - - - - - 版本 v1.0.0 - - - - - 简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。 - - - - - © 2025 TZshot. All rights reserved. - - - - - 关闭 - - - - - TZChat - - - AI 图像编辑 - - - - - 当前贴图 - - - - - 未关联贴图 - - - - - 发送后将对此图执行 AI 编辑 - - - - - 描述您想要的修改... - - - - - 取消 - - - - - 发送 - - - - - TZGeneralSettings - - - 大模型设置 - - - - - 大模型选择 - - - - - 阿里云百炼 - - - - - 火山引擎 - - - - - 请输入 API Key - - - - - 保存 - - - - - 语言设置 - - - - - 界面语言 - - - - - 截图保存路径 - - - - - 保存路径 - - - - - 浏览 - - - - - TZGifRecordBar - - - 正在生成 GIF... - - - - - TZHotkeySettings - - - 编辑快捷键 · %1 - - - - - 请按下新的快捷键组合... - - - - - 取消 - - - - - 确认 - - - - - - 截图 - - - - - - 截图并保存 - - - - - - 贴图到桌面 - - - - - - 显示/隐藏窗口 - - - - - 全局快捷键 - - - - - 编辑 - - - - - 说明 - - - - - 快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。 -若与系统或其他应用快捷键冲突,请修改为其他组合。 - - - - - TZMarkToolInfo - - - 文本 - - - - - 细 - - - - - 中 - - - - - 粗 - - - - - 特粗 - - - - - 输入文字 - - - - - 背景 - - - - - 模糊 - - - - - 自动递增 - - - - - 数字 - - - - - TZOcrResult - - - OCR 识别结果 - - - - - 正在识别中… - - - - - 识别结果将显示在这里… - - - - - 复制全部 - - - - - 关闭 - - - - - 已复制! - - - - - TZRightPanel - - - 通用设置 - - - - - 快捷键设置 - - - - - TZSettingWin - - - 设置 - - - - - 选择保存位置 - - - - - TZStickyWindow - - - - OCR - - - - - Pencil - - - - - Rectangle - - - - - Circle - - - - - Arrow - - - - - - Mosaic - - - - - - 缩小 - - - - - - 放大 - - - - - 旋转90° - - - - - - 镜像 - - - - - - 恢复原始大小 - - - - - 完成并复制 - - - - - 撤销 - - - - - - - - 隐藏工具栏 - - - - - 当前透明度 %1% - - - - - - 覆盖贴图(合并标注) - - - - - - - 复制贴图 - - - - - - - AI 编辑 - - - - - - 显示工具栏 - - - - - - - 关闭窗口 - - - - - 透明度 +5% - - - - - 透明度 -5% - - - - - 旋转 90° - - - - - 重置位置 - - - - - 保存原图 - - - - - - 另存为(合并标注) - - - - - 常用项置顶 - - - - - 覆盖贴图 - - - - - 显示/隐藏工具栏 - - - - - 选择保存位置 - - - - - TZTool - - - 取消 - - - - - 完成 - - - - - OCR - - - - - 录制GIF - - - - - 保存 - - - - - 贴图 - - - - - 矩形 - - - - - 圆形 - - - - - 箭头 - - - - - 画笔 - - - - - 高亮 - - - - - 马赛克 - - - - - 文字 - - - - - 序号 - - - - - 撤销 - - - - - 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 - - - - - main - - - TZshot - - - - - OCR 识别失败 - - - + + GifRecordViewModel + + + 录制区域无效 + + + + + 未采集到任何帧 + + + + + GIF 编码失败,请检查保存路径或磁盘空间 + + + + + QObject + + + 截图 + + + + + 设置 + + + + + 关于 + + + + + 退出 + + + + + S: + + + + Don't translate the + + + + + + + + + + + + + + + + diff --git a/qml.qrc b/qml.qrc index 0f5539a..7a812ac 100644 --- a/qml.qrc +++ b/qml.qrc @@ -1,34 +1,7 @@ - qml/main.qml - qml/Capture/TZWindow.qml - qml/Capture/TZSelectionOverlay.qml - qml/Capture/TZCaptureInputController.qml - qml/Capture/TZHotkeyGestureController.qml - qml/Capture/TZRectMaskOverlay.qml - qml/Capture/TZSelectionPositioner.qml - qml/Capture/TZSelectionInfoLabel.qml - qml/Capture/TZTextAnnotationEditor.qml - qml/Capture/TZWindowHighlightOverlay.qml - qml/Capture/TZWindowFactory.qml - qml/Capture/TZLable.qml - qml/Capture/TZTool.qml - qml/Capture/TZResizeHandle.qml - qml/Capture/TZResizeHandleGroup.qml - qml/Capture/TZMarkToolInfo.qml - qml/Capture/TZMagnifyingGlass.qml - qml/Capture/TZGifRecordBar.qml - qml/Windows/TZStickyWindow.qml - qml/Windows/TZOcrResult.qml - qml/Windows/TZChat.qml - qml/Setting/TZSettingWin.qml - qml/Setting/TZGeneralSettings.qml - qml/Setting/TZRightPanel.qml - qml/TZControls/qmldir - qml/TZControls/TZComboBox.qml - qml/Setting/TZHotkeySettings.qml - qml/Setting/TZAboutWin.qml resource/img/lc_check.svg + resource/img/lc_copy.svg resource/img/lc_x.svg resource/img/lc_save.svg resource/img/lc_pin.svg @@ -46,9 +19,6 @@ resource/img/lc_close.svg resource/img/lc_chat.svg resource/img/lc_send.svg - qml/LongCapture/TZLongCaptureBar.qml - qml/LongCapture/TZLongCapturePreview.qml - qml/LongCapture/TZLongCaptureFrame.qml resource/img/lc_gif_record.svg resource/img/lc_gif_stop.svg resource/img/lc_ocr.svg @@ -56,5 +26,7 @@ resource/img/lc_chevron_down.svg resource/qss/sticky_pin_widget.qss resource/qss/settings_dialog.qss + resource/qss/capture_overlay_widget.qss + resource/qss/long_capture_controller.qss diff --git a/qml/Capture/TZCaptureInputController.qml b/qml/Capture/TZCaptureInputController.qml deleted file mode 100644 index c19a997..0000000 --- a/qml/Capture/TZCaptureInputController.qml +++ /dev/null @@ -1,56 +0,0 @@ -import QtQuick - -QtObject { - id: controller - - property bool spaceHeld: false - property bool textClickPending: false - property bool textPressMoved: false - property point textClickAbsPoint: Qt.point(0, 0) - property point textClickLocalPoint: Qt.point(0, 0) - - function resetTextClickTracking() { - textClickPending = false - textPressMoved = false - textClickAbsPoint = Qt.point(0, 0) - textClickLocalPoint = Qt.point(0, 0) - } - - function beginTextClick(absX, absY, localX, localY) { - textClickPending = true - textPressMoved = false - textClickAbsPoint = Qt.point(absX, absY) - textClickLocalPoint = Qt.point(localX, localY) - } - - function updatePendingTextClick(absX, absY, moveThreshold) { - if (!textClickPending) - return false - - const threshold = moveThreshold === undefined ? 3 : moveThreshold - const dx = absX - textClickAbsPoint.x - const dy = absY - textClickAbsPoint.y - if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) { - textPressMoved = true - textClickPending = false - return false - } - - return true - } - - function shouldOpenTextEditor() { - return textClickPending && !textPressMoved - } - - function translatedRect(initialRect, startPoint, currentX, currentY) { - const dx = currentX - startPoint.x - const dy = currentY - startPoint.y - return Qt.rect( - initialRect.x + dx, - initialRect.y + dy, - initialRect.width, - initialRect.height - ) - } -} diff --git a/qml/Capture/TZGifRecordBar.qml b/qml/Capture/TZGifRecordBar.qml deleted file mode 100644 index f5b4003..0000000 --- a/qml/Capture/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/Capture/TZHotkeyGestureController.qml b/qml/Capture/TZHotkeyGestureController.qml deleted file mode 100644 index 03fc84b..0000000 --- a/qml/Capture/TZHotkeyGestureController.qml +++ /dev/null @@ -1,96 +0,0 @@ -import QtQuick - -Item { - id: controller - - visible: false - - property bool enabled: false - property bool armed: false - property bool longPressed: false - property string mainToken: "" - property int longPressInterval: 280 - - signal longPressTriggered() - - 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 beginGesture(shortcutSeq) { - enabled = true - armed = false - longPressed = false - mainToken = parseMainKeyToken(shortcutSeq) - longPressTimer.stop() - } - - function clearGesture() { - enabled = false - armed = false - longPressed = false - mainToken = "" - longPressTimer.stop() - } - - function armGesture() { - if (!enabled) - return - armed = true - longPressed = false - longPressTimer.restart() - } - - function handleKeyRelease(key) { - if (!armed || !keyMatchesToken(key, mainToken)) - return false - - longPressTimer.stop() - return true - } - - Timer { - id: longPressTimer - interval: controller.longPressInterval - repeat: false - onTriggered: { - if (controller.armed) { - controller.longPressed = true - controller.longPressTriggered() - } - } - } -} diff --git a/qml/Capture/TZLable.qml b/qml/Capture/TZLable.qml deleted file mode 100644 index 0863d50..0000000 --- a/qml/Capture/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/Capture/TZMagnifyingGlass.qml b/qml/Capture/TZMagnifyingGlass.qml deleted file mode 100644 index fc4705a..0000000 --- a/qml/Capture/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: magnifier.visible - ? ("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/Capture/TZMarkToolInfo.qml b/qml/Capture/TZMarkToolInfo.qml deleted file mode 100644 index 5c35099..0000000 --- a/qml/Capture/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/Capture/TZRectMaskOverlay.qml b/qml/Capture/TZRectMaskOverlay.qml deleted file mode 100644 index 7cf17d4..0000000 --- a/qml/Capture/TZRectMaskOverlay.qml +++ /dev/null @@ -1,48 +0,0 @@ -import QtQuick - -Item { - id: overlay - - property rect holeRect: Qt.rect(0, 0, 0, 0) - property color maskColor: "#99000000" - - readonly property rect normalizedHoleRect: { - const left = Math.min(holeRect.x, holeRect.x + holeRect.width) - const right = Math.max(holeRect.x, holeRect.x + holeRect.width) - const top = Math.min(holeRect.y, holeRect.y + holeRect.height) - const bottom = Math.max(holeRect.y, holeRect.y + holeRect.height) - return Qt.rect(left, top, Math.max(0, right - left), Math.max(0, bottom - top)) - } - - Rectangle { - x: 0 - y: 0 - width: overlay.width - height: Math.max(0, overlay.normalizedHoleRect.y) - color: overlay.maskColor - } - - Rectangle { - x: 0 - y: overlay.normalizedHoleRect.y - width: Math.max(0, overlay.normalizedHoleRect.x) - height: Math.max(0, overlay.normalizedHoleRect.height) - color: overlay.maskColor - } - - Rectangle { - x: overlay.normalizedHoleRect.x + overlay.normalizedHoleRect.width - y: overlay.normalizedHoleRect.y - width: Math.max(0, overlay.width - x) - height: Math.max(0, overlay.normalizedHoleRect.height) - color: overlay.maskColor - } - - Rectangle { - x: 0 - y: overlay.normalizedHoleRect.y + overlay.normalizedHoleRect.height - width: overlay.width - height: Math.max(0, overlay.height - y) - color: overlay.maskColor - } -} diff --git a/qml/Capture/TZResizeHandle.qml b/qml/Capture/TZResizeHandle.qml deleted file mode 100644 index 91c8ab1..0000000 --- a/qml/Capture/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/Capture/TZResizeHandleGroup.qml b/qml/Capture/TZResizeHandleGroup.qml deleted file mode 100644 index 2a158a4..0000000 --- a/qml/Capture/TZResizeHandleGroup.qml +++ /dev/null @@ -1,301 +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) - property var coordinateItem: null - x:area.x - y:area.y - width: area.width - height: area.height - color: "transparent" - function pointInCoordinateItem(item, x, y) { - if (coordinateItem) - return item.mapToItem(coordinateItem, x, y) - return item.mapToItem(null, x, y) - } - //左上角控制点 - 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 = handleGroup.pointInCoordinateItem(leftTop, 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 = handleGroup.pointInCoordinateItem(leftTop, 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 = handleGroup.pointInCoordinateItem(top, mouseX, mouseY); - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - // 重置所有临时变量 - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = handleGroup.pointInCoordinateItem(top, 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 = handleGroup.pointInCoordinateItem(rightTop, 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 = handleGroup.pointInCoordinateItem(rightTop, 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 = handleGroup.pointInCoordinateItem(right, mouseX, mouseY); - mousePointStart.x = globalPos.x - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = handleGroup.pointInCoordinateItem(right, 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 = handleGroup.pointInCoordinateItem(rightButtom, 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 = handleGroup.pointInCoordinateItem(rightButtom, 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 = handleGroup.pointInCoordinateItem(buttom, mouseX, mouseY); - mousePointStart.y = globalPos.y - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = handleGroup.pointInCoordinateItem(buttom, 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 = handleGroup.pointInCoordinateItem(leftBottom, 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 = handleGroup.pointInCoordinateItem(leftBottom, 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 = handleGroup.pointInCoordinateItem(left, mouseX, mouseY); - mousePointStart.x = globalPos.x - initialArea = area - } - - onReleased: { - mousePointStart = Qt.point(0,0) - initialArea = area - } - - onPositionChanged: { - if (pressed) { - let globalPos = handleGroup.pointInCoordinateItem(left, mouseX, mouseY); - let dx = globalPos.x - mousePointStart.x - area.x = initialArea.x + dx - area.width = initialArea.width - dx - } - } - } - - } - -} diff --git a/qml/Capture/TZSelectionInfoLabel.qml b/qml/Capture/TZSelectionInfoLabel.qml deleted file mode 100644 index 966ddd5..0000000 --- a/qml/Capture/TZSelectionInfoLabel.qml +++ /dev/null @@ -1,19 +0,0 @@ -import QtQuick - -TZLable { - id: label - - property rect logicalRect: Qt.rect(0, 0, 0, 0) - property rect physicalRect: Qt.rect(0, 0, 0, 0) - - width: 360 - height: 20 - lableText: qsTr("X:%1 Y:%2 P:%3*%4 L:%5*%6 ") - .arg(logicalRect.x) - .arg(logicalRect.y) - .arg(Math.abs(physicalRect.x)) - .arg(Math.abs(physicalRect.y)) - .arg(Math.abs(logicalRect.width)) - .arg(Math.abs(logicalRect.height)) - -} diff --git a/qml/Capture/TZSelectionOverlay.qml b/qml/Capture/TZSelectionOverlay.qml deleted file mode 100644 index 6d77ab3..0000000 --- a/qml/Capture/TZSelectionOverlay.qml +++ /dev/null @@ -1,197 +0,0 @@ -import QtQuick - -QtObject { - id: overlay - - property rect area: Qt.rect(0, 0, 0, 0) - property rect initialArea: Qt.rect(0, 0, 0, 0) - property point dragOffset: Qt.point(0, 0) - property string dragMode: "none" // none | select | move | expand - property var coordinateItem: null - property bool expandLeft: false - property bool expandRight: false - property bool expandTop: false - property bool expandBottom: false - - readonly property rect localArea: { - if (!coordinateItem) - return area - const topLeft = coordinateItem.mapFromGlobal(area.x, area.y) - return Qt.rect(topLeft.x, topLeft.y, area.width, area.height) - } - - readonly property rect localNormalizedArea: { - const bounds = getNormalizedRectBounds(localArea) - return bounds.isValid - ? Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height) - : Qt.rect(0, 0, 0, 0) - } - - readonly property rect normalizedArea: { - const bounds = getNormalizedRectBounds(area) - return bounds.isValid - ? Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height) - : Qt.rect(0, 0, 0, 0) - } - - function hasArea() { - return Math.abs(area.width) > 2 && Math.abs(area.height) > 2 - } - - function getNormalizedRectBounds(targetRect) { - if (targetRect.width === 0 || targetRect.height === 0) { - return { left: 0, right: 0, top: 0, bottom: 0, isValid: false } - } - - const left = Math.min(targetRect.x, targetRect.x + targetRect.width) - const right = Math.max(targetRect.x, targetRect.x + targetRect.width) - const top = Math.min(targetRect.y, targetRect.y + targetRect.height) - const bottom = Math.max(targetRect.y, targetRect.y + targetRect.height) - return { - left: left, - right: right, - top: top, - bottom: bottom, - width: right - left, - height: bottom - top, - isValid: true - } - } - - function containsPoint(targetRect, x, y) { - const bounds = getNormalizedRectBounds(targetRect) - if (!bounds.isValid) - return false - return x >= bounds.left && x <= bounds.right - && y >= bounds.top && y <= bounds.bottom - } - - function containsCurrentPoint(x, y) { - return containsPoint(area, x, y) - } - - function selectionRectFromDrag(startX, startY, endX, endY, withShift, withAlt) { - let x = startX - let y = startY - let w = endX - startX - let h = endY - startY - - if (withAlt) { - x = startX - (endX - startX) - y = startY - (endY - startY) - w = (endX - startX) * 2 - h = (endY - startY) * 2 - } - - if (withShift) { - const absW = Math.abs(w) - const absH = Math.abs(h) - const side = Math.min(absW, absH) - w = w < 0 ? -side : side - h = h < 0 ? -side : side - } - - return Qt.rect(x, y, w, h) - } - - function beginSelection(ax, ay) { - dragMode = "select" - initialArea = Qt.rect(ax, ay, 0, 0) - area = Qt.rect(ax, ay, 0, 0) - } - - function beginMove(ax, ay) { - dragMode = "move" - dragOffset = Qt.point(ax, ay) - initialArea = area - } - - function beginExpand(ax, ay) { - const bounds = getNormalizedRectBounds(area) - if (!bounds.isValid) { - beginSelection(ax, ay) - return - } - - dragMode = "expand" - initialArea = Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height) - area = initialArea - expandLeft = ax < bounds.left - expandRight = ax > bounds.right - expandTop = ay < bounds.top - expandBottom = ay > bounds.bottom - } - - function updateMove(ax, ay) { - const dx = ax - dragOffset.x - const dy = ay - dragOffset.y - area = Qt.rect( - initialArea.x + dx, - initialArea.y + dy, - initialArea.width, - initialArea.height - ) - } - - function updateSelection(ax, ay, modifiers) { - const useShift = (modifiers & Qt.ShiftModifier) !== 0 - const useAlt = (modifiers & Qt.AltModifier) !== 0 - area = selectionRectFromDrag( - initialArea.x, - initialArea.y, - ax, - ay, - useShift, - useAlt - ) - } - - function updateExpand(ax, ay) { - const bounds = getNormalizedRectBounds(initialArea) - if (!bounds.isValid) - return - - let left = bounds.left - let right = bounds.right - let top = bounds.top - let bottom = bounds.bottom - - if (expandLeft) - left = Math.min(ax, right) - if (expandRight) - right = Math.max(ax, left) - if (expandTop) - top = Math.min(ay, bottom) - if (expandBottom) - bottom = Math.max(ay, top) - - area = Qt.rect(left, top, right - left, bottom - top) - } - - function normalizeCurrentArea() { - const bounds = getNormalizedRectBounds(area) - if (bounds.isValid) { - area = Qt.rect(bounds.left, bounds.top, bounds.width, bounds.height) - initialArea = area - } - } - - function endInteraction() { - dragMode = "none" - expandLeft = false - expandRight = false - expandTop = false - expandBottom = false - } - - function reset() { - area = Qt.rect(0, 0, 0, 0) - initialArea = Qt.rect(0, 0, 0, 0) - dragOffset = Qt.point(0, 0) - dragMode = "none" - expandLeft = false - expandRight = false - expandTop = false - expandBottom = false - } -} diff --git a/qml/Capture/TZSelectionPositioner.qml b/qml/Capture/TZSelectionPositioner.qml deleted file mode 100644 index 3ea94c1..0000000 --- a/qml/Capture/TZSelectionPositioner.qml +++ /dev/null @@ -1,34 +0,0 @@ -import QtQuick - -QtObject { - id: positioner - - property int gap: 6 - - function clamp(value, minValue, maxValue) { - return Math.max(minValue, Math.min(value, maxValue)) - } - - function alignLeft(areaRect, itemWidth, viewportWidth) { - return clamp(areaRect.x, 0, Math.max(0, viewportWidth - itemWidth)) - } - - function alignRight(areaRect, itemWidth, viewportWidth) { - const preferredX = areaRect.x + areaRect.width - itemWidth - return clamp(preferredX, 0, Math.max(0, viewportWidth - itemWidth)) - } - - function placeBelowOrAbove(areaRect, itemHeight, viewportHeight, customGap) { - const spacing = customGap === undefined ? gap : customGap - const belowY = areaRect.y + areaRect.height + spacing - if (belowY + itemHeight <= viewportHeight) - return belowY - return areaRect.y - itemHeight - spacing - } - - function placeAbove(areaRect, itemHeight, viewportHeight, customGap) { - const spacing = customGap === undefined ? gap : customGap - const preferredY = areaRect.y - itemHeight - spacing - return clamp(preferredY, 0, Math.max(0, viewportHeight - itemHeight)) - } -} diff --git a/qml/Capture/TZTextAnnotationEditor.qml b/qml/Capture/TZTextAnnotationEditor.qml deleted file mode 100644 index 7b9a625..0000000 --- a/qml/Capture/TZTextAnnotationEditor.qml +++ /dev/null @@ -1,101 +0,0 @@ -import QtQuick - -Rectangle { - id: editor - - property alias editorText: textInput.text - - signal accepted(string text) - signal canceled() - - width: 260 - height: 88 - visible: false - radius: 8 - color: "#FFFFFF" - border.color: "#CBD5E1" - border.width: 1 - - function beginEdit(textValue) { - editorText = textValue - visible = true - textInput.forceActiveFocus() - textInput.selectAll() - } - - 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 - onAccepted: editor.accepted(text) - } - } - - 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: editor.canceled() - } - } - - 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: editor.accepted(textInput.text) - } - } - } - } -} diff --git a/qml/Capture/TZTool.qml b/qml/Capture/TZTool.qml deleted file mode 100644 index f117cab..0000000 --- a/qml/Capture/TZTool.qml +++ /dev/null @@ -1,278 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.5 - -Rectangle { - id: tool - 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 - property bool showScreenshotButton: true - property bool showCancelButton: true - property bool showSaveButton: true - property bool showStickyButton: true - property bool showUndoButton: true - property bool showGifRecordButton: true - property bool showLongScreenshotButton: true - property bool showOcrButton: true - property bool showRectButton: true - property bool showCircleButton: true - property bool showArrowButton: true - property bool showPencilButton: true - property bool showHighlightButton: true - 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) { - 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 = "" - } - - radius: outerRadius - implicitWidth: toolRow.implicitWidth + outerMargin * 2 - implicitHeight: Math.max(buttonSize + outerMargin * 2, 44) - - RowLayout { - id: toolRow - anchors.fill: parent - anchors.margins: tool.outerMargin - layoutDirection: Qt.RightToLeft - spacing: tool.rowSpacing - - Rectangle { - visible: tool.showScreenshotButton - 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: 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: 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: 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: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } - - Rectangle { - visible: tool.showOcrButton - 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: 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: 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: 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: 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: 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: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } - - Rectangle { - visible: tool.showSaveButton - 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: 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: 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: 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: tool.dividerHeight; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter } - - Rectangle { - visible: tool.showRectButton - 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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() } - } - } - - Rectangle { - id: tipBubble - visible: tool.visible && tool.hoverTipText.length > 0 - z: 100 - parent: tool.parent - 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 + tool.bubbleGap - - Text { - id: tipText - anchors.centerIn: parent - text: tool.hoverTipText - color: "#FFFFFF" - font.pixelSize: tool.tipFontSize - } - } -} diff --git a/qml/Capture/TZWindow.qml b/qml/Capture/TZWindow.qml deleted file mode 100644 index 02ea925..0000000 --- a/qml/Capture/TZWindow.qml +++ /dev/null @@ -1,765 +0,0 @@ -import QtQuick -import QtQuick.Window -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 alias initialScreenshotArea: selectionOverlay.initialArea - property alias screenshotArea: selectionOverlay.area - property alias offset: selectionOverlay.dragOffset - property alias dragMode: selectionOverlay.dragMode - 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 bool gifRecordMode: false - readonly property bool gifAreaLocked: O_GifRecordVM.isRecording || O_GifRecordVM.isEncoding - - property alias tool: tool - property alias paintBoard: paintBoard - property alias magnifier: magnifier - property alias gifBarObj: gifBar - - function hasSelectionArea() { - return selectionOverlay.hasArea() - } - - function beginHotkeyGesture(shortcutSeq) { - hotkeyGesture.beginGesture(shortcutSeq) - } - - function clearHotkeyGesture() { - hotkeyGesture.clearGesture() - } - - function maybeArmHotkeyGesture() { - if (!hotkeyGesture.enabled || !hasSelectionArea()) - return - hotkeyGesture.armGesture() - } - - 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 (windowFactory.closeTopOcrWindow()) { - 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 beginSelectionInteraction(ax, ay) { - if (screenshotArea.width !== 0 || screenshotArea.height !== 0) { - if (selectionOverlay.containsCurrentPoint(ax, ay)) { - selectionOverlay.beginMove(ax, ay) - return - } - - paintBoard.reset() - tool.activeTool = "" - tool.markToolInfoVisible = false - tool.visible = false - resizeHandleGroup.visible = false - hideTextEditor() - highlightRect = Qt.rect(0, 0, 0, 0) - selectionOverlay.beginExpand(ax, ay) - return - } - - paintBoard.reset() - tool.activeTool = "" - selectionOverlay.beginSelection(ax, ay) - } - - function updateSelectionInteraction(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") { - selectionOverlay.updateMove(ax, ay) - return - } - - if (dragMode === "select") { - selectionOverlay.updateSelection(ax, ay, modifiers) - if (tool.visible) tool.visible = false - return - } - - if (dragMode === "expand") { - selectionOverlay.updateExpand(ax, ay) - if (tool.visible) tool.visible = false - } - } - - function finalizeSelectionInteraction() { - selectionOverlay.endInteraction() - if (Math.abs(screenshotArea.width) <= 5 && Math.abs(screenshotArea.height) <= 5) { - if (highlightRect.width > 0 && highlightRect.height > 0) { - screenshotArea = highlightRect - console.log("screenshotArea2-->",screenshotArea) - initialScreenshotArea = highlightRect - resizeHandleGroup.area = localScreenshotArea - resizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea) - highlightRect = Qt.rect(0, 0, 0, 0) - Qt.callLater(function() { tool.visible = true }) - } - resizeHandleGroup.visible = (highlightRect.width > 0 && highlightRect.height > 0) - return - } - selectionOverlay.normalizeCurrentArea() - resizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea) - if (Math.abs(screenshotArea.width) > 2 && Math.abs(screenshotArea.height) > 2) { - Qt.callLater(function() { tool.visible = true }) - } - resizeHandleGroup.area = localScreenshotArea - resizeHandleGroup.visible = true; - - } - function toAbsolutePoint(localX, localY) { - return screenshotRect.mapToGlobal(localX, localY) - } - function toLocalRect(absRect) { - var topLeft = screenshotRect.mapFromGlobal(absRect.x, absRect.y) - return Qt.rect(topLeft.x, topLeft.y, absRect.width, absRect.height) - } - function toAbsoluteRect(localRect) { - var topLeft = screenshotRect.mapToGlobal(localRect.x, localRect.y) - return Qt.rect(topLeft.x, topLeft.y, localRect.width, localRect.height) - } - function syncResizeHandles() { - resizeHandleGroup.area = localScreenshotArea - resizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea) - } - function syncGifResizeHandles() { - gifResizeHandleGroup.area = localScreenshotArea - gifResizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea) - } - function normalizeRect(rectValue) { - var left = Math.min(rectValue.x, rectValue.x + rectValue.width) - var right = Math.max(rectValue.x, rectValue.x + rectValue.width) - var top = Math.min(rectValue.y, rectValue.y + rectValue.height) - var bottom = Math.max(rectValue.y, rectValue.y + rectValue.height) - return Qt.rect(left, top, right - left, bottom - top) - } - - // screenshotArea 在本窗口本地坐标中的等价矩形(用于绘制) - readonly property rect localScreenshotArea: toLocalRect(screenshotArea) - readonly property rect localNormalizedScreenshotArea: { - return normalizeRect(localScreenshotArea) - } - readonly property rect normalizedScreenshotArea: { - return selectionOverlay.normalizedArea - } - readonly property rect physicalScreenshotArea: { - if (screenshotArea.width === 0 || screenshotArea.height === 0) - return Qt.rect(0, 0, 0, 0) - return O_ScreenCapture.mapLogicalRectToPhysicalRect(normalizedScreenshotArea) - } - readonly property rect localHighlightRect: Qt.rect( - highlightRect.x - screenVirtualX, - highlightRect.y - screenVirtualY, - highlightRect.width, - highlightRect.height - ) - - function createStickyWindow(imageUrl, imgRect) { - return windowFactory.createStickyWindow(imageUrl, imgRect) - } - - function createOcrResultWindow() { - return windowFactory.createOcrResultWindow() - } - - function openTextEditor(localX, localY) { - pendingTextPoint = Qt.point(localX, localY) - pendingTextValue = markToolInfo.annotationText - 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)) - textEditor.beginEdit(pendingTextValue) - } - - function hideTextEditor() { - textEditor.visible = false - } - - function submitTextEditor(textValue) { - var text = textValue.trim() - if (text.length === 0) { - hideTextEditor() - return - } - markToolInfo.annotationText = text - paintBoard.addTextAnnotation(Math.round(pendingTextPoint.x), Math.round(pendingTextPoint.y), text) - hideTextEditor() - } - - function resetTextClickTracking() { - inputController.resetTextClickTracking() - } - - function beginGifInteraction(ax, ay) { - if (!gifRecordMode || gifAreaLocked) - return false - if (selectionOverlay.containsCurrentPoint(ax, ay)) { - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - } - return true - } - - function beginSpaceMoveInteraction(ax, ay) { - if (!inputController.spaceHeld || !hasSelectionArea()) - return false - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - return true - } - - function beginTextToolInteraction(ax, ay, localX, localY) { - if (tool.activeTool !== "text" - || screenshotArea.width === 0 - || screenshotArea.height === 0 - || !selectionOverlay.containsCurrentPoint(ax, ay)) { - return false - } - - inputController.beginTextClick(ax, ay, localX, localY) - offset.x = ax - offset.y = ay - initialScreenshotArea = screenshotArea - return true - } - - function updatePendingTextInteraction(ax, ay) { - var stillPending = inputController.updatePendingTextClick(ax, ay, 3) - if (!stillPending && inputController.textPressMoved && textEditor.visible) - hideTextEditor() - return stillPending - } - - function updateGifInteraction(ax, ay, isPressed) { - if (!gifRecordMode) - return false - if (gifAreaLocked) - return true - if (isPressed && selectionOverlay.containsCurrentPoint(ax, ay)) { - screenshotArea = inputController.translatedRect(initialScreenshotArea, offset, ax, ay) - O_GifRecordVM.updateCaptureRect( - screenshotArea.x, - screenshotArea.y, - screenshotArea.width, - screenshotArea.height - ) - } - return true - } - - function updateSpaceMoveInteraction(ax, ay, isPressed) { - if (!isPressed || !inputController.spaceHeld || !hasSelectionArea()) - return false - - screenshotArea = inputController.translatedRect(initialScreenshotArea, offset, ax, ay) - return true - } - - function updateMagnifierFromMouse(localX, localY, ax, ay) { - magnifier.magnifierMouseX = localX - magnifier.magnifierMouseY = localY - magnifier.magnifierAbsX = ax - magnifier.magnifierAbsY = ay - } - - function finalizeTextToolInteraction() { - if (inputController.shouldOpenTextEditor()) { - openTextEditor(inputController.textClickLocalPoint.x, - inputController.textClickLocalPoint.y) - resetTextClickTracking() - return true - } - - resetTextClickTracking() - return false - } - - function finalizeGifInteraction() { - if (!gifRecordMode) - return false - if (gifAreaLocked) - return true - - initialScreenshotArea = screenshotArea - gifResizeHandleGroup.initialArea = toLocalRect(initialScreenshotArea) - return true - } - - function resetArea() { - selectionOverlay.reset() - syncResizeHandles() - syncGifResizeHandles() - screenShotUrl = "" - paintBoard.reset() - tool.visible = false - tool.activeTool = "" - tool.markToolInfoVisible = false - hideTextEditor() - gifRecordMode = false - gifBar.visible = false - highlightRect = Qt.rect(0, 0, 0, 0) - resetTextClickTracking() - clearHotkeyGesture() - O_ScreenCapture.releaseDesktopSnapshot() - root.visibility = Window.Hidden; - } - - TZSelectionOverlay { - id: selectionOverlay - coordinateItem: screenshotRect - } - - TZWindowFactory { - id: windowFactory - } - - TZSelectionPositioner { - id: selectionPositioner - } - - TZCaptureInputController { - id: inputController - } - - TZHotkeyGestureController { - id: hotkeyGesture - onLongPressTriggered: { - if (hasSelectionArea()) - 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: selectionPositioner.alignRight(localNormalizedScreenshotArea, width, mainWin.width) - y: selectionPositioner.placeBelowOrAbove(localNormalizedScreenshotArea, height, mainWin.height) - 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 - - TZRectMaskOverlay { - anchors.fill: parent - holeRect: localNormalizedScreenshotArea - maskColor: "#99000000" - } - - TZSelectionInfoLabel { - visible: screenshotArea.width !== 0 && screenshotArea.height !== 0 - x: selectionPositioner.alignLeft(localNormalizedScreenshotArea, width, mainWin.width) - y: selectionPositioner.placeAbove(localNormalizedScreenshotArea, height, mainWin.height, 5) - logicalRect: normalizedScreenshotArea - physicalRect: physicalScreenshotArea - } - - TZTool { - id: tool - width: 656; height: 44 - z: 30 - - // Toolbar position (local coordinates) - x: selectionPositioner.alignRight(localNormalizedScreenshotArea, width, mainWin.width) - y: selectionPositioner.placeBelowOrAbove(localNormalizedScreenshotArea, height, mainWin.height) - - 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 captureRect = normalizedScreenshotArea - var url = O_ScreenCapture.captureRectToStickyUrl(paintBoard, captureRect) - O_StickyViewModel.requestSticky(url, captureRect) - 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" - } - - TZTextAnnotationEditor { - id: textEditor - z: 40 - onAccepted: function(text) { submitTextEditor(text) } - onCanceled: hideTextEditor() - } - - TZResizeHandleGroup { - id: resizeHandleGroup - coordinateItem: screenshotRect - // Handle control points use local coordinates - area: localScreenshotArea - initialArea: toLocalRect(initialScreenshotArea) - z: 2 - onAreaChanged: { - // 转回绝对坐标 - screenshotArea = toAbsoluteRect(area) - } - onInitialAreaChanged: { - initialScreenshotArea = toAbsoluteRect(initialArea) - } - } - } - - TZResizeHandleGroup { - id: gifResizeHandleGroup - coordinateItem: screenshotRect - area: localScreenshotArea - initialArea: toLocalRect(initialScreenshotArea) - visible: gifRecordMode && !gifAreaLocked - enabled: !gifAreaLocked - z: 15 - onAreaChanged: { - screenshotArea = toAbsoluteRect(area) - O_GifRecordVM.updateCaptureRect( - screenshotArea.x, screenshotArea.y, - screenshotArea.width, screenshotArea.height - ) - } - onInitialAreaChanged: { - initialScreenshotArea = toAbsoluteRect(initialArea) - } - } - - // Window highlight rectangle (local coordinates) - TZWindowHighlightOverlay { - 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 - z: 5 - } - - 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 globalPos = screenshotRect.mapToGlobal(mouseX, mouseY) - - var ax = globalPos.x, ay = globalPos.y - resetTextClickTracking() - - if (beginGifInteraction(ax, ay)) { - return - } - - if (beginSpaceMoveInteraction(ax, ay)) { - return - } - - if (beginTextToolInteraction(ax, ay, mouseX, mouseY)) { - return - } - - if (textEditor.visible) { - hideTextEditor() - } - beginSelectionInteraction(ax, ay) - } - - onPositionChanged: function(mouse) { - var globalPos = screenshotRect.mapToGlobal(mouseX, mouseY) - var ax = globalPos.x, ay = globalPos.y - - if (pressed && updatePendingTextInteraction(ax, ay)) { - return - } - - if (updateGifInteraction(ax, ay, pressed)) { - return - } - - if (updateSpaceMoveInteraction(ax, ay, pressed)) { - return - } - - updateMagnifierFromMouse(mouseX, mouseY, ax, ay) - updateSelectionInteraction(ax, ay, pressed, mouse.modifiers) - } - - onReleased: function(mouse) { - if (finalizeTextToolInteraction()) { - return - } - - if (finalizeGifInteraction()) { - return - } - - finalizeSelectionInteraction() - } - - onWheel: function(wheel) { wheel.accepted = true } - - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Space) { - inputController.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) { - inputController.spaceHeld = false - event.accepted = true - return - } - - if (hotkeyGesture.handleKeyRelease(event.key)) { - if (!hotkeyGesture.longPressed && hasSelectionArea()) - confirmSelectionToClipboard() - clearHotkeyGesture() - event.accepted = true - } - } - } - onVisibilityChanged: - { - if(root.visibility === Window.Hidden) { - resizeHandleGroup.visible = false; - gifResizeHandleGroup.visible = false; - } - } - -} diff --git a/qml/Capture/TZWindowFactory.qml b/qml/Capture/TZWindowFactory.qml deleted file mode 100644 index 42ceea7..0000000 --- a/qml/Capture/TZWindowFactory.qml +++ /dev/null @@ -1,116 +0,0 @@ -import QtQuick - -QtObject { - id: factory - - property var stickyWindows: [] - property var ocrWindows: [] - property var stickyComponent: null - property var ocrComponent: null - - function createStickyWindow(imageUrl, imgRect) { - if (!imageUrl || imageUrl.length === 0) { - console.error("Failed to create sticky window: invalid image URL") - return null - } - - if (stickyComponent === null) - stickyComponent = Qt.createComponent("qrc:/qml/Windows/TZStickyWindow.qml") - - var newWindow = null - if (stickyComponent.status === Component.Ready) { - newWindow = stickyComponent.createObject(null, { - "imageUrl": imageUrl, - "imgRect": imgRect - }) - } else if (stickyComponent.status === Component.Error) { - console.error("Failed to load TZStickyWindow.qml:", stickyComponent.errorString()) - } - - if (!newWindow) { - console.error("Failed to create sticky window object") - return null - } - - newWindow.visible = true - newWindow.raise() - newWindow.requestActivate() - - var stickyList = stickyWindows.slice() - stickyList.push(newWindow) - stickyWindows = stickyList - - var windowRef = newWindow - function onWindowClosing(closeEvent) { - var idx = stickyWindows.indexOf(windowRef) - if (idx !== -1) { - var updatedList = stickyWindows.slice() - updatedList.splice(idx, 1) - stickyWindows = updatedList - } - windowRef.closing.disconnect(onWindowClosing) - windowRef = null - } - newWindow.closing.connect(onWindowClosing) - return newWindow - } - - function createOcrResultWindow() { - if (ocrWindows.length > 0) { - var existing = ocrWindows[ocrWindows.length - 1] - if (existing) { - existing.show() - existing.raise() - existing.requestActivate() - return existing - } - } - - if (ocrComponent === null) - ocrComponent = Qt.createComponent("qrc:/qml/Windows/TZOcrResult.qml") - - var win = null - if (ocrComponent.status === Component.Ready) { - win = ocrComponent.createObject(null) - } else if (ocrComponent.status === Component.Error) { - console.error("Failed to load TZOcrResult.qml:", ocrComponent.errorString()) - } - - if (!win) - return null - - win.show() - win.raise() - win.requestActivate() - - var list = ocrWindows.slice() - list.push(win) - ocrWindows = list - - var winRef = win - function onOcrClosing(closeEvent) { - var idx = ocrWindows.indexOf(winRef) - if (idx !== -1) { - var updatedList = ocrWindows.slice() - updatedList.splice(idx, 1) - ocrWindows = updatedList - } - winRef.closing.disconnect(onOcrClosing) - winRef = null - } - win.closing.connect(onOcrClosing) - return win - } - - function closeTopOcrWindow() { - if (ocrWindows.length === 0) - return false - - var topWin = ocrWindows[ocrWindows.length - 1] - if (!topWin) - return false - - topWin.close() - return true - } -} diff --git a/qml/Capture/TZWindowHighlightOverlay.qml b/qml/Capture/TZWindowHighlightOverlay.qml deleted file mode 100644 index ee873d3..0000000 --- a/qml/Capture/TZWindowHighlightOverlay.qml +++ /dev/null @@ -1,46 +0,0 @@ -import QtQuick - -Rectangle { - id: highlight - - color: "transparent" - border.color: "#3B82F6" - border.width: 2 - - 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 } - } - } - } -} diff --git a/qml/LongCapture/TZLongCaptureBar.qml b/qml/LongCapture/TZLongCaptureBar.qml deleted file mode 100644 index ab97b06..0000000 --- a/qml/LongCapture/TZLongCaptureBar.qml +++ /dev/null @@ -1,110 +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/LongCapture/TZLongCaptureFrame.qml b/qml/LongCapture/TZLongCaptureFrame.qml deleted file mode 100644 index 24e2739..0000000 --- a/qml/LongCapture/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/LongCapture/TZLongCapturePreview.qml b/qml/LongCapture/TZLongCapturePreview.qml deleted file mode 100644 index 9be4818..0000000 --- a/qml/LongCapture/TZLongCapturePreview.qml +++ /dev/null @@ -1,83 +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/Setting/TZAboutWin.qml b/qml/Setting/TZAboutWin.qml deleted file mode 100644 index 0e9e0e9..0000000 --- a/qml/Setting/TZAboutWin.qml +++ /dev/null @@ -1,128 +0,0 @@ -import QtQuick -import QtQuick.Window -import QtQuick.Layouts - -Window { - id: aboutWin - width: 400 - height: 350 - title: qsTr("关于 TZshot") - color: "#F8FAFC" - flags: Qt.Dialog | Qt.WindowCloseButtonHint - minimumWidth: width - maximumWidth: width - minimumHeight: height - maximumHeight: height - - Component.onCompleted: { - x = (Screen.width - width) / 2 - y = (Screen.height - height) / 2 - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: 0 - - // ── Logo 区 ────────────────────────────────── - Item { - Layout.alignment: Qt.AlignHCenter - width: 72; height: 72 - - Rectangle { - anchors.fill: parent - radius: 18 - color: "#3B82F6" - - Text { - anchors.centerIn: parent - text: "T" - font.pixelSize: 40 - font.bold: true - color: "#FFFFFF" - } - } - } - - Item { Layout.preferredHeight: 16 } - - // ── 应用名称 ────────────────────────────────── - Text { - Layout.alignment: Qt.AlignHCenter - text: "TZshot" - font.pixelSize: 22 - font.bold: true - color: "#1E293B" - } - - Item { Layout.preferredHeight: 6 } - - // ── 版本号 ──────────────────────────────────── - Text { - Layout.alignment: Qt.AlignHCenter - text: qsTr("版本 v1.0.0") - font.pixelSize: 13 - color: "#64748B" - } - - Item { Layout.preferredHeight: 16 } - - // ── 分割线 ──────────────────────────────────── - Rectangle { - Layout.fillWidth: true - height: 1 - color: "#E2E8F0" - } - - Item { Layout.preferredHeight: 16 } - - // ── 描述 ────────────────────────────────────── - Text { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - text: qsTr("简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。") - font.pixelSize: 13 - color: "#475569" - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - - Item { Layout.fillHeight: true } - - // ── 版权 ────────────────────────────────────── - Text { - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 4 - text: qsTr("© 2025 TZshot. All rights reserved.") - font.pixelSize: 11 - color: "#94A3B8" - } - - // ── 关闭按钮 ────────────────────────────────── - Rectangle { - Layout.alignment: Qt.AlignHCenter - width: 88; height: 34 - radius: 8 - color: closeMouse.containsMouse ? "#2563EB" : "#3B82F6" - - Behavior on color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: qsTr("关闭") - font.pixelSize: 13 - color: "#FFFFFF" - } - - MouseArea { - id: closeMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: aboutWin.close() - } - } - - Item { Layout.preferredHeight: 4 } - } -} diff --git a/qml/Setting/TZGeneralSettings.qml b/qml/Setting/TZGeneralSettings.qml deleted file mode 100644 index 0b7f510..0000000 --- a/qml/Setting/TZGeneralSettings.qml +++ /dev/null @@ -1,500 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls.Basic -import TZControls 1.0 - -Rectangle { - color: "#F8FAFC" - - ScrollView { - id: scrollView - anchors.fill: parent - contentWidth: availableWidth - clip: true - - ColumnLayout { - // 必须绑定到 ScrollView.availableWidth,不能用 parent.width - // (ScrollView 内部 contentItem 宽度不受限) - x: 24 - width: scrollView.availableWidth - 48 - spacing: 8 - - Item { Layout.preferredHeight: 4 } - - // ── 大模型设置 标题 ────────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("大模型设置") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - // ── 大模型设置 卡片 ────────────────────────── - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 120 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - // 大模型选择 - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - Layout.preferredWidth: 90 - text: qsTr("大模型选择") - font.pixelSize: 13 - color: "#374151" - Layout.alignment: Qt.AlignVCenter - } - - TZComboBox { - id: combobox - model: [qsTr("阿里云百炼"), qsTr("火山引擎")] - currentIndex: O_AIViewModel.selectedModel - Layout.preferredWidth: 220 - Layout.preferredHeight: 36 - - onCurrentIndexChanged: { - O_AIViewModel.setSelectedModel(currentIndex) - } - } - } - - // API Key - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - Layout.preferredWidth: 90 - text: "API Key" - font.pixelSize: 13 - color: "#374151" - Layout.alignment: Qt.AlignVCenter - } - - Rectangle { - Layout.preferredWidth: 320 - Layout.preferredHeight: 36 - radius: 6 - color: apiKeyInput.activeFocus ? "#EFF6FF" : "#F8FAFC" - border.color: apiKeyInput.activeFocus ? "#3B82F6" : "#E2E8F0" - border.width: apiKeyInput.activeFocus ? 2 : 1 - - Behavior on border.color { ColorAnimation { duration: 150 } } - Behavior on color { ColorAnimation { duration: 150 } } - - TextInput { - id: apiKeyInput - anchors.fill: parent - anchors.margins: 10 - font.pixelSize: 13 - color: "#1E293B" - selectByMouse: true - verticalAlignment: Text.AlignVCenter - clip: true - - // 初始化时从后端读取已持久化的 apiKey - Component.onCompleted: { - text = O_AIViewModel.apiKey - } - - Text { - anchors.fill: parent - verticalAlignment: Text.AlignVCenter - text: qsTr("请输入 API Key") - font.pixelSize: 13 - color: "#CBD5E1" - visible: !apiKeyInput.text && !apiKeyInput.activeFocus - } - } - } - - // 保存按钮 - Rectangle { - id: saveApiKeyBtn - Layout.preferredWidth: 60 - Layout.preferredHeight: 36 - radius: 6 - property bool modified: apiKeyInput.text !== O_AIViewModel.apiKey - color: { - if (!modified) return "#E2E8F0" - if (saveArea.containsMouse) return "#2563EB" - return "#3B82F6" - } - Behavior on color { ColorAnimation { duration: 120 } } - - Text { - anchors.centerIn: parent - text: qsTr("保存") - font.pixelSize: 13 - color: saveApiKeyBtn.modified ? "#FFFFFF" : "#94A3B8" - } - - MouseArea { - id: saveArea - anchors.fill: parent - hoverEnabled: true - cursorShape: saveApiKeyBtn.modified ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: saveApiKeyBtn.modified - onClicked: { - O_AIViewModel.setApiKey(apiKeyInput.text) - } - } - } - } - - Item { Layout.fillHeight: true } - } - } - - Item { Layout.preferredHeight: 8 } - - // ── 语言设置 标题 ──────────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("语言设置") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - // ── 语言设置 卡片 ──────────────────────────── - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 72 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - RowLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - Text { - Layout.preferredWidth: 90 - text: qsTr("界面语言") - font.pixelSize: 13 - color: "#374151" - Layout.alignment: Qt.AlignVCenter - } - - TZComboBox { - id: langComboBox - // Index 0 = zh_CN, Index 1 = en - model: ["中文", "English"] - currentIndex: O_LanguageManager.language === "en" ? 1 : 0 - Layout.preferredWidth: 160 - Layout.preferredHeight: 36 - - onCurrentIndexChanged: { - var lang = currentIndex === 1 ? "en" : "zh_CN" - O_LanguageManager.setLanguage(lang) - } - } - } - } - - Item { Layout.preferredHeight: 8 } - - // ── 截图保存路径 标题 ──────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("截图保存路径") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - // ── 截图保存路径 卡片 ──────────────────────── - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 72 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - RowLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - Text { - Layout.preferredWidth: 90 - text: qsTr("保存路径") - font.pixelSize: 13 - color: "#374151" - Layout.alignment: Qt.AlignVCenter - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: "#F8FAFC" - border.color: "#E2E8F0" - border.width: 1 - - Text { - id: saveFolder - anchors.fill: parent - anchors.margins: 10 - font.pixelSize: 13 - color: "#374151" - text: O_ImageSaver.savePath - elide: Text.ElideMiddle - verticalAlignment: Text.AlignVCenter - clip: true - wrapMode: Text.NoWrap - - ToolTip { - id: folderTip - visible: false - text: saveFolder.text - delay: 500 - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: folderTip.visible = true - onExited: folderTip.visible = false - } - } - } - - // 浏览按钮 - Rectangle { - Layout.preferredWidth: 72 - Layout.preferredHeight: 36 - radius: 6 - color: browseArea.containsMouse ? "#2563EB" : "#3B82F6" - Behavior on color { ColorAnimation { duration: 120 } } - - Text { - anchors.centerIn: parent - text: qsTr("浏览") - font.pixelSize: 13 - color: "#FFFFFF" - } - - MouseArea { - id: browseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: saveDialog.open() - } - } - } - } - - Item { Layout.preferredHeight: 8 } - - // ── GIF 体积预设 ───────────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("GIF 录制") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 72 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - RowLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - Text { - Layout.preferredWidth: 90 - text: qsTr("体积预设") - font.pixelSize: 13 - color: "#374151" - Layout.alignment: Qt.AlignVCenter - } - - TZComboBox { - id: gifPresetCombo - model: [qsTr("高质量"), qsTr("均衡"), qsTr("小体积")] - currentIndex: O_GifRecordVM.qualityPreset - Layout.preferredWidth: 180 - Layout.preferredHeight: 36 - onCurrentIndexChanged: O_GifRecordVM.setQualityPreset(currentIndex) - } - } - } - - Item { Layout.preferredHeight: 8 } - - // ── OCR 自检 ───────────────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("OCR 自检") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 168 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - radius: 8 - color: "#F8FAFC" - border.color: "#E2E8F0" - border.width: 1 - - ScrollView { - anchors.fill: parent - anchors.margins: 8 - clip: true - TextArea { - text: O_OcrVM.selfCheckText.length > 0 - ? O_OcrVM.selfCheckText - : qsTr("点击“一键自检”查看 OCR 环境状态。") - readOnly: true - wrapMode: TextEdit.Wrap - selectByMouse: true - background: null - font.pixelSize: 12 - color: "#334155" - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Rectangle { - Layout.preferredWidth: 100 - Layout.preferredHeight: 34 - radius: 6 - color: selfCheckMouse.containsMouse ? "#2563EB" : "#3B82F6" - Behavior on color { ColorAnimation { duration: 100 } } - Text { - anchors.centerIn: parent - text: qsTr("一键自检") - font.pixelSize: 13 - color: "#FFFFFF" - } - MouseArea { - id: selfCheckMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: O_OcrVM.runSelfCheck() - } - } - - Rectangle { - Layout.preferredWidth: 130 - Layout.preferredHeight: 34 - radius: 6 - color: openTessMouse.containsMouse ? "#E2E8F0" : "#F1F5F9" - border.color: "#CBD5E1" - border.width: 1 - Behavior on color { ColorAnimation { duration: 100 } } - Text { - anchors.centerIn: parent - text: qsTr("打开 tessdata") - font.pixelSize: 13 - color: "#334155" - } - MouseArea { - id: openTessMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!O_OcrVM.openTessdataFolder()) - O_OcrVM.runSelfCheck() - } - } - } - } - } - } - - Item { Layout.fillHeight: true } - } - } -} diff --git a/qml/Setting/TZHotkeySettings.qml b/qml/Setting/TZHotkeySettings.qml deleted file mode 100644 index 3bfdcef..0000000 --- a/qml/Setting/TZHotkeySettings.qml +++ /dev/null @@ -1,373 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls.Basic - -Rectangle { - color: "#F8FAFC" - - // ── 按键录制弹窗 ────────────────────────────────────── - Rectangle { - id: recordOverlay - anchors.fill: parent - color: "#80000000" - visible: false - z: 100 - - property int recordingActionId: 0 - property string recordingLabel: "" - property string pendingKeySeq: "" - - // 捕获键盘焦点以接收按键事件 - focus: visible - - Keys.onPressed: function(event) { - event.accepted = true - var seq = O_GlobalShortcut.buildKeySequence(event.key, event.modifiers) - if (seq !== "") { - recordOverlay.pendingKeySeq = seq - } - } - - // 弹窗卡片 - Rectangle { - anchors.centerIn: parent - width: 360 - height: 220 - radius: 12 - color: "#FFFFFF" - border.color: "#E2E8F0" - border.width: 1 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 24 - spacing: 16 - - // 标题 - Text { - Layout.fillWidth: true - text: qsTr("编辑快捷键 · %1").arg(recordOverlay.recordingLabel) - font.pixelSize: 14 - font.bold: true - color: "#1E293B" - } - - // 录制区域 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 56 - radius: 8 - color: "#F1F5F9" - border.color: recordOverlay.pendingKeySeq !== "" ? "#3B82F6" : "#CBD5E1" - border.width: recordOverlay.pendingKeySeq !== "" ? 2 : 1 - - Behavior on border.color { ColorAnimation { duration: 120 } } - - Text { - anchors.centerIn: parent - text: recordOverlay.pendingKeySeq !== "" - ? recordOverlay.pendingKeySeq - : qsTr("请按下新的快捷键组合...") - font.pixelSize: 15 - font.family: "Monospace" - color: recordOverlay.pendingKeySeq !== "" ? "#1E293B" : "#94A3B8" - } - } - - // 冲突提示 - Text { - id: conflictHint - Layout.fillWidth: true - font.pixelSize: 12 - color: "#EF4444" - visible: false - wrapMode: Text.WordWrap - } - - // 按钮行 - RowLayout { - Layout.fillWidth: true - spacing: 8 - - // 取消 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: cancelArea.containsMouse ? "#F1F5F9" : "#F8FAFC" - border.color: "#E2E8F0" - Behavior on color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: qsTr("取消") - font.pixelSize: 13 - color: "#64748B" - } - MouseArea { - id: cancelArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - recordOverlay.visible = false - recordOverlay.pendingKeySeq = "" - conflictHint.visible = false - } - } - } - - // 确认 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 36 - radius: 6 - color: { - if (recordOverlay.pendingKeySeq === "") return "#93C5FD" - return confirmArea.containsMouse ? "#2563EB" : "#3B82F6" - } - Behavior on color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: qsTr("确认") - font.pixelSize: 13 - color: "#FFFFFF" - } - MouseArea { - id: confirmArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: recordOverlay.pendingKeySeq !== "" - onClicked: { - var seq = recordOverlay.pendingKeySeq - var aid = recordOverlay.recordingActionId - - // 检查冲突 - var conflictId = O_GlobalShortcut.checkConflict(seq) - if (conflictId !== 0 && conflictId !== aid) { - var labels = ["", qsTr("截图"), qsTr("截图并保存"), qsTr("贴图到桌面"), qsTr("显示/隐藏窗口")] - conflictHint.text = "「" + seq + "」已被「" + labels[conflictId] + "」使用,请换一个" - conflictHint.visible = true - return - } - - // 应用 - if (O_GlobalShortcut.updateShortcut(aid, seq)) { - recordOverlay.visible = false - recordOverlay.pendingKeySeq = "" - conflictHint.visible = false - } else { - conflictHint.text = "快捷键「" + seq + "」注册失败,请换一个组合" - conflictHint.visible = true - } - } - } - } - } - } - } - } - - // ── 主内容区 ────────────────────────────────────────── - ScrollView { - anchors.fill: parent - contentWidth: availableWidth - clip: true - - ColumnLayout { - width: parent.width - spacing: 8 - - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 24 - anchors.topMargin: 24 - - Item { Layout.preferredHeight: 4 } - - // ── 标题 ──────────────────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("全局快捷键") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - // ── 快捷键列表卡片 ─────────────────────────── - Rectangle { - Layout.fillWidth: true - implicitHeight: hotkeyCol.implicitHeight + 32 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - ColumnLayout { - id: hotkeyCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 16 - spacing: 0 - - Repeater { - // 动态从后端读取当前快捷键 - model: [ - { label: qsTr("截图"), actionId: 1, keySeq: O_GlobalShortcut.shortcutScreenshot }, - { label: qsTr("截图并保存"), actionId: 2, keySeq: O_GlobalShortcut.shortcutScreenshotSave }, - { label: qsTr("贴图到桌面"), actionId: 3, keySeq: O_GlobalShortcut.shortcutSticky }, - { label: qsTr("显示/隐藏窗口"), actionId: 4, keySeq: O_GlobalShortcut.shortcutToggle } - ] - - delegate: ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 12 - Layout.bottomMargin: 12 - - // 功能名称 - Text { - Layout.preferredWidth: 120 - text: modelData.label - font.pixelSize: 13 - color: "#374151" - Layout.alignment: Qt.AlignVCenter - } - - // 快捷键徽章 - RowLayout { - spacing: 4 - Layout.alignment: Qt.AlignVCenter - - Repeater { - model: modelData.keySeq.split("+") - delegate: Rectangle { - implicitWidth: kbLabel.implicitWidth + 16 - implicitHeight: 28 - radius: 5 - color: "#F1F5F9" - border.color: "#CBD5E1" - border.width: 1 - Text { - id: kbLabel - anchors.centerIn: parent - text: modelData - font.pixelSize: 12 - font.family: "Monospace" - color: "#374151" - } - } - } - } - - Item { Layout.fillWidth: true } - - // 编辑按钮 - Rectangle { - implicitWidth: 60 - implicitHeight: 28 - radius: 5 - color: editArea.containsMouse ? "#EFF6FF" : "#F8FAFC" - border.color: editArea.containsMouse ? "#3B82F6" : "#E2E8F0" - border.width: 1 - Behavior on color { ColorAnimation { duration: 100 } } - Behavior on border.color{ ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: qsTr("编辑") - font.pixelSize: 12 - color: editArea.containsMouse ? "#3B82F6" : "#64748B" - Behavior on color { ColorAnimation { duration: 100 } } - } - - MouseArea { - id: editArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - recordOverlay.recordingActionId = modelData.actionId - recordOverlay.recordingLabel = modelData.label - recordOverlay.pendingKeySeq = "" - conflictHint.visible = false - recordOverlay.visible = true - recordOverlay.forceActiveFocus() - } - } - } - } - - // 分割线(最后一行不显示) - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#F1F5F9" - visible: index < 3 - } - } - } - } - } - - Item { Layout.preferredHeight: 8 } - - // ── 说明 ──────────────────────────────────── - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - Rectangle { - width: 3; height: 15; radius: 2 - color: "#3B82F6" - Layout.alignment: Qt.AlignVCenter - } - Text { - text: qsTr("说明") - font.pixelSize: 13 - font.bold: true - color: "#1E293B" - } - } - - Rectangle { - Layout.fillWidth: true - implicitHeight: noteText.implicitHeight + 24 - color: "#FFFFFF" - radius: 10 - border.color: "#E2E8F0" - border.width: 1 - - Text { - id: noteText - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 16 - text: qsTr("快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。\n若与系统或其他应用快捷键冲突,请修改为其他组合。") - font.pixelSize: 13 - color: "#64748B" - wrapMode: Text.WordWrap - lineHeight: 1.6 - } - } - - Item { Layout.fillHeight: true } - } - } -} diff --git a/qml/Setting/TZRightPanel.qml b/qml/Setting/TZRightPanel.qml deleted file mode 100644 index ca8681e..0000000 --- a/qml/Setting/TZRightPanel.qml +++ /dev/null @@ -1,166 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -Rectangle { - signal signalSetting() - signal signalHotkeySettings() - - property string selectName: "setting" - color: "#F1F5F9" - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - // 顶部 Logo / 标题区 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 72 - color: "transparent" - - ColumnLayout { - anchors.centerIn: parent - spacing: 4 - - Rectangle { - width: 36 - height: 36 - radius: 10 - color: "#3B82F6" - Layout.alignment: Qt.AlignHCenter - - Text { - anchors.centerIn: parent - text: "T" - font.pixelSize: 20 - font.bold: true - color: "#FFFFFF" - } - } - - Text { - Layout.alignment: Qt.AlignHCenter - text: "TZshot" - font.pixelSize: 12 - color: "#64748B" - } - } - } - - // 分割线 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - color: "#E2E8F0" - } - - Item { Layout.preferredHeight: 12 } - - // ── 通用设置 菜单项 ────────────────────── - Item { - Layout.fillWidth: true - Layout.preferredHeight: 44 - Layout.leftMargin: 10 - Layout.rightMargin: 10 - - Rectangle { - anchors.fill: parent - radius: 8 - color: selectName === "setting" ? "#FFFFFF" : "transparent" - border.color: selectName === "setting" ? "#E2E8F0" : "transparent" - border.width: 1 - } - - Rectangle { - width: 3 - height: 20 - radius: 2 - anchors.left: parent.left - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - color: selectName === "setting" ? "#3B82F6" : "transparent" - } - - Text { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 18 - text: qsTr("通用设置") - font.pixelSize: 13 - font.bold: selectName === "setting" - color: selectName === "setting" ? "#3B82F6" : "#64748B" - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - selectName = "setting" - signalSetting() - } - } - } - - Item { Layout.preferredHeight: 4 } - - // ── 快捷键设置 菜单项 ──────────────────── - Item { - Layout.fillWidth: true - Layout.preferredHeight: 44 - Layout.leftMargin: 10 - Layout.rightMargin: 10 - - Rectangle { - anchors.fill: parent - radius: 8 - color: selectName === "shortcutKey" ? "#FFFFFF" : "transparent" - border.color: selectName === "shortcutKey" ? "#E2E8F0" : "transparent" - border.width: 1 - } - - Rectangle { - width: 3 - height: 20 - radius: 2 - anchors.left: parent.left - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - color: selectName === "shortcutKey" ? "#3B82F6" : "transparent" - } - - Text { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 18 - text: qsTr("快捷键设置") - font.pixelSize: 13 - font.bold: selectName === "shortcutKey" - color: selectName === "shortcutKey" ? "#3B82F6" : "#64748B" - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - selectName = "shortcutKey" - signalHotkeySettings() - } - } - } - - // 底部弹性空间 - Item { Layout.fillHeight: true } - - // 版本号 - Text { - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 12 - text: "v1.0.0" - font.pixelSize: 11 - color: "#CBD5E1" - } - } -} - diff --git a/qml/Setting/TZSettingWin.qml b/qml/Setting/TZSettingWin.qml deleted file mode 100644 index 59686a3..0000000 --- a/qml/Setting/TZSettingWin.qml +++ /dev/null @@ -1,77 +0,0 @@ -import QtQuick -import QtQuick.Window -import QtQuick.Layouts -import QtQuick.Controls.Basic -import QtQuick.Dialogs -import TZControls 1.0 - -Window { - width: 750 - height: 520 - title: qsTr("设置") - visible: true - color: "#F8FAFC" - property string currentPage: "general" - - Component.onCompleted: centerWindow() - onScreenChanged: centerWindow() - - function centerWindow() { - if (Screen) { - const screen = this.screen || Screen.primaryScreen - x = (screen.width - width) / 2 - y = (screen.height - height) / 2 - } - } - - FolderDialog { - id: saveDialog - title: qsTr("选择保存位置") - currentFolder: O_ImageSaver.savePath - onAccepted: { - O_ImageSaver.setSavePath(saveDialog.selectedFolder) - } - } - - RowLayout { - anchors.fill: parent - spacing: 0 - - // 左侧导航栏 - TZRightPanel { - Layout.preferredWidth: 160 - Layout.fillHeight: true - onSignalSetting: { - currentPage = "general" - } - onSignalHotkeySettings: { - currentPage = "hotkey" - } - } - - // 分割线 - Rectangle { - Layout.preferredWidth: 1 - Layout.fillHeight: true - color: "#E2E8F0" - } - - // 右侧内容区 - StackLayout { - Layout.fillHeight: true - Layout.fillWidth: true - currentIndex: currentPage === "general" ? 0 : 1 - - TZGeneralSettings { - Layout.fillHeight: true - Layout.fillWidth: true - } - - TZHotkeySettings { - Layout.fillHeight: true - Layout.fillWidth: true - } - } - } -} - diff --git a/qml/TZControls/TZComboBox.qml b/qml/TZControls/TZComboBox.qml deleted file mode 100644 index a9c88de..0000000 --- a/qml/TZControls/TZComboBox.qml +++ /dev/null @@ -1,128 +0,0 @@ -import QtQuick -import QtQuick.Controls.Basic - -ComboBox { - id: control - - property color fillColor: "#F8FAFC" - property color borderColor: "#E2E8F0" - property color activeBorderColor: "#3B82F6" - property color hoverColor: "#F1F5F9" - property color textColor: "#1E293B" - property color dimTextColor: "#64748B" - - width: 120 - height: 36 - font.pixelSize: 13 - font.family: "Microsoft YaHei" - - // 当前选中文字 - contentItem: Text { - leftPadding: 12 - rightPadding: 28 - text: control.displayText - font: control.font - color: control.enabled ? textColor : dimTextColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - // 背景 - background: Rectangle { - implicitWidth: control.width - implicitHeight: control.height - color: control.hovered ? hoverColor : fillColor - border.color: control.activeFocus ? activeBorderColor : borderColor - border.width: control.activeFocus ? 2 : 1 - radius: 6 - - Behavior on border.color { ColorAnimation { duration: 150 } } - Behavior on color { ColorAnimation { duration: 120 } } - } - - // 下拉箭头 - indicator: Canvas { - id: canvas - x: control.width - width - 10 - y: (control.height - height) / 2 - width: 10 - height: 6 - contextType: "2d" - - onPaint: { - context.reset() - context.lineWidth = 1.5 - context.strokeStyle = dimTextColor - context.lineCap = "round" - context.lineJoin = "round" - context.moveTo(0, 0) - context.lineTo(width / 2, height) - context.lineTo(width, 0) - context.stroke() - } - } - - // 下拉列表每项 - delegate: ItemDelegate { - width: control.width - height: 36 - - contentItem: Text { - leftPadding: 12 - text: modelData - font: control.font - color: highlighted ? "#3B82F6" : textColor - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: highlighted ? "#EFF6FF" : fillColor - } - } - - // 弹出框 - popup: Popup { - y: control.height + 4 - width: control.width - implicitHeight: listview.contentHeight - padding: 4 - - contentItem: ListView { - id: listview - clip: true - model: control.popup.visible ? control.delegateModel : null - currentIndex: control.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} - } - - background: Rectangle { - color: "#FFFFFF" - border.color: "#E2E8F0" - border.width: 1 - radius: 8 - - // 顶部细线装饰 - Rectangle { - width: parent.width - 2 - height: 1 - anchors.top: parent.top - anchors.topMargin: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: "#3B82F6" - opacity: 0.4 - } - } - - enter: Transition { - NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; duration: 120 } - NumberAnimation { property: "scale"; from: 0.95; to: 1.0; duration: 120 } - } - exit: Transition { - NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; duration: 80 } - } - } -} - - diff --git a/qml/TZControls/qmldir b/qml/TZControls/qmldir deleted file mode 100644 index 7a523c1..0000000 --- a/qml/TZControls/qmldir +++ /dev/null @@ -1,2 +0,0 @@ -module TZControls -TZComboBox 1.0 TZComboBox.qml diff --git a/qml/Windows/TZChat.qml b/qml/Windows/TZChat.qml deleted file mode 100644 index 52ee5ad..0000000 --- a/qml/Windows/TZChat.qml +++ /dev/null @@ -1,311 +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, targetScreen) { - if (targetScreen) - win.screen = targetScreen - 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 - - onPressed: function(mouse) { - mouse.accepted = true - win.startSystemMove() - } - 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 - Layout.minimumHeight: 140 - Layout.preferredHeight: 180 - 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() - } - } - } - } - } - } - } - - 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/TZOcrResult.qml b/qml/Windows/TZOcrResult.qml deleted file mode 100644 index 624766b..0000000 --- a/qml/Windows/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/Windows/TZStickyWindow.qml b/qml/Windows/TZStickyWindow.qml deleted file mode 100644 index 0f215d1..0000000 --- a/qml/Windows/TZStickyWindow.qml +++ /dev/null @@ -1,317 +0,0 @@ -import QtQuick -import QtQuick.Window -import Qt5Compat.GraphicalEffects -import Qt.labs.platform 1.0 as Platform -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 cleanedUp: false - property size sourceImageSize: Qt.size(1, 1) - 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 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)) - - 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() - } - - 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 applyNativeSize() { - O_StickyViewModel.resizeStickyWindow(stickyWin, windowWidthValue, windowHeightValue) - } - - function setZoomFactor(value) { - zoomFactor = Math.max(0.3, Math.min(4.0, value)) - updateWindowSize() - } - - function resetDisplaySize() { - setZoomFactor(1.0) - } - - 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 - 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: { - updateImageSize() - syncPaintBoardBackground() - syncInitialPosition() - applyNativeSize() - stickyWin.raise() - stickyWin.requestActivate() - } - - onImageUrlChanged: { - updateImageSize() - syncPaintBoardBackground() - } - - function onClosing(close) { - close.accepted = true - cleanup() - Qt.callLater(function() { stickyWin.destroy() }) - } - - 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 - } - - PaintBoard { - id: paintBoard - x: stickyWin.contentX - y: stickyWin.contentY - width: stickyWin.contentWidth - height: stickyWin.contentHeight - z: 5 - visible: true - enabled: toolBar.activeTool !== "" - shapeType: toolBar.shapeType - penColor: markToolInfo.currentColor - penSize: markToolInfo.currentSize - } - - TZTool { - id: toolBar - visible: stickyWin.toolbarVisible - x: Math.max(0, Math.floor((stickyWin.width - width) / 2)) - y: stickyWin.contentY + stickyWin.contentHeight + stickyWin.toolbarGap - showScreenshotButton: false - showCancelButton: false - showSaveButton: false - showStickyButton: false - showGifRecordButton: false - showLongScreenshotButton: false - showHighlightButton: false - showTextButton: false - showNumberButton: false - showRectButton: false - showCircleButton: false - showArrowButton: false - showMosaicButton: false - showPencilButton: true - showUndoButton: true - - onSignalOcr: {} - onSignalUndo: paintBoard.undo() - } - - 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 - } - - 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 - syncPaintBoardBackground() - Qt.callLater(function() { O_StickyViewModel.releaseImage(old) }) - } - - function onSignalRequestFailed(errorMsg) { - console.warn("[TZStickyWindow] AI request failed: " + errorMsg) - } - } - - Platform.Menu { - id: contextMenu - - Platform.MenuItem { - text: qsTr("复制") - onTriggered: O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl) - } - - Platform.MenuItem { - text: qsTr("AI 编辑") - onTriggered: stickyWin.openChatPanel() - } - - Platform.MenuItem { - text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏") - onTriggered: { - stickyWin.toolbarVisible = !stickyWin.toolbarVisible - updateWindowSize() - } - } - - 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 { - 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) { - pressOffset = Qt.point(mouse.x, mouse.y) - } - - onPositionChanged: function(mouse) { - 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) - stickyWin.setZoomFactor(stickyWin.zoomFactor + 0.1) - else if (wheel.angleDelta.y < 0) - 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/qml/main.qml b/qml/main.qml deleted file mode 100644 index d7225f6..0000000 --- a/qml/main.qml +++ /dev/null @@ -1,339 +0,0 @@ - -import QtQuick -import QtQuick.Window -import QtQuick.Shapes -import QtQuick.Layouts -import com.Tz.tray 1.0 -import "Capture" - -TZWindow { - id: root - visibility: Window.Hidden - title: qsTr("TZshot") - color: "transparent" - flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint - - property var settingWinObj: null - property var aboutWinObj: null - property var longCaptureBarObj: null - property var longCaptureBarComponent: null - property var longCapturePreviewObj: null - property var longCapturePreviewComponent: null - property var longCaptureFrameObj: null - property var longCaptureFrameComponent: null - property rect pendingLongCaptureRect: Qt.rect(0, 0, 0, 0) - - function _prepareSessionState() { - captureUiReady = false - root.opacity = 0.0 - screenshotArea = Qt.rect(0, 0, 0, 0) - initialScreenshotArea = Qt.rect(0, 0, 0, 0) - highlightRect = Qt.rect(0, 0, 0, 0) - screenShotUrl = "" - - paintBoard.reset() - tool.visible = false - - tool.activeTool = "" - tool.markToolInfoVisible = false - - gifRecordMode = false - if (gifBarObj) - gifBarObj.visible = false - - if (typeof resizeHandleGroup !== "undefined") { - resizeHandleGroup.area = Qt.rect(0, 0, 0, 0) - resizeHandleGroup.initialArea = Qt.rect(0, 0, 0, 0) - resizeHandleGroup.visible = false - } - if (typeof gifResizeHandleGroup !== "undefined") { - gifResizeHandleGroup.area = Qt.rect(0, 0, 0, 0) - gifResizeHandleGroup.initialArea = Qt.rect(0, 0, 0, 0) - gifResizeHandleGroup.visible = false - } - - if (typeof hideTextEditor === "function") - hideTextEditor() - } - - function startScreenshot() { - root.visibility = Window.Hidden - _prepareSessionState() - - Qt.callLater(function() { - O_ScreenCapture.captureDesktop() - var virtualRect = O_ScreenCapture.virtualGeometry() - if (virtualRect.width <= 0 || virtualRect.height <= 0) - return - - root.screenVirtualX = virtualRect.x - root.screenVirtualY = virtualRect.y - - O_ScreenCapture.grabToPaintBoard( - root.paintBoard, - Qt.rect(root.screenVirtualX, root.screenVirtualY, root.width, root.height) - ) - - root.visibility = Window.Windowed - root.raise() - root.requestActivate() - Qt.callLater(function() { - root.captureUiReady = true - Qt.callLater(function() { - root.opacity = 1.0 - }) - }) - }) - } - - function showLongCaptureBar() { - if (longCaptureBarObj !== null) { - longCaptureBarObj.visible = true - longCaptureBarObj.raise() - return - } - if (longCaptureBarComponent === null) - longCaptureBarComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCaptureBar.qml") - if (longCaptureBarComponent.status === Component.Ready) { - longCaptureBarObj = longCaptureBarComponent.createObject(null) - if (longCaptureBarObj) { - longCaptureBarObj.x = Math.round((Screen.width - longCaptureBarObj.width) / 2) - longCaptureBarObj.y = 26 - longCaptureBarObj.visible = true - longCaptureBarObj.raise() - longCaptureBarObj.startRequested.connect(function() { - if (pendingLongCaptureRect.width === 0 || pendingLongCaptureRect.height === 0) - return - longCaptureBarObj.running = true - longCaptureBarObj.message = qsTr("滚动检测中,滑动内容会自动拼接") - O_ScrollCaptureVM.start(pendingLongCaptureRect) - }) - longCaptureBarObj.stopRequested.connect(function() { - if (longCaptureBarObj.running) { - O_ScrollCaptureVM.stop() - } else { - hideLongCaptureBar() - root.resetArea() - } - }) - } - } else if (longCaptureBarComponent.status === Component.Error) { - console.error("LongCaptureBar load failed:", longCaptureBarComponent.errorString()) - } - } - - function showLongCapturePreview() { - if (longCapturePreviewObj !== null) { - longCapturePreviewObj.visible = true - longCapturePreviewObj.raise() - return - } - if (longCapturePreviewComponent === null) - longCapturePreviewComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCapturePreview.qml") - if (longCapturePreviewComponent.status === Component.Ready) { - longCapturePreviewObj = longCapturePreviewComponent.createObject(null) - if (longCapturePreviewObj) { - longCapturePreviewObj.x = Math.max(0, Screen.width - longCapturePreviewObj.width - 26) - longCapturePreviewObj.y = Math.max(20, Math.round((Screen.height - longCapturePreviewObj.height) / 2)) - longCapturePreviewObj.visible = true - longCapturePreviewObj.raise() - } - } else if (longCapturePreviewComponent.status === Component.Error) { - console.error("LongCapturePreview load failed:", longCapturePreviewComponent.errorString()) - } - } - - function showLongCaptureFrame(captureRect) { - if (longCaptureFrameComponent === null) - longCaptureFrameComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCaptureFrame.qml") - if (longCaptureFrameObj === null && longCaptureFrameComponent.status === Component.Ready) { - longCaptureFrameObj = longCaptureFrameComponent.createObject(null) - } else if (longCaptureFrameComponent.status === Component.Error) { - console.error("LongCaptureFrame load failed:", longCaptureFrameComponent.errorString()) - } - if (longCaptureFrameObj) { - longCaptureFrameObj.captureRect = captureRect - longCaptureFrameObj.visible = true - longCaptureFrameObj.raise() - } - } - - function hideLongCaptureBar() { - if (longCaptureBarObj !== null) { - longCaptureBarObj.close() - longCaptureBarObj.destroy(100) - longCaptureBarObj = null - } - if (longCapturePreviewObj !== null) { - longCapturePreviewObj.close() - longCapturePreviewObj.destroy(100) - longCapturePreviewObj = null - } - if (longCaptureFrameObj !== null) { - longCaptureFrameObj.close() - longCaptureFrameObj.destroy(100) - longCaptureFrameObj = null - } - pendingLongCaptureRect = Qt.rect(0, 0, 0, 0) - } - - function beginLongCapture(captureRect) { - pendingLongCaptureRect = captureRect - root.visibility = Window.Hidden - showLongCaptureBar() - showLongCapturePreview() - showLongCaptureFrame(captureRect) - if (longCaptureBarObj) { - longCaptureBarObj.running = false - longCaptureBarObj.frameCount = 0 - longCaptureBarObj.message = qsTr("点击开始后,滑动内容自动拼接") - } - if (longCapturePreviewObj) { - longCapturePreviewObj.previewUrl = "" - longCapturePreviewObj.frameCount = 0 - longCapturePreviewObj.statusText = qsTr("等待开始") - } - } - - function resetArea() { - _prepareSessionState() - O_ScreenCapture.releaseDesktopSnapshot() - root.visibility = Window.Hidden - } - - function showAboutWin() { - if (aboutWinObj !== null) { - aboutWinObj.raise() - aboutWinObj.requestActivate() - return - } - var component = Qt.createComponent("qrc:/qml/Setting/TZAboutWin.qml") - if (component.status === Component.Ready) - aboutWinObj = component.createObject(null) - else if (component.status === Component.Error) - console.error("About window load failed:", component.errorString()) - - if (aboutWinObj !== null) { - aboutWinObj.visible = true - aboutWinObj.raise() - aboutWinObj.requestActivate() - aboutWinObj.closing.connect(function() { - aboutWinObj.destroy(100) - aboutWinObj = null - }) - } - } - - function showSettingWin() { - O_WidgetWindows.showSettings() - } - - Connections { - target: O_StickyViewModel - function onStickyReady(imageUrl, imgRect) { - root.createStickyWindow(imageUrl, imgRect) - } - } - - Connections { - target: O_OcrVM - function onRecognizeFailed(errorMessage) { - trayHelper.showMessage(qsTr("OCR 识别失败"), errorMessage) - } - } - - Connections { - target: O_ScrollCaptureVM - function onCaptureStarted() { - if (longCaptureBarObj) { - longCaptureBarObj.running = true - longCaptureBarObj.message = qsTr("滚动检测中,滑动内容会自动拼接") - } - trayHelper.showMessage(qsTr("长截图开始"), qsTr("请直接滚动目标内容,系统会在滚动变化时自动拼接")) - } - function onCaptureSucceeded(savedPath) { - if (longCaptureBarObj) { - longCaptureBarObj.running = false - longCaptureBarObj.message = qsTr("长截图完成") - } - root.hideLongCaptureBar() - root.resetArea() - trayHelper.showMessage(qsTr("长截图完成"), qsTr("已复制到剪贴板并保存到:") + savedPath) - } - function onCaptureFailed(errorMessage) { - if (longCaptureBarObj) { - longCaptureBarObj.running = false - longCaptureBarObj.message = qsTr("长截图失败") - } - root.hideLongCaptureBar() - root.resetArea() - trayHelper.showMessage(qsTr("长截图失败"), errorMessage) - } - function onCapturedFramesChanged() { - if (longCaptureBarObj) - longCaptureBarObj.frameCount = O_ScrollCaptureVM.capturedFrames - if (longCapturePreviewObj) - longCapturePreviewObj.frameCount = O_ScrollCaptureVM.capturedFrames - } - function onStatusTextChanged() { - if (longCaptureBarObj) - longCaptureBarObj.message = O_ScrollCaptureVM.statusText - if (longCapturePreviewObj) - longCapturePreviewObj.statusText = O_ScrollCaptureVM.statusText - } - function onPreviewImageUrlChanged() { - if (longCapturePreviewObj) - longCapturePreviewObj.previewUrl = O_ScrollCaptureVM.previewImageUrl - } - } - - Connections { - target: O_InstanceBridge - function onActivationRequested() { - if (root.visible) { - root.raise() - root.requestActivate() - return - } - startScreenshot() - } - } - - TrayIconHelper { - id: trayHelper - function onTrayClicked(clickType) { - if (clickType === 0 || clickType === 2) - startScreenshot() - } - onScreenshotTriggered: startScreenshot() - onShowSettingTriggered: { showSettingWin() } - onShowAboutTriggered: { showAboutWin() } - onExitTriggered: Qt.quit() - } - // MouseArea - // { - // anchors.fill: parent; - // MouseArea { - // anchors.fill: parent - // onClicked: { - // console.log("x->", mouseX) - // console.log("y-->", mouseY) - // } - // onDoubleClicked: Qt.quit() - // } - // } - Component.onCompleted: { - trayHelper.setIcon(":/resource/img/tray_icon.svg") - trayHelper.show() - - O_GlobalShortcut.screenshotActivated.connect(function() { startScreenshot() }) - O_GlobalShortcut.screenshotSaveActivated.connect(function() { startScreenshot() }) - O_GlobalShortcut.stickyActivated.connect(function() { startScreenshot() }) - O_GlobalShortcut.toggleActivated.connect(function() { - if (root.visible) - resetArea() - else - startScreenshot() - }) - } -} diff --git a/resource/img/lc_copy.svg b/resource/img/lc_copy.svg new file mode 100644 index 0000000..471a5d5 --- /dev/null +++ b/resource/img/lc_copy.svg @@ -0,0 +1,5 @@ + + + + diff --git a/resource/qss/capture_overlay_widget.qss b/resource/qss/capture_overlay_widget.qss new file mode 100644 index 0000000..1cc5155 --- /dev/null +++ b/resource/qss/capture_overlay_widget.qss @@ -0,0 +1,186 @@ +#captureOverlayToolbar { + background: #FFFFFF; + border: 1px solid rgba(96, 165, 250, 170); + border-radius: 10px; +} + +#captureOverlayGifBar { + background: #FFFFFF; + border: 1px solid #E2E8F0; + border-radius: 10px; +} + +#captureOverlayTipBubble { + background: rgba(15, 23, 42, 235); + border: none; + border-radius: 6px; +} + +#captureOverlayTipLabel { + color: #FFFFFF; + font-size: 11px; +} + +#captureOverlayGifProgress { + background: #E2E8F0; + border: none; + border-radius: 3px; +} + +#captureOverlayGifProgress::chunk { + background: #3B82F6; + border-radius: 3px; +} + +QToolTip { + background: #0F172A; + color: #FFFFFF; + border: none; + border-radius: 6px; + padding: 6px 10px; +} + +#captureOverlayToolbar QToolButton { + background: transparent; + border: 1px solid transparent; + border-radius: 7px; + padding: 0px; + margin: 0px; + color: #0F172A; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; +} + +#captureOverlayGifBar QToolButton { + background: transparent; + border: 1px solid transparent; + border-radius: 7px; + padding: 0px; + margin: 0px; + color: #0F172A; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; +} + +#captureOverlayGifBar QToolButton:hover { + background: #F8FAFC; +} + +#captureOverlayGifBar QToolButton:pressed { + background: #EFF6FF; + border-color: #3B82F6; +} + +#captureOverlayToolbar QToolButton:hover { + background: #F1F5F9; +} + +#captureOverlayToolbar QToolButton:pressed { + background: #EFF6FF; + border-color: #3B82F6; +} + +#captureOverlayToolbar QToolButton:checked { + background: #EFF6FF; + border-color: #3B82F6; +} + +#captureOverlayGifStopButton:hover { + background: #FEF3C7; +} + +#captureOverlayGifCancelButton:hover { + background: #FEE2E2; +} + +#captureOverlayToolbar QToolTip { + background: #0F172A; + color: #FFFFFF; + border: none; + border-radius: 6px; + padding: 6px 10px; +} + +#captureOverlayToolbarDivider { + background: #E2E8F0; + min-width: 1px; + max-width: 1px; +} + +#captureOverlayGifStatus { + color: #1E293B; + font-size: 13px; + font-family: "Consolas"; + font-weight: 600; +} + +#captureOverlayGifFrames { + color: #94A3B8; + font-size: 11px; +} + +#captureOverlayGifDot { + background: #EF4444; + border-radius: 4px; +} + +#captureOverlayToolOptions { + background-color: #FFFFFF; + border: 1px solid #E2E8F0; + border-radius: 10px; +} + +#captureOverlayToolOptions QToolButton { + background-color: transparent; + border: 1px solid #E2E8F0; + border-radius: 6px; + color: #334155; +} + +#captureOverlayToolOptions QToolButton:checked { + background-color: #EFF6FF; + border-color: #3B82F6; + color: #3B82F6; +} + +#captureOverlayToolOptions QToolButton:hover { + background-color: #F1F5F9; +} + +#captureOverlayToolOptions QLineEdit, +#captureOverlayToolOptions QSpinBox { + background-color: #F8FAFC; + border: 1px solid #E2E8F0; + border-radius: 6px; + padding: 4px 6px; + color: #0F172A; +} + +#captureOverlayToolOptions QCheckBox { + color: #334155; +} + +#captureOverlayInlineTextEditor { + background-color: #FFFFFF; + border: 1px solid #3B82F6; + border-radius: 6px; + padding: 4px 6px; + color: #0F172A; +} + +#captureOverlayInlineTextPanel { + background-color: #FFFFFF; + border: 1px solid #CBD5E1; + border-radius: 10px; +} + +#captureOverlayNumberLabel { + background-color: #F8FAFC; + border: 1px solid #E2E8F0; + border-radius: 6px; + color: #0F172A; +} diff --git a/resource/qss/long_capture_controller.qss b/resource/qss/long_capture_controller.qss new file mode 100644 index 0000000..75294c2 --- /dev/null +++ b/resource/qss/long_capture_controller.qss @@ -0,0 +1,76 @@ +#longCaptureBar { + background: #111827; + border: 1px solid #1F2937; + border-radius: 12px; +} + +#longCaptureBar QLabel { + color: #E5E7EB; +} + +#longCaptureBadge { + background: #F97316; + color: white; + border-radius: 13px; + font-weight: 700; +} + +#longCaptureFrameLabel { + color: #D1D5DB; +} + +#longCapturePrimaryBtn { + background: #2563EB; + color: white; + border: none; + border-radius: 6px; + padding: 0 12px; +} + +#longCapturePrimaryBtn:hover { + background: #1D4ED8; +} + +#longCaptureDangerBtn { + background: #EF4444; + color: white; + border: none; + border-radius: 6px; + padding: 0 12px; +} + +#longCaptureDangerBtn:hover { + background: #DC2626; +} + +#longCapturePreview { + background: #111827; + border: 1px solid #1F2937; + border-radius: 12px; +} + +#longCapturePreview QLabel { + color: #F9FAFB; +} + +#longCapturePreviewTitle { + font-weight: 700; +} + +#longCapturePreviewSub { + color: #9CA3AF; +} + +#longCapturePreviewStatus { + color: #CBD5E1; +} + +#longCapturePreviewImageBox { + background: #0B1220; + border: 1px solid #334155; + border-radius: 8px; +} + +#longCapturePreviewEmpty { + color: #64748B; +} diff --git a/src/app/tzshot_app.cpp b/src/app/tzshot_app.cpp new file mode 100644 index 0000000..6961708 --- /dev/null +++ b/src/app/tzshot_app.cpp @@ -0,0 +1,90 @@ +#include "app/tzshot_app.h" + +#include +#include + +#include "app/tzshot_services.h" +#include "app/tzshot_widget_runtime.h" +#include "instance_activation.h" + +TZShotApp::TZShotApp(QApplication &app, QObject *parent) + : QObject(parent) + , m_app(app) +{ +} + +bool TZShotApp::initialize() +{ + configureApplication(); + + m_instanceActivation = new InstanceActivation(QStringLiteral("TZshot.Instance"), this); + if (!m_instanceActivation->initialize()) { + return false; + } + + m_services = new TZShotServices(this); + m_runtime = new TZShotWidgetRuntime(*m_services, this); + setupConnections(); + return true; +} + +void TZShotApp::configureApplication() const +{ + QCoreApplication::setOrganizationName("TZshot"); + QCoreApplication::setOrganizationDomain("tzshot.local"); + QCoreApplication::setApplicationName("TZshot"); + m_app.setQuitOnLastWindowClosed(false); +} + +void TZShotApp::setupConnections() +{ + connect(&m_services->ocrViewModel(), &OcrViewModel::isRecognizingChanged, this, [this]() { + if (m_services->ocrViewModel().isRecognizing()) { + m_runtime->ocrResultWidget().showAndActivate(); + } + }); + connect(&m_services->ocrViewModel(), &OcrViewModel::resultReady, this, [this](const QString &) { + m_runtime->ocrResultWidget().showAndActivate(); + }); + connect(&m_services->ocrViewModel(), &OcrViewModel::recognizeFailed, this, [this](const QString &errorMessage) { + m_runtime->trayHelper().showMessage(tr("OCR 识别失败"), errorMessage); + }); + + connect(m_instanceActivation, &InstanceActivation::activationRequested, this, [this]() { + showCaptureOverlay(QStringLiteral("copy")); + }); + + connect(&m_runtime->trayHelper(), &TrayIconHelper::trayClicked, this, [this](int clickType) { + if (clickType == 0 || clickType == 2) { + showCaptureOverlay(QStringLiteral("copy")); + } + }); + connect(&m_runtime->trayHelper(), &TrayIconHelper::screenshotTriggered, this, [this]() { + showCaptureOverlay(QStringLiteral("copy")); + }); + connect(&m_runtime->trayHelper(), &TrayIconHelper::showSettingTriggered, this, [this]() { + m_runtime->widgetWindowBridge().showSettings(); + }); + connect(&m_runtime->trayHelper(), &TrayIconHelper::showAboutTriggered, this, [this]() { + m_runtime->aboutDialog().showAndActivate(); + }); + connect(&m_runtime->trayHelper(), &TrayIconHelper::exitTriggered, &m_app, &QApplication::quit); + + connect(&m_services->globalShortcut(), &GlobalShortcut::screenshotActivated, this, [this]() { + showCaptureOverlay(QStringLiteral("copy")); + }); + connect(&m_services->globalShortcut(), &GlobalShortcut::screenshotSaveActivated, this, [this]() { + showCaptureOverlay(QStringLiteral("save")); + }); + connect(&m_services->globalShortcut(), &GlobalShortcut::stickyActivated, this, [this]() { + showCaptureOverlay(QStringLiteral("sticky")); + }); + connect(&m_services->globalShortcut(), &GlobalShortcut::toggleActivated, this, [this]() { + m_runtime->widgetWindowBridge().toggleCaptureOverlay(QStringLiteral("copy")); + }); +} + +void TZShotApp::showCaptureOverlay(const QString &mode) +{ + m_runtime->widgetWindowBridge().showCaptureOverlay(mode); +} diff --git a/src/app/tzshot_app.h b/src/app/tzshot_app.h new file mode 100644 index 0000000..1a5f5d7 --- /dev/null +++ b/src/app/tzshot_app.h @@ -0,0 +1,33 @@ +#ifndef TZSHOT_APP_H +#define TZSHOT_APP_H + +#include + +class QApplication; +class QString; + +class InstanceActivation; +class TZShotServices; +class TZShotWidgetRuntime; + +class TZShotApp : public QObject +{ + Q_OBJECT + +public: + explicit TZShotApp(QApplication &app, QObject *parent = nullptr); + + bool initialize(); + +private: + void configureApplication() const; + void setupConnections(); + void showCaptureOverlay(const QString &mode); + + QApplication &m_app; + InstanceActivation *m_instanceActivation = nullptr; + TZShotServices *m_services = nullptr; + TZShotWidgetRuntime *m_runtime = nullptr; +}; + +#endif // TZSHOT_APP_H diff --git a/src/app/tzshot_services.cpp b/src/app/tzshot_services.cpp new file mode 100644 index 0000000..8b077a7 --- /dev/null +++ b/src/app/tzshot_services.cpp @@ -0,0 +1,19 @@ +#include "app/tzshot_services.h" + +TZShotServices::TZShotServices(QObject *parent) + : QObject(parent) + , m_desktopSnapshot() + , m_appSettings() + , m_stickyImageStore(this) + , m_screenshotViewModel(m_desktopSnapshot, m_stickyImageStore, this) + , m_stickyViewModel(m_stickyImageStore, this) + , m_aiViewModel(m_appSettings, m_stickyImageStore, this) + , m_storageViewModel(m_appSettings, this) + , m_languageManager(m_appSettings, this) + , m_gifRecordViewModel(m_appSettings, this) + , m_scrollCaptureViewModel(m_screenshotViewModel, m_appSettings, this) + , m_ocrViewModel(this) + , m_globalShortcut(this) +{ + m_stickyViewModel.setAiViewModel(&m_aiViewModel); +} diff --git a/src/app/tzshot_services.h b/src/app/tzshot_services.h new file mode 100644 index 0000000..f79312b --- /dev/null +++ b/src/app/tzshot_services.h @@ -0,0 +1,54 @@ +#ifndef TZSHOT_SERVICES_H +#define TZSHOT_SERVICES_H + +#include + +#include "language_manager.h" +#include "model/app_settings.h" +#include "model/desktop_snapshot.h" +#include "shortcut_key/globalshortcut.h" +#include "sticky_image_store.h" +#include "viewmodel/ai_view_model.h" +#include "viewmodel/gif_record_view_model.h" +#include "viewmodel/ocr_view_model.h" +#include "viewmodel/screenshot_view_model.h" +#include "viewmodel/scroll_capture_view_model.h" +#include "viewmodel/sticky_view_model.h" +#include "viewmodel/storage_view_model.h" + +class TZShotServices : public QObject +{ + Q_OBJECT + +public: + explicit TZShotServices(QObject *parent = nullptr); + + DesktopSnapshot& desktopSnapshot() { return m_desktopSnapshot; } + AppSettings& appSettings() { return m_appSettings; } + StickyImageStore& stickyImageStore() { return m_stickyImageStore; } + ScreenshotViewModel& screenshotViewModel() { return m_screenshotViewModel; } + StickyViewModel& stickyViewModel() { return m_stickyViewModel; } + AIViewModel& aiViewModel() { return m_aiViewModel; } + StorageViewModel& storageViewModel() { return m_storageViewModel; } + LanguageManager& languageManager() { return m_languageManager; } + GifRecordViewModel& gifRecordViewModel() { return m_gifRecordViewModel; } + ScrollCaptureViewModel& scrollCaptureViewModel() { return m_scrollCaptureViewModel; } + OcrViewModel& ocrViewModel() { return m_ocrViewModel; } + GlobalShortcut& globalShortcut() { return m_globalShortcut; } + +private: + DesktopSnapshot m_desktopSnapshot; + AppSettings m_appSettings; + StickyImageStore m_stickyImageStore; + ScreenshotViewModel m_screenshotViewModel; + StickyViewModel m_stickyViewModel; + AIViewModel m_aiViewModel; + StorageViewModel m_storageViewModel; + LanguageManager m_languageManager; + GifRecordViewModel m_gifRecordViewModel; + ScrollCaptureViewModel m_scrollCaptureViewModel; + OcrViewModel m_ocrViewModel; + GlobalShortcut m_globalShortcut; +}; + +#endif // TZSHOT_SERVICES_H diff --git a/src/app/tzshot_widget_runtime.cpp b/src/app/tzshot_widget_runtime.cpp new file mode 100644 index 0000000..c747c70 --- /dev/null +++ b/src/app/tzshot_widget_runtime.cpp @@ -0,0 +1,30 @@ +#include "app/tzshot_widget_runtime.h" + +#include "app/tzshot_services.h" + +TZShotWidgetRuntime::TZShotWidgetRuntime(TZShotServices &services, QObject *parent) + : QObject(parent) + , m_ocrResultWidget(&services.ocrViewModel()) + , m_captureOverlayWidget(&services.screenshotViewModel(), + &services.stickyViewModel(), + &services.storageViewModel(), + &services.ocrViewModel(), + &services.gifRecordViewModel()) + , m_settingsDialog(&services.aiViewModel(), + &services.storageViewModel(), + &services.languageManager(), + &services.gifRecordViewModel(), + &services.ocrViewModel(), + &services.globalShortcut()) + , m_aboutDialog() + , m_longCaptureController(&services.scrollCaptureViewModel()) + , m_widgetWindowBridge(&m_settingsDialog, + &m_captureOverlayWidget, + &m_longCaptureController, + this) + , m_trayHelper(this) +{ + m_captureOverlayWidget.setWidgetWindowBridge(&m_widgetWindowBridge); + m_trayHelper.setIcon(QStringLiteral(":/resource/img/tray_icon.svg")); + m_trayHelper.show(); +} diff --git a/src/app/tzshot_widget_runtime.h b/src/app/tzshot_widget_runtime.h new file mode 100644 index 0000000..8723ded --- /dev/null +++ b/src/app/tzshot_widget_runtime.h @@ -0,0 +1,41 @@ +#ifndef TZSHOT_WIDGET_RUNTIME_H +#define TZSHOT_WIDGET_RUNTIME_H + +#include + +#include "tray_icon_helper.h" +#include "widgets/about_dialog.h" +#include "widgets/capture_overlay_widget.h" +#include "widgets/long_capture_controller.h" +#include "widgets/ocr_result_widget.h" +#include "widgets/settings_dialog.h" +#include "widgets/widget_window_bridge.h" + +class TZShotServices; + +class TZShotWidgetRuntime : public QObject +{ + Q_OBJECT + +public: + explicit TZShotWidgetRuntime(TZShotServices &services, QObject *parent = nullptr); + + OcrResultWidget& ocrResultWidget() { return m_ocrResultWidget; } + CaptureOverlayWidget& captureOverlayWidget() { return m_captureOverlayWidget; } + SettingsDialog& settingsDialog() { return m_settingsDialog; } + AboutDialog& aboutDialog() { return m_aboutDialog; } + LongCaptureController& longCaptureController() { return m_longCaptureController; } + WidgetWindowBridge& widgetWindowBridge() { return m_widgetWindowBridge; } + TrayIconHelper& trayHelper() { return m_trayHelper; } + +private: + OcrResultWidget m_ocrResultWidget; + CaptureOverlayWidget m_captureOverlayWidget; + SettingsDialog m_settingsDialog; + AboutDialog m_aboutDialog; + LongCaptureController m_longCaptureController; + WidgetWindowBridge m_widgetWindowBridge; + TrayIconHelper m_trayHelper; +}; + +#endif // TZSHOT_WIDGET_RUNTIME_H diff --git a/src/gif_encoder/gif_frame_grabber.cpp b/src/gif_encoder/gif_frame_grabber.cpp index 199d96a..4b853ae 100644 --- a/src/gif_encoder/gif_frame_grabber.cpp +++ b/src/gif_encoder/gif_frame_grabber.cpp @@ -3,6 +3,108 @@ #include #include #include +#include + +#ifdef Q_OS_WIN +#define WIN32_LEAN_AND_MEAN +#include +#endif + +namespace { + +#ifdef Q_OS_WIN +QImage captureVirtualDesktopImageWin(QRect *virtualGeometryOut = nullptr) +{ + const int vx = GetSystemMetrics(SM_XVIRTUALSCREEN); + const int vy = GetSystemMetrics(SM_YVIRTUALSCREEN); + const int vw = GetSystemMetrics(SM_CXVIRTUALSCREEN); + const int vh = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + if (virtualGeometryOut) { + *virtualGeometryOut = QRect(vx, vy, vw, vh); + } + if (vw <= 0 || vh <= 0) { + return {}; + } + + HDC screenDc = GetDC(nullptr); + if (!screenDc) { + return {}; + } + + HDC memDc = CreateCompatibleDC(screenDc); + HBITMAP bitmap = CreateCompatibleBitmap(screenDc, vw, vh); + HGDIOBJ oldBitmap = SelectObject(memDc, bitmap); + + BitBlt(memDc, 0, 0, vw, vh, screenDc, vx, vy, SRCCOPY | CAPTUREBLT); + + BITMAPINFO bmi{}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = vw; + bmi.bmiHeader.biHeight = -vh; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + QImage image(vw, vh, QImage::Format_ARGB32); + if (!image.isNull()) { + GetDIBits(memDc, + bitmap, + 0, + static_cast(vh), + image.bits(), + &bmi, + DIB_RGB_COLORS); + } + + SelectObject(memDc, oldBitmap); + DeleteObject(bitmap); + DeleteDC(memDc); + ReleaseDC(nullptr, screenDc); + + return image; +} +#endif + +QImage captureVirtualDesktopImageQt(QRect *virtualGeometryOut = nullptr) +{ + const QList screens = QGuiApplication::screens(); + if (screens.isEmpty()) { + return {}; + } + + QRect virtualGeometry; + for (QScreen *screen : screens) { + if (screen) { + virtualGeometry = virtualGeometry.united(screen->geometry()); + } + } + if (virtualGeometryOut) { + *virtualGeometryOut = virtualGeometry; + } + if (virtualGeometry.isEmpty()) { + return {}; + } + + QImage image(virtualGeometry.size(), QImage::Format_ARGB32); + image.fill(Qt::black); + QPainter painter(&image); + for (QScreen *screen : screens) { + if (!screen) { + continue; + } + const QRect screenGeom = screen->geometry(); + painter.drawPixmap(QRect(screenGeom.x() - virtualGeometry.x(), + screenGeom.y() - virtualGeometry.y(), + screenGeom.width(), + screenGeom.height()), + screen->grabWindow(0)); + } + painter.end(); + return image; +} + +} GifFrameGrabber::GifFrameGrabber(QObject* parent) : QObject(parent) @@ -32,32 +134,23 @@ void GifFrameGrabber::updateRect(const QRect& captureRect) void GifFrameGrabber::onTimerTick() { - QScreen* screen = QGuiApplication::primaryScreen(); - if (!screen) return; + QRect virtualGeometry; +#ifdef Q_OS_WIN + QImage fullImage = captureVirtualDesktopImageWin(&virtualGeometry); +#else + QImage fullImage = captureVirtualDesktopImageQt(&virtualGeometry); +#endif + if (fullImage.isNull() || virtualGeometry.isEmpty()) { + return; + } - // 抓取全屏后裁剪到选区(与 DesktopSnapshot 一致,HiDPI 安全) - QPixmap fullScreen = screen->grabWindow(0); - if (fullScreen.isNull()) return; - - // 将逻辑坐标转为物理像素坐标(处理 HiDPI) - qreal dpr = screen->devicePixelRatio(); - QRect physicalRect( - qRound(m_rect.x() * dpr), - qRound(m_rect.y() * dpr), - qRound(m_rect.width() * dpr), - qRound(m_rect.height() * dpr) - ); - - QImage fullImage = fullScreen.toImage(); - QImage frame = fullImage.copy(physicalRect); + const QRect localRect = m_rect.translated(-virtualGeometry.topLeft()).intersected(fullImage.rect()); + if (localRect.isEmpty()) { + return; + } + const QImage frame = fullImage.copy(localRect); if (!frame.isNull()) { - // 缩回逻辑分辨率,确保所有帧尺寸一致 - if (dpr != 1.0) { - frame = frame.scaled(m_rect.width(), m_rect.height(), - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - } emit frameCaptured(frame); } } diff --git a/src/magnifier_provider.cpp b/src/magnifier_provider.cpp deleted file mode 100644 index d5a2431..0000000 --- a/src/magnifier_provider.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "magnifier_provider.h" - -#include - -MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel) - : QQuickImageProvider(QQuickImageProvider::Image) - , m_viewModel(viewModel) -{ -} - -QImage MagnifierProvider::requestImage(const QString &id, - QSize *size, - const QSize & /*requestedSize*/) -{ - const QString cleanId = id.section('?', 0, 0); - const QStringList parts = cleanId.split(','); - if (parts.size() < 2) { - QImage blank(kOutputSize, kOutputSize, QImage::Format_ARGB32_Premultiplied); - blank.fill(Qt::black); - if (size) { - *size = blank.size(); - } - return blank; - } - - bool okX = false; - bool okY = false; - const int logicalX = parts[0].toInt(&okX); - const int logicalY = parts[1].toInt(&okY); - if (!okX || !okY) { - QImage blank(kOutputSize, kOutputSize, QImage::Format_ARGB32_Premultiplied); - blank.fill(Qt::black); - if (size) { - *size = blank.size(); - } - return blank; - } - - const int srcDiameter = kSrcRadius * 2; - QImage region(srcDiameter, srcDiameter, QImage::Format_ARGB32_Premultiplied); - region.fill(Qt::black); - - const QPixmap snapshot = m_viewModel->desktopSnapshot(); - if (snapshot.isNull()) { - QImage blank(kOutputSize, kOutputSize, QImage::Format_ARGB32_Premultiplied); - blank.fill(Qt::black); - if (size) { - *size = blank.size(); - } - return blank; - } - - const QRect virtualRect = m_viewModel->virtualGeometry(); - const QPoint center(logicalX - virtualRect.x(), logicalY - virtualRect.y()); - QRect srcRect(center.x() - kSrcRadius, - center.y() - kSrcRadius, - srcDiameter, - srcDiameter); - - QPainter regionPainter(®ion); - const QRect bounded = srcRect.intersected(snapshot.rect()); - if (!bounded.isEmpty()) { - const int destX = bounded.x() - srcRect.x(); - const int destY = bounded.y() - srcRect.y(); - regionPainter.drawPixmap(destX, - destY, - snapshot, - bounded.x(), - bounded.y(), - bounded.width(), - bounded.height()); - } - regionPainter.end(); - - QImage zoomed = region.scaled(kOutputSize, - kOutputSize, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - - QPainter p(&zoomed); - p.setRenderHint(QPainter::Antialiasing, false); - p.setPen(QPen(QColor(255, 0, 0, 220), 1)); - const int half = kOutputSize / 2; - p.drawLine(half, 0, half, kOutputSize - 1); - p.drawLine(0, half, kOutputSize - 1, half); - p.end(); - - if (size) { - *size = zoomed.size(); - } - return zoomed; -} diff --git a/src/magnifier_provider.h b/src/magnifier_provider.h deleted file mode 100644 index 46279c1..0000000 --- a/src/magnifier_provider.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef MAGNIFIER_PROVIDER_H -#define MAGNIFIER_PROVIDER_H - -#include -#include "viewmodel/screenshot_view_model.h" - -// MagnifierProvider -// ----------------- -// QQuickImageProvider 子类,供 QML Image 通过 -// source: "image://magnifier/," -// 获取以 (x,y) 为中心的放大镜图像。 -// -// 图像规格: -// 输出尺寸:110×110(QML 侧再用 width/height 撑到 130×130 预留边距) -// 采样区域:以 (x,y) 为中心、半径 40 像素(共 80×80)从桌面快照裁剪 -// 放大倍率:~1.375x(110/80) -// 十字准线:红色,绘制在 requestImage() 内部 -// -// 线程安全:requestImage() 可能在非主线程调用,只对 QImage 做只读 copy()。 - -class MagnifierProvider : public QQuickImageProvider -{ -public: - explicit MagnifierProvider(ScreenshotViewModel *viewModel); - - // QQuickImageProvider interface - QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; - -private: - ScreenshotViewModel *m_viewModel; - - static const int kSrcRadius = 20; // 采样半径(源区域 40×40) - static const int kOutputSize = 110; // 输出图像尺寸 -}; - -#endif // MAGNIFIER_PROVIDER_H diff --git a/src/main.cpp b/src/main.cpp index c9ba7b9..8d68dd5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,132 +1,15 @@ -#include -#include -#include -#include -#include +#include -#include "tray_icon_helper.h" -#include "paint_board/paint_board.h" -#include "shortcut_key/globalshortcut.h" -#include "magnifier_provider.h" -#include "sticky_image_store.h" -#include "sticky_image_provider_proxy.h" -#include "language_manager.h" -#include "instance_activation.h" -#include "widgets/ocr_result_widget.h" -#include "widgets/settings_dialog.h" -#include "widgets/widget_window_bridge.h" - -// Model -#include "model/desktop_snapshot.h" -#include "model/app_settings.h" - -// ViewModel -#include "viewmodel/screenshot_view_model.h" -#include "viewmodel/sticky_view_model.h" -#include "viewmodel/ai_view_model.h" -#include "viewmodel/storage_view_model.h" -#include "viewmodel/gif_record_view_model.h" -#include "viewmodel/scroll_capture_view_model.h" -#include "viewmodel/ocr_view_model.h" +#include "app/tzshot_app.h" int main(int argc, char *argv[]) { - // 强制让窗口跟随系统DPI,不使用虚拟像素 - // QGuiApplication::setHighDpiScaleFactorRoundingPolicy( - // Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor); - QApplication app(argc, argv); - // Single-instance activation: second launch notifies the first one and exits. - InstanceActivation instanceActivation(QStringLiteral("TZshot.Instance")); - if (!instanceActivation.initialize()) { + TZShotApp tzshotApp(app); + if (!tzshotApp.initialize()) { return 0; } - QCoreApplication::setOrganizationName("TZshot"); - QCoreApplication::setOrganizationDomain("tzshot.local"); - QCoreApplication::setApplicationName("TZshot"); - app.setQuitOnLastWindowClosed(false); - - - // ── Model 层 ────────────────────────────────────────── - DesktopSnapshot desktopSnapshot; - AppSettings appSettings; - StickyImageStore stickyImageStore; - - // ── ViewModel 层 ────────────────────────────────────── - ScreenshotViewModel screenshotViewModel(desktopSnapshot, stickyImageStore); - StickyViewModel stickyViewModel(stickyImageStore); - AIViewModel aiViewModel(appSettings, stickyImageStore); - stickyViewModel.setAiViewModel(&aiViewModel); - StorageViewModel storageViewModel(appSettings); - LanguageManager languageManager(appSettings); - GifRecordViewModel gifRecordViewModel(appSettings); - ScrollCaptureViewModel scrollCaptureViewModel(screenshotViewModel, appSettings); - OcrViewModel ocrViewModel; - OcrResultWidget ocrResultWidget(&ocrViewModel); - - // ── 其他服务(不属于 MVVM 任何层)─────────────────── - GlobalShortcut globalShortcut; - - SettingsDialog settingsDialog(&aiViewModel, - &storageViewModel, - &languageManager, - &gifRecordViewModel, - &ocrViewModel, - &globalShortcut); - WidgetWindowBridge widgetWindowBridge(&settingsDialog); - - QQmlApplicationEngine engine; - - - // ── Qt 基础设施注册 ─────────────────────────────────── - engine.addImportPath("qrc:/qml"); - qmlRegisterType("CustomComponents", 1, 0, "PaintBoard"); - qmlRegisterType("com.Tz.tray", 1, 0, "TrayIconHelper"); - - // MagnifierProvider 持有 ScreenshotViewModel 引用访问快照,engine 接管生命周期 - engine.addImageProvider("magnifier", new MagnifierProvider(&screenshotViewModel)); - // StickyImageProviderProxy 转发到 StickyImageStore,engine 接管生命周期 - engine.addImageProvider("sticky", new StickyImageProviderProxy(stickyImageStore)); - - // ── 暴露给 QML 的 ViewModel ─────────────────────────── - engine.rootContext()->setContextProperty("O_ScreenCapture", &screenshotViewModel); - engine.rootContext()->setContextProperty("O_StickyViewModel", &stickyViewModel); - engine.rootContext()->setContextProperty("O_AIViewModel", &aiViewModel); - engine.rootContext()->setContextProperty("O_ImageSaver", &storageViewModel); - engine.rootContext()->setContextProperty("O_GlobalShortcut", &globalShortcut); - engine.rootContext()->setContextProperty("O_LanguageManager", &languageManager); - engine.rootContext()->setContextProperty("O_GifRecordVM", &gifRecordViewModel); - engine.rootContext()->setContextProperty("O_ScrollCaptureVM", &scrollCaptureViewModel); - engine.rootContext()->setContextProperty("O_OcrVM", &ocrViewModel); - engine.rootContext()->setContextProperty("O_InstanceBridge", &instanceActivation); - engine.rootContext()->setContextProperty("O_WidgetWindows", &widgetWindowBridge); - - QObject::connect(&ocrViewModel, &OcrViewModel::isRecognizingChanged, &app, [&]() { - if (ocrViewModel.isRecognizing()) { - ocrResultWidget.showAndActivate(); - } - }); - QObject::connect(&ocrViewModel, &OcrViewModel::resultReady, &app, [&](const QString &) { - ocrResultWidget.showAndActivate(); - }); - - const QUrl url(QStringLiteral("qrc:/qml/main.qml")); - QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, - &app, [url](QObject *obj, const QUrl &objUrl) { - if (!obj && url == objUrl) - QCoreApplication::exit(-1); - }, Qt::QueuedConnection); - engine.load(url); - - if (!engine.rootObjects().isEmpty()) { - if (auto *window = qobject_cast(engine.rootObjects().constFirst())) { - if (QScreen *screen = QGuiApplication::primaryScreen()) { - window->setGeometry(screen->virtualGeometry()); - } - } - } - return app.exec(); } diff --git a/src/paint_board/paint_board.cpp b/src/paint_board/paint_board.cpp deleted file mode 100644 index 0a61b56..0000000 --- a/src/paint_board/paint_board.cpp +++ /dev/null @@ -1,261 +0,0 @@ -#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(); -} - -void PaintBoard::addTextAnnotation(int x, int y, const QString& text) -{ - const QString resolvedText = text.trimmed().isEmpty() ? m_annotationText : text; - Shape* shape = m_factory.createTextShape( - QPoint(x, y), - resolvedText, - m_currentColor, - m_currentSize, - m_textBackgroundEnabled - ); - if (!shape) - return; - - m_shapes.append(shape); - update(); -} - -void PaintBoard::setShapeType(int type) -{ - const auto t = static_cast(type); - if (m_currentType == t) - return; - m_currentType = t; - emit shapeTypeChanged(type); -} - -void PaintBoard::setPenColor(const QColor &color) -{ - if (m_currentColor == color) - return; - m_currentColor = color; - emit penColorChanged(color); -} - -void PaintBoard::setPenSize(int size) -{ - if (m_currentSize == size) - return; - m_currentSize = size; - emit penSizeChanged(size); -} - -void PaintBoard::setAnnotationText(const QString& text) -{ - if (m_annotationText == text) - return; - m_annotationText = text; - emit annotationTextChanged(m_annotationText); -} - -void PaintBoard::setTextBackgroundEnabled(bool enabled) -{ - if (m_textBackgroundEnabled == enabled) - return; - m_textBackgroundEnabled = enabled; - emit textBackgroundEnabledChanged(enabled); -} - -void PaintBoard::setMosaicBlurLevel(int level) -{ - const int normalized = qBound(1, level, 4); - if (m_mosaicBlurLevel == normalized) - return; - m_mosaicBlurLevel = normalized; - emit mosaicBlurLevelChanged(m_mosaicBlurLevel); -} - -void PaintBoard::setNumberAutoIncrement(bool enabled) -{ - if (m_numberAutoIncrement == enabled) - return; - m_numberAutoIncrement = enabled; - emit numberAutoIncrementChanged(m_numberAutoIncrement); -} - -void PaintBoard::setNumberValue(int value) -{ - const int normalized = qBound(1, value, 9999); - if (m_numberValue == normalized) - return; - m_numberValue = normalized; - emit numberValueChanged(m_numberValue); -} - -void PaintBoard::undo() -{ - if (!m_shapes.isEmpty()) { - delete m_shapes.last(); - m_shapes.removeLast(); - update(); - } -} - -void PaintBoard::reset() -{ - qDeleteAll(m_shapes); - m_shapes.clear(); - - delete m_currentShape; - m_currentShape = nullptr; - m_drawing = false; - - m_background_img = QImage(); - update(); -} - -QImage PaintBoard::renderToImage() const -{ - if (m_shapes.isEmpty()) - return QImage(); - - QImage img(qRound(width() ), qRound(height()), QImage::Format_ARGB32); - img.setDevicePixelRatio(1.0); - img.fill(Qt::transparent); - - QPainter painter(&img); - painter.setRenderHint(QPainter::Antialiasing); - for (Shape* shape : m_shapes) { - if (shape) - shape->draw(&painter); - } - painter.end(); - return img; -} - -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_drawing = true; - - if (m_currentType == TEXT) { - // Text insertion is handled by TZWindow popup flow. - m_drawing = false; - return; - } - - if (m_currentType == NUMBER) { - const int drawNumber = m_numberValue; - if (m_numberAutoIncrement) - setNumberValue(m_numberValue + 1); - - m_currentShape = m_factory.createNumberShape( - m_startPoint, - drawNumber, - m_currentColor, - m_currentSize - ); - m_shapes.append(m_currentShape); - m_currentShape = nullptr; - m_drawing = false; - update(); - return; - } - - if (m_currentType == MOSAIC) { - const int blurScale = qMax(1, m_mosaicBlurLevel); - auto* mosaicShape = new MosaicShape( - m_startPoint, - m_currentSize * 5 * blurScale, - qMax(2, m_currentSize * blurScale) - ); - - QImage snapshot = m_background_img; - qDebug() << "[PaintBoard][MOSAIC] press at" << m_startPoint - << "snapshotNull=" << snapshot.isNull() - << "snapshotSize=" << snapshot.size() - << "penSize=" << m_currentSize - << "blurLevel=" << m_mosaicBlurLevel; - mosaicShape->setCanvasSnapshot(snapshot); - m_currentShape = mosaicShape; - return; - } - - m_currentShape = m_factory.createShape( - m_currentType, - m_startPoint, - m_startPoint, - m_currentColor, - m_currentSize - ); -} - -void PaintBoard::mouseMoveEvent(QMouseEvent *event) -{ - if (!m_drawing || !m_currentShape) - return; - - m_currentShape->setEndPoint((event->position()).toPoint()); - update(); -} - -void PaintBoard::mouseReleaseEvent(QMouseEvent *event) -{ - if (!m_drawing || !m_currentShape) - return; - - m_currentShape->setEndPoint((event->position()).toPoint()); - m_shapes.append(m_currentShape); - m_currentShape = nullptr; - m_drawing = false; - update(); -} - -void PaintBoard::hoverMoveEvent(QHoverEvent *event) -{ - Q_UNUSED(event) -} - -bool PaintBoard::drawBkImg(QPainter *painter, QImage img) -{ - if (!painter || img.isNull()) - return false; - - painter->drawImage(QRectF(0, 0, width() , height() ), img); - return true; -} - -bool PaintBoard::drawShape(QPainter *painter) -{ - if (!painter) - return false; - - for (Shape* shape : m_shapes) { - if (shape) - shape->draw(painter); - } - - if (m_currentShape) - m_currentShape->draw(painter); - - return true; -} diff --git a/src/paint_board/paint_board.h b/src/paint_board/paint_board.h deleted file mode 100644 index bc303ef..0000000 --- a/src/paint_board/paint_board.h +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef PAINTBOARD_H -#define PAINTBOARD_H - -#include -#include -#include -#include -#include -#include - -#include "shape/shape.h" -#include "shape/shape_type.h" -#include "shape/shape_factory.h" -#include "shape/mosaic_shape.h" - -class PaintBoard : public QQuickPaintedItem -{ - Q_OBJECT - - Q_PROPERTY(int shapeType READ shapeType WRITE setShapeType NOTIFY shapeTypeChanged FINAL) - Q_PROPERTY(QColor penColor READ penColor WRITE setPenColor NOTIFY penColorChanged FINAL) - Q_PROPERTY(int penSize READ penSize WRITE setPenSize NOTIFY penSizeChanged FINAL) - Q_PROPERTY(QString annotationText READ annotationText WRITE setAnnotationText NOTIFY annotationTextChanged FINAL) - Q_PROPERTY(bool textBackgroundEnabled READ textBackgroundEnabled WRITE setTextBackgroundEnabled NOTIFY textBackgroundEnabledChanged FINAL) - Q_PROPERTY(int mosaicBlurLevel READ mosaicBlurLevel WRITE setMosaicBlurLevel NOTIFY mosaicBlurLevelChanged FINAL) - Q_PROPERTY(bool numberAutoIncrement READ numberAutoIncrement WRITE setNumberAutoIncrement NOTIFY numberAutoIncrementChanged FINAL) - Q_PROPERTY(int numberValue READ numberValue WRITE setNumberValue NOTIFY numberValueChanged FINAL) - -public: - explicit PaintBoard(QQuickItem *parent = nullptr); - - int shapeType() const { return static_cast(m_currentType); } - QColor penColor() const { return m_currentColor; } - int penSize() const { return m_currentSize; } - QString annotationText() const { return m_annotationText; } - bool textBackgroundEnabled() const { return m_textBackgroundEnabled; } - int mosaicBlurLevel() const { return m_mosaicBlurLevel; } - bool numberAutoIncrement() const { return m_numberAutoIncrement; } - int numberValue() const { return m_numberValue; } - - void setShapeType(int type); - void setPenColor(const QColor &color); - void setPenSize(int size); - void setAnnotationText(const QString& text); - void setTextBackgroundEnabled(bool enabled); - void setMosaicBlurLevel(int level); - void setNumberAutoIncrement(bool enabled); - void setNumberValue(int value); - -public: - Q_INVOKABLE void setBackgroundImg(QImage img); - Q_INVOKABLE bool hasBackgroundImg() const { return !m_background_img.isNull(); } - Q_INVOKABLE void addTextAnnotation(int x, int y, const QString& text); - - // Render all annotations onto a transparent image for screenshot merge. - Q_INVOKABLE QImage renderToImage() const; - - Q_INVOKABLE void undo(); - Q_INVOKABLE void reset(); // Clear all shapes and background image. - -signals: - void shapeTypeChanged(int shapeType); - void penColorChanged(QColor penColor); - void penSizeChanged(int penSize); - void annotationTextChanged(const QString& text); - void textBackgroundEnabledChanged(bool enabled); - void mosaicBlurLevelChanged(int level); - void numberAutoIncrementChanged(bool enabled); - void numberValueChanged(int value); - -protected: - void paint(QPainter *painter) override; - - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - - void hoverMoveEvent(QHoverEvent *event) override; - - bool drawBkImg(QPainter *painter, QImage img); - bool drawShape(QPainter *painter); - -private: - ShapeFactory m_factory; - - QList m_shapes; - Shape* m_currentShape = nullptr; - bool m_drawing = false; - QPoint m_startPoint; - - Shapeype m_currentType = RECTANGLE; - QColor m_currentColor = Qt::red; - int m_currentSize = 6; - QString m_annotationText = QStringLiteral("Text"); - bool m_textBackgroundEnabled = true; - int m_mosaicBlurLevel = 2; - bool m_numberAutoIncrement = true; - int m_numberValue = 1; - - QImage m_background_img; -}; - -#endif // PAINTBOARD_H diff --git a/src/paint_board/shape/mosaic_shape.h b/src/paint_board/shape/mosaic_shape.h index 7a69e8d..1281d55 100644 --- a/src/paint_board/shape/mosaic_shape.h +++ b/src/paint_board/shape/mosaic_shape.h @@ -29,7 +29,6 @@ public: void draw(QPainter* painter) override; // 设置当前画布的快照,用于马赛克取色 - // 每次绘制前由 PaintBoard 调用 void setCanvasSnapshot(const QImage& snapshot); private: diff --git a/src/sticky_image_provider_proxy.cpp b/src/sticky_image_provider_proxy.cpp deleted file mode 100644 index 888e540..0000000 --- a/src/sticky_image_provider_proxy.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "sticky_image_provider_proxy.h" - -StickyImageProviderProxy::StickyImageProviderProxy(StickyImageStore &store) - : QQuickImageProvider(QQuickImageProvider::Image) - , m_store(store) -{ -} - -QImage StickyImageProviderProxy::requestImage(const QString &id, QSize *size, const QSize &requestedSize) -{ - Q_UNUSED(requestedSize) - - QString cleanId = id; - const int queryIndex = cleanId.indexOf('?'); - if (queryIndex >= 0) { - cleanId = cleanId.left(queryIndex); - } - - QImage image = m_store.getImage(cleanId); - - if (size) { - const qreal dpr = image.devicePixelRatio(); - if (!image.isNull() && dpr > 0.0) { - *size = QSize(qMax(1, qRound(image.width() / dpr)), - qMax(1, qRound(image.height() / dpr))); - } else { - *size = image.size(); - } - } - return image; -} diff --git a/src/sticky_image_provider_proxy.h b/src/sticky_image_provider_proxy.h deleted file mode 100644 index 0d4f929..0000000 --- a/src/sticky_image_provider_proxy.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef STICKY_IMAGE_PROVIDER_PROXY_H -#define STICKY_IMAGE_PROVIDER_PROXY_H - -#include -#include "sticky_image_store.h" - -// StickyImageProviderProxy -// ------------------------ -// 纯粹的 QQuickImageProvider 实现,不继承 QObject。 -// 生命周期由 QQmlApplicationEngine 通过 addImageProvider() 接管并负责释放。 -// 所有实际数据操作委托给 StickyImageStore,自身不持有任何图片数据。 - -class StickyImageProviderProxy : public QQuickImageProvider -{ -public: - explicit StickyImageProviderProxy(StickyImageStore &store); - - QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; - -private: - StickyImageStore &m_store; // 引用,不持有所有权 -}; - -#endif // STICKY_IMAGE_PROVIDER_PROXY_H diff --git a/src/sticky_image_store.h b/src/sticky_image_store.h index 437c33b..5b1f189 100644 --- a/src/sticky_image_store.h +++ b/src/sticky_image_store.h @@ -11,8 +11,7 @@ // StickyImageStore // ---------------- // 纯业务类(QObject),负责贴图的存储、释放、保存、复制到剪贴板等操作。 -// 不继承 QQuickImageProvider,生命周期完全由调用方控制,不受 engine 影响。 -// 图片数据通过 storeImage() 写入,由 StickyImageProviderProxy 的 requestImage() 读取。 +// 生命周期完全由调用方控制,不依赖 Qt Quick 运行时。 class StickyImageStore : public QObject { @@ -21,11 +20,11 @@ class StickyImageStore : public QObject public: explicit StickyImageStore(QObject *parent = nullptr); - // 存储图片,返回可供 QML Image.source 使用的 URL 字符串 + // 存储图片,返回统一的贴图 URL 字符串 // 格式:"image://sticky/" QString storeImage(const QImage &image); - // 供 StickyImageProviderProxy 调用,线程安全 + // 线程安全读取 QImage getImage(const QString &id) const; QImage getImageByUrl(const QString &imageUrl) const; diff --git a/src/viewmodel/screenshot_view_model.cpp b/src/viewmodel/screenshot_view_model.cpp index fbbdcc4..6ea596e 100644 --- a/src/viewmodel/screenshot_view_model.cpp +++ b/src/viewmodel/screenshot_view_model.cpp @@ -1,13 +1,12 @@ -#include "screenshot_view_model.h" -#include "paint_board/paint_board.h" +#include "screenshot_view_model.h" +#include "widgets/sticky_canvas_widget.h" #include #include #include #include -#include -#include #include +#include #include #ifdef Q_OS_WIN @@ -38,19 +37,23 @@ qreal captureDprForRect(const QRect& rect) } } #endif - if (QScreen* screen = QGuiApplication::primaryScreen()) { + if (QScreen *screen = QGuiApplication::primaryScreen()) { return screen->devicePixelRatio(); } return 1.0; } -qreal effectiveCaptureDpr(QQuickItem *paintBoard, const QRect &rect) +qreal effectiveCaptureDpr(StickyCanvasWidget *paintBoard, const QRect &rect) { if (!rect.isEmpty()) { return captureDprForRect(rect); } - if (paintBoard && paintBoard->window()) { - return paintBoard->window()->devicePixelRatio(); + if (paintBoard) { + if (QWidget *topLevel = paintBoard->window()) { + if (QWindow *handle = topLevel->windowHandle()) { + return handle->devicePixelRatio(); + } + } } if (QScreen *screen = QGuiApplication::primaryScreen()) { return screen->devicePixelRatio(); @@ -124,7 +127,7 @@ QRect ScreenshotViewModel::windowAtPoint(int x, int y) const return m_snapshot.windowAtPoint(x, y); } -bool ScreenshotViewModel::captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect) +bool ScreenshotViewModel::captureRectToClipboard(StickyCanvasWidget *paintBoard, const QRect &rect) { const QRect r = rect.normalized(); if (r.isEmpty()) { @@ -141,7 +144,7 @@ bool ScreenshotViewModel::captureRectToClipboard(QQuickItem *paintBoard, const Q return writeImageToClipboard(img); } -QString ScreenshotViewModel::captureRectToBase64(QQuickItem *paintBoard, const QRect &rect) +QString ScreenshotViewModel::captureRectToBase64(StickyCanvasWidget *paintBoard, const QRect &rect) { const QRect r = rect.normalized(); if (r.isEmpty()) { @@ -165,7 +168,7 @@ QString ScreenshotViewModel::captureRectToBase64(QQuickItem *paintBoard, const Q return data.toBase64(); } -QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect) +QString ScreenshotViewModel::captureRectToStickyUrl(StickyCanvasWidget *paintBoard, const QRect &rect) { const QRect r = rect.normalized(); if (r.isEmpty()) { @@ -187,15 +190,6 @@ QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, cons return m_stickyStore.storeImage(img); } -void ScreenshotViewModel::grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect) -{ - const QRect r = rect.normalized(); - if (r.isEmpty()) { - return; - } - captureScreen(paintBoard, r, true); -} - QImage ScreenshotViewModel::captureRectToImage(const QRect &rect) { const QRect r = rect.normalized(); @@ -233,17 +227,14 @@ bool ScreenshotViewModel::copyImageToClipboard(const QImage &image) return writeImageToClipboard(image); } -QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard, +QImage ScreenshotViewModel::captureScreen(StickyCanvasWidget *paintBoard, const QRect &rect, bool setBackground) { - PaintBoard *board = qobject_cast(paintBoard); - QImage screenshot; - qreal dpr = 1.0; if (!m_snapshot.isNull()) { - dpr = effectiveCaptureDpr(paintBoard, rect); + const qreal dpr = effectiveCaptureDpr(paintBoard, rect); const QRect captureRect = captureRectInSnapshot(m_snapshot, rect, dpr); screenshot = m_snapshot.image().copy(captureRect).toImage(); } else { @@ -255,50 +246,22 @@ QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard, screenshot = screen->grabWindow(0).toImage(); } - if (board) { - QImage annotations = board->renderToImage(); - if (!annotations.isNull()) { - QPainter p(&screenshot); - + if (paintBoard) { + const QImage composited = paintBoard->compositedImage(); + if (!composited.isNull()) { if (!rect.isEmpty()) { - QQuickWindow *win = board->window(); - const int windowAbsX = win ? win->x() : 0; - const int windowAbsY = win ? win->y() : 0; - const int boardAbsX = windowAbsX + qRound(board->x()); - const int boardAbsY = windowAbsY + qRound(board->y()); - - const int sourceX = rect.x() - boardAbsX; - const int sourceY = rect.y() - boardAbsY; - QRect srcRect(sourceX, sourceY, rect.width(), rect.height()); - srcRect = srcRect.intersected(annotations.rect()); - - if (!srcRect.isEmpty()) { - const int dstX = qRound((srcRect.x() - sourceX) * dpr); - const int dstY = qRound((srcRect.y() - sourceY) * dpr); - const int dstW = qMax(1, qRound(srcRect.width() * dpr)); - const int dstH = qMax(1, qRound(srcRect.height() * dpr)); - p.drawImage(QRect(dstX, dstY, dstW, dstH), - annotations, - srcRect); + const QRect localRect = rect.translated(-m_snapshot.virtualGeometry().topLeft()) + .intersected(composited.rect()); + if (!localRect.isEmpty()) { + screenshot = composited.copy(localRect); } } else { - p.drawImage(0, 0, annotations); + screenshot = composited; } - p.end(); } if (setBackground) { - QImage background = screenshot; - 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, - Qt::SmoothTransformation); - } - } - board->setBackgroundImg(background); + paintBoard->setBackgroundImage(screenshot); } } diff --git a/src/viewmodel/screenshot_view_model.h b/src/viewmodel/screenshot_view_model.h index c1b484a..bc778a6 100644 --- a/src/viewmodel/screenshot_view_model.h +++ b/src/viewmodel/screenshot_view_model.h @@ -3,13 +3,13 @@ #include #include -#include #include -#include #include "model/desktop_snapshot.h" #include "model/app_settings.h" #include "sticky_image_store.h" +class StickyCanvasWidget; + class ScreenshotViewModel : public QObject { Q_OBJECT @@ -34,10 +34,9 @@ public: Q_INVOKABLE void releaseDesktopSnapshot(); Q_INVOKABLE QColor getPixelColor(int x, int y) const; Q_INVOKABLE QRect windowAtPoint(int x, int y) const; - Q_INVOKABLE bool captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect); - Q_INVOKABLE QString captureRectToBase64(QQuickItem *paintBoard, const QRect &rect); - Q_INVOKABLE QString captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect); - Q_INVOKABLE void grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect); + Q_INVOKABLE bool captureRectToClipboard(StickyCanvasWidget *paintBoard, const QRect &rect); + Q_INVOKABLE QString captureRectToBase64(StickyCanvasWidget *paintBoard, const QRect &rect); + Q_INVOKABLE QString captureRectToStickyUrl(StickyCanvasWidget *paintBoard, const QRect &rect); Q_INVOKABLE QImage captureRectToImage(const QRect &rect); QImage captureRectToImageLive(const QRect &rect); @@ -45,7 +44,7 @@ public: private: QRect captureRectForDisplay(const QRect &rect) const; - QImage captureScreen(QQuickItem *paintBoard, + QImage captureScreen(StickyCanvasWidget *paintBoard, const QRect &rect = QRect(), bool setBackground = false); bool writeImageToClipboard(const QImage &image); diff --git a/src/viewmodel/sticky_view_model.cpp b/src/viewmodel/sticky_view_model.cpp index d87bd5e..792f83e 100644 --- a/src/viewmodel/sticky_view_model.cpp +++ b/src/viewmodel/sticky_view_model.cpp @@ -42,6 +42,19 @@ void StickyViewModel::requestSticky(const QString &imageUrl, const QRect &imgRec emit stickyReady(imageUrl, targetRect); } +void StickyViewModel::requestStickyImage(const QImage &image, const QRect &imgRect) +{ + if (image.isNull()) { + return; + } + + const QString imageUrl = m_store.storeImage(image); + if (imageUrl.isEmpty()) { + return; + } + requestSticky(imageUrl, imgRect); +} + void StickyViewModel::releaseImage(const QString &imageUrl) { m_store.releaseImage(imageUrl); diff --git a/src/viewmodel/sticky_view_model.h b/src/viewmodel/sticky_view_model.h index f241190..6b3ac25 100644 --- a/src/viewmodel/sticky_view_model.h +++ b/src/viewmodel/sticky_view_model.h @@ -13,8 +13,8 @@ class AIViewModel; // --------------- // 负责贴图会话的业务逻辑: // - 从 StickyImageStore 读写贴图数据(save/copy/release) -// - 通过信号通知 QML 创建贴图窗口(View 监听信号后自行创建 TZStickyWindow) -// QML 侧只需监听 stickyReady 信号,无需知道截图细节。 +// - 在 Windows 上直接驱动 QWidget 贴图窗口 +// - 在其他平台保留信号式窗口创建接口作为回退路径 class StickyViewModel : public QObject { @@ -24,11 +24,11 @@ public: explicit StickyViewModel(StickyImageStore &store, QObject *parent = nullptr); void setAiViewModel(AIViewModel *aiViewModel); - // 通知 QML 创建贴图窗口,由 ScreenshotViewModel::captureRectToStickyUrl 产生的 URL 触发 - // QML 监听 stickyReady(imageUrl, imgRect) 信号后创建 TZStickyWindow + // 在 Windows 上直接创建 QWidget 贴图;其他平台通过 stickyReady 回退。 Q_INVOKABLE void requestSticky(const QString &imageUrl, const QRect &imgRect); + Q_INVOKABLE void requestStickyImage(const QImage &image, const QRect &imgRect); - // 以下三个方法供 TZStickyWindow.qml 直接调用(贴图窗口内部操作) + // 贴图窗口内部操作 Q_INVOKABLE void releaseImage(const QString &imageUrl); Q_INVOKABLE bool saveImage(const QString &imageUrl, const QUrl &targetUrl); Q_INVOKABLE bool copyImageToClipboard(const QString &imageUrl); @@ -42,7 +42,7 @@ public: Q_INVOKABLE bool mirrorImage(const QString &imageUrl, bool horizontal, bool vertical); signals: - // QML 监听此信号,收到后创建并显示 TZStickyWindow + // 非 Windows 平台可监听此信号自行创建贴图窗口 void stickyReady(const QString &imageUrl, const QRect &imgRect); private: diff --git a/src/widgets/about_dialog.cpp b/src/widgets/about_dialog.cpp new file mode 100644 index 0000000..505ff6a --- /dev/null +++ b/src/widgets/about_dialog.cpp @@ -0,0 +1,98 @@ +#include "widgets/about_dialog.h" + +#include +#include +#include +#include +#include +#include +#include + +AboutDialog::AboutDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(tr("关于 TZshot")); + setModal(false); + setFixedSize(400, 350); + setStyleSheet(QStringLiteral("background:#F8FAFC;")); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(32, 32, 32, 24); + layout->setSpacing(0); + + auto *logoWrap = new QWidget(this); + auto *logoLayout = new QHBoxLayout(logoWrap); + logoLayout->setContentsMargins(0, 0, 0, 0); + auto *logo = new QLabel(QStringLiteral("T"), logoWrap); + logo->setAlignment(Qt::AlignCenter); + logo->setFixedSize(72, 72); + logo->setStyleSheet(QStringLiteral( + "background:#3B82F6; color:#FFFFFF; border-radius:18px; font-size:40px; font-weight:700;")); + logoLayout->addStretch(1); + logoLayout->addWidget(logo); + logoLayout->addStretch(1); + layout->addWidget(logoWrap); + + layout->addSpacing(16); + + auto *title = new QLabel(QStringLiteral("TZshot"), this); + title->setAlignment(Qt::AlignCenter); + title->setStyleSheet(QStringLiteral("font-size:22px; font-weight:700; color:#1E293B;")); + layout->addWidget(title); + + layout->addSpacing(6); + + auto *version = new QLabel(tr("版本 v1.0.0"), this); + version->setAlignment(Qt::AlignCenter); + version->setStyleSheet(QStringLiteral("font-size:13px; color:#64748B;")); + layout->addWidget(version); + + layout->addSpacing(16); + + auto *divider = new QFrame(this); + divider->setFrameShape(QFrame::HLine); + divider->setStyleSheet(QStringLiteral("color:#E2E8F0;")); + layout->addWidget(divider); + + layout->addSpacing(16); + + auto *desc = new QLabel(tr("简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。"), this); + desc->setWordWrap(true); + desc->setAlignment(Qt::AlignCenter); + desc->setStyleSheet(QStringLiteral("font-size:13px; color:#475569;")); + layout->addWidget(desc); + + layout->addStretch(1); + + auto *copyright = new QLabel(tr("© 2025 TZshot. All rights reserved."), this); + copyright->setAlignment(Qt::AlignCenter); + copyright->setStyleSheet(QStringLiteral("font-size:11px; color:#94A3B8;")); + layout->addWidget(copyright); + + layout->addSpacing(8); + + auto *buttonWrap = new QWidget(this); + auto *buttonLayout = new QHBoxLayout(buttonWrap); + buttonLayout->setContentsMargins(0, 0, 0, 0); + auto *closeButton = new QPushButton(tr("关闭"), buttonWrap); + closeButton->setFixedSize(88, 34); + closeButton->setStyleSheet(QStringLiteral( + "QPushButton { background:#3B82F6; color:white; border:none; border-radius:8px; font-size:13px; }" + "QPushButton:hover { background:#2563EB; }")); + connect(closeButton, &QPushButton::clicked, this, &QDialog::close); + buttonLayout->addStretch(1); + buttonLayout->addWidget(closeButton); + buttonLayout->addStretch(1); + layout->addWidget(buttonWrap); +} + +void AboutDialog::showAndActivate() +{ + if (QScreen *screen = QGuiApplication::primaryScreen()) { + const QRect work = screen->availableGeometry(); + move(work.center() - rect().center()); + } + show(); + raise(); + activateWindow(); +} diff --git a/src/widgets/about_dialog.h b/src/widgets/about_dialog.h new file mode 100644 index 0000000..11edb86 --- /dev/null +++ b/src/widgets/about_dialog.h @@ -0,0 +1,16 @@ +#ifndef ABOUT_DIALOG_H +#define ABOUT_DIALOG_H + +#include + +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = nullptr); + + void showAndActivate(); +}; + +#endif // ABOUT_DIALOG_H diff --git a/src/widgets/capture_overlay_widget.cpp b/src/widgets/capture_overlay_widget.cpp new file mode 100644 index 0000000..4bd9c07 --- /dev/null +++ b/src/widgets/capture_overlay_widget.cpp @@ -0,0 +1,1384 @@ +#include "widgets/capture_overlay_widget.h" + +#include "widgets/magnifier_widget.h" +#include "widgets/sticky_canvas_widget.h" +#include "widgets/widget_window_bridge.h" +#include "viewmodel/gif_record_view_model.h" +#include "viewmodel/ocr_view_model.h" +#include "viewmodel/screenshot_view_model.h" +#include "viewmodel/sticky_view_model.h" +#include "viewmodel/storage_view_model.h" + +#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 +#endif + +namespace { + +class GifBorderOverlayWidget : public QWidget +{ +public: + explicit GifBorderOverlayWidget(QWidget *parent = nullptr) + : QWidget(parent) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_TranslucentBackground, true); + setAttribute(Qt::WA_TransparentForMouseEvents, true); + } + +protected: + void paintEvent(QPaintEvent *) override + { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, false); + QPen pen(QColor(QStringLiteral("#EF4444"))); + pen.setWidth(2); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + painter.drawRect(rect().adjusted(1, 1, -1, -1)); + } +}; + +QString loadCaptureOverlayStyleSheet() +{ + QFile file(QStringLiteral(":/resource/qss/capture_overlay_widget.qss")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + return QString::fromUtf8(file.readAll()); +} + +QToolButton *createToolbarButton(const QString &iconPath, + const QString &toolTip, + QWidget *parent) +{ + auto *button = new QToolButton(parent); + button->setIcon(QIcon(iconPath)); + button->setToolTip(toolTip); + button->setAutoRaise(false); + button->setCheckable(false); + button->setFixedSize(32, 32); + button->setIconSize(QSize(18, 18)); + button->setCursor(Qt::PointingHandCursor); + return button; +} + +QRect placementBoundsForGlobalRect(const QRect &globalRect, const QRect &fallbackBounds) +{ +#ifdef Q_OS_WIN + RECT rect{ globalRect.left(), globalRect.top(), globalRect.right() + 1, globalRect.bottom() + 1 }; + if (HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST)) { + MONITORINFO info{}; + info.cbSize = sizeof(MONITORINFO); + if (GetMonitorInfoW(monitor, &info)) { + const RECT &work = info.rcWork; + return QRect(work.left, work.top, work.right - work.left, work.bottom - work.top); + } + } +#endif + return fallbackBounds; +} + +} + +CaptureOverlayWidget::CaptureOverlayWidget(ScreenshotViewModel *screenCapture, + StickyViewModel *stickyViewModel, + StorageViewModel *storageViewModel, + OcrViewModel *ocrViewModel, + GifRecordViewModel *gifRecordViewModel, + WidgetWindowBridge *widgetWindowBridge, + QWidget *parent) + : QWidget(parent) + , m_screenCapture(screenCapture) + , m_stickyViewModel(stickyViewModel) + , m_storageViewModel(storageViewModel) + , m_ocrViewModel(ocrViewModel) + , m_gifRecordViewModel(gifRecordViewModel) + , m_widgetWindowBridge(widgetWindowBridge) +{ + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_DeleteOnClose, false); + setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); + + setStyleSheet(loadCaptureOverlayStyleSheet()); + + m_magnifier = new MagnifierWidget(this); + m_canvas = new StickyCanvasWidget(this); + m_canvas->hide(); + m_canvas->setViewScale(1.0); + m_canvas->setBackgroundVisible(false); + 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_numberLabel) { + m_numberLabel->setText(QString::number(value)); + } + if (m_numberAutoIncrementCheck) { + m_numberAutoIncrementCheck->setChecked(m_numberAutoIncrement); + } + }); + connect(m_canvas, &StickyCanvasWidget::textPlacementRequested, this, [this](const QPoint &point) { + m_pendingTextPoint = point; + if (!m_inlineTextEditor || !m_inlineTextPanel || !m_canvas) { + return; + } + m_inlineTextEditor->setText(m_annotationText); + const QRect canvasRect = m_canvas->geometry(); + const int panelW = 260; + const int panelH = 44; + int x = canvasRect.x() + point.x(); + int y = canvasRect.y() + point.y() + 6; + x = qMax(canvasRect.x(), qMin(x, canvasRect.right() - panelW + 1)); + y = qMax(canvasRect.y(), qMin(y, canvasRect.bottom() - panelH + 1)); + m_inlineTextPanel->setGeometry(x, y, panelW, panelH); + m_inlineTextPanel->show(); + m_inlineTextPanel->raise(); + m_inlineTextEditor->setFocus(); + m_inlineTextEditor->selectAll(); + }); + + m_toolOptions = new QWidget(this); + m_toolOptions->setObjectName(QStringLiteral("captureOverlayToolOptions")); + m_toolOptions->setAttribute(Qt::WA_StyledBackground, true); + m_toolOptions->hide(); + + 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_inlineTextPanel = new QWidget(this); + m_inlineTextPanel->setObjectName(QStringLiteral("captureOverlayInlineTextPanel")); + m_inlineTextPanel->setAttribute(Qt::WA_StyledBackground, true); + m_inlineTextPanel->hide(); + auto *inlineLayout = new QHBoxLayout(m_inlineTextPanel); + inlineLayout->setContentsMargins(6, 6, 6, 6); + inlineLayout->setSpacing(6); + + m_inlineTextEditor = new QLineEdit(m_inlineTextPanel); + m_inlineTextEditor->setObjectName(QStringLiteral("captureOverlayInlineTextEditor")); + m_inlineTextEditor->setPlaceholderText(tr("输入文字")); + m_inlineTextEditor->installEventFilter(this); + inlineLayout->addWidget(m_inlineTextEditor, 1); + + m_inlineTextConfirmButton = createToolbarButton(QStringLiteral(":/resource/img/lc_check.svg"), tr("确认"), m_inlineTextPanel); + m_inlineTextCancelButton = createToolbarButton(QStringLiteral(":/resource/img/lc_x.svg"), tr("取消"), m_inlineTextPanel); + inlineLayout->addWidget(m_inlineTextConfirmButton); + inlineLayout->addWidget(m_inlineTextCancelButton); + + const auto submitInlineText = [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); + } + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + }; + connect(m_inlineTextEditor, &QLineEdit::returnPressed, this, submitInlineText); + connect(m_inlineTextConfirmButton, &QToolButton::clicked, this, submitInlineText); + connect(m_inlineTextCancelButton, &QToolButton::clicked, this, [this]() { + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + }); + connect(m_inlineTextEditor, &QLineEdit::textChanged, this, [this](const QString &text) { + if (m_textInput && m_textInput->text() != text) { + m_textInput->setText(text); + } + }); + + 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); + m_numberValue = nextValue; + if (m_numberLabel) { + m_numberLabel->setText(QString::number(nextValue)); + } + if (m_canvas) { + m_canvas->setNumberValue(nextValue); + } + }); + + m_numberLabel = new QLabel(QString::number(m_numberValue), m_toolOptions); + m_numberLabel->setObjectName(QStringLiteral("captureOverlayNumberLabel")); + 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); + m_numberValue = nextValue; + if (m_numberLabel) { + m_numberLabel->setText(QString::number(nextValue)); + } + if (m_canvas) { + m_canvas->setNumberValue(nextValue); + } + }); + + m_toolbar = new QWidget(this); + m_toolbar->setObjectName(QStringLiteral("captureOverlayToolbar")); + m_toolbar->setAttribute(Qt::WA_StyledBackground, true); + auto *toolbarLayout = new QHBoxLayout(m_toolbar); + toolbarLayout->setContentsMargins(6, 5, 6, 5); + toolbarLayout->setSpacing(2); + + m_pencilButton = createToolbarButton(QStringLiteral(":/resource/img/lc_pencil.svg"), tr("画笔"), m_toolbar); + m_rectButton = createToolbarButton(QStringLiteral(":/resource/img/lc_square.svg"), tr("矩形"), m_toolbar); + m_circleButton = createToolbarButton(QStringLiteral(":/resource/img/lc_circle.svg"), tr("圆形"), m_toolbar); + m_arrowButton = createToolbarButton(QStringLiteral(":/resource/img/lc_arrow.svg"), tr("箭头"), m_toolbar); + m_mosaicButton = createToolbarButton(QStringLiteral(":/resource/img/lc_mosaic.svg"), tr("马赛克"), m_toolbar); + m_blurButton = createToolbarButton(QStringLiteral(":/resource/img/lc_blur.svg"), tr("高斯模糊"), m_toolbar); + m_textButton = createToolbarButton(QStringLiteral(":/resource/img/lc_text.svg"), tr("文字"), m_toolbar); + m_numberButton = createToolbarButton(QStringLiteral(":/resource/img/lc_list_ordered.svg"), tr("序号"), m_toolbar); + m_undoButton = createToolbarButton(QStringLiteral(":/resource/img/lc_undo.svg"), tr("撤销"), m_toolbar); + m_copyButton = createToolbarButton(QStringLiteral(":/resource/img/lc_copy.svg"), tr("复制"), m_toolbar); + m_saveButton = createToolbarButton(QStringLiteral(":/resource/img/lc_save.svg"), tr("保存"), m_toolbar); + m_stickyButton = createToolbarButton(QStringLiteral(":/resource/img/lc_pin.svg"), tr("贴图"), m_toolbar); + m_ocrButton = createToolbarButton(QStringLiteral(":/resource/img/lc_ocr.svg"), tr("OCR"), m_toolbar); + m_longCaptureButton = createToolbarButton(QStringLiteral(":/resource/img/lc_longshot.svg"), tr("长截图"), m_toolbar); + m_gifButton = createToolbarButton(QStringLiteral(":/resource/img/lc_gif_record.svg"), tr("录制 GIF"), m_toolbar); + m_cancelButton = createToolbarButton(QStringLiteral(":/resource/img/lc_x.svg"), tr("取消"), m_toolbar); + + const QVector drawButtons { + m_pencilButton, m_rectButton, m_circleButton, m_arrowButton, m_mosaicButton, m_blurButton, m_textButton, m_numberButton + }; + for (QToolButton *button : drawButtons) { + button->setCheckable(true); + toolbarLayout->addWidget(button); + } + toolbarLayout->addWidget(m_undoButton); + auto *divider = new QFrame(m_toolbar); + divider->setObjectName(QStringLiteral("captureOverlayToolbarDivider")); + divider->setFixedSize(1, 18); + toolbarLayout->addWidget(divider, 0, Qt::AlignVCenter); + toolbarLayout->addWidget(m_copyButton); + toolbarLayout->addWidget(m_saveButton); + toolbarLayout->addWidget(m_stickyButton); + toolbarLayout->addWidget(m_ocrButton); + toolbarLayout->addWidget(m_longCaptureButton); + toolbarLayout->addWidget(m_gifButton); + auto *divider2 = new QFrame(m_toolbar); + divider2->setObjectName(QStringLiteral("captureOverlayToolbarDivider")); + divider2->setFixedSize(1, 18); + toolbarLayout->addWidget(divider2, 0, Qt::AlignVCenter); + toolbarLayout->addWidget(m_cancelButton); + + m_toolbar->hide(); + + connect(m_copyButton, &QToolButton::clicked, this, [this]() { performAction(CaptureAction::Copy); }); + connect(m_saveButton, &QToolButton::clicked, this, [this]() { performAction(CaptureAction::Save); }); + connect(m_stickyButton, &QToolButton::clicked, this, [this]() { performAction(CaptureAction::Sticky); }); + connect(m_ocrButton, &QToolButton::clicked, this, [this]() { performAction(CaptureAction::Ocr); }); + connect(m_longCaptureButton, &QToolButton::clicked, this, [this]() { performAction(CaptureAction::LongCapture); }); + connect(m_gifButton, &QToolButton::clicked, this, [this]() { performAction(CaptureAction::Gif); }); + connect(m_cancelButton, &QToolButton::clicked, this, [this]() { finishCapture(); }); + connect(m_undoButton, &QToolButton::clicked, this, [this]() { + if (m_canvas) { + m_canvas->undo(); + } + }); + + const 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_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); }); + + const QList tipButtons { + m_pencilButton, + m_rectButton, + m_circleButton, + m_arrowButton, + m_mosaicButton, + m_blurButton, + m_textButton, + m_numberButton, + m_undoButton, + m_copyButton, + m_saveButton, + m_stickyButton, + m_ocrButton, + m_longCaptureButton, + m_gifButton, + m_cancelButton + }; + for (QToolButton *button : tipButtons) { + if (button) { + button->installEventFilter(this); + } + } + + m_gifBar = new QWidget(nullptr); + m_gifBar->setObjectName(QStringLiteral("captureOverlayGifBar")); + m_gifBar->setAttribute(Qt::WA_StyledBackground, true); + m_gifBar->setAttribute(Qt::WA_ShowWithoutActivating, true); + m_gifBar->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + m_gifBar->setFocusPolicy(Qt::StrongFocus); + m_gifBar->installEventFilter(this); + m_gifBar->setStyleSheet(loadCaptureOverlayStyleSheet()); + m_gifBar->setFixedWidth(252); + auto *gifLayout = new QVBoxLayout(m_gifBar); + gifLayout->setContentsMargins(6, 6, 8, 6); + gifLayout->setSpacing(5); + + auto *gifTopRow = new QHBoxLayout(); + gifTopRow->setContentsMargins(0, 0, 0, 0); + gifTopRow->setSpacing(4); + gifLayout->addLayout(gifTopRow); + + m_gifDot = new QLabel(m_gifBar); + m_gifDot->setObjectName(QStringLiteral("captureOverlayGifDot")); + m_gifDot->setFixedSize(8, 8); + gifTopRow->addWidget(m_gifDot, 0, Qt::AlignVCenter); + + m_gifStatusLabel = new QLabel(tr("00:00"), m_gifBar); + m_gifStatusLabel->setObjectName(QStringLiteral("captureOverlayGifStatus")); + m_gifStatusLabel->setAlignment(Qt::AlignCenter); + m_gifStatusLabel->setFixedWidth(96); + m_gifStatusLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + gifTopRow->addWidget(m_gifStatusLabel, 0, Qt::AlignVCenter); + + m_gifFrameLabel = new QLabel(tr("0f"), m_gifBar); + m_gifFrameLabel->setObjectName(QStringLiteral("captureOverlayGifFrames")); + m_gifFrameLabel->setAlignment(Qt::AlignCenter); + m_gifFrameLabel->setFixedWidth(42); + m_gifFrameLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + gifTopRow->addWidget(m_gifFrameLabel, 0, Qt::AlignVCenter); + + auto *gifDivider = new QFrame(m_gifBar); + gifDivider->setObjectName(QStringLiteral("captureOverlayToolbarDivider")); + gifDivider->setFixedSize(1, 18); + gifTopRow->addWidget(gifDivider, 0, Qt::AlignVCenter); + + m_gifStopButton = createToolbarButton(QStringLiteral(":/resource/img/lc_gif_stop.svg"), tr("停止录制"), m_gifBar); + m_gifCancelButton = createToolbarButton(QStringLiteral(":/resource/img/lc_x.svg"), tr("取消录制"), m_gifBar); + m_gifStopButton->setObjectName(QStringLiteral("captureOverlayGifStopButton")); + m_gifCancelButton->setObjectName(QStringLiteral("captureOverlayGifCancelButton")); + m_gifStopButton->setToolTip(QString()); + m_gifCancelButton->setToolTip(QString()); + gifTopRow->addWidget(m_gifStopButton); + gifTopRow->addWidget(m_gifCancelButton); + + m_gifProgressBar = new QProgressBar(m_gifBar); + m_gifProgressBar->setObjectName(QStringLiteral("captureOverlayGifProgress")); + m_gifProgressBar->setTextVisible(false); + m_gifProgressBar->setFixedHeight(6); + m_gifProgressBar->setRange(0, 100); + m_gifProgressBar->setValue(0); + m_gifProgressBar->hide(); + gifLayout->addWidget(m_gifProgressBar); + m_gifBar->hide(); + + m_gifBlinkTimer = new QTimer(this); + m_gifBlinkTimer->setInterval(500); + + m_gifBorderOverlay = new GifBorderOverlayWidget(nullptr); + m_gifBorderOverlay->setAttribute(Qt::WA_ShowWithoutActivating, true); + + m_tipBubble = new QWidget(this); + m_tipBubble->setObjectName(QStringLiteral("captureOverlayTipBubble")); + m_tipBubble->setAttribute(Qt::WA_TransparentForMouseEvents, true); + m_tipBubble->setAttribute(Qt::WA_StyledBackground, true); + auto *tipLayout = new QHBoxLayout(m_tipBubble); + tipLayout->setContentsMargins(14, 4, 14, 4); + tipLayout->setSpacing(0); + m_tipLabel = new QLabel(m_tipBubble); + m_tipLabel->setObjectName(QStringLiteral("captureOverlayTipLabel")); + tipLayout->addWidget(m_tipLabel); + m_tipBubble->hide(); + + connect(m_gifStopButton, &QToolButton::clicked, this, [this]() { + if (m_gifRecordViewModel) { + m_gifRecordViewModel->stopRecording(); + } + }); + connect(m_gifCancelButton, &QToolButton::clicked, this, [this]() { + if (m_gifRecordViewModel) { + m_gifRecordViewModel->cancelRecording(); + } + finishCapture(); + }); + + if (m_gifRecordViewModel) { + connect(m_gifRecordViewModel, &GifRecordViewModel::frameCountChanged, this, [this]() { + updateGifStatusText(); + }); + connect(m_gifRecordViewModel, &GifRecordViewModel::elapsedSecsChanged, this, [this]() { + updateGifStatusText(); + }); + connect(m_gifRecordViewModel, &GifRecordViewModel::isRecordingChanged, this, [this]() { + updateGifBarState(); + updateGifStatusText(); + updateGifDotState(); + }); + connect(m_gifRecordViewModel, &GifRecordViewModel::isEncodingChanged, this, [this]() { + updateGifBarState(); + updateGifStatusText(); + updateGifDotState(); + }); + connect(m_gifRecordViewModel, &GifRecordViewModel::encodingFinished, this, [this](const QString &) { + finishCapture(); + }); + connect(m_gifRecordViewModel, &GifRecordViewModel::encodingFailed, this, [this](const QString &) { + finishCapture(); + }); + } + + if (!m_colorButtons.isEmpty()) { + m_colorButtons.first()->setChecked(true); + } + for (QToolButton *sizeButton : m_sizeButtons) { + if (sizeButton->text() == tr("粗")) { + sizeButton->setChecked(true); + break; + } + } + syncCanvasToolSettings(); +} + +void CaptureOverlayWidget::showAndActivate(const QString &mode) +{ + setDefaultAction(mode); + refreshSnapshot(); + resetState(); + if (!m_virtualGeometry.isEmpty()) { + setGeometry(m_virtualGeometry); + } + show(); +#ifdef Q_OS_WIN + if (!m_virtualGeometry.isEmpty()) { + if (HWND hwnd = reinterpret_cast(winId())) { + SetWindowPos(hwnd, + HWND_TOPMOST, + m_virtualGeometry.x(), + m_virtualGeometry.y(), + m_virtualGeometry.width(), + m_virtualGeometry.height(), + SWP_NOOWNERZORDER | SWP_SHOWWINDOW); + } + } +#endif + raise(); + activateWindow(); + setFocus(Qt::ActiveWindowFocusReason); + if (m_magnifier) { + m_magnifier->hide(); + } +} + +void CaptureOverlayWidget::toggleCapture(const QString &mode) +{ + if (isVisible()) { + finishCapture(); + return; + } + showAndActivate(mode); +} + +void CaptureOverlayWidget::setWidgetWindowBridge(WidgetWindowBridge *widgetWindowBridge) +{ + m_widgetWindowBridge = widgetWindowBridge; +} + +void CaptureOverlayWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + if (m_screenCapture) { + painter.drawPixmap(rect(), m_screenCapture->desktopSnapshot()); + } else { + painter.fillRect(rect(), Qt::black); + } + + m_selection.paint(painter, rect()); + +} + +void CaptureOverlayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + if (m_canvas && m_canvas->isVisible()) { + m_canvas->setGeometry(rect()); + } +} + +void CaptureOverlayWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() != Qt::LeftButton) { + QWidget::mousePressEvent(event); + return; + } + + if (m_gifRecordingMode) { + event->accept(); + return; + } + + if (m_canvas && m_canvas->drawingEnabled()) { + event->ignore(); + return; + } + + m_toolbar->hide(); + if (m_toolOptions) { + m_toolOptions->hide(); + } + hideTipBubble(); + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + m_selection.beginInteraction(event->position().toPoint()); + setCursor(m_selection.cursorShape()); + update(); +} + +void CaptureOverlayWidget::mouseMoveEvent(QMouseEvent *event) +{ + const QPoint pos = event->position().toPoint(); + + if (m_gifRecordingMode) { + QWidget::mouseMoveEvent(event); + return; + } + + if (m_canvas && m_canvas->drawingEnabled()) { + QWidget::mouseMoveEvent(event); + return; + } + + if (event->buttons() & Qt::LeftButton) { + m_selection.updateInteraction(pos, size()); + setCursor(m_selection.cursorShape()); + updateToolbarGeometry(); + updateMagnifier(pos); + update(); + return; + } + + m_selection.updateHover(pos); + if (m_toolbar && !m_toolbar->isVisible()) { + updateMagnifier(pos); + } else if (m_magnifier) { + m_magnifier->hide(); + } + setCursor(m_selection.cursorShape()); + QWidget::mouseMoveEvent(event); +} + +void CaptureOverlayWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_canvas && m_canvas->drawingEnabled()) { + QWidget::mouseReleaseEvent(event); + return; + } + + if (event->button() == Qt::LeftButton) { + if (m_gifRecordingMode) { + event->accept(); + return; + } + m_selection.endInteraction(); + m_selection.updateHover(event->position().toPoint()); + setCursor(m_selection.cursorShape()); + if (m_selection.hasSelection()) { + refreshCanvasForSelection(false); + updateToolbarGeometry(); + m_toolbar->show(); + m_toolbar->raise(); + updateToolOptionsPanel(); + updateToolOptionsGeometry(); + } + if (m_magnifier) { + m_magnifier->hide(); + } + update(); + } + QWidget::mouseReleaseEvent(event); +} + +void CaptureOverlayWidget::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Escape: + if (m_gifRecordingMode && m_gifRecordViewModel) { + m_gifRecordViewModel->cancelRecording(); + } + finishCapture(); + event->accept(); + return; + case Qt::Key_Return: + case Qt::Key_Enter: + if (m_selection.hasSelection()) { + performAction(m_defaultAction); + } + event->accept(); + return; + default: + break; + } + QWidget::keyPressEvent(event); +} + +void CaptureOverlayWidget::closeEvent(QCloseEvent *event) +{ + if (m_screenCapture) { + m_screenCapture->releaseDesktopSnapshot(); + } + QWidget::closeEvent(event); +} + +bool CaptureOverlayWidget::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == m_inlineTextEditor && m_inlineTextEditor) { + if (event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + return true; + } + } + return QWidget::eventFilter(watched, event); + } + + if (watched == m_gifBar && m_gifBar) { + if (event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape && m_gifRecordViewModel) { + m_gifRecordViewModel->cancelRecording(); + finishCapture(); + return true; + } + } + return QWidget::eventFilter(watched, event); + } + + auto *button = qobject_cast(watched); + if (!button) { + return QWidget::eventFilter(watched, event); + } + + switch (event->type()) { + case QEvent::Enter: + showTipBubble(button); + break; + case QEvent::Leave: + case QEvent::MouseButtonPress: + case QEvent::Hide: + hideTipBubble(); + break; + default: + break; + } + + return QWidget::eventFilter(watched, event); +} + +void CaptureOverlayWidget::resetState() +{ + m_selection.reset(); + m_pendingTextPoint = QPoint(); + m_toolbar->hide(); + if (m_toolOptions) { + m_toolOptions->hide(); + } + hideTipBubble(); + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + if (m_canvas) { + m_canvas->setDrawingEnabled(false); + m_canvas->reset(); + m_canvas->hide(); + } + const QVector drawButtons { + m_pencilButton, m_rectButton, m_circleButton, m_arrowButton, m_mosaicButton, m_blurButton, m_textButton, m_numberButton + }; + for (QToolButton *button : drawButtons) { + if (button) { + button->setChecked(false); + } + } + if (m_numberLabel) { + m_numberLabel->setText(QString::number(m_numberValue)); + } + if (m_textInput) { + m_textInput->setText(m_annotationText); + } + if (m_gifBar) { + m_gifBar->hide(); + } + if (m_gifBorderOverlay) { + m_gifBorderOverlay->hide(); + } + if (m_magnifier) { + m_magnifier->hide(); + } + if (m_gifBlinkTimer) { + m_gifBlinkTimer->stop(); + } + m_gifRecordingMode = false; + m_gifCaptureRect = {}; + m_gifGlobalCaptureRect = {}; + m_gifDotVisible = true; + setCursor(Qt::CrossCursor); + update(); +} + +void CaptureOverlayWidget::refreshSnapshot() +{ + if (!m_screenCapture) { + return; + } + hide(); + qApp->processEvents(); + m_screenCapture->captureDesktop(); + m_virtualGeometry = m_screenCapture->virtualGeometry(); + if (m_canvas) { + m_canvas->setGeometry(QRect(QPoint(0, 0), size())); + m_canvas->setBackgroundImage(m_screenCapture->desktopSnapshot().toImage()); + } + if (m_magnifier) { + m_magnifier->setSnapshot(m_screenCapture->desktopSnapshot(), m_virtualGeometry); + } +} + +QRect CaptureOverlayWidget::selectionInGlobal() const +{ + const QRect selection = m_selection.rect(); + if (selection.isEmpty()) { + return {}; + } + return selection.translated(m_virtualGeometry.topLeft()); +} + +QRect CaptureOverlayWidget::currentSelectionRect() const +{ + return m_selection.rect().normalized(); +} + +QImage CaptureOverlayWidget::currentCaptureImage() const +{ + if (!m_screenCapture) { + return {}; + } + + const QRect globalRect = selectionInGlobal(); + if (globalRect.isEmpty()) { + return {}; + } + return m_screenCapture->captureRectToImage(globalRect); +} + +QImage CaptureOverlayWidget::currentOutputImage() const +{ + const QRect selection = currentSelectionRect(); + if (selection.isEmpty()) { + return {}; + } + + if (m_canvas && m_canvas->hasAnnotations()) { + const QImage composited = m_canvas->compositedImage(); + if (!composited.isNull()) { + return composited.copy(selection); + } + } + return currentCaptureImage(); +} + +void CaptureOverlayWidget::refreshCanvasForSelection(bool clearAnnotations) +{ + if (!m_canvas) { + return; + } + + if (clearAnnotations) { + m_canvas->reset(); + } + + if (!m_screenCapture) { + m_canvas->hide(); + return; + } + + const QPixmap snapshot = m_screenCapture->desktopSnapshot(); + if (snapshot.isNull()) { + m_canvas->hide(); + return; + } + + m_canvas->setGeometry(rect()); + m_canvas->setViewScale(1.0); + m_canvas->setBackgroundImage(snapshot.toImage()); + syncCanvasToolSettings(); + m_canvas->show(); + m_canvas->raise(); +} + +void CaptureOverlayWidget::setActiveTool(Shapeype type, QToolButton *button) +{ + const bool active = button && button->isChecked(); + if (!m_canvas) { + return; + } + + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + m_canvas->setDrawingEnabled(active); + if (active) { + m_canvas->setActiveShapeType(type); + } + syncCanvasToolSettings(); + updateToolOptionsPanel(); + updateToolOptionsGeometry(); +} + +void CaptureOverlayWidget::syncCanvasToolSettings() +{ + if (!m_canvas) { + return; + } + + m_canvas->setPenColor(m_currentPenColor); + m_canvas->setPenSize(m_currentPenSize); + m_canvas->setAnnotationText(m_annotationText); + m_canvas->setTextBackgroundEnabled(m_textBackgroundEnabled); + m_canvas->setNumberAutoIncrement(m_numberAutoIncrement); + m_canvas->setNumberValue(m_numberValue); +} + +void CaptureOverlayWidget::updateToolOptionsPanel() +{ + if (!m_toolOptions || !m_canvas) { + return; + } + + const bool visible = m_toolbar->isVisible() && m_canvas->drawingEnabled() && !m_gifRecordingMode; + m_toolOptions->setVisible(visible); + if (!visible) { + if (m_inlineTextPanel) { + m_inlineTextPanel->hide(); + } + return; + } + + const Shapeype type = m_canvas->activeShapeType(); + const bool showTextOptions = (type == TEXT); + const bool showNumberOptions = (type == NUMBER); + const bool showColorOptions = (type != MOSAIC && type != BLUR); + + int x = 10; + int y = 10; + for (QToolButton *button : m_colorButtons) { + button->setVisible(showColorOptions); + if (showColorOptions) { + button->setGeometry(x, y, 18, 18); + x += 24; + } + } + + y = showColorOptions ? (y + 26) : 10; + 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_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); + } +} + +QToolButton *CaptureOverlayWidget::currentActiveToolButton() const +{ + const QVector drawButtons { + m_pencilButton, m_rectButton, m_circleButton, m_arrowButton, m_mosaicButton, m_blurButton, m_textButton, m_numberButton + }; + for (QToolButton *button : drawButtons) { + if (button && button->isChecked()) { + return button; + } + } + return nullptr; +} + +void CaptureOverlayWidget::updateToolbarGeometry() +{ + const QRect selection = m_selection.rect(); + if (selection.isEmpty()) { + m_toolbar->hide(); + if (m_toolOptions) { + m_toolOptions->hide(); + } + return; + } + + const QSize hint = m_toolbar->sizeHint(); + const int x = qBound(8, selection.center().x() - hint.width() / 2, width() - hint.width() - 8); + int y = selection.bottom() + 12; + if (y + hint.height() > height() - 8) { + y = qMax(8, selection.top() - hint.height() - 12); + } + m_toolbar->setGeometry(x, y, hint.width(), hint.height()); + updateToolOptionsGeometry(); +} + +void CaptureOverlayWidget::updateGifBarGeometry() +{ + if (!m_gifBar || m_gifGlobalCaptureRect.isEmpty()) { + return; + } + + const QSize hint = m_gifBar->sizeHint(); + const QRect bounds = placementBoundsForGlobalRect(m_gifGlobalCaptureRect, m_virtualGeometry).adjusted(8, 8, -8, -8); + const int preferredX = m_gifGlobalCaptureRect.x() + m_gifGlobalCaptureRect.width() - hint.width(); + const int x = qBound(bounds.left(), + preferredX, + qMax(bounds.left(), bounds.right() - hint.width() + 1)); + int y = m_gifGlobalCaptureRect.bottom() + 12; + if (y + hint.height() > bounds.bottom() + 1) { + y = qMax(bounds.top(), m_gifGlobalCaptureRect.top() - hint.height() - 12); + } + const QPoint globalTopLeft(x, y); + m_gifBar->resize(hint); +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(m_gifBar->winId())) { + SetWindowPos(hwnd, + HWND_TOPMOST, + globalTopLeft.x(), + globalTopLeft.y(), + hint.width(), + hint.height(), + SWP_NOOWNERZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE); + } +#else + m_gifBar->setGeometry(globalTopLeft.x(), globalTopLeft.y(), hint.width(), hint.height()); +#endif + + if (m_gifBorderOverlay) { + const QRect borderRect = m_gifGlobalCaptureRect.adjusted(-3, -3, 3, 3); + m_gifBorderOverlay->resize(borderRect.size()); +#ifdef Q_OS_WIN + if (HWND borderHwnd = reinterpret_cast(m_gifBorderOverlay->winId())) { + SetWindowPos(borderHwnd, + HWND_TOPMOST, + borderRect.x(), + borderRect.y(), + borderRect.width(), + borderRect.height(), + SWP_NOOWNERZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE); + } +#else + m_gifBorderOverlay->setGeometry(borderRect); +#endif + } +} + +void CaptureOverlayWidget::updateGifStatusText() +{ + if (!m_gifRecordViewModel || !m_gifStatusLabel || !m_gifFrameLabel) { + return; + } + + if (m_gifRecordViewModel->isEncoding()) { + m_gifStatusLabel->setText(tr("生成中")); + m_gifFrameLabel->setText(QString()); + return; + } + + const int elapsedSecs = m_gifRecordViewModel->elapsedSecs(); + const int minutes = elapsedSecs / 60; + const int seconds = elapsedSecs % 60; + m_gifStatusLabel->setText(QStringLiteral("%1:%2") + .arg(minutes, 2, 10, QLatin1Char('0')) + .arg(seconds, 2, 10, QLatin1Char('0'))); + m_gifFrameLabel->setText(QStringLiteral("%1f").arg(m_gifRecordViewModel->frameCount())); +} + +void CaptureOverlayWidget::updateGifBarState() +{ + if (!m_gifRecordViewModel || !m_gifStopButton || !m_gifCancelButton || !m_gifProgressBar) { + return; + } + + const bool encoding = m_gifRecordViewModel->isEncoding(); + const bool recording = m_gifRecordViewModel->isRecording(); + m_gifStopButton->setVisible(recording && !encoding); + m_gifCancelButton->setVisible(!encoding); + m_gifFrameLabel->setVisible(!encoding); + m_gifProgressBar->setVisible(encoding); + if (encoding) { + m_gifProgressBar->setRange(0, 0); + } else { + m_gifProgressBar->setRange(0, 100); + m_gifProgressBar->setValue(recording ? 100 : 0); + } +} + +void CaptureOverlayWidget::updateGifDotState() +{ + if (!m_gifRecordViewModel || !m_gifDot || !m_gifBlinkTimer) { + return; + } + + if (m_gifRecordViewModel->isRecording() && !m_gifRecordViewModel->isEncoding()) { + m_gifBlinkTimer->stop(); + m_gifDotVisible = true; + m_gifDot->setVisible(true); + if (m_gifBorderOverlay && !m_gifBorderOverlay->isVisible()) { + m_gifBorderOverlay->show(); + m_gifBorderOverlay->raise(); + } + return; + } + + m_gifBlinkTimer->stop(); + m_gifDotVisible = true; + m_gifDot->setVisible(!m_gifRecordViewModel->isEncoding()); + if (m_gifBorderOverlay) { + m_gifBorderOverlay->hide(); + } +} + +void CaptureOverlayWidget::updateMagnifier(const QPoint &localPos) +{ + if (!m_magnifier || !m_screenCapture) { + return; + } + + const QPoint globalPos = localPos + m_virtualGeometry.topLeft(); + const QColor pixelColor = m_screenCapture->getPixelColor(globalPos.x(), globalPos.y()); + m_magnifier->updateView(localPos, globalPos, pixelColor, size()); +} + +void CaptureOverlayWidget::updateToolOptionsGeometry() +{ + if (!m_toolOptions || !m_toolOptions->isVisible()) { + return; + } + m_toolOptions->setGeometry(toolOptionsRect()); + m_toolOptions->raise(); +} + +QRect CaptureOverlayWidget::toolOptionsRect() const +{ + if (!m_toolOptions || !m_toolbar) { + return {}; + } + + const int width = 260; + const Shapeype type = m_canvas ? m_canvas->activeShapeType() : PEN; + int height = 88; + if (type == TEXT || type == NUMBER) { + height = 116; + } else if (type == MOSAIC || type == BLUR) { + height = 48; + } + int x = m_toolbar->geometry().center().x() - width / 2; + if (QToolButton *anchor = currentActiveToolButton()) { + const QPoint anchorTopLeft = anchor->mapTo(this, QPoint(0, 0)); + x = anchorTopLeft.x() + anchor->width() / 2 - width / 2; + } + x = qBound(8, x, qMax(8, this->width() - width - 8)); + int y = m_toolbar->geometry().bottom() + 8; + if (y + height > this->height() - 8) { + y = qMax(8, m_toolbar->geometry().top() - height - 8); + } + return QRect(x, y, width, height); +} + +void CaptureOverlayWidget::showTipBubble(QToolButton *button) +{ + if (!button || !m_tipBubble || !m_tipLabel || !button->isVisible()) { + return; + } + + const QString tipText = button->toolTip(); + if (tipText.isEmpty()) { + hideTipBubble(); + return; + } + + m_tipLabel->setText(tipText); + m_tipBubble->adjustSize(); + + const QPoint topLeft = button->mapTo(this, QPoint(0, 0)); + const QRect buttonRect(topLeft, button->size()); + const QPoint center = buttonRect.center(); + int x = center.x() - m_tipBubble->width() / 2; + x = qBound(8, x, width() - m_tipBubble->width() - 8); + + int y = buttonRect.bottom() + 8; + if (y + m_tipBubble->height() > height() - 8) { + y = buttonRect.top() - m_tipBubble->height() - 8; + } + + m_tipBubble->move(x, y); + m_tipBubble->show(); + m_tipBubble->raise(); +} + +void CaptureOverlayWidget::hideTipBubble() +{ + if (m_tipBubble) { + m_tipBubble->hide(); + } +} + +void CaptureOverlayWidget::setDefaultAction(const QString &mode) +{ + if (mode == QStringLiteral("save")) { + m_defaultAction = CaptureAction::Save; + } else if (mode == QStringLiteral("sticky")) { + m_defaultAction = CaptureAction::Sticky; + } else if (mode == QStringLiteral("ocr")) { + m_defaultAction = CaptureAction::Ocr; + } else { + m_defaultAction = CaptureAction::Copy; + } +} + +void CaptureOverlayWidget::performAction(CaptureAction action) +{ + const QRect globalRect = selectionInGlobal(); + if (globalRect.isEmpty() || !m_screenCapture) { + return; + } + + switch (action) { + case CaptureAction::Copy: + m_screenCapture->copyImageToClipboard(currentOutputImage()); + break; + case CaptureAction::Save: { + const QImage image = currentOutputImage(); + if (!image.isNull()) { + const QString filePath = QFileDialog::getSaveFileName(this, + tr("保存截图"), + defaultSaveFilePath(), + tr("PNG 图片 (*.png)")); + if (!filePath.isEmpty()) { + image.save(filePath); + } + } + break; + } + case CaptureAction::Sticky: { + const QImage image = currentOutputImage(); + if (!image.isNull() && m_stickyViewModel) { + m_stickyViewModel->requestStickyImage(image, globalRect); + } + break; + } + case CaptureAction::Ocr: + if (m_ocrViewModel) { + const QImage image = m_screenCapture->captureRectToImage(globalRect); + if (!image.isNull()) { + m_ocrViewModel->recognize(image); + } + } + break; + case CaptureAction::LongCapture: + if (m_widgetWindowBridge) { + m_widgetWindowBridge->requestLongCapture(globalRect); + } + finishCapture(); + return; + case CaptureAction::Gif: + if (!m_gifRecordViewModel) { + break; + } + m_gifCaptureRect = m_selection.rect(); + m_gifGlobalCaptureRect = globalRect; + m_gifRecordingMode = true; + m_toolbar->hide(); + if (m_toolOptions) { + m_toolOptions->hide(); + } + if (m_canvas) { + m_canvas->setDrawingEnabled(false); + } + hideTipBubble(); + if (m_magnifier) { + m_magnifier->hide(); + } + updateGifBarState(); + updateGifStatusText(); + updateGifDotState(); + m_gifBar->show(); + m_gifBar->raise(); + hide(); + qApp->processEvents(); + updateGifBarGeometry(); + m_gifRecordViewModel->startRecording(globalRect.x(), globalRect.y(), globalRect.width(), globalRect.height()); + return; + } + + finishCapture(); +} + +void CaptureOverlayWidget::finishCapture() +{ + hide(); + hideTipBubble(); + if (m_screenCapture) { + m_screenCapture->releaseDesktopSnapshot(); + } + resetState(); +} + +QString CaptureOverlayWidget::defaultSaveFilePath() const +{ + QString baseDir; + if (m_storageViewModel) { + baseDir = m_storageViewModel->savePath().toLocalFile(); + } + if (baseDir.isEmpty()) { + baseDir = QDir::homePath(); + } + const QString fileName = QStringLiteral("TZshot_%1.png") + .arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss"))); + return QDir(baseDir).filePath(fileName); +} diff --git a/src/widgets/capture_overlay_widget.h b/src/widgets/capture_overlay_widget.h new file mode 100644 index 0000000..494cab4 --- /dev/null +++ b/src/widgets/capture_overlay_widget.h @@ -0,0 +1,160 @@ +#ifndef CAPTURE_OVERLAY_WIDGET_H +#define CAPTURE_OVERLAY_WIDGET_H + +#include +#include +#include +#include +#include +#include + +#include "paint_board/shape/shape_type.h" +#include "widgets/selection_mask_controller.h" + +class QToolButton; +class QLabel; +class QProgressBar; +class QTimer; +class QCheckBox; +class QLineEdit; +class ScreenshotViewModel; +class StickyViewModel; +class StorageViewModel; +class OcrViewModel; +class GifRecordViewModel; +class MagnifierWidget; +class WidgetWindowBridge; +class StickyCanvasWidget; + +class CaptureOverlayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CaptureOverlayWidget(ScreenshotViewModel *screenCapture, + StickyViewModel *stickyViewModel, + StorageViewModel *storageViewModel, + OcrViewModel *ocrViewModel, + GifRecordViewModel *gifRecordViewModel = nullptr, + WidgetWindowBridge *widgetWindowBridge = nullptr, + QWidget *parent = nullptr); + + void showAndActivate(const QString &mode = QStringLiteral("copy")); + void toggleCapture(const QString &mode = QStringLiteral("copy")); + void setWidgetWindowBridge(WidgetWindowBridge *widgetWindowBridge); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + enum class CaptureAction { + Copy, + Save, + Sticky, + Ocr, + LongCapture, + Gif + }; + + void resetState(); + void refreshSnapshot(); + QRect selectionInGlobal() const; + QRect currentSelectionRect() const; + QImage currentCaptureImage() const; + QImage currentOutputImage() const; + void refreshCanvasForSelection(bool clearAnnotations); + void setActiveTool(Shapeype type, QToolButton *button); + void syncCanvasToolSettings(); + void updateToolOptionsPanel(); + QToolButton *currentActiveToolButton() const; + void updateToolbarGeometry(); + void updateGifBarGeometry(); + void updateGifStatusText(); + void updateGifBarState(); + void updateGifDotState(); + void updateMagnifier(const QPoint &localPos); + void updateToolOptionsGeometry(); + QRect toolOptionsRect() const; + void showTipBubble(QToolButton *button); + void hideTipBubble(); + void setDefaultAction(const QString &mode); + void performAction(CaptureAction action); + void finishCapture(); + QString defaultSaveFilePath() const; + + QPointer m_screenCapture; + QPointer m_stickyViewModel; + QPointer m_storageViewModel; + QPointer m_ocrViewModel; + QPointer m_gifRecordViewModel; + QPointer m_widgetWindowBridge; + + QRect m_virtualGeometry; + CaptureAction m_defaultAction = CaptureAction::Copy; + SelectionMaskController m_selection; + bool m_gifRecordingMode = false; + QRect m_gifCaptureRect; + QRect m_gifGlobalCaptureRect; + + QWidget *m_toolbar = nullptr; + StickyCanvasWidget *m_canvas = nullptr; + QWidget *m_toolOptions = nullptr; + QWidget *m_inlineTextPanel = nullptr; + QLineEdit *m_inlineTextEditor = nullptr; + QToolButton *m_inlineTextConfirmButton = nullptr; + QToolButton *m_inlineTextCancelButton = 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; + QToolButton *m_copyButton = nullptr; + QToolButton *m_saveButton = nullptr; + QToolButton *m_stickyButton = nullptr; + QToolButton *m_ocrButton = nullptr; + QToolButton *m_longCaptureButton = nullptr; + QToolButton *m_gifButton = nullptr; + QToolButton *m_cancelButton = nullptr; + QToolButton *m_pencilButton = nullptr; + QToolButton *m_rectButton = nullptr; + QToolButton *m_circleButton = nullptr; + QToolButton *m_arrowButton = nullptr; + QToolButton *m_mosaicButton = nullptr; + QToolButton *m_blurButton = nullptr; + QToolButton *m_textButton = nullptr; + QToolButton *m_numberButton = nullptr; + QToolButton *m_undoButton = nullptr; + QVector m_colorButtons; + QVector m_sizeButtons; + QColor m_currentPenColor = QColor("#F43F5E"); + int m_currentPenSize = 6; + QString m_annotationText = QStringLiteral("文本"); + bool m_textBackgroundEnabled = true; + bool m_numberAutoIncrement = true; + int m_numberValue = 1; + QPoint m_pendingTextPoint; + + QWidget *m_gifBar = nullptr; + QWidget *m_gifBorderOverlay = nullptr; + QLabel *m_gifDot = nullptr; + QLabel *m_gifStatusLabel = nullptr; + QLabel *m_gifFrameLabel = nullptr; + QProgressBar *m_gifProgressBar = nullptr; + QToolButton *m_gifStopButton = nullptr; + QToolButton *m_gifCancelButton = nullptr; + QTimer *m_gifBlinkTimer = nullptr; + bool m_gifDotVisible = true; + MagnifierWidget *m_magnifier = nullptr; + QWidget *m_tipBubble = nullptr; + QLabel *m_tipLabel = nullptr; +}; + +#endif // CAPTURE_OVERLAY_WIDGET_H diff --git a/src/widgets/long_capture_controller.cpp b/src/widgets/long_capture_controller.cpp new file mode 100644 index 0000000..1942af6 --- /dev/null +++ b/src/widgets/long_capture_controller.cpp @@ -0,0 +1,388 @@ +#include "widgets/long_capture_controller.h" + +#include "viewmodel/scroll_capture_view_model.h" + +#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 +#endif + +namespace { + +QString loadLongCaptureStyleSheet() +{ + QFile file(QStringLiteral(":/resource/qss/long_capture_controller.qss")); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + return QString::fromUtf8(file.readAll()); +} + +class LongCaptureFrameWidget : public QWidget +{ +public: + explicit LongCaptureFrameWidget(QWidget *parent = nullptr) + : QWidget(parent) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_TranslucentBackground, true); + setAttribute(Qt::WA_TransparentForMouseEvents, true); + setFocusPolicy(Qt::NoFocus); + } + +protected: + void paintEvent(QPaintEvent *) override + { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, false); + QPen pen(QColor(QStringLiteral("#0EA5E9"))); + pen.setWidth(2); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + painter.drawRect(rect().adjusted(1, 1, -1, -1)); + painter.fillRect(QRect(0, 0, 6, 6), QColor(QStringLiteral("#0EA5E9"))); + painter.fillRect(QRect(width() - 6, 0, 6, 6), QColor(QStringLiteral("#0EA5E9"))); + painter.fillRect(QRect(0, height() - 6, 6, 6), QColor(QStringLiteral("#0EA5E9"))); + painter.fillRect(QRect(width() - 6, height() - 6, 6, 6), QColor(QStringLiteral("#0EA5E9"))); + } +}; + +void placeTopMost(QWidget *widget, const QRect &geometry) +{ + if (!widget) { + return; + } + widget->setGeometry(geometry); +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(widget->winId())) { + SetWindowPos(hwnd, + HWND_TOPMOST, + geometry.x(), + geometry.y(), + geometry.width(), + geometry.height(), + SWP_NOOWNERZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE); + } +#endif +} + +QPixmap decodePreviewPixmap(const QString &previewUrl) +{ + if (!previewUrl.startsWith(QStringLiteral("data:image/png;base64,"))) { + return {}; + } + const QByteArray data = QByteArray::fromBase64(previewUrl.mid(22).toLatin1()); + QPixmap pixmap; + pixmap.loadFromData(data, "PNG"); + return pixmap; +} + +} + +LongCaptureController::LongCaptureController(ScrollCaptureViewModel *scrollCaptureViewModel, + QObject *parent) + : QObject(parent) + , m_scrollCaptureViewModel(scrollCaptureViewModel) +{ + if (!m_scrollCaptureViewModel) { + return; + } + + connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::captureStarted, this, [this]() { + setRunning(true); + updateStatusText(tr("滚动检测中,滑动内容会自动拼接")); + }); + connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::captureSucceeded, this, [this](const QString &) { + closeAll(); + }); + connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::captureFailed, this, [this](const QString &) { + closeAll(); + }); + connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::capturedFramesChanged, this, [this]() { + updateFrameCount(m_scrollCaptureViewModel->capturedFrames()); + }); + connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::statusTextChanged, this, [this]() { + updateStatusText(m_scrollCaptureViewModel->statusText()); + }); + connect(m_scrollCaptureViewModel, &ScrollCaptureViewModel::previewImageUrlChanged, this, [this]() { + updatePreviewImage(m_scrollCaptureViewModel->previewImageUrl()); + }); +} + +void LongCaptureController::beginCapture(const QRect &captureRect) +{ + m_captureRect = captureRect.normalized(); + if (m_captureRect.isEmpty()) { + return; + } + + ensureWidgets(); + setRunning(false); + updateFrameCount(0); + updateStatusText(tr("点击开始后,滑动内容自动拼接")); + updatePreviewImage(QString()); + updateWidgetGeometry(); + + if (m_barWidget) { + m_barWidget->show(); + m_barWidget->raise(); + } + if (m_previewWidget) { + m_previewWidget->show(); + m_previewWidget->raise(); + } + if (m_frameWidget) { + m_frameWidget->show(); + m_frameWidget->raise(); + } +} + +bool LongCaptureController::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched); + if (event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + if (m_running && m_scrollCaptureViewModel) { + m_scrollCaptureViewModel->stop(); + } else { + closeAll(); + } + return true; + } + } + return QObject::eventFilter(watched, event); +} + +void LongCaptureController::closeAll() +{ + setRunning(false); + if (m_barWidget) { + m_barWidget->hide(); + } + if (m_previewWidget) { + m_previewWidget->hide(); + } + if (m_frameWidget) { + m_frameWidget->hide(); + } + m_captureRect = {}; +} + +void LongCaptureController::ensureWidgets() +{ + if (!m_barWidget) { + m_barWidget = new QWidget(nullptr); + m_barWidget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); + m_barWidget->setAttribute(Qt::WA_ShowWithoutActivating, true); + m_barWidget->installEventFilter(this); + m_barWidget->setObjectName(QStringLiteral("longCaptureBar")); + m_barWidget->setStyleSheet(loadLongCaptureStyleSheet()); + m_barWidget->setFixedSize(500, 56); + + auto *layout = new QHBoxLayout(m_barWidget); + layout->setContentsMargins(10, 10, 10, 10); + layout->setSpacing(10); + + auto *badge = new QLabel(QStringLiteral("长"), m_barWidget); + badge->setObjectName(QStringLiteral("longCaptureBadge")); + badge->setAlignment(Qt::AlignCenter); + badge->setFixedSize(26, 26); + layout->addWidget(badge); + + m_barMessageLabel = new QLabel(tr("点击开始后,滑动内容自动拼接"), m_barWidget); + m_barMessageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + layout->addWidget(m_barMessageLabel, 1); + + m_barFrameLabel = new QLabel(tr("0 帧"), m_barWidget); + m_barFrameLabel->setObjectName(QStringLiteral("longCaptureFrameLabel")); + layout->addWidget(m_barFrameLabel); + + m_startButton = new QPushButton(tr("开始"), m_barWidget); + m_startButton->setObjectName(QStringLiteral("longCapturePrimaryBtn")); + m_startButton->setFixedSize(72, 30); + layout->addWidget(m_startButton); + + m_stopButton = new QPushButton(tr("关闭"), m_barWidget); + m_stopButton->setObjectName(QStringLiteral("longCaptureDangerBtn")); + m_stopButton->setFixedSize(72, 30); + layout->addWidget(m_stopButton); + + connect(m_startButton, &QPushButton::clicked, this, [this]() { + if (m_captureRect.isEmpty() || !m_scrollCaptureViewModel) { + return; + } + setRunning(true); + updateStatusText(tr("滚动检测中,滑动内容会自动拼接")); + m_scrollCaptureViewModel->start(m_captureRect); + }); + connect(m_stopButton, &QPushButton::clicked, this, [this]() { + if (m_running && m_scrollCaptureViewModel) { + m_scrollCaptureViewModel->stop(); + } else { + closeAll(); + } + }); + } + + if (!m_previewWidget) { + m_previewWidget = new QWidget(nullptr); + m_previewWidget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); + m_previewWidget->setAttribute(Qt::WA_ShowWithoutActivating, true); + m_previewWidget->installEventFilter(this); + m_previewWidget->setObjectName(QStringLiteral("longCapturePreview")); + m_previewWidget->setStyleSheet(loadLongCaptureStyleSheet()); + m_previewWidget->setFixedSize(260, 420); + + auto *layout = new QVBoxLayout(m_previewWidget); + layout->setContentsMargins(10, 10, 10, 10); + layout->setSpacing(8); + + auto *headerLayout = new QHBoxLayout(); + headerLayout->setContentsMargins(0, 0, 0, 0); + headerLayout->setSpacing(8); + auto *title = new QLabel(tr("长截图预览"), m_previewWidget); + title->setObjectName(QStringLiteral("longCapturePreviewTitle")); + headerLayout->addWidget(title); + headerLayout->addStretch(1); + m_previewFrameLabel = new QLabel(tr("0 帧"), m_previewWidget); + m_previewFrameLabel->setObjectName(QStringLiteral("longCapturePreviewSub")); + headerLayout->addWidget(m_previewFrameLabel); + layout->addLayout(headerLayout); + + auto *imageBox = new QWidget(m_previewWidget); + imageBox->setObjectName(QStringLiteral("longCapturePreviewImageBox")); + imageBox->setFixedHeight(352); + auto *imageLayout = new QVBoxLayout(imageBox); + imageLayout->setContentsMargins(6, 6, 6, 6); + imageLayout->setSpacing(0); + m_previewImageLabel = new QLabel(imageBox); + m_previewImageLabel->setObjectName(QStringLiteral("longCapturePreviewEmpty")); + m_previewImageLabel->setAlignment(Qt::AlignCenter); + m_previewImageLabel->setText(tr("暂无预览")); + imageLayout->addWidget(m_previewImageLabel, 1); + layout->addWidget(imageBox, 1); + + m_previewStatusLabel = new QLabel(tr("等待开始"), m_previewWidget); + m_previewStatusLabel->setObjectName(QStringLiteral("longCapturePreviewStatus")); + m_previewStatusLabel->setWordWrap(false); + layout->addWidget(m_previewStatusLabel); + } + + if (!m_frameWidget) { + m_frameWidget = new LongCaptureFrameWidget(nullptr); + m_frameWidget->setAttribute(Qt::WA_ShowWithoutActivating, true); + m_frameWidget->installEventFilter(this); + } +} + +void LongCaptureController::updateWidgetGeometry() +{ + if (m_captureRect.isEmpty()) { + return; + } + + QScreen *screen = QGuiApplication::screenAt(m_captureRect.center()); + QRect workArea; + if (screen) { + workArea = screen->availableGeometry(); + } else if (QScreen *primary = QGuiApplication::primaryScreen()) { + workArea = primary->availableGeometry(); + } + if (workArea.isEmpty()) { + workArea = QRect(0, 0, 1920, 1080); + } + + if (m_barWidget) { + const QSize size = m_barWidget->size(); + const int x = qBound(workArea.left(), + m_captureRect.x() + m_captureRect.width() - size.width(), + qMax(workArea.left(), workArea.right() - size.width() + 1)); + int y = m_captureRect.bottom() + 12; + if (y + size.height() > workArea.bottom() + 1) { + y = qMax(workArea.top(), m_captureRect.top() - size.height() - 12); + } + placeTopMost(m_barWidget, QRect(x, y, size.width(), size.height())); + } + + if (m_previewWidget) { + const QSize size = m_previewWidget->size(); + const int x = qMax(workArea.left(), workArea.right() - size.width() - 26); + const int y = qMax(workArea.top() + 20, workArea.center().y() - size.height() / 2); + placeTopMost(m_previewWidget, QRect(x, y, size.width(), size.height())); + } + + if (m_frameWidget) { + const QRect frameRect = m_captureRect.adjusted(-2, -2, 2, 2); + placeTopMost(m_frameWidget, frameRect); + } +} + +void LongCaptureController::updateFrameCount(int value) +{ + if (m_barFrameLabel) { + m_barFrameLabel->setText(tr("%1 帧").arg(value)); + } + if (m_previewFrameLabel) { + m_previewFrameLabel->setText(tr("%1 帧").arg(value)); + } +} + +void LongCaptureController::updateStatusText(const QString &text) +{ + if (m_barMessageLabel) { + m_barMessageLabel->setText(text); + } + if (m_previewStatusLabel) { + m_previewStatusLabel->setText(text); + } +} + +void LongCaptureController::updatePreviewImage(const QString &previewUrl) +{ + if (!m_previewImageLabel) { + return; + } + const QPixmap pixmap = decodePreviewPixmap(previewUrl); + if (pixmap.isNull()) { + m_previewImageLabel->setPixmap(QPixmap()); + m_previewImageLabel->setText(tr("暂无预览")); + m_previewImageLabel->setObjectName(QStringLiteral("longCapturePreviewEmpty")); + m_previewImageLabel->style()->unpolish(m_previewImageLabel); + m_previewImageLabel->style()->polish(m_previewImageLabel); + return; + } + + m_previewImageLabel->setText(QString()); + m_previewImageLabel->setObjectName(QString()); + m_previewImageLabel->style()->unpolish(m_previewImageLabel); + m_previewImageLabel->style()->polish(m_previewImageLabel); + m_previewImageLabel->setPixmap(pixmap.scaled(236, 340, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +void LongCaptureController::setRunning(bool running) +{ + m_running = running; + if (m_startButton) { + m_startButton->setVisible(!running); + } + if (m_stopButton) { + m_stopButton->setText(running ? tr("完成") : tr("关闭")); + } +} diff --git a/src/widgets/long_capture_controller.h b/src/widgets/long_capture_controller.h new file mode 100644 index 0000000..7bfb50a --- /dev/null +++ b/src/widgets/long_capture_controller.h @@ -0,0 +1,54 @@ +#ifndef LONG_CAPTURE_CONTROLLER_H +#define LONG_CAPTURE_CONTROLLER_H + +#include +#include +#include + +class QLabel; +class QPushButton; +class QWidget; +class ScrollCaptureViewModel; + +class LongCaptureController : public QObject +{ + Q_OBJECT + +public: + explicit LongCaptureController(ScrollCaptureViewModel *scrollCaptureViewModel, + QObject *parent = nullptr); + + void beginCapture(const QRect &captureRect); + void closeAll(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + void ensureWidgets(); + void updateWidgetGeometry(); + void updateFrameCount(int value); + void updateStatusText(const QString &text); + void updatePreviewImage(const QString &previewUrl); + void setRunning(bool running); + + QPointer m_scrollCaptureViewModel; + QRect m_captureRect; + + QWidget *m_barWidget = nullptr; + QWidget *m_previewWidget = nullptr; + QWidget *m_frameWidget = nullptr; + + QLabel *m_barMessageLabel = nullptr; + QLabel *m_barFrameLabel = nullptr; + QPushButton *m_startButton = nullptr; + QPushButton *m_stopButton = nullptr; + + QLabel *m_previewFrameLabel = nullptr; + QLabel *m_previewStatusLabel = nullptr; + QLabel *m_previewImageLabel = nullptr; + + bool m_running = false; +}; + +#endif // LONG_CAPTURE_CONTROLLER_H diff --git a/src/widgets/magnifier_widget.cpp b/src/widgets/magnifier_widget.cpp new file mode 100644 index 0000000..5e67561 --- /dev/null +++ b/src/widgets/magnifier_widget.cpp @@ -0,0 +1,147 @@ +#include "widgets/magnifier_widget.h" + +#include + +namespace { +constexpr int kFrameSize = 130; +constexpr int kInfoHeight = 48; +constexpr int kOuterWidth = 130; +constexpr int kOuterHeight = 180; +constexpr int kLensPadding = 10; +constexpr int kSampleRadius = 11; +constexpr int kOffset = 15; +} + +MagnifierWidget::MagnifierWidget(QWidget *parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_TransparentForMouseEvents, true); + setAttribute(Qt::WA_NoSystemBackground, true); + setFixedSize(kOuterWidth, kOuterHeight); + hide(); +} + +void MagnifierWidget::setSnapshot(const QPixmap &snapshot, const QRect &virtualGeometry) +{ + m_snapshot = snapshot; + m_virtualGeometry = virtualGeometry; + update(); +} + +void MagnifierWidget::updateView(const QPoint &localPoint, + const QPoint &globalPoint, + const QColor &pixelColor, + const QSize &overlaySize) +{ + m_localPoint = localPoint; + m_globalPoint = globalPoint; + m_pixelColor = pixelColor; + + int x = localPoint.x() + kOffset; + if (x + width() > overlaySize.width()) { + x = localPoint.x() - width() - kOffset; + } + int y = localPoint.y() + kOffset; + if (y + height() > overlaySize.height()) { + y = localPoint.y() - height() - kOffset; + } + move(x, y); + show(); + raise(); + update(); +} + +void MagnifierWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + + const QRect frameRect(0, 0, kFrameSize, kFrameSize); + const QRect infoRect(0, kFrameSize + 2, kOuterWidth, kInfoHeight); + + painter.setPen(QPen(Qt::white, 1)); + painter.setBrush(QColor(26, 26, 26)); + painter.drawRoundedRect(frameRect, 4, 4); + + const QRect innerRect(kLensPadding, kLensPadding, 110, 110); + if (!m_snapshot.isNull()) { + const QRect srcRect(m_localPoint.x() - kSampleRadius, + m_localPoint.y() - kSampleRadius, + kSampleRadius * 2, + kSampleRadius * 2); + const QSize sampleSize(srcRect.width(), srcRect.height()); + QPixmap region(sampleSize); + region.fill(Qt::black); + + QPainter regionPainter(®ion); + const QRect bounded = srcRect.intersected(m_snapshot.rect()); + if (!bounded.isEmpty()) { + const int destX = bounded.x() - srcRect.x(); + const int destY = bounded.y() - srcRect.y(); + regionPainter.drawPixmap(destX, + destY, + m_snapshot, + bounded.x(), + bounded.y(), + bounded.width(), + bounded.height()); + } + regionPainter.end(); + + painter.setRenderHint(QPainter::SmoothPixmapTransform, false); + painter.drawPixmap(innerRect, region, region.rect()); + + const int sampleCount = sampleSize.width(); + const qreal cellW = qreal(innerRect.width()) / qreal(sampleCount); + const qreal cellH = qreal(innerRect.height()) / qreal(sampleCount); + painter.setPen(QPen(QColor(255, 255, 255, 38), 1)); + for (int i = 1; i < sampleCount; ++i) { + const int x = qRound(innerRect.left() + i * cellW); + const int y = qRound(innerRect.top() + i * cellH); + painter.drawLine(x, innerRect.top(), x, innerRect.bottom()); + painter.drawLine(innerRect.left(), y, innerRect.right(), y); + } + + painter.setPen(QPen(QColor(255, 0, 0, 220), 1)); + const int centerX = innerRect.center().x(); + const int centerY = innerRect.center().y(); + painter.drawLine(centerX, innerRect.top(), centerX, innerRect.bottom()); + painter.drawLine(innerRect.left(), centerY, innerRect.right(), centerY); + } + + painter.setPen(QPen(QColor("#555555"), 1)); + painter.setBrush(QColor(26, 26, 26, 204)); + painter.drawRoundedRect(infoRect, 4, 4); + + const QRect swatchRect(10, infoRect.y() + 11, 16, 16); + painter.setPen(QPen(Qt::white, 1)); + painter.setBrush(m_pixelColor); + painter.drawRoundedRect(swatchRect, 2, 2); + + painter.setPen(Qt::white); + QFont textFont = painter.font(); + textFont.setPixelSize(11); + textFont.setFamily(QStringLiteral("Consolas")); + painter.setFont(textFont); + const QString rgbText = QStringLiteral("R:%1 G:%2 B:%3") + .arg(m_pixelColor.red()) + .arg(m_pixelColor.green()) + .arg(m_pixelColor.blue()); + painter.drawText(QRect(34, infoRect.y() + 4, 90, 18), + Qt::AlignLeft | Qt::AlignVCenter, + rgbText); + + painter.setPen(QColor("#CBD5E1")); + QFont coordFont = painter.font(); + coordFont.setPixelSize(10); + coordFont.setFamily(QStringLiteral("Consolas")); + painter.setFont(coordFont); + const QString pointText = QStringLiteral("X:%1 Y:%2") + .arg(m_globalPoint.x()) + .arg(m_globalPoint.y()); + painter.drawText(QRect(34, infoRect.y() + 22, 90, 16), + Qt::AlignLeft | Qt::AlignVCenter, + pointText); +} diff --git a/src/widgets/magnifier_widget.h b/src/widgets/magnifier_widget.h new file mode 100644 index 0000000..d1e5cf7 --- /dev/null +++ b/src/widgets/magnifier_widget.h @@ -0,0 +1,30 @@ +#ifndef MAGNIFIER_WIDGET_H +#define MAGNIFIER_WIDGET_H + +#include +#include +#include +#include + +class MagnifierWidget : public QWidget +{ + Q_OBJECT + +public: + explicit MagnifierWidget(QWidget *parent = nullptr); + + void setSnapshot(const QPixmap &snapshot, const QRect &virtualGeometry); + void updateView(const QPoint &localPoint, const QPoint &globalPoint, const QColor &pixelColor, const QSize &overlaySize); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QPixmap m_snapshot; + QRect m_virtualGeometry; + QPoint m_localPoint; + QPoint m_globalPoint; + QColor m_pixelColor = Qt::black; +}; + +#endif // MAGNIFIER_WIDGET_H diff --git a/src/widgets/selection_mask_controller.cpp b/src/widgets/selection_mask_controller.cpp new file mode 100644 index 0000000..9f89240 --- /dev/null +++ b/src/widgets/selection_mask_controller.cpp @@ -0,0 +1,386 @@ +#include "widgets/selection_mask_controller.h" + +#include +#include + +namespace { + +bool nearValue(int value, int target, int margin) +{ + return value >= target - margin && value <= target + margin; +} + +bool isResizeMode(SelectionMaskController::DragMode mode) +{ + return mode != SelectionMaskController::DragMode::None + && mode != SelectionMaskController::DragMode::Selecting + && mode != SelectionMaskController::DragMode::Moving; +} + +} + +void SelectionMaskController::reset() +{ + m_rect = QRect(); + m_pressRect = QRect(); + m_pressPos = QPoint(); + m_dragMode = DragMode::None; + m_hoverMode = DragMode::None; +} + +void SelectionMaskController::beginInteraction(const QPoint &pos) +{ + m_pressPos = pos; + m_pressRect = rect(); + + if (hasSelection()) { + const DragMode hit = hitTest(pos); + if (hit != DragMode::None) { + m_dragMode = hit; + return; + } + } + + m_dragMode = DragMode::Selecting; + m_rect = QRect(pos, pos); +} + +void SelectionMaskController::updateInteraction(const QPoint &pos, const QSize &bounds) +{ + const QPoint clampedPos = clampPointToBounds(pos, bounds); + + switch (m_dragMode) { + case DragMode::Selecting: + m_rect = clampRectToBounds(QRect(m_pressPos, clampedPos).normalized(), bounds); + break; + case DragMode::Moving: { + QRect moved = m_pressRect.translated(clampedPos - m_pressPos); + if (moved.left() < 0) moved.moveLeft(0); + if (moved.top() < 0) moved.moveTop(0); + if (moved.right() > bounds.width()) moved.moveRight(bounds.width()); + if (moved.bottom() > bounds.height()) moved.moveBottom(bounds.height()); + m_rect = moved; + break; + } + case DragMode::ResizeTopLeft: { + QRect r = m_pressRect; + r.setTopLeft(clampedPos); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeTop: { + QRect r = m_pressRect; + r.setTop(clampedPos.y()); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeTopRight: { + QRect r = m_pressRect; + r.setTopRight(clampedPos); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeRight: { + QRect r = m_pressRect; + r.setRight(clampedPos.x()); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeBottomRight: { + QRect r = m_pressRect; + r.setBottomRight(clampedPos); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeBottom: { + QRect r = m_pressRect; + r.setBottom(clampedPos.y()); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeBottomLeft: { + QRect r = m_pressRect; + r.setBottomLeft(clampedPos); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::ResizeLeft: { + QRect r = m_pressRect; + r.setLeft(clampedPos.x()); + m_rect = clampRectToBounds(r.normalized(), bounds); + break; + } + case DragMode::None: + break; + } +} + +void SelectionMaskController::endInteraction() +{ + if (m_rect.width() < 2 || m_rect.height() < 2) { + m_rect = QRect(); + } + m_dragMode = DragMode::None; +} + +void SelectionMaskController::updateHover(const QPoint &pos) +{ + if (!hasSelection()) { + m_hoverMode = DragMode::None; + return; + } + m_hoverMode = hitTest(pos); +} + +Qt::CursorShape SelectionMaskController::cursorShape() const +{ + const DragMode mode = (m_dragMode != DragMode::None) ? m_dragMode : m_hoverMode; + switch (mode) { + case DragMode::Moving: return Qt::SizeAllCursor; + case DragMode::ResizeTopLeft: + case DragMode::ResizeBottomRight: return Qt::SizeFDiagCursor; + case DragMode::ResizeTopRight: + case DragMode::ResizeBottomLeft: return Qt::SizeBDiagCursor; + case DragMode::ResizeTop: + case DragMode::ResizeBottom: return Qt::SizeVerCursor; + case DragMode::ResizeLeft: + case DragMode::ResizeRight: return Qt::SizeHorCursor; + case DragMode::Selecting: + case DragMode::None: + return Qt::CrossCursor; + } + return Qt::CrossCursor; +} + +void SelectionMaskController::paint(QPainter &painter, const QRect &canvasRect) const +{ + const QRect selection = rect(); + const DragMode activeMode = (m_dragMode != DragMode::None) ? m_dragMode : m_hoverMode; + + painter.setBrush(QColor(0, 0, 0, 120)); + painter.setPen(Qt::NoPen); + + QPainterPath path; + path.addRect(canvasRect.adjusted(-1, -1, 1, 1)); + if (!selection.isEmpty()) { + path.addRect(selection); + } + painter.drawPath(path); + + if (selection.isEmpty()) { + return; + } + + painter.setBrush(Qt::NoBrush); + painter.setPen(QPen(QColor(255, 255, 255, 235), 2.0)); + painter.drawRect(selection); + painter.setPen(QPen(QColor(37, 99, 235, 235), activeMode == DragMode::Moving ? 1.6 : 1.2)); + painter.drawRect(selection); + + const QString text = QStringLiteral("X:%1 Y:%2 宽:%3 高:%4") + .arg(selection.x()).arg(selection.y()) + .arg(selection.width()).arg(selection.height()); + QFont font = painter.font(); + font.setPointSizeF(font.pointSizeF() > 0 ? font.pointSizeF() + 1.5 : 12.5); + font.setBold(true); + painter.setFont(font); + const QFontMetrics fm(font); + const int textWidth = fm.horizontalAdvance(text); + const int textHeight = fm.height(); + + QRect labelRect; + if (selection.y() - 25 >= 0) { + labelRect = QRect(selection.x(), selection.y() - 32, textWidth + 22, textHeight + 10); + } else { + labelRect = QRect(selection.x() + 2, selection.y() + 2, textWidth + 22, textHeight + 10); + } + + painter.setPen(QPen(QColor(125, 211, 252, 210), 1)); + painter.setBrush(QColor(15, 23, 42, 210)); + painter.drawRoundedRect(labelRect, 5, 5); + painter.setPen(Qt::white); + painter.setBrush(Qt::NoBrush); + painter.drawText(labelRect, Qt::AlignCenter, text); + + const QList handleModes { + DragMode::ResizeTopLeft, + DragMode::ResizeTop, + DragMode::ResizeTopRight, + DragMode::ResizeRight, + DragMode::ResizeBottomRight, + DragMode::ResizeBottom, + DragMode::ResizeBottomLeft, + DragMode::ResizeLeft + }; + + painter.setPen(Qt::NoPen); + for (DragMode mode : handleModes) { + const QRect r = handleRect(mode); + if (r.isEmpty()) { + continue; + } + + const bool highlighted = (activeMode == mode); + const QRect outerRect = r.adjusted(-1, -1, 1, 1); + painter.setBrush(QColor(255, 255, 255, highlighted ? 255 : 235)); + painter.setPen(Qt::NoPen); + painter.drawEllipse(outerRect); + painter.setBrush(highlighted ? QColor("#2563EB") : QColor("#0EA5E9")); + painter.drawEllipse(r); + if (highlighted) { + painter.setBrush(QColor(37, 99, 235, 50)); + painter.drawEllipse(outerRect.adjusted(-2, -2, 2, 2)); + } + } + + if (activeMode != DragMode::None && activeMode != DragMode::Selecting && activeMode != DragMode::Moving) { + painter.setPen(QPen(QColor(37, 99, 235, 180), 1, Qt::DashLine)); + switch (activeMode) { + case DragMode::ResizeTop: + painter.drawLine(selection.topLeft(), selection.topRight()); + break; + case DragMode::ResizeBottom: + painter.drawLine(selection.bottomLeft(), selection.bottomRight()); + break; + case DragMode::ResizeLeft: + painter.drawLine(selection.topLeft(), selection.bottomLeft()); + break; + case DragMode::ResizeRight: + painter.drawLine(selection.topRight(), selection.bottomRight()); + break; + case DragMode::ResizeTopLeft: + painter.drawLine(selection.topLeft(), selection.topRight()); + painter.drawLine(selection.topLeft(), selection.bottomLeft()); + break; + case DragMode::ResizeTopRight: + painter.drawLine(selection.topLeft(), selection.topRight()); + painter.drawLine(selection.topRight(), selection.bottomRight()); + break; + case DragMode::ResizeBottomLeft: + painter.drawLine(selection.topLeft(), selection.bottomLeft()); + painter.drawLine(selection.bottomLeft(), selection.bottomRight()); + break; + case DragMode::ResizeBottomRight: + painter.drawLine(selection.topRight(), selection.bottomRight()); + painter.drawLine(selection.bottomLeft(), selection.bottomRight()); + break; + default: + break; + } + } +} + +QRect SelectionMaskController::clampRectToBounds(const QRect &rect, const QSize &bounds) +{ + QRect r = rect; + if (r.left() < 0) r.moveLeft(0); + if (r.top() < 0) r.moveTop(0); + if (r.right() > bounds.width()) r.setRight(bounds.width()); + if (r.bottom() > bounds.height()) r.setBottom(bounds.height()); + return r.normalized(); +} + +QPoint SelectionMaskController::clampPointToBounds(const QPoint &point, const QSize &bounds) +{ + return QPoint(qBound(0, point.x(), bounds.width()), + qBound(0, point.y(), bounds.height())); +} + +SelectionMaskController::DragMode SelectionMaskController::hitTest(const QPoint &pos) const +{ + const QRect r = rect(); + if (r.isEmpty()) { + return DragMode::None; + } + + const int left = r.left(); + const int top = r.top(); + const int right = r.right(); + const int bottom = r.bottom(); + const int x = pos.x(); + const int y = pos.y(); + + if (nearValue(x, left, m_hitMargin) && nearValue(y, top, m_hitMargin)) { + return DragMode::ResizeTopLeft; + } + if (nearValue(y, top, m_hitMargin) && x >= left && x <= right) { + return DragMode::ResizeTop; + } + if (nearValue(x, right, m_hitMargin) && nearValue(y, top, m_hitMargin)) { + return DragMode::ResizeTopRight; + } + if (nearValue(x, right, m_hitMargin) && y >= top && y <= bottom) { + return DragMode::ResizeRight; + } + if (nearValue(x, right, m_hitMargin) && nearValue(y, bottom, m_hitMargin)) { + return DragMode::ResizeBottomRight; + } + if (nearValue(y, bottom, m_hitMargin) && x >= left && x <= right) { + return DragMode::ResizeBottom; + } + if (nearValue(x, left, m_hitMargin) && nearValue(y, bottom, m_hitMargin)) { + return DragMode::ResizeBottomLeft; + } + if (nearValue(x, left, m_hitMargin) && y >= top && y <= bottom) { + return DragMode::ResizeLeft; + } + if (isMoveHit(pos)) { + return DragMode::Moving; + } + return DragMode::None; +} + +bool SelectionMaskController::isMoveHit(const QPoint &pos) const +{ + const QRect r = rect(); + if (r.isEmpty()) { + return false; + } + return pos.x() > r.left() + 1 + && pos.y() > r.top() + 1 + && pos.x() < r.right() - 1 + && pos.y() < r.bottom() - 1; +} + +QRect SelectionMaskController::handleRect(DragMode mode) const +{ + const QRect r = rect(); + if (r.isEmpty() || !isResizeMode(mode)) { + return {}; + } + + QPoint center; + switch (mode) { + case DragMode::ResizeTopLeft: + center = r.topLeft(); + break; + case DragMode::ResizeTop: + center = QPoint(r.center().x(), r.top()); + break; + case DragMode::ResizeTopRight: + center = r.topRight(); + break; + case DragMode::ResizeRight: + center = QPoint(r.right(), r.center().y()); + break; + case DragMode::ResizeBottomRight: + center = r.bottomRight(); + break; + case DragMode::ResizeBottom: + center = QPoint(r.center().x(), r.bottom()); + break; + case DragMode::ResizeBottomLeft: + center = r.bottomLeft(); + break; + case DragMode::ResizeLeft: + center = QPoint(r.left(), r.center().y()); + break; + default: + return {}; + } + + return QRect(center.x() - m_handleSize / 2, + center.y() - m_handleSize / 2, + m_handleSize, + m_handleSize); +} diff --git a/src/widgets/selection_mask_controller.h b/src/widgets/selection_mask_controller.h new file mode 100644 index 0000000..7f105f4 --- /dev/null +++ b/src/widgets/selection_mask_controller.h @@ -0,0 +1,60 @@ +#ifndef SELECTION_MASK_CONTROLLER_H +#define SELECTION_MASK_CONTROLLER_H + +#include +#include +#include +#include + +class QPainter; + +class SelectionMaskController +{ +public: + enum class DragMode { + None, + Selecting, + Moving, + ResizeTopLeft, + ResizeTop, + ResizeTopRight, + ResizeRight, + ResizeBottomRight, + ResizeBottom, + ResizeBottomLeft, + ResizeLeft + }; + + void reset(); + + QRect rect() const { return m_rect.normalized(); } + QRect rawRect() const { return m_rect; } + bool hasSelection() const { return !m_rect.normalized().isEmpty(); } + DragMode dragMode() const { return m_dragMode; } + + void beginInteraction(const QPoint &pos); + void updateInteraction(const QPoint &pos, const QSize &bounds); + void endInteraction(); + void updateHover(const QPoint &pos); + + Qt::CursorShape cursorShape() const; + void paint(QPainter &painter, const QRect &canvasRect) const; + +private: + static QRect clampRectToBounds(const QRect &rect, const QSize &bounds); + static QPoint clampPointToBounds(const QPoint &point, const QSize &bounds); + + DragMode hitTest(const QPoint &pos) const; + bool isMoveHit(const QPoint &pos) const; + QRect handleRect(DragMode mode) const; + + QRect m_rect; + QRect m_pressRect; + QPoint m_pressPos; + DragMode m_dragMode = DragMode::None; + DragMode m_hoverMode = DragMode::None; + int m_hitMargin = 8; + int m_handleSize = 10; +}; + +#endif // SELECTION_MASK_CONTROLLER_H diff --git a/src/widgets/sticky_canvas_widget.cpp b/src/widgets/sticky_canvas_widget.cpp index 292d00d..ce69869 100644 --- a/src/widgets/sticky_canvas_widget.cpp +++ b/src/widgets/sticky_canvas_widget.cpp @@ -32,6 +32,15 @@ void StickyCanvasWidget::setBackgroundImage(const QImage &image) update(); } +void StickyCanvasWidget::setBackgroundVisible(bool visible) +{ + if (m_backgroundVisible == visible) { + return; + } + m_backgroundVisible = visible; + update(); +} + void StickyCanvasWidget::setViewScale(qreal scale) { const qreal next = qMax(0.01, scale); @@ -110,6 +119,39 @@ void StickyCanvasWidget::addTextAnnotation(const QPoint &point, const QString &t update(); } +QImage StickyCanvasWidget::compositedImage() const +{ + if (size().isEmpty()) { + return {}; + } + + QImage image(size(), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing, true); + + if (!m_backgroundImage.isNull()) { + painter.drawImage(QRect(QPoint(0, 0), size()), m_backgroundImage); + } else { + painter.fillRect(image.rect(), Qt::white); + } + + 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(); + + return image; +} + void StickyCanvasWidget::undo() { if (!m_shapes.isEmpty()) { @@ -131,9 +173,11 @@ void StickyCanvasWidget::paintEvent(QPaintEvent *event) Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); - painter.fillRect(rect(), Qt::white); + if (m_backgroundVisible) { + painter.fillRect(rect(), Qt::white); + } - if (!m_backgroundImage.isNull()) { + if (m_backgroundVisible && !m_backgroundImage.isNull()) { painter.drawImage(rect(), m_backgroundImage); } diff --git a/src/widgets/sticky_canvas_widget.h b/src/widgets/sticky_canvas_widget.h index fd043bc..7c9d762 100644 --- a/src/widgets/sticky_canvas_widget.h +++ b/src/widgets/sticky_canvas_widget.h @@ -19,6 +19,7 @@ public: ~StickyCanvasWidget() override; void setBackgroundImage(const QImage &image); + void setBackgroundVisible(bool visible); void setViewScale(qreal scale); void setActiveShapeType(Shapeype type); void setPenColor(const QColor &color); @@ -30,6 +31,8 @@ public: void setNumberAutoIncrement(bool enabled); bool drawingEnabled() const { return m_drawingEnabled; } Shapeype activeShapeType() const { return m_shapeType; } + bool hasAnnotations() const { return !m_shapes.isEmpty(); } + QImage compositedImage() const; void addTextAnnotation(const QPoint &point, const QString &text); void undo(); void reset(); @@ -52,6 +55,7 @@ private: QList m_shapes; Shape *m_currentShape = nullptr; QImage m_backgroundImage; + bool m_backgroundVisible = true; QColor m_penColor = Qt::red; int m_penSize = 6; Shapeype m_shapeType = PEN; diff --git a/src/widgets/widget_window_bridge.cpp b/src/widgets/widget_window_bridge.cpp index 4eec9eb..593b515 100644 --- a/src/widgets/widget_window_bridge.cpp +++ b/src/widgets/widget_window_bridge.cpp @@ -1,10 +1,17 @@ #include "widgets/widget_window_bridge.h" +#include "widgets/capture_overlay_widget.h" +#include "widgets/long_capture_controller.h" #include "widgets/settings_dialog.h" -WidgetWindowBridge::WidgetWindowBridge(SettingsDialog *settingsDialog, QObject *parent) +WidgetWindowBridge::WidgetWindowBridge(SettingsDialog *settingsDialog, + CaptureOverlayWidget *captureOverlay, + LongCaptureController *longCaptureController, + QObject *parent) : QObject(parent) , m_settingsDialog(settingsDialog) + , m_captureOverlay(captureOverlay) + , m_longCaptureController(longCaptureController) { } @@ -14,3 +21,24 @@ void WidgetWindowBridge::showSettings() m_settingsDialog->showAndActivate(); } } + +void WidgetWindowBridge::showCaptureOverlay(const QString &mode) +{ + if (m_captureOverlay) { + m_captureOverlay->showAndActivate(mode); + } +} + +void WidgetWindowBridge::toggleCaptureOverlay(const QString &mode) +{ + if (m_captureOverlay) { + m_captureOverlay->toggleCapture(mode); + } +} + +void WidgetWindowBridge::requestLongCapture(const QRect &captureRect) +{ + if (m_longCaptureController) { + m_longCaptureController->beginCapture(captureRect); + } +} diff --git a/src/widgets/widget_window_bridge.h b/src/widgets/widget_window_bridge.h index 1f590be..8cfe9f3 100644 --- a/src/widgets/widget_window_bridge.h +++ b/src/widgets/widget_window_bridge.h @@ -2,20 +2,32 @@ #define WIDGET_WINDOW_BRIDGE_H #include +#include +#include class SettingsDialog; +class CaptureOverlayWidget; +class LongCaptureController; class WidgetWindowBridge : public QObject { Q_OBJECT public: - explicit WidgetWindowBridge(SettingsDialog *settingsDialog, QObject *parent = nullptr); + explicit WidgetWindowBridge(SettingsDialog *settingsDialog, + CaptureOverlayWidget *captureOverlay, + LongCaptureController *longCaptureController, + QObject *parent = nullptr); Q_INVOKABLE void showSettings(); + Q_INVOKABLE void showCaptureOverlay(const QString &mode = QStringLiteral("copy")); + Q_INVOKABLE void toggleCaptureOverlay(const QString &mode = QStringLiteral("copy")); + Q_INVOKABLE void requestLongCapture(const QRect &captureRect); private: SettingsDialog *m_settingsDialog = nullptr; + CaptureOverlayWidget *m_captureOverlay = nullptr; + LongCaptureController *m_longCaptureController = nullptr; }; #endif // WIDGET_WINDOW_BRIDGE_H