diff --git a/src/main_window.cpp b/src/main_window.cpp index b80a8d607..1e38dc217 100755 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -51,8 +51,11 @@ extern "C" { #include #include #include +#include #include #include +#include +#include #include #include #include @@ -112,15 +115,35 @@ const int MainWindow::CAMERA_WIDGET_MIN_HEIGHT = 45; DWIDGET_USE_NAMESPACE +// X11 的 X.h 把 CursorShape 定义成了宏(#define CursorShape 0),会破坏 Qt::CursorShape, +// 这里先取消该宏,本文件改用 Qt 的光标枚举。 +#ifdef CursorShape +#undef CursorShape +#endif + namespace { const int TOOLBAR_X_SPACING = 85; // const int TOOLBAR_Y_SPACING = 3; const int TOOLBAR_Y_SPACING = 5; const int SIDEBAR_X_SPACING = 8; const int SIDEBAR_Y_SPACING = -2; -const int CURSOR_WIDTH = 8; -const int CURSOR_HEIGHT = 18; const int INDICATOR_WIDTH = 50; +// 放大镜浮窗相对光标点的右下偏移,取自 Sketch 画板「截图录屏-光标指针」 +const int MAGNIFIER_OFFSET = 12; + +// 工程内的光标统一走 QApplication override cursor;override cursor 会压过控件级 +// setCursor,因此调整选区边框这里也必须用同一套接口。 +// 优先替换当前 override cursor,避免反复 push 造成光标栈无限增长。 +void applyOverrideCursorShape(int shape) +{ + const QCursor cursor(static_cast(shape)); + const QCursor *current = qApp->overrideCursor(); + if (current == nullptr) { + qApp->setOverrideCursor(cursor); + } else if (*current != cursor) { + qApp->changeOverrideCursor(cursor); + } +} } // namespace #ifdef KF5_WAYLAND_FLAGE_ON /** @@ -562,6 +585,12 @@ void MainWindow::initAttributes() m_sizeTips->setFullScreenRecord(m_isFullScreenRecord); m_sizeTips->hide(); + // 左下角像素级微调快捷键提示(仅 X11 普通截图模式使用) + if (m_fineAdjustTips == nullptr) { + m_fineAdjustTips = new FineAdjustTips(this); + } + updateFineAdjustTips(); + if (m_functionType == status::record) { m_zoomIndicator = new ZoomIndicator(this, true); } else { @@ -1185,8 +1214,8 @@ void MainWindow::initToolBarShortcut() QShortcut *keyBoardSC = new QShortcut(QKeySequence("K"), this); // 录屏模式(未做穿透) 摄像头 QShortcut *cameraSC = new QShortcut(QKeySequence("C"), this); - // 截图模式 AI 助手(按键 A) - QShortcut *aiAssistantSC = new QShortcut(QKeySequence("A"), this); + // 截图模式 AI 助手(按键 Alt+A) + QShortcut *aiAssistantSC = new QShortcut(QKeySequence("Alt+A"), this); // 截图模式/录屏模式(未做穿透)/滚动模式 帮助快捷面板 #if (QT_VERSION_MAJOR == 5) @@ -1204,9 +1233,9 @@ void MainWindow::initToolBarShortcut() }); // 截图模式 AI 助手应用内快捷键 connect(aiAssistantSC, &QShortcut::activated, this, [=] { - qCWarning(dsrApp) << "shortcut : aiAssistantSC (key: a)"; + qCWarning(dsrApp) << "shortcut : aiAssistantSC (key: alt+a)"; if ((status::shot == m_functionType || status::scrollshot == m_functionType)&& Utils::is3rdInterfaceStart == false) { - qCWarning(dsrApp) << "shortcut : aiAssistantSC (key: a)"; + qCWarning(dsrApp) << "shortcut : aiAssistantSC (key: alt+a)"; m_toolBar->shapeClickedFromMain("aiassistant"); } }); @@ -1569,6 +1598,9 @@ void MainWindow::initScreenShot() m_sizeTips->updateTips(QPoint(recordX, recordY), QSize(recordWidth, recordHeight)); } + // 启动截图后立即展示像素级微调快捷键提示 + updateFineAdjustTips(); + if (m_toolBar) { m_toolBar->setFocus(); } @@ -4430,6 +4462,40 @@ void MainWindow::onAiAssistantSelected(int func) }); } +void MainWindow::updateFineAdjustTips() +{ + if (m_fineAdjustTips == nullptr) { + return; + } + + // 仅在普通截图模式、尚未进入编辑态且非平板/非 Treeland 时展示 + if (status::shot != m_functionType || m_isShapesWidgetExist + || Utils::isTabletEnvironment || Utils::isTreelandMode + || !m_initScreenShot) { + m_fineAdjustTips->hide(); + return; + } + + m_fineAdjustTips->setMode(isFirstReleaseButton ? FineAdjustTips::SelectionMode + : FineAdjustTips::CursorMode); + // 固定比例激活时,扩大/缩小快捷键不可用,对应两行置灰 + m_fineAdjustTips->setResizeEnabled(!(m_roundedCornerWidget && m_roundedCornerWidget->isRatioLocked())); + m_fineAdjustTips->dockToBottomLeft(); + + // 光标(或选区)与提示面板重叠时隐藏提示面板 + const QRect tipsRect(m_fineAdjustTips->pos(), m_fineAdjustTips->size()); + const QRect blockedRect = isFirstReleaseButton + ? QRect(recordX, recordY, recordWidth, recordHeight) + : QRect(m_currentCursor, QSize(1, 1)); + if (tipsRect.intersects(blockedRect)) { + m_fineAdjustTips->hide(); + return; + } + + m_fineAdjustTips->show(); + m_fineAdjustTips->raise(); +} + // 公共函数:隐藏截图提示 void MainWindow::hideScreenshotTips() { @@ -5368,10 +5434,31 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) if (Utils::isTreelandMode) return DWidget::eventFilter(obj,event); - bool needRepaint = false; - #undef KeyPress #undef KeyRelease + // 需求:标注模式下仍可调整截图区域大小。 + // ShapesWidget 覆盖在选区之上并自行接收事件,这里先过滤它的事件: + // 命中选区边框拖拽点(鼠标)或选区尺寸快捷键(Ctrl/Ctrl+Shift+方向键)时优先 + // 按“调整截图区域”处理,其余一律交回 ShapesWidget,避免影响标注交互。 + if (obj != this && m_isShapesWidgetExist && m_shapesWidget && obj == m_shapesWidget) { + bool consumed = false; + if (event->type() == QEvent::KeyPress) { + consumed = handleEditModeSelectionResizeKey(static_cast(event)); + } else if (event->type() == QEvent::MouseButtonPress + || event->type() == QEvent::MouseMove + || event->type() == QEvent::MouseButtonRelease) { + QMouseEvent *mouseEvent = static_cast(event); + const QPoint mainPos = m_shapesWidget->mapTo(this, mouseEvent->pos()); + consumed = handleEditModeSelectionResize(mouseEvent, mainPos); + } + if (consumed) { + return true; + } + return DWidget::eventFilter(obj, event); + } + + bool needRepaint = false; + if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); // 键盘按下事件处理 @@ -5479,6 +5566,13 @@ int MainWindow::mouseDblClickEF(QMouseEvent *mouseEvent, bool &needRepaint) int MainWindow::mousePressEF(QMouseEvent *mouseEvent, bool &needRepaint) { Q_UNUSED(needRepaint); + if (m_isShapesWidgetExist) { + // 标注模式下,光标落在选区边框拖拽点上时开始调整截图区域大小 + if (handleEditModeSelectionResize(mouseEvent, mouseEvent->pos())) { + return 2; + } + return 1; + } if (!m_isShapesWidgetExist) { // 未打开截图形状编辑界面 if (mouseEvent->button() == Qt::LeftButton) { @@ -5540,6 +5634,13 @@ int MainWindow::mousePressEF(QMouseEvent *mouseEvent, bool &needRepaint) // 事件过滤器过滤的鼠标双击事件在此方法处理 int MainWindow::mouseReleaseEF(QMouseEvent *mouseEvent, bool &needRepaint) { + if (m_isShapesWidgetExist) { + // 标注模式下结束选区调整 + if (handleEditModeSelectionResize(mouseEvent, mouseEvent->pos())) { + return 2; + } + return 1; + } if (!m_isShapesWidgetExist) { // 未打开截图形状编辑界面 if (mouseEvent->button() == Qt::LeftButton) { @@ -5633,7 +5734,13 @@ int MainWindow::mouseReleaseEF(QMouseEvent *mouseEvent, bool &needRepaint) isPressMouseLeftButton = false; isReleaseMouseLeftButton = true; + // 调整/移动选区结束后工具栏重新弹出,放大镜同步隐藏(需求 2.9) + if (m_zoomIndicator) { + m_zoomIndicator->hideMagnifier(); + } + needRepaint = true; + updateFineAdjustTips(); } } return 1; @@ -5645,6 +5752,14 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint) // qCDebug(dsrApp) << "mouseEvent->pos()" << mouseEvent->pos() << " , isFirsMove: " << isFirstMove; // 当前光标的位置 m_currentCursor = mouseEvent->pos(); + updateFineAdjustTips(); + if (m_isShapesWidgetExist) { + // 标注模式下,悬停/拖拽选区边框时调整截图区域大小 + if (handleEditModeSelectionResize(mouseEvent, mouseEvent->pos())) { + return 2; + } + return 1; + } // 没打开截图的编辑模式 if (!m_isShapesWidgetExist) { if (m_toolBar && m_toolBar->isVisible()) { @@ -5658,10 +5773,14 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint) isFirstMove = true; } else { if (status::shot == m_functionType) { - if (m_toolBar && !m_toolBar->isVisible() && !isFirstReleaseButton) { + // 需求 2.9:工具栏隐藏时显示放大镜。框选完成后工具栏弹出即隐藏, + // 拖拽调整选区边界时工具栏会自动收起,放大镜随之重新显示。 + if (m_toolBar && !m_toolBar->isVisible()) { // QPoint curPos = this->cursor().pos(); 采用全局坐标,替换局部坐标 // mouseEvent->globalPos()此接口获取的光标坐标是已经缩放后的坐标,需还原 QPoint curPos = this->cursor().pos(); // 采用全局坐标,替换局部坐标 + // 逻辑坐标,放大镜浮窗上的坐标文案使用该值 + const QPoint screenCursorPos = curPos; // qCDebug(dsrApp) << "1 >>>> curPos: " << curPos << " , mouseEvent->globalPos(): "<< mouseEvent->globalPos(); // mouseEvent->globalPos()此接口获取的光标坐标是已经缩放后的坐标,需还原 // QPoint curPos = mouseEvent->globalPos(); @@ -5684,18 +5803,25 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint) QPoint tmpPos; QPoint topLeft = m_backgroundRect.topLeft() * m_pixelRatio; - // 光标x坐标+110+8 > 截图背景左上角x坐标+截图背景宽度 判断光标横向是否超出屏幕 - if (curPos.x() + INDICATOR_WIDTH + CURSOR_WIDTH > topLeft.x() + m_backgroundRect.width() * m_pixelRatio) { - tmpPos.setX(curPos.x() - INDICATOR_WIDTH); + // 放大镜浮窗尺寸与偏移(统一换算到当前缩放比下的物理像素) + const int zoomWidth = + static_cast(std::lround(ZoomIndicator::PANEL_WIDTH * m_pixelRatio)); + const int zoomHeight = + static_cast(std::lround(ZoomIndicator::PANEL_HEIGHT * m_pixelRatio)); + const int zoomOffset = + static_cast(std::lround(MAGNIFIER_OFFSET * m_pixelRatio)); + + // 默认放在光标右下方;右侧/下方放不下时翻转到另一侧,保证浮窗完整可见。 + if (curPos.x() + zoomOffset + zoomWidth > topLeft.x() + m_backgroundRect.width() * m_pixelRatio) { + tmpPos.setX(curPos.x() - zoomOffset - zoomWidth); } else { - tmpPos.setX(curPos.x() + CURSOR_WIDTH); + tmpPos.setX(curPos.x() + zoomOffset); } - // 光标y坐标+110 > 截图背景左上角y坐标+截图背景高度 判断光标纵向是否超出屏幕 - if (curPos.y() + INDICATOR_WIDTH > topLeft.y() + m_backgroundRect.height() * m_pixelRatio) { - tmpPos.setY(curPos.y() - INDICATOR_WIDTH); + if (curPos.y() + zoomOffset + zoomHeight > topLeft.y() + m_backgroundRect.height() * m_pixelRatio) { + tmpPos.setY(curPos.y() - zoomOffset - zoomHeight); } else { - tmpPos.setY(curPos.y() + CURSOR_HEIGHT); + tmpPos.setY(curPos.y() + zoomOffset); } // 判断是否进行缩放且缩放的时候屏幕数量大于1 @@ -5715,6 +5841,7 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint) QPoint tempPoint = QPoint(std::max(tmpPos.x() - topLeft.x(), 0), std::max(tmpPos.y() - topLeft.y(), 0)); // 由于move接口,移动的坐标点都是直接将未经缩放的点直接缩放后得到,即point / m_pixelRatio m_zoomIndicator->setCursorPos(curPos); + m_zoomIndicator->setScreenPos(screenCursorPos); m_zoomIndicator->showMagnifier(tempPoint / m_pixelRatio); } } @@ -5743,25 +5870,25 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint) recordY = std::max( std::min(dragRecordY + mouseEvent->y() - dragStartY, m_backgroundRect.height() - recordHeight), 0); } else if (dragAction == ACTION_RESIZE_TOP_LEFT) { - resizeTop(mouseEvent); - resizeLeft(mouseEvent); + resizeTop(mouseEvent->pos()); + resizeLeft(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_TOP_RIGHT) { - resizeTop(mouseEvent); - resizeRight(mouseEvent); + resizeTop(mouseEvent->pos()); + resizeRight(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_BOTTOM_LEFT) { - resizeBottom(mouseEvent); - resizeLeft(mouseEvent); + resizeBottom(mouseEvent->pos()); + resizeLeft(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_BOTTOM_RIGHT) { - resizeBottom(mouseEvent); - resizeRight(mouseEvent); + resizeBottom(mouseEvent->pos()); + resizeRight(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_TOP) { - resizeTop(mouseEvent); + resizeTop(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_BOTTOM) { - resizeBottom(mouseEvent); + resizeBottom(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_LEFT) { - resizeLeft(mouseEvent); + resizeLeft(mouseEvent->pos()); } else if (dragAction == ACTION_RESIZE_RIGHT) { - resizeRight(mouseEvent); + resizeRight(mouseEvent->pos()); } // 锁定比例时,拖拽后按固定宽高比等比修正选区 @@ -5960,6 +6087,11 @@ int MainWindow::keyPressEF(QKeyEvent *keyEvent, bool &needRepaint) m_shapesWidget->setShiftKeyPressed(m_isShiftPressed); } + // 未选中标注时,Ctrl / Ctrl+Shift + 方向键调整截图区域大小(需求:标注模式仍可调整选区) + if (handleEditModeSelectionResizeKey(keyEvent)) { + return 0; + } + if (keyEvent->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) { if (keyEvent->key() == Qt::Key_Left) { m_shapesWidget->microAdjust("Ctrl+Shift+Left"); @@ -6059,11 +6191,16 @@ int MainWindow::keyPressEF(QKeyEvent *keyEvent, bool &needRepaint) } if (keyEvent->key() == Qt::Key_C) { - // ConfigSettings::instance()->setValue("save", "save_op", - // SaveAction::SaveToClipboard); - // m_copyToClipboard = true; - // saveScreenshot(); - // saveScreenShot(); + // 需求 2.9:放大镜显示状态下按 Ctrl+C,把当前光标像素的 + // HEX 色值(小写带 #)复制到系统剪贴板,并退出截图。 + if (m_zoomIndicator && m_zoomIndicator->isVisible()) { + const QString colorHex = m_zoomIndicator->currentColorHex(); + if (!colorHex.isEmpty()) { + qCInfo(dsrApp) << "copy color to clipboard:" << colorHex; + qApp->clipboard()->setText(colorHex, QClipboard::Clipboard); + } + exitApp(); + } } if (keyEvent->key() == Qt::Key_Left) { recordX = std::max(0, recordX - 1); @@ -6350,6 +6487,7 @@ void MainWindow::keyPressEvent(QKeyEvent *event) // cursorMove(event); Utils::cursorMove(m_currentCursor, event); } + updateFineAdjustTips(); if (Utils::isWaylandMode) { onKeyboardPressWayland(static_cast(event->nativeScanCode())); @@ -7608,52 +7746,52 @@ void MainWindow::shotFullScreen(bool isFull) // } //} -void MainWindow::resizeTop(QMouseEvent *mouseEvent) +void MainWindow::resizeTop(const QPoint &pos) { if (status::record == m_functionType) { - int offsetY = mouseEvent->y() - dragStartY; + int offsetY = pos.y() - dragStartY; recordY = std::max(std::min(dragRecordY + offsetY, dragRecordY + dragRecordHeight - RECORD_MIN_HEIGHT), 1); recordHeight = std::max(std::min(dragRecordHeight - offsetY, m_backgroundRect.height()), RECORD_MIN_HEIGHT); } else if (status::shot == m_functionType) { - int offsetY = mouseEvent->y() - dragStartY; + int offsetY = pos.y() - dragStartY; recordY = std::max(std::min(dragRecordY + offsetY, dragRecordY + dragRecordHeight - RECORD_MIN_SHOT_SIZE), 1); recordHeight = std::max(std::min(dragRecordHeight - offsetY, m_backgroundRect.height()), RECORD_MIN_SHOT_SIZE); } } -void MainWindow::resizeBottom(QMouseEvent *mouseEvent) +void MainWindow::resizeBottom(const QPoint &pos) { if (status::record == m_functionType) { - int offsetY = mouseEvent->y() - dragStartY; + int offsetY = pos.y() - dragStartY; recordHeight = std::max(std::min(dragRecordHeight + offsetY, m_backgroundRect.height()), RECORD_MIN_HEIGHT); } else if (status::shot == m_functionType) { - int offsetY = mouseEvent->y() - dragStartY; + int offsetY = pos.y() - dragStartY; recordHeight = std::max(std::min(dragRecordHeight + offsetY, m_backgroundRect.height()), RECORD_MIN_SHOT_SIZE); } } -void MainWindow::resizeLeft(QMouseEvent *mouseEvent) +void MainWindow::resizeLeft(const QPoint &pos) { if (status::record == m_functionType) { - int offsetX = mouseEvent->x() - dragStartX; + int offsetX = pos.x() - dragStartX; recordX = std::max(std::min(dragRecordX + offsetX, dragRecordX + dragRecordWidth - RECORD_MIN_SIZE), 1); recordWidth = std::max(std::min(dragRecordWidth - offsetX, m_backgroundRect.width()), RECORD_MIN_SIZE); } else if (status::shot == m_functionType) { - int offsetX = mouseEvent->x() - dragStartX; + int offsetX = pos.x() - dragStartX; recordX = std::max(std::min(dragRecordX + offsetX, dragRecordX + dragRecordWidth - RECORD_MIN_SHOT_SIZE), 1); recordWidth = std::max(std::min(dragRecordWidth - offsetX, m_backgroundRect.width()), RECORD_MIN_SHOT_SIZE); } } -void MainWindow::resizeRight(QMouseEvent *mouseEvent) +void MainWindow::resizeRight(const QPoint &pos) { if (status::record == m_functionType) { - int offsetX = mouseEvent->x() - dragStartX; + int offsetX = pos.x() - dragStartX; recordWidth = std::max(std::min(dragRecordWidth + offsetX, m_backgroundRect.width()), RECORD_MIN_SIZE); } else if (status::shot == m_functionType) { - int offsetX = mouseEvent->x() - dragStartX; + int offsetX = pos.x() - dragStartX; recordWidth = std::max(std::min(dragRecordWidth + offsetX, m_backgroundRect.width()), RECORD_MIN_SHOT_SIZE); } } @@ -7661,8 +7799,13 @@ void MainWindow::resizeRight(QMouseEvent *mouseEvent) int MainWindow::getAction(QEvent *event) { QMouseEvent *mouseEvent = static_cast(event); - int cursorX = mouseEvent->x(); - int cursorY = mouseEvent->y(); + return getAction(mouseEvent->pos()); +} + +int MainWindow::getAction(const QPoint &pos) +{ + int cursorX = pos.x(); + int cursorY = pos.y(); if (cursorX > recordX - m_cursorBound && cursorX < recordX + m_cursorBound && cursorY > recordY - m_cursorBound && cursorY < recordY + m_cursorBound) { @@ -8035,6 +8178,19 @@ void MainWindow::startCountdown() repaint(); } +void MainWindow::showEvent(QShowEvent *event) +{ + QMainWindow::showEvent(event); + // 窗口真正显示(如全屏)后再停靠提示面板,避免拿到 0x0 的窗口尺寸 + updateFineAdjustTips(); +} + +void MainWindow::resizeEvent(QResizeEvent *event) +{ + QMainWindow::resizeEvent(event); + updateFineAdjustTips(); +} + void MainWindow::hideAllWidget() { @@ -8086,7 +8242,18 @@ void MainWindow::adjustLayout(QVBoxLayout *layout, int layoutWidth, int layoutHe void MainWindow::initShapeWidget(QString type) { qCDebug(dsrApp) << "正在初始化截图编辑界面..."; + // 进入编辑态时重置选区调整状态,避免上次拖拽残留 + m_editModeResizing = false; + m_editModeResizeCursorActive = false; + if (m_fineAdjustTips) { + // 进入截图编辑态后不再提示微调快捷键 + m_fineAdjustTips->hide(); + } m_shapesWidget = new ShapesWidget(this); + // 需求:标注模式下仍可调整截图区域大小。 + // ShapesWidget 覆盖在选区之上并自行处理鼠标/键盘事件,这里装上过滤器, + // 在光标命中选区边框拖拽点时优先按“调整选区”处理,其余事件照常交给标注。 + m_shapesWidget->installEventFilter(this); m_shapesWidget->setShiftKeyPressed(m_isShiftPressed); if (type != "color") @@ -8278,6 +8445,264 @@ void MainWindow::applyRememberedSelectionSetting() << "selection:" << QRect(recordX, recordY, recordWidth, recordHeight); } +// ===== 需求:标注模式(编辑态)下仍可调整截图区域大小 ===== + +bool MainWindow::handleEditModeSelectionResize(QMouseEvent *event, const QPoint &mainPos) +{ + if (status::shot != m_functionType || !m_isShapesWidgetExist || !m_shapesWidget) { + return false; + } + + // 标注正在绘制/拖动时,选区边框不能让鼠标事件改道,否则标注移动会 + // 在中途被选区调整截获,表现为只显示起点和终点、位置跳变。 + if (!m_editModeResizing && m_shapesWidget->isPressed()) { + return false; + } + + if (m_editModeResizing) { + if (event->type() == QEvent::MouseMove) { + applyEditModeSelectionResize(mainPos); + // 拖拽过程中保持边框缩放光标 + applyOverrideCursorShape(resizeCursorShape(dragAction)); + m_editModeResizeCursorActive = true; + return true; + } + if (event->type() == QEvent::MouseButtonRelease && event->button() == Qt::LeftButton) { + m_editModeResizing = false; + isPressMouseLeftButton = false; + isReleaseMouseLeftButton = true; + // 恢复标注工具的默认光标 + if (m_editModeResizeCursorActive) { + m_shapesWidget->updateCursorShape(); + m_editModeResizeCursorActive = false; + } + updateFineAdjustTips(); + updateToolBarPos(); + if (m_sideBar && m_sideBar->isVisible()) { + updateSideBarPos(); + } + update(); + return true; + } + return false; + } + + if (event->type() == QEvent::MouseButtonPress && event->button() == Qt::LeftButton) { + const int action = getAction(mainPos); + if (action == ACTION_MOVE) { + // 不在边框拖拽点上:交给 ShapesWidget 正常绘制/编辑标注 + return false; + } + // 开始调整截图区域大小 + dragStartX = mainPos.x(); + dragStartY = mainPos.y(); + dragRecordX = recordX; + dragRecordY = recordY; + dragRecordWidth = recordWidth; + dragRecordHeight = recordHeight; + dragAction = action; + m_lockedRatio = (recordWidth > 0 && recordHeight > 0) + ? static_cast(recordWidth) / static_cast(recordHeight) : 0.0; + isPressMouseLeftButton = true; + isReleaseMouseLeftButton = false; + m_editModeResizing = true; + // 立即切换为缩放光标,避免按下瞬间仍是标注工具光标 + applyOverrideCursorShape(resizeCursorShape(action)); + m_editModeResizeCursorActive = true; + return true; + } + + if (event->type() == QEvent::MouseMove) { + // 悬停在边框拖拽点上时显示骨架点与缩放光标 + const int action = getAction(mainPos); + if (action != ACTION_MOVE) { + drawDragPoint = true; + applyOverrideCursorShape(resizeCursorShape(action)); + m_editModeResizeCursorActive = true; + update(); + return true; + } + if (drawDragPoint || m_editModeResizeCursorActive) { + drawDragPoint = false; + // 离开边框后恢复标注工具的默认光标 + m_shapesWidget->updateCursorShape(); + m_editModeResizeCursorActive = false; + update(); + } + return false; + } + return false; +} + +int MainWindow::resizeCursorShape(int action) const +{ + switch (action) { + case ACTION_RESIZE_TOP_LEFT: + case ACTION_RESIZE_BOTTOM_RIGHT: + return Qt::SizeFDiagCursor; + case ACTION_RESIZE_TOP_RIGHT: + case ACTION_RESIZE_BOTTOM_LEFT: + return Qt::SizeBDiagCursor; + case ACTION_RESIZE_LEFT: + case ACTION_RESIZE_RIGHT: + return Qt::SizeHorCursor; + case ACTION_RESIZE_TOP: + case ACTION_RESIZE_BOTTOM: + return Qt::SizeVerCursor; + default: + return Qt::ArrowCursor; + } +} + +void MainWindow::applyEditModeSelectionResize(const QPoint &mainPos) +{ + switch (dragAction) { + case ACTION_RESIZE_TOP_LEFT: + resizeTop(mainPos); + resizeLeft(mainPos); + break; + case ACTION_RESIZE_TOP_RIGHT: + resizeTop(mainPos); + resizeRight(mainPos); + break; + case ACTION_RESIZE_BOTTOM_LEFT: + resizeBottom(mainPos); + resizeLeft(mainPos); + break; + case ACTION_RESIZE_BOTTOM_RIGHT: + resizeBottom(mainPos); + resizeRight(mainPos); + break; + case ACTION_RESIZE_TOP: + resizeTop(mainPos); + break; + case ACTION_RESIZE_BOTTOM: + resizeBottom(mainPos); + break; + case ACTION_RESIZE_LEFT: + resizeLeft(mainPos); + break; + case ACTION_RESIZE_RIGHT: + resizeRight(mainPos); + break; + default: + return; + } + + applyLockedRatioToSelection(); + // 已画标注保持屏幕绝对位置不动 + syncShapesWidgetGeometry(true); + if (m_sizeTips && status::scrollshot != m_functionType) { + m_sizeTips->updateTips(QPoint(recordX, recordY), QSize(recordWidth, recordHeight)); + } + updateToolBarPos(); + if (m_sideBar && m_sideBar->isVisible()) { + updateSideBarPos(); + } + update(); +} + +bool MainWindow::handleEditModeSelectionResizeKey(QKeyEvent *keyEvent) +{ + if (status::shot != m_functionType || !m_isShapesWidgetExist || !m_shapesWidget) { + return false; + } + // 有选中标注时,Ctrl+方向键仍用于微调标注,不改变截图区域 + if (m_shapesWidget->hasSelectedShape()) { + return false; + } + // 固定比例时禁用单边扩大/缩小,与框选阶段保持一致 + if (m_roundedCornerWidget && m_roundedCornerWidget->isRatioLocked()) { + return false; + } + + const int key = keyEvent->key(); + if (key != Qt::Key_Left && key != Qt::Key_Right && key != Qt::Key_Up && key != Qt::Key_Down) { + return false; + } + const Qt::KeyboardModifiers modifiers = keyEvent->modifiers(); + const bool enlarge = (modifiers == Qt::ControlModifier); + const bool shrink = (modifiers == (Qt::ControlModifier | Qt::ShiftModifier)); + if (!enlarge && !shrink) { + return false; + } + + switch (key) { + case Qt::Key_Left: + if (enlarge) { + if (recordX > 0) { + recordX -= 1; + recordWidth += 1; + } + } else if (recordWidth > RECORD_MIN_SHOT_SIZE) { + recordX += 1; + recordWidth -= 1; + } + break; + case Qt::Key_Right: + if (enlarge) { + if (recordX + recordWidth < m_backgroundRect.width()) { + recordWidth += 1; + } + } else if (recordWidth > RECORD_MIN_SHOT_SIZE) { + recordWidth -= 1; + } + break; + case Qt::Key_Up: + if (enlarge) { + if (recordY > 0) { + recordY -= 1; + recordHeight += 1; + } + } else if (recordHeight > RECORD_MIN_SHOT_SIZE) { + recordY += 1; + recordHeight -= 1; + } + break; + case Qt::Key_Down: + if (enlarge) { + if (recordY + recordHeight < m_backgroundRect.height()) { + recordHeight += 1; + } + } else if (recordHeight > RECORD_MIN_SHOT_SIZE) { + recordHeight -= 1; + } + break; + default: + return false; + } + + syncShapesWidgetGeometry(true); + if (m_sizeTips && status::scrollshot != m_functionType) { + m_sizeTips->updateTips(QPoint(recordX, recordY), QSize(recordWidth, recordHeight)); + } + updateToolBarPos(); + if (m_sideBar && m_sideBar->isVisible()) { + updateSideBarPos(); + } + updateFineAdjustTips(); + update(); + return true; +} + +void MainWindow::syncShapesWidgetGeometry(bool keepAnnotationsInPlace) +{ + if (!m_shapesWidget || Utils::isTreelandMode) { + return; + } + const QPoint newTopLeft(recordX + 2, recordY + 2); + if (keepAnnotationsInPlace) { + const QPoint delta = newTopLeft - m_shapesWidget->pos(); + if (!delta.isNull()) { + // 控件左上角移动多少,标注局部坐标就反向补偿多少,保证标注屏幕上位置不变 + m_shapesWidget->translateAllShapes(QPointF(-delta.x(), -delta.y())); + } + } + m_shapesWidget->setFixedSize(qMax(1, recordWidth - 4), qMax(1, recordHeight - 4)); + m_shapesWidget->move(newTopLeft); + m_shapesWidget->setGlobalRect(QRect(recordX, recordY, recordWidth, recordHeight)); +} + void MainWindow::onRoundedSelectionSizeChanged(int width, int height) { if (status::shot != m_functionType) { @@ -8295,6 +8720,8 @@ void MainWindow::onRoundedSelectionSizeChanged(int width, int height) } recordWidth = newWidth; recordHeight = newHeight; + // 尺寸面板只改变宽高、不动左上角,标注保持屏幕绝对位置即可 + syncShapesWidgetGeometry(false); if (m_sizeTips) { m_sizeTips->updateTips(QPoint(recordX, recordY), QSize(recordWidth, recordHeight)); } diff --git a/src/main_window.h b/src/main_window.h index fa0ebf088..f4b7d8438 100755 --- a/src/main_window.h +++ b/src/main_window.h @@ -18,6 +18,7 @@ #include "show_buttons.h" #include "widgets/shapeswidget.h" #include "widgets/toptips.h" +#include "widgets/fineadjusttips.h" #include QT_BEGIN_NAMESPACE @@ -535,6 +536,11 @@ public slots: void onAiAssistantSelected(int func); void prepareScreenshot(); void hideScreenshotTips(); + + /** + * @brief 刷新像素级微调快捷键提示面板(模式、位置、遮挡与置灰状态) + */ + void updateFineAdjustTips(); void captureScreenshotImage(); void finishScreenshot(); bool saveAction(const QPixmap &pix); @@ -819,6 +825,7 @@ public slots: void keyReleaseEvent(QKeyEvent *event) override; int getAction(QEvent *event); + int getAction(const QPoint &pos); /** @@ -828,10 +835,12 @@ public slots: */ QPixmap paintImage(); void paintEvent(QPaintEvent *event) override; - void resizeBottom(QMouseEvent *event); - void resizeLeft(QMouseEvent *event); - void resizeRight(QMouseEvent *event); - void resizeTop(QMouseEvent *event); + void showEvent(QShowEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void resizeBottom(const QPoint &pos); + void resizeLeft(const QPoint &pos); + void resizeRight(const QPoint &pos); + void resizeTop(const QPoint &pos); /** * @brief 更新光标样式 * @param event @@ -849,6 +858,30 @@ public slots: void hideAllWidget(); void adjustLayout(QVBoxLayout *layout, int layoutWidth, int layoutHeight); void initShapeWidget(QString type); + /** + * @brief 标注模式下是否命中选区边框拖拽点,命中则开始/继续调整截图区域大小 + * @param event 原始鼠标事件 + * @param mainPos 主窗口坐标系下的光标位置 + * @return 是否已消费该事件 + */ + bool handleEditModeSelectionResize(QMouseEvent *event, const QPoint &mainPos); + /** + * @brief 应用标注模式下的选区拖拽调整(复用框选阶段的 resize 逻辑) + */ + void applyEditModeSelectionResize(const QPoint &mainPos); + /** + * @brief 标注模式下用 Ctrl/Ctrl+Shift + 方向键调整截图区域大小 + * @return 是否已处理该按键 + */ + bool handleEditModeSelectionResizeKey(QKeyEvent *keyEvent); + /** + * @brief 同步 ShapesWidget 到当前选区,并可保持已画标注的屏幕绝对位置 + */ + void syncShapesWidgetGeometry(bool keepAnnotationsInPlace); + /** + * @brief 选区边框拖拽动作对应的鼠标样式 + */ + int resizeCursorShape(int action) const; int getRecordInputType(bool selectedMic, bool selectedSystemAudio); /** * @brief initBackground 初始化截图背景,启动截图时调用 @@ -1037,6 +1070,14 @@ public slots: * @brief 是否画骨架点 */ bool drawDragPoint = false; + /** + * @brief 标注模式下是否正在拖拽选区边框调整截图区域大小 + */ + bool m_editModeResizing = false; + /** + * @brief 标注模式下是否已把光标切换为边框缩放样式 + */ + bool m_editModeResizeCursorActive = false; /** * @brief 是否第一次拖动鼠标 */ @@ -1205,6 +1246,10 @@ public slots: * @brief 左上角图片大小提示 */ TopTips *m_sizeTips = nullptr; + /** + * @brief 左下角像素级微调快捷键提示 + */ + FineAdjustTips *m_fineAdjustTips = nullptr; /** * @brief 滚动截图时图片的大小 */ diff --git a/src/src.pro b/src/src.pro index 489247d02..8a5ca01af 100755 --- a/src/src.pro +++ b/src/src.pro @@ -195,6 +195,7 @@ HEADERS += main_window.h \ widgets/zoomIndicator.h \ widgets/textedit.h \ widgets/toptips.h \ + widgets/fineadjusttips.h \ widgets/toolbar.h \ widgets/shapeswidget.h \ widgets/toolbutton.h \ @@ -290,6 +291,7 @@ SOURCES += main.cpp \ utils_interface.cpp \ wayland-treeland-capture-unstable-v1-protocol.c \ widgets/toptips.cpp \ + widgets/fineadjusttips.cpp \ widgets/shapeswidget.cpp \ widgets/textedit.cpp \ widgets/zoomIndicator.cpp \ diff --git a/src/utils/borderprocessinterface.cpp b/src/utils/borderprocessinterface.cpp index 0ce09cdf1..42f0fef6f 100644 --- a/src/utils/borderprocessinterface.cpp +++ b/src/utils/borderprocessinterface.cpp @@ -11,7 +11,10 @@ #include #include +#include #include +#include +#include QT_BEGIN_NAMESPACE // 参考DTK blurEffect 的实现 @@ -570,6 +573,25 @@ QPixmap ShadowBorderProcess::getPixmapAddBorder(const QPixmap &pix) const QColor borderColor(0, 0, 0, 50); const QColor shadowColor(0, 0, 0, 70); + // 判断截图是否包含透明区域(圆角截图会切掉四个圆角), + // 有透明区域时投影与描边都要跟随 alpha 轮廓,否则会透出直角阴影/边框。 + QImage shapeImage; + bool shaped = false; + if (pix.hasAlphaChannel()) { + shapeImage = pix.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + const int w = shapeImage.width(); + const int h = shapeImage.height(); + for (int y = 0; y < h && !shaped; ++y) { + const QRgb *line = reinterpret_cast(shapeImage.constScanLine(y)); + for (int x = 0; x < w; ++x) { + if (qAlpha(line[x]) != 255) { + shaped = true; + break; + } + } + } + } + // 获取当前缩放比例 qreal radius = shadowRadius * pix.devicePixelRatioF(); QRect generateRect = pix.rect().adjusted(0, 0, radius * 2, radius * 2); @@ -580,7 +602,7 @@ QPixmap ShadowBorderProcess::getPixmapAddBorder(const QPixmap &pix) qCDebug(dsrApp) << "Adjusting draw area: xOffset and yOffset are less than radius."; imageRect.moveTo(radius - xOffset, radius - yOffset); } else { - qCDebug(dsrApp) << "Adjusting draw area: xOffset or yOffset is greater than or equal to radius."; + qCDebug(dsrApp) << "Adjusting draw area: xOffset or yOffset are greater than or equal to radius."; if (xOffset > radius) { qCDebug(dsrApp) << "Adjusting draw area: xOffset > radius."; imageRect.setX(0); @@ -604,7 +626,7 @@ QPixmap ShadowBorderProcess::getPixmapAddBorder(const QPixmap &pix) painter.save(); qCDebug(dsrApp) << "Drawing shadow."; // 绘制投影 DTK提供的接口调整了绘制的透明度,不便直接使用,参考 QPixmapDropShadowFilter 实现 - drawShadow(&painter, generateRect, shadowColor, radius); + drawShadow(&painter, generateRect, shadowColor, radius, shapeImage, shaped); painter.restore(); qCDebug(dsrApp) << "Drawing image."; @@ -613,8 +635,13 @@ QPixmap ShadowBorderProcess::getPixmapAddBorder(const QPixmap &pix) qCDebug(dsrApp) << "Drawing border."; // 绘制边框 设置 0, 不同缩放率下均绘制1px - painter.setPen(QPen(borderColor)); - painter.drawRect(imageRect.adjusted(0, 0, -1, -1)); + if (shaped) { + // 圆角截图:描边跟随 alpha 轮廓,避免直角边框压在圆角上 + drawShapeBorder(&painter, imageRect, borderColor, shapeImage); + } else { + painter.setPen(QPen(borderColor)); + painter.drawRect(imageRect.adjusted(0, 0, -1, -1)); + } painter.end(); return newPixmap; @@ -623,7 +650,8 @@ QPixmap ShadowBorderProcess::getPixmapAddBorder(const QPixmap &pix) /** @brief 根据传入的矩形 \a rect 绘制居中阴影,以模糊强度 \a radius 为边距(像素计算范围)居中,阴影基础颜色以 \a color 为准 */ -void ShadowBorderProcess::drawShadow(QPainter *p, const QRect &rect, const QColor &color, int radius) +void ShadowBorderProcess::drawShadow(QPainter *p, const QRect &rect, const QColor &color, int radius, + const QImage &source, bool shaped) { qCDebug(dsrApp) << "Drawing shadow with rect:" << rect << ", color:" << color << ", radius:" << radius; // 绘制区域必须大于像素模糊半径 @@ -637,10 +665,23 @@ void ShadowBorderProcess::drawShadow(QPainter *p, const QRect &rect, const QColo tmp.fill(0); QPainter tmpPainter(&tmp); tmpPainter.setCompositionMode(QPainter::CompositionMode_Source); - tmpPainter.setBrush(color); // 调整蒙版图片,居中 QRect tmpRect = rect.adjusted(radius, radius, -radius, -radius); - tmpPainter.drawRect(tmpRect); + if (shaped && !source.isNull()) { + // 用截图自身的 alpha 轮廓作为阴影蒙版:圆角截图的投影同样保持圆角。 + QImage silhouette(source.size(), QImage::Format_ARGB32_Premultiplied); + silhouette.fill(Qt::transparent); + { + QPainter silhouettePainter(&silhouette); + silhouettePainter.drawImage(0, 0, source); + silhouettePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + silhouettePainter.fillRect(silhouette.rect(), color); + } + tmpPainter.drawImage(tmpRect.topLeft(), silhouette); + } else { + tmpPainter.setBrush(color); + tmpPainter.drawRect(tmpRect); + } tmpPainter.end(); // 在区域中绘制模糊背景 @@ -649,3 +690,30 @@ void ShadowBorderProcess::drawShadow(QPainter *p, const QRect &rect, const QColo qt_blurImage(p, tmp, radius, false, true); p->restore(); } + +void ShadowBorderProcess::drawShapeBorder(QPainter *p, const QRect &imageRect, const QColor &color, + const QImage &source) +{ + if (source.isNull()) { + return; + } + qCDebug(dsrApp) << "Drawing shape border for rounded pixmap."; + // 1px 圆角描边:对 alpha 轮廓做 1px 膨胀后减去原始轮廓, + // 得到跟随圆角的细边,避免直角边框压在圆角截图上。 + const QRegion base(QBitmap::fromImage(source.createAlphaMask(Qt::ThresholdDither))); + QRegion dilated; + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + dilated = dilated.united(base.translated(dx, dy)); + } + } + const QRegion ring = dilated.subtracted(base).translated(imageRect.topLeft()); + if (ring.isEmpty()) { + return; + } + + p->save(); + p->setClipRegion(ring); + p->fillRect(imageRect.adjusted(-1, -1, 1, 1), color); + p->restore(); +} diff --git a/src/utils/borderprocessinterface.h b/src/utils/borderprocessinterface.h index 035074cc1..e50734444 100644 --- a/src/utils/borderprocessinterface.h +++ b/src/utils/borderprocessinterface.h @@ -7,6 +7,7 @@ #define BORDERPROCESSINTERFACE_H #include +#include #include #include class BorderProcessInterface : public QObject @@ -117,7 +118,11 @@ class ShadowBorderProcess : public BorderProcessInterface virtual QPixmap getPixmapAddBorder(const QPixmap &pix) override; private: - void drawShadow(QPainter *p, const QRect &rect, const QColor &color, int radius); + // 依据截图 alpha 轮廓绘制投影/边框,使圆角截图的投影和描边同样保持圆角 + void drawShadow(QPainter *p, const QRect &rect, const QColor &color, int radius, + const QImage &source, bool shaped); + void drawShapeBorder(QPainter *p, const QRect &imageRect, const QColor &color, + const QImage &source); }; #endif // BORDERPROCESSINTERFACE_H diff --git a/src/widgets/fineadjusttips.cpp b/src/widgets/fineadjusttips.cpp new file mode 100644 index 000000000..5594684f0 --- /dev/null +++ b/src/widgets/fineadjusttips.cpp @@ -0,0 +1,247 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "fineadjusttips.h" +#include "utils/log.h" + +#include +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +namespace { + +// 以下尺寸与 Sketch 控件 “提示语/截图时”“提示语/选区完成时” 保持一致 +const int PANEL_WIDTH = 395; +const int CURSOR_PANEL_HEIGHT = 44; +const int SELECTION_PANEL_HEIGHT = 112; +const int PANEL_PADDING = 10; +const int KEY_CAP_SIZE = 24; +const int KEY_CAP_RADIUS = 4; +const int KEY_CAP_STEP = 28; // 单个字符按键 24 + 4 间距 +const int ROW_STEP = 34; // 行高 24 + 10 行距 +const int PANEL_RADIUS = 12; + +// 黑 40% 底板,白 10% 描边 / 按键底色,白 70% 文字 +const QColor PANEL_BACKGROUND(0, 0, 0, 102); +const QColor PANEL_BORDER(255, 255, 255, 26); +const QColor KEY_CAP_BACKGROUND(255, 255, 255, 26); +const QColor TEXT_ENABLED(255, 255, 255, 178); +const QColor TEXT_DISABLED(255, 255, 255, 102); + +QFont keyCapFont() +{ + QFont font; + font.setPixelSize(14); + font.setWeight(QFont::Medium); + return font; +} + +QFont descriptionFont() +{ + QFont font; + font.setPixelSize(14); + font.setWeight(QFont::Normal); + return font; +} + +QColor capBackground(bool enabled) +{ + QColor color = KEY_CAP_BACKGROUND; + if (!enabled) { + color.setAlpha(16); + } + return color; +} + +QColor textColor(bool enabled) +{ + return enabled ? TEXT_ENABLED : TEXT_DISABLED; +} + +} // namespace + +FineAdjustTips::FineAdjustTips(DWidget *parent) + : DWidget(parent) +{ + qCDebug(dsrApp) << "FineAdjustTips constructor called."; + setObjectName("FineAdjustTips"); + setAccessibleName("FineAdjustTips"); + setAttribute(Qt::WA_TransparentForMouseEvents, true); + setAttribute(Qt::WA_TranslucentBackground, true); + setFocusPolicy(Qt::NoFocus); + dockToBottomLeft(); +} + +FineAdjustTips::~FineAdjustTips() +{ + qCDebug(dsrApp) << "FineAdjustTips destructor called."; +} + +void FineAdjustTips::setMode(Mode mode) +{ + if (m_mode == mode) { + return; + } + m_mode = mode; + dockToBottomLeft(); + update(); +} + +FineAdjustTips::Mode FineAdjustTips::mode() const +{ + return m_mode; +} + +void FineAdjustTips::setResizeEnabled(bool enabled) +{ + if (m_resizeEnabled == enabled) { + return; + } + m_resizeEnabled = enabled; + update(); +} + +bool FineAdjustTips::resizeEnabled() const +{ + return m_resizeEnabled; +} + +int FineAdjustTips::contentWidth() const +{ + // 设计稿按中文文案定宽 395;当文案(如未翻译的英文)更长时自动加宽,避免被裁剪 + QFontMetrics metrics(descriptionFont()); + int rightEdge = 241 + metrics.horizontalAdvance( + m_mode == SelectionMode ? tr("Move the capture area by 1 pixel") + : tr("Move the mouse pointer by 1 pixel")); + if (m_mode == SelectionMode) { + rightEdge = qMax(rightEdge, 169 + metrics.horizontalAdvance(tr("Enlarge the capture area by 1 pixel"))); + rightEdge = qMax(rightEdge, 226 + metrics.horizontalAdvance(tr("Shrink the capture area by 1 pixel"))); + } + return qMax(PANEL_WIDTH, rightEdge + PANEL_PADDING); +} + +QSize FineAdjustTips::panelSize() const +{ + const int height = (m_mode == SelectionMode) ? SELECTION_PANEL_HEIGHT : CURSOR_PANEL_HEIGHT; + return QSize(contentWidth(), height); +} + +void FineAdjustTips::dockToBottomLeft() +{ + QWidget *host = parentWidget(); + // 窗口尚未完成布局(宽高为 0)时先不移动,避免停靠到左上角 + if (host == nullptr || host->width() <= 0 || host->height() <= 0) { + return; + } + setFixedSize(panelSize()); + const int x = PANEL_PADDING; + const int y = qMax(0, host->height() - PANEL_PADDING - height()); + move(x, y); +} + +void FineAdjustTips::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::TextAntialiasing, true); + + const QRectF panelRect = QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5); + QPainterPath panelPath; + panelPath.addRoundedRect(panelRect, PANEL_RADIUS, PANEL_RADIUS); + painter.fillPath(panelPath, PANEL_BACKGROUND); + painter.setPen(QPen(PANEL_BORDER, 1)); + painter.drawPath(panelPath); + + // 第一行:W A S D / ↑ ↓ ← → 描述 + const int row1Y = PANEL_PADDING; + static const char *const letterKeys[] = {"W", "A", "S", "D"}; + int keyX = 0; + for (const char *key : letterKeys) { + drawKeyCap(painter, QRect(keyX, row1Y, KEY_CAP_SIZE, KEY_CAP_SIZE), + QString::fromLatin1(key), true); + keyX += KEY_CAP_STEP; + } + drawSeparator(painter, QRect(111, row1Y + 3, 9, 18), true); + drawArrowKeys(painter, 123, row1Y, true); + const QString firstDescription = (m_mode == SelectionMode) + ? tr("Move the capture area by 1 pixel") + : tr("Move the mouse pointer by 1 pixel"); + drawDescription(painter, QPoint(241, row1Y), firstDescription, true); + + if (m_mode != SelectionMode) { + return; + } + + // 第二行:Ctrl + ↑ ↓ ← → 扩大选区 + const int row2Y = PANEL_PADDING + ROW_STEP; + drawKeyCap(painter, QRect(0, row2Y, 36, KEY_CAP_SIZE), QStringLiteral("Ctrl"), m_resizeEnabled); + drawPlus(painter, QRect(39, row2Y + 8, 9, 9), m_resizeEnabled); + drawArrowKeys(painter, 51, row2Y, m_resizeEnabled); + drawDescription(painter, QPoint(169, row2Y), tr("Enlarge the capture area by 1 pixel"), m_resizeEnabled); + + // 第三行:Ctrl + Shift + ↑ ↓ ← → 缩小选区 + const int row3Y = PANEL_PADDING + ROW_STEP * 2; + drawKeyCap(painter, QRect(0, row3Y, 36, KEY_CAP_SIZE), QStringLiteral("Ctrl"), m_resizeEnabled); + drawPlus(painter, QRect(39, row3Y + 8, 9, 9), m_resizeEnabled); + drawKeyCap(painter, QRect(51, row3Y, 42, KEY_CAP_SIZE), QStringLiteral("Shift"), m_resizeEnabled); + drawPlus(painter, QRect(96, row3Y + 8, 9, 9), m_resizeEnabled); + drawArrowKeys(painter, 108, row3Y, m_resizeEnabled); + drawDescription(painter, QPoint(226, row3Y), tr("Shrink the capture area by 1 pixel"), m_resizeEnabled); +} + +void FineAdjustTips::drawKeyCap(QPainter &painter, const QRect &rect, const QString &text, bool enabled) +{ + QPainterPath path; + path.addRoundedRect(rect, KEY_CAP_RADIUS, KEY_CAP_RADIUS); + painter.fillPath(path, capBackground(enabled)); + + painter.setFont(keyCapFont()); + painter.setPen(textColor(enabled)); + painter.drawText(rect, Qt::AlignCenter, text); +} + +void FineAdjustTips::drawSeparator(QPainter &painter, const QRect &rect, bool enabled) +{ + QPen pen(textColor(enabled)); + pen.setWidthF(1.5); + pen.setCapStyle(Qt::RoundCap); + painter.setPen(pen); + painter.drawLine(rect.bottomLeft(), rect.topRight()); +} + +void FineAdjustTips::drawPlus(QPainter &painter, const QRect &rect, bool enabled) +{ + QPen pen(textColor(enabled)); + pen.setWidthF(1.2); + pen.setCapStyle(Qt::RoundCap); + painter.setPen(pen); + const int centerX = rect.center().x(); + const int centerY = rect.center().y(); + painter.drawLine(rect.left(), centerY, rect.right(), centerY); + painter.drawLine(centerX, rect.top(), centerX, rect.bottom()); +} + +void FineAdjustTips::drawArrowKeys(QPainter &painter, int x, int y, bool enabled) +{ + // 上/下/左/右方向键符号,避免源码中直接出现非 ASCII 字符 + static const ushort arrowKeys[] = {0x2191, 0x2193, 0x2190, 0x2192}; + for (ushort code : arrowKeys) { + drawKeyCap(painter, QRect(x, y, KEY_CAP_SIZE, KEY_CAP_SIZE), + QString(QChar(code)), enabled); + x += KEY_CAP_STEP; + } +} + +void FineAdjustTips::drawDescription(QPainter &painter, const QPoint &topLeft, const QString &text, bool enabled) +{ + painter.setFont(descriptionFont()); + painter.setPen(textColor(enabled)); + const QRect box(topLeft.x(), topLeft.y(), width() - topLeft.x() - PANEL_PADDING, KEY_CAP_SIZE); + painter.drawText(box, Qt::AlignLeft | Qt::AlignVCenter, text); +} diff --git a/src/widgets/fineadjusttips.h b/src/widgets/fineadjusttips.h new file mode 100644 index 000000000..dc7110f25 --- /dev/null +++ b/src/widgets/fineadjusttips.h @@ -0,0 +1,68 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef FINEADJUSTTIPS_H +#define FINEADJUSTTIPS_H + +#include + +#include +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +/** + * @brief 像素级微调快捷键提示面板。 + * + * 对应 Sketch 中的 “提示语/截图时” 与 “提示语/选区完成时” 两个控件: + * 未框选时提示移动鼠标指针,框选完成后提示移动、扩大、缩小选区。 + */ +class FineAdjustTips : public DWidget +{ + Q_OBJECT +public: + enum Mode { + CursorMode, // 尚未框选,提示移动鼠标指针 + SelectionMode // 框选完成,提示移动/扩大/缩小选区 + }; + + explicit FineAdjustTips(DWidget *parent = nullptr); + ~FineAdjustTips() override; + + /** + * @brief 切换提示内容(移动指针 / 调整选区) + */ + void setMode(Mode mode); + Mode mode() const; + + /** + * @brief 固定比例激活时,扩大/缩小两行置灰且不可用 + */ + void setResizeEnabled(bool enabled); + bool resizeEnabled() const; + + /** + * @brief 依据当前内容重新计算尺寸,并停靠到父窗口左下角 + */ + void dockToBottomLeft(); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QSize panelSize() const; + int contentWidth() const; + void drawKeyCap(QPainter &painter, const QRect &rect, const QString &text, bool enabled); + void drawSeparator(QPainter &painter, const QRect &rect, bool enabled); + void drawPlus(QPainter &painter, const QRect &rect, bool enabled); + void drawArrowKeys(QPainter &painter, int x, int y, bool enabled); + void drawDescription(QPainter &painter, const QPoint &topLeft, const QString &text, bool enabled); + + Mode m_mode = CursorMode; + bool m_resizeEnabled = true; +}; + +#endif // FINEADJUSTTIPS_H diff --git a/src/widgets/shapeswidget.cpp b/src/widgets/shapeswidget.cpp index fa279c7de..74f9ab7fb 100755 --- a/src/widgets/shapeswidget.cpp +++ b/src/widgets/shapeswidget.cpp @@ -310,6 +310,12 @@ void ShapesWidget::clearSelected() //qDebug() << "clear selected!!!"; m_isSelected = false; + m_isResize = false; + m_isRotated = false; + m_isArrowRotated = false; + m_resizeDirection = Outting; + m_clickedKey = Unknow; + m_isMoving = false; m_selectedIndex = -1; m_hoveredIndex = -1; m_selectedOrder = -1; @@ -391,6 +397,13 @@ bool ShapesWidget::clickedOnShapes(QPointF pos) qCDebug(dsrApp) << "Checking for shape click at position:" << pos; bool onShapes = false; m_selectedOrder = -1; + // 上一次选择可能停留在缩放/旋转状态;命中检测会按当前图形重新设置。 + // 序号只参与整体移动,不复用其它图形遗留的缩放/旋转语义。 + m_isResize = false; + m_isRotated = false; + m_isArrowRotated = false; + m_resizeDirection = Outting; + m_clickedKey = Unknow; // qDebug() << ">>>>> function: " << __func__ << ", line: " << __LINE__ // << ", pos: " << pos // << ", m_shapes.length(): " << m_shapes.length(); @@ -462,6 +475,9 @@ bool ShapesWidget::clickedOnShapes(QPointF pos) if (seqRect.contains(pos)) { currentOnShape = true; m_isSelected = true; + m_isResize = false; + m_isRotated = false; + m_resizeDirection = Moving; emit shapeClicked("sequence"); } } @@ -1507,6 +1523,15 @@ void ShapesWidget::mousePressEvent(QMouseEvent *e) return; } + // 开始新的绘制前必须先清掉旧的选中态,否则 mouseReleaseEvent 会因 + // m_isSelected 仍为 true 而跳过落盘,并且旧标注的拖动锚点会污染新图形。 + clearSelected(); + setAllTextEditReadOnly(); + m_editing = false; + m_selectedShape = Toolshape(); + m_hoveredShape = Toolshape(); + m_currentShape = Toolshape(); + m_isRecording = true; //qDebug() << "no one shape be clicked!" << m_selectedIndex << m_shapes.length(); @@ -2019,6 +2044,76 @@ void ShapesWidget::mouseMoveEvent(QMouseEvent *e) DFrame::mouseMoveEvent(e); } +bool ShapesWidget::hasSelectedShape() const +{ + return m_selectedIndex != -1 && m_selectedOrder >= 0 + && m_selectedOrder < static_cast(m_shapes.size()); +} + +bool ShapesWidget::isPressed() const +{ + return m_isPressed; +} + +void ShapesWidget::translateAllShapes(const QPointF &delta) +{ + if (delta.isNull()) { + return; + } + auto shiftPoints = [&delta](QList &points) { + for (QPointF &point : points) { + point += delta; + } + }; + auto hasGeometry = [](const Toolshape &shape) { + for (const QPointF &point : shape.mainPoints) { + if (!point.isNull()) { + return true; + } + } + return !shape.points.isEmpty() || !shape.arrowRotatePos.isNull(); + }; + auto translateShape = [&delta, &shiftPoints, &hasGeometry](Toolshape &shape) { + // 空 Toolshape 的 mainPoints 全是 (0,0),不能直接整体加偏移; + // 否则会把“未绘制”误变成有效几何,后续绘制和拖动都会出现幽灵图形。 + if (!hasGeometry(shape)) { + return; + } + shiftPoints(shape.mainPoints); + shiftPoints(shape.points); + if (!shape.arrowRotatePos.isNull()) { + shape.arrowRotatePos += delta; + } + }; + for (Toolshape &shape : m_shapes) { + translateShape(shape); + } + for (Toolshape *shape : {&m_currentShape, &m_selectedShape, &m_hoveredShape}) { + translateShape(*shape); + } + for (TextEdit *edit : m_editMap) { + if (edit) { + edit->move(edit->pos() + delta.toPoint()); + } + } + + // ShapesWidget 的原点移动后,鼠标锚点也必须同步平移,否则下一次 + // mouseMoveEvent 会用新旧两个坐标系计算增量,表现为标注跳跃。 + if (!m_pos1.isNull()) { + m_pos1 += delta; + } + if (!m_pos2.isNull()) { + m_pos2 += delta; + } + if (!m_pressedPoint.isNull()) { + m_pressedPoint += delta; + } + if (!m_movingPoint.isNull()) { + m_movingPoint += delta; + } + update(); +} + //鼠标选中图形时,可以通过w/a/s/d及小键盘方向键控制光标移动 void ShapesWidget::keyPressEvent(QKeyEvent *keyEvent) { @@ -2959,6 +3054,9 @@ void ShapesWidget::paintSequence(QPainter &painter, FourPoints mainFPoints, return; } QColor fillColor = BaseUtils::colorIndexOf(colorIndex); + // 序号需要填充圆和文字;必须恢复 painter 状态,否则后续图形会继承 + // 这里的 brush,圆形/矩形会被误画成实心图形。 + painter.save(); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(Qt::NoPen); painter.setBrush(QBrush(fillColor)); @@ -2973,6 +3071,7 @@ void ShapesWidget::paintSequence(QPainter &painter, FourPoints mainFPoints, font.setWeight(QFont::Black); painter.setFont(font); painter.drawText(rectF, Qt::AlignCenter, QString::number(sequenceNumber)); + painter.restore(); } void ShapesWidget::updateSequencePreview(const QPointF &pos) diff --git a/src/widgets/shapeswidget.h b/src/widgets/shapeswidget.h index 39c4ecdb2..9e6abc908 100755 --- a/src/widgets/shapeswidget.h +++ b/src/widgets/shapeswidget.h @@ -173,6 +173,20 @@ public slots: void deleteCurrentShape(); //QString getCurrentType(); void microAdjust(QString direction); + /** + * @brief hasSelectedShape: 当前是否存在选中的标注 + * 编辑态下用于区分 Ctrl+方向键等快捷键的作用对象(标注 / 截图区域)。 + */ + bool hasSelectedShape() const; + /** + * @brief isPressed: 当前是否正在拖拽/绘制标注 + */ + bool isPressed() const; + /** + * @brief translateAllShapes: 整体平移所有标注,保持其在屏幕上的绝对位置不变 + * @param delta 平移量(标注局部坐标系) + */ + void translateAllShapes(const QPointF &delta); void setShiftKeyPressed(bool isShift); void updateCursorShape(); void menuSaveSlot(); diff --git a/src/widgets/subtoolwidget.cpp b/src/widgets/subtoolwidget.cpp index e8651d8f7..e97fdc7a6 100755 --- a/src/widgets/subtoolwidget.cpp +++ b/src/widgets/subtoolwidget.cpp @@ -745,7 +745,7 @@ void SubToolWidget::initShotLabel() Utils::setAccessibility(m_aiAssistantButton, AC_SUBTOOLWIDGET_AI_ASSISTANT_BUTTON); m_shotBtnGroup->addButton(m_aiAssistantButton); m_aiAssistantButton->setFixedSize(TOOL_BUTTON_SIZE); - installTipHint(m_aiAssistantButton, tr("AI Screenshot (A)")); + installTipHint(m_aiAssistantButton, tr("AI Screenshot (Alt+A)")); btnList.append(m_aiAssistantButton); connect(m_aiAssistantButton, &ToolButton::clicked, this, [=] { ConfigSettings::instance()->setValue("shot", "ai_assistant_used", true); @@ -1540,7 +1540,7 @@ void SubToolWidget::initScrollLabel() Utils::setAccessibility(m_aiAssistantScrollButton, AC_SUBTOOLWIDGET_AI_ASSISTANT_BUTTON); m_aiAssistantScrollButton->setFixedSize(TOOL_BUTTON_SIZE); - installTipHint(m_aiAssistantScrollButton, tr("AI Screenshot (A)")); + installTipHint(m_aiAssistantScrollButton, tr("AI Screenshot (Alt+A)")); scrollBtnGroup->addButton(m_aiAssistantScrollButton); btnList.append(m_aiAssistantScrollButton); connect(m_aiAssistantScrollButton, &ToolButton::clicked, this, [=]{ diff --git a/src/widgets/zoomIndicator.cpp b/src/widgets/zoomIndicator.cpp index 2b08a5257..1f1cb45e4 100755 --- a/src/widgets/zoomIndicator.cpp +++ b/src/widgets/zoomIndicator.cpp @@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "zoomIndicator.h" + #include "../utils/baseutils.h" #include "../utils/tempfile.h" #include "../utils.h" @@ -15,21 +16,82 @@ #include #endif +#include #include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include + namespace { -const QSize BACKGROUND_SIZE = QSize(59, 59); -//const int SCALE_VALUE = 4; -const int IMG_WIDTH = 12; -const int INDICATOR_WIDTH = 49; -const int CENTER_RECT_WIDTH = 12; -const int BOTTOM_RECT_HEIGHT = 14; +// 以下尺寸、颜色、字号均取自 Sketch 画板「截图录屏-光标指针」(8F8261D2-...)。 +// 放大镜浮窗:预览区 150x105 + 信息区 150x65。 +constexpr int PREVIEW_HEIGHT = 105; +constexpr int INFO_HEIGHT = 65; +static_assert(PREVIEW_HEIGHT + INFO_HEIGHT == ZoomIndicator::PANEL_HEIGHT, + "preview + info height must match the panel height"); +constexpr int PREVIEW_RADIUS = 8; + +// 以光标为中心取样约 15px 宽的区域,放大 10 倍渲染。 +constexpr int SAMPLE_WIDTH = 15; +constexpr int SAMPLE_HEIGHT = 11; + +// 中心准心框(1px 白描边,中心缺口由十字分隔线让出)。 +constexpr int CENTER_BOX_SIZE = 9; + +constexpr int SWATCH_SIZE = 10; +constexpr int SWATCH_TEXT_GAP = 6; +constexpr int FONT_PIXEL_SIZE = 12; + +// 预览区 1px 描边:#FFFFFF 19.8% +const QColor PREVIEW_BORDER_COLOR(255, 255, 255, 50); +// 十字分隔线:#FFFFFF 20% +const QColor DIVIDER_COLOR(255, 255, 255, 51); +// 中心准心框:#FFFFFF 50% +const QColor CENTER_BOX_COLOR(255, 255, 255, 128); +// 信息区底色:#000000 49.8% +const QColor INFO_FILL_COLOR(0, 0, 0, 127); +// 信息区描边:#000000 19.8% +const QColor INFO_BORDER_COLOR(0, 0, 0, 50); +// 取样失败时的兜底底色 +const QColor PREVIEW_FALLBACK_COLOR(0, 0, 0, 127); + +QString toHexString(const QColor &color) +{ + // 取样失败(图像为空或坐标越界)时返回空串,避免把无效色值写成 "#-1-1-1"。 + if (!color.isValid()) { + return QString(); + } + return QString("#%1%2%3") + .arg(color.red(), 2, 16, QChar('0')) + .arg(color.green(), 2, 16, QChar('0')) + .arg(color.blue(), 2, 16, QChar('0')) + .toLower(); +} + +// 预览区路径:上方圆角,下边缘与信息区平接。 +QPainterPath previewPath() +{ + const qreal r = PREVIEW_RADIUS; + const qreal w = ZoomIndicator::PANEL_WIDTH; + const qreal h = PREVIEW_HEIGHT; + QPainterPath path; + path.moveTo(0, r); + path.arcTo(0, 0, 2 * r, 2 * r, 180, -90); + path.lineTo(w - r, 0); + path.arcTo(w - 2 * r, 0, 2 * r, 2 * r, 90, -90); + path.lineTo(w, h); + path.lineTo(0, h); + path.closeSubpath(); + return path; } -ZoomIndicator::ZoomIndicator(DWidget *parent,bool isRecord) +} // namespace + +ZoomIndicator::ZoomIndicator(DWidget *parent, bool isRecord) : DLabel(parent) { qCDebug(dsrApp) << "ZoomIndicator constructor called, isRecord:" << isRecord; @@ -62,17 +124,9 @@ ZoomIndicator::ZoomIndicator(DWidget *parent,bool isRecord) } #endif - setFixedSize(BACKGROUND_SIZE); -// setStyleSheet(getFileContent(":/resources/qss/zoomindicator.qss")); + setFixedSize(PANEL_WIDTH, PANEL_HEIGHT); setAttribute(Qt::WA_TransparentForMouseEvents); -// QString t_zoomStyle = QString("border-image: url(:/resources/images/action/magnifier.png);"); -// this->setStyleSheet(t_zoomStyle); - - m_centerRect = QRect((BACKGROUND_SIZE.width() - CENTER_RECT_WIDTH) / 2 + 1, - (BACKGROUND_SIZE.width() - CENTER_RECT_WIDTH) / 2 + 1, - CENTER_RECT_WIDTH, CENTER_RECT_WIDTH); - - m_globalRect = QRect(0, 0, BACKGROUND_SIZE.width(), BACKGROUND_SIZE.height()); + setAttribute(Qt::WA_TranslucentBackground); } ZoomIndicator::~ZoomIndicator() @@ -81,76 +135,151 @@ ZoomIndicator::~ZoomIndicator() #ifndef ENABLE_UNIT_TEST if (Utils::isWaylandMode && !m_isOpenWM && m_zoomIndicatorGL && !m_isRecord) { qCDebug(dsrApp) << "Deleting ZoomIndicatorGL in Wayland mode."; - delete m_zoomIndicatorGL; + delete m_zoomIndicatorGL; } #endif } +const QImage &ZoomIndicator::cachedFullscreenImage() const +{ + // getFullscreenPixmap() 每次返回的时候是隐式共享的拷贝,这里按 cacheKey 缓存 + // 转换后的 QImage,避免每次重绘都对整屏图做一次深拷贝。 + const QPixmap pixmap = TempFile::instance()->getFullscreenPixmap(); + const qint64 key = pixmap.cacheKey(); + if (key != m_cachedPixmapKey || m_cachedImage.isNull()) { + m_cachedImage = pixmap.toImage(); + m_cachedImage.setDevicePixelRatio(1.0); + m_cachedPixmapKey = key; + } + return m_cachedImage; +} + +QColor ZoomIndicator::sampleColor() const +{ + const QImage &image = cachedFullscreenImage(); + if (image.isNull() || !image.rect().contains(m_cursorPos)) { + return QColor(); + } + return QColor(image.pixel(m_cursorPos)); +} + +QString ZoomIndicator::currentColorHex() const +{ + return toHexString(sampleColor()); +} + +void ZoomIndicator::drawPreview(QPainter &painter) +{ + const QPainterPath path = previewPath(); + const QRectF previewRect(0, 0, PANEL_WIDTH, PREVIEW_HEIGHT); + + painter.save(); + painter.setClipPath(path); + // 放大视图要保持像素边界清晰,这里关闭平滑。 + painter.setRenderHint(QPainter::SmoothPixmapTransform, false); + + const QImage &image = cachedFullscreenImage(); + const QRect sourceRect(m_cursorPos.x() - SAMPLE_WIDTH / 2, + m_cursorPos.y() - SAMPLE_HEIGHT / 2, + SAMPLE_WIDTH, SAMPLE_HEIGHT); + if (!image.isNull() && image.rect().intersects(sourceRect)) { + painter.drawImage(previewRect, image, sourceRect); + } else { + painter.fillRect(previewRect, PREVIEW_FALLBACK_COLOR); + } + painter.restore(); + + painter.save(); + painter.setBrush(Qt::NoBrush); + painter.setPen(QPen(PREVIEW_BORDER_COLOR, 1)); + painter.drawPath(path); + painter.restore(); +} + +void ZoomIndicator::drawCrosshair(QPainter &painter) +{ + // 设计稿:水平分隔线 y=53、垂直分隔线 x=75、中心准心框 9x9 位于 (70.5, 48.5)。 + // 1px 线条必须落在半像素上,否则会被平分到相邻两列/行,渲染成模糊的浅色线。 + const qreal lineY = PREVIEW_HEIGHT / 2.0; // 52.5 + const qreal lineX = PANEL_WIDTH / 2.0 + 0.5; // 75.5 + const qreal boxLeft = PANEL_WIDTH / 2.0 - 4.5; // 70.5 + const qreal boxTop = PREVIEW_HEIGHT / 2.0 - 4.0; // 48.5 + const qreal boxRight = boxLeft + CENTER_BOX_SIZE; // 79.5 + const qreal boxBottom = boxTop + CENTER_BOX_SIZE; // 57.5 + + painter.save(); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.setPen(QPen(DIVIDER_COLOR, 1)); + // 水平分隔线,中间让出中心准心框 + painter.drawLine(QPointF(0, lineY), QPointF(boxLeft, lineY)); + painter.drawLine(QPointF(boxRight, lineY), QPointF(PANEL_WIDTH, lineY)); + // 垂直分隔线 + painter.drawLine(QPointF(lineX, 0), QPointF(lineX, boxTop)); + painter.drawLine(QPointF(lineX, boxBottom), QPointF(lineX, PREVIEW_HEIGHT)); + + // 中心准心框:1px 描边 + painter.setBrush(Qt::NoBrush); + painter.setPen(QPen(CENTER_BOX_COLOR, 1)); + painter.drawRect(QRectF(boxLeft, boxTop, CENTER_BOX_SIZE, CENTER_BOX_SIZE)); + painter.restore(); +} + +void ZoomIndicator::drawInfoPanel(QPainter &painter) +{ + const QRectF infoRect(0, PREVIEW_HEIGHT, PANEL_WIDTH, INFO_HEIGHT); + + painter.save(); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.fillRect(infoRect, INFO_FILL_COLOR); + painter.setBrush(Qt::NoBrush); + painter.setPen(QPen(INFO_BORDER_COLOR, 1)); + painter.drawRect(infoRect.adjusted(0.5, 0.5, -0.5, -0.5)); + + QFont font = painter.font(); + font.setPixelSize(FONT_PIXEL_SIZE); + painter.setFont(font); + painter.setPen(QColor(Qt::white)); + + // 坐标:(X,Y) + const QRectF coordRect(0, PREVIEW_HEIGHT + 4, PANEL_WIDTH, 17); + painter.drawText(coordRect, Qt::AlignCenter, + QString("(%1,%2)").arg(m_screenPos.x()).arg(m_screenPos.y())); + + // 颜色行:色块 + HEX,整体水平居中 + const QColor color = sampleColor(); + const QString hex = toHexString(color); + const QFontMetrics metrics(font); + const qreal textWidth = metrics.horizontalAdvance(hex); + const qreal rowWidth = SWATCH_SIZE + SWATCH_TEXT_GAP + textWidth; + const qreal rowLeft = (PANEL_WIDTH - rowWidth) / 2.0; + const qreal rowTop = PREVIEW_HEIGHT + 23; + + const QRectF swatchRect(rowLeft, PREVIEW_HEIGHT + 28, SWATCH_SIZE, SWATCH_SIZE); + if (color.isValid()) { + painter.fillRect(swatchRect, color); + } + painter.setBrush(Qt::NoBrush); + painter.setPen(QPen(QColor(Qt::white), 1)); + painter.drawRect(swatchRect); + + painter.setPen(QColor(Qt::white)); + painter.drawText(QRectF(rowLeft + SWATCH_SIZE + SWATCH_TEXT_GAP, rowTop, textWidth, 17), + Qt::AlignCenter, hex); + + // 底部操作提示 + const QRectF tipRect(0, PREVIEW_HEIGHT + 42, PANEL_WIDTH, 17); + painter.drawText(tipRect, Qt::AlignCenter, tr("Press Ctrl+c to copy color")); + + painter.restore(); +} + void ZoomIndicator::paintEvent(QPaintEvent *) { - qCDebug(dsrApp) << "ZoomIndicator::paintEvent called."; -// using namespace utils; - //通过此方式获取光标的位置实际是当前光标所在的屏幕的位置加上光标的位置,实际上已经进行了缩放,但是只缩放光标所在的屏幕 - //例如两个1920的屏幕横连,实际宽度是3840,进行1.25缩放后实际宽度是3072、但是屏幕1上的点的范围是0~1920,屏幕二上的范围是1920~3840 -// QPoint centerPos = this->cursor().pos(); //不清楚为啥有些时候此接口获取的值是两个屏幕都缩放了,有些时候只有一个屏幕缩放 - qreal ration = this->devicePixelRatioF(); - QPoint centerPos = m_cursorPos / ration; -// qDebug() << "0 centerPos: " << centerPos << "m_cursorPos: " << m_cursorPos; -// centerPos = QPoint(std::max(centerPos.x() - this->window()->x(), 0), -// std::max(centerPos.y() - this->window()->y(), 0)); -// qDebug() << "1 centerPos: " << centerPos; QPainter painter(this); - //获取背景图片 - const QPixmap &fullscreenImgFile = TempFile::instance()->getFullscreenPixmap(); -// qDebug() << "fullscreenImgFile.size(): " << fullscreenImgFile.size(); - QImage fullscreenImg = fullscreenImgFile.toImage(); -// qDebug() << "1 fullscreenImg.size(): " << fullscreenImg.size(); - - //将背景图片按屏幕缩放比进行缩放 - fullscreenImg = fullscreenImg.scaled(static_cast(fullscreenImg.width() / ration), - static_cast(fullscreenImg.height() / ration), Qt::KeepAspectRatio); - -// qDebug() << "2 fullscreenImg.size(): " << fullscreenImg.size(); - //获取所有屏幕的信息 -// QList screensInfo = Utils::getScreensInfo(); -// for (int i = 0; i < screensInfo.size(); i++) { -// //判断当前点在哪块屏幕上 -// if (centerPos.x() > screensInfo[i].x && centerPos.x() < screensInfo[i].x + screensInfo[i].width && -// centerPos.y() > screensInfo[i].y && centerPos.y() < screensInfo[i].y + screensInfo[i].height) { -// centerPos.setX(static_cast((centerPos.x() - screensInfo[i].x) * ration + screensInfo[i].x )); -// centerPos.setY(static_cast((centerPos.y() - screensInfo[i].y) * ration + screensInfo[i].y )); -// break; -// } -// } -// qDebug() << "2 centerPos: " << centerPos; -// centerPos = centerPos*ration; -// qDebug() << "3 centerPos: " << centerPos; - //返回centerPos位置的像素颜色 - const QRgb centerRectRgb = fullscreenImg.pixel(centerPos); - QRect tempRec = QRect(centerPos.x() - IMG_WIDTH / 2, centerPos.y() - IMG_WIDTH / 2, IMG_WIDTH, IMG_WIDTH) ; - //qDebug() << " tempRec: " << tempRec; - QPixmap zoomPix = QPixmap(fullscreenImgFile).scaled( - fullscreenImg.width(), fullscreenImg.height()).copy(tempRec); - - zoomPix = zoomPix.scaled(QSize(INDICATOR_WIDTH, INDICATOR_WIDTH), - Qt::KeepAspectRatio); - painter.drawPixmap(QRect(5, 5, INDICATOR_WIDTH, INDICATOR_WIDTH), zoomPix); - painter.drawPixmap(m_centerRect, QPixmap(":/images/action/center_rect.png")); - painter.drawPixmap(m_globalRect, QPixmap(":/images/action/magnifier.png")); - m_lastCenterPosBrush = QBrush(QColor(qRed(centerRectRgb), - qGreen(centerRectRgb), qBlue(centerRectRgb))); - painter.fillRect(QRect(INDICATOR_WIDTH / 2 + 2, INDICATOR_WIDTH / 2 + 2, - CENTER_RECT_WIDTH - 4, CENTER_RECT_WIDTH - 4), m_lastCenterPosBrush); - painter.fillRect(QRect(5, INDICATOR_WIDTH - 9, INDICATOR_WIDTH, BOTTOM_RECT_HEIGHT), - QBrush(QColor(0, 0, 0, 125))); - QFont posFont; - posFont.setPixelSize(9); - painter.setFont(posFont); - painter.setPen(QColor(Qt::white)); - QTextOption posTextOption; - posTextOption.setAlignment(Qt::AlignHCenter | Qt::AlignTop); - painter.drawText(QRectF(5, INDICATOR_WIDTH - 10, INDICATOR_WIDTH, INDICATOR_WIDTH), - QString("%1, %2").arg(m_cursorPos.x()).arg(m_cursorPos.y()), posTextOption); + painter.setRenderHint(QPainter::Antialiasing, true); + drawPreview(painter); + drawCrosshair(painter); + drawInfoPanel(painter); } void ZoomIndicator::showMagnifier(QPoint pos) @@ -162,10 +291,9 @@ void ZoomIndicator::showMagnifier(QPoint pos) } #endif - this->show(); - this->move(pos); - //qDebug() << "this->pos(): " << this->pos(); + this->show(); + this->update(); } void ZoomIndicator::hideMagnifier() @@ -185,5 +313,9 @@ void ZoomIndicator::hideMagnifier() void ZoomIndicator::setCursorPos(QPoint pos) { m_cursorPos = pos; - //qDebug() << "0 pos: " << pos << "m_cursorPos: " << m_cursorPos; +} + +void ZoomIndicator::setScreenPos(QPoint pos) +{ + m_screenPos = pos; } diff --git a/src/widgets/zoomIndicator.h b/src/widgets/zoomIndicator.h index 58f5b34c0..b0cea9254 100755 --- a/src/widgets/zoomIndicator.h +++ b/src/widgets/zoomIndicator.h @@ -7,34 +7,58 @@ #define ZOOMINDICATOR_H #include "zoomIndicatorGL.h" + #include -#include -#include -#include +#include +#include +#include +#include DWIDGET_USE_NAMESPACE +class QPainter; + +/** + * @brief The ZoomIndicator class + * 截图选区阶段跟随光标的像素放大镜与取色浮窗。 + * 视觉参数取自 Sketch 画板「截图录屏-光标指针」。 + */ class ZoomIndicator : public DLabel { Q_OBJECT public: - explicit ZoomIndicator(DWidget *parent = 0,bool isRecord = false); - ~ZoomIndicator(); + // 浮窗尺寸(逻辑像素),取自 Sketch 画板「截图录屏-光标指针」 + static constexpr int PANEL_WIDTH = 150; + static constexpr int PANEL_HEIGHT = 170; + + explicit ZoomIndicator(DWidget *parent = nullptr, bool isRecord = false); + ~ZoomIndicator() override; void showMagnifier(QPoint pos); void hideMagnifier(); + // 屏幕物理像素坐标,用于放大取样与取色 void setCursorPos(QPoint pos); + // 屏幕逻辑坐标,用于浮窗上的坐标文案 + void setScreenPos(QPoint pos); + + QString currentColorHex() const; + protected: - void paintEvent(QPaintEvent *); + void paintEvent(QPaintEvent *) override; private: - QRect m_globalRect; - QRect m_centerRect; - QBrush m_lastCenterPosBrush; + QColor sampleColor() const; + const QImage &cachedFullscreenImage() const; + void drawPreview(QPainter &painter); + void drawCrosshair(QPainter &painter); + void drawInfoPanel(QPainter &painter); ZoomIndicatorGL *m_zoomIndicatorGL = nullptr; + // 物理像素坐标(全屏截图坐标系) QPoint m_cursorPos; + // 逻辑屏幕坐标(坐标文案显示) + QPoint m_screenPos; /** * @brief 是否打开多任务视图界面。 @@ -43,6 +67,9 @@ class ZoomIndicator : public DLabel */ bool m_isOpenWM = false; bool m_isRecord = false; + + mutable QImage m_cachedImage; + mutable qint64 m_cachedPixmapKey = 0; }; -#endif // MAGNIFIER_H +#endif // ZOOMINDICATOR_H diff --git a/translations/deepin-screen-recorder_zh_CN.ts b/translations/deepin-screen-recorder_zh_CN.ts index 77f676044..acd5f1b8b 100644 --- a/translations/deepin-screen-recorder_zh_CN.ts +++ b/translations/deepin-screen-recorder_zh_CN.ts @@ -775,8 +775,8 @@ Hold down Shift to draw squares or circles. 保存时选择位置 - AI Screenshot (A) - AI截图 (A) + AI Screenshot (Alt+A) + AI截图 (Alt+A) Save to Desktop @@ -881,4 +881,30 @@ Hold down Shift to draw squares or circles. 选中后,下次截图保持该尺寸/比例 + + FineAdjustTips + + Move the mouse pointer by 1 pixel + 将鼠标指针移动1像素 + + + Move the capture area by 1 pixel + 将截屏选区移动1像素 + + + Enlarge the capture area by 1 pixel + 将截屏选区扩大1像素 + + + Shrink the capture area by 1 pixel + 将截屏选区缩小1像素 + + + + ZoomIndicator + + Press Ctrl+c to copy color + 按Ctrl+c复制色值 + +