diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6778e984..eca7270d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -37,6 +37,7 @@ qt_add_library(quickshell-core STATIC common.cpp iconprovider.cpp scriptmodel.cpp + colorquantizer.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/clock.cpp b/src/core/clock.cpp index ebb7e92a..90938d21 100644 --- a/src/core/clock.cpp +++ b/src/core/clock.cpp @@ -6,8 +6,6 @@ #include #include -#include "util.hpp" - SystemClock::SystemClock(QObject* parent): QObject(parent) { QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout); this->update(); @@ -48,19 +46,16 @@ void SystemClock::update() { void SystemClock::setTime(const QDateTime& targetTime) { auto currentTime = QDateTime::currentDateTime(); auto offset = currentTime.msecsTo(targetTime); - auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime; - auto time = dtime.time(); + this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime; - auto secondPrecision = this->mPrecision >= SystemClock::Seconds; - auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); + auto time = this->currentTime.time(); + this->currentTime.setTime(QTime( + this->mPrecision >= SystemClock::Hours ? time.hour() : 0, + this->mPrecision >= SystemClock::Minutes ? time.minute() : 0, + this->mPrecision >= SystemClock::Seconds ? time.second() : 0 + )); - auto minutePrecision = this->mPrecision >= SystemClock::Minutes; - auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0); - - auto hourPrecision = this->mPrecision >= SystemClock::Hours; - auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0); - - DropEmitter::call(secondChanged, minuteChanged, hourChanged); + emit this->dateChanged(); } void SystemClock::schedule(const QDateTime& targetTime) { @@ -76,11 +71,11 @@ void SystemClock::schedule(const QDateTime& targetTime) { auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime; auto baseTimeT = nextTime.time(); - nextTime.setTime( - {hourPrecision ? baseTimeT.hour() : 0, - minutePrecision ? baseTimeT.minute() : 0, - secondPrecision ? baseTimeT.second() : 0} - ); + nextTime.setTime(QTime( + hourPrecision ? baseTimeT.hour() : 0, + minutePrecision ? baseTimeT.minute() : 0, + secondPrecision ? baseTimeT.second() : 0 + )); if (secondPrecision) nextTime = nextTime.addSecs(1); else if (minutePrecision) nextTime = nextTime.addSecs(60); @@ -91,7 +86,3 @@ void SystemClock::schedule(const QDateTime& targetTime) { this->timer.start(static_cast(delay)); this->targetTime = nextTime; } - -DEFINE_MEMBER_GETSET(SystemClock, hours, setHours); -DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes); -DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds); diff --git a/src/core/clock.hpp b/src/core/clock.hpp index 3e669589..67461911 100644 --- a/src/core/clock.hpp +++ b/src/core/clock.hpp @@ -7,9 +7,26 @@ #include #include -#include "util.hpp" - ///! System clock accessor. +/// SystemClock is a view into the system's clock. +/// It updates at hour, minute, or second intervals depending on @@precision. +/// +/// # Examples +/// ```qml +/// SystemClock { +/// id: clock +/// precision: SystemClock.Seconds +/// } +/// +/// @@QtQuick.Text { +/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd") +/// } +/// ``` +/// +/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing, +/// > however this can be either before or after the clock changes (+-50ms). If you +/// > need a date object, use @@date instead of constructing a new one, or the time +/// > of the constructed object could be off by up to a second. class SystemClock: public QObject { Q_OBJECT; /// If the clock should update. Defaults to true. @@ -18,12 +35,17 @@ class SystemClock: public QObject { Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); /// The precision the clock should measure at. Defaults to `SystemClock.Seconds`. Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged); + /// The current date and time. + /// + /// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in + /// > your format of choice. + Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged); /// The current hour. - Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged); + Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged); /// The current minute, or 0 if @@precision is `SystemClock.Hours`. - Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged); + Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged); /// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`. - Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged); + Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged); QML_ELEMENT; public: @@ -43,12 +65,15 @@ public: [[nodiscard]] SystemClock::Enum precision() const; void setPrecision(SystemClock::Enum precision); + [[nodiscard]] QDateTime date() const { return this->currentTime; } + [[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); } + [[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); } + [[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); } + signals: void enabledChanged(); void precisionChanged(); - void hoursChanged(); - void minutesChanged(); - void secondsChanged(); + void dateChanged(); private slots: void onTimeout(); @@ -56,17 +81,11 @@ private slots: private: bool mEnabled = true; SystemClock::Enum mPrecision = SystemClock::Seconds; - quint32 mHours = 0; - quint32 mMinutes = 0; - quint32 mSeconds = 0; QTimer timer; + QDateTime currentTime; QDateTime targetTime; void update(); void setTime(const QDateTime& targetTime); void schedule(const QDateTime& targetTime); - - DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged); - DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged); - DECLARE_PRIVATE_MEMBER(SystemClock, seconds, setSeconds, mSeconds, secondsChanged); }; diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp new file mode 100644 index 00000000..d08f87b4 --- /dev/null +++ b/src/core/colorquantizer.cpp @@ -0,0 +1,182 @@ +#include "colorquantizer.hpp" + +#include +#include +#include +#include +#include + +ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize) + : source(source) + , maxDepth(depth) + , rescaleSize(rescaleSize) { + mColors = QList(); +} + +void ColorQuantizerOperation::run() { + quantizeImage(); + + emit done(mColors); +} + +void ColorQuantizerOperation::quantizeImage() { + mColors.clear(); + + if (source->isEmpty()) { + return; + } + + QImage image(source->toLocalFile()); + + if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { + image = image.scaled( + static_cast(rescaleSize), + static_cast(rescaleSize), + Qt::KeepAspectRatio, + Qt::SmoothTransformation + ); + } + + if (image.isNull()) { + qWarning() << "Failed to load image from" << source; + return; + } + + QList pixels; + for (int y = 0; y < image.height(); ++y) { + for (int x = 0; x < image.width(); ++x) { + QRgb pixel = image.pixel(x, y); + if (qAlpha(pixel) == 0) { + continue; + } + + pixels.append(QColor::fromRgb(pixel)); + } + } + + QDateTime startTime = QDateTime::currentDateTime(); + + mColors = quantization(pixels, 0); + + QDateTime endTime = QDateTime::currentDateTime(); + qint64 milliseconds = startTime.msecsTo(endTime); + qDebug() << "Color Quantization took: " << milliseconds << "ms"; +} + +QList ColorQuantizerOperation::quantization(QList& rgbValues, qreal depth) { + if (depth >= maxDepth || rgbValues.isEmpty()) { + if (rgbValues.isEmpty()) { + return QList(); + } + + int totalR = 0; + int totalG = 0; + int totalB = 0; + + for (const QColor& color: rgbValues) { + totalR += color.red(); + totalG += color.green(); + totalB += color.blue(); + } + + QColor avgColor( + qRound(totalR / static_cast(rgbValues.size())), + qRound(totalG / static_cast(rgbValues.size())), + qRound(totalB / static_cast(rgbValues.size())) + ); + + return QList() << avgColor; + } + + QString dominantChannel = findBiggestColorRange(rgbValues); + std::ranges::sort(rgbValues, [dominantChannel](const QColor& a, const QColor& b) { + if (dominantChannel == "r") return a.red() < b.red(); + else if (dominantChannel == "g") return a.green() < b.green(); + return a.blue() < b.blue(); + }); + + qsizetype mid = rgbValues.size() / 2; + + QList leftHalf = rgbValues.mid(0, mid); + QList rightHalf = rgbValues.mid(mid + 1); + + QList result; + result.append(quantization(leftHalf, depth + 1)); + result.append(quantization(rightHalf, depth + 1)); + + return result; +} + +QString ColorQuantizerOperation::findBiggestColorRange(const QList& rgbValues) { + if (rgbValues.isEmpty()) return "r"; + + int rMin = 255; + int gMin = 255; + int bMin = 255; + int rMax = 0; + int gMax = 0; + int bMax = 0; + + for (const QColor& color: rgbValues) { + rMin = qMin(rMin, color.red()); + gMin = qMin(gMin, color.green()); + bMin = qMin(bMin, color.blue()); + + rMax = qMax(rMax, color.red()); + gMax = qMax(gMax, color.green()); + bMax = qMax(bMax, color.blue()); + } + + int rRange = rMax - rMin; + int gRange = gMax - gMin; + int bRange = bMax - bMin; + + int biggestRange = qMax(rRange, qMax(gRange, bRange)); + if (biggestRange == rRange) { + return "r"; + } else if (biggestRange == gRange) { + return "g"; + } else { + return "b"; + } +} + +QList ColorQuantizer::colors() { return mColors; } + +void ColorQuantizer::setSource(const QUrl& source) { + if (mSource != source) { + mSource = source; + emit sourceChanged(); + quantizeAsync(); + } +} + +void ColorQuantizer::setDepth(qreal depth) { + if (mDepth != depth) { + mDepth = depth; + emit depthChanged(); + } +} + +void ColorQuantizer::setRescaleSize(int rescaleSize) { + if (mRescaleSize != rescaleSize) { + mRescaleSize = rescaleSize; + emit rescaleSizeChanged(); + } +} + +void ColorQuantizer::operationFinished(const QList& result) { + mColors = result; + emit colorsChanged(); + isProcessing = false; +} + +void ColorQuantizer::quantizeAsync() { + if (isProcessing) return; + + qDebug() << "Starting color quantization asynchronously"; + isProcessing = true; + auto* task = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); + QObject::connect(task, &ColorQuantizerOperation::done, this, &ColorQuantizer::operationFinished); + QThreadPool::globalInstance()->start(task); +} diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp new file mode 100644 index 00000000..1c0435de --- /dev/null +++ b/src/core/colorquantizer.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class ColorQuantizerOperation + : public QObject + , public QRunnable { + Q_OBJECT; + +public: + ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); + + void run() override; + +signals: + void done(QList colors); + +private: + QList mColors; + QUrl* source; + qreal maxDepth; + qreal rescaleSize; + + void quantizeImage(); + QList quantization(QList& rgbValues, qreal depth); + static QString findBiggestColorRange(const QList& rgbValues); +}; + +///! Color Quantization Utility +/// A color quantization utility used for getting prevalent colors in an image, by +/// averaging out the image's color data recursively. +/// +/// #### Example +/// ```qml +/// ColorQuantizer { +/// id: colorQuantizer +/// source: Qt.resolvedUrl("./yourImage.png") +/// depth: 3 // Will produce 8 colors (2³) +/// rescaleSize: 64 // Rescale to 64x64 for faster processing +/// } +/// ``` + +class ColorQuantizer: public QObject { + Q_OBJECT; + /// Access the colors resulting from the color quantization performed. + /// > [!NOTE] The amount of colors returned from the quantization is determined by + /// > the property depth, specifically 2ⁿ where n is the depth. + Q_PROPERTY(QList colors READ colors NOTIFY colorsChanged); + + /// Path to the image you'd like to run the color quantization on. + Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged); + + /// Max depth for the color quantization. Each level of depth represents another + /// binary split of the color space + Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged); + + /// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done. + /// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's + /// > reccommended to rescale, otherwise the quantization process will take much longer. + Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged); + QML_ELEMENT; + +public: + QList colors(); + [[nodiscard]] QUrl source() const { return mSource; } + void setSource(const QUrl& source); + [[nodiscard]] qreal depth() const { return mDepth; } + void setDepth(qreal depth); + [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } + void setRescaleSize(int rescaleSize); + +signals: + void colorsChanged(); + void sourceChanged(); + void depthChanged(); + void rescaleSizeChanged(); + +public slots: + void operationFinished(const QList& result); + +private: + bool isProcessing = false; + QList mColors; + QUrl mSource; + qreal mDepth = 0; + qreal mRescaleSize = 0; + + void quantizeAsync(); +}; diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 063aacd6..75a088d9 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -213,7 +213,7 @@ QVector DesktopEntry::parseExecString(const QString& execString) { currentArgument += c; escape = 0; - } else if (c == u'"') { + } else if (c == u'"' || c == u'\'') { parsingString = false; } else { currentArgument += c; @@ -229,7 +229,7 @@ QVector DesktopEntry::parseExecString(const QString& execString) { percent = false; } else if (c == '%') { percent = true; - } else if (c == u'"') { + } else if (c == u'"' || c == u'\'') { parsingString = true; } else if (c == u' ') { if (!currentArgument.isEmpty()) { diff --git a/src/core/imageprovider.cpp b/src/core/imageprovider.cpp index 256faaed..47f284c7 100644 --- a/src/core/imageprovider.cpp +++ b/src/core/imageprovider.cpp @@ -1,5 +1,6 @@ #include "imageprovider.hpp" +#include #include #include #include @@ -7,10 +8,14 @@ #include #include #include +#include namespace { +namespace { QMap liveImages; // NOLINT +quint32 handleIndex = 0; // NOLINT +} // namespace void parseReq(const QString& req, QString& target, QString& param) { auto splitIdx = req.indexOf('/'); @@ -24,14 +29,9 @@ void parseReq(const QString& req, QString& target, QString& param) { } // namespace -QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent) - : QObject(parent) - , type(type) { - { - auto dbg = QDebug(&this->id); - dbg.nospace() << static_cast(this); - } - +QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type) + : type(type) + , id(QString::number(++handleIndex)) { liveImages.insert(this->id, this); } @@ -85,3 +85,9 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req return QPixmap(); } } + +QString QsIndexedImageHandle::url() const { + return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex); +} + +void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; } diff --git a/src/core/imageprovider.hpp b/src/core/imageprovider.hpp index 5ea7843d..8568d4f7 100644 --- a/src/core/imageprovider.hpp +++ b/src/core/imageprovider.hpp @@ -20,15 +20,13 @@ public: QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; }; -class QsImageHandle: public QObject { - Q_OBJECT; - +class QsImageHandle { public: - explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr); - ~QsImageHandle() override; + explicit QsImageHandle(QQmlImageProviderBase::ImageType type); + virtual ~QsImageHandle(); Q_DISABLE_COPY_MOVE(QsImageHandle); - [[nodiscard]] QString url() const; + [[nodiscard]] virtual QString url() const; virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize); virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize); @@ -37,3 +35,14 @@ private: QQmlImageProviderBase::ImageType type; QString id; }; + +class QsIndexedImageHandle: public QsImageHandle { +public: + explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {} + + [[nodiscard]] QString url() const override; + void imageChanged(); + +private: + quint32 changeIndex = 0; +}; diff --git a/src/core/module.md b/src/core/module.md index c8b17ab9..b9404ea9 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -29,5 +29,6 @@ headers = [ "qsmenuanchor.hpp", "clock.hpp", "scriptmodel.hpp", + "colorquantizer.hpp", ] ----- diff --git a/src/core/qmlscreen.cpp b/src/core/qmlscreen.cpp index 34588b77..105b4f01 100644 --- a/src/core/qmlscreen.cpp +++ b/src/core/qmlscreen.cpp @@ -42,6 +42,24 @@ QString QuickshellScreenInfo::name() const { return this->screen->name(); } +QString QuickshellScreenInfo::model() const { + if (this->screen == nullptr) { + this->warnDangling(); + return "{ NULL SCREEN }"; + } + + return this->screen->model(); +} + +QString QuickshellScreenInfo::serialNumber() const { + if (this->screen == nullptr) { + this->warnDangling(); + return "{ NULL SCREEN }"; + } + + return this->screen->serialNumber(); +} + qint32 QuickshellScreenInfo::x() const { if (this->screen == nullptr) { this->warnDangling(); diff --git a/src/core/qmlscreen.hpp b/src/core/qmlscreen.hpp index c74d3b2c..5e978bc0 100644 --- a/src/core/qmlscreen.hpp +++ b/src/core/qmlscreen.hpp @@ -29,6 +29,10 @@ class QuickshellScreenInfo: public QObject { /// /// Usually something like `DP-1`, `HDMI-1`, `eDP-1`. Q_PROPERTY(QString name READ name CONSTANT); + /// The model of the screen as seen by the operating system. + Q_PROPERTY(QString model READ model CONSTANT); + /// The serial number of the screen as seen by the operating system. + Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT); Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged); Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged); Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged); @@ -49,6 +53,8 @@ public: bool operator==(QuickshellScreenInfo& other) const; [[nodiscard]] QString name() const; + [[nodiscard]] QString model() const; + [[nodiscard]] QString serialNumber() const; [[nodiscard]] qint32 x() const; [[nodiscard]] qint32 y() const; [[nodiscard]] qint32 width() const; diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 0267af8e..2b633b76 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -59,8 +59,8 @@ QString DBusMenuItem::icon() const { this->iconName, this->menu->iconThemePath.value().join(':') ); - } else if (this->image != nullptr) { - return this->image->url(); + } else if (this->image.hasData()) { + return this->image.url(); } else return nullptr; } @@ -113,7 +113,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString auto originalEnabled = this->mEnabled; auto originalVisible = this->visible; auto originalIconName = this->iconName; - auto* originalImage = this->image; + auto imageChanged = false; auto originalIsSeparator = this->mSeparator; auto originalButtonType = this->mButtonType; auto originalToggleState = this->mCheckState; @@ -173,12 +173,16 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (iconData.canConvert()) { auto data = iconData.value(); if (data.isEmpty()) { - this->image = nullptr; - } else if (this->image == nullptr || this->image->data != data) { - this->image = new DBusMenuPngImage(data, this); + imageChanged = this->image.hasData(); + this->image.data.clear(); + } else if (!this->image.hasData() || this->image.data != data) { + imageChanged = true; + this->image.data = data; + this->image.imageChanged(); } } else if (removed.isEmpty() || removed.contains("icon-data")) { - this->image = nullptr; + imageChanged = this->image.hasData(); + image.data.clear(); } auto type = properties.value("type"); @@ -239,17 +243,13 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged(); if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); - if (this->iconName != originalIconName || this->image != originalImage) { - if (this->image != originalImage) { - delete originalImage; - } - + if (this->iconName != originalIconName || imageChanged) { emit this->iconChanged(); } qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText << ", enabled=" << this->mEnabled << ", visible=" << this->visible - << ", iconName=" << this->iconName << ", iconData=" << this->image + << ", iconName=" << this->iconName << ", iconData=" << &this->image << ", separator=" << this->mSeparator << ", toggleType=" << this->mButtonType << ", toggleState=" << this->mCheckState diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 35afa98e..1a8b399e 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -30,7 +30,17 @@ namespace qs::dbus::dbusmenu { using menu::QsMenuEntry; class DBusMenu; -class DBusMenuPngImage; +class DBusMenuItem; + +class DBusMenuPngImage: public QsIndexedImageHandle { +public: + explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} + + [[nodiscard]] bool hasData() const { return !data.isEmpty(); } + QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; + + QByteArray data; +}; ///! Menu item shared by an external program. /// Menu item shared by an external program via the @@ -93,7 +103,7 @@ private: bool visible = true; bool mSeparator = false; QString iconName; - DBusMenuPngImage* image = nullptr; + DBusMenuPngImage image; menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; Qt::CheckState mCheckState = Qt::Unchecked; bool displayChildren = false; @@ -156,17 +166,6 @@ private: QDebug operator<<(QDebug debug, DBusMenu* menu); -class DBusMenuPngImage: public QsImageHandle { -public: - explicit DBusMenuPngImage(QByteArray data, DBusMenuItem* parent) - : QsImageHandle(QQuickImageProvider::Image, parent) - , data(std::move(data)) {} - - QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; - - QByteArray data; -}; - class DBusMenuHandle; QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp index 37a37eb3..768299ed 100644 --- a/src/io/ipc.cpp +++ b/src/io/ipc.cpp @@ -1,9 +1,12 @@ #include "ipc.hpp" +#include #include #include #include #include +#include +#include namespace qs::io::ipc { @@ -14,6 +17,12 @@ const BoolIpcType BoolIpcType::INSTANCE {}; const DoubleIpcType DoubleIpcType::INSTANCE {}; const ColorIpcType ColorIpcType::INSTANCE {}; +void* IpcType::copyStorage(const void* data) const { + auto* storage = this->createStorage(); + memcpy(storage, data, this->size()); + return storage; +} + const IpcType* IpcType::ipcType(const QMetaType& metaType) { if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE; if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE; @@ -70,12 +79,18 @@ void IpcTypeSlot::replace(void* value) { this->storage = value; } +void IpcTypeSlot::replace(const QVariant& value) { + this->replace(this->mType->copyStorage(value.constData())); +} + const char* VoidIpcType::name() const { return "void"; } const char* VoidIpcType::genericArgumentName() const { return "void"; } +qsizetype VoidIpcType::size() const { return 0; } // string const char* StringIpcType::name() const { return "string"; } const char* StringIpcType::genericArgumentName() const { return "QString"; } +qsizetype StringIpcType::size() const { return sizeof(QString); } void* StringIpcType::fromString(const QString& string) const { return new QString(string); } QString StringIpcType::toString(void* slot) const { return *static_cast(slot); } void* StringIpcType::createStorage() const { return new QString(); } @@ -84,6 +99,7 @@ void StringIpcType::destroyStorage(void* slot) const { delete static_cast(slo // bool const char* BoolIpcType::name() const { return "bool"; } const char* BoolIpcType::genericArgumentName() const { return "bool"; } +qsizetype BoolIpcType::size() const { return sizeof(bool); } void* BoolIpcType::fromString(const QString& string) const { if (string == "true") return new bool(true); @@ -121,6 +138,7 @@ void BoolIpcType::destroyStorage(void* slot) const { delete static_cast(s // double const char* DoubleIpcType::name() const { return "real"; } const char* DoubleIpcType::genericArgumentName() const { return "double"; } +qsizetype DoubleIpcType::size() const { return sizeof(double); } void* DoubleIpcType::fromString(const QString& string) const { auto ok = false; @@ -139,6 +157,7 @@ void DoubleIpcType::destroyStorage(void* slot) const { delete static_castname % '(' % paramString % "): " % this->returnType; } +QString WirePropertyDefinition::toString() const { + return "property " % this->name % ": " % this->type; +} + QString WireTargetDefinition::toString() const { QString accum = "target " % this->name; @@ -174,6 +197,10 @@ QString WireTargetDefinition::toString() const { accum += "\n " % func.toString(); } + for (const auto& prop: this->properties) { + accum += "\n " % prop.toString(); + } + return accum; } diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index 924f045e..d2b865a2 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../ipc/ipc.hpp" @@ -21,10 +22,12 @@ public: [[nodiscard]] virtual const char* name() const = 0; [[nodiscard]] virtual const char* genericArgumentName() const = 0; + [[nodiscard]] virtual qsizetype size() const = 0; [[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; } [[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; } [[nodiscard]] virtual void* createStorage() const { return nullptr; } virtual void destroyStorage(void* /*slot*/) const {} + void* copyStorage(const void* data) const; static const IpcType* ipcType(const QMetaType& metaType); }; @@ -43,6 +46,7 @@ public: [[nodiscard]] QGenericReturnArgument asGenericReturnArgument(); void replace(void* value); + void replace(const QVariant& value); private: const IpcType* mType = nullptr; @@ -53,6 +57,7 @@ class VoidIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; static const VoidIpcType INSTANCE; }; @@ -61,6 +66,7 @@ class StringIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -73,6 +79,7 @@ class IntIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -85,6 +92,7 @@ class BoolIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -97,6 +105,7 @@ class DoubleIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -109,6 +118,7 @@ class ColorIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -127,13 +137,23 @@ struct WireFunctionDefinition { DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments); -struct WireTargetDefinition { +struct WirePropertyDefinition { QString name; - QVector functions; + QString type; [[nodiscard]] QString toString() const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions); +DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); + +struct WireTargetDefinition { + QString name; + QVector functions; + QVector properties; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); } // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index f2a9118a..7203a307 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -21,16 +21,17 @@ namespace qs::io::ipc::comm { struct NoCurrentGeneration: std::monostate {}; struct TargetNotFound: std::monostate {}; -struct FunctionNotFound: std::monostate {}; +struct EntryNotFound: std::monostate {}; using QueryResponse = std::variant< std::monostate, NoCurrentGeneration, TargetNotFound, - FunctionNotFound, + EntryNotFound, QVector, WireTargetDefinition, - WireFunctionDefinition>; + WireFunctionDefinition, + WirePropertyDefinition>; void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { auto resp = conn->responseStream(); @@ -44,16 +45,24 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { auto* handler = registry->findHandler(this->target); if (handler) { - if (this->function.isEmpty()) { + if (this->name.isEmpty()) { resp << handler->wireDef(); } else { - auto* func = handler->findFunction(this->function); + auto* func = handler->findFunction(this->name); if (func) { resp << func->wireDef(); - } else { - resp << FunctionNotFound(); + return; } + + auto* prop = handler->findProperty(this->name); + + if (prop) { + resp << prop->wireDef(); + return; + } + + resp << EntryNotFound(); } } else { resp << TargetNotFound(); @@ -64,8 +73,8 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { } } -int queryMetadata(IpcClient* client, const QString& target, const QString& function) { - client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function})); +int queryMetadata(IpcClient* client, const QString& target, const QString& name) { + client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .name = name})); QueryResponse slot; if (!client->waitForResponse(slot)) return -1; @@ -82,9 +91,11 @@ int queryMetadata(IpcClient* client, const QString& target, const QString& funct qCInfo(logBare).noquote() << std::get(slot).toString(); } else if (std::holds_alternative(slot)) { qCInfo(logBare).noquote() << std::get(slot).toString(); + } else if (std::holds_alternative(slot)) { + qCInfo(logBare).noquote() << std::get(slot).toString(); } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Target not found."; - } else if (std::holds_alternative(slot)) { + } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Function not found."; } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Not ready to accept queries yet."; @@ -119,7 +130,7 @@ using StringCallResponse = std::variant< std::monostate, NoCurrentGeneration, TargetNotFound, - FunctionNotFound, + EntryNotFound, ArgParseFailed, Completed>; @@ -137,7 +148,7 @@ void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const { auto* func = handler->findFunction(this->function); if (!func) { - resp << FunctionNotFound(); + resp << EntryNotFound(); return; } @@ -223,7 +234,7 @@ int callFunction( qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString(); } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Target not found."; - } else if (std::holds_alternative(slot)) { + } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Function not found."; } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Not ready to accept queries yet."; @@ -233,4 +244,74 @@ int callFunction( return -1; } + +struct PropertyValue { + QString value; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(PropertyValue, data.value); + +using StringPropReadResponse = + std::variant; + +void StringPropReadCommand::exec(qs::ipc::IpcServerConnection* conn) const { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + auto* handler = registry->findHandler(this->target); + if (!handler) { + resp << TargetNotFound(); + return; + } + + auto* prop = handler->findProperty(this->property); + if (!prop) { + resp << EntryNotFound(); + return; + } + + auto slot = IpcTypeSlot(prop->type); + prop->read(handler, slot); + + resp << PropertyValue { + .value = slot.type()->toString(slot.get()), + }; + } else { + conn->respond(StringCallResponse(NoCurrentGeneration())); + } +} + +int getProperty(IpcClient* client, const QString& target, const QString& property) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to send message."; + return -1; + } else if (property.isEmpty()) { + qCCritical(logBare) << "Property required to send message."; + return -1; + } + + client->sendMessage(IpcCommand(StringPropReadCommand {.target = target, .property = property})); + + StringPropReadResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + QTextStream(stdout) << result.value << Qt::endl; + return 0; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Property not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + + return -1; +} + } // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index 69463983..bc7dbf97 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "../ipc/ipc.hpp" @@ -9,12 +10,12 @@ namespace qs::io::ipc::comm { struct QueryMetadataCommand { QString target; - QString function; + QString name; void exec(qs::ipc::IpcServerConnection* conn) const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.function); +DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.name); struct StringCallCommand { QString target; @@ -27,7 +28,7 @@ struct StringCallCommand { DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments); void handleMsg(qs::ipc::IpcServerConnection* conn); -int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& function); +int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& name); int callFunction( qs::ipc::IpcClient* client, @@ -36,4 +37,15 @@ int callFunction( const QVector& arguments ); +struct StringPropReadCommand { + QString target; + QString property; + + void exec(qs::ipc::IpcServerConnection* conn) const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property); + +int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); + } // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 510b2055..517e4505 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -107,6 +107,32 @@ WireFunctionDefinition IpcFunction::wireDef() const { return wire; } +bool IpcProperty::resolve(QString& error) { + this->type = IpcType::ipcType(this->property.metaType()); + + if (!this->type) { + error = QString("Type %1 cannot be used across IPC.").arg(this->property.metaType().name()); + return false; + } + + return true; +} + +void IpcProperty::read(QObject* target, IpcTypeSlot& slot) const { + slot.replace(this->property.read(target)); +} + +QString IpcProperty::toString() const { + return QString("property ") % this->property.name() % ": " % this->type->name(); +} + +WirePropertyDefinition IpcProperty::wireDef() const { + WirePropertyDefinition wire; + wire.name = this->property.name(); + wire.type = this->type->name(); + return wire; +} + IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { for (const auto& arg: function.argumentTypes) { this->argumentSlots.emplace_back(arg); @@ -153,6 +179,21 @@ void IpcHandler::onPostReload() { } } + for (auto i = smeta.propertyCount(); i != meta->propertyCount(); i++) { + const auto& property = meta->property(i); + if (!property.isReadable() || !property.hasNotifySignal()) continue; + + auto ipcProp = IpcProperty(property); + QString error; + + if (!ipcProp.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing property \"" << property.name() << "\": " << error; + } else { + this->propertyMap.insert(property.name(), ipcProp); + } + } + this->complete = true; this->updateRegistration(); @@ -270,6 +311,10 @@ WireTargetDefinition IpcHandler::wireDef() const { wire.functions += func.wireDef(); } + for (const auto& prop: this->propertyMap.values()) { + wire.properties += prop.wireDef(); + } + return wire; } @@ -307,6 +352,13 @@ IpcFunction* IpcHandler::findFunction(const QString& name) { else return &*itr; } +IpcProperty* IpcHandler::findProperty(const QString& name) { + auto itr = this->propertyMap.find(name); + + if (itr == this->propertyMap.end()) return nullptr; + else return &*itr; +} + IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { return this->handlers.value(target); } diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index cc4ee5f4..e6b24ba1 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -53,14 +53,28 @@ private: friend class IpcFunction; }; +class IpcProperty { +public: + explicit IpcProperty(QMetaProperty property): property(property) {} + + bool resolve(QString& error); + void read(QObject* target, IpcTypeSlot& slot) const; + + [[nodiscard]] QString toString() const; + [[nodiscard]] WirePropertyDefinition wireDef() const; + + QMetaProperty property; + const IpcType* type = nullptr; +}; + class IpcHandlerRegistry; ///! Handler for IPC message calls. /// Each IpcHandler is registered into a per-instance map by its unique @@target. -/// Functions defined on the IpcHandler can be called by `qs msg`. +/// Functions and properties defined on the IpcHandler can be accessed via `qs ipc`. /// /// #### Handler Functions -/// IPC handler functions can be called by `qs msg` as long as they have at most 10 +/// IPC handler functions can be called by `qs ipc call` as long as they have at most 10 /// arguments, and all argument types along with the return type are listed below. /// /// **Argument and return types must be explicitly specified or they will not @@ -112,9 +126,9 @@ class IpcHandlerRegistry; /// } /// } /// ``` -/// The list of registered targets can be inspected using `qs msg -s`. +/// The list of registered targets can be inspected using `qs ipc show`. /// ```sh -/// $ qs msg -s +/// $ qs ipc show /// target rect /// function setColor(color: color): void /// function getColor(): color @@ -124,18 +138,22 @@ class IpcHandlerRegistry; /// function getRadius(): int /// ``` /// -/// and then invoked using `qs msg`. +/// and then invoked using `qs ipc call`. /// ```sh -/// $ qs msg rect setColor orange -/// $ qs msg rect setAngle 40.5 -/// $ qs msg rect setRadius 30 -/// $ qs msg rect getColor +/// $ qs ipc call rect setColor orange +/// $ qs ipc call rect setAngle 40.5 +/// $ qs ipc call rect setRadius 30 +/// $ qs ipc call rect getColor /// #ffffa500 -/// $ qs msg rect getAngle +/// $ qs ipc call rect getAngle /// 40.5 -/// $ qs msg rect getRadius +/// $ qs ipc call rect getRadius /// 30 /// ``` +/// +/// #### Properties +/// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are +/// of an IPC compatible type. See the table above for compatible types. class IpcHandler : public QObject , public PostReloadHook { @@ -162,12 +180,16 @@ public: QString listMembers(qsizetype indent); [[nodiscard]] IpcFunction* findFunction(const QString& name); + [[nodiscard]] IpcProperty* findProperty(const QString& name); [[nodiscard]] WireTargetDefinition wireDef() const; signals: void enabledChanged(); void targetChanged(); +private slots: + //void handleIpcPropertyChange(); + private: void updateRegistration(bool destroying = false); @@ -183,6 +205,7 @@ private: bool complete = false; QHash functionMap; + QHash propertyMap; friend class IpcHandlerRegistry; }; diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp index c2e5059f..b221b460 100644 --- a/src/ipc/ipccommand.hpp +++ b/src/ipc/ipccommand.hpp @@ -15,6 +15,7 @@ using IpcCommand = std::variant< std::monostate, IpcKillCommand, qs::io::ipc::comm::QueryMetadataCommand, - qs::io::ipc::comm::StringCallCommand>; + qs::io::ipc::comm::StringCallCommand, + qs::io::ipc::comm::StringPropReadCommand>; } // namespace qs::ipc diff --git a/src/launch/command.cpp b/src/launch/command.cpp index eb27df74..00ad613e 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -102,9 +102,10 @@ int locateConfigFile(CommandState& cmd, QString& path) { return 0; } -void sortInstances(QVector& list) { - std::ranges::sort(list, [](const InstanceLockInfo& a, const InstanceLockInfo& b) { - return a.instance.launchTime < b.instance.launchTime; +void sortInstances(QVector& list, bool newestFirst) { + std::ranges::sort(list, [=](const InstanceLockInfo& a, const InstanceLockInfo& b) { + auto r = a.instance.launchTime < b.instance.launchTime; + return newestFirst ? !r : r; }); }; @@ -153,7 +154,7 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { path = QDir(basePath->filePath("by-path")).filePath(pathId); auto instances = QsPaths::collectInstances(path); - sortInstances(instances); + sortInstances(instances, cmd.config.newest); if (instances.isEmpty()) { qCInfo(logBare) << "No running instances for" << configFilePath; @@ -227,7 +228,7 @@ int listInstances(CommandState& cmd) { qCInfo(logBare) << "Use --all to list all instances."; } } else { - sortInstances(instances); + sortInstances(instances, cmd.config.newest); if (cmd.output.json) { auto array = QJsonArray(); @@ -284,26 +285,23 @@ int killInstances(CommandState& cmd) { }); } -int msgInstance(CommandState& cmd) { +int ipcCommand(CommandState& cmd) { InstanceLockInfo instance; auto r = selectInstance(cmd, &instance); if (r != 0) return r; return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - if (cmd.ipc.info) { - return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + if (*cmd.ipc.show || cmd.ipc.showOld) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); + } else if (*cmd.ipc.getprop) { + return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { arguments += *arg; } - return qs::io::ipc::comm::callFunction( - &client, - *cmd.ipc.target, - *cmd.ipc.function, - arguments - ); + return qs::io::ipc::comm::callFunction(&client, *cmd.ipc.target, *cmd.ipc.name, arguments); } return -1; @@ -422,8 +420,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { return listInstances(state); } else if (*state.subcommand.kill) { return killInstances(state); - } else if (*state.subcommand.msg) { - return msgInstance(state); + } else if (*state.subcommand.msg || *state.ipc.ipc) { + return ipcCommand(state); } else { if (strcmp(qVersion(), QT_VERSION_STR) != 0) { qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index d752edbc..77808450 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -49,6 +49,7 @@ struct CommandState { QStringOption path; QStringOption manifest; QStringOption name; + bool newest = false; } config; struct { @@ -67,9 +68,13 @@ struct CommandState { } output; struct { - bool info = false; + CLI::App* ipc = nullptr; + CLI::App* show = nullptr; + CLI::App* call = nullptr; + CLI::App* getprop = nullptr; + bool showOld = false; QStringOption target; - QStringOption function; + QStringOption name; std::vector arguments; } ipc; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index bee9dd00..1edbf01e 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -16,7 +16,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { .argv = argv, }; - auto addConfigSelection = [&](CLI::App* cmd) { + auto addConfigSelection = [&](CLI::App* cmd, bool withNewestOption = false) { auto* group = cmd->add_option_group("Config Selection") ->description("If no options in this group are specified,\n" "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); @@ -37,6 +37,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") ->envname("QS_CONFIG_NAME"); + if (withNewestOption) { + group->add_flag("-n,--newest", state.config.newest) + ->description("Operate on the most recently launched instance instead of the oldest"); + } + return group; }; @@ -130,7 +135,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); auto* instance = addInstanceSelection(sub)->excludes(file); - addConfigSelection(sub)->excludes(instance)->excludes(file); + addConfigSelection(sub, true)->excludes(instance)->excludes(file); addLoggingOptions(sub, false); state.subcommand.log = sub; @@ -146,7 +151,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); - addConfigSelection(sub)->excludes(all); + addConfigSelection(sub, true)->excludes(all); addLoggingOptions(sub, false, true); state.subcommand.list = sub; @@ -156,36 +161,71 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* sub = cli->add_subcommand("kill", "Kill quickshell instances."); //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); + addConfigSelection(sub, true)->excludes(instance); addLoggingOptions(sub, false, true); state.subcommand.kill = sub; } { - auto* sub = cli->add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); - - auto* target = sub->add_option("target", state.ipc.target, "The target to message."); - - auto* function = sub->add_option("function", state.ipc.function) - ->description("The function to call in the target.") - ->needs(target); - - auto* arguments = sub->add_option("arguments", state.ipc.arguments) - ->description("Arguments to the called function.") - ->needs(function) - ->allow_extra_args(); - - sub->add_flag("-s,--show", state.ipc.info) - ->description("Print information about a function or target if given, or all available " - "targets if not.") - ->excludes(arguments); + auto* sub = cli->add_subcommand("ipc", "Communicate with other Quickshell instances.") + ->require_subcommand(); + state.ipc.ipc = sub; auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); + addConfigSelection(sub, true)->excludes(instance); addLoggingOptions(sub, false, true); - sub->require_option(); + { + auto* show = sub->add_subcommand("show", "Print information about available IPC targets."); + state.ipc.show = show; + } + + { + auto* call = sub->add_subcommand("call", "Call an IpcHandler function."); + state.ipc.call = call; + + call->add_option("target", state.ipc.target, "The target to message."); + + call->add_option("function", state.ipc.name) + ->description("The function to call in the target."); + + call->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->allow_extra_args(); + } + + { + auto* prop = + sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand(); + + { + auto* get = prop->add_subcommand("get", "Read the value of a property."); + state.ipc.getprop = get; + get->add_option("target", state.ipc.target, "The target to read the property of."); + get->add_option("property", state.ipc.name)->description("The property to read."); + } + } + } + + { + auto* sub = cli->add_subcommand("msg", "[DEPRECATED] Moved to `ipc call`.")->require_option(); + + sub->add_option("target", state.ipc.target, "The target to message."); + + sub->add_option("function", state.ipc.name)->description("The function to call in the target."); + + sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->allow_extra_args(); + + sub->add_flag("-s,--show", state.ipc.showOld) + ->description("Print information about a function or target if given, or all available " + "targets if not."); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub, true)->excludes(instance); + addLoggingOptions(sub, false, true); state.subcommand.msg = sub; } diff --git a/src/services/notifications/dbusimage.hpp b/src/services/notifications/dbusimage.hpp index d81d1e74..c310d95e 100644 --- a/src/services/notifications/dbusimage.hpp +++ b/src/services/notifications/dbusimage.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -23,14 +21,22 @@ struct DBusNotificationImage { const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap); const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap); -class NotificationImage: public QsImageHandle { +class NotificationImage: public QsIndexedImageHandle { public: - explicit NotificationImage(DBusNotificationImage image, QObject* parent) - : QsImageHandle(QQuickAsyncImageProvider::Image, parent) - , image(std::move(image)) {} + explicit NotificationImage(): QsIndexedImageHandle(QQuickAsyncImageProvider::Image) {} + + [[nodiscard]] bool hasData() const { return !this->image.data.isEmpty(); } + void clear() { this->image.data.clear(); } + + [[nodiscard]] DBusNotificationImage& writeImage() { + this->imageChanged(); + return this->image; + } QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; +private: DBusNotificationImage image; }; + } // namespace qs::service::notifications diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 51a64154..2703cf6d 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -1,5 +1,4 @@ #include "notification.hpp" -#include #include #include @@ -117,13 +116,12 @@ void Notification::updateProperties( QString imagePath; - if (!imageDataName.isEmpty()) { + if (imageDataName.isEmpty()) { + this->mImagePixmap.clear(); + } else { auto value = hints.value(imageDataName).value(); - DBusNotificationImage image; - value >> image; - if (this->mImagePixmap) this->mImagePixmap->deleteLater(); - this->mImagePixmap = new NotificationImage(std::move(image), this); - imagePath = this->mImagePixmap->url(); + value >> this->mImagePixmap.writeImage(); + imagePath = this->mImagePixmap.url(); } // don't store giant byte arrays longer than necessary @@ -131,7 +129,7 @@ void Notification::updateProperties( hints.remove("image_data"); hints.remove("icon_data"); - if (!this->mImagePixmap) { + if (!this->mImagePixmap.hasData()) { QString imagePathName; if (hints.contains("image-path")) imagePathName = "image-path"; else if (hints.contains("image_path")) imagePathName = "image_path"; diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index 25b9e330..85fe023d 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -12,11 +12,10 @@ #include "../../core/retainable.hpp" #include "../../core/util.hpp" +#include "dbusimage.hpp" namespace qs::service::notifications { -class NotificationImage; - ///! The urgency level of a Notification. /// See @@Notification.urgency. class NotificationUrgency: public QObject { @@ -187,7 +186,7 @@ private: quint32 mId; NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed; bool mLastGeneration = false; - NotificationImage* mImagePixmap = nullptr; + NotificationImage mImagePixmap; QList mActions; // clang-format off diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index d145c451..42847399 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -282,7 +282,7 @@ void StatusNotifierItem::onGetAllFailed() const { } TrayImageHandle::TrayImageHandle(StatusNotifierItem* item) - : QsImageHandle(QQmlImageProviderBase::Pixmap, item) + : QsImageHandle(QQmlImageProviderBase::Pixmap) , item(item) {} QPixmap diff --git a/src/wayland/hyprland/surface/hyprland-surface-v1.xml b/src/wayland/hyprland/surface/hyprland-surface-v1.xml index 2f683365..c4b1424f 100644 --- a/src/wayland/hyprland/surface/hyprland-surface-v1.xml +++ b/src/wayland/hyprland/surface/hyprland-surface-v1.xml @@ -34,7 +34,7 @@ This protocol exposes hyprland-specific wl_surface properties. - + This interface allows a client to create hyprland surface objects. @@ -63,7 +63,7 @@ - + This interface allows access to hyprland-specific properties of a wl_surface. @@ -96,5 +96,31 @@ + + + + This request sets the region of the surface that contains visible content. + Visible content refers to content that has an alpha value greater than zero. + + The visible region is an optimization hint for the compositor that lets it + avoid drawing parts of the surface that are not visible. Setting a visible region + that does not contain all content in the surface may result in missing content + not being drawn. + + The visible region is specified in buffer-local coordinates. + + The compositor ignores the parts of the visible region that fall outside of the surface. + When all parts of the region fall outside of the buffer geometry, the compositor may + avoid rendering the surface entirely. + + The initial value for the visible region is empty. Setting the + visible region has copy semantics, and the wl_region object can be destroyed immediately. + A NULL wl_region causes the visible region to be set to empty. + + Does not take effect until wl_surface.commit is called. + + + + diff --git a/src/wayland/hyprland/surface/manager.cpp b/src/wayland/hyprland/surface/manager.cpp index 31829bb6..6354255e 100644 --- a/src/wayland/hyprland/surface/manager.cpp +++ b/src/wayland/hyprland/surface/manager.cpp @@ -7,13 +7,13 @@ namespace qs::hyprland::surface::impl { -HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(1) { +HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(2) { this->initialize(); } HyprlandSurface* HyprlandSurfaceManager::createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface) { - return new HyprlandSurface(this->get_hyprland_surface(surface->surface())); + return new HyprlandSurface(this->get_hyprland_surface(surface->surface()), surface); } HyprlandSurfaceManager* HyprlandSurfaceManager::instance() { diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index 5150487f..b00ee33e 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -1,17 +1,20 @@ #include "qml.hpp" #include +#include #include #include #include #include +#include #include #include +#include #include +#include "../../../core/region.hpp" #include "../../../window/proxywindow.hpp" #include "../../../window/windowinterface.hpp" -#include "../../util.hpp" #include "manager.hpp" #include "surface.hpp" @@ -40,6 +43,15 @@ HyprlandWindow::HyprlandWindow(ProxyWindowBase* window): QObject(nullptr), proxy &HyprlandWindow::onWindowConnected ); + QObject::connect(window, &ProxyWindowBase::polished, this, &HyprlandWindow::onWindowPolished); + + QObject::connect( + window, + &ProxyWindowBase::devicePixelRatioChanged, + this, + &HyprlandWindow::updateVisibleMask + ); + QObject::connect(window, &QObject::destroyed, this, &HyprlandWindow::onProxyWindowDestroyed); if (window->backingWindow()) { @@ -60,14 +72,76 @@ void HyprlandWindow::setOpacity(qreal opacity) { this->mOpacity = opacity; - if (this->surface) { - this->surface->setOpacity(opacity); - qs::wayland::util::scheduleCommit(this->proxyWindow); + if (this->surface && this->proxyWindow) { + this->pendingPolish.opacity = true; + this->proxyWindow->schedulePolish(); } emit this->opacityChanged(); } +PendingRegion* HyprlandWindow::visibleMask() const { return this->mVisibleMask; } + +void HyprlandWindow::setVisibleMask(PendingRegion* mask) { + if (mask == this->mVisibleMask) return; + + if (this->mVisibleMask) { + QObject::disconnect(this->mVisibleMask, nullptr, this, nullptr); + } + + this->mVisibleMask = mask; + + if (mask) { + QObject::connect(mask, &QObject::destroyed, this, &HyprlandWindow::onVisibleMaskDestroyed); + QObject::connect(mask, &PendingRegion::changed, this, &HyprlandWindow::updateVisibleMask); + } + + this->updateVisibleMask(); + emit this->visibleMaskChanged(); +} + +void HyprlandWindow::onVisibleMaskDestroyed() { + this->mVisibleMask = nullptr; + this->updateVisibleMask(); + emit this->visibleMaskChanged(); +} + +void HyprlandWindow::updateVisibleMask() { + if (!this->surface || !this->proxyWindow) return; + + this->pendingPolish.visibleMask = true; + this->proxyWindow->schedulePolish(); +} + +void HyprlandWindow::onWindowPolished() { + if (!this->surface) return; + + if (this->pendingPolish.opacity) { + this->surface->setOpacity(this->mOpacity); + this->pendingPolish.opacity = false; + } + + if (this->pendingPolish.visibleMask) { + QRegion mask; + if (this->mVisibleMask != nullptr) { + mask = + this->mVisibleMask->applyTo(QRect(0, 0, this->mWindow->width(), this->mWindow->height())); + } + + auto dpr = this->proxyWindow->devicePixelRatio(); + if (dpr != 1.0) { + mask = QHighDpi::scale(mask, dpr); + } + + if (mask.isEmpty() && this->mVisibleMask) { + mask = QRect(-1, -1, 1, 1); + } + + this->surface->setVisibleRegion(mask); + this->pendingPolish.visibleMask = false; + } +} + void HyprlandWindow::onWindowConnected() { this->mWindow = this->proxyWindow->backingWindow(); // disconnected by destructor @@ -86,33 +160,46 @@ void HyprlandWindow::onWindowVisibleChanged() { if (!this->mWindow->handle()) { this->mWindow->create(); } + } - this->mWaylandWindow = dynamic_cast(this->mWindow->handle()); + auto* window = dynamic_cast(this->mWindow->handle()); + if (window == this->mWaylandWindow) return; - if (this->mWaylandWindow) { - // disconnected by destructor + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + } - QObject::connect( - this->mWaylandWindow, - &QWaylandWindow::surfaceCreated, - this, - &HyprlandWindow::onWaylandSurfaceCreated - ); + this->mWaylandWindow = window; + if (!window) return; - QObject::connect( - this->mWaylandWindow, - &QWaylandWindow::surfaceDestroyed, - this, - &HyprlandWindow::onWaylandSurfaceDestroyed - ); + QObject::connect( + this->mWaylandWindow, + &QObject::destroyed, + this, + &HyprlandWindow::onWaylandWindowDestroyed + ); - if (this->mWaylandWindow->surface()) { - this->onWaylandSurfaceCreated(); - } - } + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &HyprlandWindow::onWaylandSurfaceCreated + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &HyprlandWindow::onWaylandSurfaceDestroyed + ); + + if (this->mWaylandWindow->surface()) { + this->onWaylandSurfaceCreated(); } } +void HyprlandWindow::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + void HyprlandWindow::onWaylandSurfaceCreated() { auto* manager = impl::HyprlandSurfaceManager::instance(); @@ -122,12 +209,26 @@ void HyprlandWindow::onWaylandSurfaceCreated() { return; } - auto* ext = manager->createHyprlandExtension(this->mWaylandWindow); - this->surface = std::unique_ptr(ext); + auto v = this->mWaylandWindow->property("hyprland_window_ext"); + if (v.canConvert()) { + auto* windowExt = v.value(); + if (windowExt != this && windowExt->surface) { + this->surface.swap(windowExt->surface); + } + } - if (this->mOpacity != 1.0) { - this->surface->setOpacity(this->mOpacity); - qs::wayland::util::scheduleCommit(this->proxyWindow); + if (!this->surface) { + auto* ext = manager->createHyprlandExtension(this->mWaylandWindow); + this->surface = std::unique_ptr(ext); + } + + this->mWaylandWindow->setProperty("hyprland_window_ext", QVariant::fromValue(this)); + + this->pendingPolish.opacity = this->mOpacity != 1.0; + this->pendingPolish.visibleMask = this->mVisibleMask; + + if (this->pendingPolish.opacity || this->pendingPolish.visibleMask) { + this->proxyWindow->schedulePolish(); } } @@ -144,8 +245,9 @@ void HyprlandWindow::onProxyWindowDestroyed() { // Deleting it when the proxy window is deleted will cause a full opacity frame between the destruction of the // hyprland_surface_v1 and wl_surface objects. + this->proxyWindow = nullptr; + if (this->surface == nullptr) { - this->proxyWindow = nullptr; this->deleteLater(); } } diff --git a/src/wayland/hyprland/surface/qml.hpp b/src/wayland/hyprland/surface/qml.hpp index ce32a967..157b8f32 100644 --- a/src/wayland/hyprland/surface/qml.hpp +++ b/src/wayland/hyprland/surface/qml.hpp @@ -9,6 +9,7 @@ #include #include +#include "../../../core/region.hpp" #include "../../../window/proxywindow.hpp" #include "surface.hpp" @@ -31,11 +32,18 @@ namespace qs::hyprland::surface { /// [hyprland-surface-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-surface-v1.xml class HyprlandWindow: public QObject { Q_OBJECT; + // clang-format off /// A multiplier for the window's overall opacity, ranging from 1.0 to 0.0. Overall opacity includes the opacity of /// both the window content *and* visual effects such as blur that apply to it. /// /// Default: 1.0 Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged); + /// A hint to the compositor that only certain regions of the surface should be rendered. + /// This can be used to avoid rendering large empty regions of a window which can increase + /// performance, especially if the window is blurred. The mask should include all pixels + /// of the window that do not have an alpha value of 0. + Q_PROPERTY(PendingRegion* visibleMask READ visibleMask WRITE setVisibleMask NOTIFY visibleMaskChanged); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("HyprlandWindow can only be used as an attached object."); QML_ATTACHED(HyprlandWindow); @@ -48,17 +56,25 @@ public: [[nodiscard]] qreal opacity() const; void setOpacity(qreal opacity); + [[nodiscard]] PendingRegion* visibleMask() const; + virtual void setVisibleMask(PendingRegion* mask); + static HyprlandWindow* qmlAttachedProperties(QObject* object); signals: void opacityChanged(); + void visibleMaskChanged(); private slots: void onWindowConnected(); void onWindowVisibleChanged(); + void onWaylandWindowDestroyed(); void onWaylandSurfaceCreated(); void onWaylandSurfaceDestroyed(); void onProxyWindowDestroyed(); + void onVisibleMaskDestroyed(); + void onWindowPolished(); + void updateVisibleMask(); private: void disconnectWaylandWindow(); @@ -67,7 +83,13 @@ private: QWindow* mWindow = nullptr; QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + struct { + bool opacity : 1 = false; + bool visibleMask : 1 = false; + } pendingPolish; + qreal mOpacity = 1.0; + PendingRegion* mVisibleMask = nullptr; std::unique_ptr surface; }; diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp index d1aa24fb..487da40b 100644 --- a/src/wayland/hyprland/surface/surface.cpp +++ b/src/wayland/hyprland/surface/surface.cpp @@ -1,19 +1,53 @@ #include "surface.hpp" +#include +#include +#include +#include +#include #include #include +#include #include #include namespace qs::hyprland::surface::impl { -HyprlandSurface::HyprlandSurface(::hyprland_surface_v1* surface) - : QtWayland::hyprland_surface_v1(surface) {} +HyprlandSurface::HyprlandSurface( + ::hyprland_surface_v1* surface, + QtWaylandClient::QWaylandWindow* backer +) + : QtWayland::hyprland_surface_v1(surface) + , backer(backer) + , backerSurface(backer->surface()) {} HyprlandSurface::~HyprlandSurface() { this->destroy(); } +bool HyprlandSurface::surfaceEq(wl_surface* surface) const { + return surface == this->backerSurface; +} + void HyprlandSurface::setOpacity(qreal opacity) { this->set_opacity(wl_fixed_from_double(opacity)); } +void HyprlandSurface::setVisibleRegion(const QRegion& region) { + if (this->version() < HYPRLAND_SURFACE_V1_SET_VISIBLE_REGION_SINCE_VERSION) { + qWarning() << "Cannot set hyprland surface visible region: compositor does not support " + "hyprland_surface_v1.set_visible_region"; + return; + } + + if (region.isEmpty()) { + this->set_visible_region(nullptr); + } else { + static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance(); + auto* display = waylandIntegration->display(); + + auto* wlRegion = display->createRegion(region); + this->set_visible_region(wlRegion); + wl_region_destroy(wlRegion); // NOLINT(misc-include-cleaner) + } +} + } // namespace qs::hyprland::surface::impl diff --git a/src/wayland/hyprland/surface/surface.hpp b/src/wayland/hyprland/surface/surface.hpp index a27e50e3..1c8b5486 100644 --- a/src/wayland/hyprland/surface/surface.hpp +++ b/src/wayland/hyprland/surface/surface.hpp @@ -1,21 +1,31 @@ #pragma once +#include #include +#include #include #include #include #include +#include #include namespace qs::hyprland::surface::impl { class HyprlandSurface: public QtWayland::hyprland_surface_v1 { public: - explicit HyprlandSurface(::hyprland_surface_v1* surface); + explicit HyprlandSurface(::hyprland_surface_v1* surface, QtWaylandClient::QWaylandWindow* backer); ~HyprlandSurface() override; Q_DISABLE_COPY_MOVE(HyprlandSurface); + [[nodiscard]] bool surfaceEq(wl_surface* surface) const; + void setOpacity(qreal opacity); + void setVisibleRegion(const QRegion& region); + +private: + QtWaylandClient::QWaylandWindow* backer; + wl_surface* backerSurface = nullptr; }; } // namespace qs::hyprland::surface::impl diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 1a338522..a649603c 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -90,8 +90,8 @@ void WlrLayershell::setHeight(qint32 height) { } void WlrLayershell::setScreen(QuickshellScreenInfo* screen) { - this->ProxyWindowBase::setScreen(screen); this->ext->setUseWindowScreen(screen != nullptr); + this->ProxyWindowBase::setScreen(screen); } // NOLINTBEGIN diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index f75fc641..8ce80c20 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -200,9 +201,6 @@ void ProxyWindowBase::completeWindow() { if (this->mScreen != nullptr && this->window->screen() != this->mScreen) { if (this->window->isVisible()) this->window->setVisible(false); this->window->setScreen(this->mScreen); - } else if (this->mScreen == nullptr) { - this->mScreen = this->window->screen(); - QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); } this->setWidth(this->mWidth); @@ -327,39 +325,39 @@ void ProxyWindowBase::setHeight(qint32 height) { void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { auto* qscreen = screen == nullptr ? nullptr : screen->screen; - if (qscreen == this->mScreen) return; + auto newMScreen = this->mScreen != qscreen; - if (this->mScreen != nullptr) { + if (this->mScreen && newMScreen) { QObject::disconnect(this->mScreen, nullptr, this, nullptr); } - if (this->window == nullptr) { - emit this->screenChanged(); - } else { - auto reshow = this->isVisibleDirect(); - if (reshow) this->setVisibleDirect(false); - if (this->window != nullptr) this->window->setScreen(qscreen); - if (reshow) this->setVisibleDirect(true); + if (this->qscreen() != qscreen) { + this->mScreen = qscreen; + if (this->window == nullptr) { + emit this->screenChanged(); + } else if (qscreen) { + auto reshow = this->isVisibleDirect(); + if (reshow) this->setVisibleDirect(false); + if (this->window != nullptr) this->window->setScreen(qscreen); + if (reshow) this->setVisibleDirect(true); + } } - if (qscreen) this->mScreen = qscreen; - else this->mScreen = this->window->screen(); - - QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); + if (qscreen && newMScreen) { + QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); + } } void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } +QScreen* ProxyWindowBase::qscreen() const { + if (this->window) return this->window->screen(); + if (this->mScreen) return this->mScreen; + return QGuiApplication::primaryScreen(); +} + QuickshellScreenInfo* ProxyWindowBase::screen() const { - QScreen* qscreen = nullptr; - - if (this->window == nullptr) { - if (this->mScreen != nullptr) qscreen = this->mScreen; - } else { - qscreen = this->window->screen(); - } - - return QuickshellTracked::instance()->screenInfo(qscreen); + return QuickshellTracked::instance()->screenInfo(this->qscreen()); } QColor ProxyWindowBase::color() const { return this->mColor; } diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 6f02e05a..92a85f4f 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -100,7 +100,8 @@ public: [[nodiscard]] qreal devicePixelRatio() const; - [[nodiscard]] virtual QuickshellScreenInfo* screen() const; + [[nodiscard]] QScreen* qscreen() const; + [[nodiscard]] QuickshellScreenInfo* screen() const; virtual void setScreen(QuickshellScreenInfo* screen); [[nodiscard]] QColor color() const;