1
0
Fork 0

Compare commits

...

4 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
outfoxxed 9506c1bb62
docs: update CONTRIBUTING style guide 2025-01-26 18:37:53 -08:00
9 changed files with 558 additions and 30 deletions

View file

@ -30,6 +30,139 @@ If the results look stupid, fix the clang-format file if possible,
or disable clang-format in the affected area
using `// clang-format off` and `// clang-format on`.
#### Style preferences not caught by clang-format
These are flexible. You can ignore them if it looks or works better to
for one reason or another.
Use `auto` if the type of a variable can be deduced automatically, instead of
redeclaring the returned value's type. Additionally, auto should be used when a
constructor takes arguments.
```cpp
auto x = <expr>; // ok
auto x = QString::number(3); // ok
QString x; // ok
QString x = "foo"; // ok
auto x = QString("foo"); // ok
auto x = QString(); // avoid
QString x(); // avoid
QString x("foo"); // avoid
```
Put newlines around logical units of code, and after closing braces. If the
most reasonable logical unit of code takes only a single line, it should be
merged into the next single line logical unit if applicable.
```cpp
// multiple units
auto x = <expr>; // unit 1
auto y = <expr>; // unit 2
auto x = <expr>; // unit 1
emit this->y(); // unit 2
auto x1 = <expr>; // unit 1
auto x2 = <expr>; // unit 1
auto x3 = <expr>; // unit 1
auto y1 = <expr>; // unit 2
auto y2 = <expr>; // unit 2
auto y3 = <expr>; // unit 2
// one unit
auto x = <expr>;
if (x...) {
// ...
}
// if more than one variable needs to be used then add a newline
auto x = <expr>;
auto y = <expr>;
if (x && y) {
// ...
}
```
Class formatting:
```cpp
//! Doc comment summary
/// Doc comment body
class Foo: public QObject {
// The Q_OBJECT macro comes first. Macros are ; terminated.
Q_OBJECT;
QML_ELEMENT;
QML_CLASSINFO(...);
// Properties must stay on a single line or the doc generator won't be able to pick them up
Q_PROPERTY(...);
/// Doc comment
Q_PROPERTY(...);
/// Doc comment
Q_PROPERTY(...);
public:
// Classes should have explicit constructors if they aren't intended to
// implicitly cast. The constructor can be inline in the header if it has no body.
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
// Instance functions if applicable.
static Foo* instance();
// Member functions unrelated to properties come next
void function();
void function();
void function();
// Then Q_INVOKABLEs
Q_INVOKABLE function();
/// Doc comment
Q_INVOKABLE function();
/// Doc comment
Q_INVOKABLE function();
// Then property related functions, in the order (bindable, getter, setter).
// Related functions may be included here as well. Function bodies may be inline
// if they are a single expression. There should be a newline between each
// property's methods.
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
[[nodiscard]] T foo() const { return this->foo; }
void setFoo();
[[nodiscard]] T bar() const { return this->foo; }
void setBar();
signals:
// Signals that are not property change related go first.
// Property change signals go in property definition order.
void asd();
void asd2();
void fooChanged();
void barChanged();
public slots:
// generally Q_INVOKABLEs are preferred to public slots.
void slot();
private slots:
// ...
private:
// statics, then functions, then fields
static const foo BAR;
static void foo();
void foo();
void bar();
// property related members are prefixed with `m`.
QString mFoo;
QString bar;
// Bindables go last and should be prefixed with `b`.
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
};
```
### Linter
All contributions should pass the linter.
@ -37,11 +170,11 @@ Note that running the linter requires disabling precompiled
headers and including the test codepaths:
```sh
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
$ just lint
$ just lint-changed
```
If the linter is complaining about something that you think it should not,
please disable the lint in your MR and explain your reasoning.
please disable the lint in your MR and explain your reasoning if it isn't obvious.
### Tests
If you feel like the feature you are working on is very complex or likely to break,
@ -77,7 +210,7 @@ and list of headers to scan for documentation.
### Commits
Please structure your commit messages as `scope[!]: commit` where
the scope is something like `core` or `service/mpris`. (pick what has been
used historically or what makes sense if new.) Add `!` for changes that break
used historically or what makes sense if new). Add `!` for changes that break
existing APIs or functionality.
Commit descriptions should contain a summary of the changes if they are not

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