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

Use checkbox menu in crate selection. #1341

Merged
merged 7 commits into from
Dec 23, 2017

Conversation

poelzi
Copy link
Contributor

@poelzi poelzi commented Sep 9, 2017

Switch from normal QAction menu to a composition of QCheckBox and QActionWidget.
The checkbox shows in which crates the selection is in.
Changing the crates selection does not collapse the menu, which allows
much easier categorization of tracks without going through the menu from scratch.

@daschuer
Copy link
Member

Thank you for this PR. Nice Idea.

It work well for a single track, unfortunately not well for multiple tracks. I think we need a three state check box or something, for the case where some of the selected tracks are in a crate and some not.

Nit: Please move the pointer asterisk next to QWidget to meet our coding style.

Be fore merge, we need your permission.
Please sign https://docs.google.com/a/mixxx.org/spreadsheet/viewform?formkey=dEpYN2NkVEFnWWQzbkFfM0ZYYUZ5X2c6MQ and comment here when done.

@gramanas: how does this merge with your crate hierarchy branch?

@gramanas
Copy link
Contributor

I can't check it atm, but this is based on master and not the new library redesign. Since it only changes widget files, I don't think it'll be hard to resolve the merge conflicts, but I can definitely see some.

@poelzi poelzi force-pushed the feature/crate_selectbox branch from 59683aa to 4795bd9 Compare September 12, 2017 21:17
@poelzi
Copy link
Contributor Author

poelzi commented Sep 12, 2017

I implemented the tristate when multiple tracks are selected that do not share a crate fully.
Fixed your styling comment as well (I hope ;))

Copy link
Member

@daschuer daschuer left a comment

Choose a reason for hiding this comment

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

Thank you for the changes.
It works nice now.

I have added some comments most of them are to improve the readability of the code.

// here. But since the coresponding FK column is indexed the impact on the
// performance should be negligible. By reusing an existing query we reduce
// the amount of code and the number of prepared SQL queries.
CrateTrackSelectResult crateTracks(selectTrackCratesSorted(trackId));
Copy link
Member

Choose a reason for hiding this comment

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

The plural is kind of misleading here.
Isn't it trackCrates? I am unsure any other name?

