Migrate capture overlay to widgets
This commit is contained in:
1 parent
3e345b0aca
commit
409fdcb91e
72 files changed
+3607
-7832
No files matched your search
+16
-27
@@ -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}
|
||||
|
||||
+2
-732
@@ -1,4 +1,4 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TS version="2.1" language="en_US">
|
||||
<context>
|
||||
<name>GifRecordViewModel</name>
|
||||
@@ -62,734 +62,4 @@
|
||||
<translation />
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZAboutWin</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="9" />
|
||||
<source>关于 TZshot</source>
|
||||
<translation>About TZshot</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="63" />
|
||||
<source>版本 v1.0.0</source>
|
||||
<translation>Version v1.0.0</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="83" />
|
||||
<source>简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。</source>
|
||||
<translation>A lightweight screenshot and annotation tool with AI editing, mosaic, and more.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="96" />
|
||||
<source>© 2025 TZshot. All rights reserved.</source>
|
||||
<translation>© 2025 TZshot. All rights reserved.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="112" />
|
||||
<source>关闭</source>
|
||||
<translation>Close</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZChat</name>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="83" />
|
||||
<source>AI 图像编辑</source>
|
||||
<translation>AI Image Edit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="162" />
|
||||
<source>当前贴图</source>
|
||||
<translation>Current Image</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="171" />
|
||||
<source>未关联贴图</source>
|
||||
<translation>No Image Linked</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="178" />
|
||||
<source>发送后将对此图执行 AI 编辑</source>
|
||||
<translation>AI edit will be applied to this image after sending</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="203" />
|
||||
<source>描述您想要的修改...</source>
|
||||
<translation>Describe the changes you want...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="228" />
|
||||
<source>取消</source>
|
||||
<translation>Cancel</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="245" />
|
||||
<source>发送</source>
|
||||
<translation>Send</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZGeneralSettings</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="35" />
|
||||
<source>大模型设置</source>
|
||||
<translation>AI Model Settings</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="63" />
|
||||
<source>大模型选择</source>
|
||||
<translation>Select Model</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71" />
|
||||
<source>阿里云百炼</source>
|
||||
<translation>Alibaba Cloud Bailian</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71" />
|
||||
<source>火山引擎</source>
|
||||
<translation>Volcano Engine</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="124" />
|
||||
<source>请输入 API Key</source>
|
||||
<translation>Enter API Key</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="148" />
|
||||
<source>保存</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="183" />
|
||||
<source>语言设置</source>
|
||||
<translation>Language</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="206" />
|
||||
<source>界面语言</source>
|
||||
<translation>UI Language</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="241" />
|
||||
<source>截图保存路径</source>
|
||||
<translation>Screenshot Save Path</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="264" />
|
||||
<source>保存路径</source>
|
||||
<translation>Save Path</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="316" />
|
||||
<source>浏览</source>
|
||||
<translation>Browse</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZGifRecordBar</name>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZGifRecordBar.qml" line="186" />
|
||||
<source>正在生成 GIF...</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZHotkeySettings</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="49" />
|
||||
<source>编辑快捷键 · %1</source>
|
||||
<translation>Edit Shortcut · %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="70" />
|
||||
<source>请按下新的快捷键组合...</source>
|
||||
<translation>Press the new shortcut key combination...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="103" />
|
||||
<source>取消</source>
|
||||
<translation>Cancel</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="133" />
|
||||
<source>确认</source>
|
||||
<translation>Confirm</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="228" />
|
||||
<source>截图</source>
|
||||
<translation>Screenshot</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="229" />
|
||||
<source>截图并保存</source>
|
||||
<translation>Screenshot & Save</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="230" />
|
||||
<source>贴图到桌面</source>
|
||||
<translation>Pin to Desktop</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150" />
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="231" />
|
||||
<source>显示/隐藏窗口</source>
|
||||
<translation>Show/Hide Window</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="201" />
|
||||
<source>全局快捷键</source>
|
||||
<translation>Global Shortcuts</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="293" />
|
||||
<source>编辑</source>
|
||||
<translation>Edit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="341" />
|
||||
<source>说明</source>
|
||||
<translation>Notes</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="362" />
|
||||
<source>快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。
|
||||
若与系统或其他应用快捷键冲突,请修改为其他组合。</source>
|
||||
<translation>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.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZMarkToolInfo</name>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="13" />
|
||||
<source>文本</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="62" />
|
||||
<source>细</source>
|
||||
<translation>Thin</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="63" />
|
||||
<source>中</source>
|
||||
<translation>Medium</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="64" />
|
||||
<source>粗</source>
|
||||
<translation>Thick</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="65" />
|
||||
<source>特粗</source>
|
||||
<translation>Extra Thick</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="133" />
|
||||
<source>输入文字</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="140" />
|
||||
<source>背景</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="152" />
|
||||
<source>模糊</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="177" />
|
||||
<source>自动递增</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="187" />
|
||||
<source>数字</source>
|
||||
<translation type="unfinished" />
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZOcrResult</name>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="67" />
|
||||
<source>OCR 识别结果</source>
|
||||
<translation>OCR Results</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="167" />
|
||||
<source>正在识别中…</source>
|
||||
<translation>Recognizing…</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="190" />
|
||||
<source>识别结果将显示在这里…</source>
|
||||
<translation>Recognition results will appear here…</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="239" />
|
||||
<source>复制全部</source>
|
||||
<translation>Copy All</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="265" />
|
||||
<source>关闭</source>
|
||||
<translation>Close</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="293" />
|
||||
<source>已复制!</source>
|
||||
<translation>Copied!</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZRightPanel</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZRightPanel.qml" line="90" />
|
||||
<source>通用设置</source>
|
||||
<translation>General</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZRightPanel.qml" line="137" />
|
||||
<source>快捷键设置</source>
|
||||
<translation>Shortcuts</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZSettingWin</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZSettingWin.qml" line="11" />
|
||||
<source>设置</source>
|
||||
<translation>Settings</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZSettingWin.qml" line="29" />
|
||||
<source>选择保存位置</source>
|
||||
<translation>Choose Save Location</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZStickyWindow</name>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="515" />
|
||||
<source>当前透明度 %1%</source>
|
||||
<translation>Opacity: %1%</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="518" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="572" />
|
||||
<source>覆盖贴图(合并标注)</source>
|
||||
<translation>Overwrite Image (Merge Annotations)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="523" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="558" />
|
||||
<source>显示工具栏</source>
|
||||
<translation>Show Toolbar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="534" />
|
||||
<source>透明度 +5%</source>
|
||||
<translation>Opacity +5%</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="535" />
|
||||
<source>透明度 -5%</source>
|
||||
<translation>Opacity -5%</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="541" />
|
||||
<source>旋转 90°</source>
|
||||
<translation>Rotate 90°</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="548" />
|
||||
<source>重置位置</source>
|
||||
<translation>Reset Position</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="520" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="566" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="581" />
|
||||
<source>AI 编辑</source>
|
||||
<translation>AI Edit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>保存图片</source>
|
||||
<translation type="vanished">Save Image</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="519" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="573" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="580" />
|
||||
<source>复制贴图</source>
|
||||
<translation>Copy Image</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="530" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="583" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="588" />
|
||||
<source>关闭窗口</source>
|
||||
<translation>Close Window</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="570" />
|
||||
<source>保存原图</source>
|
||||
<translation>Save Original Image</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="571" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="602" />
|
||||
<source>另存为(合并标注)</source>
|
||||
<translation>Save As (Merge Annotations)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="578" />
|
||||
<source>常用项置顶</source>
|
||||
<translation>Pin Common Items</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="579" />
|
||||
<source>覆盖贴图</source>
|
||||
<translation>Overwrite Image</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="582" />
|
||||
<source>显示/隐藏工具栏</source>
|
||||
<translation>Show/Hide Toolbar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="593" />
|
||||
<source>选择保存位置</source>
|
||||
<translation>Choose Save Location</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="341" />
|
||||
<source>Pencil</source>
|
||||
<translation>Pencil</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="350" />
|
||||
<source>Rectangle</source>
|
||||
<translation>Rectangle</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="359" />
|
||||
<source>Circle</source>
|
||||
<translation>Circle</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="368" />
|
||||
<source>Arrow</source>
|
||||
<translation>Arrow</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="382" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="383" />
|
||||
<source>Mosaic</source>
|
||||
<translation>Mosaic</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="404" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="539" />
|
||||
<source>缩小</source>
|
||||
<translation>Zoom Out</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="406" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="540" />
|
||||
<source>放大</source>
|
||||
<translation>Zoom In</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="407" />
|
||||
<source>旋转90°</source>
|
||||
<translation>Rotate 90°</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="408" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="542" />
|
||||
<source>镜像</source>
|
||||
<translation>Mirror</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="409" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="543" />
|
||||
<source>恢复原始大小</source>
|
||||
<translation>Reset Size</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="415" />
|
||||
<source>完成并复制</source>
|
||||
<translation>Apply and Copy</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="422" />
|
||||
<source>撤销</source>
|
||||
<translation>Undo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="434" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="435" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="523" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="558" />
|
||||
<source>隐藏工具栏</source>
|
||||
<translation>Hide Toolbar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>序号</source>
|
||||
<translation>Number</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>旋转90°</source>
|
||||
<translation>Rotate 90°</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>恢复原始大小</source>
|
||||
<translation>Reset Size</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>隐藏工具栏</source>
|
||||
<translation>Hide Toolbar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="324" />
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="325" />
|
||||
<source>OCR</source>
|
||||
<translation>OCR</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZTool</name>
|
||||
<message>
|
||||
<source>确认截图</source>
|
||||
<translation type="vanished">Confirm</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="83" />
|
||||
<source>取消</source>
|
||||
<translation>Cancel</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>保存到文件</source>
|
||||
<translation type="vanished">Save to File</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>贴图到桌面</source>
|
||||
<translation type="vanished">Pin to Desktop</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>矩形标注</source>
|
||||
<translation type="vanished">Rectangle</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>椭圆标注</source>
|
||||
<translation type="vanished">Ellipse</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>箭头标注</source>
|
||||
<translation type="vanished">Arrow</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="76" />
|
||||
<source>完成</source>
|
||||
<translation>Done</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="100" />
|
||||
<source>录制GIF</source>
|
||||
<translation>Record GIF</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="109" />
|
||||
<source>保存</source>
|
||||
<translation>Save</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="116" />
|
||||
<source>贴图</source>
|
||||
<translation>Pin</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="127" />
|
||||
<source>矩形</source>
|
||||
<translation>Rectangle</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="136" />
|
||||
<source>圆形</source>
|
||||
<translation>Circle</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="145" />
|
||||
<source>箭头</source>
|
||||
<translation>Arrow</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="154" />
|
||||
<source>画笔</source>
|
||||
<translation>Pen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="163" />
|
||||
<source>高亮</source>
|
||||
<translation>Highlight</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="172" />
|
||||
<source>马赛克</source>
|
||||
<translation>Mosaic</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="181" />
|
||||
<source>文字</source>
|
||||
<translation>Text</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="190" />
|
||||
<source>序号</source>
|
||||
<translation>Number</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="197" />
|
||||
<source>撤销</source>
|
||||
<translation>Undo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>序号</source>
|
||||
<translation>Number</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="92" />
|
||||
<source>OCR</source>
|
||||
<translation>OCR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>录制GIF</source>
|
||||
<translation>Record GIF</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>马赛克</source>
|
||||
<translation type="vanished">Mosaic</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZWindow</name>
|
||||
<message>
|
||||
<source>选择保存位置</source>
|
||||
<translation type="vanished">Choose Save Location</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>保存成功</source>
|
||||
<translation type="vanished">Saved</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>截图已保存到文件</source>
|
||||
<translation type="vanished">Screenshot saved to file</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>保存失败</source>
|
||||
<translation type="vanished">Save Failed</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>截图文件保存失败</source>
|
||||
<translation type="vanished">Failed to save screenshot file</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="563" />
|
||||
<source>%1 * %2</source>
|
||||
<translation>%1 * %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>截图完成</source>
|
||||
<translation type="vanished">Done</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>已复制到剪贴板</source>
|
||||
<translation type="vanished">Copied to clipboard</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>截图失败</source>
|
||||
<translation type="vanished">Screenshot Failed</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>截图复制到剪贴板失败</source>
|
||||
<translation type="vanished">Failed to copy screenshot to clipboard</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="134" />
|
||||
<source>Capture done</source>
|
||||
<translation>Capture done</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="134" />
|
||||
<source>Copied to clipboard</source>
|
||||
<translation>Copied to clipboard</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="136" />
|
||||
<source>Capture failed</source>
|
||||
<translation>Capture failed</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="136" />
|
||||
<source>Failed to copy image</source>
|
||||
<translation>Failed to copy image</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="434" />
|
||||
<source>Select save location</source>
|
||||
<translation>Select save location</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="442" />
|
||||
<source>Save success</source>
|
||||
<translation>Save success</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="442" />
|
||||
<source>Screenshot saved to file</source>
|
||||
<translation>Screenshot saved to file</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="445" />
|
||||
<source>Save failed</source>
|
||||
<translation>Save failed</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="445" />
|
||||
<source>Failed to save screenshot file</source>
|
||||
<translation>Failed to save screenshot file</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="705" />
|
||||
<source>Cancel</source>
|
||||
<translation>Cancel</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="727" />
|
||||
<source>OK</source>
|
||||
<translation>OK</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../qml/main.qml" line="10" />
|
||||
<source>TZshot</source>
|
||||
<translation>TZshot</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/main.qml" line="199" />
|
||||
<source>OCR 识别失败</source>
|
||||
<translation>OCR Recognition Failed</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
||||
+64
-688
@@ -1,691 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<!DOCTYPE TS []>
|
||||
<TS version="2.1" language="zh_CN">
|
||||
<context>
|
||||
<name>GifRecordViewModel</name>
|
||||
<message>
|
||||
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="48"/>
|
||||
<source>录制区域无效</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="96"/>
|
||||
<source>未采集到任何帧</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="150"/>
|
||||
<source>GIF 编码失败,请检查保存路径或磁盘空间</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="8"/>
|
||||
<source>截图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="11"/>
|
||||
<source>设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="14"/>
|
||||
<source>关于</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="17"/>
|
||||
<source>退出</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>S:</name>
|
||||
<message>
|
||||
<location filename="../thirdpart/vcpkg/buildtrees/liblzma/src/v5.8.2-30365c07c3.clean/src/liblzma/common/string_conversion.c" line="731"/>
|
||||
<source></source>
|
||||
<comment>Don't translate the</comment>
|
||||
<translation></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZAboutWin</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="9"/>
|
||||
<source>关于 TZshot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="63"/>
|
||||
<source>版本 v1.0.0</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="83"/>
|
||||
<source>简洁高效的截图贴图工具,支持标注、AI 编辑、马赛克等功能。</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="96"/>
|
||||
<source>© 2025 TZshot. All rights reserved.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZAboutWin.qml" line="112"/>
|
||||
<source>关闭</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZChat</name>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="83"/>
|
||||
<source>AI 图像编辑</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="162"/>
|
||||
<source>当前贴图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="171"/>
|
||||
<source>未关联贴图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="178"/>
|
||||
<source>发送后将对此图执行 AI 编辑</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="203"/>
|
||||
<source>描述您想要的修改...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="228"/>
|
||||
<source>取消</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZChat.qml" line="245"/>
|
||||
<source>发送</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZGeneralSettings</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="35"/>
|
||||
<source>大模型设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="63"/>
|
||||
<source>大模型选择</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71"/>
|
||||
<source>阿里云百炼</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="71"/>
|
||||
<source>火山引擎</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="124"/>
|
||||
<source>请输入 API Key</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="148"/>
|
||||
<source>保存</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="183"/>
|
||||
<source>语言设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="206"/>
|
||||
<source>界面语言</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="241"/>
|
||||
<source>截图保存路径</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="264"/>
|
||||
<source>保存路径</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZGeneralSettings.qml" line="316"/>
|
||||
<source>浏览</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZGifRecordBar</name>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZGifRecordBar.qml" line="186"/>
|
||||
<source>正在生成 GIF...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZHotkeySettings</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="49"/>
|
||||
<source>编辑快捷键 · %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="70"/>
|
||||
<source>请按下新的快捷键组合...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="103"/>
|
||||
<source>取消</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="133"/>
|
||||
<source>确认</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="228"/>
|
||||
<source>截图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="229"/>
|
||||
<source>截图并保存</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="230"/>
|
||||
<source>贴图到桌面</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="150"/>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="231"/>
|
||||
<source>显示/隐藏窗口</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="201"/>
|
||||
<source>全局快捷键</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="293"/>
|
||||
<source>编辑</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="341"/>
|
||||
<source>说明</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZHotkeySettings.qml" line="362"/>
|
||||
<source>快捷键在全局范围内生效。点击「编辑」后直接按下新的组合键,再点「确认」即可保存。
|
||||
若与系统或其他应用快捷键冲突,请修改为其他组合。</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZMarkToolInfo</name>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="13"/>
|
||||
<source>文本</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="62"/>
|
||||
<source>细</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="63"/>
|
||||
<source>中</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="64"/>
|
||||
<source>粗</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="65"/>
|
||||
<source>特粗</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="133"/>
|
||||
<source>输入文字</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="140"/>
|
||||
<source>背景</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="152"/>
|
||||
<source>模糊</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="177"/>
|
||||
<source>自动递增</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZMarkToolInfo.qml" line="187"/>
|
||||
<source>数字</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZOcrResult</name>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="67"/>
|
||||
<source>OCR 识别结果</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="167"/>
|
||||
<source>正在识别中…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="190"/>
|
||||
<source>识别结果将显示在这里…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="239"/>
|
||||
<source>复制全部</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="265"/>
|
||||
<source>关闭</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZOcrResult.qml" line="293"/>
|
||||
<source>已复制!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZRightPanel</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZRightPanel.qml" line="90"/>
|
||||
<source>通用设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZRightPanel.qml" line="137"/>
|
||||
<source>快捷键设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZSettingWin</name>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZSettingWin.qml" line="11"/>
|
||||
<source>设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Setting/TZSettingWin.qml" line="29"/>
|
||||
<source>选择保存位置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZStickyWindow</name>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="324"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="325"/>
|
||||
<source>OCR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="341"/>
|
||||
<source>Pencil</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="350"/>
|
||||
<source>Rectangle</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="359"/>
|
||||
<source>Circle</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="368"/>
|
||||
<source>Arrow</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="382"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="383"/>
|
||||
<source>Mosaic</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="404"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="539"/>
|
||||
<source>缩小</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="406"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="540"/>
|
||||
<source>放大</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="407"/>
|
||||
<source>旋转90°</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="408"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="542"/>
|
||||
<source>镜像</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="409"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="543"/>
|
||||
<source>恢复原始大小</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="415"/>
|
||||
<source>完成并复制</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="422"/>
|
||||
<source>撤销</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="434"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="435"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="523"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="558"/>
|
||||
<source>隐藏工具栏</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="515"/>
|
||||
<source>当前透明度 %1%</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="518"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="572"/>
|
||||
<source>覆盖贴图(合并标注)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="519"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="573"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="580"/>
|
||||
<source>复制贴图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="520"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="566"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="581"/>
|
||||
<source>AI 编辑</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="523"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="558"/>
|
||||
<source>显示工具栏</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="530"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="583"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="588"/>
|
||||
<source>关闭窗口</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="534"/>
|
||||
<source>透明度 +5%</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="535"/>
|
||||
<source>透明度 -5%</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="541"/>
|
||||
<source>旋转 90°</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="548"/>
|
||||
<source>重置位置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="570"/>
|
||||
<source>保存原图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="571"/>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="602"/>
|
||||
<source>另存为(合并标注)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="578"/>
|
||||
<source>常用项置顶</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="579"/>
|
||||
<source>覆盖贴图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="582"/>
|
||||
<source>显示/隐藏工具栏</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Windows/TZStickyWindow.qml" line="593"/>
|
||||
<source>选择保存位置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZTool</name>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="83"/>
|
||||
<source>取消</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="76"/>
|
||||
<source>完成</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="92"/>
|
||||
<source>OCR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="100"/>
|
||||
<source>录制GIF</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="109"/>
|
||||
<source>保存</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="116"/>
|
||||
<source>贴图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="127"/>
|
||||
<source>矩形</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="136"/>
|
||||
<source>圆形</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="145"/>
|
||||
<source>箭头</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="154"/>
|
||||
<source>画笔</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="163"/>
|
||||
<source>高亮</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="172"/>
|
||||
<source>马赛克</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="181"/>
|
||||
<source>文字</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="190"/>
|
||||
<source>序号</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZTool.qml" line="197"/>
|
||||
<source>撤销</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>TZWindow</name>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="134"/>
|
||||
<source>Capture done</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="134"/>
|
||||
<source>Copied to clipboard</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="136"/>
|
||||
<source>Capture failed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="136"/>
|
||||
<source>Failed to copy image</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="434"/>
|
||||
<source>Select save location</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="442"/>
|
||||
<source>Save success</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="442"/>
|
||||
<source>Screenshot saved to file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="445"/>
|
||||
<source>Save failed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="445"/>
|
||||
<source>Failed to save screenshot file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="563"/>
|
||||
<source>%1 * %2</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="705"/>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/Capture/TZWindow.qml" line="727"/>
|
||||
<source>OK</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../qml/main.qml" line="10"/>
|
||||
<source>TZshot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/main.qml" line="199"/>
|
||||
<source>OCR 识别失败</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>GifRecordViewModel</name>
|
||||
<message>
|
||||
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="48" />
|
||||
<source>录制区域无效</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="96" />
|
||||
<source>未采集到任何帧</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/viewmodel/gif_record_view_model.cpp" line="150" />
|
||||
<source>GIF 编码失败,请检查保存路径或磁盘空间</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="8" />
|
||||
<source>截图</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="11" />
|
||||
<source>设置</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="14" />
|
||||
<source>关于</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/tray_icon_helper.cpp" line="17" />
|
||||
<source>退出</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>S:</name>
|
||||
<message>
|
||||
<location filename="../thirdpart/vcpkg/buildtrees/liblzma/src/v5.8.2-30365c07c3.clean/src/liblzma/common/string_conversion.c" line="731" />
|
||||
<source></source>
|
||||
<comment>Don't translate the</comment>
|
||||
<translation></translation>
|
||||
</message>
|
||||
</context>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</TS>
|
||||
@@ -1,34 +1,7 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>qml/main.qml</file>
|
||||
<file>qml/Capture/TZWindow.qml</file>
|
||||
<file>qml/Capture/TZSelectionOverlay.qml</file>
|
||||
<file>qml/Capture/TZCaptureInputController.qml</file>
|
||||
<file>qml/Capture/TZHotkeyGestureController.qml</file>
|
||||
<file>qml/Capture/TZRectMaskOverlay.qml</file>
|
||||
<file>qml/Capture/TZSelectionPositioner.qml</file>
|
||||
<file>qml/Capture/TZSelectionInfoLabel.qml</file>
|
||||
<file>qml/Capture/TZTextAnnotationEditor.qml</file>
|
||||
<file>qml/Capture/TZWindowHighlightOverlay.qml</file>
|
||||
<file>qml/Capture/TZWindowFactory.qml</file>
|
||||
<file>qml/Capture/TZLable.qml</file>
|
||||
<file>qml/Capture/TZTool.qml</file>
|
||||
<file>qml/Capture/TZResizeHandle.qml</file>
|
||||
<file>qml/Capture/TZResizeHandleGroup.qml</file>
|
||||
<file>qml/Capture/TZMarkToolInfo.qml</file>
|
||||
<file>qml/Capture/TZMagnifyingGlass.qml</file>
|
||||
<file>qml/Capture/TZGifRecordBar.qml</file>
|
||||
<file>qml/Windows/TZStickyWindow.qml</file>
|
||||
<file>qml/Windows/TZOcrResult.qml</file>
|
||||
<file>qml/Windows/TZChat.qml</file>
|
||||
<file>qml/Setting/TZSettingWin.qml</file>
|
||||
<file>qml/Setting/TZGeneralSettings.qml</file>
|
||||
<file>qml/Setting/TZRightPanel.qml</file>
|
||||
<file>qml/TZControls/qmldir</file>
|
||||
<file>qml/TZControls/TZComboBox.qml</file>
|
||||
<file>qml/Setting/TZHotkeySettings.qml</file>
|
||||
<file>qml/Setting/TZAboutWin.qml</file>
|
||||
<file>resource/img/lc_check.svg</file>
|
||||
<file>resource/img/lc_copy.svg</file>
|
||||
<file>resource/img/lc_x.svg</file>
|
||||
<file>resource/img/lc_save.svg</file>
|
||||
<file>resource/img/lc_pin.svg</file>
|
||||
@@ -46,9 +19,6 @@
|
||||
<file>resource/img/lc_close.svg</file>
|
||||
<file>resource/img/lc_chat.svg</file>
|
||||
<file>resource/img/lc_send.svg</file>
|
||||
<file>qml/LongCapture/TZLongCaptureBar.qml</file>
|
||||
<file>qml/LongCapture/TZLongCapturePreview.qml</file>
|
||||
<file>qml/LongCapture/TZLongCaptureFrame.qml</file>
|
||||
<file>resource/img/lc_gif_record.svg</file>
|
||||
<file>resource/img/lc_gif_stop.svg</file>
|
||||
<file>resource/img/lc_ocr.svg</file>
|
||||
@@ -56,5 +26,7 @@
|
||||
<file>resource/img/lc_chevron_down.svg</file>
|
||||
<file>resource/qss/sticky_pin_widget.qss</file>
|
||||
<file>resource/qss/settings_dialog.qss</file>
|
||||
<file>resource/qss/capture_overlay_widget.qss</file>
|
||||
<file>resource/qss/long_capture_controller.qss</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ───────────────────────────────────────────
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
module TZControls
|
||||
TZComboBox 1.0 TZComboBox.qml
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
-339
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="10" height="10" rx="2" ry="2"/>
|
||||
<path d="M5 15V7a2 2 0 0 1 2-2h8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 291 B |
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "app/tzshot_app.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef TZSHOT_APP_H
|
||||
#define TZSHOT_APP_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef TZSHOT_SERVICES_H
|
||||
#define TZSHOT_SERVICES_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef TZSHOT_WIDGET_RUNTIME_H
|
||||
#define TZSHOT_WIDGET_RUNTIME_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#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
|
||||
@@ -3,6 +3,108 @@
|
||||
#include <QScreen>
|
||||
#include <QGuiApplication>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#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<UINT>(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<QScreen*> 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);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#include "magnifier_provider.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#ifndef MAGNIFIER_PROVIDER_H
|
||||
#define MAGNIFIER_PROVIDER_H
|
||||
|
||||
#include <QQuickImageProvider>
|
||||
#include "viewmodel/screenshot_view_model.h"
|
||||
|
||||
// MagnifierProvider
|
||||
// -----------------
|
||||
// QQuickImageProvider 子类,供 QML Image 通过
|
||||
// source: "image://magnifier/<x>,<y>"
|
||||
// 获取以 (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
|
||||
+4
-121
@@ -1,132 +1,15 @@
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickWindow>
|
||||
#include <QScreen>
|
||||
#include <QApplication>
|
||||
|
||||
#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<PaintBoard>("CustomComponents", 1, 0, "PaintBoard");
|
||||
qmlRegisterType<TrayIconHelper>("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<QQuickWindow*>(engine.rootObjects().constFirst())) {
|
||||
if (QScreen *screen = QGuiApplication::primaryScreen()) {
|
||||
window->setGeometry(screen->virtualGeometry());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
#include "paint_board.h"
|
||||
#include "shape/mosaic_shape.h"
|
||||
#include <QDebug>
|
||||
|
||||
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<Shapeype>(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;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
#ifndef PAINTBOARD_H
|
||||
#define PAINTBOARD_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQuickPaintedItem>
|
||||
#include <QPainter>
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <memory>
|
||||
|
||||
#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<int>(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<Shape*> 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
|
||||
@@ -29,7 +29,6 @@ public:
|
||||
void draw(QPainter* painter) override;
|
||||
|
||||
// 设置当前画布的快照,用于马赛克取色
|
||||
// 每次绘制前由 PaintBoard 调用
|
||||
void setCanvasSnapshot(const QImage& snapshot);
|
||||
|
||||
private:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#ifndef STICKY_IMAGE_PROVIDER_PROXY_H
|
||||
#define STICKY_IMAGE_PROVIDER_PROXY_H
|
||||
|
||||
#include <QQuickImageProvider>
|
||||
#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
|
||||
@@ -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/<uuid>"
|
||||
QString storeImage(const QImage &image);
|
||||
|
||||
// 供 StickyImageProviderProxy 调用,线程安全
|
||||
// 线程安全读取
|
||||
QImage getImage(const QString &id) const;
|
||||
QImage getImageByUrl(const QString &imageUrl) const;
|
||||
|
||||
|
||||
@@ -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 <QBuffer>
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QPainter>
|
||||
#include <QQuickWindow>
|
||||
#include <QScreen>
|
||||
#include <QWidget>
|
||||
#include <QWindow>
|
||||
|
||||
#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*>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QRect>
|
||||
#include <QWindow>
|
||||
#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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "widgets/about_dialog.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef ABOUT_DIALOG_H
|
||||
#define ABOUT_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class AboutDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AboutDialog(QWidget *parent = nullptr);
|
||||
|
||||
void showAndActivate();
|
||||
};
|
||||
|
||||
#endif // ABOUT_DIALOG_H
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,160 @@
|
||||
#ifndef CAPTURE_OVERLAY_WIDGET_H
|
||||
#define CAPTURE_OVERLAY_WIDGET_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QColor>
|
||||
#include <QPoint>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QWidget>
|
||||
|
||||
#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<ScreenshotViewModel> m_screenCapture;
|
||||
QPointer<StickyViewModel> m_stickyViewModel;
|
||||
QPointer<StorageViewModel> m_storageViewModel;
|
||||
QPointer<OcrViewModel> m_ocrViewModel;
|
||||
QPointer<GifRecordViewModel> m_gifRecordViewModel;
|
||||
QPointer<WidgetWindowBridge> 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<QToolButton*> m_colorButtons;
|
||||
QVector<QToolButton*> 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
|
||||
@@ -0,0 +1,388 @@
|
||||
#include "widgets/long_capture_controller.h"
|
||||
|
||||
#include "viewmodel/scroll_capture_view_model.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QStyle>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#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<HWND>(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<QKeyEvent *>(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("关闭"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef LONG_CAPTURE_CONTROLLER_H
|
||||
#define LONG_CAPTURE_CONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QRect>
|
||||
|
||||
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<ScrollCaptureViewModel> 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
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "widgets/magnifier_widget.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef MAGNIFIER_WIDGET_H
|
||||
#define MAGNIFIER_WIDGET_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QPixmap>
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "widgets/selection_mask_controller.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
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<DragMode> 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);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef SELECTION_MASK_CONTROLLER_H
|
||||
#define SELECTION_MASK_CONTROLLER_H
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QSize>
|
||||
#include <Qt>
|
||||
|
||||
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
|
||||
@@ -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<qreal>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Shape*> 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,32 @@
|
||||
#define WIDGET_WINDOW_BRIDGE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user