1
0
Fork 0

Compare commits

...

3 commits

Author SHA1 Message Date
kossLAN a813a51a12
core/colorquant: add ColorQuantizer 2025-01-28 18:39:22 -08:00
outfoxxed fb343ab639
hyprland/ipc: prefer ID based workspace lookups to name based ones
Should (hopefully) reduce race condition issues.
2025-01-27 22:19:28 -08:00
outfoxxed d3b1a65911
hyprland/ipc: reduce impact of racing workspace queries 2025-01-27 21:13:53 -08:00
8 changed files with 422 additions and 27 deletions

View file

@ -37,6 +37,7 @@ qt_add_library(quickshell-core STATIC
common.cpp
iconprovider.cpp
scriptmodel.cpp
colorquantizer.cpp
)
qt_add_qml_module(quickshell-core

230
src/core/colorquantizer.cpp Normal file
View file

@ -0,0 +1,230 @@
#include "colorquantizer.hpp"
#include <qcolor.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qthreadpool.h>
#include <qtypes.h>
namespace {
Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
}
ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
: source(source)
, maxDepth(depth)
, rescaleSize(rescaleSize) {
setAutoDelete(false);
}
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
colors.clear();
auto image = QImage(source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
image = image.scaled(
static_cast<int>(rescaleSize),
static_cast<int>(rescaleSize),
Qt::KeepAspectRatio,
Qt::SmoothTransformation
);
}
if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source;
return;
}
QList<QColor> pixels;
for (int y = 0; y != image.height(); ++y) {
for (int x = 0; x != image.width(); ++x) {
auto pixel = image.pixel(x, y);
if (qAlpha(pixel) == 0) continue;
pixels.append(QColor::fromRgb(pixel));
}
}
auto startTime = QDateTime::currentDateTime();
colors = quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime);
qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms";
}
QList<QColor> ColorQuantizerOperation::quantization(
QList<QColor>& rgbValues,
qreal depth,
const QAtomicInteger<bool>& shouldCancel
) {
if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0;
auto totalG = 0;
auto totalB = 0;
for (const auto& color: rgbValues) {
if (shouldCancel.loadAcquire()) return QList<QColor>();
totalR += color.red();
totalG += color.green();
totalB += color.blue();
}
auto avgColor = QColor(
qRound(totalR / static_cast<double>(rgbValues.size())),
qRound(totalG / static_cast<double>(rgbValues.size())),
qRound(totalB / static_cast<double>(rgbValues.size()))
);
return QList<QColor>() << avgColor;
}
auto dominantChannel = findBiggestColorRange(rgbValues);
std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) {
if (dominantChannel == 'r') return a.red() < b.red();
else if (dominantChannel == 'g') return a.green() < b.green();
return a.blue() < b.blue();
});
auto mid = rgbValues.size() / 2;
auto leftHalf = rgbValues.mid(0, mid);
auto rightHalf = rgbValues.mid(mid);
QList<QColor> result;
result.append(quantization(leftHalf, depth + 1));
result.append(quantization(rightHalf, depth + 1));
return result;
}
char ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
if (rgbValues.isEmpty()) return 'r';
auto rMin = 255;
auto gMin = 255;
auto bMin = 255;
auto rMax = 0;
auto gMax = 0;
auto bMax = 0;
for (const auto& 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());
}
auto rRange = rMax - rMin;
auto gRange = gMax - gMin;
auto bRange = bMax - bMin;
auto biggestRange = qMax(rRange, qMax(gRange, bRange));
if (biggestRange == rRange) {
return 'r';
} else if (biggestRange == gRange) {
return 'g';
} else {
return 'b';
}
}
void ColorQuantizerOperation::finishRun() {
QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection);
}
void ColorQuantizerOperation::finished() {
emit this->done(colors);
delete this;
}
void ColorQuantizerOperation::run() {
if (!this->shouldCancel) {
this->quantizeImage();
if (this->shouldCancel.loadAcquire()) {
qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled";
}
}
this->finishRun();
}
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() {
componentCompleted = true;
if (!mSource.isEmpty()) quantizeAsync();
}
void ColorQuantizer::setSource(const QUrl& source) {
if (mSource != source) {
mSource = source;
emit this->sourceChanged();
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
}
}
void ColorQuantizer::setDepth(qreal depth) {
if (mDepth != depth) {
mDepth = depth;
emit this->depthChanged();
if (this->componentCompleted) quantizeAsync();
}
}
void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (mRescaleSize != rescaleSize) {
mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged();
if (this->componentCompleted) quantizeAsync();
}
}
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
bColors = result;
this->liveOperation = nullptr;
emit this->colorsChanged();
}
void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
QObject::connect(
this->liveOperation,
&ColorQuantizerOperation::done,
this,
&ColorQuantizer::operationFinished
);
QThreadPool::globalInstance()->start(this->liveOperation);
}
void ColorQuantizer::cancelAsync() {
if (!this->liveOperation) return;
this->liveOperation->tryCancel();
QThreadPool::globalInstance()->waitForDone();
QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
this->liveOperation = nullptr;
}

