From b7eb562abc1c92be667e56a2eab18aedb57a06d8 Mon Sep 17 00:00:00 2001
From: kossLAN <kosslan@kosslan.dev>
Date: Mon, 27 Jan 2025 20:30:07 -0500
Subject: [PATCH] fix: address review requests

---
 src/core/colorquantizer.cpp | 184 +++++++++++++++++++++++-------------
 src/core/colorquantizer.hpp |  59 +++++++++---
 2 files changed, 162 insertions(+), 81 deletions(-)

diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp
index d08f87b4..b97cf676 100644
--- a/src/core/colorquantizer.cpp
+++ b/src/core/colorquantizer.cpp
@@ -1,32 +1,33 @@
 #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) {
-	mColors = QList<QColor>();
+	setAutoDelete(false);
+	colors = QList<QColor>();
 }
 
-void ColorQuantizerOperation::run() {
-	quantizeImage();
+void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
+	if (shouldCancel.loadAcquire()) return;
 
-	emit done(mColors);
-}
+	colors.clear();
 
-void ColorQuantizerOperation::quantizeImage() {
-	mColors.clear();
+	if (source->isEmpty()) return;
 
-	if (source->isEmpty()) {
-		return;
-	}
-
-	QImage image(source->toLocalFile());
+	auto image = QImage(source->toLocalFile());
 
 	if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
 		image = image.scaled(
@@ -38,48 +39,52 @@ void ColorQuantizerOperation::quantizeImage() {
 	}
 
 	if (image.isNull()) {
-		qWarning() << "Failed to load image from" << source;
+		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) {
-			QRgb pixel = image.pixel(x, y);
-			if (qAlpha(pixel) == 0) {
-				continue;
-			}
+	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));
 		}
 	}
 
-	QDateTime startTime = QDateTime::currentDateTime();
+	auto startTime = QDateTime::currentDateTime();
 
-	mColors = quantization(pixels, 0);
+	colors = quantization(pixels, 0);
 
-	QDateTime endTime = QDateTime::currentDateTime();
-	qint64 milliseconds = startTime.msecsTo(endTime);
+	auto endTime = QDateTime::currentDateTime();
+	auto milliseconds = startTime.msecsTo(endTime);
 	qDebug() << "Color Quantization took: " << milliseconds << "ms";
 }
 
-QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qreal depth) {
+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>();
-		}
+		if (rgbValues.isEmpty()) return QList<QColor>();
 
