diff --git a/assets/icons/icons.qrc b/assets/icons/icons.qrc index 6311349ce..25e02b646 100644 --- a/assets/icons/icons.qrc +++ b/assets/icons/icons.qrc @@ -16,6 +16,14 @@ texts/ocr-normal_32px.svg texts/table-recognition_32px.svg texts/sequence_32px.svg + texts/corner-radius_32px.svg + texts/corner-sharp_32px.svg + texts/corner-adjust_32px.svg + texts/size-ratio_32px.svg + texts/ratio-free_32px.svg + texts/ratio-lock_32px.svg + texts/ratio-unlock_32px.svg + texts/selection-fixed_32px.svg texts/oval-normal_32px.svg texts/pinscreenshots_32px.svg texts/recorder_32px.svg diff --git a/assets/icons/texts/corner-adjust_32px.svg b/assets/icons/texts/corner-adjust_32px.svg new file mode 100644 index 000000000..1d40294c4 --- /dev/null +++ b/assets/icons/texts/corner-adjust_32px.svg @@ -0,0 +1,7 @@ + + + 图标/截图工具栏/圆角调节 + + + + \ No newline at end of file diff --git a/assets/icons/texts/corner-radius_32px.svg b/assets/icons/texts/corner-radius_32px.svg new file mode 100644 index 000000000..c65deba5e --- /dev/null +++ b/assets/icons/texts/corner-radius_32px.svg @@ -0,0 +1,7 @@ + + + 图标/截图工具栏/圆角 + + + + \ No newline at end of file diff --git a/assets/icons/texts/corner-sharp_32px.svg b/assets/icons/texts/corner-sharp_32px.svg new file mode 100644 index 000000000..bd9f1ca24 --- /dev/null +++ b/assets/icons/texts/corner-sharp_32px.svg @@ -0,0 +1,9 @@ + + + 直角 + + + + + + diff --git a/assets/icons/texts/ratio-free_32px.svg b/assets/icons/texts/ratio-free_32px.svg new file mode 100644 index 000000000..3520977eb --- /dev/null +++ b/assets/icons/texts/ratio-free_32px.svg @@ -0,0 +1,7 @@ + + + 图标/截图工具栏/比例/自由 + + + + \ No newline at end of file diff --git a/assets/icons/texts/ratio-lock_32px.svg b/assets/icons/texts/ratio-lock_32px.svg new file mode 100644 index 000000000..33b880577 --- /dev/null +++ b/assets/icons/texts/ratio-lock_32px.svg @@ -0,0 +1,7 @@ + + + 锁定比例 + + + + diff --git a/assets/icons/texts/ratio-unlock_32px.svg b/assets/icons/texts/ratio-unlock_32px.svg new file mode 100644 index 000000000..d7a3f7b15 --- /dev/null +++ b/assets/icons/texts/ratio-unlock_32px.svg @@ -0,0 +1,7 @@ + + + 不锁定比例 + + + + diff --git a/assets/icons/texts/selection-fixed_32px.svg b/assets/icons/texts/selection-fixed_32px.svg new file mode 100644 index 000000000..f28dd97ec --- /dev/null +++ b/assets/icons/texts/selection-fixed_32px.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/texts/size-ratio_32px.svg b/assets/icons/texts/size-ratio_32px.svg new file mode 100644 index 000000000..3520977eb --- /dev/null +++ b/assets/icons/texts/size-ratio_32px.svg @@ -0,0 +1,7 @@ + + + 图标/截图工具栏/比例/自由 + + + + \ No newline at end of file diff --git a/src/main_window.cpp b/src/main_window.cpp index 77e6247ab..b80a8d607 100755 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -49,6 +49,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -521,6 +523,41 @@ void MainWindow::initAttributes() m_sideBar = new SideBar(this); m_sideBar->hide(); + // 选区右上角的圆角 / 尺寸比例工具组 + m_roundedCornerWidget = new RoundedCornerWidget(this); + m_roundedCornerWidget->hide(); + connect(m_roundedCornerWidget, &RoundedCornerWidget::radiusChanged, this, [this](int radius) { + // 圆角实时预览只影响选区四个角,按角区域局部重绘即可。 + // 直接 update() 会整屏重绘(背景位图 + 蒙层),拖动滑块时会明显卡顿。 + if (status::shot != m_functionType || recordWidth <= 0 || recordHeight <= 0) { + m_radiusPreviewRadius = radius; + update(); + return; + } + // 同时覆盖旧值和新值影响的范围,半径变小时旧圆角能正确恢复 + const int span = qMax(m_radiusPreviewRadius, radius); + m_radiusPreviewRadius = radius; + QRegion dirty; + if (span > 0) { + const int side = span + 2; // +1 抗锯齿边界,+1 虚线框 + const int left = recordX - 1; + const int right = recordX + recordWidth - span - 1; + const int top = recordY - 1; + const int bottom = recordY + recordHeight - span - 1; + dirty += QRect(left, top, side, side); + dirty += QRect(right, top, side, side); + dirty += QRect(left, bottom, side, side); + dirty += QRect(right, bottom, side, side); + } + if (dirty.isEmpty()) { + update(); + } else { + update(dirty); + } + }); + connect(m_roundedCornerWidget, &RoundedCornerWidget::selectionSizeChanged, + this, &MainWindow::onRoundedSelectionSizeChanged); + m_sizeTips = new TopTips(this); m_sizeTips->setFullScreenRecord(m_isFullScreenRecord); m_sizeTips->hide(); @@ -1315,12 +1352,53 @@ void MainWindow::initToolBarShortcut() qCDebug(dsrApp) << "initShortcut end"; } +bool MainWindow::isRoundedPanelEditorFocused() const +{ + QWidget *focus = QApplication::focusWidget(); + if (!focus || !m_roundedCornerWidget) { + return false; + } + // 只对文本输入控件让路:滑块、按钮上的回车仍然保持“保存截图”的含义。 + if (!qobject_cast(focus) && !qobject_cast(focus)) { + return false; + } + for (QWidget *widget = focus; widget; widget = widget->parentWidget()) { + if (widget == m_roundedCornerWidget) { + return true; + } + } + return false; +} + void MainWindow::initSaveShortcut() { + // 重复初始化时先清掉旧的快捷方式,避免残留的 QShortcut 继续抢占回车 + delete m_returnSaveShortcut; + delete m_enterSaveShortcut; + m_returnSaveShortcut = nullptr; + m_enterSaveShortcut = nullptr; + // 截图模式/滚动模式 保存截图 大键盘的enter QShortcut *returnSC = new QShortcut(QKeySequence(Qt::Key_Return), this); // 截图模式/滚动模式 保存截图 小键盘的enter QShortcut *enterSC = new QShortcut(QKeySequence(Qt::Key_Enter), this); + m_returnSaveShortcut = returnSC; + m_enterSaveShortcut = enterSC; + + // QShortcut 会先于焦点控件拿到按键:焦点在圆角/尺寸比例面板的输入框里时, + // 回车属于“确认输入”,必须先把保存截图的快捷方式禁用掉,输入框才能收到回车。 + auto updateSaveShortcutEnabled = [this]() { + const bool editing = isRoundedPanelEditorFocused(); + if (m_returnSaveShortcut) { + m_returnSaveShortcut->setEnabled(!editing); + } + if (m_enterSaveShortcut) { + m_enterSaveShortcut->setEnabled(!editing); + } + }; + connect(qApp, &QApplication::focusChanged, this, [updateSaveShortcutEnabled](QWidget *, QWidget *) { + updateSaveShortcutEnabled(); + }); // 截图模式/滚动模式 保存截图 QShortcut *saveShotSC = new QShortcut(QKeySequence("Ctrl+S"), this); // 截图模式/录屏模式(未做穿透)/滚动模式 退出 @@ -2723,7 +2801,17 @@ bool MainWindow::saveImg(const QPixmap &pix, const QString &fileName, const char } if (status::pinscreenshots == m_functionType) return false; - if (pix.save(fileName, format, quality)) { + + // 圆角截图:JPG/BMP 不支持透明,圆角外按设计稿填充白色。 + // 此处只做「白底合成」,不能再裁一次圆角,否则会把已经生成的投影边缘裁掉。 + // 不读取配置里的 radius:滑块落盘有防抖,保存时配置可能仍停留在 0; + // 以图像实际 alpha 为准,才能保证透明圆角不会在 JPEG/BMP 中被编码成黑角。 + QPixmap savePix = pix; + const QString fmt = QString::fromLatin1(format).toUpper(); + if (fmt != QLatin1String("PNG") && pix.hasAlphaChannel()) { + savePix = RoundedCornerWidget::compositeOnWhite(pix); + } + if (savePix.save(fileName, format, quality)) { qCInfo(dsrApp) << __FUNCTION__ << __LINE__ << "保存图片成功!保存质量: " << quality; return true; } else { @@ -3367,6 +3455,26 @@ void MainWindow::updateToolBarPos() // TODO: treeland适配,解决问题临时方案 if (!(Utils::isTreelandMode)) m_toolBar->showAt(toolbarPoint); + + // 圆角 / 尺寸比例工具组跟随选区右上角 + if (m_roundedCornerWidget) { + const bool canShow = (status::shot == m_functionType) + && !Utils::isTreelandMode + && recordWidth > 0 && recordHeight > 0; + if (canShow) { + // 尺寸面板与左上角尺寸提示保持一致,展示实际截图像素大小 + const qreal pixelRatio = qApp->primaryScreen()->devicePixelRatio(); + m_roundedCornerWidget->setSelectionSize( + qMax(1, qRound((recordWidth - 2) * pixelRatio)), + qMax(1, qRound((recordHeight - 1) * pixelRatio))); + m_roundedCornerWidget->followSelection( + QRect(recordX, recordY, recordWidth, recordHeight), m_backgroundRect); + m_roundedCornerWidget->show(); + m_roundedCornerWidget->raise(); + } else { + m_roundedCornerWidget->hide(); + } + } } void MainWindow::updateSideBarPos() @@ -3965,6 +4073,10 @@ void MainWindow::changeShotToolEvent(const QString &func) qCDebug(dsrApp) << "MainWindow::changeShotToolEvent >> func: " << func; + // 单击其它工具图标时收起圆角/尺寸比例属性面板 + if (m_roundedCornerWidget) { + m_roundedCornerWidget->collapsePanels(); + } // 调用ocr功能时先截图后,退出截图录屏,将刚截图的图片串递到ocr识别界面; if (func == "ocr") { { @@ -5187,13 +5299,40 @@ void MainWindow::paintEvent(QPaintEvent *event) painter.setBrush(QBrush()); // clear brush painter.setPen(framePen); - // 计算虚线绘制的实际区域 - // 修复:让虚线边界与实际选中区域完全一致 - // 移除不必要的调整,确保虚线显示的就是实际选中的区域 - - // 直接使用实际选中区域绘制虚线,确保一致性 - painter.drawRect(QRect(recordX, recordY, recordWidth, recordHeight)); + // QPainter::drawRect(QRect(...)) 在 1px 画笔下会把上/左边画在选区内部的第一个像素, + // 内容为白色时虚线会完全融进背景;下/右边则落在选区外,因此视觉上只剩两条边。 + // 默认将虚线框整体外移 1px 放到蒙层上;选区贴到屏幕边缘时不能再外移, + // 否则边缘像素会被窗口裁掉,因此按绘制区域夹取四个边界。 + const QRect windowRect = rect(); + const int frameLeft = qMax(recordX - 1, windowRect.left()); + const int frameTop = qMax(recordY - 1, windowRect.top()); + const int frameRight = qMin(recordX + recordWidth, windowRect.right()); + const int frameBottom = qMin(recordY + recordHeight, windowRect.bottom()); + // QPainter::drawRect(QRect) 的右/下边界按 x + width / y + height 落点, + // 因此这里使用 right-left / bottom-top,保证最后一行、最后一列不被裁掉。 + painter.drawRect(QRect(frameLeft, frameTop, + frameRight - frameLeft, + frameBottom - frameTop)); + painter.setRenderHint(QPainter::Antialiasing, true); + } + + // 圆角实时预览:把选中区域四个圆角外重新覆盖半透明蒙层,使其呈现圆角 + if (m_roundedCornerWidget && m_roundedCornerWidget->radius() > 0 + && status::shot == m_functionType && status::scrollshot != m_functionType) { + const qreal r = static_cast(m_roundedCornerWidget->radius()); + QPainterPath roundedPath; + roundedPath.addRoundedRect(QRectF(frameRect), r, r); + QPainterPath framePath; + framePath.addRect(QRectF(frameRect)); + const QPainterPath outside = framePath.subtracted(roundedPath); + painter.save(); painter.setRenderHint(QPainter::Antialiasing, true); + painter.setOpacity(0.2); + painter.setPen(Qt::NoPen); + painter.setBrush(QBrush("#000000")); + painter.setClipRegion(QRegion(backgroundRect)); + painter.drawPath(outside); + painter.restore(); } // Draw drag pint. @@ -5357,6 +5496,10 @@ int MainWindow::mousePressEF(QMouseEvent *mouseEvent, bool &needRepaint) dragRecordY = recordY; dragRecordWidth = recordWidth; dragRecordHeight = recordHeight; + // 记录锁定比例基准,供拖拽过程中等比缩放使用 + m_lockedRatio = (recordWidth > 0 && recordHeight > 0) + ? static_cast(recordWidth) / static_cast(recordHeight) + : 0.0; if (recordButtonStatus == RECORD_BUTTON_NORMAL) { // hideRecordButton(); @@ -5445,6 +5588,10 @@ int MainWindow::mouseReleaseEF(QMouseEvent *mouseEvent, bool &needRepaint) } if (recordX < 0) recordX = 0; if (recordY < 0) recordY = 0; + + // 图钉开启时,本次启动的第一次框选完成后再套用记忆值。 + // 放在释放阶段可以拿到最终选区,也能避免拖拽过程中反复纠正。 + applyRememberedSelectionSetting(); } // 尺寸调整完成后更新提示和工具栏位置 @@ -5617,6 +5764,9 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint) resizeRight(mouseEvent); } + // 锁定比例时,拖拽后按固定宽高比等比修正选区 + applyLockedRatioToSelection(); + needRepaint = true; } } @@ -5859,8 +6009,15 @@ int MainWindow::keyPressEF(QKeyEvent *keyEvent, bool &needRepaint) } if (m_shotStatus == ShotMouseStatus::Normal) { + // 锁定比例后禁用单边扩大/缩小快捷键(Ctrl/Shift+Ctrl + 方向键) + const bool ratioLocked = m_roundedCornerWidget && m_roundedCornerWidget->isRatioLocked(); + const bool resizeShortcut = keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Right + || keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down; // 是否按住 shift+ctrl if (keyEvent->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) { + if (ratioLocked && resizeShortcut) { + return 0; + } if (keyEvent->key() == Qt::Key_Left) { if (recordWidth > RECORD_MIN_SHOT_SIZE) { recordX = std::max(0, recordX + 1); @@ -5893,6 +6050,9 @@ int MainWindow::keyPressEF(QKeyEvent *keyEvent, bool &needRepaint) } // 是否只按住 ctrl else if ((qApp->keyboardModifiers() == Qt::ControlModifier)) { + if (ratioLocked && resizeShortcut) { + return 0; + } if (keyEvent->key() == Qt::Key_S) { // expressSaveScreenshot(); saveScreenShot(); @@ -7280,8 +7440,20 @@ void MainWindow::shotCurrentImg() m_resultPixmap = paintImage(); } - m_resultPixmap = ImageBorderHelper::instance()->getPixmapAddBorder(m_resultPixmap); - addCursorToImage(); + const int roundedRadius = m_roundedCornerWidget ? m_roundedCornerWidget->radius() : 0; + // 圆角属于截图成像效果:普通截图与「贴图」都需要带上圆角, + // OCR / 表格识别 / 滚动截图需要的是原始像素,不做圆角裁切。 + const bool roundableMode = (status::shot == m_functionType + || status::pinscreenshots == m_functionType); + if (roundedRadius > 0 && roundableMode) { + // 顺序:标注 -> 光标 -> 圆角裁切 -> 投影 + addCursorToImage(); + m_resultPixmap = RoundedCornerWidget::applyRoundedCorners(m_resultPixmap, roundedRadius); + m_resultPixmap = ImageBorderHelper::instance()->getPixmapAddBorder(m_resultPixmap); + } else { + m_resultPixmap = ImageBorderHelper::instance()->getPixmapAddBorder(m_resultPixmap); + addCursorToImage(); + } qCInfo(dsrApp) << __FUNCTION__ << __LINE__ << "已截取当前图片!"; #endif } @@ -7874,6 +8046,9 @@ void MainWindow::hideAllWidget() m_toolBar->hide(); m_sideBar->hide(); } + if (m_roundedCornerWidget) { + m_roundedCornerWidget->hide(); + } // m_toolBar->hide(); // m_sideBar->hide(); @@ -8040,6 +8215,151 @@ void MainWindow::reloadImage(QString effect, int radius) #endif } +void MainWindow::applyRememberedSelectionSetting() +{ + if (status::shot != m_functionType || !m_roundedCornerWidget + || !m_roundedCornerWidget->hasPendingRememberedSetting()) { + return; + } + + const QSize setting = m_roundedCornerWidget->rememberedSetting(); + if (setting.width() <= 0 || setting.height() <= 0) { + m_roundedCornerWidget->consumeRememberedSetting(); + return; + } + + const int minWidth = qMax(1, RECORD_MIN_SHOT_SIZE); + const int minHeight = qMax(1, RECORD_MIN_SHOT_SIZE); + const int maxWidth = qMax(minWidth, m_backgroundRect.width()); + const int maxHeight = qMax(minHeight, m_backgroundRect.height()); + + int newWidth = recordWidth; + int newHeight = recordHeight; + if (m_roundedCornerWidget->isRememberedSettingRatio()) { + const qreal ratio = static_cast(setting.width()) / static_cast(setting.height()); + // 选区宽高到最终截图还会减去 2/1 像素边框;这里先在最终输出像素上套用比例, + // 否则保存出来的图片比例会因边框补偿产生偏差。 + const int minOutputWidth = qMax(1, minWidth - 2); + const int minOutputHeight = qMax(1, minHeight - 1); + const int maxOutputWidth = qMax(minOutputWidth, maxWidth - 2); + const int maxOutputHeight = qMax(minOutputHeight, maxHeight - 1); + int outputWidth = qMax(minOutputWidth, recordWidth - 2); + int outputHeight = qMax(minOutputHeight, qRound(outputWidth / ratio)); + + if (outputHeight > maxOutputHeight) { + outputHeight = maxOutputHeight; + outputWidth = qMax(minOutputWidth, qRound(outputHeight * ratio)); + } + if (outputWidth > maxOutputWidth) { + outputWidth = maxOutputWidth; + outputHeight = qMax(minOutputHeight, qRound(outputWidth / ratio)); + } + + newWidth = outputWidth + 2; + newHeight = outputHeight + 1; + } else { + const qreal pixelRatio = qApp->primaryScreen()->devicePixelRatio() > 0 + ? qApp->primaryScreen()->devicePixelRatio() : 1.0; + newWidth = qRound(setting.width() / pixelRatio) + 2; + newHeight = qRound(setting.height() / pixelRatio) + 1; + newWidth = qBound(minWidth, newWidth, maxWidth); + newHeight = qBound(minHeight, newHeight, maxHeight); + } + + recordWidth = qBound(minWidth, newWidth, maxWidth); + recordHeight = qBound(minHeight, newHeight, maxHeight); + recordX = qBound(0, recordX, qMax(0, m_backgroundRect.width() - recordWidth)); + recordY = qBound(0, recordY, qMax(0, m_backgroundRect.height() - recordHeight)); + m_lockedRatio = static_cast(recordWidth) / static_cast(recordHeight); + m_roundedCornerWidget->consumeRememberedSetting(); + + qCDebug(dsrApp) << "apply remembered selection setting:" + << setting << "ratio:" << m_roundedCornerWidget->isRememberedSettingRatio() + << "selection:" << QRect(recordX, recordY, recordWidth, recordHeight); +} + +void MainWindow::onRoundedSelectionSizeChanged(int width, int height) +{ + if (status::shot != m_functionType) { + return; + } + const qreal pixelRatio = qApp->primaryScreen()->devicePixelRatio() > 0 + ? qApp->primaryScreen()->devicePixelRatio() : 1.0; + // 尺寸面板与实际截图裁剪保持一致:宽-2、高-1,并限制不超出屏幕 + const int maxWidth = qMax(RECORD_MIN_SHOT_SIZE, m_backgroundRect.width() - recordX - 2); + const int maxHeight = qMax(RECORD_MIN_SHOT_SIZE, m_backgroundRect.height() - recordY - 1); + const int newWidth = qBound(RECORD_MIN_SHOT_SIZE, qRound(width / pixelRatio) + 2, maxWidth); + const int newHeight = qBound(RECORD_MIN_SHOT_SIZE, qRound(height / pixelRatio) + 1, maxHeight); + if (newWidth == recordWidth && newHeight == recordHeight) { + return; + } + recordWidth = newWidth; + recordHeight = newHeight; + if (m_sizeTips) { + m_sizeTips->updateTips(QPoint(recordX, recordY), QSize(recordWidth, recordHeight)); + } + updateToolBarPos(); + if (m_sideBar && m_sideBar->isVisible()) { + updateSideBarPos(); + } + if (m_cameraWidget && m_cameraWidget->isVisible()) { + updateCameraWidgetPos(); + } + update(); +} + +void MainWindow::applyLockedRatioToSelection() +{ + if (status::shot != m_functionType || dragAction == ACTION_MOVE) { + return; + } + if (!m_roundedCornerWidget || !m_roundedCornerWidget->isRatioLocked() || m_lockedRatio <= 0.0) { + return; + } + + // 固定比例后,以变化幅度较大的一边作为驱动边,另一边按比例联动 + const qreal wScale = dragRecordWidth > 0 + ? static_cast(recordWidth) / static_cast(dragRecordWidth) : 1.0; + const qreal hScale = dragRecordHeight > 0 + ? static_cast(recordHeight) / static_cast(dragRecordHeight) : 1.0; + const bool widthDriven = qAbs(wScale - 1.0) >= qAbs(hScale - 1.0); + + const bool leftEdge = (dragAction == ACTION_RESIZE_LEFT + || dragAction == ACTION_RESIZE_TOP_LEFT + || dragAction == ACTION_RESIZE_BOTTOM_LEFT); + const bool topEdge = (dragAction == ACTION_RESIZE_TOP + || dragAction == ACTION_RESIZE_TOP_LEFT + || dragAction == ACTION_RESIZE_TOP_RIGHT); + + // 保持对边(单边拖拽)或对角(角点拖拽)固定 + const int anchorRight = dragRecordX + dragRecordWidth; + const int anchorBottom = dragRecordY + dragRecordHeight; + const int maxWidth = leftEdge ? anchorRight : (m_backgroundRect.width() - dragRecordX); + const int maxHeight = topEdge ? anchorBottom : (m_backgroundRect.height() - dragRecordY); + + int newWidth = qMax(RECORD_MIN_SHOT_SIZE, recordWidth); + int newHeight = qMax(RECORD_MIN_SHOT_SIZE, recordHeight); + if (widthDriven) { + newHeight = qMax(RECORD_MIN_SHOT_SIZE, qRound(newWidth / m_lockedRatio)); + } else { + newWidth = qMax(RECORD_MIN_SHOT_SIZE, qRound(newHeight * m_lockedRatio)); + } + + // 限制在屏幕可用范围内,受限时按比例回退 + newWidth = qMin(newWidth, qMax(RECORD_MIN_SHOT_SIZE, maxWidth)); + newHeight = qMax(RECORD_MIN_SHOT_SIZE, qRound(newWidth / m_lockedRatio)); + if (newHeight > qMax(RECORD_MIN_SHOT_SIZE, maxHeight)) { + newHeight = qMax(RECORD_MIN_SHOT_SIZE, maxHeight); + newWidth = qMax(RECORD_MIN_SHOT_SIZE, qRound(newHeight * m_lockedRatio)); + newWidth = qMin(newWidth, qMax(RECORD_MIN_SHOT_SIZE, maxWidth)); + } + + recordWidth = newWidth; + recordHeight = newHeight; + recordX = qMax(0, leftEdge ? (anchorRight - newWidth) : dragRecordX); + recordY = qMax(0, topEdge ? (anchorBottom - newHeight) : dragRecordY); +} + void MainWindow::shotImgWidthEffect() { #ifndef ENABLE_UNIT_TEST diff --git a/src/main_window.h b/src/main_window.h index 0e7170b0d..fa0ebf088 100755 --- a/src/main_window.h +++ b/src/main_window.h @@ -19,8 +19,13 @@ #include "widgets/shapeswidget.h" #include "widgets/toptips.h" #include + +QT_BEGIN_NAMESPACE +class QShortcut; +QT_END_NAMESPACE #include "widgets/toolbar.h" #include "widgets/sidebar.h" +#include "widgets/roundedcornerwidget.h" #include "widgets/keybuttonwidget.h" #include "widgets/zoomIndicator.h" #include "widgets/camerawidget.h" @@ -1283,6 +1288,33 @@ public slots: int m_screenWidth = 0; //屏幕宽度 int m_screenHeight = 0; //屏幕高度 SideBar *m_sideBar = nullptr; //截图功能侧边栏功能 + RoundedCornerWidget *m_roundedCornerWidget = nullptr; //选区右上角圆角/尺寸比例工具组 + qreal m_lockedRatio = 0.0; //拖拽开始时记录的选区宽高比(锁定比例用) + int m_radiusPreviewRadius = 0; //上一次圆角预览半径,用于计算需要重绘的四角区域 + // 保存截图的回车快捷方式:焦点在圆角/尺寸比例面板的输入框时要临时禁用, + // 否则 QShortcut 会先于输入框吃掉回车,导致“输入尺寸回车直接结束截图”。 + QShortcut *m_returnSaveShortcut = nullptr; + QShortcut *m_enterSaveShortcut = nullptr; + + /** + * @brief 当前焦点是否位于圆角/尺寸比例面板的文本输入框内 + */ + bool isRoundedPanelEditorFocused() const; + + /** + * @brief 圆角工具组尺寸/比例面板修改了选区尺寸 + */ + void onRoundedSelectionSizeChanged(int width, int height); + + /** + * @brief 应用上次勾选“下次截图保持”的尺寸或比例 + */ + void applyRememberedSelectionSetting(); + + /** + * @brief 锁定比例后,拖拽选区时按固定宽高比等比调整 + */ + void applyLockedRatioToSelection(); /** * @brief 在一级工具栏按下鼠标左键的时,此时记录下二级菜单的位置 */ diff --git a/src/src.pro b/src/src.pro index 97f55a9d6..489247d02 100755 --- a/src/src.pro +++ b/src/src.pro @@ -203,6 +203,7 @@ HEADERS += main_window.h \ widgets/keybuttonwidget.h \ widgets/sidebar.h \ widgets/shottoolwidget.h \ + widgets/roundedcornerwidget.h \ widgets/colortoolwidget.h \ widgets/shapetoolwidget.h \ widgets/savebutton.h \ @@ -298,6 +299,7 @@ SOURCES += main.cpp \ widgets/keybuttonwidget.cpp \ widgets/sidebar.cpp \ widgets/shottoolwidget.cpp \ + widgets/roundedcornerwidget.cpp \ widgets/colortoolwidget.cpp \ widgets/shapetoolwidget.cpp \ widgets/savebutton.cpp \ diff --git a/src/utils/configsettings.cpp b/src/utils/configsettings.cpp index 167cd7e0e..9b38d2747 100755 --- a/src/utils/configsettings.cpp +++ b/src/utils/configsettings.cpp @@ -36,6 +36,9 @@ const QMap> ConfigSettings::m_defaultConfig = { {"text", {{"color_index", 3}, {"fontsize", 0}}}, // 序号标注工具,颜色与线宽(线宽对应序号圆圈大小档位) {"sequence", {{"color_index", 3}, {"line_width", 1}}}, + // 圆角截图:开启状态由 radius>0 表示;ratio_type 表示尺寸/比例下拉模式, + // remember 表示下次启动是否应用 fixed_width/fixed_height。 + {"rounded", {{"radius", 0}, {"ratio_type", 0}, {"remember", false}, {"remember_ratio", false}, {"fixed_width", 0}, {"fixed_height", 0}}}, // 几何图形类型,矩形或椭圆 {"shape", {{"current", "rectangle"}}}, // 截图保存选项,格式,保存位置选项 diff --git a/src/widgets/roundedcornerwidget.cpp b/src/widgets/roundedcornerwidget.cpp new file mode 100644 index 000000000..f68cfb80d --- /dev/null +++ b/src/widgets/roundedcornerwidget.cpp @@ -0,0 +1,1027 @@ +// Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "roundedcornerwidget.h" +#include "toolbutton.h" +#include "../utils/configsettings.h" +#include "../utils.h" +#include "../utils/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +namespace { + +// 设计稿「截图录屏-工具栏-圆角 / 比例」尺寸(24x24 图标按 32px 资源提供,控件按 1x 逻辑像素实现) +const int GROUP_WIDTH = 56; +const int GROUP_HEIGHT = 103; +const int BUTTON_SIZE = 36; +const int BUTTON_ICON_SIZE = 24; +const int BUTTON_MARGIN_X = 10; +const int FIRST_BUTTON_Y = 11; +const int SECOND_BUTTON_Y = 57; + +const int PANEL_WIDTH = 270; +const int PANEL_HEIGHT = 56; +const int PANEL_RADIUS = 12; +const int PANEL_PADDING = 10; + +const int SLIDER_X = 36; +const int SLIDER_Y = 16; +const int SLIDER_WIDTH = 152; +const int SLIDER_HEIGHT = 24; +const int SPIN_WIDTH = 62; +const int SPIN_HEIGHT = 36; +const int ADJUST_ICON_SIZE = 16; + +const int SIZE_MODE_X = 10; +const int SIZE_MODE_Y = 16; +const int SIZE_MODE_WIDTH = 55; +const int SIZE_MODE_HEIGHT = 24; +const int WIDTH_SPIN_X = 74; +const int HEIGHT_SPIN_X = 154; +const int DIMENSION_SPIN_WIDTH = 60; +const int DIMENSION_SEPARATOR_X = 136; +const int DIMENSION_SEPARATOR_Y = 19; +const int DIMENSION_SEPARATOR_SIZE = 16; +const int LOCK_BUTTON_X = 224; + +const int RADIUS_MAX = 60; +const int MAX_DIMENSION_VALUE = 99999; +const int MAX_RATIO_VALUE = 999; +// 按钮组与选区、按钮组与属性面板的间距 +const int SELECTION_GAP = 16; +const int PANEL_GAP = 10; + +// DTK 默认 DSlider 的滑块是蓝色胶囊;设计稿要求 16x16、圆角 4px 的深色方形滑块。 +// 轨道仍由当前 DTK 样式绘制,只替换 handle,避免重新实现整套 slider 行为。 +class RoundedSliderStyle : public QProxyStyle +{ +public: + explicit RoundedSliderStyle(QObject *parent) + // 这里必须传 nullptr 基样式,不能传控件当前样式: + // QProxyStyle 会把基样式收成自己的子对象,而 QSlider 默认拿到的是 + // QApplication 的应用样式;一旦把应用样式传进来,slider 析构时就会 + // 连带销毁应用样式,QApplicationPrivate::app_style 变成悬空指针, + // 退出时 ~QApplication 再次释放它就会崩溃。 + // 传 nullptr 时 QProxyStyle::baseStyle() 会在绘制时动态取回应用样式。 + : QProxyStyle(nullptr) + { + setParent(parent); + } + + void drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, + QPainter *painter, const QWidget *widget) const override + { + if (control != QStyle::CC_Slider || !option || !painter) { + QProxyStyle::drawComplexControl(control, option, painter, widget); + return; + } + + const auto *sliderOption = qstyleoption_cast(option); + if (!sliderOption) { + QProxyStyle::drawComplexControl(control, option, painter, widget); + return; + } + + const bool darkTheme = DGuiApplicationHelper::instance()->themeType() + == DGuiApplicationHelper::DarkType; + constexpr qreal HANDLE_SIZE = 16.0; + constexpr qreal HANDLE_RADIUS = 4.0; + + // 轨道范围用 groove(也就是可滑动区间),不要用 defaultHandle 的圆心定位: + // DTK 原生手柄约 28px,设计稿手柄是 16px,两者圆心相同但宽度不同, + // 直接套用圆心会把 16px 手柄整体往右顶 6px,最小值时轨道会在手柄左边露出一截。 + QRectF trackRect(QProxyStyle::subControlRect( + control, sliderOption, QStyle::SC_SliderGroove, widget)); + if (!trackRect.isValid()) { + trackRect = QRectF(sliderOption->rect); + } + if (trackRect.width() < HANDLE_SIZE) { + trackRect = QRectF(sliderOption->rect); + } + + const int range = sliderOption->maximum - sliderOption->minimum; + const qreal fraction = range > 0 + ? qreal(sliderOption->sliderPosition - sliderOption->minimum) / range + : 0.0; + // 最小值时手柄左边缘贴住轨道左端,最大值时右边缘贴住轨道右端, + // 这样手柄两端各占 16px,中间正好留出设计稿的 120px 虚线区间。 + QRectF handleRect(0, 0, HANDLE_SIZE, HANDLE_SIZE); + handleRect.moveTopLeft(QPointF(trackRect.left() + fraction * (trackRect.width() - HANDLE_SIZE), + trackRect.center().y() - HANDLE_SIZE / 2.0)); + + const qreal trackY = trackRect.center().y(); + const QColor trackColor = darkTheme ? QColor(255, 255, 255, 51) + : QColor(0, 0, 0, 51); + const QColor progressColor = darkTheme ? QColor(2, 76, 202) + : QColor(0, 91, 255); + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, false); + + QPen trackPen(trackColor, 2.0, Qt::CustomDashLine, Qt::FlatCap, Qt::MiterJoin); + trackPen.setDashPattern(QVector{1.0, 0.5}); + painter->setPen(trackPen); + painter->drawLine(QPointF(trackRect.left(), trackY), + QPointF(trackRect.right(), trackY)); + + if (sliderOption->sliderPosition > sliderOption->minimum) { + if (darkTheme) { + QPen progressShadowPen(QColor(0, 40, 128, 128), 2.0, + Qt::CustomDashLine, Qt::FlatCap, Qt::MiterJoin); + progressShadowPen.setDashPattern(QVector{1.0, 0.5}); + painter->setPen(progressShadowPen); + painter->drawLine(QPointF(trackRect.left(), trackY + 2.0), + QPointF(handleRect.center().x(), trackY + 2.0)); + } + + QPen progressPen(progressColor, 2.0, Qt::CustomDashLine, + Qt::FlatCap, Qt::MiterJoin); + progressPen.setDashPattern(QVector{1.0, 0.5}); + painter->setPen(progressPen); + painter->drawLine(QPointF(trackRect.left(), trackY), + QPointF(handleRect.center().x(), trackY)); + } + + painter->setRenderHint(QPainter::Antialiasing, true); + if (darkTheme) { + DDrawUtils::drawShadow(painter, handleRect.toRect(), HANDLE_RADIUS, HANDLE_RADIUS, + QColor(31, 31, 31, 102), 6.0, QPoint(0, 4)); + + QLinearGradient gradient(handleRect.topLeft(), handleRect.bottomLeft()); + gradient.setColorAt(0, QColor(83, 83, 83)); + gradient.setColorAt(1, QColor(46, 46, 46)); + painter->setPen(Qt::NoPen); + painter->setBrush(gradient); + painter->drawRoundedRect(handleRect.adjusted(0.5, 0.5, -0.5, -0.5), + HANDLE_RADIUS, HANDLE_RADIUS); + + painter->setPen(QPen(QColor(0, 0, 0, 51), 1.0)); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(handleRect.adjusted(0.5, 0.5, -0.5, -0.5), + HANDLE_RADIUS, HANDLE_RADIUS); + painter->setPen(QPen(QColor(255, 255, 255, 26), 1.0)); + painter->drawRoundedRect(handleRect.adjusted(1.5, 1.5, -1.5, -1.5), + HANDLE_RADIUS - 1.0, HANDLE_RADIUS - 1.0); + } else { + DDrawUtils::drawShadow(painter, handleRect.toRect(), HANDLE_RADIUS, HANDLE_RADIUS, + QColor(0, 0, 0, 77), 4.0, QPoint(0, 2)); + painter->setPen(Qt::NoPen); + painter->setBrush(QColor(77, 77, 77)); + painter->drawRoundedRect(handleRect.adjusted(0.5, 0.5, -0.5, -0.5), + HANDLE_RADIUS, HANDLE_RADIUS); + } + painter->restore(); + } +}; + +// 把单色图标整体着色成指定颜色(保留原始 alpha 形状)。 +// 图标资源本身固定是 #000000,DTK 对普通 QIcon 不会按 palette 重新着色, +// 所以选中态(蓝底白图标)需要在这里显式着色。 +QIcon tintedIcon(const QIcon &icon, const QSize &iconSize, const QColor &color) +{ + if (icon.isNull()) { + return icon; + } + const qreal dpr = qApp->devicePixelRatio(); + const QPixmap base = icon.pixmap(iconSize, dpr); + if (base.isNull()) { + return icon; + } + QPixmap tinted(base.size()); + tinted.fill(Qt::transparent); + tinted.setDevicePixelRatio(dpr); + QPainter painter(&tinted); + painter.drawPixmap(0, 0, base); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(tinted.rect(), color); + painter.end(); + return QIcon(tinted); +} + +// 设计稿「截图录屏-工具栏-比例2(激活)」中的图钉按钮: +// 36x36、圆角 8;normal 黑色 10% 背景 + 深色图标、hover 20% 背景、 +// press 更深背景 + 主题高亮色图标、active(选中)主题高亮色背景 + 白色图标。 +// 按钮仍继承 DTK DIconButton,只重写背景和图标着色,保证交互行为复用 DTK。 +class SelectionLockButton : public DIconButton +{ +public: + explicit SelectionLockButton(QWidget *parent = nullptr) + : DIconButton(parent) + { + setCheckable(true); + setFlat(true); + } + +protected: + void initStyleOption(DStyleOptionButton *option) const override + { + DIconButton::initStyleOption(option); + // 选中态配色由本类自己绘制,不再交给 DTK 的蓝色选中态处理,避免与 + // 自绘背景叠加出两层高亮。 + option->state &= ~QStyle::State_On; + } + + void paintEvent(QPaintEvent *event) override + { + Q_UNUSED(event); + const bool darkTheme = DGuiApplicationHelper::instance()->themeType() + == DGuiApplicationHelper::DarkType; + const bool active = isChecked(); + + // 设计稿:normal #000000 10%、hover #000000 20%,press 再深一档; + // 深色主题下换成同比例的白色蒙层,保证在深色底板上依然可辨。 + QColor background; + if (active) { + background = palette().highlight().color(); + } else if (isDown()) { + background = darkTheme ? QColor(255, 255, 255, 51) : QColor(0, 0, 0, 64); + } else if (underMouse()) { + background = darkTheme ? QColor(255, 255, 255, 38) : QColor(0, 0, 0, 51); + } else { + background = darkTheme ? QColor(255, 255, 255, 26) : QColor(0, 0, 0, 26); + } + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + painter.setBrush(background); + painter.drawRoundedRect(rect(), 8, 8); + painter.end(); + + DStylePainter stylePainter(this); + DStyleOptionButton option; + initStyleOption(&option); + + QColor iconColor; + if (active) { + iconColor = option.palette.highlightedText().color(); + } else if (isDown()) { + iconColor = option.palette.highlight().color(); + } else { + iconColor = darkTheme ? QColor(255, 255, 255) : QColor(0, 0, 0); + } + const QSize iconSize = option.iconSize.isValid() ? option.iconSize + : QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE); + option.icon = tintedIcon(option.icon, iconSize, iconColor); + + stylePainter.drawControl(DStyle::CE_IconButton, option); + } +}; + +QColor blurMaskColor() +{ + // 与主工具栏保持一致的模糊背景:浅色 #F7F7F7 60%,深色 #000000 30% + if (Utils::themeType == 1) { + return QColor(0xF7, 0xF7, 0xF7, 153); + } + return QColor(0, 0, 0, 76); +} + +// DFloatingWidget 会在内容四周预留阴影留白(常规档位为 6px), +// 设计稿给出的 56x103 / 270x56 指的是可见底板,控件尺寸要再加上两侧留白。 +int plateShadowMargin(const QWidget *widget) +{ + return DStyle::pixelMetric(widget->style(), DStyle::PM_FloatingWidgetShadowMargins, nullptr, widget); +} + +QWidget *createFloatingPlate(QWidget *parent, const QSize &plateSize, int radius, int margin) +{ + // 与主工具栏(ToolBarWidget/DFloatingWidget)保持同一套悬浮样式: + // 高斯模糊背景 + DTK 悬浮阴影(设计稿:偏移 0/4、模糊 8、10% 黑) + DFloatingWidget *plate = new DFloatingWidget(parent); + plate->setBlurBackgroundEnabled(true); + plate->blurBackground()->setRadius(30); + plate->blurBackground()->setMode(DBlurEffectWidget::GaussianBlur); + plate->blurBackground()->setBlurEnabled(true); + plate->blurBackground()->setBlendMode(DBlurEffectWidget::InWidgetBlend); + plate->blurBackground()->setMaskColor(blurMaskColor()); + plate->setFramRadius(radius); + plate->setFixedSize(plateSize.width() + margin * 2, plateSize.height() + margin * 2); + return plate; +} + +// 设计稿 DTK/输入框/Spinbox2:输入框底色为 #FFFFFF 60%。 +// DTK/chameleon 的 embed 样式是按 QPalette::Button 取这个底色的, +// 默认浅色主题下该角色与悬浮面板底色几乎一致(实测 228 vs 229), +// 结果设计稿里那个圆角输入框在界面上"消失"了。这里按设计稿显式指定底色。 +void applySpinBoxPlateColor(DSpinBox *spin) +{ + QPalette pal = spin->palette(); + pal.setColor(QPalette::Button, QColor(255, 255, 255, 153)); + spin->setPalette(pal); +} + +void applyLineEditPlateColor(DLineEdit *edit) +{ + if (!edit) { + return; + } + + const QColor plateColor(255, 255, 255, 153); + QPalette pal = edit->palette(); + pal.setColor(QPalette::Base, plateColor); + pal.setColor(QPalette::Button, plateColor); + edit->setPalette(pal); + + if (QLineEdit *lineEdit = edit->lineEdit()) { + QPalette lineEditPal = lineEdit->palette(); + lineEditPal.setColor(QPalette::Base, plateColor); + lineEditPal.setColor(QPalette::Button, plateColor); + lineEdit->setPalette(lineEditPal); + } +} + +int greatestCommonDivisor(int a, int b) +{ + a = qAbs(a); + b = qAbs(b); + while (b != 0) { + const int remainder = a % b; + a = b; + b = remainder; + } + return qMax(1, a); +} + +QPair simplifiedRatio(int width, int height) +{ + const int divisor = greatestCommonDivisor(width, height); + return {qMax(1, width / divisor), qMax(1, height / divisor)}; +} + +DLabel *createIconLabel(QWidget *parent, const QString &iconName, const QString &objectName, + const QSize &size, const QSize &iconSize) +{ + DLabel *label = new DLabel(parent); + label->setObjectName(objectName); + label->setAccessibleName(objectName); + label->setFixedSize(size); + label->setAlignment(Qt::AlignCenter); + label->setPixmap(QIcon::fromTheme(iconName).pixmap(iconSize)); + return label; +} + +} + +RoundedCornerWidget::RoundedCornerWidget(QWidget *parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_NoSystemBackground); + const int margin = plateShadowMargin(this); + setFixedSize(GROUP_WIDTH + margin * 2, GROUP_HEIGHT + margin * 2); + initUi(); + syncFromConfig(); +} + +RoundedCornerWidget::~RoundedCornerWidget() +{ + flushPendingPersist(); +} + +void RoundedCornerWidget::initUi() +{ + m_groupFrame = buildGroup(); + m_cornerPanel = buildCornerPanel(); + m_sizePanel = buildSizePanel(); + + m_cornerPanel->hide(); + m_sizePanel->hide(); + + // 圆角按钮:单击只展开/收起属性面板并高亮按钮。 + // 圆角半径默认保持 0(设计稿:0px~60px,默认 0),需要用户拖动滑块或输入数值才真正生效。 + connect(m_cornerButton, &ToolButton::clicked, this, [this]() { + setPanel(m_panel == CornerPanel ? NoPanel : CornerPanel); + syncButtonStates(); + }); + + // 尺寸/比例按钮:展开/收起尺寸比例面板 + connect(m_ratioButton, &ToolButton::clicked, this, [this]() { + setPanel(m_panel == SizePanel ? NoPanel : SizePanel); + syncButtonStates(); + }); + + // 拖动滑块时每一步都写配置会造成明显卡顿:QSettings::sync() 在 /home 上单次实测 + // 可达 150ms 以上。这里改为防抖落盘(松手/输入完成/控件隐藏时立即写入)。 + m_persistTimer = new QTimer(this); + m_persistTimer->setSingleShot(true); + m_persistTimer->setInterval(300); + connect(m_persistTimer, &QTimer::timeout, this, &RoundedCornerWidget::persistRadius); + + connect(m_radiusSlider, &DSlider::valueChanged, this, [this](int value) { + if (m_radiusSpin->value() != value) { + m_radiusSpin->setValue(value); + } + if (m_radius == value) { + return; + } + m_radius = value; + m_persistTimer->start(); + emit radiusChanged(m_radius); + }); + // 松开滑块 / 输入框结束编辑:立刻落盘,保证拖完就记住 + connect(m_radiusSlider, &DSlider::sliderReleased, this, [this]() { + flushPendingPersist(); + }); + connect(m_radiusSpin, QOverload::of(&DSpinBox::valueChanged), this, [this](int value) { + if (m_radiusSlider->value() != value) { + m_radiusSlider->setValue(value); + } + }); + connect(m_radiusSpin, &DSpinBox::editingFinished, this, [this]() { + flushPendingPersist(); + }); + + // 面板输入框里回车代表“确认当前数值”:确认后主动移出焦点, + // 这样紧接着再按一次回车才会轮到主窗口的“保存截图”快捷方式。 + const QList panelEditors = { + m_radiusSpin->lineEdit(), + m_widthEdit->lineEdit(), + m_heightEdit->lineEdit(), + }; + for (QLineEdit *editor : panelEditors) { + if (!editor) { + continue; + } + connect(editor, &QLineEdit::returnPressed, this, [editor]() { + editor->clearFocus(); + }); + } + + // 宽/高输入框用 DLineEdit,整段编辑完成(回车或失焦)后再提交,避免输入中途反复改选区。 + connect(m_widthEdit, &DLineEdit::editingFinished, this, [this]() { + if (m_syncingSelectionSize) { + return; + } + + const int maxValue = m_ratioLocked ? MAX_RATIO_VALUE : MAX_DIMENSION_VALUE; + const int widthValue = qBound(1, m_widthEdit->text().toInt(), maxValue); + const int heightValue = qBound(1, m_heightEdit->text().toInt(), maxValue); + if (m_ratioLocked) { + const int anchorHeight = qMax(1, m_selectionHeight); + m_selectionWidth = qMax(1, qRound(anchorHeight * static_cast(widthValue) / heightValue)); + m_selectionHeight = anchorHeight; + } else { + m_selectionWidth = widthValue; + m_selectionHeight = heightValue; + } + updateDimensionFields(); + persistFixedSize(); + emit selectionSizeChanged(m_selectionWidth, m_selectionHeight); + }); + connect(m_heightEdit, &DLineEdit::editingFinished, this, [this]() { + if (m_syncingSelectionSize) { + return; + } + + const int maxValue = m_ratioLocked ? MAX_RATIO_VALUE : MAX_DIMENSION_VALUE; + const int widthValue = qBound(1, m_widthEdit->text().toInt(), maxValue); + const int heightValue = qBound(1, m_heightEdit->text().toInt(), maxValue); + if (m_ratioLocked) { + const int anchorWidth = qMax(1, m_selectionWidth); + m_selectionHeight = qMax(1, qRound(anchorWidth * static_cast(heightValue) / widthValue)); + m_selectionWidth = anchorWidth; + } else { + m_selectionWidth = widthValue; + m_selectionHeight = heightValue; + } + updateDimensionFields(); + persistFixedSize(); + emit selectionSizeChanged(m_selectionWidth, m_selectionHeight); + }); + + connect(m_lockButton, &DIconButton::clicked, this, [this]() { + m_rememberSetting = m_lockButton->isChecked(); + if (m_rememberSetting) { + // 图钉只负责“下次启动继续使用”,当前这次框选仍由用户自己控制。 + // 勾选时把面板当前值固化为记忆值,避免只有图钉状态而没有参数。 + captureCurrentSetting(); + } + m_pendingRememberedSetting = false; + persistRememberSetting(); + persistFixedSize(); + syncButtonStates(); + }); +} + +QWidget *RoundedCornerWidget::buildGroup() +{ + const int margin = plateShadowMargin(this); + QWidget *group = createFloatingPlate(this, QSize(GROUP_WIDTH, GROUP_HEIGHT), PANEL_RADIUS, margin); + + m_cornerButton = new ToolButton(group); + m_cornerButton->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + m_cornerButton->setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE)); + m_cornerButton->setCheckable(true); + m_cornerButton->setIcon(QIcon::fromTheme("corner-radius")); + m_cornerButton->setObjectName("RoundedCornerButton"); + m_cornerButton->setAccessibleName("RoundedCornerButton"); + m_cornerButton->setToolTip(tr("Radius")); + m_cornerButton->move(margin + BUTTON_MARGIN_X, margin + FIRST_BUTTON_Y); + + m_ratioButton = new ToolButton(group); + m_ratioButton->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + m_ratioButton->setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE)); + m_ratioButton->setCheckable(true); + m_ratioButton->setIcon(QIcon::fromTheme("size-ratio")); + m_ratioButton->setObjectName("SizeRatioButton"); + m_ratioButton->setAccessibleName("SizeRatioButton"); + m_ratioButton->setToolTip(tr("Size / ratio")); + m_ratioButton->move(margin + BUTTON_MARGIN_X, margin + SECOND_BUTTON_Y); + + return group; +} + +QWidget *RoundedCornerWidget::buildCornerPanel() +{ + const int margin = plateShadowMargin(this); + QWidget *panel = createFloatingPlate(this, QSize(PANEL_WIDTH, PANEL_HEIGHT), PANEL_RADIUS, margin); + + DLabel *leading = createIconLabel(panel, "corner-adjust", "CornerAdjustIcon", + QSize(ADJUST_ICON_SIZE, ADJUST_ICON_SIZE), + QSize(ADJUST_ICON_SIZE, ADJUST_ICON_SIZE)); + leading->move(margin + PANEL_PADDING, margin + (PANEL_HEIGHT - ADJUST_ICON_SIZE) / 2); + + m_radiusSlider = new DSlider(Qt::Horizontal, panel); + m_radiusSlider->setMinimum(0); + m_radiusSlider->setMaximum(RADIUS_MAX); + m_radiusSlider->setFixedSize(SLIDER_WIDTH, SLIDER_HEIGHT); + m_radiusSlider->setObjectName("CornerRadiusSlider"); + m_radiusSlider->setAccessibleName("CornerRadiusSlider"); + m_radiusSlider->move(margin + SLIDER_X, margin + SLIDER_Y); + QSlider *radiusSlider = m_radiusSlider->slider(); + radiusSlider->setStyle(new RoundedSliderStyle(radiusSlider)); + connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::themeTypeChanged, + radiusSlider, qOverload<>(&QWidget::update)); + + m_radiusSpin = new DSpinBox(panel); + m_radiusSpin->setEnabledEmbedStyle(true); + applySpinBoxPlateColor(m_radiusSpin); + m_radiusSpin->setRange(0, RADIUS_MAX); + m_radiusSpin->setFixedSize(SPIN_WIDTH, SPIN_HEIGHT); + m_radiusSpin->setObjectName("CornerRadiusSpinBox"); + m_radiusSpin->setAccessibleName("CornerRadiusSpinBox"); + m_radiusSpin->move(margin + PANEL_WIDTH - PANEL_PADDING - SPIN_WIDTH, + margin + (PANEL_HEIGHT - SPIN_HEIGHT) / 2); + + return panel; +} + +QWidget *RoundedCornerWidget::buildSizePanel() +{ + const int margin = plateShadowMargin(this); + QWidget *panel = createFloatingPlate(this, QSize(PANEL_WIDTH, PANEL_HEIGHT), PANEL_RADIUS, margin); + + // 设计稿「尺寸 ▾」:55x24,前面是文字,后面是下拉箭头。 + // 用 DTK 的 DPushButton(QPushButton 派生)挂 DMenu,按钮自带“文本 + 下拉箭头”, + // 不去自绘箭头,也不依赖 ComboBox 的额外内边距。 + m_sizeModeButton = new DPushButton(panel); + m_sizeModeButton->setFlat(true); + // Sketch 里整块下拉区可见宽度是 55;DPushButton 自带内边距, + // 固定 55 会把“尺寸”文字裁掉,这里放宽到贴住右侧输入框的 64。 + m_sizeModeButton->setFixedSize(64, SIZE_MODE_HEIGHT); + m_sizeModeButton->setObjectName("SizeRatioModeButton"); + m_sizeModeButton->setAccessibleName("SizeRatioModeButton"); + m_sizeModeButton->setToolTip(tr("Size / ratio")); + m_sizeModeButton->setText(tr("Size")); + DFontSizeManager::instance()->bind(m_sizeModeButton, DFontSizeManager::T6); + + DMenu *modeMenu = new DMenu(m_sizeModeButton); + QAction *sizeAction = modeMenu->addAction(tr("Size")); + QAction *ratioAction = modeMenu->addAction(tr("Ratio")); + sizeAction->setCheckable(true); + ratioAction->setCheckable(true); + sizeAction->setChecked(true); + QActionGroup *modeGroup = new QActionGroup(modeMenu); + modeGroup->setExclusive(true); + modeGroup->addAction(sizeAction); + modeGroup->addAction(ratioAction); + connect(modeGroup, &QActionGroup::triggered, this, [this, ratioAction](QAction *action) { + if (!action || !m_sizeModeButton) { + return; + } + const bool locked = (action == ratioAction); + m_sizeModeButton->setText(action->text()); + if (m_ratioLocked != locked) { + m_ratioLocked = locked; + persistRatioType(); + // 图钉已经选中时,前面切换“尺寸/比例”也属于修改记忆设置, + // 立即同步 remember_ratio,保证图钉、下拉选项和实际参数一致。 + if (m_rememberSetting) { + captureCurrentSetting(); + } + } + updateDimensionFields(); + syncButtonStates(); + }); + m_sizeModeButton->setMenu(modeMenu); + m_sizeModeButton->move(margin + SIZE_MODE_X, margin + SIZE_MODE_Y); + + // 宽/高输入框:设计稿 60x36 的 DTK 输入框,无上下调节按钮,数字居中。 + m_widthEdit = new DLineEdit(panel); + applyLineEditPlateColor(m_widthEdit); + m_widthEdit->setFixedSize(DIMENSION_SPIN_WIDTH, SPIN_HEIGHT); + m_widthEdit->setClearButtonEnabled(false); + m_widthEdit->setObjectName("SelectionWidthEdit"); + m_widthEdit->setAccessibleName("SelectionWidthEdit"); + m_widthEdit->lineEdit()->setAlignment(Qt::AlignCenter); + m_widthValidator = new QIntValidator(1, MAX_DIMENSION_VALUE, m_widthEdit); + m_widthEdit->lineEdit()->setValidator(m_widthValidator); + DFontSizeManager::instance()->bind(m_widthEdit, DFontSizeManager::T6); + m_widthEdit->move(margin + WIDTH_SPIN_X, margin + (PANEL_HEIGHT - SPIN_HEIGHT) / 2); + + DLabel *separator = new DLabel(QString(QChar(0x00D7)), panel); + separator->setFixedSize(DIMENSION_SEPARATOR_SIZE, DIMENSION_SEPARATOR_SIZE); + separator->setAlignment(Qt::AlignCenter); + separator->setObjectName("SelectionSizeSeparator"); + separator->setAccessibleName("SelectionSizeSeparator"); + DFontSizeManager::instance()->bind(separator, DFontSizeManager::T6); + separator->move(margin + DIMENSION_SEPARATOR_X, margin + DIMENSION_SEPARATOR_Y); + + m_heightEdit = new DLineEdit(panel); + applyLineEditPlateColor(m_heightEdit); + m_heightEdit->setFixedSize(DIMENSION_SPIN_WIDTH, SPIN_HEIGHT); + m_heightEdit->setClearButtonEnabled(false); + m_heightEdit->setObjectName("SelectionHeightEdit"); + m_heightEdit->setAccessibleName("SelectionHeightEdit"); + m_heightEdit->lineEdit()->setAlignment(Qt::AlignCenter); + m_heightValidator = new QIntValidator(1, MAX_DIMENSION_VALUE, m_heightEdit); + m_heightEdit->lineEdit()->setValidator(m_heightValidator); + DFontSizeManager::instance()->bind(m_heightEdit, DFontSizeManager::T6); + m_heightEdit->move(margin + HEIGHT_SPIN_X, margin + (PANEL_HEIGHT - SPIN_HEIGHT) / 2); + + m_lockButton = new SelectionLockButton(panel); + m_lockButton->setFixedSize(BUTTON_SIZE, BUTTON_SIZE); + m_lockButton->setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE)); + m_lockButton->setIcon(QIcon::fromTheme("selection-fixed")); + m_lockButton->setObjectName("RatioLockButton"); + m_lockButton->setAccessibleName("RatioLockButton"); + m_lockButton->setToolTip(tr("Keep the selected size or ratio for the next screenshot")); + DStyle::setFocusRectVisible(m_lockButton, false); + m_lockButton->move(margin + LOCK_BUTTON_X, margin + (PANEL_HEIGHT - BUTTON_SIZE) / 2); + + return panel; +} + +void RoundedCornerWidget::syncFromConfig() +{ + ConfigSettings *config = ConfigSettings::instance(); + m_radius = config->getValue("rounded", "radius").toInt(); + m_radius = qBound(0, m_radius, RADIUS_MAX); + m_persistedRadius = m_radius; + m_radiusSlider->setValue(m_radius); + m_radiusSpin->setValue(m_radius); + + // ratio_type 只表示面板当前下拉选择:0 尺寸、1 比例;图钉单独用 remember 保存。 + // 此前两者共用 ratio_type,导致仅切换“比例”也会在下次启动被当成已勾选图钉。 + const bool dropdownRatio = config->getValue("rounded", "ratio_type").toInt() > 0; + m_ratioLocked = dropdownRatio; + m_rememberSetting = config->getValue("rounded", "remember").toBool(); + m_rememberedIsRatio = config->getValue("rounded", "remember_ratio").toBool(); + m_fixedWidth = qMax(0, config->getValue("rounded", "fixed_width").toInt()); + m_fixedHeight = qMax(0, config->getValue("rounded", "fixed_height").toInt()); + + // 图钉选中时,以 remember_ratio 作为权威模式回显:这样下次打开时 + // 图钉的 checked 状态、下拉菜单的 checked 项和实际要应用的参数三者一致。 + if (m_rememberSetting) { + m_ratioLocked = m_rememberedIsRatio; + if (dropdownRatio != m_ratioLocked) { + persistRatioType(); + } + } + m_pendingRememberedSetting = m_rememberSetting && m_fixedWidth > 0 && m_fixedHeight > 0; + + m_lockButton->setChecked(m_rememberSetting); + if (m_sizeModeButton && m_sizeModeButton->menu()) { + QList actions = m_sizeModeButton->menu()->actions(); + if (actions.size() > 1) { + actions.at(m_ratioLocked ? 1 : 0)->setChecked(true); + m_sizeModeButton->setText(actions.at(m_ratioLocked ? 1 : 0)->text()); + } + } + syncButtonStates(); +} + +void RoundedCornerWidget::syncButtonStates() +{ + // 圆角按钮高亮表示面板已展开或圆角已开启;尺寸比例按钮高亮表示面板展开或已锁定比例 + m_cornerButton->setChecked(m_panel == CornerPanel || m_radius > 0); + m_ratioButton->setChecked(m_panel == SizePanel || m_ratioLocked); +} + +void RoundedCornerWidget::persistRadius() +{ + if (m_persistedRadius == m_radius) { + return; + } + m_persistedRadius = m_radius; + ConfigSettings::instance()->setValue("rounded", "radius", m_radius); +} + +// 把防抖待写入的值立刻落盘(松手、输入完成、控件隐藏/销毁时调用) +void RoundedCornerWidget::flushPendingPersist() +{ + if (m_persistTimer && m_persistTimer->isActive()) { + m_persistTimer->stop(); + } + persistRadius(); +} + +void RoundedCornerWidget::hideEvent(QHideEvent *event) +{ + flushPendingPersist(); + QWidget::hideEvent(event); +} + +void RoundedCornerWidget::persistRatioType() +{ + ConfigSettings::instance()->setValue("rounded", "ratio_type", m_ratioLocked ? 1 : 0); +} + +void RoundedCornerWidget::persistRememberSetting() +{ + ConfigSettings::instance()->setValue("rounded", "remember", m_rememberSetting); +} + +void RoundedCornerWidget::persistFixedSize() +{ + if (m_selectionWidth <= 0 || m_selectionHeight <= 0) { + return; + } + + m_rememberedIsRatio = m_ratioLocked; + if (m_rememberedIsRatio) { + const QPair ratio = simplifiedRatio(m_selectionWidth, m_selectionHeight); + m_fixedWidth = ratio.first; + m_fixedHeight = ratio.second; + } else { + m_fixedWidth = qMax(1, m_selectionWidth); + m_fixedHeight = qMax(1, m_selectionHeight); + } + + ConfigSettings *config = ConfigSettings::instance(); + config->setValue("rounded", "remember_ratio", m_rememberedIsRatio); + config->setValue("rounded", "fixed_width", m_fixedWidth); + config->setValue("rounded", "fixed_height", m_fixedHeight); +} + +void RoundedCornerWidget::captureCurrentSetting() +{ + persistFixedSize(); +} + +bool RoundedCornerWidget::hasPendingRememberedSetting() const +{ + return m_pendingRememberedSetting && m_rememberSetting + && m_fixedWidth > 0 && m_fixedHeight > 0; +} + +bool RoundedCornerWidget::isRememberedSettingRatio() const +{ + return m_rememberedIsRatio; +} + +QSize RoundedCornerWidget::rememberedSetting() const +{ + return QSize(m_fixedWidth, m_fixedHeight); +} + +void RoundedCornerWidget::consumeRememberedSetting() +{ + m_pendingRememberedSetting = false; +} + +int RoundedCornerWidget::radius() const +{ + return m_radius; +} + +bool RoundedCornerWidget::isCornerEnabled() const +{ + return m_radius > 0; +} + +bool RoundedCornerWidget::isRatioLocked() const +{ + return m_ratioLocked; +} + +void RoundedCornerWidget::setRadius(int radius) +{ + m_radiusSlider->setValue(qBound(0, radius, RADIUS_MAX)); +} + +void RoundedCornerWidget::setPanel(PanelType panel) +{ + if (m_panel == panel) { + return; + } + m_panel = panel; + updateLayout(); + reposition(); + emit panelExpandedChanged(m_panel != NoPanel); +} + +void RoundedCornerWidget::collapsePanels() +{ + flushPendingPersist(); + if (m_panel != NoPanel) { + setPanel(NoPanel); + } +} + +void RoundedCornerWidget::setSelectionSize(int width, int height) +{ + m_selectionWidth = width; + m_selectionHeight = height; + updateDimensionFields(); +} + +void RoundedCornerWidget::updateDimensionFields() +{ + if (!m_widthEdit || !m_heightEdit) { + return; + } + + const int maxValue = m_ratioLocked ? MAX_RATIO_VALUE : MAX_DIMENSION_VALUE; + if (m_widthValidator) { + m_widthValidator->setRange(1, maxValue); + } + if (m_heightValidator) { + m_heightValidator->setRange(1, maxValue); + } + + m_syncingSelectionSize = true; + if (m_ratioLocked && m_selectionWidth > 0 && m_selectionHeight > 0) { + const QPair ratio = simplifiedRatio(m_selectionWidth, m_selectionHeight); + if (m_widthEdit->text().toInt() != ratio.first) { + m_widthEdit->setText(QString::number(ratio.first)); + } + if (m_heightEdit->text().toInt() != ratio.second) { + m_heightEdit->setText(QString::number(ratio.second)); + } + } else { + if (m_widthEdit->text().toInt() != m_selectionWidth) { + m_widthEdit->setText(QString::number(m_selectionWidth)); + } + if (m_heightEdit->text().toInt() != m_selectionHeight) { + m_heightEdit->setText(QString::number(m_selectionHeight)); + } + } + m_syncingSelectionSize = false; +} + +void RoundedCornerWidget::updateLayout() +{ + const bool hasPanel = (m_panel != NoPanel); + const int panelHeightOffset = (m_panel == SizePanel) ? SECOND_BUTTON_Y + BUTTON_SIZE / 2 : FIRST_BUTTON_Y + BUTTON_SIZE / 2; + const int totalWidth = hasPanel ? (GROUP_WIDTH + PANEL_GAP + PANEL_WIDTH) : GROUP_WIDTH; + const int margin = plateShadowMargin(this); + + // 控件尺寸 = 可见底板尺寸 + DFloatingWidget 的阴影留白 + setFixedSize(totalWidth + margin * 2, GROUP_HEIGHT + margin * 2); + + const int groupX = (hasPanel && m_panelOnLeft) ? (PANEL_WIDTH + PANEL_GAP) : 0; + m_groupFrame->move(groupX, 0); + + const int panelX = m_panelOnLeft ? 0 : (GROUP_WIDTH + PANEL_GAP); + const int panelY = panelHeightOffset - PANEL_HEIGHT / 2; + + m_cornerPanel->setVisible(m_panel == CornerPanel); + m_sizePanel->setVisible(m_panel == SizePanel); + m_cornerPanel->move(panelX, panelY); + m_sizePanel->move(panelX, panelY); + + if (m_panel != NoPanel) { + (m_panel == CornerPanel ? m_cornerPanel : m_sizePanel)->raise(); + } +} + +void RoundedCornerWidget::followSelection(const QRect &selection, const QRect &screen) +{ + if (selection.isEmpty() || screen.isEmpty()) { + return; + } + m_lastSelection = selection; + m_lastScreen = screen; + reposition(); +} + +void RoundedCornerWidget::reposition() +{ + const QRect &selection = m_lastSelection; + const QRect &screen = m_lastScreen; + if (selection.isEmpty() || screen.isEmpty()) { + return; + } + + // 面板展开方向与位置都以「按钮组 + 面板」的完整宽度计算, + // 保证面板展开前后按钮组位置不会跳动(设计稿 ①/②/③ 判定条件)。 + const int totalWidth = GROUP_WIDTH + PANEL_GAP + PANEL_WIDTH; + const bool hasPanel = (m_panel != NoPanel); + + // 按钮组在屏幕坐标系下应贴合的左边位置 + int groupLeft = 0; + bool panelOnLeft = false; + // ① 选区右侧空间足够:按钮组在选区右上角右侧,面板向右展开 + if (selection.right() + SELECTION_GAP + totalWidth <= screen.right()) { + groupLeft = selection.right() + SELECTION_GAP; + } else if (selection.left() - SELECTION_GAP - totalWidth >= screen.left()) { + // ② 选区左侧空间足够:按钮组在选区左上角左侧,面板向左展开 + panelOnLeft = true; + groupLeft = selection.left() - SELECTION_GAP - GROUP_WIDTH; + } else { + // ③ 两侧均不足:按钮组贴在选区右上角内侧,面板向左展开 + panelOnLeft = true; + groupLeft = selection.right() - GROUP_WIDTH + 1; + } + + if (m_panelOnLeft != panelOnLeft) { + m_panelOnLeft = panelOnLeft; + updateLayout(); + } + + // 面板向左展开时按钮组在控件内部有固定偏移;面板收起时该偏移为 0。 + // 必须按当前展开状态换算控件原点,否则收起面板时按钮组会偏移一个面板宽度。 + const int groupOffset = (panelOnLeft && hasPanel) ? (PANEL_WIDTH + PANEL_GAP) : 0; + // 控件左上角比可见底板多出一个阴影留白,定位时要去掉 + const int margin = plateShadowMargin(this); + int x = groupLeft - groupOffset - margin; + int y = selection.top() - margin; + + x = qBound(screen.left(), x, qMax(screen.left(), screen.right() - width() + 1)); + y = qBound(screen.top(), y, qMax(screen.top(), screen.bottom() - height() + 1)); + move(x, y); +} + +QPixmap RoundedCornerWidget::applyRoundedCorners(const QPixmap &source, int radius) +{ + if (source.isNull() || radius <= 0) { + return source; + } + QImage image = source.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + const qreal dpr = image.devicePixelRatio() > 0 ? image.devicePixelRatio() : 1.0; + const qreal scaledRadius = radius * dpr; + QImage out(image.size(), QImage::Format_ARGB32_Premultiplied); + out.fill(Qt::transparent); + out.setDevicePixelRatio(dpr); + + QPainter painter(&out); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + QPainterPath path; + path.addRoundedRect(QRectF(0, 0, image.width(), image.height()), scaledRadius, scaledRadius); + painter.setClipPath(path); + painter.drawImage(0, 0, image); + painter.end(); + + QPixmap result = QPixmap::fromImage(out); + result.setDevicePixelRatio(dpr); + return result; +} + +QPixmap RoundedCornerWidget::compositeOnWhite(const QPixmap &source) +{ + if (source.isNull() || !source.hasAlphaChannel()) { + return source; + } + QImage image = source.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + const qreal dpr = image.devicePixelRatio() > 0 ? image.devicePixelRatio() : 1.0; + QImage out(image.size(), QImage::Format_ARGB32_Premultiplied); + out.fill(Qt::white); + out.setDevicePixelRatio(dpr); + + QPainter painter(&out); + painter.drawImage(0, 0, image); + painter.end(); + + QPixmap result = QPixmap::fromImage(out); + result.setDevicePixelRatio(dpr); + return result; +} diff --git a/src/widgets/roundedcornerwidget.h b/src/widgets/roundedcornerwidget.h new file mode 100644 index 000000000..bd6ba20fe --- /dev/null +++ b/src/widgets/roundedcornerwidget.h @@ -0,0 +1,142 @@ +// Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ROUNDEDCORNERWIDGET_H +#define ROUNDEDCORNERWIDGET_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +DWIDGET_USE_NAMESPACE + +class ToolButton; + +/** + * @brief 选区右上角的圆角 / 尺寸比例工具组。 + * + * 对齐设计稿「截图录屏-工具栏-圆角」「截图录屏-工具栏-比例」: + * 56x103 竖排按钮组(圆角按钮在上、尺寸/比例按钮在下), + * 属性面板 270x56 在按钮组左侧或右侧水平展开。 + * + * 本控件只是按钮组与面板的容器,容器本身保持透明, + * 按钮组与面板背景使用 DTK 的 DBlurEffectWidget(与主工具栏一致的模糊背景)。 + */ +class RoundedCornerWidget : public QWidget +{ + Q_OBJECT +public: + enum PanelType { + NoPanel = 0, + CornerPanel, + SizePanel + }; + + explicit RoundedCornerWidget(QWidget *parent = nullptr); + ~RoundedCornerWidget() override; + + int radius() const; + bool isCornerEnabled() const; + void setRadius(int radius); + + bool isRatioLocked() const; + PanelType currentPanel() const { return m_panel; } + + // 本次启动是否还有待应用的记忆尺寸/比例。图钉开启且配置中有有效值时, + // 第一次完成框选后由主窗口应用一次。 + bool hasPendingRememberedSetting() const; + bool isRememberedSettingRatio() const; + QSize rememberedSetting() const; + void consumeRememberedSetting(); + + /** + * @brief 跟随选区右上角;按设计稿优先级计算位置与面板展开方向 + * ① 选区右侧空间足够 -> 按钮组在选区右上角右侧,面板向右展开 + * ② 右侧不足、左侧足够 -> 按钮组在选区左上角左侧,面板向左展开 + * ③ 两侧均不足 -> 按钮组在选区右上角内侧,面板向左展开 + */ + void followSelection(const QRect &selection, const QRect &screen); + // 主窗口同步当前选区尺寸到尺寸/比例面板 + void setSelectionSize(int width, int height); + + // 生成带圆角透明通道的图片;radius <= 0 时返回原图 + static QPixmap applyRoundedCorners(const QPixmap &source, int radius); + // JPG/BMP 等不支持透明格式:把含 Alpha 的成图合成到白底上, + // 使圆角外区域呈现白色(投影等半透明像素也一并正确压在白色上)。 + static QPixmap compositeOnWhite(const QPixmap &source); + +signals: + void radiusChanged(int radius); + // 用户在尺寸/比例面板中输入了新的选区尺寸 + void selectionSizeChanged(int width, int height); + void panelExpandedChanged(bool expanded); + +public slots: + // 点击其它工具时收起属性面板(保留圆角与尺寸比例设置) + void collapsePanels(); + +private: + void initUi(); + QWidget *buildGroup(); + QWidget *buildCornerPanel(); + QWidget *buildSizePanel(); + void updateLayout(); + void reposition(); + void setPanel(PanelType panel); + void syncFromConfig(); + void persistRadius(); + void flushPendingPersist(); + void hideEvent(QHideEvent *event) override; + void persistRatioType(); + void persistRememberSetting(); + void persistFixedSize(); + void captureCurrentSetting(); + void syncButtonStates(); + void updateDimensionFields(); + + QWidget *m_groupFrame = nullptr; + ToolButton *m_cornerButton = nullptr; + ToolButton *m_ratioButton = nullptr; + QWidget *m_cornerPanel = nullptr; + QWidget *m_sizePanel = nullptr; + DSlider *m_radiusSlider = nullptr; + DSpinBox *m_radiusSpin = nullptr; + DPushButton *m_sizeModeButton = nullptr; + DLineEdit *m_widthEdit = nullptr; + DLineEdit *m_heightEdit = nullptr; + QIntValidator *m_widthValidator = nullptr; + QIntValidator *m_heightValidator = nullptr; + DIconButton *m_lockButton = nullptr; + + QTimer *m_persistTimer = nullptr; + int m_radius = 0; + // 已经写入配置文件的值,避免无变化的重复落盘 + int m_persistedRadius = -1; + bool m_ratioLocked = false; + bool m_rememberSetting = false; + bool m_pendingRememberedSetting = false; + bool m_rememberedIsRatio = false; + int m_fixedWidth = 0; + int m_fixedHeight = 0; + int m_selectionWidth = 0; + int m_selectionHeight = 0; + PanelType m_panel = NoPanel; + bool m_panelOnLeft = false; + QRect m_lastSelection; + QRect m_lastScreen; + bool m_syncingSelectionSize = false; +}; + +#endif // ROUNDEDCORNERWIDGET_H diff --git a/translations/deepin-screen-recorder_zh_CN.ts b/translations/deepin-screen-recorder_zh_CN.ts index 60703b94c..77f676044 100644 --- a/translations/deepin-screen-recorder_zh_CN.ts +++ b/translations/deepin-screen-recorder_zh_CN.ts @@ -854,4 +854,31 @@ Hold down Shift to draw squares or circles. 退出 + + RoundedCornerWidget + + Radius + 圆角 + + + Size / ratio + 尺寸/比例 + + + Size + 尺寸 + + + Lock ratio + 锁定比例 + + + Ratio + 比例 + + + Keep the selected size or ratio for the next screenshot + 选中后,下次截图保持该尺寸/比例 + + diff --git a/translations/deepin-screen-recorder_zh_HK.ts b/translations/deepin-screen-recorder_zh_HK.ts index 3e22e4b0a..a7069e07a 100644 --- a/translations/deepin-screen-recorder_zh_HK.ts +++ b/translations/deepin-screen-recorder_zh_HK.ts @@ -854,4 +854,31 @@ Hold down Shift to draw squares or circles. 離開 + + RoundedCornerWidget + + Radius + 圓角 + + + Size / ratio + 尺寸/比例 + + + Size + 尺寸 + + + Lock ratio + 鎖定比例 + + + Ratio + 比例 + + + Keep the selected size or ratio for the next screenshot + 選中後,下次截圖保持該尺寸/比例 + + diff --git a/translations/deepin-screen-recorder_zh_TW.ts b/translations/deepin-screen-recorder_zh_TW.ts index 0b377e9d5..ca8decb85 100644 --- a/translations/deepin-screen-recorder_zh_TW.ts +++ b/translations/deepin-screen-recorder_zh_TW.ts @@ -854,4 +854,31 @@ Hold down Shift to draw squares or circles. 離開 + + RoundedCornerWidget + + Radius + 圓角 + + + Size / ratio + 尺寸/比例 + + + Size + 尺寸 + + + Lock ratio + 鎖定比例 + + + Ratio + 比例 + + + Keep the selected size or ratio for the next screenshot + 選中後,下次截圖保持該尺寸/比例 + +