128
src/core/colorquantizer.hpp Normal file
View file

@ -0,0 +1,128 @@
#pragma once
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qqmlparserstatus.h>
#include <qrunnable.h>
#include <qtmetamacros.h>
#include <qtypes.h>
class ColorQuantizerOperation
: public QObject
, public QRunnable {
Q_OBJECT;
public:
explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
void run() override;
void tryCancel();
signals:
void done(QList<QColor> colors);
private slots:
void finished();
private:
static char findBiggestColorRange(const QList<QColor>& rgbValues);
void quantizeImage(const QAtomicInteger<bool>& shouldCancel = false);
QList<QColor> quantization(
QList<QColor>& rgbValues,
qreal depth,
const QAtomicInteger<bool>& shouldCancel = false
);
void finishRun();
QAtomicInteger<bool> shouldCancel = false;
QList<QColor> colors;
QUrl* source;
qreal maxDepth;
qreal rescaleSize;
};
///! 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
, public QQmlParserStatus {
Q_OBJECT;
QML_ELEMENT;
Q_INTERFACES(QQmlParserStatus);
/// 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<QColor> colors READ default NOTIFY colorsChanged BINDABLE bindableColors);
/// 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);
public:
explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {}
void componentComplete() override;
void classBegin() override {}
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[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<QColor>& result);
private:
void quantizeAsync();
void cancelAsync();
bool componentCompleted = false;
ColorQuantizerOperation* liveOperation = nullptr;
QUrl mSource;
qreal mDepth = 0;
qreal mRescaleSize = 0;
Q_OBJECT_BINDABLE_PROPERTY(
ColorQuantizer,
QList<QColor>,
bColors,
&ColorQuantizer::colorsChanged
);
};

View file

@ -29,5 +29,6 @@ headers = [
"qsmenuanchor.hpp",
"clock.hpp",
"scriptmodel.hpp",
"colorquantizer.hpp",
]
-----

View file