-		int totalR = 0;
-		int totalG = 0;
-		int totalB = 0;
+		auto totalR = 0;
+		auto totalG = 0;
+		auto totalB = 0;
+
+		for (const auto& color: rgbValues) {
+			if (shouldCancel.loadAcquire()) return QList<QColor>();
 
-		for (const QColor& color: rgbValues) {
 			totalR += color.red();
 			totalG += color.green();
 			totalB += color.blue();
 		}
 
-		QColor avgColor(
+		auto avgColor = QColor(
 		    qRound(totalR / static_cast<double>(rgbValues.size())),
 		    qRound(totalG / static_cast<double>(rgbValues.size())),
 		    qRound(totalB / static_cast<double>(rgbValues.size()))
@@ -88,17 +93,17 @@ QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qr
 		return QList<QColor>() << avgColor;
 	}
 
-	QString dominantChannel = findBiggestColorRange(rgbValues);
-	std::ranges::sort(rgbValues, [dominantChannel](const QColor& a, const QColor& b) {
+	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();
 	});
 
-	qsizetype mid = rgbValues.size() / 2;
+	auto mid = rgbValues.size() / 2;
 
-	QList<QColor> leftHalf = rgbValues.mid(0, mid);
-	QList<QColor> rightHalf = rgbValues.mid(mid + 1);
+	auto leftHalf = rgbValues.mid(0, mid);
+	auto rightHalf = rgbValues.mid(mid);
 
 	QList<QColor> result;
 	result.append(quantization(leftHalf, depth + 1));
@@ -107,17 +112,17 @@ QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qr
 	return result;
 }
 
-QString ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
-	if (rgbValues.isEmpty()) return "r";
+QChar ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) {
+	if (rgbValues.isEmpty()) return 'r';
 
-	int rMin = 255;
-	int gMin = 255;
-	int bMin = 255;
-	int rMax = 0;
-	int gMax = 0;
-	int bMax = 0;
+	auto rMin = 255;
+	auto gMin = 255;
+	auto bMin = 255;
+	auto rMax = 0;
+	auto gMax = 0;
+	auto bMax = 0;
 
-	for (const QColor& color: rgbValues) {
+	for (const auto& color: rgbValues) {
 		rMin = qMin(rMin, color.red());
 		gMin = qMin(gMin, color.green());
 		bMin = qMin(bMin, color.blue());
@@ -127,56 +132,101 @@ QString ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbV
 		bMax = qMax(bMax, color.blue());
 	}
 
-	int rRange = rMax - rMin;
-	int gRange = gMax - gMin;
-	int bRange = bMax - bMin;
+	auto rRange = rMax - rMin;
+	auto gRange = gMax - gMin;
+	auto bRange = bMax - bMin;
 
-	int biggestRange = qMax(rRange, qMax(gRange, bRange));
+	auto biggestRange = qMax(rRange, qMax(gRange, bRange));
 	if (biggestRange == rRange) {
-		return "r";
+		return 'r';
 	} else if (biggestRange == gRange) {
-		return "g";
+		return 'g';
 	} else {
-		return "b";
+		return 'b';
 	}
 }
 
-QList<QColor> ColorQuantizer::colors() { return mColors; }
+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 sourceChanged();
-		quantizeAsync();
+		emit this->sourceChanged();
+
+		if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
 	}
 }
 
 void ColorQuantizer::setDepth(qreal depth) {
 	if (mDepth != depth) {
 		mDepth = depth;
-		emit depthChanged();
+		emit this->depthChanged();
+
+		if (this->componentCompleted) quantizeAsync();
 	}
 }
 
 void ColorQuantizer::setRescaleSize(int rescaleSize) {
 	if (mRescaleSize != rescaleSize) {
 		mRescaleSize = rescaleSize;
-		emit rescaleSizeChanged();
+		emit this->rescaleSizeChanged();
+
+		if (this->componentCompleted) quantizeAsync();
 	}
 }
 
 void ColorQuantizer::operationFinished(const QList<QColor>& result) {
-	mColors = result;
-	emit colorsChanged();
-	isProcessing = false;
+	bColors = result;
+	emit this->colorsChanged();
+	this->liveOperation = nullptr;
 }
 
 void ColorQuantizer::quantizeAsync() {
-	if (isProcessing) return;
+	if (this->liveOperation) this->cancelAsync();
 
-	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);
+	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;
 }
diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp
index 1c0435de..0e9461a1 100644
--- a/src/core/colorquantizer.hpp
+++ b/src/core/colorquantizer.hpp
@@ -1,9 +1,10 @@
 #pragma once
 
 #include <qlist.h>
-#include <qmutex.h>
 #include <qobject.h>
+#include <qproperty.h>
 #include <qqmlintegration.h>
+#include <qqmlparserstatus.h>
 #include <qrunnable.h>
 #include <qtmetamacros.h>
 #include <qtypes.h>
@@ -14,22 +15,33 @@ class ColorQuantizerOperation
 	Q_OBJECT;
 
 public:
-	ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
+	explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
 
 	void run() override;
+	void tryCancel();
 
 signals:
 	void done(QList<QColor> colors);
 
+private slots:
+	void finished();
+
 private:
-	QList<QColor> mColors;
+	static QChar 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;
-
-	void quantizeImage();
-	QList<QColor> quantization(QList<QColor>& rgbValues, qreal depth);
-	static QString findBiggestColorRange(const QList<QColor>& rgbValues);
 };
 
 ///! Color Quantization Utility
@@ -45,13 +57,17 @@ private:
 ///   rescaleSize: 64 // Rescale to 64x64 for faster processing
 /// }
 /// ```
+class ColorQuantizer
+    : public QObject
+    , public QQmlParserStatus {
 
-class ColorQuantizer: public QObject {
 	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 colors NOTIFY colorsChanged);
+	Q_PROPERTY(QList<QColor> colors READ default 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);
@@ -64,14 +80,21 @@ class ColorQuantizer: public QObject {
 	/// > [!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<QColor> colors();
+	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);
 
@@ -85,11 +108,19 @@ public slots:
 	void operationFinished(const QList<QColor>& result);
 
 private:
-	bool isProcessing = false;
-	QList<QColor> mColors;
+	void quantizeAsync();
+	void cancelAsync();
+
+	bool componentCompleted = false;
+	ColorQuantizerOperation* liveOperation = nullptr;
 	QUrl mSource;
 	qreal mDepth = 0;
 	qreal mRescaleSize = 0;
 
-	void quantizeAsync();
+	Q_OBJECT_BINDABLE_PROPERTY(
+	    ColorQuantizer,
+	    QList<QColor>,
+	    bColors,
+	    &ColorQuantizer::colorsChanged
+	);
 };