-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow assigning more than one Hotcue to a Color in a Colorpalette #2902
Changes from all commits
8ffb21e
8d53dcd
68b2115
02de7df
20df43c
4e80be1
15e76e8
f2d9a39
c2f5668
bc50748
08c842f
7c5b0de
ffaa010
14b7560
d989d73
f1aa399
b549298
cd95da5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,14 @@ | ||
#include "preferences/colorpaletteeditormodel.h" | ||
|
||
#include <util/assert.h> | ||
#include <util/rangelist.h> | ||
|
||
#include <QList> | ||
#include <QMap> | ||
#include <QMultiMap> | ||
#include <algorithm> | ||
|
||
#include "engine/controls/cuecontrol.h" | ||
#include "moc_colorpaletteeditormodel.cpp" | ||
|
||
namespace { | ||
|
@@ -10,6 +19,13 @@ QIcon toQIcon(const QColor& color) { | |
return QIcon(pixmap); | ||
} | ||
|
||
HotcueIndexListItem* toHotcueIndexListItem(QStandardItem* pFrom) { | ||
VERIFY_OR_DEBUG_ASSERT(pFrom->type() == QStandardItem::UserType) { | ||
return nullptr; | ||
} | ||
return static_cast<HotcueIndexListItem*>(pFrom); | ||
} | ||
|
||
} // namespace | ||
|
||
ColorPaletteEditorModel::ColorPaletteEditorModel(QObject* parent) | ||
|
@@ -53,25 +69,39 @@ bool ColorPaletteEditorModel::dropMimeData(const QMimeData* data, Qt::DropAction | |
bool ColorPaletteEditorModel::setData(const QModelIndex& modelIndex, const QVariant& value, int role) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still confused when setData on the model and when setData on the Item is called... |
||
setDirty(true); | ||
if (modelIndex.isValid() && modelIndex.column() == 1) { | ||
bool ok; | ||
int hotcueIndex = value.toInt(&ok); | ||
const bool initialAttemptSuccessful = QStandardItemModel::setData(modelIndex, value, role); | ||
|
||
// Make sure that the value is valid | ||
if (!ok || hotcueIndex <= 0 || hotcueIndex > rowCount()) { | ||
return QStandardItemModel::setData(modelIndex, QVariant(), role); | ||
const auto* pHotcueIndexListItem = toHotcueIndexListItem(itemFromIndex(modelIndex)); | ||
VERIFY_OR_DEBUG_ASSERT(pHotcueIndexListItem) { | ||
return false; | ||
} | ||
|
||
// Make sure there is no other row with the same hotcue index | ||
for (int i = 0; i < rowCount(); i++) { | ||
QModelIndex otherModelIndex = index(i, 1); | ||
QVariant otherValue = data(otherModelIndex); | ||
int otherHotcueIndex = otherValue.toInt(&ok); | ||
if (ok && otherHotcueIndex == hotcueIndex) { | ||
QStandardItemModel::setData(otherModelIndex, QVariant(), role); | ||
auto hotcueIndexList = pHotcueIndexListItem->getHotcueIndexList(); | ||
|
||
// make sure no index is outside of range | ||
DEBUG_ASSERT(std::is_sorted(hotcueIndexList.cbegin(), hotcueIndexList.cend())); | ||
auto endUpper = std::upper_bound( | ||
hotcueIndexList.begin(), hotcueIndexList.end(), NUM_HOT_CUES); | ||
hotcueIndexList.erase(endUpper, hotcueIndexList.end()); | ||
auto endLower = std::upper_bound(hotcueIndexList.begin(), hotcueIndexList.end(), 0); | ||
hotcueIndexList.erase(hotcueIndexList.begin(), endLower); | ||
|
||
for (int i = 0; i < rowCount(); ++i) { | ||
auto* pHotcueIndexListItem = toHotcueIndexListItem(item(i, 1)); | ||
|
||
if (pHotcueIndexListItem == nullptr) { | ||
continue; | ||
} | ||
|
||
if (i == modelIndex.row()) { | ||
pHotcueIndexListItem->setHotcueIndexList(hotcueIndexList); | ||
} else { | ||
pHotcueIndexListItem->removeIndicies(hotcueIndexList); | ||
} | ||
} | ||
} | ||
|
||
return initialAttemptSuccessful; | ||
} | ||
return QStandardItemModel::setData(modelIndex, value, role); | ||
} | ||
|
||
|
@@ -84,62 +114,119 @@ void ColorPaletteEditorModel::setColor(int row, const QColor& color) { | |
setDirty(true); | ||
} | ||
|
||
void ColorPaletteEditorModel::appendRow(const QColor& color, int hotcueIndex) { | ||
void ColorPaletteEditorModel::appendRow( | ||
const QColor& color, const QList<int>& hotcueIndicies) { | ||
QStandardItem* pColorItem = new QStandardItem(toQIcon(color), color.name()); | ||
pColorItem->setEditable(false); | ||
pColorItem->setDropEnabled(false); | ||
|
||
QString hotcueIndexStr; | ||
if (hotcueIndex >= 0) { | ||
hotcueIndexStr = QString::number(hotcueIndex + 1); | ||
} | ||
|
||
QStandardItem* pHotcueIndexItem = new QStandardItem(hotcueIndexStr); | ||
QStandardItem* pHotcueIndexItem = new HotcueIndexListItem(hotcueIndicies); | ||
pHotcueIndexItem->setEditable(true); | ||
pHotcueIndexItem->setDropEnabled(false); | ||
|
||
QStandardItemModel::appendRow(QList<QStandardItem*>{pColorItem, pHotcueIndexItem}); | ||
QStandardItemModel::appendRow( | ||
QList<QStandardItem*>{pColorItem, pHotcueIndexItem}); | ||
} | ||
|
||
void ColorPaletteEditorModel::setColorPalette(const ColorPalette& palette) { | ||
// Remove all rows | ||
removeRows(0, rowCount()); | ||
|
||
// Make a map of hotcue indices | ||
QMap<int, int> hotcueColorIndicesMap; | ||
QMultiMap<int, int> hotcueColorIndicesMap; | ||
QList<int> colorIndicesByHotcue = palette.getIndicesByHotcue(); | ||
for (int i = 0; i < colorIndicesByHotcue.size(); i++) { | ||
int colorIndex = colorIndicesByHotcue.at(i); | ||
hotcueColorIndicesMap.insert(colorIndex, i); | ||
hotcueColorIndicesMap.insert(colorIndex, i + 1); | ||
} | ||
|
||
for (int i = 0; i < palette.size(); i++) { | ||
QColor color = mixxx::RgbColor::toQColor(palette.at(i)); | ||
int colorIndex = hotcueColorIndicesMap.value(i, kNoHotcueIndex); | ||
QList<int> colorIndex = hotcueColorIndicesMap.values(i); | ||
Swiftb0y marked this conversation as resolved.
Show resolved
Hide resolved
|
||
appendRow(color, colorIndex); | ||
} | ||
|
||
setDirty(false); | ||
} | ||
|
||
ColorPalette ColorPaletteEditorModel::getColorPalette(const QString& name) const { | ||
ColorPalette ColorPaletteEditorModel::getColorPalette( | ||
const QString& name) const { | ||
QList<mixxx::RgbColor> colors; | ||
QMap<int, int> hotcueColorIndices; | ||
for (int i = 0; i < rowCount(); i++) { | ||
QStandardItem* pColorItem = item(i, 0); | ||
QStandardItem* pHotcueIndexItem = item(i, 1); | ||
mixxx::RgbColor::optional_t color = mixxx::RgbColor::fromQString(pColorItem->text()); | ||
|
||
const auto* pHotcueIndexItem = toHotcueIndexListItem(item(i, 1)); | ||
if (!pHotcueIndexItem) { | ||
continue; | ||
} | ||
|
||
mixxx::RgbColor::optional_t color = | ||
mixxx::RgbColor::fromQString(pColorItem->text()); | ||
|
||
if (color) { | ||
const QList<int> hotcueIndexes = pHotcueIndexItem->getHotcueIndexList(); | ||
colors << *color; | ||
|
||
bool ok; | ||
int hotcueIndex = pHotcueIndexItem->text().toInt(&ok); | ||
if (ok) { | ||
hotcueColorIndices.insert(hotcueIndex - 1, colors.size() - 1); | ||
for (int index : hotcueIndexes) { | ||
hotcueColorIndices.insert(index - 1, colors.size() - 1); | ||
} | ||
} | ||
} | ||
// If we have a non consequitive list of hotcue indexes, indexes are shifted down | ||
// due to the sorting nature of QMap. This is intended, this way we have a color for every hotcue. | ||
return ColorPalette(name, colors, hotcueColorIndices.values()); | ||
Comment on lines
176
to
178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fear handling this silently results in confusion for the user. While we unfortunately don't have a "default color" we could insert into these empty positions, we should still test whether this shifting will occur and warn the user in that case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opinions? @Holzhaus There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to implement a warning icon/message that is displayed in the edit dialog, go ahead. |
||
} | ||
|
||
HotcueIndexListItem::HotcueIndexListItem(const QList<int>& hotcueList) | ||
: QStandardItem(), m_hotcueIndexList(hotcueList) { | ||
std::sort(m_hotcueIndexList.begin(), m_hotcueIndexList.end()); | ||
} | ||
QVariant HotcueIndexListItem::data(int role) const { | ||
switch (role) { | ||
case Qt::DisplayRole: | ||
case Qt::EditRole: { | ||
return QVariant(mixxx::stringifyRangeList(m_hotcueIndexList)); | ||
break; | ||
} | ||
default: | ||
return QStandardItem::data(role); | ||
break; | ||
} | ||
} | ||
|
||
void HotcueIndexListItem::setData(const QVariant& value, int role) { | ||
switch (role) { | ||
case Qt::EditRole: { | ||
const QList<int> newHotcueIndicies = mixxx::parseRangeList(value.toString()); | ||
|
||
if (m_hotcueIndexList != newHotcueIndicies) { | ||
m_hotcueIndexList = newHotcueIndicies; | ||
emitDataChanged(); | ||
} | ||
break; | ||
} | ||
default: | ||
QStandardItem::setData(value, role); | ||
break; | ||
} | ||
} | ||
|
||
void HotcueIndexListItem::removeIndicies(const QList<int>& otherIndicies) { | ||
DEBUG_ASSERT(std::is_sorted(otherIndicies.cbegin(), otherIndicies.cend())); | ||
DEBUG_ASSERT(std::is_sorted(m_hotcueIndexList.cbegin(), m_hotcueIndexList.cend())); | ||
|
||
QList<int> hotcueIndiciesWithOthersRemoved; | ||
hotcueIndiciesWithOthersRemoved.reserve(m_hotcueIndexList.size()); | ||
|
||
std::set_difference(m_hotcueIndexList.cbegin(), | ||
m_hotcueIndexList.cend(), | ||
otherIndicies.cbegin(), | ||
otherIndicies.cend(), | ||
std::back_inserter(hotcueIndiciesWithOthersRemoved)); | ||
|
||
if (m_hotcueIndexList != hotcueIndiciesWithOthersRemoved) { | ||
m_hotcueIndexList = hotcueIndiciesWithOthersRemoved; | ||
emitDataChanged(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,23 +1,23 @@ | ||||||
#pragma once | ||||||
#include <QStandardItem> | ||||||
#include <QStandardItemModel> | ||||||
#include <QVariant> | ||||||
|
||||||
#include "util/color/colorpalette.h" | ||||||
|
||||||
// Model that is used by the QTableView of the ColorPaletteEditor. | ||||||
// Takes of displaying palette colors and provides a getter/setter for | ||||||
// Takes care of displaying palette colors and provides a getter/setter for | ||||||
// ColorPalette instances. | ||||||
class ColorPaletteEditorModel : public QStandardItemModel { | ||||||
Q_OBJECT | ||||||
public: | ||||||
static constexpr int kNoHotcueIndex = -1; | ||||||
|
||||||
ColorPaletteEditorModel(QObject* parent = nullptr); | ||||||
|
||||||
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; | ||||||
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; | ||||||
|
||||||
void setColor(int row, const QColor& color); | ||||||
void appendRow(const QColor& color, int hotcueIndex = kNoHotcueIndex); | ||||||
void appendRow(const QColor& color, const QList<int>& hotcueIndicies); | ||||||
daschuer marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
void setDirty(bool bDirty) { | ||||||
if (m_bDirty == bDirty) { | ||||||
|
@@ -46,3 +46,27 @@ class ColorPaletteEditorModel : public QStandardItemModel { | |||||
bool m_bEmpty; | ||||||
bool m_bDirty; | ||||||
}; | ||||||
|
||||||
class HotcueIndexListItem : public QStandardItem { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The entire purpose of creating this class is to shift the "(de-)serialization" logic out of the model into the item so the item is responsible for translating between the String input and the data actually supposed to be represented as the string. |
||||||
public: | ||||||
HotcueIndexListItem(const QList<int>& hotcueList = {}); | ||||||
|
||||||
void setData(const QVariant& value, int role = Qt::UserRole + 1) override; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still don't quite understand the purpose of roles. I get that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. |
||||||
QVariant data(int role = Qt::UserRole + 1) const override; | ||||||
|
||||||
int type() const override { | ||||||
return QStandardItem::UserType; | ||||||
}; | ||||||
|
||||||
const QList<int>& getHotcueIndexList() const { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
QList is implicitly shared, should we really return a const reference here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. My guess was that explicitly constructing a QList with that reference at the call site when needed should still be pretty fast because of implicit sharing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, references should be always const, except in a locals scope like fool[i].bar() and similar. |
||||||
return m_hotcueIndexList; | ||||||
} | ||||||
void setHotcueIndexList(const QList<int>& list) { | ||||||
m_hotcueIndexList = QList(list); | ||||||
} | ||||||
|
||||||
void removeIndicies(const QList<int>& otherIndicies); | ||||||
|
||||||
private: | ||||||
QList<int> m_hotcueIndexList; | ||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
#include "util/rangelist.h" | ||
|
||
#include <gtest/gtest.h> | ||
|
||
#include <QString> | ||
#include <algorithm> | ||
|
||
#include "test/mixxxtest.h" | ||
|
||
void roundTripTestStr(const QString& in, const QString* out = nullptr) { | ||
const QString roundTrip = mixxx::stringifyRangeList(mixxx::parseRangeList(in)); | ||
if (out == nullptr) { | ||
EXPECT_QSTRING_EQ(in, roundTrip); | ||
} else { | ||
EXPECT_QSTRING_EQ(*out, roundTrip); | ||
} | ||
} | ||
|
||
void roundTripTestList(const QList<int>& in, const QList<int>* out = nullptr) { | ||
const QList<int> roundTrip = mixxx::parseRangeList(mixxx::stringifyRangeList(in)); | ||
if (out == nullptr) { | ||
EXPECT_EQ(in, roundTrip); | ||
} else { | ||
EXPECT_EQ(*out, roundTrip); | ||
} | ||
} | ||
|
||
TEST(DisplayIntListTest, ListEmpty) { | ||
roundTripTestList({}); | ||
roundTripTestStr(""); | ||
} | ||
|
||
TEST(DisplayIntListTest, smallContinousRangeIsSeparate) { | ||
const QList<int> list = mixxx::parseRangeList(QStringLiteral("1 - 2")); | ||
EXPECT_EQ(list, QList({1, 2})); | ||
EXPECT_QSTRING_EQ("1, 2", mixxx::stringifyRangeList(list)); | ||
} | ||
|
||
TEST(DisplayIntListTest, whiteSpaceAreIgnored) { | ||
EXPECT_EQ(QList<int>({1, 2, 3, 4}), mixxx::parseRangeList(" 1,2 , 3, 4")); | ||
} | ||
|
||
TEST(DisplayIntListTest, trailingCommaIsIgnored) { | ||
EXPECT_EQ(QList<int>({1}), mixxx::parseRangeList("1,")); | ||
} | ||
TEST(DisplayIntListTest, largeRangesAreExpanded) { | ||
EXPECT_EQ(QList<int>({1, 2, 3, 4, 5, 6, 7}), mixxx::parseRangeList("1 - 7")); | ||
} | ||
|
||
TEST(DisplayIntListTest, duplicateValuesAreIgnored) { | ||
EXPECT_EQ(QList<int>({1, 2, 3}), mixxx::parseRangeList("1, 1, 1, 1, 2, 2, 3")); | ||
} | ||
|
||
TEST(DisplayIntListTest, resultingListIsSortedAscending) { | ||
const auto list = mixxx::parseRangeList("4, 2, 3, 1, 6, 5"); | ||
EXPECT_TRUE(std::is_sorted(list.cbegin(), list.cend())); | ||
} | ||
TEST(DisplayIntListTest, consequitiveValuesAreRanges) { | ||
EXPECT_QSTRING_EQ("1 - 4", mixxx::stringifyRangeList(QList<int>({1, 2, 3, 4}))); | ||
} | ||
TEST(DisplayIntListTest, adjacentRangeAndLiteralGetsCollapsed) { | ||
EXPECT_EQ(QList<int>({1, 2, 3, 4, 5}), mixxx::parseRangeList("1, 2 - 4, 5")); | ||
} | ||
TEST(DisplayIntListTest, overLappingRangesGetUnionized) { | ||
EXPECT_EQ(QList<int>({1, 2, 3, 4}), mixxx::parseRangeList("1 - 3, 2 - 4")); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
including the entire large header file (whose contents are largely unrelated) just for
NUM_HOT_CUES
is probably overkill.NUM_HOT_CUES
should probably be factored out into a smaller header file and renamedstatic constexpr int mixxx::kNumHotCues
, wdyt?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that it should factored out, but I think we should do that for main, not 2.3 so short before a release.
PS:
static
is undesireable for simple globalconstexpr
constants that are not part of a class. AFAIK it forces the variable to be initialized at runtime, even if that variable is never used.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I forgot that
static
has different meanings in different contexts.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static in this context is nothing else than a anonymous namespace. There where efforts to deprecate global statics in favor of this but that has not made it's way into the C++ standard. In Mixxx we prefer the anonymous namespace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://web.archive.org/web/20140116013834/http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=463
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but anonymous namespaces are only used in
*.cpp
files, not header files.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A header file is nothing special in cpp. It is just copied into the cpp file in the preprocessor stage.
In case the global variable is constexpr it is part of the text segment. This is also the case for static const or const in an anonymous namespace, where the compiler does not need to expose the address to the linker.
If you have an anonymous namespace in a header and you include the header in two different cpp files, you have two separate variables. The compiler is free to duplicate the value even more for performance reasons.
If the variable is not a constexpr or plane old data type, the variable is initialized when accessed the first time.
This happens for all our QStrings we have there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know, but he point is that an anonymous namespace tells the compiler that everything inside it is local to the compilation unit it is found in.
If you put an anonymous namespace into a header file, you need to use every single function or variable inside that namespace in each cpp file that includes it, otherwise you'll get a bunch of "unused variable" warnings. And for functions this will make the executable larger due to redundant code (unless the compiler deduplicates the redundant function at link time).
Therefore, anonymous namespace don't make sense in header files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, however this applied to static const and to namespaces in the same way. The workaround is to use macros.