@ -333,6 +333,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
auto* monitor = this->findMonitorByName(name, true);
this->setFocusedMonitor(monitor);
monitor->setActiveWorkspace(workspace);
qCDebug(logHyprlandIpc) << "Monitor" << name << "focused with workspace" << workspace->id();
} else if (event->name == "workspacev2") {
auto args = event->parseView(2);
auto id = args.at(0).toInt();
@ -341,6 +342,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
if (this->mFocusedMonitor != nullptr) {
auto* workspace = this->findWorkspaceByName(name, true, id);
this->mFocusedMonitor->setActiveWorkspace(workspace);
qCDebug(logHyprlandIpc) << "Workspace" << id << "activated on"
<< this->mFocusedMonitor->name();
}
} else if (event->name == "moveworkspacev2") {
auto args = event->parseView(3);
@ -351,6 +354,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
auto* workspace = this->findWorkspaceByName(name, true, id);
auto* monitor = this->findMonitorByName(monitorName, true);
qCDebug(logHyprlandIpc) << "Workspace" << id << "moved to monitor" << monitorName;
workspace->setMonitor(monitor);
} else if (event->name == "renameworkspace") {
auto args = event->parseView(2);
@ -374,15 +378,28 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
HyprlandWorkspace*
HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) {
const auto& mList = this->mWorkspaces.valueList();
HyprlandWorkspace* workspace = nullptr;
auto workspaceIter =
std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) { return m->name() == name; });
if (id != -1) {
auto workspaceIter =
std::ranges::find_if(mList, [&](const HyprlandWorkspace* m) { return m->id() == id; });
if (workspaceIter != mList.end()) {
return *workspaceIter;
workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter;
}
if (!workspace) {
auto workspaceIter =
std::ranges::find_if(mList, [&](const HyprlandWorkspace* m) { return m->name() == name; });
workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter;
}
if (workspace) {
return workspace;
} else if (createIfMissing) {
qCDebug(logHyprlandIpc) << "Workspace" << name
<< "requested before creation, performing early init";
<< "requested before creation, performing early init with id" << id;
auto* workspace = new HyprlandWorkspace(this);
workspace->updateInitial(id, name);
this->mWorkspaces.insertObject(workspace);
@ -400,24 +417,34 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) {
this->requestingWorkspaces = false;
if (!success) return;
qCDebug(logHyprlandIpc) << "parsing workspaces response";
qCDebug(logHyprlandIpc) << "Parsing workspaces response";
auto json = QJsonDocument::fromJson(resp).array();
const auto& mList = this->mWorkspaces.valueList();
auto names = QVector<QString>();
auto ids = QVector<quint32>();
for (auto entry: json) {
auto object = entry.toObject().toVariantMap();
auto name = object.value("name").toString();
auto workspaceIter = std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) {
return m->name() == name;
});
auto id = object.value("id").toInt();
auto workspaceIter =
std::ranges::find_if(mList, [&](const HyprlandWorkspace* m) { return m->id() == id; });
// Only fall back to name-based filtering as a last resort, for workspaces where
// no ID has been determined yet.
if (workspaceIter == mList.end()) {
auto name = object.value("name").toString();
workspaceIter = std::ranges::find_if(mList, [&](const HyprlandWorkspace* m) {
return m->id() == -1 && m->name() == name;
});
}
auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter;
auto existed = workspace != nullptr;
if (workspace == nullptr) {
if (!existed) {
if (!canCreate) continue;
workspace = new HyprlandWorkspace(this);
}
@ -428,20 +455,22 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) {
this->mWorkspaces.insertObject(workspace);
}
names.push_back(name);
ids.push_back(id);
}
auto removedWorkspaces = QVector<HyprlandWorkspace*>();
if (canCreate) {
auto removedWorkspaces = QVector<HyprlandWorkspace*>();
for (auto* workspace: mList) {
if (!names.contains(workspace->name())) {
removedWorkspaces.push_back(workspace);
for (auto* workspace: mList) {
if (!ids.contains(workspace->id())) {
removedWorkspaces.push_back(workspace);
}
}
}
for (auto* workspace: removedWorkspaces) {
this->mWorkspaces.removeObject(workspace);
delete workspace;
for (auto* workspace: removedWorkspaces) {
this->mWorkspaces.removeObject(workspace);
delete workspace;
}
}
});
}

View file

@ -81,7 +81,7 @@ public:
[[nodiscard]] ObjectModel<HyprlandWorkspace>* workspaces();
// No byId because these preemptively create objects. The given id is set if created.
HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = 0);
HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = -1);
HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1);
// canCreate avoids making ghost workspaces when the connection races

View file

@ -117,6 +117,8 @@ void HyprlandMonitor::setActiveWorkspace(HyprlandWorkspace* workspace) {
this->mActiveWorkspace = workspace;
if (workspace != nullptr) {
workspace->setMonitor(this);
QObject::connect(
workspace,
&QObject::destroyed,

View file

@ -35,18 +35,22 @@ void HyprlandWorkspace::updateInitial(qint32 id, QString name) {
}
void HyprlandWorkspace::updateFromObject(QVariantMap object) {
auto id = object.value("id").value<qint32>();
auto name = object.value("name").value<QString>();
auto monitorId = object.value("monitorID").value<qint32>();
auto monitorName = object.value("monitor").value<QString>();
if (id != this->mId) {
this->mId = id;
auto initial = this->mId = -1;
// ID cannot be updated after creation
if (initial) {
this->mId = object.value("id").value<qint32>();
emit this->idChanged();
}
if (name != this->mName) {
this->mName = std::move(name);
// No events we currently handle give a workspace id but not a name,
// so we shouldn't set this if it isn't an initial query
if (initial && name != this->mName) {
this->mName = name;
emit this->nameChanged();
}