Refine GIF and long capture overlays

This commit is contained in:
easyz committed 2026-03-28 20:46:33 +08:00
1 parent 68b2eed29b
commit 684f2f0a0e
15 files changed
+1004 -373

No files matched your search

+2
View File
@@ -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
+71 -44
View File
@@ -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;
+43 -5
View File
@@ -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 &center)
{
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
+9 -1
View File
@@ -3,6 +3,7 @@
#include "shape.h"
#include <QImage>
#include <QRect>
#include <QVector>
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 &center);
QImage makeBlurredImage(const QImage& source) const;
QVector<QPoint> m_points;
QImage m_canvasSnapshot;
QImage m_blurredSnapshot;
QVector<BlurPatch> m_patches;
int m_brushRadius;
int m_blurRadius;
};
+25 -16
View File
@@ -173,6 +173,11 @@ void GifRecordViewModel::startEncoding()
qDebug() << "[GifRecord] 开始编码 →" << m_pendingSavePath;
// 把原始帧所有权移交给编码线程,避免主线程上再保留一份全量副本。
QVector<QImage> 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<QImage> 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<bool> future = QtConcurrent::run([framesToEncode, savePath, encodeFps]() -> bool {
QFuture<bool> future = QtConcurrent::run([sourceFrames = std::move(sourceFrames),
savePath,
encodeFps,
frameStep,
scale]() mutable -> bool {
QVector<QImage> 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) {
+5 -10
View File
@@ -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) {
+39 -274
View File
@@ -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 <QMouseEvent>
#include <QPainter>
#include <QProgressBar>
#include <QScreen>
#include <QSizePolicy>
#include <QStyle>
#include <QToolButton>
@@ -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<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape && m_gifRecordViewModel) {
m_gifRecordViewModel->cancelRecording();
finishCapture();
return true;
}
}
return QWidget::eventFilter(watched, event);
}
auto *button = qobject_cast<QToolButton *>(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<HWND>(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<HWND>(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;
}
+2 -15
View File
@@ -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;
+714
View File
@@ -0,0 +1,714 @@
#include "widgets/gif_record_overlay_widget.h"
#include <QColor>
#include <QGuiApplication>
#include <QGraphicsDropShadowEffect>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QPainter>
#include <QPen>
#include <QDebug>
#include <QScreen>
#include <QSize>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWindow>
#include <algorithm>
#ifdef Q_OS_WIN
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#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<QRect> enumerateMonitorRects()
{
struct MonitorRects {
QVector<QRect> rects;
} monitors;
EnumDisplayMonitors(nullptr,
nullptr,
[](HMONITOR monitor, HDC, LPRECT, LPARAM data) -> BOOL {
auto *rects = reinterpret_cast<MonitorRects *>(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<LPARAM>(&monitors));
std::sort(monitors.rects.begin(), monitors.rects.end(), ScreenSortLess{});
return monitors.rects;
}
#endif
QVector<ScreenEntry> buildScreenEntries()
{
QVector<ScreenEntry> entries;
QList<QScreen *> 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<QRect> 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<ScreenEntry> &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<ScreenEntry> 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<HWND>(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();
}
}
+47
View File
@@ -0,0 +1,47 @@
#ifndef GIF_RECORD_OVERLAY_WIDGET_H
#define GIF_RECORD_OVERLAY_WIDGET_H
#include <QWidget>
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
+28 -3
View File
@@ -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);
}
}
+11 -4
View File
@@ -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();
+1
View File
@@ -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();
+1 -1
View File
@@ -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,
+6
View File
@@ -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;
};