Refactor QML capture flow and polish sticky tools
This commit is contained in:
1 parent
f140eec539
commit
1efdf4dbad
40 files changed
+4756
-377
No files matched your search
@@ -84,6 +84,7 @@ x64/
|
||||
x86/
|
||||
[Bb]uild/
|
||||
[Bb]uild*/
|
||||
dist/
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
|
||||
+16
-1
@@ -25,6 +25,11 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
|
||||
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)
|
||||
@@ -148,6 +153,10 @@ 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
|
||||
@@ -393,9 +402,15 @@ if(WIN32)
|
||||
|
||||
target_include_directories(TZshot PRIVATE "${OCR_INSTALL_DIR}/include")
|
||||
target_link_directories(TZshot PRIVATE "${OCR_INSTALL_DIR}/lib")
|
||||
set(OCR_LEPTONICA_LINK_NAME "leptonica-${OCR_LEPTONICA_VERSION}$<$<CONFIG:Debug>:d>")
|
||||
if(MINGW)
|
||||
# Our MinGW Leptonica ExternalProject installs libleptonica.dll.a
|
||||
# without a version/debug suffix, so link against that stable name.
|
||||
set(OCR_LEPTONICA_LINK_NAME "leptonica")
|
||||
endif()
|
||||
target_link_libraries(TZshot PRIVATE
|
||||
"tesseract55$<$<CONFIG:Debug>:d>"
|
||||
"leptonica-${OCR_LEPTONICA_VERSION}$<$<CONFIG:Debug>:d>"
|
||||
"${OCR_LEPTONICA_LINK_NAME}"
|
||||
Ws2_32
|
||||
)
|
||||
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>qml/main.qml</file>
|
||||
<file>qml/TZStickyWindow.qml</file>
|
||||
<file>qml/TZWindow.qml</file>
|
||||
<file>qml/TZLable.qml</file>
|
||||
<file>qml/TZTool.qml</file>
|
||||
<file>qml/TZResizeHandle.qml</file>
|
||||
<file>qml/TZResizeHandleGroup.qml</file>
|
||||
<file>qml/TZChat.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/TZStickyExtraToolBar.qml</file>
|
||||
<file>qml/Windows/TZOcrResult.qml</file>
|
||||
<file>qml/Windows/TZChat.qml</file>
|
||||
<file>qml/Windows/TZLoadingSpinner.qml</file>
|
||||
<file>qml/Setting/TZSettingWin.qml</file>
|
||||
<file>qml/Setting/TZGeneralSettings.qml</file>
|
||||
<file>qml/Setting/TZRightPanel.qml</file>
|
||||
@@ -29,19 +44,14 @@
|
||||
<file>resource/img/lc_highlighter.svg</file>
|
||||
<file>resource/img/lc_undo.svg</file>
|
||||
<file>resource/img/tray_icon.svg</file>
|
||||
<file>qml/TZMarkToolInfo.qml</file>
|
||||
<file>qml/TZMagnifyingGlass.qml</file>
|
||||
<file>resource/img/lc_close.svg</file>
|
||||
<file>resource/img/lc_chat.svg</file>
|
||||
<file>resource/img/lc_emoji.svg</file>
|
||||
<file>resource/img/lc_file.svg</file>
|
||||
<file>resource/img/lc_send.svg</file>
|
||||
<file>qml/TZLoadingSpinner.qml</file>
|
||||
<file>qml/TZGifRecordBar.qml</file>
|
||||
<file>qml/TZOcrResult.qml</file>
|
||||
<file>qml/TZLongCaptureBar.qml</file>
|
||||
<file>qml/TZLongCapturePreview.qml</file>
|
||||
<file>qml/TZLongCaptureFrame.qml</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>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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 ───────────────────────────────────────────
|
||||
@@ -0,0 +1,257 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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
|
||||
property var coordinateItem: null
|
||||
|
||||
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 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 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"
|
||||
}
|
||||
|
||||
function reset() {
|
||||
area = Qt.rect(0, 0, 0, 0)
|
||||
initialArea = Qt.rect(0, 0, 0, 0)
|
||||
dragOffset = Qt.point(0, 0)
|
||||
dragMode = "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Controls 2.5
|
||||
|
||||
Rectangle {
|
||||
id: tool
|
||||
radius: 10
|
||||
color: "#FFFFFF"
|
||||
visible: false
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
implicitWidth: toolRow.implicitWidth + 12
|
||||
implicitHeight: 44
|
||||
|
||||
signal signalScreenshot
|
||||
signal signalCancel
|
||||
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 shapeType: {
|
||||
switch (activeTool) {
|
||||
case "pencil": return 0
|
||||
case "rect": return 1
|
||||
case "circle": return 2
|
||||
case "arrow": return 3
|
||||
case "mosaic": return 4
|
||||
case "text": return 5
|
||||
case "number": return 6
|
||||
case "highlight": return 7
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveTool(name) {
|
||||
if (activeTool === name) {
|
||||
activeTool = ""
|
||||
markToolInfoVisible = false
|
||||
} else {
|
||||
activeTool = name
|
||||
markToolInfoVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
function showTip(text, targetItem) {
|
||||
hoverTipText = text
|
||||
if (targetItem) {
|
||||
var p = tool.mapFromItem(targetItem, targetItem.width / 2, targetItem.height)
|
||||
hoverTipCenterX = p.x
|
||||
} else {
|
||||
hoverTipCenterX = width / 2
|
||||
}
|
||||
}
|
||||
|
||||
function hideTip(text) {
|
||||
if (text === undefined || hoverTipText === text)
|
||||
hoverTipText = ""
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: toolRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
layoutDirection: Qt.RightToLeft
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showScreenshotButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: checkArea.containsMouse ? "#DCFCE7" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: checkArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("完成"), parent); onExited: tool.hideTip(qsTr("完成")); onPressed: signalScreenshot() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showCancelButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: closeArea.containsMouse ? "#FEE2E2" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: closeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("取消"), parent); onExited: tool.hideTip(qsTr("取消")); onPressed: signalCancel() }
|
||||
}
|
||||
|
||||
Rectangle { visible: tool.showScreenshotButton || tool.showCancelButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showOcrButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: ocrArea.containsMouse ? "#F0FDF4" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_ocr.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: ocrArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("OCR"), parent); onExited: tool.hideTip(qsTr("OCR")); onPressed: signalOcr() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: gifRecordBtn
|
||||
visible: tool.showGifRecordButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: gifArea.containsMouse ? "#FFF7ED" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_gif_record.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: gifArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("录制GIF"), parent); onExited: tool.hideTip(qsTr("录制GIF")); onPressed: signalGifRecord() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showLongScreenshotButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: longShotArea.containsMouse ? "#FFF7ED" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_longshot.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: longShotArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("长截图"), parent); onExited: tool.hideTip(qsTr("长截图")); onPressed: signalLongScreenshot() }
|
||||
}
|
||||
|
||||
Rectangle { visible: tool.showOcrButton || tool.showGifRecordButton || tool.showLongScreenshotButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showSaveButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: saveArea.containsMouse ? "#EFF6FF" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_save.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: saveArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("保存"), parent); onExited: tool.hideTip(qsTr("保存")); onPressed: signalSave() }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showStickyButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: fixArea.containsMouse ? "#EFF6FF" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pin.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: fixArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("贴图"), parent); onExited: tool.hideTip(qsTr("贴图")); onPressed: signalSticky() }
|
||||
}
|
||||
|
||||
Rectangle { visible: tool.showSaveButton || tool.showStickyButton; width: 1; height: 20; color: "#E2E8F0"; Layout.alignment: Qt.AlignVCenter }
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showRectButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "rect") ? "#EFF6FF" : (rectArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "rect") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_square.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: rectArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("矩形"), parent); onExited: tool.hideTip(qsTr("矩形")); onPressed: tool.setActiveTool("rect") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showCircleButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "circle") ? "#EFF6FF" : (circleArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "circle") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_circle.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: circleArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("圆形"), parent); onExited: tool.hideTip(qsTr("圆形")); onPressed: tool.setActiveTool("circle") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showArrowButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "arrow") ? "#EFF6FF" : (arrowArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "arrow") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_arrow.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: arrowArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("箭头"), parent); onExited: tool.hideTip(qsTr("箭头")); onPressed: tool.setActiveTool("arrow") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showPencilButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "pencil") ? "#EFF6FF" : (pencilArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "pencil") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_pencil.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: pencilArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("画笔"), parent); onExited: tool.hideTip(qsTr("画笔")); onPressed: tool.setActiveTool("pencil") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showHighlightButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "highlight") ? "#EFF6FF" : (highlightArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "highlight") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_highlighter.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: highlightArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("高亮"), parent); onExited: tool.hideTip(qsTr("高亮")); onPressed: tool.setActiveTool("highlight") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showMosaicButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "mosaic") ? "#EFF6FF" : (mosaicArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "mosaic") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_mosaic.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: mosaicArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("马赛克"), parent); onExited: tool.hideTip(qsTr("马赛克")); onPressed: tool.setActiveTool("mosaic") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showTextButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "text") ? "#EFF6FF" : (textArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "text") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_text.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: textArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("文字"), parent); onExited: tool.hideTip(qsTr("文字")); onPressed: tool.setActiveTool("text") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showNumberButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: (tool.activeTool === "number") ? "#EFF6FF" : (numberArea.containsMouse ? "#F1F5F9" : "transparent")
|
||||
border.color: (tool.activeTool === "number") ? "#3B82F6" : "transparent"
|
||||
border.width: 1
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_list_ordered.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: numberArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("序号"), parent); onExited: tool.hideTip(qsTr("序号")); onPressed: tool.setActiveTool("number") }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: tool.showUndoButton
|
||||
width: 32; height: 32; radius: 7
|
||||
color: undoArea.containsMouse ? "#EFF6FF" : "transparent"
|
||||
Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_undo.svg"; width: 18; height: 18 }
|
||||
MouseArea { id: undoArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: tool.showTip(qsTr("撤销"), parent); onExited: tool.hideTip(qsTr("撤销")); onPressed: signalUndo() }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: tipBubble
|
||||
visible: tool.visible && tool.hoverTipText.length > 0
|
||||
z: 100
|
||||
parent: tool.parent
|
||||
width: Math.max(44, tipText.implicitWidth + 14)
|
||||
height: 24
|
||||
radius: 6
|
||||
color: "#0F172A"
|
||||
opacity: 0.92
|
||||
x: Math.max(tool.x, Math.min(tool.x + tool.width - width, tool.x + tool.hoverTipCenterX - width / 2))
|
||||
y: tool.y + tool.height + 6
|
||||
|
||||
Text {
|
||||
id: tipText
|
||||
anchors.centerIn: parent
|
||||
text: tool.hoverTipText
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
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()
|
||||
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)
|
||||
selectionOverlay.beginSelection(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
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
console.log("imgRect-->",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)
|
||||
console.log("[Sticky] imageUrl:", url)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ Rectangle {
|
||||
// 跟随鼠标移动,始终显示(不随截图状态切换)
|
||||
property int magnifierMouseX: 0
|
||||
property int magnifierMouseY: 0
|
||||
property int magnifierAbsX: 0
|
||||
property int magnifierAbsY: 0
|
||||
property color magnifierPixelColor: "#000000"
|
||||
|
||||
// 跟随鼠标,偏移 +15px,防止被遮挡;超出屏幕时翻转
|
||||
@@ -49,7 +51,7 @@ Rectangle {
|
||||
cache: false
|
||||
// source 用时间戳 +坐标,强制 QML 重新加载
|
||||
source: root.visibility === Window.FullScreen
|
||||
? ("image://magnifier/" + magnifierMouseX + "," + magnifierMouseY + "?t=" + Date.now())
|
||||
? ("image://magnifier/" + magnifierAbsX + "," + magnifierAbsY + "?t=" + Date.now())
|
||||
: ""
|
||||
fillMode: Image.Stretch
|
||||
}
|
||||
|
||||
+4
-11
@@ -661,17 +661,10 @@ Window {
|
||||
}
|
||||
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.modifiers & Qt.ControlModifier) {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
setZoomValue(stickyWin.zoomFactor + 0.1)
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
setZoomValue(stickyWin.zoomFactor - 0.1)
|
||||
} else {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
setOpacityValue(stickyWin.imgOpacity + 0.05)
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
setOpacityValue(stickyWin.imgOpacity - 0.05)
|
||||
}
|
||||
if (wheel.angleDelta.y > 0)
|
||||
setZoomValue(stickyWin.zoomFactor + 0.1)
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
setZoomValue(stickyWin.zoomFactor - 0.1)
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,6 +982,8 @@ Window {
|
||||
// Magnifier follows mouse in local coordinates
|
||||
magnifier.magnifierMouseX = mouseX
|
||||
magnifier.magnifierMouseY = mouseY
|
||||
magnifier.magnifierAbsX = ax
|
||||
magnifier.magnifierAbsY = ay
|
||||
handleMouseMoved(ax, ay, pressed, mouse.modifiers)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
QtObject {
|
||||
id: chatPanel
|
||||
|
||||
// 对应贴图的图片 URL,由外部绑定
|
||||
property string imageUrl: ""
|
||||
|
||||
signal signalSendPrompt(var text)
|
||||
|
||||
// 公开方法
|
||||
function open(px, py) {
|
||||
win.x = px
|
||||
win.y = py
|
||||
win.visible = true
|
||||
win.raise()
|
||||
win.requestActivate()
|
||||
}
|
||||
|
||||
function close() {
|
||||
win.visible = false
|
||||
}
|
||||
|
||||
// ── 内部 Window ─────────────────────────────────────
|
||||
property var win: Window {
|
||||
id: win
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||
color: "transparent"
|
||||
width: 420
|
||||
height: 380
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#ffffff"
|
||||
radius: 10
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
// ── 顶部标题栏 ────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 36
|
||||
color: "transparent"
|
||||
|
||||
// 拖动区域(z:-1 让关闭按钮的点击不被拦截)
|
||||
MouseArea {
|
||||
id: dragHandle
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
property point startPos
|
||||
|
||||
onPressed: startPos = Qt.point(mouseX, mouseY)
|
||||
onPositionChanged: {
|
||||
if (pressed) {
|
||||
win.x += mouseX - startPos.x
|
||||
win.y += mouseY - startPos.y
|
||||
}
|
||||
}
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Image {
|
||||
source: "qrc:/resource/img/lc_chat.svg"
|
||||
width: 20
|
||||
height: 20
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("AI 图像编辑")
|
||||
font.pixelSize: 14
|
||||
font.weight: Font.Medium
|
||||
color: "#1E293B"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// 关闭按钮
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 6
|
||||
color: clearMouse.containsMouse ? "#FEE2E2" : "transparent"
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
source: "qrc:/resource/img/lc_close.svg"
|
||||
width: 14
|
||||
height: 14
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: chatPanel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标题栏底部分割线
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: "#E2E8F0"
|
||||
}
|
||||
}
|
||||
|
||||
// ── 贴图缩略图预览 ────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 64
|
||||
radius: 6
|
||||
color: "#F1F5F9"
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 10
|
||||
|
||||
// 缩略图
|
||||
Rectangle {
|
||||
width: 48
|
||||
height: 48
|
||||
radius: 4
|
||||
color: "#E2E8F0"
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: chatPanel.imageUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
}
|
||||
}
|
||||
|
||||
// 文字说明
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
text: qsTr("当前贴图")
|
||||
font.pixelSize: 11
|
||||
color: "#94A3B8"
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: chatPanel.imageUrl !== ""
|
||||
? chatPanel.imageUrl.replace(/^image:\/\/[^/]+\//, "ID: ")
|
||||
: qsTr("未关联贴图")
|
||||
font.pixelSize: 12
|
||||
color: "#475569"
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("发送后将对此图执行 AI 编辑")
|
||||
font.pixelSize: 10
|
||||
color: "#94A3B8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 输入区域 ──────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "#F8FAFC"
|
||||
radius: 8
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
|
||||
TextArea {
|
||||
id: input
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
font.family: "Segoe UI"
|
||||
font.pixelSize: 13
|
||||
color: "#1E293B"
|
||||
wrapMode: Text.Wrap
|
||||
placeholderText: qsTr("描述您想要的修改...")
|
||||
background: Rectangle { color: "transparent" }
|
||||
selectByMouse: true
|
||||
focus: true
|
||||
}
|
||||
}
|
||||
|
||||
// ── 底部工具栏 ────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 40
|
||||
color: "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// 取消按钮
|
||||
Rectangle {
|
||||
width: 70; height: 32; radius: 6
|
||||
color: cancelMouse.containsMouse ? "#F1F5F9" : "#F8FAFC"
|
||||
border.color: "#E2E8F0"; border.width: 1
|
||||
|
||||
Text { anchors.centerIn: parent; text: qsTr("取消"); font.pixelSize: 13; color: "#64748B" }
|
||||
|
||||
MouseArea {
|
||||
id: cancelMouse
|
||||
anchors.fill: parent; hoverEnabled: true
|
||||
onClicked: chatPanel.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 发送按钮
|
||||
Rectangle {
|
||||
width: 70; height: 32; radius: 6
|
||||
color: sendMouse.pressed ? "#0066cc" : (sendMouse.containsMouse ? "#3399ff" : "#0088ff")
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent; spacing: 4
|
||||
Image { source: "qrc:/resource/img/lc_send.svg"; width: 14; height: 14 }
|
||||
Text { text: qsTr("发送"); font.pixelSize: 13; color: "white" }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sendMouse
|
||||
anchors.fill: parent; hoverEnabled: true
|
||||
onClicked: {
|
||||
if (input.text.trim() !== "") {
|
||||
chatPanel.signalSendPrompt(input.text)
|
||||
chatPanel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import QtQuick
|
||||
|
||||
// ── AI 加载动画层 ─────────────────────────────────────
|
||||
Rectangle {
|
||||
|
||||
width: 100
|
||||
height: 100
|
||||
radius: 2
|
||||
color: "#99000000" // 半透明黑色遮罩
|
||||
|
||||
|
||||
// 旋转圆弧
|
||||
Item {
|
||||
id: spinnerRoot
|
||||
anchors.centerIn: parent
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
RotationAnimator {
|
||||
target: spinnerRoot
|
||||
from: 0; to: 360
|
||||
duration: 900
|
||||
loops: Animation.Infinite
|
||||
running: O_AIViewModel.isLoading
|
||||
}
|
||||
|
||||
// 四个小圆点模拟旋转圆弧
|
||||
Repeater {
|
||||
model: 8
|
||||
Rectangle {
|
||||
width: 6
|
||||
height: 6
|
||||
radius: 3
|
||||
color: "white"
|
||||
opacity: (index + 1) / 8
|
||||
x: spinnerRoot.width / 2 + 18 * Math.cos(index * Math.PI / 4) - width / 2
|
||||
y: spinnerRoot.height / 2 + 18 * Math.sin(index * Math.PI / 4) - height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提示文字
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: spinnerRoot.bottom
|
||||
anchors.topMargin: 10
|
||||
text: "AI 生成中..."
|
||||
color: "white"
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: bar
|
||||
radius: 10
|
||||
color: "#FFFFFF"
|
||||
border.color: "#E2E8F0"
|
||||
border.width: 1
|
||||
|
||||
signal zoomOut()
|
||||
signal zoomIn()
|
||||
signal rotate90()
|
||||
signal mirror()
|
||||
signal resetSize()
|
||||
signal doneAndCopy()
|
||||
signal hideToolbar()
|
||||
signal hoverEntered(string text, Item targetItem)
|
||||
signal hoverExited(string text)
|
||||
|
||||
property real zoomFactor: 1.0
|
||||
|
||||
width: extraRow.implicitWidth + 16
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
id: extraRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: zoomOutArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "-"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomOutArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Zoom out"), parent); onExited: bar.hoverExited(qsTr("Zoom out")); onPressed: bar.zoomOut() } }
|
||||
Rectangle { width: 40; height: 30; radius: 7; color: "transparent"; Text { anchors.centerIn: parent; text: Math.round(bar.zoomFactor * 100) + "%"; color: "#64748B"; font.pixelSize: 11 } }
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: zoomInArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "+"; color: "#334155"; font.pixelSize: 16 } MouseArea { id: zoomInArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Zoom in"), parent); onExited: bar.hoverExited(qsTr("Zoom in")); onPressed: bar.zoomIn() } }
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: rotateArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "↻"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: rotateArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Rotate 90°"), parent); onExited: bar.hoverExited(qsTr("Rotate 90°")); onPressed: bar.rotate90() } }
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: mirrorArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "⇋"; color: "#334155"; font.pixelSize: 14 } MouseArea { id: mirrorArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Mirror"), parent); onExited: bar.hoverExited(qsTr("Mirror")); onPressed: bar.mirror() } }
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: resetSizeArea.containsMouse ? "#F1F5F9" : "transparent"; Text { anchors.centerIn: parent; text: "1:1"; color: "#334155"; font.pixelSize: 10 } MouseArea { id: resetSizeArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Reset size"), parent); onExited: bar.hoverExited(qsTr("Reset size")); onPressed: bar.resetSize() } }
|
||||
Rectangle { width: 1; height: 18; color: "#E2E8F0"; anchors.verticalCenter: parent.verticalCenter }
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: overwriteArea.containsMouse ? "#E0F2FE" : "transparent"; Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_check.svg"; width: 18; height: 18 } MouseArea { id: overwriteArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Done and copy"), parent); onExited: bar.hoverExited(qsTr("Done and copy")); onPressed: bar.doneAndCopy() } }
|
||||
Rectangle { width: 30; height: 30; radius: 7; color: clearToolArea.containsMouse ? "#FEE2E2" : "transparent"; Image { anchors.centerIn: parent; source: "qrc:/resource/img/lc_x.svg"; width: 18; height: 18 } MouseArea { id: clearToolArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onEntered: bar.hoverEntered(qsTr("Hide toolbar"), parent); onExited: bar.hoverExited(qsTr("Hide toolbar")); onPressed: bar.hideToolbar() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls.Basic
|
||||
import QtCore
|
||||
import CustomComponents 1.0
|
||||
import "../Capture"
|
||||
|
||||
Window {
|
||||
id: stickyWin
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
|
||||
color: "transparent"
|
||||
|
||||
property string imageUrl: ""
|
||||
property rect imgRect: Qt.rect(0, 0, 0, 0)
|
||||
property bool contentActive: true
|
||||
property bool cleanedUp: false
|
||||
|
||||
property real imgOpacity: 1.0
|
||||
property string activeTool: ""
|
||||
property bool toolbarVisible: true
|
||||
property string toolbarHoverTipText: ""
|
||||
property real toolbarHoverTipCenterX: 0
|
||||
property bool contextMenuOpen: false
|
||||
|
||||
property int imageRevision: 0
|
||||
property int sourcePixelW: 0
|
||||
property int sourcePixelH: 0
|
||||
property real currentDpr: (screen && screen.devicePixelRatio > 0) ? screen.devicePixelRatio : 1.0
|
||||
property int imageBaseW: Math.max(1, Math.abs(imgRect.width))
|
||||
property int imageBaseH: Math.max(1, Math.abs(imgRect.height))
|
||||
property real zoomFactor: 1.0
|
||||
|
||||
readonly property int toolbarHeight: 40
|
||||
readonly property int toolbarGap: 8
|
||||
readonly property int toolbarTipGap: 6
|
||||
readonly property int toolbarTipHeight: 24
|
||||
readonly property int shadowRadius: 12
|
||||
readonly property int shadowOffset: 4
|
||||
readonly property int imageTopLeftX: (imgRect.width >= 0 ? imgRect.x : imgRect.x + imgRect.width)
|
||||
readonly property int imageTopLeftY: (imgRect.height >= 0 ? imgRect.y : imgRect.y + imgRect.height)
|
||||
property var targetScreen: null
|
||||
|
||||
readonly property int displayW: Math.max(1, Math.round(imageBaseW * zoomFactor))
|
||||
readonly property int displayH: Math.max(1, Math.round(imageBaseH * zoomFactor))
|
||||
readonly property int extraToolbarWidth: stickyExtraBar.width
|
||||
readonly property int toolbarContentWidth: stickyTool.implicitWidth + extraToolbarWidth + 6
|
||||
readonly property int toolbarBlockX: Math.max(8, Math.min(stickyWin.width - toolbarContentWidth - 8, (stickyWin.displayW - toolbarContentWidth) / 2))
|
||||
|
||||
readonly property int stickyShapeType: {
|
||||
switch (activeTool) {
|
||||
case "pencil": return 0
|
||||
case "rect": return 1
|
||||
case "circle": return 2
|
||||
case "arrow": return 3
|
||||
case "mosaic": return 4
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
|
||||
width: Math.max(displayW, toolbarContentWidth + 16)
|
||||
height: displayH + toolbarGap + toolbarHeight + toolbarTipGap + toolbarTipHeight + 4
|
||||
function findScreenForPoint(px, py) {
|
||||
var screens = Qt.application.screens
|
||||
for (var i = 0; i < screens.length; ++i) {
|
||||
var s = screens[i]
|
||||
if (!s || !s.geometry)
|
||||
continue
|
||||
var g = s.geometry
|
||||
if (px >= g.x && px < g.x + g.width && py >= g.y && py < g.y + g.height)
|
||||
return s
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function syncInitialPosition() {
|
||||
O_StickyViewModel.positionStickyWindow(stickyWin, imgRect)
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: menuPrefs
|
||||
category: "sticky_menu"
|
||||
property bool pinSaveMerged: true
|
||||
property bool pinCopy: true
|
||||
property bool pinAi: false
|
||||
property bool pinToolbar: false
|
||||
property bool pinClose: false
|
||||
}
|
||||
|
||||
function setActiveTool(name) {
|
||||
if (stickyTool.activeTool === name)
|
||||
stickyTool.activeTool = ""
|
||||
else
|
||||
stickyTool.activeTool = name
|
||||
}
|
||||
|
||||
function showToolbarTip(text, item) {
|
||||
toolbarHoverTipText = text
|
||||
if (item) {
|
||||
var p = stickyExtraBar.mapFromItem(item, item.width / 2, item.height)
|
||||
toolbarHoverTipCenterX = p.x
|
||||
} else {
|
||||
toolbarHoverTipCenterX = stickyExtraBar.width / 2
|
||||
}
|
||||
}
|
||||
|
||||
function hideToolbarTip(text) {
|
||||
if (text === undefined || toolbarHoverTipText === text)
|
||||
toolbarHoverTipText = ""
|
||||
}
|
||||
|
||||
function displaySourceUrl() {
|
||||
if (typeof stickyWin.imageUrl !== "string" || stickyWin.imageUrl.length === 0)
|
||||
return ""
|
||||
return stickyWin.imageUrl + "?rev=" + stickyWin.imageRevision
|
||||
}
|
||||
|
||||
function setOpacityValue(v) {
|
||||
stickyWin.imgOpacity = Math.max(0.1, Math.min(1.0, v))
|
||||
}
|
||||
|
||||
function setZoomValue(v) {
|
||||
zoomFactor = Math.max(0.3, Math.min(4.0, v))
|
||||
}
|
||||
|
||||
function resetDisplaySize() {
|
||||
zoomFactor = 1.0
|
||||
}
|
||||
|
||||
function refreshDisplayMetrics() {
|
||||
currentDpr = (screen && screen.devicePixelRatio > 0) ? screen.devicePixelRatio : 1.0
|
||||
if (sourcePixelW > 0 && sourcePixelH > 0) {
|
||||
imageBaseW = Math.max(1, Math.round(sourcePixelW / currentDpr))
|
||||
imageBaseH = Math.max(1, Math.round(sourcePixelH / currentDpr))
|
||||
} else {
|
||||
imageBaseW = Math.max(1, Math.abs(imgRect.width))
|
||||
imageBaseH = Math.max(1, Math.abs(imgRect.height))
|
||||
}
|
||||
}
|
||||
|
||||
function updateImageMetricsFromStore() {
|
||||
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
|
||||
if (img && img.width > 0 && img.height > 0) {
|
||||
sourcePixelW = img.width
|
||||
sourcePixelH = img.height
|
||||
refreshDisplayMetrics()
|
||||
stickyPaintBoard.setBackgroundImg(img)
|
||||
} else {
|
||||
sourcePixelW = 0
|
||||
sourcePixelH = 0
|
||||
refreshDisplayMetrics()
|
||||
syncPaintboardBackgroundFromDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
function syncPaintboardBackgroundFromDisplay() {
|
||||
if (!stickyWin.visible || !stickyWin.contentActive)
|
||||
return
|
||||
if (stickyImage.status !== Image.Ready)
|
||||
return
|
||||
stickyImage.grabToImage(function(result) {
|
||||
if (!result || !result.image || result.image.width <= 0 || result.image.height <= 0)
|
||||
return
|
||||
stickyPaintBoard.setBackgroundImg(result.image)
|
||||
}, Qt.size(Math.max(1, imageBaseW), Math.max(1, imageBaseH)))
|
||||
}
|
||||
|
||||
function refreshAfterImageChanged() {
|
||||
imageRevision += 1
|
||||
stickyPaintBoard.reset()
|
||||
updateImageMetricsFromStore()
|
||||
}
|
||||
|
||||
function overwriteWithEdits() {
|
||||
var layer = stickyPaintBoard.renderToImage()
|
||||
var ok = O_StickyViewModel.overwriteWithAnnotations(stickyWin.imageUrl, layer)
|
||||
if (ok)
|
||||
refreshAfterImageChanged()
|
||||
}
|
||||
|
||||
function rotateCurrent(degrees) {
|
||||
if (O_StickyViewModel.rotateImage(stickyWin.imageUrl, degrees))
|
||||
refreshAfterImageChanged()
|
||||
}
|
||||
|
||||
function mirrorCurrent() {
|
||||
if (O_StickyViewModel.mirrorImage(stickyWin.imageUrl, true, false))
|
||||
refreshAfterImageChanged()
|
||||
}
|
||||
|
||||
function saveMergedTo(pathUrl) {
|
||||
var layer = stickyPaintBoard.renderToImage()
|
||||
O_StickyViewModel.saveMergedImage(stickyWin.imageUrl, layer, pathUrl)
|
||||
}
|
||||
|
||||
function completeEditsAndCopy() {
|
||||
var layer = stickyPaintBoard.renderToImage()
|
||||
if (layer && layer.width > 0 && layer.height > 0)
|
||||
overwriteWithEdits()
|
||||
O_StickyViewModel.copyImageToClipboard(stickyWin.imageUrl)
|
||||
stickyTool.activeTool = ""
|
||||
stickyWin.activeTool = ""
|
||||
stickyWin.toolbarVisible = false
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (cleanedUp)
|
||||
return
|
||||
cleanedUp = true
|
||||
|
||||
stickyWin.visible = false
|
||||
contentActive = false
|
||||
stickyWin.releaseResources()
|
||||
O_StickyViewModel.releaseImage(imageUrl)
|
||||
imageUrl = ""
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshDisplayMetrics()
|
||||
syncInitialPosition()
|
||||
console.log("[StickyWindow] imgRect:", imgRect.x, imgRect.y, imgRect.width, imgRect.height)
|
||||
console.log("[StickyWindow] imageTopLeft:", imageTopLeftX, imageTopLeftY)
|
||||
console.log("[StickyWindow] windowPos:", x, y, "display:", displayW, displayH)
|
||||
updateImageMetricsFromStore()
|
||||
stickyWin.raise()
|
||||
stickyWin.requestActivate()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Esc"
|
||||
onActivated: {
|
||||
if (stickyWin.activeTool !== "") {
|
||||
stickyTool.activeTool = ""
|
||||
stickyWin.activeTool = ""
|
||||
return
|
||||
}
|
||||
if (stickyWin.toolbarVisible) {
|
||||
stickyWin.toolbarVisible = false
|
||||
return
|
||||
}
|
||||
stickyWin.close()
|
||||
}
|
||||
}
|
||||
|
||||
onImageUrlChanged: {
|
||||
imageRevision = 0
|
||||
resetDisplaySize()
|
||||
sourcePixelW = 0
|
||||
sourcePixelH = 0
|
||||
refreshDisplayMetrics()
|
||||
refreshAfterImageChanged()
|
||||
}
|
||||
|
||||
onActiveToolChanged: {
|
||||
if (activeTool === "mosaic")
|
||||
updateImageMetricsFromStore()
|
||||
}
|
||||
|
||||
onImgRectChanged: {
|
||||
refreshDisplayMetrics()
|
||||
syncInitialPosition()
|
||||
}
|
||||
|
||||
onScreenChanged: refreshDisplayMetrics()
|
||||
|
||||
Component.onDestruction: cleanup()
|
||||
|
||||
onClosing: function(close) {
|
||||
close.accepted = true
|
||||
chatPanel.close()
|
||||
cleanup()
|
||||
Qt.callLater(function() { stickyWin.destroy() })
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: imgContainer
|
||||
visible: stickyWin.contentActive
|
||||
z: 10
|
||||
x: 0
|
||||
y: 0
|
||||
width: stickyWin.displayW
|
||||
height: stickyWin.displayH
|
||||
radius: 2
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
Item {
|
||||
id: visualStage
|
||||
anchors.centerIn: parent
|
||||
width: stickyWin.imageBaseW
|
||||
height: stickyWin.imageBaseH
|
||||
scale: stickyWin.zoomFactor
|
||||
|
||||
Image {
|
||||
id: stickyImage
|
||||
anchors.fill: parent
|
||||
source: displaySourceUrl()
|
||||
fillMode: Image.Stretch
|
||||
opacity: stickyWin.imgOpacity
|
||||
cache: false
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready && stickyWin.activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg())
|
||||
stickyWin.syncPaintboardBackgroundFromDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
PaintBoard {
|
||||
id: stickyPaintBoard
|
||||
anchors.fill: parent
|
||||
z: 20
|
||||
visible: stickyWin.contentActive
|
||||
enabled: stickyWin.activeTool !== ""
|
||||
shapeType: stickyWin.stickyShapeType
|
||||
penColor: stickyMarkTool.currentColor
|
||||
penSize: stickyMarkTool.currentSize
|
||||
mosaicBlurLevel: stickyMarkTool.mosaicBlurLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
visible: stickyWin.contentActive
|
||||
z: 5
|
||||
source: stickyImage
|
||||
anchors.fill: imgContainer
|
||||
shadowEnabled: true
|
||||
shadowColor: "#55000000"
|
||||
shadowBlur: stickyWin.shadowRadius / 20.0
|
||||
shadowVerticalOffset: stickyWin.shadowOffset
|
||||
shadowHorizontalOffset: 0
|
||||
opacity: stickyWin.imgOpacity
|
||||
}
|
||||
|
||||
TZLoadingSpinner {
|
||||
anchors.centerIn: imgContainer
|
||||
width: stickyWin.displayW
|
||||
height: stickyWin.displayH
|
||||
visible: O_AIViewModel.isLoading && stickyWin.contentActive
|
||||
}
|
||||
|
||||
TZMarkToolInfo {
|
||||
id: stickyMarkTool
|
||||
z: 32
|
||||
visible: stickyWin.toolbarVisible && stickyWin.activeTool !== ""
|
||||
toolName: stickyWin.activeTool
|
||||
showColorRow: stickyWin.activeTool !== "mosaic"
|
||||
x: Math.max(8, toolbarBlockX)
|
||||
y: stickyTool.y - height - 6
|
||||
}
|
||||
|
||||
TZTool {
|
||||
id: stickyTool
|
||||
z: 30
|
||||
visible: stickyWin.toolbarVisible
|
||||
width: implicitWidth
|
||||
height: stickyWin.toolbarHeight + 4
|
||||
x: stickyWin.toolbarBlockX
|
||||
y: stickyWin.displayH + stickyWin.toolbarGap
|
||||
showScreenshotButton: false
|
||||
showCancelButton: false
|
||||
showSaveButton: false
|
||||
showStickyButton: false
|
||||
showGifRecordButton: false
|
||||
showLongScreenshotButton: false
|
||||
showHighlightButton: false
|
||||
showTextButton: false
|
||||
showNumberButton: false
|
||||
|
||||
onActiveToolChanged: {
|
||||
stickyWin.activeTool = activeTool
|
||||
if (activeTool === "mosaic" && !stickyPaintBoard.hasBackgroundImg()) {
|
||||
Qt.callLater(function() {
|
||||
var retryImg = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
|
||||
if (retryImg && retryImg.width > 0 && retryImg.height > 0)
|
||||
stickyPaintBoard.setBackgroundImg(retryImg)
|
||||
if (!stickyPaintBoard.hasBackgroundImg())
|
||||
stickyWin.syncPaintboardBackgroundFromDisplay()
|
||||
})
|
||||
}
|
||||
}
|
||||
onSignalOcr: {
|
||||
var img = O_StickyViewModel.getImageByUrl(stickyWin.imageUrl)
|
||||
O_OcrVM.recognize(img)
|
||||
}
|
||||
onSignalUndo: stickyPaintBoard.undo()
|
||||
}
|
||||
|
||||
TZStickyExtraToolBar {
|
||||
id: stickyExtraBar
|
||||
z: 30
|
||||
visible: stickyWin.toolbarVisible
|
||||
height: stickyWin.toolbarHeight
|
||||
x: stickyWin.toolbarBlockX + stickyTool.width + 6
|
||||
y: stickyTool.y + 2
|
||||
zoomFactor: stickyWin.zoomFactor
|
||||
onZoomOut: setZoomValue(stickyWin.zoomFactor - 0.1)
|
||||
onZoomIn: setZoomValue(stickyWin.zoomFactor + 0.1)
|
||||
onRotate90: rotateCurrent(90)
|
||||
onMirror: mirrorCurrent()
|
||||
onResetSize: resetDisplaySize()
|
||||
onDoneAndCopy: completeEditsAndCopy()
|
||||
onHideToolbar: {
|
||||
stickyTool.activeTool = ""
|
||||
stickyWin.activeTool = ""
|
||||
stickyWin.toolbarVisible = false
|
||||
}
|
||||
onHoverEntered: function(text, targetItem) { stickyWin.showToolbarTip(text, targetItem) }
|
||||
onHoverExited: function(text) { stickyWin.hideToolbarTip(text) }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stickyToolbarTip
|
||||
parent: stickyWin.contentItem
|
||||
visible: stickyWin.toolbarVisible && stickyWin.toolbarHoverTipText.length > 0
|
||||
z: 40
|
||||
width: Math.min(stickyWin.width - 8, Math.max(64, stickyToolbarTipText.implicitWidth + 16))
|
||||
height: 24
|
||||
radius: 6
|
||||
color: "#0F172A"
|
||||
opacity: 0.92
|
||||
x: Math.max(4, Math.min(stickyWin.width - width - 4, stickyExtraBar.x + stickyWin.toolbarHoverTipCenterX - width / 2))
|
||||
y: Math.min(stickyWin.height - height - 4, stickyExtraBar.y + stickyExtraBar.height + stickyWin.toolbarTipGap)
|
||||
|
||||
Text {
|
||||
id: stickyToolbarTipText
|
||||
anchors.centerIn: parent
|
||||
text: stickyWin.toolbarHoverTipText
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int chatPanelGap: 6
|
||||
|
||||
TZChat {
|
||||
id: chatPanel
|
||||
imageUrl: stickyWin.imageUrl
|
||||
onSignalSendPrompt: function(text) {
|
||||
O_AIViewModel.sendPrompt(text, stickyWin.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: O_AIViewModel
|
||||
function onSignalRequestComplete(oldImageUrl, newImageUrl) {
|
||||
if (oldImageUrl !== stickyWin.imageUrl)
|
||||
return
|
||||
var old = oldImageUrl
|
||||
stickyWin.imageUrl = newImageUrl
|
||||
Qt.callLater(function() { O_StickyViewModel.releaseImage(old) })
|
||||
}
|
||||
function onSignalRequestFailed(errorMsg) {
|
||||
console.warn("[TZStickyWindow] AI request failed: " + errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
function openChatPanel() {
|
||||
var imgLeft = stickyWin.x
|
||||
var imgTop = stickyWin.y
|
||||
var imgBottom = imgTop + stickyWin.displayH
|
||||
|
||||
var screenH = Screen.desktopAvailableHeight
|
||||
var screenW = Screen.desktopAvailableWidth
|
||||
|
||||
var cx = imgLeft
|
||||
if (cx + chatPanel.win.width > screenW)
|
||||
cx = imgLeft + stickyWin.displayW - chatPanel.win.width
|
||||
|
||||
var cy
|
||||
if (imgBottom + chatPanelGap + chatPanel.win.height <= screenH)
|
||||
cy = imgBottom + chatPanelGap
|
||||
else
|
||||
cy = imgTop - chatPanelGap - chatPanel.win.height
|
||||
|
||||
chatPanel.open(Math.max(0, cx), Math.max(0, cy))
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
onAboutToShow: stickyWin.contextMenuOpen = true
|
||||
onAboutToHide: stickyWin.contextMenuOpen = false
|
||||
|
||||
Platform.MenuItem { text: qsTr("当前透明度 %1%").arg(Math.round(stickyWin.imgOpacity * 100)); enabled: false }
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem { visible: menuPrefs.pinSaveMerged; text: qsTr("覆盖贴图(合并标注)"); onTriggered: overwriteWithEdits() }
|
||||
Platform.MenuItem { visible: menuPrefs.pinCopy; text: qsTr("复制贴图"); onTriggered: O_StickyViewModel.copyImageToClipboard(imageUrl) }
|
||||
Platform.MenuItem { visible: menuPrefs.pinAi; text: qsTr("AI 编辑"); onTriggered: openChatPanel() }
|
||||
Platform.MenuItem {
|
||||
visible: menuPrefs.pinToolbar
|
||||
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
|
||||
onTriggered: {
|
||||
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
|
||||
if (!stickyWin.toolbarVisible) {
|
||||
stickyTool.activeTool = ""
|
||||
stickyWin.activeTool = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Platform.MenuItem { visible: menuPrefs.pinClose; text: qsTr("关闭窗口"); onTriggered: stickyWin.close() }
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem { text: qsTr("透明度 +5%"); onTriggered: setOpacityValue(stickyWin.imgOpacity + 0.05) }
|
||||
Platform.MenuItem { text: qsTr("透明度 -5%"); onTriggered: setOpacityValue(stickyWin.imgOpacity - 0.05) }
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem { text: qsTr("缩小"); onTriggered: setZoomValue(stickyWin.zoomFactor - 0.1) }
|
||||
Platform.MenuItem { text: qsTr("放大"); onTriggered: setZoomValue(stickyWin.zoomFactor + 0.1) }
|
||||
Platform.MenuItem { text: qsTr("旋转 90°"); onTriggered: rotateCurrent(90) }
|
||||
Platform.MenuItem { text: qsTr("镜像"); onTriggered: mirrorCurrent() }
|
||||
Platform.MenuItem { text: qsTr("恢复原始大小"); onTriggered: resetDisplaySize() }
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("重置位置")
|
||||
onTriggered: {
|
||||
syncInitialPosition()
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: stickyWin.toolbarVisible ? qsTr("隐藏工具栏") : qsTr("显示工具栏")
|
||||
onTriggered: {
|
||||
stickyWin.toolbarVisible = !stickyWin.toolbarVisible
|
||||
if (!stickyWin.toolbarVisible) {
|
||||
stickyTool.activeTool = ""
|
||||
stickyWin.activeTool = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuItem { text: qsTr("AI 编辑"); onTriggered: openChatPanel() }
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem { text: qsTr("保存原图"); onTriggered: saveDialog.open() }
|
||||
Platform.MenuItem { text: qsTr("另存为(合并标注)"); onTriggered: mergedSaveDialog.open() }
|
||||
Platform.MenuItem { text: qsTr("覆盖贴图(合并标注)"); onTriggered: overwriteWithEdits() }
|
||||
Platform.MenuItem { text: qsTr("复制贴图"); onTriggered: O_StickyViewModel.copyImageToClipboard(imageUrl) }
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.Menu {
|
||||
title: qsTr("常用项置顶")
|
||||
Platform.MenuItem { text: qsTr("覆盖贴图"); checkable: true; checked: menuPrefs.pinSaveMerged; onTriggered: menuPrefs.pinSaveMerged = !menuPrefs.pinSaveMerged }
|
||||
Platform.MenuItem { text: qsTr("复制贴图"); checkable: true; checked: menuPrefs.pinCopy; onTriggered: menuPrefs.pinCopy = !menuPrefs.pinCopy }
|
||||
Platform.MenuItem { text: qsTr("AI 编辑"); checkable: true; checked: menuPrefs.pinAi; onTriggered: menuPrefs.pinAi = !menuPrefs.pinAi }
|
||||
Platform.MenuItem { text: qsTr("显示/隐藏工具栏"); checkable: true; checked: menuPrefs.pinToolbar; onTriggered: menuPrefs.pinToolbar = !menuPrefs.pinToolbar }
|
||||
Platform.MenuItem { text: qsTr("关闭窗口"); checkable: true; checked: menuPrefs.pinClose; onTriggered: menuPrefs.pinClose = !menuPrefs.pinClose }
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
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(imageUrl, saveDialog.file)
|
||||
}
|
||||
|
||||
Platform.FileDialog {
|
||||
id: mergedSaveDialog
|
||||
title: qsTr("另存为(合并标注)")
|
||||
folder: O_ImageSaver.savePath
|
||||
fileMode: Platform.FileDialog.SaveFile
|
||||
nameFilters: ["PNG (*.png)", "JPG (*.jpg)", "BMP (*.bmp)"]
|
||||
onAccepted: saveMergedTo(mergedSaveDialog.file)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
enabled: stickyWin.activeTool === "" && !stickyWin.contextMenuOpen
|
||||
property point dragPos: Qt.point(0, 0)
|
||||
property bool leftDragActive: false
|
||||
z: 0
|
||||
|
||||
function inImageArea(mx, my) {
|
||||
return mx >= 0
|
||||
&& mx <= stickyWin.displayW
|
||||
&& my >= 0
|
||||
&& my <= stickyWin.displayH
|
||||
}
|
||||
|
||||
onPressed: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
dragPos = Qt.point(mouseX, mouseY)
|
||||
leftDragActive = true
|
||||
} else {
|
||||
leftDragActive = false
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: function(mouse) {
|
||||
//console.log("mouse.x",mouseX)
|
||||
if (pressed && leftDragActive && (mouse.buttons & Qt.LeftButton) && stickyWin.activeTool === "" && !stickyWin.contextMenuOpen) {
|
||||
stickyWin.x += mouseX - dragPos.x
|
||||
stickyWin.y += mouseY - dragPos.y
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: function(mouse) {
|
||||
if (!(mouse.buttons & Qt.LeftButton))
|
||||
leftDragActive = false
|
||||
}
|
||||
|
||||
onDoubleClicked: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton && inImageArea(mouseX, mouseY))
|
||||
resetDisplaySize()
|
||||
}
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
leftDragActive = false
|
||||
contextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: function(wheel) {
|
||||
if (wheel.angleDelta.y > 0)
|
||||
setZoomValue(stickyWin.zoomFactor + 0.1)
|
||||
else if (wheel.angleDelta.y < 0)
|
||||
setZoomValue(stickyWin.zoomFactor - 0.1)
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-19
@@ -1,8 +1,9 @@
|
||||
import QtQuick
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Layouts
|
||||
import com.Tz.tray 1.0
|
||||
import "Capture"
|
||||
|
||||
TZWindow {
|
||||
id: root
|
||||
@@ -31,6 +32,7 @@ TZWindow {
|
||||
|
||||
paintBoard.reset()
|
||||
tool.visible = false
|
||||
|
||||
tool.activeTool = ""
|
||||
tool.markToolInfoVisible = false
|
||||
|
||||
@@ -59,22 +61,12 @@ TZWindow {
|
||||
|
||||
Qt.callLater(function() {
|
||||
O_ScreenCapture.captureDesktop()
|
||||
|
||||
var screens = Qt.application.screens
|
||||
if (screens.length === 0)
|
||||
var virtualRect = O_ScreenCapture.virtualGeometry()
|
||||
if (virtualRect.width <= 0 || virtualRect.height <= 0)
|
||||
return
|
||||
|
||||
// Single-screen mode: always use the first screen as capture target.
|
||||
var primary = screens[0]
|
||||
|
||||
var vx = (primary.virtualX !== undefined) ? primary.virtualX : 0
|
||||
var vy = (primary.virtualY !== undefined) ? primary.virtualY : 0
|
||||
root.screenVirtualX = vx
|
||||
root.screenVirtualY = vy
|
||||
root.x = vx
|
||||
root.y = vy
|
||||
root.width = primary.width
|
||||
root.height = primary.height
|
||||
root.screenVirtualX = virtualRect.x
|
||||
root.screenVirtualY = virtualRect.y
|
||||
|
||||
O_ScreenCapture.grabToPaintBoard(
|
||||
root.paintBoard,
|
||||
@@ -100,7 +92,7 @@ TZWindow {
|
||||
return
|
||||
}
|
||||
if (longCaptureBarComponent === null)
|
||||
longCaptureBarComponent = Qt.createComponent("qrc:/qml/TZLongCaptureBar.qml")
|
||||
longCaptureBarComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCaptureBar.qml")
|
||||
if (longCaptureBarComponent.status === Component.Ready) {
|
||||
longCaptureBarObj = longCaptureBarComponent.createObject(null)
|
||||
if (longCaptureBarObj) {
|
||||
@@ -136,7 +128,7 @@ TZWindow {
|
||||
return
|
||||
}
|
||||
if (longCapturePreviewComponent === null)
|
||||
longCapturePreviewComponent = Qt.createComponent("qrc:/qml/TZLongCapturePreview.qml")
|
||||
longCapturePreviewComponent = Qt.createComponent("qrc:/qml/LongCapture/TZLongCapturePreview.qml")
|
||||
if (longCapturePreviewComponent.status === Component.Ready) {
|
||||
longCapturePreviewObj = longCapturePreviewComponent.createObject(null)
|
||||
if (longCapturePreviewObj) {
|
||||
@@ -152,7 +144,7 @@ TZWindow {
|
||||
|
||||
function showLongCaptureFrame(captureRect) {
|
||||
if (longCaptureFrameComponent === null)
|
||||
longCaptureFrameComponent = Qt.createComponent("qrc:/qml/TZLongCaptureFrame.qml")
|
||||
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) {
|
||||
@@ -343,7 +335,18 @@ TZWindow {
|
||||
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()
|
||||
|
||||
+55
-56
@@ -1,4 +1,5 @@
|
||||
#include "magnifier_provider.h"
|
||||
#include "magnifier_provider.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel)
|
||||
@@ -7,87 +8,85 @@ MagnifierProvider::MagnifierProvider(ScreenshotViewModel *viewModel)
|
||||
{
|
||||
}
|
||||
|
||||
// requestImage
|
||||
// ------------
|
||||
// id 格式:"<x>,<y>"(由 QML 传入的逻辑像素鼠标坐标)
|
||||
QImage MagnifierProvider::requestImage(const QString &id, QSize *size, const QSize & /*requestedSize*/)
|
||||
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();
|
||||
if (size) {
|
||||
*size = blank.size();
|
||||
}
|
||||
return blank;
|
||||
}
|
||||
|
||||
bool okX = false;
|
||||
bool okY = false;
|
||||
const int cx = parts[0].toInt(&okX);
|
||||
const int cy = parts[1].toInt(&okY);
|
||||
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();
|
||||
if (size) {
|
||||
*size = blank.size();
|
||||
}
|
||||
return blank;
|
||||
}
|
||||
|
||||
// 从桌面快照中取一份线程安全的副本
|
||||
const QImage snapshot = m_viewModel->desktopSnapshot();
|
||||
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();
|
||||
if (size) {
|
||||
*size = blank.size();
|
||||
}
|
||||
return blank;
|
||||
}
|
||||
|
||||
// 逻辑坐标 → 快照像素坐标(按虚拟桌面逻辑尺寸与快照物理像素比例换算,支持多屏)
|
||||
const QRect vg = m_viewModel->virtualGeometry();
|
||||
const qreal scaleX = (vg.width() > 0) ? (qreal(snapshot.width()) / qreal(vg.width())) : 1.0;
|
||||
const qreal scaleY = (vg.height() > 0) ? (qreal(snapshot.height()) / qreal(vg.height())) : 1.0;
|
||||
const int pcx = qRound((cx - vg.x()) * scaleX);
|
||||
const int pcy = qRound((cy - vg.y()) * scaleY);
|
||||
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);
|
||||
|
||||
// 采样半径按 X 轴比例缩放;Y 轴保持一致比例,保证中心点准确
|
||||
const int srcRadiusX = qMax(1, qRound(kSrcRadius * scaleX));
|
||||
const int srcRadiusY = qMax(1, qRound(kSrcRadius * scaleY));
|
||||
const int srcDiamW = srcRadiusX * 2;
|
||||
const int srcDiamH = srcRadiusY * 2;
|
||||
|
||||
// 计算裁剪矩形(以物理像素光标为中心)
|
||||
QRect srcRect(pcx - srcRadiusX, pcy - srcRadiusY, srcDiamW, srcDiamH);
|
||||
|
||||
// 处理边界:当光标靠近屏幕边缘时,用黑色填充越界区域
|
||||
QImage region(srcDiamW, srcDiamH, QImage::Format_ARGB32_Premultiplied);
|
||||
region.fill(Qt::black);
|
||||
{
|
||||
QPainter rp(®ion);
|
||||
QRect bounded = srcRect.intersected(snapshot.rect());
|
||||
if (!bounded.isEmpty()) {
|
||||
int destX = bounded.x() - srcRect.x();
|
||||
int destY = bounded.y() - srcRect.y();
|
||||
rp.drawImage(destX, destY, snapshot, bounded.x(), bounded.y(),
|
||||
bounded.width(), bounded.height());
|
||||
}
|
||||
rp.end();
|
||||
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();
|
||||
|
||||
// 放大到 kOutputSize×kOutputSize
|
||||
QImage zoomed = region.scaled(kOutputSize, kOutputSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
QImage zoomed = region.scaled(kOutputSize,
|
||||
kOutputSize,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
|
||||
// 绘制红色十字准线(1px 宽)
|
||||
{
|
||||
QPainter p(&zoomed);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
QPen pen(QColor(255, 0, 0, 220), 1);
|
||||
p.setPen(pen);
|
||||
const int half = kOutputSize / 2;
|
||||
p.drawLine(half, 0, half, kOutputSize - 1); // 竖线
|
||||
p.drawLine(0, half, kOutputSize - 1, half); // 横线
|
||||
p.end();
|
||||
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();
|
||||
}
|
||||
|
||||
if (size) *size = zoomed.size();
|
||||
return zoomed;
|
||||
}
|
||||
+13
-2
@@ -1,6 +1,8 @@
|
||||
#include <QApplication>
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickWindow>
|
||||
#include <QScreen>
|
||||
|
||||
#include "tray_icon_helper.h"
|
||||
#include "paint_board/paint_board.h"
|
||||
@@ -77,7 +79,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
// ── 暴露给 QML 的 ViewModel ───────────────────────────
|
||||
engine.rootContext()->setContextProperty("O_ScreenCapture", &screenshotViewModel);
|
||||
engine.rootContext()->setContextProperty("O_StickyViewModel", &stickyViewModel);
|
||||
engine.rootContext()->setContextProperty("O_StickyViewModel", &stickyViewModel);
|
||||
engine.rootContext()->setContextProperty("O_AIViewModel", &aiViewModel);
|
||||
engine.rootContext()->setContextProperty("O_ImageSaver", &storageViewModel);
|
||||
engine.rootContext()->setContextProperty("O_GlobalShortcut", &globalShortcut);
|
||||
@@ -94,5 +96,14 @@ int main(int argc, char *argv[])
|
||||
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();
|
||||
}
|
||||
+224
-121
@@ -1,134 +1,243 @@
|
||||
#include "desktop_snapshot.h"
|
||||
#include "desktop_snapshot.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QScreen>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QDir>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QScreen>
|
||||
|
||||
/* ── grab / release / pixelColor / windowAtPoint ────────
|
||||
* 这几个函数与平台无关,统一实现
|
||||
* ────────────────────────────────────────────────────── */
|
||||
#ifdef Q_OS_WIN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
QRect desktopGeometry()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return QRect(GetSystemMetrics(SM_XVIRTUALSCREEN),
|
||||
GetSystemMetrics(SM_YVIRTUALSCREEN),
|
||||
GetSystemMetrics(SM_CXVIRTUALSCREEN),
|
||||
GetSystemMetrics(SM_CYVIRTUALSCREEN));
|
||||
#else
|
||||
QRect geometry;
|
||||
|
||||
for (QScreen* const screen : QGuiApplication::screens()) {
|
||||
QRect scrRect = screen->geometry();
|
||||
// https://doc.qt.io/qt-6/highdpi.html#device-independent-screen-geometry
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
scrRect.moveTo(QPointF(scrRect.x() / dpr, scrRect.y() / dpr).toPoint());
|
||||
geometry = geometry.united(scrRect);
|
||||
}
|
||||
return geometry;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QImage captureVirtualDesktopImage(const QRect& geometry)
|
||||
{
|
||||
HDC screenDC = GetDC(nullptr);
|
||||
HDC memoryDC = CreateCompatibleDC(screenDC);
|
||||
HBITMAP bitmap = CreateCompatibleBitmap(screenDC, geometry.width(), geometry.height());
|
||||
DeleteObject(SelectObject(memoryDC, bitmap));
|
||||
BitBlt(memoryDC,
|
||||
0,
|
||||
0,
|
||||
geometry.width(),
|
||||
geometry.height(),
|
||||
screenDC,
|
||||
geometry.x(),
|
||||
geometry.y(),
|
||||
SRCCOPY);
|
||||
|
||||
QImage image(geometry.width(), geometry.height(), QImage::Format_ARGB32_Premultiplied);
|
||||
BITMAPINFO bmi = { sizeof(BITMAPINFOHEADER),
|
||||
static_cast<LONG>(geometry.width()),
|
||||
-static_cast<LONG>(geometry.height()),
|
||||
1,
|
||||
32,
|
||||
BI_RGB,
|
||||
static_cast<DWORD>(geometry.width() * 4 * geometry.height()),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0 };
|
||||
GetDIBits(memoryDC,
|
||||
bitmap,
|
||||
0,
|
||||
geometry.height(),
|
||||
image.bits(),
|
||||
&bmi,
|
||||
DIB_RGB_COLORS);
|
||||
|
||||
DeleteDC(memoryDC);
|
||||
DeleteObject(bitmap);
|
||||
ReleaseDC(nullptr, screenDC);
|
||||
return image;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void DesktopSnapshot::grab()
|
||||
{
|
||||
QScreen *primary = QGuiApplication::primaryScreen();
|
||||
if (!primary) {
|
||||
qWarning() << "[DesktopSnapshot] 无法获取主屏";
|
||||
const QRect geometry = desktopGeometry();
|
||||
if (geometry.isEmpty()) {
|
||||
release();
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待合成器/DWM 将截图窗口从帧缓冲移除
|
||||
QEventLoop loop;
|
||||
QTimer::singleShot(50, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
#ifdef Q_OS_WIN
|
||||
const QImage image = captureVirtualDesktopImage(geometry);
|
||||
if (image.isNull()) {
|
||||
release();
|
||||
return;
|
||||
}
|
||||
m_snapshot = QPixmap::fromImage(image);
|
||||
#else
|
||||
const QList<QScreen*> screens = QGuiApplication::screens();
|
||||
const int minLogicalX = geometry.x();
|
||||
const int minLogicalY = geometry.y();
|
||||
|
||||
// 单屏模式:仅使用主屏坐标系。
|
||||
m_virtualGeometry = primary->geometry();
|
||||
qDebug() << "[DesktopSnapshot] 单屏模式,主屏逻辑矩形:" << m_virtualGeometry;
|
||||
QPixmap desktop(geometry.size());
|
||||
desktop.fill(Qt::black);
|
||||
|
||||
// 创建与虚拟桌面等大的物理像素画布(以主屏 DPR 为基准)
|
||||
// 主屏 DPR 即虚拟桌面坐标系的缩放基准,与 QML 侧一致
|
||||
const qreal baseDpr = primary->devicePixelRatio();
|
||||
|
||||
const int canvasW = qRound(m_virtualGeometry.width() * baseDpr);
|
||||
const int canvasH = qRound(m_virtualGeometry.height() * baseDpr);
|
||||
m_snapshot = QImage(canvasW, canvasH, QImage::Format_ARGB32_Premultiplied);
|
||||
m_snapshot.fill(Qt::black);
|
||||
|
||||
// 仅抓取主屏
|
||||
QPainter painter(&m_snapshot);
|
||||
QImage img = primary->grabWindow(0).toImage();
|
||||
if (img.width() != canvasW || img.height() != canvasH)
|
||||
img = img.scaled(canvasW, canvasH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
painter.drawImage(0, 0, img);
|
||||
QPainter painter(&desktop);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
for (QScreen* screen : screens) {
|
||||
if (!screen) {
|
||||
continue;
|
||||
}
|
||||
const QRect screenGeom = screen->geometry();
|
||||
painter.drawPixmap(QRect(screenGeom.x() - minLogicalX,
|
||||
screenGeom.y() - minLogicalY,
|
||||
screenGeom.width(),
|
||||
screenGeom.height()),
|
||||
screen->grabWindow(0));
|
||||
}
|
||||
painter.end();
|
||||
m_snapshot = desktop;
|
||||
#endif
|
||||
|
||||
qDebug() << "[DesktopSnapshot] 快照已缓存,尺寸:"
|
||||
<< m_snapshot.width() << "x" << m_snapshot.height();
|
||||
|
||||
m_virtualGeometry = geometry;
|
||||
buildWindowList();
|
||||
}
|
||||
|
||||
void DesktopSnapshot::release()
|
||||
{
|
||||
m_snapshot = QImage();
|
||||
m_snapshot = QPixmap();
|
||||
m_virtualGeometry = QRect();
|
||||
m_windowRects.clear();
|
||||
}
|
||||
|
||||
QPoint DesktopSnapshot::mapLogicalPointToPhysical(const QPoint &logicalPoint) const
|
||||
{
|
||||
if (m_virtualGeometry.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return logicalPoint - m_virtualGeometry.topLeft();
|
||||
}
|
||||
|
||||
QRect DesktopSnapshot::mapLogicalRectToPhysical(const QRect &logicalRect) const
|
||||
{
|
||||
const QRect normalized = logicalRect.normalized();
|
||||
if (normalized.isEmpty() || m_virtualGeometry.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
QRect translated = normalized.translated(-m_virtualGeometry.topLeft());
|
||||
return translated.intersected(m_snapshot.rect());
|
||||
}
|
||||
|
||||
QColor DesktopSnapshot::pixelColor(int logicalX, int logicalY) const
|
||||
{
|
||||
if (m_snapshot.isNull() || m_virtualGeometry.isEmpty()) return QColor();
|
||||
#ifdef Q_OS_WIN
|
||||
HDC screenDC = GetDC(nullptr);
|
||||
if (!screenDC) {
|
||||
return {};
|
||||
}
|
||||
const COLORREF color = GetPixel(screenDC, logicalX, logicalY);
|
||||
ReleaseDC(nullptr, screenDC);
|
||||
if (color == CLR_INVALID) {
|
||||
return {};
|
||||
}
|
||||
return QColor(GetRValue(color), GetGValue(color), GetBValue(color));
|
||||
#else
|
||||
if (m_snapshot.isNull() || m_virtualGeometry.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 逻辑坐标相对于虚拟桌面原点的偏移
|
||||
const int relX = logicalX - m_virtualGeometry.x();
|
||||
const int relY = logicalY - m_virtualGeometry.y();
|
||||
|
||||
// 按快照实际尺寸与虚拟桌面逻辑尺寸比例换算(统一 DPR)
|
||||
const qreal scaleX = m_virtualGeometry.width() > 0
|
||||
? qreal(m_snapshot.width()) / qreal(m_virtualGeometry.width()) : 1.0;
|
||||
const qreal scaleY = m_virtualGeometry.height() > 0
|
||||
? qreal(m_snapshot.height()) / qreal(m_virtualGeometry.height()) : 1.0;
|
||||
|
||||
const int px = qRound(relX * scaleX);
|
||||
const int py = qRound(relY * scaleY);
|
||||
|
||||
if (px < 0 || py < 0 || px >= m_snapshot.width() || py >= m_snapshot.height())
|
||||
return QColor();
|
||||
|
||||
return QColor(m_snapshot.pixel(px, py));
|
||||
const QPoint pixelPoint = mapLogicalPointToPhysical(QPoint(logicalX, logicalY));
|
||||
if (!m_snapshot.rect().contains(pixelPoint)) {
|
||||
return {};
|
||||
}
|
||||
const QImage image = m_snapshot.toImage();
|
||||
return QColor::fromRgba(image.pixel(pixelPoint));
|
||||
#endif
|
||||
}
|
||||
|
||||
QRect DesktopSnapshot::windowAtPoint(int x, int y) const
|
||||
{
|
||||
for (const QRect &r : m_windowRects) {
|
||||
if (r.contains(x, y))
|
||||
if (r.contains(x, y)) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return QRect();
|
||||
return {};
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* buildWindowList — 平台专属实现
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ────────────────────────────────────────────────────────
|
||||
* Linux / Unix:通过 _NET_CLIENT_LIST_STACKING 枚举窗口
|
||||
* ──────────────────────────────────────────────────────── */
|
||||
#ifdef Q_OS_UNIX
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
void DesktopSnapshot::buildWindowList()
|
||||
{
|
||||
m_windowRects.clear();
|
||||
|
||||
Display *dpy = XOpenDisplay(nullptr);
|
||||
if (!dpy) return;
|
||||
if (!dpy) {
|
||||
return;
|
||||
}
|
||||
|
||||
Window root = DefaultRootWindow(dpy);
|
||||
|
||||
Atom netClientListStacking = XInternAtom(dpy, "_NET_CLIENT_LIST_STACKING", False);
|
||||
Atom actualType;
|
||||
int actualFormat;
|
||||
unsigned long nitems = 0, bytesAfter = 0;
|
||||
unsigned char *data = nullptr;
|
||||
int actualFormat;
|
||||
unsigned long nitems = 0;
|
||||
unsigned long bytesAfter = 0;
|
||||
unsigned char *data = nullptr;
|
||||
|
||||
int ret = XGetWindowProperty(dpy, root, netClientListStacking,
|
||||
0, 65536, False, XA_WINDOW,
|
||||
&actualType, &actualFormat,
|
||||
&nitems, &bytesAfter, &data);
|
||||
int ret = XGetWindowProperty(dpy,
|
||||
root,
|
||||
netClientListStacking,
|
||||
0,
|
||||
65536,
|
||||
False,
|
||||
XA_WINDOW,
|
||||
&actualType,
|
||||
&actualFormat,
|
||||
&nitems,
|
||||
&bytesAfter,
|
||||
&data);
|
||||
|
||||
if (ret != Success || !data) {
|
||||
Atom netClientList = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
|
||||
ret = XGetWindowProperty(dpy, root, netClientList,
|
||||
0, 65536, False, XA_WINDOW,
|
||||
&actualType, &actualFormat,
|
||||
&nitems, &bytesAfter, &data);
|
||||
ret = XGetWindowProperty(dpy,
|
||||
root,
|
||||
netClientList,
|
||||
0,
|
||||
65536,
|
||||
False,
|
||||
XA_WINDOW,
|
||||
&actualType,
|
||||
&actualFormat,
|
||||
&nitems,
|
||||
&bytesAfter,
|
||||
&data);
|
||||
if (ret != Success || !data) {
|
||||
XCloseDisplay(dpy);
|
||||
return;
|
||||
@@ -136,92 +245,89 @@ void DesktopSnapshot::buildWindowList()
|
||||
}
|
||||
|
||||
Window *wins = reinterpret_cast<Window*>(data);
|
||||
|
||||
for (long i = static_cast<long>(nitems) - 1; i >= 0; --i) {
|
||||
Window w = wins[i];
|
||||
|
||||
XWindowAttributes attr;
|
||||
if (!XGetWindowAttributes(dpy, w, &attr)) continue;
|
||||
if (attr.map_state != IsViewable) continue;
|
||||
if (attr.width < 10) continue;
|
||||
if (attr.height < 10) continue;
|
||||
if (!XGetWindowAttributes(dpy, w, &attr) || attr.map_state != IsViewable ||
|
||||
attr.width < 10 || attr.height < 10) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int screenX = 0, screenY = 0;
|
||||
int screenX = 0;
|
||||
int screenY = 0;
|
||||
Window child;
|
||||
XTranslateCoordinates(dpy, w, root, 0, 0, &screenX, &screenY, &child);
|
||||
|
||||
QRect r(screenX, screenY, attr.width, attr.height);
|
||||
r = r.intersected(m_virtualGeometry);
|
||||
if (r.isEmpty()) continue;
|
||||
|
||||
m_windowRects.append(r);
|
||||
if (!r.isEmpty()) {
|
||||
m_windowRects.append(r);
|
||||
}
|
||||
}
|
||||
|
||||
XFree(data);
|
||||
XCloseDisplay(dpy);
|
||||
|
||||
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
|
||||
}
|
||||
|
||||
#endif /* Q_OS_UNIX */
|
||||
#endif
|
||||
|
||||
/* ────────────────────────────────────────────────────────
|
||||
* Windows:通过 EnumWindows 枚举可见顶层窗口
|
||||
* ──────────────────────────────────────────────────────── */
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
/* EnumWindows 回调:收集所有可见且有标题栏的顶层窗口矩形 */
|
||||
struct EnumWindowsParam {
|
||||
QVector<QRect> *rects;
|
||||
QRect snapshotRect; /* 逻辑像素,用于裁剪 */
|
||||
QRect snapshotRect;
|
||||
};
|
||||
|
||||
static qreal hwndScaleFactor(HWND hwnd)
|
||||
{
|
||||
using GetDpiForWindowFn = UINT (WINAPI *)(HWND);
|
||||
static GetDpiForWindowFn fn =
|
||||
reinterpret_cast<GetDpiForWindowFn>(
|
||||
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
|
||||
using GetDpiForWindowFn = UINT(WINAPI *)(HWND);
|
||||
static GetDpiForWindowFn fn = reinterpret_cast<GetDpiForWindowFn>(
|
||||
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
|
||||
if (fn) {
|
||||
const UINT dpi = fn(hwnd);
|
||||
if (dpi > 0)
|
||||
if (dpi > 0) {
|
||||
return qreal(dpi) / 96.0;
|
||||
}
|
||||
}
|
||||
return QGuiApplication::primaryScreen()
|
||||
? QGuiApplication::primaryScreen()->devicePixelRatio()
|
||||
: 1.0;
|
||||
? QGuiApplication::primaryScreen()->devicePixelRatio()
|
||||
: 1.0;
|
||||
}
|
||||
|
||||
static BOOL CALLBACK enumWindowsProc(HWND hwnd, LPARAM lParam)
|
||||
{
|
||||
EnumWindowsParam *param = reinterpret_cast<EnumWindowsParam*>(lParam);
|
||||
|
||||
if (!IsWindowVisible(hwnd)) return TRUE;
|
||||
if (IsIconic(hwnd)) return TRUE; /* 最小化,跳过 */
|
||||
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* 跳过没有 WS_CAPTION 的工具窗口、桌面壳层等 */
|
||||
LONG style = GetWindowLong(hwnd, GWL_STYLE);
|
||||
if (!(style & WS_CAPTION)) return TRUE;
|
||||
if (!(style & WS_CAPTION)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
RECT rc;
|
||||
if (!GetWindowRect(hwnd, &rc)) return TRUE;
|
||||
if (!GetWindowRect(hwnd, &rc)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int w = rc.right - rc.left;
|
||||
int h = rc.bottom - rc.top;
|
||||
if (w < 10 || h < 10) return TRUE;
|
||||
const int w = rc.right - rc.left;
|
||||
const int h = rc.bottom - rc.top;
|
||||
if (w < 10 || h < 10) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* GetWindowRect 返回物理像素坐标,按窗口自身 DPI 换算逻辑坐标 */
|
||||
const qreal scale = hwndScaleFactor(hwnd);
|
||||
QRect r(qRound(rc.left / scale),
|
||||
qRound(rc.top / scale),
|
||||
qRound(w / scale),
|
||||
qRound(h / scale));
|
||||
r = r.intersected(param->snapshotRect);
|
||||
if (!r.isEmpty())
|
||||
param->rects->append(r); /* EnumWindows 本身按 Z 序(顶->底)枚举,append 才能保持从顶到底 */
|
||||
if (!r.isEmpty()) {
|
||||
param->rects->append(r);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -229,11 +335,8 @@ static BOOL CALLBACK enumWindowsProc(HWND hwnd, LPARAM lParam)
|
||||
void DesktopSnapshot::buildWindowList()
|
||||
{
|
||||
m_windowRects.clear();
|
||||
|
||||
EnumWindowsParam param { &m_windowRects, m_virtualGeometry };
|
||||
EnumWindows(enumWindowsProc, reinterpret_cast<LPARAM>(¶m));
|
||||
|
||||
qDebug() << "[DesktopSnapshot] 枚举到顶层窗口数:" << m_windowRects.size();
|
||||
}
|
||||
|
||||
#endif /* Q_OS_WIN */
|
||||
#endif
|
||||
@@ -1,46 +1,43 @@
|
||||
#ifndef DESKTOP_SNAPSHOT_H
|
||||
#define DESKTOP_SNAPSHOT_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QColor>
|
||||
#include <QPoint>
|
||||
#include <QPixmap>
|
||||
#include <QRect>
|
||||
#include <QVector>
|
||||
#include <QColor>
|
||||
|
||||
// DesktopSnapshot — Model
|
||||
// 负责全屏快照的采集与存储,以及顶层窗口列表的枚举。
|
||||
// 支持多屏:将所有屏幕的快照拼合为一张以虚拟桌面坐标系为基准的大图。
|
||||
// 不依赖 QML,不继承 QObject,纯数据/系统调用。
|
||||
|
||||
class DesktopSnapshot
|
||||
{
|
||||
public:
|
||||
DesktopSnapshot() = default;
|
||||
|
||||
// 抓取所有屏幕快照并拼合,枚举顶层窗口列表(含 50ms 延时等待合成器)
|
||||
void grab();
|
||||
// 释放快照和窗口列表
|
||||
void release();
|
||||
|
||||
bool isNull() const { return m_snapshot.isNull(); }
|
||||
|
||||
// 拼合后的虚拟桌面快照(物理像素坐标系)
|
||||
const QImage& image() const { return m_snapshot; }
|
||||
|
||||
// 虚拟桌面的逻辑坐标矩形(所有屏幕的 geometry 联合,逻辑像素)
|
||||
const QPixmap& image() const { return m_snapshot; }
|
||||
QRect virtualGeometry() const { return m_virtualGeometry; }
|
||||
|
||||
// 将逻辑坐标 (x,y) 转换为快照中的颜色(支持多屏 DPI 混合)
|
||||
QColor pixelColor(int logicalX, int logicalY) const;
|
||||
|
||||
// 返回包含逻辑坐标点 (x,y) 的最上层窗口矩形(逻辑坐标)
|
||||
QPoint mapLogicalPointToPhysical(const QPoint &logicalPoint) const;
|
||||
QRect mapLogicalRectToPhysical(const QRect &logicalRect) const;
|
||||
QRect windowAtPoint(int x, int y) const;
|
||||
|
||||
private:
|
||||
void buildWindowList();
|
||||
struct ScreenEntry {
|
||||
QRect logicalGeometry;
|
||||
QRect physicalRect;
|
||||
qreal dpr = 1.0;
|
||||
};
|
||||
|
||||
QImage m_snapshot;
|
||||
QRect m_virtualGeometry; // 虚拟桌面逻辑矩形(所有屏幕 geometry 的联合)
|
||||
QVector<QRect> m_windowRects; // 按 Z 轴从顶到底排列(逻辑坐标)
|
||||
void buildWindowList();
|
||||
const ScreenEntry* screenEntryForPoint(const QPoint &logicalPoint) const;
|
||||
|
||||
QPixmap m_snapshot;
|
||||
QRect m_virtualGeometry;
|
||||
QVector<QRect> m_windowRects;
|
||||
QVector<ScreenEntry> m_screenEntries;
|
||||
};
|
||||
|
||||
#endif // DESKTOP_SNAPSHOT_H
|
||||
@@ -1,13 +1,51 @@
|
||||
#include "screenshot_view_model.h"
|
||||
#include "screenshot_view_model.h"
|
||||
#include "paint_board/paint_board.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
#include <QDebug>
|
||||
#include <QQuickWindow>
|
||||
#include <QScreen>
|
||||
#include <QWindow>
|
||||
|
||||
namespace {
|
||||
|
||||
qreal captureDprForRect(const QRect& rect)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
if (QScreen* screen = QGuiApplication::screenAt(rect.center())) {
|
||||
return screen->devicePixelRatio();
|
||||
}
|
||||
#endif
|
||||
if (QScreen* screen = QGuiApplication::primaryScreen()) {
|
||||
return screen->devicePixelRatio();
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
QRect captureRectInSnapshot(const DesktopSnapshot& snapshot, const QRect& rect, qreal dpr)
|
||||
{
|
||||
if (snapshot.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (rect.isEmpty()) {
|
||||
return snapshot.image().rect();
|
||||
}
|
||||
|
||||
const QRect localRect = rect.translated(-snapshot.virtualGeometry().topLeft());
|
||||
const QPoint topLeft = QPoint(qRound(localRect.left() * dpr),
|
||||
qRound(localRect.top() * dpr));
|
||||
const QPoint bottomRight = QPoint(qRound(localRect.right() * dpr),
|
||||
qRound(localRect.bottom() * dpr));
|
||||
QRect target;
|
||||
target.setCoords(topLeft.x(), topLeft.y(), bottomRight.x(), bottomRight.y());
|
||||
return target.intersected(snapshot.image().rect());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScreenshotViewModel::ScreenshotViewModel(DesktopSnapshot &snapshot,
|
||||
StickyImageStore &stickyStore,
|
||||
@@ -18,118 +56,120 @@ ScreenshotViewModel::ScreenshotViewModel(DesktopSnapshot &snapshot,
|
||||
{
|
||||
}
|
||||
|
||||
// ── captureDesktop ────────────────────────────────────────
|
||||
QRect ScreenshotViewModel::captureRectForDisplay(const QRect &rect) const
|
||||
{
|
||||
const QRect r = rect.normalized();
|
||||
if (r.isEmpty() || m_snapshot.isNull()) {
|
||||
return {};
|
||||
}
|
||||
return captureRectInSnapshot(m_snapshot, r, captureDprForRect(r));
|
||||
}
|
||||
|
||||
void ScreenshotViewModel::captureDesktop()
|
||||
{
|
||||
m_snapshot.grab();
|
||||
}
|
||||
|
||||
// ── releaseDesktopSnapshot ────────────────────────────────
|
||||
void ScreenshotViewModel::releaseDesktopSnapshot()
|
||||
{
|
||||
m_snapshot.release();
|
||||
}
|
||||
|
||||
// ── getPixelColor ─────────────────────────────────────────
|
||||
QColor ScreenshotViewModel::getPixelColor(int x, int y) const
|
||||
{
|
||||
return m_snapshot.pixelColor(x, y);
|
||||
}
|
||||
|
||||
// ── windowAtPoint ─────────────────────────────────────────
|
||||
QRect ScreenshotViewModel::windowAtPoint(int x, int y) const
|
||||
{
|
||||
return m_snapshot.windowAtPoint(x, y);
|
||||
}
|
||||
|
||||
// ── captureRectToClipboard ────────────────────────────────
|
||||
bool ScreenshotViewModel::captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect)
|
||||
{
|
||||
const QRect r = rect.normalized();
|
||||
if (r.isEmpty()) {
|
||||
qWarning() << "[ScreenshotViewModel] 截图区域为空";
|
||||
qWarning() << "[ScreenshotViewModel] capture area is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
QImage img = captureScreen(paintBoard, r);
|
||||
if (img.isNull()) {
|
||||
qWarning() << "[ScreenshotViewModel] 截图失败,捕获到空图片";
|
||||
qWarning() << "[ScreenshotViewModel] failed to capture image";
|
||||
return false;
|
||||
}
|
||||
|
||||
return writeImageToClipboard(img);
|
||||
}
|
||||
|
||||
// ── captureRectToBase64 ───────────────────────────────────
|
||||
QString ScreenshotViewModel::captureRectToBase64(QQuickItem *paintBoard, const QRect &rect)
|
||||
{
|
||||
const QRect r = rect.normalized();
|
||||
if (r.isEmpty()) {
|
||||
qWarning() << "[ScreenshotViewModel] 截图区域为空";
|
||||
qWarning() << "[ScreenshotViewModel] capture area is empty";
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage img = captureScreen(paintBoard, r);
|
||||
if (img.isNull()) {
|
||||
qWarning() << "[ScreenshotViewModel] 截图失败,捕获到空图片";
|
||||
qWarning() << "[ScreenshotViewModel] failed to capture image";
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray data;
|
||||
QBuffer buf(&data);
|
||||
QBuffer buf(&data);
|
||||
buf.open(QIODevice::WriteOnly);
|
||||
if (!img.save(&buf, "PNG")) {
|
||||
qWarning() << "[ScreenshotViewModel] PNG 编码失败";
|
||||
qWarning() << "[ScreenshotViewModel] failed to encode PNG";
|
||||
return {};
|
||||
}
|
||||
return data.toBase64();
|
||||
}
|
||||
|
||||
// ── captureRectToStickyUrl ────────────────────────────────
|
||||
QString ScreenshotViewModel::captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect)
|
||||
{
|
||||
const QRect r = rect.normalized();
|
||||
if (r.isEmpty()) {
|
||||
qWarning() << "[ScreenshotViewModel] 贴图区域为空";
|
||||
qWarning() << "[ScreenshotViewModel] sticky area is empty";
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage img = captureScreen(paintBoard, r);
|
||||
if (img.isNull()) {
|
||||
qWarning() << "[ScreenshotViewModel] 贴图截图失败";
|
||||
qWarning() << "[ScreenshotViewModel] failed to capture sticky image";
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_stickyStore.storeImage(img);
|
||||
}
|
||||
|
||||
// ── grabToPaintBoard ──────────────────────────────────────
|
||||
void ScreenshotViewModel::grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect)
|
||||
{
|
||||
const QRect r = rect.normalized();
|
||||
if (r.isEmpty()) return;
|
||||
captureScreen(paintBoard, r, /*setBackground=*/true);
|
||||
if (r.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
captureScreen(paintBoard, r, true);
|
||||
}
|
||||
|
||||
// ── captureRectToImage ────────────────────────────────────
|
||||
QImage ScreenshotViewModel::captureRectToImage(const QRect &rect)
|
||||
{
|
||||
const QRect r = rect.normalized();
|
||||
if (r.isEmpty()) {
|
||||
qWarning() << "[ScreenshotViewModel] OCR 区域为空";
|
||||
qWarning() << "[ScreenshotViewModel] OCR area is empty";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!m_snapshot.isNull()) {
|
||||
const QRect vg = m_snapshot.virtualGeometry();
|
||||
const qreal scaleX = vg.width() > 0 ? qreal(m_snapshot.image().width()) / qreal(vg.width()) : 1.0;
|
||||
const qreal scaleY = vg.height() > 0 ? qreal(m_snapshot.image().height()) / qreal(vg.height()) : 1.0;
|
||||
|
||||
const QRect physRect = QRect(qRound((r.x() - vg.x()) * scaleX),
|
||||
qRound((r.y() - vg.y()) * scaleY),
|
||||
qRound(r.width() * scaleX),
|
||||
qRound(r.height() * scaleY))
|
||||
.intersected(m_snapshot.image().rect());
|
||||
return m_snapshot.image().copy(physRect);
|
||||
const QRect captureRect = captureRectInSnapshot(m_snapshot, r, captureDprForRect(r));
|
||||
return m_snapshot.image().copy(captureRect).toImage();
|
||||
}
|
||||
|
||||
qWarning() << "[ScreenshotViewModel] 快照未缓存,OCR 回退到实时抓屏(仅主屏)";
|
||||
qWarning() << "[ScreenshotViewModel] snapshot unavailable, falling back to primary screen";
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) return {};
|
||||
if (!screen) {
|
||||
return {};
|
||||
}
|
||||
return screen->grabWindow(0, r.x(), r.y(), r.width(), r.height()).toImage();
|
||||
}
|
||||
|
||||
@@ -149,7 +189,6 @@ bool ScreenshotViewModel::copyImageToClipboard(const QImage &image)
|
||||
return writeImageToClipboard(image);
|
||||
}
|
||||
|
||||
// ── captureScreen (private) ───────────────────────────────
|
||||
QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard,
|
||||
const QRect &rect,
|
||||
bool setBackground)
|
||||
@@ -157,93 +196,86 @@ QImage ScreenshotViewModel::captureScreen(QQuickItem *paintBoard,
|
||||
PaintBoard *board = qobject_cast<PaintBoard*>(paintBoard);
|
||||
|
||||
QImage screenshot;
|
||||
qreal scaleX = 1.0;
|
||||
qreal scaleY = 1.0;
|
||||
qreal dpr = 1.0;
|
||||
|
||||
if (!m_snapshot.isNull()) {
|
||||
// 快照坐标系:逻辑坐标相对于虚拟桌面原点;物理像素按统一 DPR 缩放。
|
||||
// 将逻辑选区映射到快照物理像素矩形后裁剪。
|
||||
const QRect vg = m_snapshot.virtualGeometry();
|
||||
scaleX = vg.width() > 0 ? qreal(m_snapshot.image().width()) / qreal(vg.width()) : 1.0;
|
||||
scaleY = vg.height() > 0 ? qreal(m_snapshot.image().height()) / qreal(vg.height()) : 1.0;
|
||||
const QRect physRect = rect.isEmpty()
|
||||
? m_snapshot.image().rect()
|
||||
: QRect(qRound((rect.x() - vg.x()) * scaleX),
|
||||
qRound((rect.y() - vg.y()) * scaleY),
|
||||
qRound(rect.width() * scaleX),
|
||||
qRound(rect.height() * scaleY))
|
||||
.intersected(m_snapshot.image().rect());
|
||||
|
||||
screenshot = m_snapshot.image().copy(physRect);
|
||||
if (board && board->window()) {
|
||||
dpr = board->window()->devicePixelRatio();
|
||||
} else if (!rect.isEmpty()) {
|
||||
dpr = captureDprForRect(rect);
|
||||
}
|
||||
const QRect captureRect = captureRectInSnapshot(m_snapshot, rect, dpr);
|
||||
screenshot = m_snapshot.image().copy(captureRect).toImage();
|
||||
} else {
|
||||
qWarning() << "[ScreenshotViewModel] 快照未缓存,回退到实时抓屏(仅主屏)";
|
||||
qWarning() << "[ScreenshotViewModel] snapshot unavailable, falling back to primary screen";
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) return {};
|
||||
if (!screen) {
|
||||
return {};
|
||||
}
|
||||
screenshot = screen->grabWindow(0).toImage();
|
||||
}
|
||||
|
||||
if (board) {
|
||||
QImage annotations = board->renderToImage();
|
||||
if (!annotations.isNull()) {
|
||||
// 标注层是逻辑像素,先映射到物理像素坐标系,避免高 DPI 下偏移。
|
||||
const QSize physicalAnnoSize(
|
||||
qMax(1, qRound(annotations.width() * scaleX)),
|
||||
qMax(1, qRound(annotations.height() * scaleY)));
|
||||
if (annotations.size() != physicalAnnoSize) {
|
||||
annotations = annotations.scaled(physicalAnnoSize,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
QPainter p(&screenshot);
|
||||
|
||||
// PaintBoard 现在是全屏画布:导出选区时,需要从全屏标注层裁剪对应区域再叠加
|
||||
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 boardAbsX = windowAbsX + qRound(board->x());
|
||||
const int boardAbsY = windowAbsY + qRound(board->y());
|
||||
|
||||
const int wantX = qRound((rect.x() - boardAbsX) * scaleX);
|
||||
const int wantY = qRound((rect.y() - boardAbsY) * scaleY);
|
||||
QRect srcRect(wantX, wantY, screenshot.width(), screenshot.height());
|
||||
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 = srcRect.x() - wantX;
|
||||
const int dstY = srcRect.y() - wantY;
|
||||
p.drawImage(QPoint(dstX, dstY), annotations, srcRect);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
p.drawImage(0, 0, annotations);
|
||||
}
|
||||
p.end();
|
||||
}
|
||||
|
||||
if (setBackground) {
|
||||
// PaintBoard 背景图使用逻辑像素尺寸,避免高 DPI 下比例异常。
|
||||
QImage logicalBackground = screenshot;
|
||||
const QSize logicalSize(
|
||||
qMax(1, qRound(screenshot.width() / scaleX)),
|
||||
qMax(1, qRound(screenshot.height() / scaleY)));
|
||||
if (logicalBackground.size() != logicalSize) {
|
||||
logicalBackground = logicalBackground.scaled(logicalSize,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
QImage background = screenshot;
|
||||
if (board->window() && board->window()->devicePixelRatio() > 1.0) {
|
||||
const QSize logicalSize(qRound(background.width() / board->window()->devicePixelRatio()),
|
||||
qRound(background.height() / board->window()->devicePixelRatio()));
|
||||
if (logicalSize.isValid() && logicalSize != background.size()) {
|
||||
background = background.scaled(logicalSize,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
}
|
||||
board->setBackgroundImg(logicalBackground);
|
||||
board->setBackgroundImg(background);
|
||||
}
|
||||
}
|
||||
|
||||
return screenshot;
|
||||
}
|
||||
|
||||
// ── writeImageToClipboard (private) ──────────────────────
|
||||
bool ScreenshotViewModel::writeImageToClipboard(const QImage &image)
|
||||
{
|
||||
if (image.isNull()) return false;
|
||||
if (image.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QClipboard *cb = QGuiApplication::clipboard();
|
||||
if (!cb) return false;
|
||||
if (!cb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cb->setImage(image, QClipboard::Clipboard);
|
||||
#ifdef Q_OS_LINUX
|
||||
cb->setImage(image, QClipboard::Selection);
|
||||
|
||||
@@ -1,79 +1,56 @@
|
||||
#ifndef SCREENSHOT_VIEW_MODEL_H
|
||||
#define SCREENSHOT_VIEW_MODEL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#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"
|
||||
|
||||
// ScreenshotViewModel
|
||||
// -------------------
|
||||
// 编排截图完整流程:抓快照 → 裁剪 → 合并标注 → 输出(剪贴板/Base64/贴图)。
|
||||
// 暴露 Q_INVOKABLE 方法和信号供 QML 调用,自身不感知任何 QML 组件 id。
|
||||
// 依赖的 Model:DesktopSnapshot(数据)、StickyImageStore(贴图存储)。
|
||||
|
||||
class ScreenshotViewModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ScreenshotViewModel(DesktopSnapshot &snapshot,
|
||||
StickyImageStore &stickyStore,
|
||||
QObject *parent = nullptr);
|
||||
StickyImageStore &stickyStore,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
// ── 供 MagnifierProvider 只读访问快照 ────────────────
|
||||
const QImage& desktopSnapshot() const { return m_snapshot.image(); }
|
||||
|
||||
// 虚拟桌面的逻辑矩形(所有屏幕 geometry 的并集),供 QML 定位截图窗口
|
||||
const QPixmap& desktopSnapshot() const { return m_snapshot.image(); }
|
||||
Q_INVOKABLE QRect virtualGeometry() const { return m_snapshot.virtualGeometry(); }
|
||||
QPoint mapLogicalPointToPhysical(const QPoint &point) const
|
||||
{
|
||||
return m_snapshot.mapLogicalPointToPhysical(point);
|
||||
}
|
||||
Q_INVOKABLE QRect mapLogicalRectToPhysicalRect(const QRect &rect) const
|
||||
{
|
||||
return captureRectForDisplay(rect);
|
||||
}
|
||||
|
||||
// ── Q_INVOKABLE:供 QML 调用 ─────────────────────────
|
||||
|
||||
// 抓取全屏快照(在截图窗口显示之前调用)
|
||||
Q_INVOKABLE void captureDesktop();
|
||||
|
||||
// 释放快照(截图会话结束后调用)
|
||||
Q_INVOKABLE void releaseDesktopSnapshot();
|
||||
|
||||
// 取指定逻辑坐标的像素颜色(放大镜取色)
|
||||
Q_INVOKABLE QColor getPixelColor(int x, int y) const;
|
||||
|
||||
// 返回 (x,y) 下最上层窗口矩形(窗口识别)
|
||||
Q_INVOKABLE QRect windowAtPoint(int x, int y) const;
|
||||
|
||||
// 截图选区并写入剪贴板,返回是否成功
|
||||
Q_INVOKABLE bool captureRectToClipboard(QQuickItem *paintBoard, const QRect &rect);
|
||||
|
||||
// 截图选区并返回 Base64 字符串(供保存文件使用,不写剪贴板)
|
||||
Q_INVOKABLE QString captureRectToBase64(QQuickItem *paintBoard, const QRect &rect);
|
||||
|
||||
// 截图选区并存入 StickyImageStore,返回 image://sticky/<id> URL
|
||||
Q_INVOKABLE QString captureRectToStickyUrl(QQuickItem *paintBoard, const QRect &rect);
|
||||
|
||||
// 仅将选区写入 PaintBoard 背景(马赛克工具使用)
|
||||
Q_INVOKABLE void grabToPaintBoard(QQuickItem *paintBoard, const QRect &rect);
|
||||
|
||||
// 截图选区并直接返回 QImage(OCR 使用)
|
||||
Q_INVOKABLE QImage captureRectToImage(const QRect &rect);
|
||||
|
||||
// 实时抓取选区(每次都会刷新桌面快照),用于长截图连续采样。
|
||||
QImage captureRectToImageLive(const QRect &rect);
|
||||
|
||||
// 复制任意图像到剪贴板。
|
||||
bool copyImageToClipboard(const QImage &image);
|
||||
|
||||
private:
|
||||
// 内部:从快照裁剪选区并叠加 PaintBoard 标注层
|
||||
// setBackground=true 时将结果写回 PaintBoard 背景(仅马赛克路径)
|
||||
QImage captureScreen(QQuickItem *paintBoard, const QRect &rect = QRect(),
|
||||
QRect captureRectForDisplay(const QRect &rect) const;
|
||||
QImage captureScreen(QQuickItem *paintBoard,
|
||||
const QRect &rect = QRect(),
|
||||
bool setBackground = false);
|
||||
|
||||
bool writeImageToClipboard(const QImage &image);
|
||||
|
||||
DesktopSnapshot &m_snapshot;
|
||||
DesktopSnapshot &m_snapshot;
|
||||
StickyImageStore &m_stickyStore;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
#include "sticky_view_model.h"
|
||||
#include "sticky_view_model.h"
|
||||
#include <QPainter>
|
||||
#include <QTransform>
|
||||
#include <QGuiApplication>
|
||||
#include <QScreen>
|
||||
#include <QWindow>
|
||||
#include <QDebug>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
StickyViewModel::StickyViewModel(StickyImageStore &store, QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -10,8 +18,34 @@ StickyViewModel::StickyViewModel(StickyImageStore &store, QObject *parent)
|
||||
|
||||
void StickyViewModel::requestSticky(const QString &imageUrl, const QRect &imgRect)
|
||||
{
|
||||
if (imageUrl.isEmpty()) return;
|
||||
emit stickyReady(imageUrl, imgRect);
|
||||
if (imageUrl.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QRect targetRect = imgRect.normalized();
|
||||
QImage image = m_store.getImageByUrl(imageUrl);
|
||||
if (!image.isNull()) {
|
||||
QScreen *screen = QGuiApplication::screenAt(targetRect.topLeft());
|
||||
if (!screen) {
|
||||
screen = QGuiApplication::screenAt(targetRect.center());
|
||||
}
|
||||
if (!screen) {
|
||||
screen = QGuiApplication::primaryScreen();
|
||||
}
|
||||
|
||||
const qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
|
||||
if (dpr > 0.0) {
|
||||
if (!qFuzzyCompare(image.devicePixelRatio(), dpr)) {
|
||||
image.setDevicePixelRatio(dpr);
|
||||
m_store.replaceImage(imageUrl, image);
|
||||
}
|
||||
const QSize logicalSize(qMax(1, qRound(image.width() / dpr)),
|
||||
qMax(1, qRound(image.height() / dpr)));
|
||||
targetRect.setSize(logicalSize);
|
||||
}
|
||||
}
|
||||
|
||||
emit stickyReady(imageUrl, targetRect);
|
||||
}
|
||||
|
||||
void StickyViewModel::releaseImage(const QString &imageUrl)
|
||||
@@ -19,6 +53,34 @@ void StickyViewModel::releaseImage(const QString &imageUrl)
|
||||
m_store.releaseImage(imageUrl);
|
||||
}
|
||||
|
||||
void StickyViewModel::positionStickyWindow(QObject *windowObject, const QRect &imgRect) const
|
||||
{
|
||||
auto *window = qobject_cast<QWindow*>(windowObject);
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QRect targetRect = imgRect.normalized();
|
||||
const QPoint topLeft = targetRect.topLeft();
|
||||
|
||||
window->setPosition(topLeft);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (HWND hwnd = reinterpret_cast<HWND>(window->winId())) {
|
||||
RECT rect{};
|
||||
GetWindowRect(hwnd, &rect);
|
||||
qDebug() << "[StickyHWND] qt-pos:"
|
||||
<< topLeft
|
||||
<< "hwnd:"
|
||||
<< QRect(rect.left,
|
||||
rect.top,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top)
|
||||
<< "qmlPos:" << window->x() << window->y();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool StickyViewModel::saveImage(const QString &imageUrl, const QUrl &targetUrl)
|
||||
{
|
||||
return m_store.saveImage(imageUrl, targetUrl);
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
Q_INVOKABLE bool saveImage(const QString &imageUrl, const QUrl &targetUrl);
|
||||
Q_INVOKABLE bool copyImageToClipboard(const QString &imageUrl);
|
||||
Q_INVOKABLE QImage getImageByUrl(const QString &imageUrl) const;
|
||||
Q_INVOKABLE void positionStickyWindow(QObject *windowObject, const QRect &imgRect) const;
|
||||
Q_INVOKABLE bool overwriteWithAnnotations(const QString &imageUrl, const QImage &annotationLayer);
|
||||
Q_INVOKABLE bool saveMergedImage(const QString &imageUrl, const QImage &annotationLayer, const QUrl &targetUrl);
|
||||
Q_INVOKABLE bool rotateImage(const QString &imageUrl, int degreesClockwise);
|
||||
|
||||
Reference in new issue
Block a user