Skip to content
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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL
src/util/mac.cpp
src/util/movinginterquartilemean.cpp
src/util/performancetimer.cpp
src/util/rangelist.cpp
src/util/readaheadsamplebuffer.cpp
src/util/rlimit.cpp
src/util/rotary.cpp
Expand Down Expand Up @@ -1513,6 +1514,7 @@ add_executable(mixxx-test
src/test/portmidicontroller_test.cpp
src/test/portmidienumeratortest.cpp
src/test/queryutiltest.cpp
src/test/rangelist_test.cpp
src/test/readaheadmanager_test.cpp
src/test/replaygaintest.cpp
src/test/rescalertest.cpp
Expand Down
2 changes: 1 addition & 1 deletion src/preferences/colorpaletteeditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ void ColorPaletteEditor::slotTableViewDoubleClicked(const QModelIndex& index) {
}

void ColorPaletteEditor::slotAddColor() {
m_pModel->appendRow(kDefaultPaletteColor);
m_pModel->appendRow(kDefaultPaletteColor, {});
m_pTableView->scrollToBottom();
m_pTableView->setCurrentIndex(
m_pModel->index(m_pModel->rowCount() - 1, 0));
Expand Down
149 changes: 118 additions & 31 deletions src/preferences/colorpaletteeditormodel.cpp
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"
Copy link
Member Author

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 renamed static constexpr int mixxx::kNumHotCues, wdyt?

Copy link
Member

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 global constexpr 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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Mixxx we prefer the anonymous namespace.

True, but anonymous namespaces are only used in *.cpp files, not header files.

Copy link
Member

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.

Copy link
Member

@Holzhaus Holzhaus Apr 13, 2021

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.

Copy link
Member

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.

#include "moc_colorpaletteeditormodel.cpp"

namespace {
Expand All @@ -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)
Expand Down Expand Up @@ -53,25 +69,39 @@ bool ColorPaletteEditorModel::dropMimeData(const QMimeData* data, Qt::DropAction
bool ColorPaletteEditorModel::setData(const QModelIndex& modelIndex, const QVariant& value, int role) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Expand All @@ -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
Copy link
Member Author

@Swiftb0y Swiftb0y Apr 3, 2021

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opinions? @Holzhaus

Copy link
Member

Choose a reason for hiding this comment

The 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();
}
}
32 changes: 28 additions & 4 deletions src/preferences/colorpaletteeditormodel.h
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) {
Expand Down Expand Up @@ -46,3 +46,27 @@ class ColorPaletteEditorModel : public QStandardItemModel {
bool m_bEmpty;
bool m_bDirty;
};

class HotcueIndexListItem : public QStandardItem {
Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Copy link
Member Author

Choose a reason for hiding this comment

The 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 setData and data are "generic" accessors to get data for different purposes (such as displaying the data itself, providing a tooltip description etc). Since I only intent to intercept the data for editing and displaying, I should be able to reuse those roles instead of making my own role, right? Or what exactly am I supposed to do with the custom role.

Copy link
Member

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const QList<int>& getHotcueIndexList() const {
const QList<int>& getHotcueIndexList() const {

QList is implicitly shared, should we really return a const reference here?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daschuer @uklotzde opinions? I don't think really care and it can be changed in a subsequent PR you think it's needed. So no need to delay this PR.

Copy link
Member

Choose a reason for hiding this comment

The 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;
};
66 changes: 66 additions & 0 deletions src/test/rangelist_test.cpp
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"));
}
Loading