übernahme Code Shortcut

This commit is contained in:
georg0480
2026-01-31 15:28:10 +01:00
parent 6f4d6b9301
commit ef46c21291
1787 changed files with 1126465 additions and 0 deletions

View 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);
}

View 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

View 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;
}

View 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

View 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();
}

View 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

View 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;
}

View 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