diff --git a/CMakeLists.txt b/CMakeLists.txt index b4bdfdc..8085e38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,8 @@ set(SOURCES src/widgets/sticky_canvas_widget.h src/widgets/capture_overlay_widget.cpp src/widgets/capture_overlay_widget.h + src/widgets/gif_record_overlay_widget.cpp + src/widgets/gif_record_overlay_widget.h src/widgets/long_capture_controller.cpp src/widgets/long_capture_controller.h src/widgets/magnifier_widget.cpp diff --git a/resource/qss/capture_overlay_widget.qss b/resource/qss/capture_overlay_widget.qss index 1cc5155..6aefde3 100644 --- a/resource/qss/capture_overlay_widget.qss +++ b/resource/qss/capture_overlay_widget.qss @@ -5,9 +5,33 @@ } #captureOverlayGifBar { - background: #FFFFFF; - border: 1px solid #E2E8F0; - border-radius: 10px; + background: rgba(255, 255, 255, 246); + border: 1px solid rgba(226, 232, 240, 220); + border-radius: 16px; +} + +#captureOverlayGifInfoPanel { + background: rgba(248, 250, 252, 232); + border: 1px solid rgba(226, 232, 240, 220); + border-radius: 12px; +} + +#captureOverlayGifTimeLabel { + color: #0F172A; + font-size: 15px; + font-weight: 700; + min-width: 64px; +} + +#captureOverlayGifMetaLabel { + color: #475569; + font-size: 12px; + font-weight: 600; + min-width: 58px; + padding: 0 8px; + background: rgba(255, 255, 255, 210); + border: 1px solid rgba(226, 232, 240, 220); + border-radius: 9px; } #captureOverlayTipBubble { @@ -21,17 +45,6 @@ font-size: 11px; } -#captureOverlayGifProgress { - background: #E2E8F0; - border: none; - border-radius: 3px; -} - -#captureOverlayGifProgress::chunk { - background: #3B82F6; - border-radius: 3px; -} - QToolTip { background: #0F172A; color: #FFFFFF; @@ -54,25 +67,26 @@ QToolTip { } #captureOverlayGifBar QToolButton { - background: transparent; - border: 1px solid transparent; - border-radius: 7px; - padding: 0px; + background: #F8FAFC; + border: 1px solid #E2E8F0; + border-radius: 10px; + padding: 0 18px; margin: 0px; color: #0F172A; - min-width: 32px; - max-width: 32px; - min-height: 32px; - max-height: 32px; + font-size: 13px; + font-weight: 600; + min-width: 122px; + min-height: 38px; + max-height: 38px; } #captureOverlayGifBar QToolButton:hover { - background: #F8FAFC; + background: #F1F5F9; } #captureOverlayGifBar QToolButton:pressed { - background: #EFF6FF; - border-color: #3B82F6; + background: #E2E8F0; + border-color: #94A3B8; } #captureOverlayToolbar QToolButton:hover { @@ -90,11 +104,41 @@ QToolTip { } #captureOverlayGifStopButton:hover { - background: #FEF3C7; + background: #2563EB; + border-color: #2563EB; + color: #FFFFFF; +} + +#captureOverlayGifStopButton { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #3B82F6, + stop:1 #2563EB); + border-color: #2563EB; + color: #FFFFFF; +} + +#captureOverlayGifStopButton:pressed { + background: #1D4ED8; + border-color: #1D4ED8; + color: #FFFFFF; } #captureOverlayGifCancelButton:hover { - background: #FEE2E2; + background: #FFF1F2; + border-color: #FDA4AF; + color: #BE123C; +} + +#captureOverlayGifCancelButton { + background: rgba(255, 255, 255, 235); + border-color: #CBD5E1; + color: #334155; +} + +#captureOverlayGifCancelButton:pressed { + background: #FFE4E6; + border-color: #FB7185; + color: #9F1239; } #captureOverlayToolbar QToolTip { @@ -111,23 +155,6 @@ QToolTip { max-width: 1px; } -#captureOverlayGifStatus { - color: #1E293B; - font-size: 13px; - font-family: "Consolas"; - font-weight: 600; -} - -#captureOverlayGifFrames { - color: #94A3B8; - font-size: 11px; -} - -#captureOverlayGifDot { - background: #EF4444; - border-radius: 4px; -} - #captureOverlayToolOptions { background-color: #FFFFFF; border: 1px solid #E2E8F0; diff --git a/src/paint_board/shape/blur_shape.cpp b/src/paint_board/shape/blur_shape.cpp index 3332a21..2bcd2d2 100644 --- a/src/paint_board/shape/blur_shape.cpp +++ b/src/paint_board/shape/blur_shape.cpp @@ -17,22 +17,23 @@ void BlurShape::setEndPoint(const QPoint &point) return; } m_points.append(point); + applyBlurAt(point); } void BlurShape::draw(QPainter *painter) { - if (!painter || m_blurredSnapshot.isNull() || m_points.isEmpty()) { + if (!painter || m_patches.isEmpty()) { return; } painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); - for (const QPoint &point : m_points) { + for (const BlurPatch &patch : m_patches) { QPainterPath path; - path.addEllipse(QPointF(point), m_brushRadius, m_brushRadius); + path.addEllipse(QPointF(patch.center), m_brushRadius, m_brushRadius); painter->save(); painter->setClipPath(path); - painter->drawImage(QPoint(0, 0), m_blurredSnapshot); + painter->drawImage(patch.rect.topLeft(), patch.image); painter->restore(); } painter->restore(); @@ -44,7 +45,44 @@ void BlurShape::setCanvasSnapshot(const QImage &snapshot) return; } m_canvasSnapshot = snapshot.convertToFormat(QImage::Format_ARGB32); - m_blurredSnapshot = makeBlurredImage(m_canvasSnapshot); + m_patches.clear(); + for (const QPoint &point : m_points) { + applyBlurAt(point); + } +} + +void BlurShape::applyBlurAt(const QPoint ¢er) +{ + if (m_canvasSnapshot.isNull()) { + return; + } + + if (!m_patches.isEmpty()) { + const QPoint delta = m_patches.constLast().center - center; + const int minDistance = qMax(2, m_brushRadius / 3); + if ((delta.x() * delta.x() + delta.y() * delta.y()) < (minDistance * minDistance)) { + return; + } + } + + const int patchRadius = m_brushRadius + m_blurRadius * 2; + QRect patchRect(center.x() - patchRadius, + center.y() - patchRadius, + patchRadius * 2 + 1, + patchRadius * 2 + 1); + patchRect = patchRect.intersected(m_canvasSnapshot.rect()); + if (patchRect.isEmpty()) { + return; + } + + BlurPatch patch; + patch.center = center; + patch.rect = patchRect; + patch.image = makeBlurredImage(m_canvasSnapshot.copy(patchRect)); + if (patch.image.isNull()) { + return; + } + m_patches.append(patch); } QImage BlurShape::makeBlurredImage(const QImage &source) const diff --git a/src/paint_board/shape/blur_shape.h b/src/paint_board/shape/blur_shape.h index 14f45c4..9df27be 100644 --- a/src/paint_board/shape/blur_shape.h +++ b/src/paint_board/shape/blur_shape.h @@ -3,6 +3,7 @@ #include "shape.h" #include +#include #include class BlurShape : public Shape @@ -17,11 +18,18 @@ public: void setCanvasSnapshot(const QImage& snapshot); private: + struct BlurPatch { + QPoint center; + QRect rect; + QImage image; + }; + + void applyBlurAt(const QPoint ¢er); QImage makeBlurredImage(const QImage& source) const; QVector m_points; QImage m_canvasSnapshot; - QImage m_blurredSnapshot; + QVector m_patches; int m_brushRadius; int m_blurRadius; }; diff --git a/src/viewmodel/gif_record_view_model.cpp b/src/viewmodel/gif_record_view_model.cpp index e74ef1d..75e0bae 100644 --- a/src/viewmodel/gif_record_view_model.cpp +++ b/src/viewmodel/gif_record_view_model.cpp @@ -173,6 +173,11 @@ void GifRecordViewModel::startEncoding() qDebug() << "[GifRecord] 开始编码 →" << m_pendingSavePath; + // 把原始帧所有权移交给编码线程,避免主线程上再保留一份全量副本。 + QVector sourceFrames = std::move(m_frames); + m_frames.clear(); + emit frameCountChanged(); + // 根据质量预设做抽帧/缩放,平衡体积与清晰度。 const int preset = m_settings.gifQualityPreset(); int frameStep = 1; @@ -188,24 +193,28 @@ void GifRecordViewModel::startEncoding() encodeFps = 10; } - QVector framesToEncode; - framesToEncode.reserve((m_frames.size() + frameStep - 1) / frameStep); - for (int i = 0; i < m_frames.size(); i += frameStep) { - QImage frame = m_frames[i]; - if (scale < 0.999) { - const int dstW = qMax(2, int(frame.width() * scale)); - const int dstH = qMax(2, int(frame.height() * scale)); - frame = frame.scaled(dstW, dstH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - } - framesToEncode.append(frame); - } - if (framesToEncode.isEmpty() && !m_frames.isEmpty()) { - framesToEncode.append(m_frames.first()); - } - QString savePath = m_pendingSavePath; - QFuture future = QtConcurrent::run([framesToEncode, savePath, encodeFps]() -> bool { + QFuture future = QtConcurrent::run([sourceFrames = std::move(sourceFrames), + savePath, + encodeFps, + frameStep, + scale]() mutable -> bool { + QVector framesToEncode; + framesToEncode.reserve((sourceFrames.size() + frameStep - 1) / frameStep); + for (int i = 0; i < sourceFrames.size(); i += frameStep) { + QImage frame = sourceFrames[i]; + if (scale < 0.999) { + const int dstW = qMax(2, int(frame.width() * scale)); + const int dstH = qMax(2, int(frame.height() * scale)); + frame = frame.scaled(dstW, dstH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + framesToEncode.append(std::move(frame)); + } + if (framesToEncode.isEmpty() && !sourceFrames.isEmpty()) { + framesToEncode.append(sourceFrames.first()); + } + GifEncoder encoder; bool ok = encoder.encode(framesToEncode, savePath, encodeFps, true); if (!ok) { diff --git a/src/viewmodel/screenshot_view_model.cpp b/src/viewmodel/screenshot_view_model.cpp index 6ea596e..2f58a11 100644 --- a/src/viewmodel/screenshot_view_model.cpp +++ b/src/viewmodel/screenshot_view_model.cpp @@ -247,17 +247,12 @@ QImage ScreenshotViewModel::captureScreen(StickyCanvasWidget *paintBoard, } if (paintBoard) { - const QImage composited = paintBoard->compositedImage(); + const QRect localRect = rect.translated(-m_snapshot.virtualGeometry().topLeft()); + const QImage composited = rect.isEmpty() + ? paintBoard->compositedImage() + : paintBoard->compositedImage(localRect); if (!composited.isNull()) { - if (!rect.isEmpty()) { - const QRect localRect = rect.translated(-m_snapshot.virtualGeometry().topLeft()) - .intersected(composited.rect()); - if (!localRect.isEmpty()) { - screenshot = composited.copy(localRect); - } - } else { - screenshot = composited; - } + screenshot = composited; } if (setBackground) { diff --git a/src/widgets/capture_overlay_widget.cpp b/src/widgets/capture_overlay_widget.cpp index 4bd9c07..8da99f0 100644 --- a/src/widgets/capture_overlay_widget.cpp +++ b/src/widgets/capture_overlay_widget.cpp @@ -1,5 +1,6 @@ #include "widgets/capture_overlay_widget.h" +#include "widgets/gif_record_overlay_widget.h" #include "widgets/magnifier_widget.h" #include "widgets/sticky_canvas_widget.h" #include "widgets/widget_window_bridge.h" @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -40,30 +42,6 @@ namespace { -class GifBorderOverlayWidget : public QWidget -{ -public: - explicit GifBorderOverlayWidget(QWidget *parent = nullptr) - : QWidget(parent) - { - setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_TranslucentBackground, true); - setAttribute(Qt::WA_TransparentForMouseEvents, true); - } - -protected: - void paintEvent(QPaintEvent *) override - { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, false); - QPen pen(QColor(QStringLiteral("#EF4444"))); - pen.setWidth(2); - painter.setPen(pen); - painter.setBrush(Qt::NoBrush); - painter.drawRect(rect().adjusted(1, 1, -1, -1)); - } -}; - QString loadCaptureOverlayStyleSheet() { QFile file(QStringLiteral(":/resource/qss/capture_overlay_widget.qss")); @@ -88,22 +66,6 @@ QToolButton *createToolbarButton(const QString &iconPath, return button; } -QRect placementBoundsForGlobalRect(const QRect &globalRect, const QRect &fallbackBounds) -{ -#ifdef Q_OS_WIN - RECT rect{ globalRect.left(), globalRect.top(), globalRect.right() + 1, globalRect.bottom() + 1 }; - if (HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST)) { - MONITORINFO info{}; - info.cbSize = sizeof(MONITORINFO); - if (GetMonitorInfoW(monitor, &info)) { - const RECT &work = info.rcWork; - return QRect(work.left, work.top, work.right - work.left, work.bottom - work.top); - } - } -#endif - return fallbackBounds; -} - } CaptureOverlayWidget::CaptureOverlayWidget(ScreenshotViewModel *screenCapture, @@ -432,72 +394,19 @@ CaptureOverlayWidget::CaptureOverlayWidget(ScreenshotViewModel *screenCapture, } } - m_gifBar = new QWidget(nullptr); - m_gifBar->setObjectName(QStringLiteral("captureOverlayGifBar")); - m_gifBar->setAttribute(Qt::WA_StyledBackground, true); - m_gifBar->setAttribute(Qt::WA_ShowWithoutActivating, true); - m_gifBar->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); - m_gifBar->setFocusPolicy(Qt::StrongFocus); - m_gifBar->installEventFilter(this); - m_gifBar->setStyleSheet(loadCaptureOverlayStyleSheet()); - m_gifBar->setFixedWidth(252); - auto *gifLayout = new QVBoxLayout(m_gifBar); - gifLayout->setContentsMargins(6, 6, 8, 6); - gifLayout->setSpacing(5); - - auto *gifTopRow = new QHBoxLayout(); - gifTopRow->setContentsMargins(0, 0, 0, 0); - gifTopRow->setSpacing(4); - gifLayout->addLayout(gifTopRow); - - m_gifDot = new QLabel(m_gifBar); - m_gifDot->setObjectName(QStringLiteral("captureOverlayGifDot")); - m_gifDot->setFixedSize(8, 8); - gifTopRow->addWidget(m_gifDot, 0, Qt::AlignVCenter); - - m_gifStatusLabel = new QLabel(tr("00:00"), m_gifBar); - m_gifStatusLabel->setObjectName(QStringLiteral("captureOverlayGifStatus")); - m_gifStatusLabel->setAlignment(Qt::AlignCenter); - m_gifStatusLabel->setFixedWidth(96); - m_gifStatusLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - gifTopRow->addWidget(m_gifStatusLabel, 0, Qt::AlignVCenter); - - m_gifFrameLabel = new QLabel(tr("0f"), m_gifBar); - m_gifFrameLabel->setObjectName(QStringLiteral("captureOverlayGifFrames")); - m_gifFrameLabel->setAlignment(Qt::AlignCenter); - m_gifFrameLabel->setFixedWidth(42); - m_gifFrameLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - gifTopRow->addWidget(m_gifFrameLabel, 0, Qt::AlignVCenter); - - auto *gifDivider = new QFrame(m_gifBar); - gifDivider->setObjectName(QStringLiteral("captureOverlayToolbarDivider")); - gifDivider->setFixedSize(1, 18); - gifTopRow->addWidget(gifDivider, 0, Qt::AlignVCenter); - - m_gifStopButton = createToolbarButton(QStringLiteral(":/resource/img/lc_gif_stop.svg"), tr("停止录制"), m_gifBar); - m_gifCancelButton = createToolbarButton(QStringLiteral(":/resource/img/lc_x.svg"), tr("取消录制"), m_gifBar); - m_gifStopButton->setObjectName(QStringLiteral("captureOverlayGifStopButton")); - m_gifCancelButton->setObjectName(QStringLiteral("captureOverlayGifCancelButton")); - m_gifStopButton->setToolTip(QString()); - m_gifCancelButton->setToolTip(QString()); - gifTopRow->addWidget(m_gifStopButton); - gifTopRow->addWidget(m_gifCancelButton); - - m_gifProgressBar = new QProgressBar(m_gifBar); - m_gifProgressBar->setObjectName(QStringLiteral("captureOverlayGifProgress")); - m_gifProgressBar->setTextVisible(false); - m_gifProgressBar->setFixedHeight(6); - m_gifProgressBar->setRange(0, 100); - m_gifProgressBar->setValue(0); - m_gifProgressBar->hide(); - gifLayout->addWidget(m_gifProgressBar); - m_gifBar->hide(); - - m_gifBlinkTimer = new QTimer(this); - m_gifBlinkTimer->setInterval(500); - - m_gifBorderOverlay = new GifBorderOverlayWidget(nullptr); - m_gifBorderOverlay->setAttribute(Qt::WA_ShowWithoutActivating, true); + m_gifOverlay = new GifRecordOverlayWidget(); + connect(this, &QObject::destroyed, m_gifOverlay, &QObject::deleteLater); + connect(m_gifOverlay, &GifRecordOverlayWidget::stopRequested, this, [this]() { + if (m_gifRecordViewModel) { + m_gifRecordViewModel->stopRecording(); + } + }); + connect(m_gifOverlay, &GifRecordOverlayWidget::cancelRequested, this, [this]() { + if (m_gifRecordViewModel) { + m_gifRecordViewModel->cancelRecording(); + } + finishCapture(); + }); m_tipBubble = new QWidget(this); m_tipBubble->setObjectName(QStringLiteral("captureOverlayTipBubble")); @@ -511,34 +420,28 @@ CaptureOverlayWidget::CaptureOverlayWidget(ScreenshotViewModel *screenCapture, tipLayout->addWidget(m_tipLabel); m_tipBubble->hide(); - connect(m_gifStopButton, &QToolButton::clicked, this, [this]() { - if (m_gifRecordViewModel) { - m_gifRecordViewModel->stopRecording(); - } - }); - connect(m_gifCancelButton, &QToolButton::clicked, this, [this]() { - if (m_gifRecordViewModel) { - m_gifRecordViewModel->cancelRecording(); - } - finishCapture(); - }); - if (m_gifRecordViewModel) { connect(m_gifRecordViewModel, &GifRecordViewModel::frameCountChanged, this, [this]() { - updateGifStatusText(); + if (m_gifOverlay) { + m_gifOverlay->setFrameCount(m_gifRecordViewModel->frameCount()); + } }); connect(m_gifRecordViewModel, &GifRecordViewModel::elapsedSecsChanged, this, [this]() { - updateGifStatusText(); + if (m_gifOverlay) { + m_gifOverlay->setElapsedSecs(m_gifRecordViewModel->elapsedSecs()); + } }); connect(m_gifRecordViewModel, &GifRecordViewModel::isRecordingChanged, this, [this]() { - updateGifBarState(); - updateGifStatusText(); - updateGifDotState(); + if (m_gifOverlay) { + m_gifOverlay->setRecordingState(m_gifRecordViewModel->isRecording(), + m_gifRecordViewModel->isEncoding()); + } }); connect(m_gifRecordViewModel, &GifRecordViewModel::isEncodingChanged, this, [this]() { - updateGifBarState(); - updateGifStatusText(); - updateGifDotState(); + if (m_gifOverlay) { + m_gifOverlay->setRecordingState(m_gifRecordViewModel->isRecording(), + m_gifRecordViewModel->isEncoding()); + } }); connect(m_gifRecordViewModel, &GifRecordViewModel::encodingFinished, this, [this](const QString &) { finishCapture(); @@ -768,18 +671,6 @@ bool CaptureOverlayWidget::eventFilter(QObject *watched, QEvent *event) return QWidget::eventFilter(watched, event); } - if (watched == m_gifBar && m_gifBar) { - if (event->type() == QEvent::KeyPress) { - auto *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Escape && m_gifRecordViewModel) { - m_gifRecordViewModel->cancelRecording(); - finishCapture(); - return true; - } - } - return QWidget::eventFilter(watched, event); - } - auto *button = qobject_cast(watched); if (!button) { return QWidget::eventFilter(watched, event); @@ -832,22 +723,14 @@ void CaptureOverlayWidget::resetState() if (m_textInput) { m_textInput->setText(m_annotationText); } - if (m_gifBar) { - m_gifBar->hide(); - } - if (m_gifBorderOverlay) { - m_gifBorderOverlay->hide(); - } if (m_magnifier) { m_magnifier->hide(); } - if (m_gifBlinkTimer) { - m_gifBlinkTimer->stop(); + if (m_gifOverlay) { + m_gifOverlay->resetOverlay(); } m_gifRecordingMode = false; - m_gifCaptureRect = {}; m_gifGlobalCaptureRect = {}; - m_gifDotVisible = true; setCursor(Qt::CrossCursor); update(); } @@ -905,9 +788,9 @@ QImage CaptureOverlayWidget::currentOutputImage() const } if (m_canvas && m_canvas->hasAnnotations()) { - const QImage composited = m_canvas->compositedImage(); + const QImage composited = m_canvas->compositedImage(selection); if (!composited.isNull()) { - return composited.copy(selection); + return composited; } } return currentCaptureImage(); @@ -1066,123 +949,6 @@ void CaptureOverlayWidget::updateToolbarGeometry() updateToolOptionsGeometry(); } -void CaptureOverlayWidget::updateGifBarGeometry() -{ - if (!m_gifBar || m_gifGlobalCaptureRect.isEmpty()) { - return; - } - - const QSize hint = m_gifBar->sizeHint(); - const QRect bounds = placementBoundsForGlobalRect(m_gifGlobalCaptureRect, m_virtualGeometry).adjusted(8, 8, -8, -8); - const int preferredX = m_gifGlobalCaptureRect.x() + m_gifGlobalCaptureRect.width() - hint.width(); - const int x = qBound(bounds.left(), - preferredX, - qMax(bounds.left(), bounds.right() - hint.width() + 1)); - int y = m_gifGlobalCaptureRect.bottom() + 12; - if (y + hint.height() > bounds.bottom() + 1) { - y = qMax(bounds.top(), m_gifGlobalCaptureRect.top() - hint.height() - 12); - } - const QPoint globalTopLeft(x, y); - m_gifBar->resize(hint); -#ifdef Q_OS_WIN - if (HWND hwnd = reinterpret_cast(m_gifBar->winId())) { - SetWindowPos(hwnd, - HWND_TOPMOST, - globalTopLeft.x(), - globalTopLeft.y(), - hint.width(), - hint.height(), - SWP_NOOWNERZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE); - } -#else - m_gifBar->setGeometry(globalTopLeft.x(), globalTopLeft.y(), hint.width(), hint.height()); -#endif - - if (m_gifBorderOverlay) { - const QRect borderRect = m_gifGlobalCaptureRect.adjusted(-3, -3, 3, 3); - m_gifBorderOverlay->resize(borderRect.size()); -#ifdef Q_OS_WIN - if (HWND borderHwnd = reinterpret_cast(m_gifBorderOverlay->winId())) { - SetWindowPos(borderHwnd, - HWND_TOPMOST, - borderRect.x(), - borderRect.y(), - borderRect.width(), - borderRect.height(), - SWP_NOOWNERZORDER | SWP_SHOWWINDOW | SWP_NOACTIVATE); - } -#else - m_gifBorderOverlay->setGeometry(borderRect); -#endif - } -} - -void CaptureOverlayWidget::updateGifStatusText() -{ - if (!m_gifRecordViewModel || !m_gifStatusLabel || !m_gifFrameLabel) { - return; - } - - if (m_gifRecordViewModel->isEncoding()) { - m_gifStatusLabel->setText(tr("生成中")); - m_gifFrameLabel->setText(QString()); - return; - } - - const int elapsedSecs = m_gifRecordViewModel->elapsedSecs(); - const int minutes = elapsedSecs / 60; - const int seconds = elapsedSecs % 60; - m_gifStatusLabel->setText(QStringLiteral("%1:%2") - .arg(minutes, 2, 10, QLatin1Char('0')) - .arg(seconds, 2, 10, QLatin1Char('0'))); - m_gifFrameLabel->setText(QStringLiteral("%1f").arg(m_gifRecordViewModel->frameCount())); -} - -void CaptureOverlayWidget::updateGifBarState() -{ - if (!m_gifRecordViewModel || !m_gifStopButton || !m_gifCancelButton || !m_gifProgressBar) { - return; - } - - const bool encoding = m_gifRecordViewModel->isEncoding(); - const bool recording = m_gifRecordViewModel->isRecording(); - m_gifStopButton->setVisible(recording && !encoding); - m_gifCancelButton->setVisible(!encoding); - m_gifFrameLabel->setVisible(!encoding); - m_gifProgressBar->setVisible(encoding); - if (encoding) { - m_gifProgressBar->setRange(0, 0); - } else { - m_gifProgressBar->setRange(0, 100); - m_gifProgressBar->setValue(recording ? 100 : 0); - } -} - -void CaptureOverlayWidget::updateGifDotState() -{ - if (!m_gifRecordViewModel || !m_gifDot || !m_gifBlinkTimer) { - return; - } - - if (m_gifRecordViewModel->isRecording() && !m_gifRecordViewModel->isEncoding()) { - m_gifBlinkTimer->stop(); - m_gifDotVisible = true; - m_gifDot->setVisible(true); - if (m_gifBorderOverlay && !m_gifBorderOverlay->isVisible()) { - m_gifBorderOverlay->show(); - m_gifBorderOverlay->raise(); - } - return; - } - - m_gifBlinkTimer->stop(); - m_gifDotVisible = true; - m_gifDot->setVisible(!m_gifRecordViewModel->isEncoding()); - if (m_gifBorderOverlay) { - m_gifBorderOverlay->hide(); - } -} - void CaptureOverlayWidget::updateMagnifier(const QPoint &localPos) { if (!m_magnifier || !m_screenCapture) { @@ -1330,7 +1096,6 @@ void CaptureOverlayWidget::performAction(CaptureAction action) if (!m_gifRecordViewModel) { break; } - m_gifCaptureRect = m_selection.rect(); m_gifGlobalCaptureRect = globalRect; m_gifRecordingMode = true; m_toolbar->hide(); @@ -1344,14 +1109,14 @@ void CaptureOverlayWidget::performAction(CaptureAction action) if (m_magnifier) { m_magnifier->hide(); } - updateGifBarState(); - updateGifStatusText(); - updateGifDotState(); - m_gifBar->show(); - m_gifBar->raise(); + if (m_gifOverlay) { + m_gifOverlay->setCaptureRect(globalRect); + m_gifOverlay->setElapsedSecs(0); + m_gifOverlay->setFrameCount(0); + m_gifOverlay->setRecordingState(true, false); + } hide(); qApp->processEvents(); - updateGifBarGeometry(); m_gifRecordViewModel->startRecording(globalRect.x(), globalRect.y(), globalRect.width(), globalRect.height()); return; } diff --git a/src/widgets/capture_overlay_widget.h b/src/widgets/capture_overlay_widget.h index 494cab4..79ee569 100644 --- a/src/widgets/capture_overlay_widget.h +++ b/src/widgets/capture_overlay_widget.h @@ -22,6 +22,7 @@ class StickyViewModel; class StorageViewModel; class OcrViewModel; class GifRecordViewModel; +class GifRecordOverlayWidget; class MagnifierWidget; class WidgetWindowBridge; class StickyCanvasWidget; @@ -75,10 +76,6 @@ private: void updateToolOptionsPanel(); QToolButton *currentActiveToolButton() const; void updateToolbarGeometry(); - void updateGifBarGeometry(); - void updateGifStatusText(); - void updateGifBarState(); - void updateGifDotState(); void updateMagnifier(const QPoint &localPos); void updateToolOptionsGeometry(); QRect toolOptionsRect() const; @@ -100,7 +97,6 @@ private: CaptureAction m_defaultAction = CaptureAction::Copy; SelectionMaskController m_selection; bool m_gifRecordingMode = false; - QRect m_gifCaptureRect; QRect m_gifGlobalCaptureRect; QWidget *m_toolbar = nullptr; @@ -142,16 +138,7 @@ private: int m_numberValue = 1; QPoint m_pendingTextPoint; - QWidget *m_gifBar = nullptr; - QWidget *m_gifBorderOverlay = nullptr; - QLabel *m_gifDot = nullptr; - QLabel *m_gifStatusLabel = nullptr; - QLabel *m_gifFrameLabel = nullptr; - QProgressBar *m_gifProgressBar = nullptr; - QToolButton *m_gifStopButton = nullptr; - QToolButton *m_gifCancelButton = nullptr; - QTimer *m_gifBlinkTimer = nullptr; - bool m_gifDotVisible = true; + GifRecordOverlayWidget *m_gifOverlay = nullptr; MagnifierWidget *m_magnifier = nullptr; QWidget *m_tipBubble = nullptr; QLabel *m_tipLabel = nullptr; diff --git a/src/widgets/gif_record_overlay_widget.cpp b/src/widgets/gif_record_overlay_widget.cpp new file mode 100644 index 0000000..c47492b --- /dev/null +++ b/src/widgets/gif_record_overlay_widget.cpp @@ -0,0 +1,714 @@ +#include "widgets/gif_record_overlay_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#define WIN32_LEAN_AND_MEAN +#include +#endif + +namespace { + +constexpr auto kGifOverlayLogTag = "[GifOverlay v2]"; +constexpr int kGifOverlayDisplayFps = 15; + +struct ScreenSortLess +{ + bool operator()(const QRect &lhs, const QRect &rhs) const + { + if (lhs.left() != rhs.left()) { + return lhs.left() < rhs.left(); + } + return lhs.top() < rhs.top(); + } +}; + +struct ScreenEntry +{ + QScreen *screen = nullptr; + QRect logicalGeometry; + QRect physicalRect; + qreal dpr = 1.0; +}; + +#ifdef Q_OS_WIN +QVector enumerateMonitorRects() +{ + struct MonitorRects { + QVector rects; + } monitors; + + EnumDisplayMonitors(nullptr, + nullptr, + [](HMONITOR monitor, HDC, LPRECT, LPARAM data) -> BOOL { + auto *rects = reinterpret_cast(data); + MONITORINFOEXW info{}; + info.cbSize = sizeof(info); + if (GetMonitorInfoW(monitor, &info)) { + const RECT &rc = info.rcMonitor; + rects->rects.append(QRect(rc.left, + rc.top, + rc.right - rc.left, + rc.bottom - rc.top)); + } + return TRUE; + }, + reinterpret_cast(&monitors)); + + std::sort(monitors.rects.begin(), monitors.rects.end(), ScreenSortLess{}); + return monitors.rects; +} +#endif + +QVector buildScreenEntries() +{ + QVector entries; + QList screens = QGuiApplication::screens(); + if (screens.isEmpty()) { + return entries; + } + + std::sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) { + if (!lhs || !rhs) { + return lhs < rhs; + } + return ScreenSortLess{}(lhs->geometry(), rhs->geometry()); + }); + +#ifdef Q_OS_WIN + const QVector monitorRects = enumerateMonitorRects(); + const int pairCount = qMin(screens.size(), monitorRects.size()); + for (int i = 0; i < pairCount; ++i) { + QScreen *screen = screens.at(i); + if (!screen) { + continue; + } + ScreenEntry entry; + entry.screen = screen; + entry.logicalGeometry = screen->geometry(); + entry.physicalRect = monitorRects.at(i); + entry.dpr = screen->devicePixelRatio(); + if (entry.dpr <= 0.0) { + entry.dpr = 1.0; + } + entries.append(entry); + } + + for (int i = pairCount; i < screens.size(); ++i) { + QScreen *screen = screens.at(i); + if (!screen) { + continue; + } + ScreenEntry entry; + entry.screen = screen; + entry.logicalGeometry = screen->geometry(); + entry.dpr = screen->devicePixelRatio(); + if (entry.dpr <= 0.0) { + entry.dpr = 1.0; + } + entry.physicalRect = QRect(qRound(entry.logicalGeometry.x() * entry.dpr), + qRound(entry.logicalGeometry.y() * entry.dpr), + qRound(entry.logicalGeometry.width() * entry.dpr), + qRound(entry.logicalGeometry.height() * entry.dpr)); + entries.append(entry); + } +#else + for (QScreen *screen : screens) { + if (!screen) { + continue; + } + ScreenEntry entry; + entry.screen = screen; + entry.logicalGeometry = screen->geometry(); + entry.physicalRect = entry.logicalGeometry; + entry.dpr = screen->devicePixelRatio(); + if (entry.dpr <= 0.0) { + entry.dpr = 1.0; + } + entries.append(entry); + } +#endif + + return entries; +} + +const ScreenEntry *screenEntryForPhysicalRect(const QRect &physicalRect, const QVector &entries) +{ + if (entries.isEmpty()) { + return nullptr; + } + + const ScreenEntry *bestEntry = nullptr; + int bestArea = -1; + for (const ScreenEntry &entry : entries) { + const QRect overlap = entry.physicalRect.intersected(physicalRect); + const int area = overlap.width() * overlap.height(); + if (area > bestArea) { + bestArea = area; + bestEntry = &entry; + } + } + + if (bestEntry) { + return bestEntry; + } + + return &entries.first(); +} + +QRect mapPhysicalRectToLogical(const QRect &physicalRect, const ScreenEntry &entry) +{ + const QRect normalized = physicalRect.normalized(); + const QPoint localTopLeft = normalized.topLeft() - entry.physicalRect.topLeft(); + const QPoint logicalTopLeft(entry.logicalGeometry.x() + qRound(localTopLeft.x() / entry.dpr), + entry.logicalGeometry.y() + qRound(localTopLeft.y() / entry.dpr)); + return QRect(logicalTopLeft, + QSize(qMax(1, qRound(normalized.width() / entry.dpr)), + qMax(1, qRound(normalized.height() / entry.dpr)))); +} + +class GifBorderOverlayView : public QWidget +{ +public: + explicit GifBorderOverlayView(QWidget *parent = nullptr) + : QWidget(parent) + { + setAttribute(Qt::WA_TransparentForMouseEvents, true); + setAttribute(Qt::WA_TranslucentBackground, true); + } + +protected: + void paintEvent(QPaintEvent *) override + { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, false); + QPen pen(QColor(QStringLiteral("#EF4444"))); + pen.setWidth(2); + painter.setPen(pen); + painter.setBrush(Qt::NoBrush); + painter.drawRect(rect().adjusted(1, 1, -1, -1)); + } +}; + +QToolButton *createOverlayButton(const QString &iconPath, + const QString &toolTip, + const QString &text, + QWidget *parent) +{ + auto *button = new QToolButton(parent); + button->setToolTip(toolTip); + button->setAutoRaise(false); + button->setText(text); + if (!iconPath.isEmpty()) { + button->setIcon(QIcon(iconPath)); + button->setIconSize(QSize(14, 14)); + if (text.trimmed().isEmpty()) { + button->setToolButtonStyle(Qt::ToolButtonIconOnly); + button->setFixedSize(30, 30); + } else { + button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + button->setFixedHeight(30); + button->setMinimumWidth(78); + } + } else { + button->setToolButtonStyle(Qt::ToolButtonTextOnly); + button->setFixedHeight(30); + button->setMinimumWidth(78); + } + button->setCursor(Qt::PointingHandCursor); + return button; +} + +QRect layoutBoundsForRect(const QRect &globalRect) +{ + if (QScreen *screen = QGuiApplication::screenAt(globalRect.center())) { + return screen->geometry(); + } + return globalRect; +} + +QScreen *targetScreenForRect(const QRect &globalRect) +{ + if (QScreen *screen = QGuiApplication::screenAt(globalRect.center())) { + qDebug() << "[GifOverlay] screenAt center hit" + << "center=" << globalRect.center() + << "screenGeom=" << screen->geometry() + << "screenName=" << screen->name(); + return screen; + } + + QScreen *bestScreen = nullptr; + int bestArea = -1; + for (QScreen *screen : QGuiApplication::screens()) { + if (!screen) { + continue; + } + const QRect overlap = screen->geometry().intersected(globalRect); + const int area = overlap.width() * overlap.height(); + qDebug() << "[GifOverlay] fallback screen candidate" + << "screenGeom=" << screen->geometry() + << "screenName=" << screen->name() + << "overlap=" << overlap + << "area=" << area; + if (area > bestArea) { + bestArea = area; + bestScreen = screen; + } + } + if (bestScreen) { + qDebug() << "[GifOverlay] fallback selected" + << "screenGeom=" << bestScreen->geometry() + << "screenName=" << bestScreen->name() + << "bestArea=" << bestArea; + } else { + qDebug() << "[GifOverlay] fallback failed, no screen selected"; + } + return bestScreen; +} + +} + +GifRecordOverlayWidget::GifRecordOverlayWidget(QWidget *parent) + : QWidget(parent) +{ + setObjectName(QStringLiteral("gifRecordOverlayHost")); + setAttribute(Qt::WA_TranslucentBackground, true); + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + hide(); + + m_borderOverlay = new GifBorderOverlayView(this); + m_borderOverlay->hide(); + + m_bar = new QWidget(this); + m_bar->setObjectName(QStringLiteral("captureOverlayGifBar")); + m_bar->setAttribute(Qt::WA_StyledBackground, true); + m_bar->setStyleSheet(QStringLiteral( + "#captureOverlayGifBar {" + " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 rgba(255,255,255,248)," + " stop:1 rgba(248,250,252,244));" + " border: 1px solid rgba(148,163,184,72);" + " border-radius: 13px;" + "}" + "#captureOverlayGifInfoPanel {" + " background: rgba(15,23,42,10);" + " border: 1px solid rgba(148,163,184,58);" + " border-radius: 10px;" + "}" + "#captureOverlayGifTimeLabel {" + " color: #0F172A;" + " font-size: 15px;" + " font-weight: 700;" + " min-width: 52px;" + "}" + "#captureOverlayGifMetaLabel {" + " color: #64748B;" + " font-size: 10px;" + " font-weight: 600;" + " min-width: 82px;" + "}" + "#captureOverlayGifBar QToolButton {" + " background: rgba(255,255,255,224);" + " border: 1px solid rgba(203,213,225,230);" + " border-radius: 9px;" + " padding: 0px;" + " margin: 0px;" + " color: #0F172A;" + " font-size: 10px;" + " font-weight: 600;" + " min-width: 30px;" + " max-width: 30px;" + " min-height: 30px;" + " max-height: 30px;" + "}" + "#captureOverlayGifBar QToolButton:hover {" + " background: rgba(255,255,255,246);" + " border-color: rgba(148,163,184,255);" + "}" + "#captureOverlayGifBar QToolButton:pressed {" + " background: #E2E8F0;" + " border-color: #64748B;" + "}" + "#captureOverlayGifBar QToolButton:disabled {" + " background: rgba(226,232,240,210);" + " border-color: rgba(203,213,225,210);" + " color: #94A3B8;" + "}" + "#captureOverlayGifStopButton {" + " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #FB7185," + " stop:1 #EF4444);" + " border: 1px solid #E11D48;" + " color: #FFFFFF;" + "}" + "#captureOverlayGifStopButton:hover {" + " background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #F43F5E," + " stop:1 #DC2626);" + " border-color: #BE123C;" + "}" + "#captureOverlayGifStopButton:pressed {" + " background: #BE123C;" + " border-color: #9F1239;" + "}" + "#captureOverlayGifStopButton:disabled {" + " background: rgba(251,113,133,160);" + " border-color: rgba(225,29,72,120);" + " color: rgba(255,255,255,220);" + "}" + "#captureOverlayGifCancelButton {" + " background: rgba(255,255,255,232);" + " border-color: rgba(203,213,225,240);" + " color: #334155;" + "}" + "#captureOverlayGifCancelButton:hover {" + " background: #FFF1F2;" + " border-color: #FDA4AF;" + " color: #BE123C;" + "}" + "#captureOverlayGifCancelButton:pressed {" + " background: #FFE4E6;" + " border-color: #FB7185;" + " color: #9F1239;" + "}" + )); + auto *shadow = new QGraphicsDropShadowEffect(m_bar); + shadow->setBlurRadius(24); + shadow->setOffset(0, 8); + shadow->setColor(QColor(15, 23, 42, 42)); + m_bar->setGraphicsEffect(shadow); + auto *gifLayout = new QHBoxLayout(m_bar); + gifLayout->setContentsMargins(7, 7, 7, 7); + gifLayout->setSpacing(7); + gifLayout->setAlignment(Qt::AlignVCenter); + + m_infoPanel = new QWidget(m_bar); + m_infoPanel->setObjectName(QStringLiteral("captureOverlayGifInfoPanel")); + auto *infoLayout = new QHBoxLayout(m_infoPanel); + infoLayout->setContentsMargins(9, 6, 10, 6); + infoLayout->setSpacing(6); + infoLayout->setAlignment(Qt::AlignVCenter); + + m_stateLabel = new QLabel(m_infoPanel); + m_stateLabel->setAlignment(Qt::AlignCenter); + m_stateLabel->setMinimumWidth(44); + m_stateLabel->setStyleSheet(QStringLiteral( + "background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #FB7185," + " stop:1 #EF4444);" + "color: #FFFFFF;" + "font-size: 9px;" + "font-weight: 700;" + "padding: 3px 7px;" + "border-radius: 8px;")); + + auto *textPanel = new QWidget(m_infoPanel); + auto *textLayout = new QVBoxLayout(textPanel); + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->setSpacing(1); + + m_timeLabel = new QLabel(textPanel); + m_timeLabel->setObjectName(QStringLiteral("captureOverlayGifTimeLabel")); + m_timeLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + + m_fpsLabel = new QLabel(textPanel); + m_fpsLabel->setObjectName(QStringLiteral("captureOverlayGifMetaLabel")); + m_fpsLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + + textLayout->addWidget(m_timeLabel); + textLayout->addWidget(m_fpsLabel); + + infoLayout->addWidget(m_stateLabel, 0, Qt::AlignVCenter); + infoLayout->addWidget(textPanel, 1); + + m_stopButton = createOverlayButton(QStringLiteral(":/resource/img/lc_check.svg"), + tr("完成录制"), + QString(), + m_bar); + m_cancelButton = createOverlayButton(QStringLiteral(":/resource/img/lc_x.svg"), + tr("取消录制"), + QString(), + m_bar); + m_stopButton->setObjectName(QStringLiteral("captureOverlayGifStopButton")); + m_cancelButton->setObjectName(QStringLiteral("captureOverlayGifCancelButton")); + gifLayout->addWidget(m_infoPanel, 0, Qt::AlignVCenter); + gifLayout->addStretch(1); + gifLayout->addWidget(m_stopButton, 0, Qt::AlignVCenter); + gifLayout->addWidget(m_cancelButton, 0, Qt::AlignVCenter); + + updateStatusText(); + m_bar->adjustSize(); + m_bar->setFixedSize(m_bar->sizeHint().expandedTo(QSize(286, 50))); + + connect(m_stopButton, &QToolButton::clicked, this, &GifRecordOverlayWidget::stopRequested); + connect(m_cancelButton, &QToolButton::clicked, this, &GifRecordOverlayWidget::cancelRequested); +} + +void GifRecordOverlayWidget::setCaptureRect(const QRect &globalRect) +{ + qDebug() << kGifOverlayLogTag << "setCaptureRect" << globalRect; + if (m_captureRect == globalRect) { + return; + } + m_captureRect = globalRect; + updateLayout(); +} + +void GifRecordOverlayWidget::setRecordingState(bool recording, bool encoding) +{ + qDebug() << kGifOverlayLogTag << "setRecordingState" + << "recording=" << recording + << "encoding=" << encoding; + if (m_recording == recording && m_encoding == encoding) { + return; + } + m_recording = recording; + m_encoding = encoding; + updatePresentation(); +} + +void GifRecordOverlayWidget::setElapsedSecs(int elapsedSecs) +{ + if (m_elapsedSecs == elapsedSecs) { + return; + } + m_elapsedSecs = elapsedSecs; + updateStatusText(); +} + +void GifRecordOverlayWidget::setFrameCount(int frameCount) +{ + if (m_frameCount == frameCount) { + return; + } + m_frameCount = frameCount; + updateStatusText(); +} + +void GifRecordOverlayWidget::resetOverlay() +{ + qDebug() << kGifOverlayLogTag << "resetOverlay"; + m_captureRect = {}; + m_elapsedSecs = 0; + m_frameCount = 0; + m_recording = false; + m_encoding = false; + if (m_bar) { + m_bar->hide(); + } + if (m_borderOverlay) { + m_borderOverlay->hide(); + } + hide(); +} + +void GifRecordOverlayWidget::updateLayout() +{ + qDebug() << kGifOverlayLogTag << "updateLayout start" + << "captureRect=" << m_captureRect + << "recording=" << m_recording + << "encoding=" << m_encoding; + if (m_captureRect.isEmpty()) { + if (m_bar) m_bar->hide(); + if (m_borderOverlay) m_borderOverlay->hide(); + hide(); + qDebug() << kGifOverlayLogTag << "updateLayout abort: empty capture rect"; + return; + } + + const QVector entries = buildScreenEntries(); + const ScreenEntry *entry = screenEntryForPhysicalRect(m_captureRect, entries); + if (!entry || !entry->screen) { + qDebug() << kGifOverlayLogTag << "updateLayout abort: no screen entry for physical rect" << m_captureRect; + return; + } + + const QRect logicalCaptureRect = mapPhysicalRectToLogical(m_captureRect, *entry); + const QRect borderRect = logicalCaptureRect.adjusted(-3, -3, 3, 3); + m_bar->adjustSize(); + const QSize hint = m_bar->sizeHint(); + const QRect screenRect = entry->screen->geometry(); + const QRect bounds = screenRect.adjusted(8, 8, -8, -8); + const int preferredX = borderRect.x() + borderRect.width() - hint.width(); + const int x = qBound(bounds.left(), + preferredX, + qMax(bounds.left(), bounds.right() - hint.width() + 1)); + int y = borderRect.bottom() + 12; + if (y + hint.height() > bounds.bottom() + 1) { + y = qMax(bounds.top(), borderRect.top() - hint.height() - 12); + } + + const QRect barRect(x, y, hint.width(), hint.height()); + qDebug() << kGifOverlayLogTag << "computed rects" + << "screenPhysical=" << entry->physicalRect + << "screenLogical=" << entry->logicalGeometry + << "screenName=" << entry->screen->name() + << "captureRect(physical)=" << m_captureRect + << "captureRect(logical)=" << logicalCaptureRect + << "borderRect=" << borderRect + << "barHint=" << hint + << "screenRect=" << screenRect + << "bounds=" << bounds + << "barRect=" << barRect; + if (!isVisible()) { + show(); + winId(); + qDebug() << kGifOverlayLogTag << "host shown first time" + << "hostVisible=" << isVisible() + << "winId=" << winId(); + } + if (QWindow *handle = windowHandle()) { + handle->setScreen(entry->screen); + qDebug() << kGifOverlayLogTag << "host setScreen" + << "screenName=" << entry->screen->name() + << "screenGeom=" << entry->screen->geometry(); + } + + setGeometry(screenRect); +#ifdef Q_OS_WIN + if (HWND hwnd = reinterpret_cast(winId())) { + SetWindowPos(hwnd, + HWND_TOPMOST, + entry->physicalRect.x(), + entry->physicalRect.y(), + entry->physicalRect.width(), + entry->physicalRect.height(), + SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW); + + RECT nativeRect{}; + if (GetWindowRect(hwnd, &nativeRect)) { + qDebug() << kGifOverlayLogTag << "host native rect" + << QRect(nativeRect.left, + nativeRect.top, + nativeRect.right - nativeRect.left, + nativeRect.bottom - nativeRect.top); + } + } +#endif + m_bar->setGeometry(barRect.translated(-screenRect.topLeft())); + m_borderOverlay->setGeometry(borderRect.translated(-screenRect.topLeft())); + qDebug() << kGifOverlayLogTag << "applied geometry" + << "hostGeom=" << geometry() + << "barGeom(local)=" << m_bar->geometry() + << "borderGeom(local)=" << m_borderOverlay->geometry(); + + if (!m_bar->isVisible()) m_bar->show(); + m_bar->raise(); + if (m_borderOverlay->isVisible()) m_borderOverlay->raise(); + qDebug() << kGifOverlayLogTag << "updateLayout end" + << "hostVisible=" << isVisible() + << "barVisible=" << m_bar->isVisible() + << "borderVisible=" << m_borderOverlay->isVisible(); +} + +void GifRecordOverlayWidget::updateStatusText() +{ + if (!m_stateLabel || !m_timeLabel || !m_fpsLabel) { + return; + } + + if (m_encoding) { + m_stateLabel->setText(tr("导出中")); + m_stateLabel->setStyleSheet(QStringLiteral( + "background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #F59E0B," + " stop:1 #D97706);" + "color: #FFFFFF;" + "font-size: 11px;" + "font-weight: 700;" + "padding: 5px 10px;" + "border-radius: 10px;")); + m_timeLabel->setText(tr("正在生成 GIF")); + m_fpsLabel->setText(tr("已捕获 %1 帧").arg(qMax(0, m_frameCount))); + return; + } + + if (m_recording) { + m_stateLabel->setText(tr("REC")); + m_stateLabel->setStyleSheet(QStringLiteral( + "background: qlineargradient(x1:0, y1:0, x2:1, y2:1," + " stop:0 #FB7185," + " stop:1 #EF4444);" + "color: #FFFFFF;" + "font-size: 11px;" + "font-weight: 700;" + "padding: 5px 10px;" + "border-radius: 10px;")); + } else { + m_stateLabel->setText(tr("GIF")); + m_stateLabel->setStyleSheet(QStringLiteral( + "background: rgba(148,163,184,40);" + "color: #475569;" + "font-size: 11px;" + "font-weight: 700;" + "padding: 5px 10px;" + "border-radius: 10px;" + "border: 1px solid rgba(148,163,184,64);")); + } + + const int totalSecs = qMax(0, m_elapsedSecs); + const int hours = totalSecs / 3600; + const int minutes = (totalSecs % 3600) / 60; + const int seconds = totalSecs % 60; + if (hours > 0) { + m_timeLabel->setText(QStringLiteral("%1:%2:%3") + .arg(hours, 2, 10, QLatin1Char('0')) + .arg(minutes, 2, 10, QLatin1Char('0')) + .arg(seconds, 2, 10, QLatin1Char('0'))); + } else { + m_timeLabel->setText(QStringLiteral("%1:%2") + .arg(minutes, 2, 10, QLatin1Char('0')) + .arg(seconds, 2, 10, QLatin1Char('0'))); + } + m_fpsLabel->setText(QStringLiteral("%1 FPS | %2 %3") + .arg(kGifOverlayDisplayFps) + .arg(qMax(0, m_frameCount)) + .arg(tr("帧"))); +} + +void GifRecordOverlayWidget::updatePresentation() +{ + if (!m_stopButton || !m_cancelButton || !m_borderOverlay) { + return; + } + + if (m_encoding) { + m_borderOverlay->hide(); + m_stopButton->setEnabled(false); + m_cancelButton->setEnabled(false); + } else if (m_recording) { + m_borderOverlay->show(); + m_stopButton->setEnabled(true); + m_cancelButton->setEnabled(true); + } else { + m_borderOverlay->hide(); + m_stopButton->setEnabled(true); + m_cancelButton->setEnabled(true); + } + + qDebug() << kGifOverlayLogTag << "updatePresentation" + << "recording=" << m_recording + << "encoding=" << m_encoding + << "barVisible=" << (m_bar ? m_bar->isVisible() : false) + << "borderVisible=" << m_borderOverlay->isVisible(); + + updateStatusText(); + if (!m_captureRect.isEmpty()) { + updateLayout(); + } +} diff --git a/src/widgets/gif_record_overlay_widget.h b/src/widgets/gif_record_overlay_widget.h new file mode 100644 index 0000000..9227fd1 --- /dev/null +++ b/src/widgets/gif_record_overlay_widget.h @@ -0,0 +1,47 @@ +#ifndef GIF_RECORD_OVERLAY_WIDGET_H +#define GIF_RECORD_OVERLAY_WIDGET_H + +#include + +class QLabel; +class QToolButton; + +class GifRecordOverlayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit GifRecordOverlayWidget(QWidget *parent = nullptr); + + void setCaptureRect(const QRect &globalRect); + void setRecordingState(bool recording, bool encoding); + void setElapsedSecs(int elapsedSecs); + void setFrameCount(int frameCount); + void resetOverlay(); + +signals: + void stopRequested(); + void cancelRequested(); + +private: + void updateLayout(); + void updateStatusText(); + void updatePresentation(); + + QWidget *m_borderOverlay = nullptr; + QWidget *m_bar = nullptr; + QWidget *m_infoPanel = nullptr; + QLabel *m_stateLabel = nullptr; + QLabel *m_timeLabel = nullptr; + QLabel *m_fpsLabel = nullptr; + QToolButton *m_stopButton = nullptr; + QToolButton *m_cancelButton = nullptr; + + QRect m_captureRect; + int m_elapsedSecs = 0; + int m_frameCount = 0; + bool m_recording = false; + bool m_encoding = false; +}; + +#endif // GIF_RECORD_OVERLAY_WIDGET_H diff --git a/src/widgets/long_capture_controller.cpp b/src/widgets/long_capture_controller.cpp index 1942af6..0c8d546 100644 --- a/src/widgets/long_capture_controller.cpp +++ b/src/widgets/long_capture_controller.cpp @@ -244,6 +244,8 @@ void LongCaptureController::ensureWidgets() if (!m_previewWidget) { m_previewWidget = new QWidget(nullptr); m_previewWidget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); + m_previewWidget->setAttribute(Qt::WA_TranslucentBackground, true); + m_previewWidget->setAttribute(Qt::WA_StyledBackground, true); m_previewWidget->setAttribute(Qt::WA_ShowWithoutActivating, true); m_previewWidget->installEventFilter(this); m_previewWidget->setObjectName(QStringLiteral("longCapturePreview")); @@ -323,13 +325,36 @@ void LongCaptureController::updateWidgetGeometry() if (m_previewWidget) { const QSize size = m_previewWidget->size(); - const int x = qMax(workArea.left(), workArea.right() - size.width() - 26); - const int y = qMax(workArea.top() + 20, workArea.center().y() - size.height() / 2); + constexpr int kPreviewGap = 14; + const int rightX = m_captureRect.right() + 1 + kPreviewGap; + const int leftX = m_captureRect.left() - size.width() - kPreviewGap; + + int x = 0; + if (rightX + size.width() <= workArea.right() + 1) { + x = rightX; + } else if (leftX >= workArea.left()) { + x = leftX; + } else { + const int rightSpace = workArea.right() - m_captureRect.right(); + const int leftSpace = m_captureRect.left() - workArea.left(); + if (rightSpace >= leftSpace) { + x = qMax(workArea.left(), workArea.right() - size.width() + 1); + } else { + x = workArea.left(); + } + } + + const int maxY = qMax(workArea.top(), workArea.bottom() - size.height() + 1); + const int y = qBound(workArea.top(), + m_captureRect.top(), + maxY); placeTopMost(m_previewWidget, QRect(x, y, size.width(), size.height())); } if (m_frameWidget) { - const QRect frameRect = m_captureRect.adjusted(-2, -2, 2, 2); + // Keep the decorative frame fully outside the sampled area so + // its corner markers never leak into the captured image. + const QRect frameRect = m_captureRect.adjusted(-6, -6, 6, 6); placeTopMost(m_frameWidget, frameRect); } } diff --git a/src/widgets/sticky_canvas_widget.cpp b/src/widgets/sticky_canvas_widget.cpp index ce69869..6d69e88 100644 --- a/src/widgets/sticky_canvas_widget.cpp +++ b/src/widgets/sticky_canvas_widget.cpp @@ -121,20 +121,27 @@ void StickyCanvasWidget::addTextAnnotation(const QPoint &point, const QString &t QImage StickyCanvasWidget::compositedImage() const { - if (size().isEmpty()) { + return compositedImage(rect()); +} + +QImage StickyCanvasWidget::compositedImage(const QRect &displayRect) const +{ + const QRect targetRect = displayRect.normalized().intersected(rect()); + if (targetRect.isEmpty()) { return {}; } - QImage image(size(), QImage::Format_ARGB32_Premultiplied); + QImage image(targetRect.size(), QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, true); + painter.translate(-targetRect.topLeft()); if (!m_backgroundImage.isNull()) { - painter.drawImage(QRect(QPoint(0, 0), size()), m_backgroundImage); + painter.drawImage(rect(), m_backgroundImage); } else { - painter.fillRect(image.rect(), Qt::white); + painter.fillRect(targetRect, Qt::white); } painter.save(); diff --git a/src/widgets/sticky_canvas_widget.h b/src/widgets/sticky_canvas_widget.h index 7c9d762..56f7bf9 100644 --- a/src/widgets/sticky_canvas_widget.h +++ b/src/widgets/sticky_canvas_widget.h @@ -33,6 +33,7 @@ public: Shapeype activeShapeType() const { return m_shapeType; } bool hasAnnotations() const { return !m_shapes.isEmpty(); } QImage compositedImage() const; + QImage compositedImage(const QRect &displayRect) const; void addTextAnnotation(const QPoint &point, const QString &text); void undo(); void reset(); diff --git a/src/widgets/sticky_pin_widget.cpp b/src/widgets/sticky_pin_widget.cpp index 81593f8..ad97360 100644 --- a/src/widgets/sticky_pin_widget.cpp +++ b/src/widgets/sticky_pin_widget.cpp @@ -306,7 +306,7 @@ StickyPinWidget::StickyPinWidget(const QString &imageUrl, this, &StickyPinWidget::applyAiImage); connect(m_aiViewModel, &AIViewModel::signalRequestFailed, this, [this](const QString &errorMsg) { - setAiLoading(false); + this->setAiLoading(false); QMessageBox::warning(this, tr("AI 编辑失败"), errorMsg); }); connect(m_aiViewModel, &AIViewModel::isLoadingChanged, diff --git a/src/widgets/sticky_pin_widget.h b/src/widgets/sticky_pin_widget.h index 2dcbbd9..fd9520e 100644 --- a/src/widgets/sticky_pin_widget.h +++ b/src/widgets/sticky_pin_widget.h @@ -17,6 +17,7 @@ class QCheckBox; class QLabel; class QLineEdit; class QSpinBox; +class QTimer; class QToolButton; class StickyCanvasWidget; class VisionResultWidget; @@ -51,6 +52,8 @@ private: void setZoomFactor(qreal value); void openAiEditor(); void openVisionWindow(); + void setAiLoading(bool loading); + void updateAiLoadingIcon(); void applyAiImage(const QString &oldImageUrl, const QString &newImageUrl); void releaseStoredImage(); bool saveCurrentImage(); @@ -116,7 +119,10 @@ private: int m_numberValue = 1; bool m_toolbarVisible = true; bool m_shadowVisible = true; + bool m_aiLoading = false; + int m_aiLoadingFrame = 0; qreal m_zoomFactor = 1.0; + QTimer *m_aiLoadingTimer = nullptr; bool m_released = false; };