forked from quickshell/quickshell
		
	Compare commits
	
		
			4 commits
		
	
	
		
			master
			...
			color-quan
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a813a51a12 | |||
| fb343ab639 | |||
| d3b1a65911 | |||
| 9506c1bb62 | 
					 9 changed files with 558 additions and 30 deletions
				
			
		
							
								
								
									
										139
									
								
								CONTRIBUTING.md
									
										
									
									
									
								
							
							
						
						
									
										139
									
								
								CONTRIBUTING.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
									
								
							
							
						
						
									
										230
									
								
								src/core/colorquantizer.cpp
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										128
									
								
								src/core/colorquantizer.hpp
									
										
									
									
									
										Normal 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
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -29,5 +29,6 @@ headers = [
 | 
			
		|||
	"qsmenuanchor.hpp",
 | 
			
		||||
	"clock.hpp",
 | 
			
		||||
	"scriptmodel.hpp",
 | 
			
		||||
	"colorquantizer.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,6 +117,8 @@ void HyprlandMonitor::setActiveWorkspace(HyprlandWorkspace* workspace) {
 | 
			
		|||
	this->mActiveWorkspace = workspace;
 | 
			
		||||
 | 
			
		||||
	if (workspace != nullptr) {
 | 
			
		||||
		workspace->setMonitor(this);
 | 
			
		||||
 | 
			
		||||
		QObject::connect(
 | 
			
		||||
		    workspace,
 | 
			
		||||
		    &QObject::destroyed,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue