übernahme Code Shortcut
This commit is contained in:
184
src/screencapture/rectangleselector.cpp
Normal file
184
src/screencapture/rectangleselector.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "rectangleselector.h"
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
|
||||
RectangleSelector::RectangleSelector(QWidget *parent)
|
||||
: QWidget(parent,
|
||||
Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
|
||||
| Qt::X11BypassWindowManagerHint)
|
||||
, m_selecting(false)
|
||||
{
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setMouseTracking(true);
|
||||
setCursor(Qt::CrossCursor);
|
||||
// Ensure we can receive key events like Esc
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
activateWindow();
|
||||
setFocus(Qt::ActiveWindowFocusReason);
|
||||
|
||||
// Make fullscreen
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (screen) {
|
||||
setGeometry(screen->geometry());
|
||||
}
|
||||
}
|
||||
|
||||
RectangleSelector::~RectangleSelector() {}
|
||||
|
||||
void RectangleSelector::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw semi-transparent overlay
|
||||
painter.fillRect(rect(), QColor(0, 0, 0, 100));
|
||||
|
||||
if (m_selecting) {
|
||||
QRect selection = getSelectionRect();
|
||||
|
||||
// Clear the selection area
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
painter.fillRect(selection, Qt::transparent);
|
||||
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
||||
// Draw selection border
|
||||
painter.setPen(QPen(QColor(100, 150, 255), 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(selection);
|
||||
|
||||
// Draw corner handles
|
||||
const int handleSize = 8;
|
||||
painter.setBrush(QColor(100, 150, 255));
|
||||
painter.drawRect(selection.topLeft().x() - handleSize / 2,
|
||||
selection.topLeft().y() - handleSize / 2,
|
||||
handleSize,
|
||||
handleSize);
|
||||
painter.drawRect(selection.topRight().x() - handleSize / 2,
|
||||
selection.topRight().y() - handleSize / 2,
|
||||
handleSize,
|
||||
handleSize);
|
||||
painter.drawRect(selection.bottomLeft().x() - handleSize / 2,
|
||||
selection.bottomLeft().y() - handleSize / 2,
|
||||
handleSize,
|
||||
handleSize);
|
||||
painter.drawRect(selection.bottomRight().x() - handleSize / 2,
|
||||
selection.bottomRight().y() - handleSize / 2,
|
||||
handleSize,
|
||||
handleSize);
|
||||
|
||||
// Draw dimensions text
|
||||
QString dimensions = QString("%1 x %2").arg(selection.width()).arg(selection.height());
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(14);
|
||||
painter.setFont(font);
|
||||
|
||||
QRect textRect = painter.fontMetrics().boundingRect(dimensions);
|
||||
textRect.adjust(-5, -3, 5, 3);
|
||||
|
||||
int textX = selection.center().x() - textRect.width() / 2;
|
||||
int textY = selection.top() - textRect.height() - 5;
|
||||
|
||||
if (textY < 0) {
|
||||
textY = selection.top() + 5;
|
||||
}
|
||||
|
||||
textRect.moveTo(textX, textY);
|
||||
|
||||
painter.setBrush(QColor(30, 30, 30, 200));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(textRect, 3, 3);
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(textRect, Qt::AlignCenter, dimensions);
|
||||
}
|
||||
|
||||
// Draw instruction text
|
||||
if (!m_selecting) {
|
||||
QString instruction = tr("Click and drag to select an area. Press ESC to cancel.");
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(16);
|
||||
painter.setFont(font);
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(rect(), Qt::AlignCenter, instruction);
|
||||
}
|
||||
|
||||
QWidget::paintEvent(event);
|
||||
}
|
||||
|
||||
void RectangleSelector::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
m_startPoint = event->pos();
|
||||
m_currentPoint = event->pos();
|
||||
m_selecting = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void RectangleSelector::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_selecting) {
|
||||
m_currentPoint = event->pos();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void RectangleSelector::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && m_selecting) {
|
||||
QRect selection = getSelectionRect();
|
||||
|
||||
if (selection.width() > 10 && selection.height() > 10) {
|
||||
emit rectangleSelected(selection);
|
||||
close();
|
||||
} else {
|
||||
m_selecting = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RectangleSelector::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Escape) {
|
||||
emit canceled();
|
||||
close();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
||||
QRect RectangleSelector::getSelectionRect() const
|
||||
{
|
||||
int x = qMin(m_startPoint.x(), m_currentPoint.x());
|
||||
int y = qMin(m_startPoint.y(), m_currentPoint.y());
|
||||
int w = qAbs(m_currentPoint.x() - m_startPoint.x());
|
||||
int h = qAbs(m_currentPoint.y() - m_startPoint.y());
|
||||
|
||||
return QRect(x, y, w, h);
|
||||
}
|
||||
52
src/screencapture/rectangleselector.h
Normal file
52
src/screencapture/rectangleselector.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef RECTANGLESELECTOR_H
|
||||
#define RECTANGLESELECTOR_H
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
class RectangleSelector : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RectangleSelector(QWidget *parent = nullptr);
|
||||
~RectangleSelector();
|
||||
|
||||
signals:
|
||||
void rectangleSelected(const QRect &rect);
|
||||
void canceled();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private:
|
||||
QRect getSelectionRect() const;
|
||||
|
||||
QPoint m_startPoint;
|
||||
QPoint m_currentPoint;
|
||||
bool m_selecting;
|
||||
};
|
||||
|
||||
#endif // RECTANGLESELECTOR_H
|
||||
635
src/screencapture/screencapture.cpp
Normal file
635
src/screencapture/screencapture.cpp
Normal file
@@ -0,0 +1,635 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "screencapture.h"
|
||||
#include "Logger.h"
|
||||
#include "rectangleselector.h"
|
||||
#include "toolbarwidget.h"
|
||||
#include "windowpicker.h"
|
||||
#include <QApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDBusPendingCall>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
#endif
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QPixmap>
|
||||
#include <QScreen>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
|
||||
ScreenCapture::ScreenCapture(const QString &outputFile, CaptureMode mode, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_outputFile(outputFile)
|
||||
, m_mode(mode)
|
||||
, m_isImageMode(false)
|
||||
, m_minimizeShotcut(false)
|
||||
, m_recordAudio(false)
|
||||
{}
|
||||
|
||||
ScreenCapture::~ScreenCapture() {}
|
||||
|
||||
void ScreenCapture::startRecording()
|
||||
{
|
||||
switch (m_mode) {
|
||||
case Fullscreen:
|
||||
startFullscreenRecording();
|
||||
break;
|
||||
case Rectangle:
|
||||
startRectangleRecording();
|
||||
break;
|
||||
case Window:
|
||||
startWindowRecording();
|
||||
break;
|
||||
case Interactive:
|
||||
m_toolbar = std::make_unique<ScreenCaptureToolbar>(true);
|
||||
// Toolbar emits int, convert to CaptureMode via lambda for Qt6 typed connect compatibility
|
||||
connect(m_toolbar.get(),
|
||||
&ScreenCaptureToolbar::captureModeSelected,
|
||||
this,
|
||||
[this](int mode, bool minimizeShotcut, bool recordAudio) {
|
||||
this->onCaptureModeSelected(static_cast<CaptureMode>(mode),
|
||||
minimizeShotcut,
|
||||
recordAudio);
|
||||
});
|
||||
m_toolbar->show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapture::startSnapshot()
|
||||
{
|
||||
m_isImageMode = true;
|
||||
|
||||
switch (m_mode) {
|
||||
case Fullscreen:
|
||||
startFullscreenSnapshot();
|
||||
break;
|
||||
case Rectangle:
|
||||
startRectangleSnapshot();
|
||||
break;
|
||||
case Window:
|
||||
startWindowSnapshot();
|
||||
break;
|
||||
case Interactive:
|
||||
// Show toolbar for user to choose mode
|
||||
m_toolbar = std::make_unique<ScreenCaptureToolbar>(false);
|
||||
connect(m_toolbar.get(),
|
||||
&ScreenCaptureToolbar::captureModeSelected,
|
||||
this,
|
||||
[this](int mode, bool minimizeShotcut, bool recordAudio) {
|
||||
this->onCaptureModeSelected(static_cast<CaptureMode>(mode),
|
||||
minimizeShotcut,
|
||||
recordAudio);
|
||||
});
|
||||
m_toolbar->show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapture::onCaptureModeSelected(CaptureMode mode, bool minimizeShotcut, bool recordAudio)
|
||||
{
|
||||
// Close the toolbar safely after signal processing completes
|
||||
if (m_toolbar) {
|
||||
m_toolbar->close();
|
||||
m_toolbar->deleteLater();
|
||||
m_toolbar.release(); // Release ownership so unique_ptr doesn't delete it
|
||||
}
|
||||
|
||||
m_mode = mode;
|
||||
m_minimizeShotcut = minimizeShotcut;
|
||||
m_recordAudio = recordAudio;
|
||||
|
||||
// Emit minimize signal if requested
|
||||
if (m_minimizeShotcut) {
|
||||
emit this->minimizeShotcut();
|
||||
}
|
||||
|
||||
// Continue with the selected mode, respecting image vs video
|
||||
if (m_isImageMode) {
|
||||
startSnapshot();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapture::onRectangleSelected(const QRect &rect)
|
||||
{
|
||||
// Close the rectangle selector safely after signal processing completes
|
||||
if (m_rectangleSelector) {
|
||||
m_rectangleSelector->close();
|
||||
m_rectangleSelector->deleteLater();
|
||||
m_rectangleSelector.release();
|
||||
}
|
||||
|
||||
// Apply device pixel ratio to convert logical to physical pixels
|
||||
auto newRect = applyDevicePixelRatio(rect);
|
||||
auto screen = QGuiApplication::primaryScreen();
|
||||
if (screen)
|
||||
newRect &= applyDevicePixelRatio(screen->geometry());
|
||||
|
||||
emit beginRecording(adjustRectForVideo(newRect), m_recordAudio);
|
||||
}
|
||||
|
||||
void ScreenCapture::onWindowSelected(const QRect &rect)
|
||||
{
|
||||
// Close the window picker safely after signal processing completes
|
||||
if (m_windowPicker) {
|
||||
m_windowPicker->close();
|
||||
m_windowPicker->deleteLater();
|
||||
m_windowPicker.release();
|
||||
}
|
||||
|
||||
// Constrain the rect in case it has padding for shadows
|
||||
auto newRect = applyDevicePixelRatio(rect);
|
||||
newRect.setX(rect.x());
|
||||
newRect.setY(rect.y());
|
||||
auto screen = QGuiApplication::primaryScreen();
|
||||
if (screen)
|
||||
newRect &= applyDevicePixelRatio(screen->geometry());
|
||||
|
||||
emit beginRecording(adjustRectForVideo(newRect), m_recordAudio);
|
||||
}
|
||||
|
||||
void ScreenCapture::startFullscreenRecording()
|
||||
{
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) {
|
||||
LOG_ERROR() << "Error: No screen found";
|
||||
return;
|
||||
}
|
||||
|
||||
auto physicalRect = screen->geometry();
|
||||
if (!isWayland() || !qEnvironmentVariable("XDG_CURRENT_DESKTOP").toLower().contains("gnome"))
|
||||
physicalRect = applyDevicePixelRatio(screen->geometry());
|
||||
|
||||
emit beginRecording(adjustRectForVideo(physicalRect), m_recordAudio);
|
||||
}
|
||||
|
||||
void ScreenCapture::startRectangleRecording()
|
||||
{
|
||||
m_rectangleSelector = std::make_unique<RectangleSelector>();
|
||||
connect(m_rectangleSelector.get(),
|
||||
&RectangleSelector::rectangleSelected,
|
||||
this,
|
||||
&ScreenCapture::onRectangleSelected);
|
||||
connect(m_rectangleSelector.get(),
|
||||
&RectangleSelector::canceled,
|
||||
this,
|
||||
&ScreenCapture::onSelectionCanceled);
|
||||
m_rectangleSelector->show();
|
||||
}
|
||||
|
||||
void ScreenCapture::startWindowRecording()
|
||||
{
|
||||
if (isWayland()) {
|
||||
LOG_ERROR() << "Window capture is not supported on Wayland";
|
||||
return;
|
||||
}
|
||||
m_windowPicker = std::make_unique<WindowPicker>();
|
||||
connect(m_windowPicker.get(),
|
||||
&WindowPicker::windowSelected,
|
||||
this,
|
||||
&ScreenCapture::onWindowSelected);
|
||||
m_windowPicker->show();
|
||||
}
|
||||
|
||||
QPixmap ScreenCapture::captureScreen(const QRect &rect)
|
||||
{
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) {
|
||||
return QPixmap();
|
||||
}
|
||||
|
||||
return screen->grabWindow(0, rect.x(), rect.y(), rect.width(), rect.height());
|
||||
}
|
||||
|
||||
void ScreenCapture::startFullscreenSnapshot()
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
// Use native macOS screencapture command for fullscreen
|
||||
QStringList args;
|
||||
args << "-S" // Fullscreen
|
||||
<< "-t"
|
||||
<< "png" << m_outputFile;
|
||||
|
||||
QProcess *process = new QProcess(this);
|
||||
connect(process, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus) {
|
||||
const bool success = (exitCode == 0 && QFileInfo::exists(m_outputFile));
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR() << "Failed to capture fullscreen snapshot";
|
||||
QString stdoutData = QString::fromUtf8(process->readAllStandardOutput());
|
||||
QString stderrData = QString::fromUtf8(process->readAllStandardError());
|
||||
if (!stdoutData.isEmpty()) {
|
||||
LOG_INFO() << "stdout:" << stdoutData;
|
||||
}
|
||||
if (!stderrData.isEmpty()) {
|
||||
LOG_INFO() << "stderr:" << stderrData;
|
||||
}
|
||||
}
|
||||
process->deleteLater();
|
||||
emit finished(success);
|
||||
});
|
||||
|
||||
process->start("screencapture", args);
|
||||
#else
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) {
|
||||
LOG_ERROR() << "Error: No screen found";
|
||||
return;
|
||||
}
|
||||
|
||||
// Small delay to let any UI elements disappear
|
||||
QTimer::singleShot(100, this, [this, screen]() { captureAndSaveImage(screen->geometry()); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScreenCapture::startRectangleSnapshot()
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
// Use native macOS screencapture command with rectangle selection
|
||||
QStringList args;
|
||||
args << "-i"
|
||||
<< "-t"
|
||||
<< "png" << m_outputFile;
|
||||
|
||||
QProcess *process = new QProcess(this);
|
||||
connect(process, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus) {
|
||||
const bool success = (exitCode == 0 && QFileInfo::exists(m_outputFile));
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR() << "Failed to capture rectangle snapshot";
|
||||
QString stdoutData = QString::fromUtf8(process->readAllStandardOutput());
|
||||
QString stderrData = QString::fromUtf8(process->readAllStandardError());
|
||||
if (!stdoutData.isEmpty()) {
|
||||
LOG_INFO() << "stdout:" << stdoutData;
|
||||
}
|
||||
if (!stderrData.isEmpty()) {
|
||||
LOG_INFO() << "stderr:" << stderrData;
|
||||
}
|
||||
}
|
||||
process->deleteLater();
|
||||
emit finished(success);
|
||||
});
|
||||
|
||||
process->start("screencapture", args);
|
||||
#else
|
||||
m_rectangleSelector = std::make_unique<RectangleSelector>();
|
||||
connect(m_rectangleSelector.get(),
|
||||
&RectangleSelector::rectangleSelected,
|
||||
this,
|
||||
&ScreenCapture::onImageRectangleSelected);
|
||||
connect(m_rectangleSelector.get(),
|
||||
&RectangleSelector::canceled,
|
||||
this,
|
||||
&ScreenCapture::onSelectionCanceled);
|
||||
m_rectangleSelector->show();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScreenCapture::startWindowSnapshot()
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
// Use native macOS screencapture command with window selection
|
||||
QStringList args;
|
||||
args << "-W"
|
||||
<< "-t"
|
||||
<< "png" << m_outputFile;
|
||||
|
||||
QProcess *process = new QProcess(this);
|
||||
connect(process, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus) {
|
||||
const bool success = (exitCode == 0 && QFileInfo::exists(m_outputFile));
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR() << "Failed to capture window snapshot";
|
||||
QString stdoutData = QString::fromUtf8(process->readAllStandardOutput());
|
||||
QString stderrData = QString::fromUtf8(process->readAllStandardError());
|
||||
if (!stdoutData.isEmpty()) {
|
||||
LOG_INFO() << "stdout:" << stdoutData;
|
||||
}
|
||||
if (!stderrData.isEmpty()) {
|
||||
LOG_INFO() << "stderr:" << stderrData;
|
||||
}
|
||||
}
|
||||
process->deleteLater();
|
||||
emit finished(success);
|
||||
});
|
||||
|
||||
process->start("screencapture", args);
|
||||
#else
|
||||
if (isWayland()) {
|
||||
LOG_ERROR() << "Window image capture is not supported on Wayland";
|
||||
return;
|
||||
}
|
||||
m_windowPicker = std::make_unique<WindowPicker>();
|
||||
connect(m_windowPicker.get(),
|
||||
&WindowPicker::windowSelected,
|
||||
this,
|
||||
&ScreenCapture::onImageWindowSelected);
|
||||
m_windowPicker->show();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScreenCapture::onImageRectangleSelected(const QRect &rect)
|
||||
{
|
||||
// Close the rectangle selector safely after signal processing completes
|
||||
if (m_rectangleSelector) {
|
||||
m_rectangleSelector->close();
|
||||
m_rectangleSelector->deleteLater();
|
||||
m_rectangleSelector.release();
|
||||
}
|
||||
// Translate from selector widget coords (primary screen) to global coords
|
||||
QRect globalRect = rect;
|
||||
if (QScreen *screen = QGuiApplication::primaryScreen()) {
|
||||
globalRect.translate(screen->geometry().topLeft());
|
||||
}
|
||||
captureAndSaveImage(globalRect);
|
||||
}
|
||||
|
||||
void ScreenCapture::onImageWindowSelected(const QRect &rect)
|
||||
{
|
||||
// Close the window picker safely after signal processing completes
|
||||
if (m_windowPicker) {
|
||||
m_windowPicker->close();
|
||||
m_windowPicker->deleteLater();
|
||||
m_windowPicker.release();
|
||||
}
|
||||
|
||||
captureAndSaveImage(invertDevicePixelRatio(rect));
|
||||
}
|
||||
|
||||
void ScreenCapture::captureAndSaveImage(const QRect &rect)
|
||||
{
|
||||
// Ensure our UI elements (toolbar/selector) are gone before capture.
|
||||
// Close any open tool UIs immediately.
|
||||
if (m_toolbar) {
|
||||
m_toolbar->close();
|
||||
m_toolbar->deleteLater();
|
||||
m_toolbar.release();
|
||||
}
|
||||
if (m_rectangleSelector) {
|
||||
m_rectangleSelector->close();
|
||||
m_rectangleSelector->deleteLater();
|
||||
m_rectangleSelector.release();
|
||||
}
|
||||
if (m_windowPicker) {
|
||||
m_windowPicker->close();
|
||||
m_windowPicker->deleteLater();
|
||||
m_windowPicker.release();
|
||||
}
|
||||
|
||||
// Defer a bit so the window manager/compositor has time to unmap our UI
|
||||
// before the snapshot is taken.
|
||||
QTimer::singleShot(120, this, [this, rect]() { doCaptureAndSaveImage(rect); });
|
||||
}
|
||||
|
||||
void ScreenCapture::doCaptureAndSaveImage(const QRect &rect)
|
||||
{
|
||||
LOG_DEBUG() << "Capturing image:" << rect;
|
||||
LOG_DEBUG() << "Output file:" << m_outputFile;
|
||||
|
||||
if (isWayland()) {
|
||||
// Try xdg-desktop-portal first (permission prompt)
|
||||
if (captureImagePortal(rect, m_outputFile)) {
|
||||
LOG_DEBUG() << "Image saved successfully to:" << m_outputFile;
|
||||
emit finished(true);
|
||||
return;
|
||||
}
|
||||
LOG_WARNING() << "Portal screenshot failed, will try Qt grabWindow next.";
|
||||
}
|
||||
|
||||
QPixmap pixmap = captureScreen(rect);
|
||||
if (pixmap.isNull()) {
|
||||
LOG_ERROR() << "Failed to capture screen";
|
||||
emit finished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pixmap.save(m_outputFile)) {
|
||||
LOG_DEBUG() << "Image saved successfully to:" << m_outputFile;
|
||||
emit finished(true);
|
||||
} else {
|
||||
LOG_ERROR() << "Failed to save image to:" << m_outputFile;
|
||||
emit finished(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenCapture::captureImagePortal(const QRect &rect, const QString &outputPath)
|
||||
{
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
auto bus = QDBusConnection::sessionBus();
|
||||
if (!bus.isConnected()) {
|
||||
LOG_WARNING() << "DBus session bus not connected";
|
||||
return false;
|
||||
}
|
||||
|
||||
QDBusMessage msg = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Screenshot",
|
||||
"Screenshot");
|
||||
|
||||
QVariantMap options;
|
||||
options.insert("interactive", false);
|
||||
if (rect.isValid() && rect.width() > 0 && rect.height() > 0) {
|
||||
QVariantList area;
|
||||
area << rect.x() << rect.y() << rect.width() << rect.height();
|
||||
options.insert("area", area);
|
||||
}
|
||||
|
||||
msg << QString("") << options;
|
||||
|
||||
QDBusPendingCall async = bus.asyncCall(msg);
|
||||
QDBusPendingCallWatcher watcher(async);
|
||||
QEventLoop callLoop;
|
||||
QObject::connect(&watcher, &QDBusPendingCallWatcher::finished, &callLoop, &QEventLoop::quit);
|
||||
callLoop.exec();
|
||||
|
||||
QDBusPendingReply<QDBusObjectPath> reply = watcher.reply();
|
||||
if (reply.isError()) {
|
||||
LOG_WARNING() << "Portal Screenshot call failed:" << reply.error().message();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto handlePath = reply.value();
|
||||
if (handlePath.path().isEmpty()) {
|
||||
LOG_WARNING() << "Portal returned empty handle";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare to receive Response
|
||||
m_portalSuccess = false;
|
||||
m_portalUri.clear();
|
||||
QEventLoop responseLoop;
|
||||
m_portalEventLoop = &responseLoop;
|
||||
|
||||
bool ok = bus.connect("org.freedesktop.portal.Desktop",
|
||||
handlePath.path(),
|
||||
"org.freedesktop.portal.Request",
|
||||
QStringLiteral("Response"),
|
||||
this,
|
||||
SLOT(onPortalResponse(uint, QVariantMap)));
|
||||
if (!ok) {
|
||||
LOG_WARNING() << "Failed to connect to portal Response signal";
|
||||
m_portalEventLoop = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
QObject::connect(&timeout, &QTimer::timeout, &responseLoop, &QEventLoop::quit);
|
||||
timeout.start(30000);
|
||||
responseLoop.exec();
|
||||
m_portalEventLoop = nullptr;
|
||||
bus.disconnect("org.freedesktop.portal.Desktop",
|
||||
handlePath.path(),
|
||||
"org.freedesktop.portal.Request",
|
||||
QStringLiteral("Response"),
|
||||
this,
|
||||
SLOT(onPortalResponse(uint, QVariantMap)));
|
||||
|
||||
if (!m_portalSuccess) {
|
||||
LOG_WARNING() << "Portal screenshot failed or timed out";
|
||||
return false;
|
||||
}
|
||||
|
||||
QUrl url(m_portalUri);
|
||||
QString srcPath = url.isValid() && url.scheme() == "file" ? url.toLocalFile() : m_portalUri;
|
||||
if (srcPath.isEmpty()) {
|
||||
LOG_WARNING() << "Portal provided empty source path";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QFile::exists(outputPath)) {
|
||||
QFile::remove(outputPath);
|
||||
}
|
||||
if (!QFile::copy(srcPath, outputPath)) {
|
||||
LOG_WARNING() << "Failed to copy portal image from" << srcPath << "to" << outputPath;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapture::onPortalResponse(uint response, const QVariantMap &results)
|
||||
{
|
||||
if (response == 0) {
|
||||
m_portalUri = results.value("uri").toString();
|
||||
m_portalSuccess = !m_portalUri.isEmpty();
|
||||
} else {
|
||||
m_portalSuccess = false;
|
||||
LOG_WARNING() << "Portal responded with error code" << response;
|
||||
}
|
||||
if (m_portalEventLoop) {
|
||||
m_portalEventLoop->quit();
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenCapture::isWayland()
|
||||
{
|
||||
const QString platformName = QGuiApplication::platformName();
|
||||
const QString sessionType = qEnvironmentVariable("XDG_SESSION_TYPE");
|
||||
return platformName.contains("wayland", Qt::CaseInsensitive)
|
||||
|| sessionType.compare("wayland", Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
QRect ScreenCapture::adjustRectForVideo(const QRect &rect)
|
||||
{
|
||||
// H.264 encoders require dimensions to be even (divisible by 2)
|
||||
// Reduce width/height by 1 if odd
|
||||
int width = rect.width();
|
||||
int height = rect.height();
|
||||
|
||||
if (width % 2)
|
||||
--width;
|
||||
if (height % 2)
|
||||
--height;
|
||||
|
||||
auto result = QRect(rect.x(), rect.y(), width, height);
|
||||
if (result != rect)
|
||||
LOG_DEBUG() << "Adjusted capture area from" << rect << "to" << result
|
||||
<< "(H.264 requires even dimensions)";
|
||||
return result;
|
||||
}
|
||||
|
||||
QRect ScreenCapture::applyDevicePixelRatio(const QRect &rect)
|
||||
{
|
||||
// Get the device pixel ratio from the primary screen
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) {
|
||||
LOG_WARNING() << "Could not get screen for device pixel ratio";
|
||||
return rect;
|
||||
}
|
||||
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
|
||||
// If DPR is 1.0, no conversion needed
|
||||
if (qFuzzyCompare(dpr, 1.0)) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
int logicalX = qRound(rect.x() * dpr);
|
||||
int logicalY = qRound(rect.y() * dpr);
|
||||
int logicalWidth = qRound(rect.width() * dpr);
|
||||
int logicalHeight = qRound(rect.height() * dpr);
|
||||
|
||||
QRect logicalRect(logicalX, logicalY, logicalWidth, logicalHeight);
|
||||
|
||||
LOG_DEBUG() << "Device Pixel Ratio:" << dpr;
|
||||
LOG_DEBUG() << "Physical rect (from selector):" << rect;
|
||||
LOG_DEBUG() << "Logical rect (for grabWindow):" << logicalRect;
|
||||
|
||||
return logicalRect;
|
||||
}
|
||||
|
||||
QRect ScreenCapture::invertDevicePixelRatio(const QRect &rect)
|
||||
{
|
||||
// Get the device pixel ratio from the primary screen
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (!screen) {
|
||||
LOG_WARNING() << "Could not get screen for device pixel ratio";
|
||||
return rect;
|
||||
}
|
||||
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
|
||||
// If DPR is 1.0, no conversion needed
|
||||
if (qFuzzyCompare(dpr, 1.0)) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
int logicalX = qRound(rect.x() / dpr);
|
||||
int logicalY = qRound(rect.y() / dpr);
|
||||
int logicalWidth = qRound(rect.width() / dpr);
|
||||
int logicalHeight = qRound(rect.height() / dpr);
|
||||
|
||||
QRect logicalRect(logicalX, logicalY, logicalWidth, logicalHeight);
|
||||
|
||||
LOG_DEBUG() << "Device Pixel Ratio:" << dpr;
|
||||
LOG_DEBUG() << "Physical rect (from selector):" << rect;
|
||||
LOG_DEBUG() << "Logical rect (for grabWindow):" << logicalRect;
|
||||
|
||||
return logicalRect;
|
||||
}
|
||||
92
src/screencapture/screencapture.h
Normal file
92
src/screencapture/screencapture.h
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SCREENCAPTURE_H
|
||||
#define SCREENCAPTURE_H
|
||||
|
||||
#include <memory>
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#include <QScreen>
|
||||
#include <QVariant>
|
||||
|
||||
class QEventLoop;
|
||||
class ScreenCaptureToolbar;
|
||||
class RectangleSelector;
|
||||
class WindowPicker;
|
||||
|
||||
class ScreenCapture : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum CaptureMode { Fullscreen, Rectangle, Window, Interactive };
|
||||
|
||||
explicit ScreenCapture(const QString &outputFile, CaptureMode mode, QObject *parent = nullptr);
|
||||
~ScreenCapture();
|
||||
|
||||
void startRecording();
|
||||
void startSnapshot();
|
||||
static bool isWayland();
|
||||
|
||||
signals:
|
||||
void finished(bool success);
|
||||
void beginRecording(const QRect &captureRect, bool recordAudio);
|
||||
void onSelectionCanceled();
|
||||
void minimizeShotcut();
|
||||
|
||||
private slots:
|
||||
void onCaptureModeSelected(CaptureMode mode, bool minimizeShotcut, bool recordAudio);
|
||||
void onRectangleSelected(const QRect &rect);
|
||||
void onWindowSelected(const QRect &rect);
|
||||
void onImageRectangleSelected(const QRect &rect);
|
||||
void onImageWindowSelected(const QRect &rect);
|
||||
// xdg-desktop-portal response handler
|
||||
void onPortalResponse(uint response, const QVariantMap &results);
|
||||
|
||||
private:
|
||||
void startFullscreenRecording();
|
||||
void startRectangleRecording();
|
||||
void startWindowRecording();
|
||||
void startFullscreenSnapshot();
|
||||
void startRectangleSnapshot();
|
||||
void startWindowSnapshot();
|
||||
void captureAndSaveImage(const QRect &rect);
|
||||
void doCaptureAndSaveImage(const QRect &rect);
|
||||
QPixmap captureScreen(const QRect &rect);
|
||||
QRect adjustRectForVideo(const QRect &rect);
|
||||
QRect applyDevicePixelRatio(const QRect &rect);
|
||||
QRect invertDevicePixelRatio(const QRect &rect);
|
||||
bool captureImagePortal(const QRect &rect, const QString &outputPath);
|
||||
|
||||
// Portal call state
|
||||
bool m_portalSuccess = false;
|
||||
QString m_portalUri;
|
||||
QEventLoop *m_portalEventLoop = nullptr;
|
||||
|
||||
QString m_outputFile;
|
||||
CaptureMode m_mode;
|
||||
bool m_isImageMode;
|
||||
bool m_minimizeShotcut;
|
||||
bool m_recordAudio;
|
||||
|
||||
std::unique_ptr<ScreenCaptureToolbar> m_toolbar;
|
||||
std::unique_ptr<RectangleSelector> m_rectangleSelector;
|
||||
std::unique_ptr<WindowPicker> m_windowPicker;
|
||||
};
|
||||
|
||||
#endif // SCREENCAPTURE_H
|
||||
190
src/screencapture/toolbarwidget.cpp
Normal file
190
src/screencapture/toolbarwidget.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "toolbarwidget.h"
|
||||
#include "screencapture.h"
|
||||
#include <QCheckBox>
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
ScreenCaptureToolbar::ScreenCaptureToolbar(bool isRecordingMode, QWidget *parent)
|
||||
: QWidget(parent,
|
||||
Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
|
||||
| Qt::X11BypassWindowManagerHint)
|
||||
, m_isRecordingMode(isRecordingMode)
|
||||
{
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
// Create buttons
|
||||
m_fullscreenButton = new QPushButton(tr("Fullscreen"), this);
|
||||
m_rectangleButton = new QPushButton(tr("Rectangle"), this);
|
||||
m_windowButton = new QPushButton(tr("Window"), this);
|
||||
|
||||
// Style buttons
|
||||
QString buttonStyle = "QPushButton {"
|
||||
" background-color: rgba(255, 50, 50, 200);"
|
||||
" color: white;"
|
||||
" border: 1px solid rgba(255, 255, 255, 100);"
|
||||
" border-radius: 5px;"
|
||||
" padding: 10px 20px;"
|
||||
" font-size: 14px;"
|
||||
" min-width: 100px;"
|
||||
"}"
|
||||
"QPushButton:hover {"
|
||||
" background-color: rgba(255, 70, 70, 220);"
|
||||
" border: 1px solid rgba(255, 255, 255, 150);"
|
||||
"}"
|
||||
"QPushButton:pressed {"
|
||||
" background-color: rgba(255, 90, 90, 240);"
|
||||
"}";
|
||||
|
||||
m_fullscreenButton->setStyleSheet(buttonStyle);
|
||||
m_rectangleButton->setStyleSheet(buttonStyle);
|
||||
m_windowButton->setStyleSheet(buttonStyle);
|
||||
|
||||
// Create checkboxes
|
||||
m_minimizeCheckbox = new QCheckBox(tr("Minimize Shotcut"), this);
|
||||
m_audioCheckbox = new QCheckBox(tr("Record Audio"), this);
|
||||
|
||||
// Style checkboxes
|
||||
QString checkboxStyle = "QCheckBox {"
|
||||
" color: white;"
|
||||
" font-size: 12px;"
|
||||
" spacing: 5px;"
|
||||
"}"
|
||||
"QCheckBox::indicator {"
|
||||
" width: 16px;"
|
||||
" height: 16px;"
|
||||
" border: 1px solid rgba(255, 255, 255, 150);"
|
||||
" border-radius: 3px;"
|
||||
" background-color: rgba(40, 40, 40, 200);"
|
||||
"}"
|
||||
"QCheckBox::indicator:checked {"
|
||||
" background-color: rgba(255, 50, 50, 200);"
|
||||
" border: 1px solid rgba(255, 255, 255, 200);"
|
||||
"}"
|
||||
"QCheckBox::indicator:hover {"
|
||||
" border: 1px solid rgba(255, 255, 255, 220);"
|
||||
"}";
|
||||
|
||||
m_minimizeCheckbox->setStyleSheet(checkboxStyle);
|
||||
m_audioCheckbox->setStyleSheet(checkboxStyle);
|
||||
m_minimizeCheckbox->setChecked(true); // Default to minimize
|
||||
m_audioCheckbox->setChecked(true); // Default to record audio
|
||||
|
||||
// Create layout
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||
mainLayout->setSpacing(10);
|
||||
|
||||
// Buttons row
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setSpacing(10);
|
||||
buttonLayout->addWidget(m_fullscreenButton);
|
||||
buttonLayout->addWidget(m_rectangleButton);
|
||||
|
||||
// Connect signals
|
||||
connect(m_fullscreenButton,
|
||||
&QPushButton::clicked,
|
||||
this,
|
||||
&ScreenCaptureToolbar::onFullscreenClicked);
|
||||
connect(m_rectangleButton,
|
||||
&QPushButton::clicked,
|
||||
this,
|
||||
&ScreenCaptureToolbar::onRectangleClicked);
|
||||
|
||||
// Hide window button if on Wayland or Windows
|
||||
#ifdef Q_OS_WIN
|
||||
const bool withWindowButton = false;
|
||||
#else
|
||||
const bool withWindowButton = !ScreenCapture::isWayland();
|
||||
#endif
|
||||
if (withWindowButton) {
|
||||
buttonLayout->addWidget(m_windowButton);
|
||||
connect(m_windowButton, &QPushButton::clicked, this, &ScreenCaptureToolbar::onWindowClicked);
|
||||
} else {
|
||||
m_windowButton->hide();
|
||||
}
|
||||
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
// Checkboxes row
|
||||
QHBoxLayout *checkboxLayout = new QHBoxLayout();
|
||||
checkboxLayout->setSpacing(15);
|
||||
checkboxLayout->addWidget(m_minimizeCheckbox);
|
||||
if (m_isRecordingMode) {
|
||||
checkboxLayout->addWidget(m_audioCheckbox);
|
||||
} else {
|
||||
m_audioCheckbox->hide();
|
||||
}
|
||||
mainLayout->addLayout(checkboxLayout);
|
||||
|
||||
// Position at top center of screen
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (screen) {
|
||||
QRect screenGeometry = screen->geometry();
|
||||
adjustSize();
|
||||
int x = screenGeometry.center().x() - width() / 2;
|
||||
int y = screenGeometry.top() + 20;
|
||||
move(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ScreenCaptureToolbar::~ScreenCaptureToolbar() {}
|
||||
|
||||
void ScreenCaptureToolbar::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw semi-transparent background
|
||||
painter.setBrush(QColor(30, 30, 30, 220));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(rect(), 10, 10);
|
||||
|
||||
QWidget::paintEvent(event);
|
||||
}
|
||||
|
||||
void ScreenCaptureToolbar::onFullscreenClicked()
|
||||
{
|
||||
emit captureModeSelected(static_cast<int>(ScreenCapture::Fullscreen),
|
||||
m_minimizeCheckbox->isChecked(),
|
||||
m_audioCheckbox->isChecked());
|
||||
close();
|
||||
}
|
||||
|
||||
void ScreenCaptureToolbar::onRectangleClicked()
|
||||
{
|
||||
emit captureModeSelected(static_cast<int>(ScreenCapture::Rectangle),
|
||||
m_minimizeCheckbox->isChecked(),
|
||||
m_audioCheckbox->isChecked());
|
||||
close();
|
||||
}
|
||||
|
||||
void ScreenCaptureToolbar::onWindowClicked()
|
||||
{
|
||||
emit captureModeSelected(static_cast<int>(ScreenCapture::Window),
|
||||
m_minimizeCheckbox->isChecked(),
|
||||
m_audioCheckbox->isChecked());
|
||||
close();
|
||||
}
|
||||
55
src/screencapture/toolbarwidget.h
Normal file
55
src/screencapture/toolbarwidget.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SCREENCAPTURETOOLBAR_H
|
||||
#define SCREENCAPTURETOOLBAR_H
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
class ScreenCapture;
|
||||
|
||||
class ScreenCaptureToolbar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ScreenCaptureToolbar(bool isRecordingMode, QWidget *parent = nullptr);
|
||||
~ScreenCaptureToolbar();
|
||||
|
||||
signals:
|
||||
void captureModeSelected(int mode, bool minimizeShotcut, bool recordAudio);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void onFullscreenClicked();
|
||||
void onRectangleClicked();
|
||||
void onWindowClicked();
|
||||
|
||||
private:
|
||||
bool m_isRecordingMode;
|
||||
QPushButton *m_fullscreenButton;
|
||||
QPushButton *m_rectangleButton;
|
||||
QPushButton *m_windowButton;
|
||||
QCheckBox *m_minimizeCheckbox;
|
||||
QCheckBox *m_audioCheckbox;
|
||||
};
|
||||
|
||||
#endif // TOOLBARWIDGET_H
|
||||
281
src/screencapture/windowpicker.cpp
Normal file
281
src/screencapture/windowpicker.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "windowpicker.h"
|
||||
#include "Logger.h"
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
// clang-format off
|
||||
extern "C" {
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
}
|
||||
// clang-format on
|
||||
#endif
|
||||
|
||||
WindowPicker::WindowPicker(QWidget *parent)
|
||||
: QWidget(parent,
|
||||
Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
|
||||
| Qt::X11BypassWindowManagerHint)
|
||||
, m_highlightedWindow(-1)
|
||||
{
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setMouseTracking(true);
|
||||
setCursor(Qt::CrossCursor);
|
||||
|
||||
// Make fullscreen
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (screen) {
|
||||
setGeometry(screen->geometry());
|
||||
}
|
||||
|
||||
detectWindows();
|
||||
}
|
||||
|
||||
WindowPicker::~WindowPicker() {}
|
||||
|
||||
void WindowPicker::detectWindows()
|
||||
{
|
||||
m_windows.clear();
|
||||
|
||||
// Try X11 window detection
|
||||
if (QGuiApplication::platformName() == "xcb") {
|
||||
m_windows = getX11Windows();
|
||||
}
|
||||
|
||||
LOG_DEBUG() << "Found" << m_windows.size() << "windows";
|
||||
}
|
||||
|
||||
QList<WindowPicker::WindowInfo> WindowPicker::getX11Windows()
|
||||
{
|
||||
QList<WindowInfo> windows;
|
||||
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
Display *display = XOpenDisplay(nullptr);
|
||||
if (!display) {
|
||||
LOG_WARNING() << "Could not open X display";
|
||||
return windows;
|
||||
}
|
||||
|
||||
Window root = DefaultRootWindow(display);
|
||||
Window parent;
|
||||
Window *children;
|
||||
unsigned int nChildren;
|
||||
|
||||
// Get the atom for window state
|
||||
Atom wmStateAtom = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
Atom wmStateHiddenAtom = XInternAtom(display, "_NET_WM_STATE_HIDDEN", False);
|
||||
|
||||
if (XQueryTree(display, root, &root, &parent, &children, &nChildren)) {
|
||||
for (unsigned int i = 0; i < nChildren; i++) {
|
||||
XWindowAttributes attrs;
|
||||
if (XGetWindowAttributes(display, children[i], &attrs)) {
|
||||
// Skip invisible, unmapped, or override-redirect windows
|
||||
if (attrs.map_state != IsViewable || attrs.override_redirect) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if window is minimized/hidden
|
||||
Atom actualType;
|
||||
int actualFormat;
|
||||
unsigned long numItems, bytesAfter;
|
||||
unsigned char *data = nullptr;
|
||||
|
||||
bool isMinimized = false;
|
||||
if (XGetWindowProperty(display,
|
||||
children[i],
|
||||
wmStateAtom,
|
||||
0,
|
||||
(~0L),
|
||||
False,
|
||||
XA_ATOM,
|
||||
&actualType,
|
||||
&actualFormat,
|
||||
&numItems,
|
||||
&bytesAfter,
|
||||
&data)
|
||||
== Success
|
||||
&& data) {
|
||||
Atom *states = (Atom *) data;
|
||||
for (unsigned long j = 0; j < numItems; j++) {
|
||||
if (states[j] == wmStateHiddenAtom) {
|
||||
isMinimized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
XFree(data);
|
||||
}
|
||||
|
||||
// Skip minimized windows
|
||||
if (isMinimized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip our own window and very small windows
|
||||
if (attrs.width < 50 || attrs.height < 50) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get window title
|
||||
char *windowName = nullptr;
|
||||
XFetchName(display, children[i], &windowName);
|
||||
|
||||
WindowInfo info;
|
||||
info.windowId = children[i];
|
||||
// Store X11 physical coordinates
|
||||
info.physicalGeometry = QRect(attrs.x, attrs.y, attrs.width, attrs.height);
|
||||
info.geometry = info.physicalGeometry; // Will be converted below if needed
|
||||
info.title = windowName ? QString::fromUtf8(windowName) : QString();
|
||||
|
||||
if (windowName) {
|
||||
XFree(windowName);
|
||||
}
|
||||
|
||||
windows.append(info);
|
||||
}
|
||||
}
|
||||
|
||||
if (children) {
|
||||
XFree(children);
|
||||
}
|
||||
}
|
||||
|
||||
XCloseDisplay(display);
|
||||
#endif
|
||||
|
||||
// Convert X11 physical coordinates to Qt logical coordinates for display
|
||||
QScreen *screen = QGuiApplication::primaryScreen();
|
||||
if (screen) {
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
if (!qFuzzyCompare(dpr, 1.0)) {
|
||||
for (WindowInfo &window : windows) {
|
||||
QRect physical = window.geometry;
|
||||
window.geometry = QRect(qRound(physical.x() / dpr),
|
||||
qRound(physical.y() / dpr),
|
||||
qRound(physical.width() / dpr),
|
||||
qRound(physical.height() / dpr));
|
||||
LOG_DEBUG() << "Window:" << window.title << "Physical:" << physical
|
||||
<< "Logical:" << window.geometry << "DPR:" << dpr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
void WindowPicker::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw semi-transparent overlay
|
||||
painter.fillRect(rect(), QColor(0, 0, 0, 100));
|
||||
|
||||
// Highlight the window under cursor
|
||||
if (m_highlightedWindow >= 0 && m_highlightedWindow < m_windows.size()) {
|
||||
const WindowInfo &window = m_windows[m_highlightedWindow];
|
||||
|
||||
// Clear the window area
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
painter.fillRect(window.geometry, Qt::transparent);
|
||||
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
||||
// Draw highlight border
|
||||
painter.setPen(QPen(QColor(100, 150, 255), 3));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(window.geometry);
|
||||
|
||||
// Draw window title
|
||||
if (!window.title.isEmpty()) {
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(14);
|
||||
painter.setFont(font);
|
||||
|
||||
QRect textRect = painter.fontMetrics().boundingRect(window.title);
|
||||
textRect.adjust(-5, -3, 5, 3);
|
||||
|
||||
int textX = window.geometry.center().x() - textRect.width() / 2;
|
||||
int textY = window.geometry.top() - textRect.height() - 5;
|
||||
|
||||
if (textY < 0) {
|
||||
textY = window.geometry.top() + 5;
|
||||
}
|
||||
|
||||
textRect.moveTo(textX, textY);
|
||||
|
||||
painter.setBrush(QColor(30, 30, 30, 200));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(textRect, 3, 3);
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(textRect, Qt::AlignCenter, window.title);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw instruction text
|
||||
QString instruction(tr("Click on a window to select it"));
|
||||
QFont font = painter.font();
|
||||
font.setPixelSize(16);
|
||||
painter.setFont(font);
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
QRect textRect = rect();
|
||||
textRect.setTop(20);
|
||||
painter.drawText(textRect, Qt::AlignTop | Qt::AlignHCenter, instruction);
|
||||
|
||||
QWidget::paintEvent(event);
|
||||
}
|
||||
|
||||
void WindowPicker::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
int oldHighlighted = m_highlightedWindow;
|
||||
m_highlightedWindow = findWindowAtPosition(event->pos());
|
||||
|
||||
if (oldHighlighted != m_highlightedWindow) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void WindowPicker::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (m_highlightedWindow >= 0 && m_highlightedWindow < m_windows.size()) {
|
||||
// Emit physical coordinates for capture
|
||||
emit windowSelected(m_windows[m_highlightedWindow].physicalGeometry);
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int WindowPicker::findWindowAtPosition(const QPoint &pos)
|
||||
{
|
||||
// Find the topmost window at this position
|
||||
for (int i = 0; i < m_windows.size(); ++i) {
|
||||
if (m_windows[i].geometry.contains(pos)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
59
src/screencapture/windowpicker.h
Normal file
59
src/screencapture/windowpicker.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meltytech, LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef WINDOWPICKER_H
|
||||
#define WINDOWPICKER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
class WindowPicker : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WindowPicker(QWidget *parent = nullptr);
|
||||
~WindowPicker();
|
||||
|
||||
signals:
|
||||
void windowSelected(const QRect &rect);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
struct WindowInfo
|
||||
{
|
||||
QRect geometry; // Logical coordinates for Qt display
|
||||
QRect physicalGeometry; // Physical coordinates from X11
|
||||
QString title;
|
||||
unsigned long windowId;
|
||||
};
|
||||
|
||||
void detectWindows();
|
||||
int findWindowAtPosition(const QPoint &pos);
|
||||
QRect getWindowGeometry(unsigned long windowId);
|
||||
QList<WindowInfo> getX11Windows();
|
||||
|
||||
QList<WindowInfo> m_windows;
|
||||
int m_highlightedWindow;
|
||||
};
|
||||
|
||||
#endif // WINDOWPICKER_H
|
||||
Reference in New Issue
Block a user