Refine GIF and long capture overlays
This commit is contained in:
1 parent
68b2eed29b
commit
684f2f0a0e
15 files changed
+1004
-373
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¢er);
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user