diff --git a/CMakeLists.txt b/CMakeLists.txt index 849aa6b3e5e..f11ce8c997e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -961,6 +961,7 @@ add_executable(mixxx-test src/test/readaheadmanager_test.cpp src/test/replaygaintest.cpp src/test/rescalertest.cpp + src/test/rgbcolor_test.cpp src/test/samplebuffertest.cpp src/test/sampleutiltest.cpp src/test/schemamanager_test.cpp diff --git a/src/mixxxapplication.cpp b/src/mixxxapplication.cpp index b167df2c31a..91cc0586c96 100644 --- a/src/mixxxapplication.cpp +++ b/src/mixxxapplication.cpp @@ -9,6 +9,7 @@ #include "soundio/soundmanagerutil.h" #include "track/track.h" #include "track/trackref.h" +#include "util/color/rgbcolor.h" #include "util/math.h" // When linking Qt statically on Windows we have to Q_IMPORT_PLUGIN all the @@ -67,6 +68,7 @@ void MixxxApplication::registerMetaTypes() { qRegisterMetaType("mixxx::ReplayGain"); qRegisterMetaType("mixxx::Bpm"); qRegisterMetaType("mixxx::Duration"); + qRegisterMetaType>("std::optional"); qRegisterMetaType("SoundDeviceId"); QMetaType::registerComparators(); } diff --git a/src/test/rgbcolor_test.cpp b/src/test/rgbcolor_test.cpp new file mode 100644 index 00000000000..92b6ff0a4d2 --- /dev/null +++ b/src/test/rgbcolor_test.cpp @@ -0,0 +1,65 @@ +#include "util/color/rgbcolor.h" + +#include + +#include "test/mixxxtest.h" + +namespace mixxx { + +TEST(RgbColorTest, OptionalRgbColorFromInvalidQColor) { + EXPECT_FALSE(RgbColor::optional(QColor())); + EXPECT_EQ(RgbColor::nullopt(), RgbColor::optional(QColor())); +} + +TEST(RgbColorTest, OptionalRgbColorFromQColorWithoutAlpha) { + EXPECT_TRUE(RgbColor::optional(QColor::fromRgb(0x000000))); + EXPECT_EQ(0x000000, *RgbColor::optional(QColor::fromRgb(0x000000))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgb(0xFF0000))); + EXPECT_EQ(RgbColor::optional(0xFF0000), RgbColor::optional(QColor::fromRgb(0xFF0000))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgb(0x00FF00))); + EXPECT_EQ(RgbColor::optional(0x00FF00), RgbColor::optional(QColor::fromRgb(0x00FF00))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgb(0x0000FF))); + EXPECT_EQ(RgbColor::optional(0x0000FF), RgbColor::optional(QColor::fromRgb(0x0000FF))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgb(0xFFFFFF))); + EXPECT_EQ(RgbColor::optional(0xFFFFFF), RgbColor::optional(QColor::fromRgb(0xFFFFFF))); +} + +TEST(RgbColorTest, OptionalRgbColorFromQColorWithAlpha) { + EXPECT_TRUE(RgbColor::optional(QColor::fromRgba(0xAA000000))); + EXPECT_EQ(RgbColor::optional(0x000000), RgbColor::optional(QColor::fromRgba(0xAA000000))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgba(0xAAFF0000))); + EXPECT_EQ(RgbColor::optional(0xFF0000), RgbColor::optional(QColor::fromRgba(0xAAFF0000))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgba(0xAA00FF00))); + EXPECT_EQ(RgbColor::optional(0x00FF00), RgbColor::optional(QColor::fromRgba(0xAA00FF00))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgba(0xAA0000FF))); + EXPECT_EQ(RgbColor::optional(0x0000FF), RgbColor::optional(QColor::fromRgba(0xAA0000FF))); + EXPECT_TRUE(RgbColor::optional(QColor::fromRgba(0xAAFFFFFF))); + EXPECT_EQ(RgbColor::optional(0xFFFFFF), RgbColor::optional(QColor::fromRgba(0xAAFFFFFF))); +} + +TEST(RgbColorTest,RgbColorToQColor) { + EXPECT_EQ(QColor::fromRgb(0x123456), toQColor(RgbColor(0x123456))); +} + +TEST(RgbColorTest, OptionalRgbColorToQColor) { + EXPECT_EQ(QColor(), toQColor(RgbColor::nullopt())); + EXPECT_EQ(QColor::fromRgba(0xAABBCCDD), + toQColor(RgbColor::nullopt(), QColor::fromRgba(0xAABBCCDD))); + EXPECT_EQ(QColor::fromRgb(0x123456), + toQColor(RgbColor::optional(QColor::fromRgba(0xAA123456)))); + EXPECT_EQ(QColor::fromRgb(0x123456), + toQColor(RgbColor::optional(QColor::fromRgba(0xAA123456)), QColor::fromRgba(0xAABBCCDD))); +} + +TEST(RgbColorTest, RgbColorToQVariant) { + EXPECT_EQ(QVariant(), toQVariant(RgbColor::nullopt())); + EXPECT_EQ(QVariant(0x123456), toQVariant(RgbColor(0x123456))); +} + +TEST(RgbColorTest, OptionalRgbColorToQVariant) { + EXPECT_EQ(QVariant(), toQVariant(RgbColor::nullopt())); + EXPECT_EQ(QVariant(0x123456), + toQVariant(RgbColor::optional(QColor::fromRgba(0xAA123456)))); +} + +} // namespace mixxx diff --git a/src/util/color/rgbcolor.h b/src/util/color/rgbcolor.h new file mode 100644 index 00000000000..f7f79b4367c --- /dev/null +++ b/src/util/color/rgbcolor.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include + +#include "util/assert.h" +#include "util/optional.h" + +namespace mixxx { + +// Type-safe wrapper for 24-bit RGB color codes without an alpha +// channel. Code values are implicitly validated upon construction. +// +// Apart from the assignment operator this type is immutable. +class RgbColor { + public: + // A pure 24-bit 0xxRRGGBB color code with 8-bit per channel + // without an an alpha channel. + // We are using a separate typedef, because QRgb implicitly + // includes an alpha channel whereas this type does not! + typedef quint32 code_t; + + static constexpr code_t validateCode(code_t code) { + return code & kRgbCodeMask; + } + static bool isValidCode(code_t code) { + return code == validateCode(code); + } + + // The default constructor is not available, because there is + // no common default value that fits all possible use cases! + RgbColor() = delete; + // Explicit conversion from code_t with implicit validation. + explicit constexpr RgbColor(code_t code) + : m_code(validateCode(code)) { + } + // Explicit conversion from QColor. + RgbColor(QColor anyColor, code_t codeIfInvalid) + : m_code(anyColorToCode(anyColor, codeIfInvalid)) { + } + + // Implicit conversion to a color code. + constexpr operator code_t() const { + return m_code; + } + + friend bool operator==(RgbColor lhs, RgbColor rhs) { + return lhs.m_code == rhs.m_code; + } + + typedef std::optional optional_t; + + static constexpr optional_t nullopt() { + return std::nullopt; + } + + // Overloaded conversion functions for conveniently creating + // std::optional. + static constexpr optional_t optional(code_t code) { + return optional(RgbColor(code)); + } + static constexpr optional_t optional(RgbColor color) { + return std::make_optional(color); + } + static optional_t optional(QColor color) { + if (color.isValid()) { + return optional(validateCode(color.rgb())); + } else { + return nullopt(); + } + } + static optional_t optional(const QVariant& varCode) { + if (varCode.isNull()) { + return nullopt(); + } else { + DEBUG_ASSERT(varCode.canConvert(QMetaType::UInt)); + bool ok = false; + const auto code = varCode.toUInt(&ok); + VERIFY_OR_DEBUG_ASSERT(ok) { + return nullopt(); + } + return optional(static_cast(code)); + } + } + + protected: + // Bitmask of valid codes = 0x00RRGGBB + static constexpr code_t kRgbCodeMask = 0x00FFFFFF; + + static code_t anyColorToCode(QColor anyColor, code_t codeIfInvalid) { + if (anyColor.isValid()) { + // Strip alpha channel bits! + return validateCode(anyColor.rgb()); + } else { + return codeIfInvalid; + } + } + + code_t m_code; +}; + +inline bool operator!=(RgbColor lhs, RgbColor rhs) { + return !(lhs == rhs); +} + +// Explicit conversion of both non-optional and optional +// RgbColor values to QColor as overloaded free functions. + +inline QColor toQColor(RgbColor color) { + return QColor::fromRgb(color); +} + +inline QColor toQColor(RgbColor::optional_t optional, QColor defaultColor = QColor()) { + if (optional) { + return toQColor(*optional); + } else { + return defaultColor; + } +} + +// Explicit conversion of both non-optional and optional +// RgbColor values to QVariant as overloaded free functions. + +inline QVariant toQVariant(RgbColor color) { + return QVariant(static_cast(color)); +} + +inline QVariant toQVariant(RgbColor::optional_t optional) { + if (optional) { + return toQVariant(*optional); + } else { + return QVariant(); + } +} + +// Debug output stream operators + +inline QDebug operator<<(QDebug dbg, RgbColor color) { + return dbg << toQColor(color); +} + +inline QDebug operator<<(QDebug dbg, RgbColor::optional_t optional) { + return dbg << toQColor(optional); +} + +} // namespace mixxx + +// Assumption: A primitive type wrapped into std::optional is +// still a primitive type. +Q_DECLARE_TYPEINFO(std::optional, Q_PRIMITIVE_TYPE); +Q_DECLARE_METATYPE(std::optional) diff --git a/src/util/optional.h b/src/util/optional.h new file mode 100644 index 00000000000..5767794f366 --- /dev/null +++ b/src/util/optional.h @@ -0,0 +1,24 @@ +#pragma once + +#if __has_include() || \ + (defined(__cpp_lib_optional) && __cpp_lib_optional >= 201606L) + +#include + +#else + +#include + +namespace std { + +using std::experimental::make_optional; +using std::experimental::nullopt; +using std::experimental::optional; + +// Workarounds for missing member functions: +// option::has_value() -> explicit operator bool() +// option::value() -> operator*() + +} // namespace std + +#endif