while (crateTracks.next()) {
DEBUG_ASSERT(crateTracks.trackId() == trackId);
CrateId cid = crateTracks.crateId();
if(!trackCrates.contains(cid)) {
Copy link
Member

Choose a reason for hiding this comment

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

I do not understand this. Isn't it always false?

Copy link
Member

Choose a reason for hiding this comment

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

this if condition is redundant, because operator[] has already such a code.

pAction->setDefaultWidget(pCheckBox.get());

// FIXME: we could use the tristate feature here and show grey in case of multi track selection
// in which not all track are in the crate
Copy link
Member

Choose a reason for hiding this comment

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

Is this comment outdated?


const QList<TrackId> trackIds = getSelectedTrackIds();

QHash<CrateId, QSet<TrackId>> currentCrates(m_pTrackCollection->crates().collectCrateTrackListOfTracks(trackIds));
Copy link
Member

Choose a reason for hiding this comment

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

Please wrap this line around column 80. Vertical scrolling in Github is odd ;-)

auto pCheckBox = std::make_unique<QCheckBox>(m_pCrateMenu);

pCheckBox->setText(crate.getName());
pCheckBox->setProperty("crateId", crate.getId().toInt());
Copy link
Member

Choose a reason for hiding this comment

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

use toVariant()

@@ -1466,22 +1494,51 @@ void WTrackTableView::addSelectionToPlaylist(int iPlaylistId) {
playlistDao.appendTracksToPlaylist(trackIds, iPlaylistId);
}

void WTrackTableView::addSelectionToCrate(int iCrateId) {
void WTrackTableView::addRemoveSelectionInCrate(QWidget* qc) {
CrateId crateId = static_cast<CrateId>(qc->property("crateId"));
Copy link
Member

Choose a reason for hiding this comment

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

CrateId has a QVariant constructor, so you can omit the static cast.

crateId = CrateFeatureHelper(
m_pTrackCollection, m_pConfig).createEmptyCrate();
// we need to disable tristate again as the mixed state will now be gone and can't be brought back
if (qc->property("tristate").toBool() == true) {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using string properties, we should use a qobject_cast at the very beginning of the function and check for null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried different techniques for hours, but this was the only only I got working properly. I'm quite open if someone gets this running using more slick code, but as I think it's none critical code from performance aspects, I think it's OK.
I will write some more optimal SQL queries in a separate pull request which should be a much better optimization.

Copy link
Member

Choose a reason for hiding this comment

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

See poelzi#1

// FIXME: we could use the tristate feature here and show grey in case of multi track selection
// in which not all track are in the crate
if(currentCrates.contains(crate.getId())) {
if(currentCrates[crate.getId()].size() != trackIds.size()) {
Copy link
Member

Choose a reason for hiding this comment

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

Ok, this works, but it took me quite a while to find out how.
I think this requires some comments.
currentCrates should also be renamed ... mmm
or can we move the size compare if condition to collectCrateTrackListOfTracks ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thought was, that the API maybe more useful in the future. Giving a list of tracks and getting a hash with the crates and the containing tracks. Maybe I was overthinking it. I changed the api to just count the tracks in the crate.


// FIXME: we could use the tristate feature here and show grey in case of multi track selection
// in which not all track are in the crate
if(currentCrates.contains(crate.getId())) {
Copy link
Member

Choose a reason for hiding this comment

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

contains() and a subsequent [] is a double hash lookup. Use find();

@poelzi poelzi force-pushed the feature/crate_selectbox branch from 4795bd9 to cc8af31 Compare September 15, 2017 13:08
@poelzi
Copy link
Contributor Author

poelzi commented Sep 15, 2017

Thanks for the review. I haven't user QT C++ before and haven't touched C++ for a very long time.
I hope I have addressed all points. Unfortunately I haven't found a way to avoid string properties in a slick way, but I don't think it's critical in any way.

// the amount of code and the number of prepared SQL queries.
CrateTrackSelectResult trackCrates(selectTrackCratesSorted(trackId));
while (trackCrates.next()) {
rv[trackCrates.crateId()] += 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure that the integer value in each entry is explicitly initialized with 0??? The fundamental type int does not have a default constructor and QHash will leave it uninitialized when inserting a new key/value pair.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I understood the QList documentation. It states that such types are initialized with 0.

@uklotzde
Copy link
Contributor

Very nice, I like this feature 👍 Now I can delete my custom branch that allowed me to delete selected tracks from a crate. But I omitted to add visual feedback, if those tracks are actually contained in crates. That's why I never created a PR.

@uklotzde
Copy link
Contributor

Proposal: The name of the menu entry should be changed from "Add to Crate" to just "Crates"!


const QList<TrackId> trackIds = getSelectedTrackIds();

QHash<CrateId, int> currentCrates(m_pTrackCollection->crates().\
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately many queries are executed even if I don't open the Crates menu entry. Each right click on selected tracks now has a noticeable delay when you have many crates. The list of crates with their state should only be populated when the corresponding sub-menu entry becomes visible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I will look into optimizing it and change the name to crates :)

Crate crate;
while (allCrates.populateNext(&crate)) {
auto pAction = std::make_unique<QAction>(crate.getName(), m_pCrateMenu);
auto pAction = std::make_unique<QWidgetAction>(m_pCrateMenu);
Copy link
Member

Choose a reason for hiding this comment

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

we have recently introduced "make_parented" to document the qt object tree ownership and to avoid the unused unique_ptr nature.

@uklotzde uklotzde added the ui label Oct 29, 2017
@uklotzde
Copy link
Contributor

uklotzde commented Nov 6, 2017

@poelzi Any plans to address the open comments?

I must admit that this is a really helpful feature that I merge into my private builds. I would like to see it in the next release(s). I usually have around 50+ crates and the performance penalties seem to be acceptable. We could even discuss to deliver the performance improvements in a follow-up release if (hopefully) we are able to return to a more frequent release cycle ;)

@poelzi
Copy link
Contributor Author

poelzi commented Nov 7, 2017

I will hopefully get back addressing all the comments I have on my open pull requests and this one is annoying me as well ;) I had no working development machine for a while or was quite busy.

@poelzi poelzi force-pushed the feature/crate_selectbox branch from f257b90 to 97a0684 Compare November 10, 2017 02:32
@poelzi
Copy link
Contributor Author

poelzi commented Nov 10, 2017

I was able to optimize everything into one query with a subselect and reuse the CratesSummery code.
Got even slicker :)

Switch from normal QAction menu to a composition of QCheckBox and QActionWidget.
The checkbox shows in which crates the selection is in.
Changing the crates selection does not collapse the menu, which allows
much easier categorization of tracks without going through the menu from scratch.

Use tristate when multiple tracks are selected.

When mulitple tracks are selected which do not share a crate, use
the tristate partially selected to indicate this.

Fix styling of pointers.

Use QVariant to transport CrateId in checkbox

Use optimized SQL query to select crates and count tracks.

Unfortunatelly QSqlQuery has no way of binding QLists
in WHERE x IN statements.

Use make_partented as suggested by daschuer

rename "Add to Crates" -> "Crates" to reflect function better
@poelzi poelzi force-pushed the feature/crate_selectbox branch from bf5080a to 0a75b38 Compare November 11, 2017 01:01
@Be-ing
Copy link
Contributor

Be-ing commented Nov 11, 2017

@poelzi for the future, it's easier to review changes since the last review if new commits are added instead of rebasing.

Copy link
Contributor

@Be-ing Be-ing left a comment

Choose a reason for hiding this comment

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

just some small style nitpicks

@@ -468,6 +468,42 @@ CrateTrackSelectResult CrateStorage::selectTrackCratesSorted(TrackId trackId) co
}
}

CrateSummarySelectResult CrateStorage::selectCratesWithTrackCount(const QList<TrackId>& trackIds) const {

Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: extra blank line here

// Using bindValue did not work, only constructing the SQL string befor.
// As TrackId is a int, we are safe here
QStringList idstrings;
foreach(TrackId id, trackIds) {
Copy link
Contributor

@Be-ing Be-ing Nov 11, 2017

Choose a reason for hiding this comment

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

Use standard C++11 for loop instead of old Qt foreach macro: for (TrackId id : trackIds) {

} else {
return CrateSummarySelectResult();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

extra blank line

@poelzi
Copy link
Contributor Author

poelzi commented Nov 11, 2017

@Be-ing: I think your nit picks where fixed by the optimization Uwe Klotz did.

@Be-ing
Copy link
Contributor

Be-ing commented Nov 11, 2017

Oh, you're correct. Sorry for the noise.

@uklotzde
Copy link
Contributor

The idea of lazily populating the context sub-menus is also mentioned in this bug report:
https://bugs.launchpad.net/mixxx/+bug/1395022
Executing even more synchronous database queries to obtain results that might never be needed will impact the user experience even more.

Again, I like the functionality and would accept the lag in the first place. But we should think about how this can be fixed ASAP.

@poelzi
Copy link
Contributor Author

poelzi commented Nov 14, 2017

@uklotzde i totally agree, but the current iteration does not make any more SQL calls then before.

sqlite> SELECT *, (    SELECT COUNT(*) FROM crate_tracks WHERE crates.id = crate_tracks.crate_id and crate_tracks.track_id in (29, 31, 28, 19, 15,135, 267) ) AS track_count, 0 as track_duration FROM crates ORDER BY name;
6|Haha1|0|1|0|0|3|0
4|blbl|0|1|0|0|0|0
3|blubb|0|1|0|0|0|0
5|huhu|0|1|0|0|3|0
10|huhuihaenuae|0|1|0|0|0|0
1|test1|0|1|1|0|2|0
2|test2|0|1|0|0|1|0
7|turaine|0|1|0|0|2|0
8|uaierutaors|0|1|0|0|0|0
9|uiaeuaie|0|1|0|0|1|0
-------- scanstats --------
Loop  1: SCAN TABLE crates USING INDEX sqlite_autoindex_crates_1
         nLoop=1        nRow=10       estRow=1048576  estRow/Loop=1.04858e+06
-------- subquery 1 -------
Loop  1: SEARCH TABLE crate_tracks USING COVERING INDEX sqlite_autoindex_crate_tracks_1 (crate_id=? AND track_id=?)
         nLoop=10       nRow=12       estRow=80       estRow/Loop=8       
---------------------------
Run Time: real 0.000 user 0.000000 sys 0.000000
sqlite> 

It's 2 loops instead of one but fully covered by indexes.

@uklotzde
Copy link
Contributor

The lazy initialization of 2nd level actions affects multiple use cases and is a cross-cutting concern and could be solved in a separate task.

I vote for integrating this extremly useful feature before improving the performance of all context menus that require database access.

If the song changes while the current history view is open, the selection of
songs is lost. Therefor all crate changes are lost.

The history view saves and restores the selection through a new usefull api for selecting tracks in the wtracklistview.
@Be-ing
Copy link
Contributor

Be-ing commented Nov 19, 2017

I tested this and it works nicely. It would be cool to have this for Playlists too, but before that is implemented I think this needs to be lazy initialized or right clicking tracks will become even more terribly slow.

@Be-ing
Copy link
Contributor

Be-ing commented Nov 21, 2017

Is there anything left to do here? Ready for merge?

@uklotzde
Copy link
Contributor

LGTM.

Some nitpicking about typos and coding style. But I don't think we need another round here. I would like to get the approval of 1 or 2 other reviewers before merging anything myself.

@daschuer How do we verify that the author has signed the contributor agreement?

@daschuer
Copy link
Member

@poelzi is already a contributor.
Do you have a Launchpad account? Than I can add you to the Mixxx Contributors Group.

@poelzi
Copy link
Contributor Author

poelzi commented Nov 22, 2017

@daschuer it's poelzi on launchpad :)

@uklotzde uklotzde added this to the 2.1.0 milestone Nov 25, 2017
@@ -1466,22 +1519,55 @@ void WTrackTableView::addSelectionToPlaylist(int iPlaylistId) {
playlistDao.appendTracksToPlaylist(trackIds, iPlaylistId);
}

void WTrackTableView::addSelectionToCrate(int iCrateId) {
void WTrackTableView::addRemoveSelectionInCrate(QWidget* pWidget) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function name is ambiguous. Does it add or does it remove?

void WTrackTableView::addRemoveSelectionInCrate(QWidget* pWidget) {
auto pCheckBox = qobject_cast<QCheckBox*>(pWidget);
VERIFY_OR_DEBUG_ASSERT(pCheckBox) {
qWarning() << "crateId is not ef CrateId type";
Copy link
Contributor

Choose a reason for hiding this comment

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

"not ef"? Did you mean "not of"?

// assume that all track ids fit into 6 decimal digits and
// add 1 character for the list separator.
joinedTrackIds.reserve((6 + 1) * trackIds.size());
for (const auto trackId: trackIds) {
Copy link
Contributor

Choose a reason for hiding this comment

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

const auto& trackId to prevent unnecessary copy

connect(pAction.get(), SIGNAL(triggered()), &m_crateMapper, SLOT(map()));
pAction->setDefaultWidget(pCheckBox.get());

if(crate.getTrackCount() == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (


if(crate.getTrackCount() == 0) {
pCheckBox->setChecked(false);
} else if(crate.getTrackCount() == (uint)trackIds.length()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

} else if (

return;
}

foreach(TrackId trackId, trackIds) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use modern standard C++ instead of old Qt foreach macro: for (const auto& trackId : trackIds) {

@uklotzde
Copy link
Contributor

@poelzi Do you have time to address Be's comments and resolve the pending merge conflict, Daniel? I would like to have this in 2.1, because it has proven to be a valuable feature 👍

@Be-ing
Copy link
Contributor

Be-ing commented Dec 14, 2017

LGTM, thank you! @poelzi would you be interested in implementing the lazy initialization to alleviate https://bugs.launchpad.net/mixxx/+bug/1733200 for 2.1?

@poelzi
Copy link
Contributor Author

poelzi commented Dec 14, 2017

@Be-ing kinda added the required infrastructure in another branch already. I added a widget (qlistview) that displays the crates of the selected track. I used a background thread to fetch the information in background and update the gui when done. If we decide to use one thread for sql queryies and gui support or one thread per component. https://ibb.co/dJsuf6

@Be-ing
Copy link
Contributor

Be-ing commented Dec 18, 2017

@uklotzde ready for merge?

@@ -1373,6 +1401,31 @@ QList<TrackId> WTrackTableView::getSelectedTrackIds() const {
return trackIds;
}

void WTrackTableView::setSelectedTracks(QList<TrackId> trackIds) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This parameter should be passed as const-ref to avoid a deep copy of the list. Unfortunately the range-based for loops do not work well with Qt containers. Declaring the loop variable as const-ref is not enough. The container itself must be const! There is already a link in our coding guidelines.

We have this issue at many places and I have done this wrong until recently. Checking the code with clazy could reveal those hidden performance killers.

Copy link
Member

Choose a reason for hiding this comment

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

I will fix this after merge

Copy link
Contributor

Choose a reason for hiding this comment

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

I already did in #1427

Copy link
Member

Choose a reason for hiding this comment

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

included in 187ec3d by @Be-ing

@uklotzde
Copy link
Contributor

Almost ready. Just one minor issue that we should fix, now that we know better ;)

@daschuer daschuer merged commit 341acf1 into mixxxdj:master Dec 23, 2017
@Be-ing
Copy link
Contributor

Be-ing commented Dec 23, 2017

Thank you for this big usability improvement for crates! Let's continue this momentum to make Mixxx 2.2 the best music library management software. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants