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