Use checkbox menu in crate selection.#1341
Conversation
|
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. @gramanas: how does this merge with your crate hierarchy branch? |
|
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. |
59683aa to
4795bd9
Compare
|
I implemented the tristate when multiple tracks are selected that do not share a crate fully. |
daschuer
left a comment
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
I do not understand this. Isn't it always false?
There was a problem hiding this comment.
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 |
|
|
||
| const QList<TrackId> trackIds = getSelectedTrackIds(); | ||
|
|
||
| QHash<CrateId, QSet<TrackId>> currentCrates(m_pTrackCollection->crates().collectCrateTrackListOfTracks(trackIds)); |
There was a problem hiding this comment.
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()); |
|
|
||
| void WTrackTableView::addSelectionToCrate(int iCrateId) { | ||
| void WTrackTableView::addRemoveSelectionInCrate(QWidget* qc) { | ||
| CrateId crateId = static_cast<CrateId>(qc->property("crateId")); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Instead of using string properties, we should use a qobject_cast at the very beginning of the function and check for null.
There was a problem hiding this comment.
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.
| // 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()) { |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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())) { |
There was a problem hiding this comment.
contains() and a subsequent [] is a double hash lookup. Use find();
4795bd9 to
cc8af31
Compare
|
Thanks for the review. I haven't user QT C++ before and haven't touched C++ for a very long time. |
| // the amount of code and the number of prepared SQL queries. | ||
| CrateTrackSelectResult trackCrates(selectTrackCratesSorted(trackId)); | ||
| while (trackCrates.next()) { | ||
| rv[trackCrates.crateId()] += 1; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
As far as I understood the QList documentation. It states that such types are initialized with 0.
|
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. |
|
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().\ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
we have recently introduced "make_parented" to document the qt object tree ownership and to avoid the unused unique_ptr nature.
|
@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 ;) |
|
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. |
f257b90 to
97a0684
Compare
|
I was able to optimize everything into one query with a subselect and reuse the CratesSummery code. |
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
bf5080a to
0a75b38
Compare
Optimize formatting of string lists for SQL queries
|
@poelzi for the future, it's easier to review changes since the last review if new commits are added instead of rebasing. |
Be-ing
left a comment
There was a problem hiding this comment.
just some small style nitpicks
| } | ||
|
|
||
| CrateSummarySelectResult CrateStorage::selectCratesWithTrackCount(const QList<TrackId>& trackIds) const { | ||
|
|
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Use standard C++11 for loop instead of old Qt foreach macro: for (TrackId id : trackIds) {
| } else { | ||
| return CrateSummarySelectResult(); | ||
| } | ||
|
|
|
@Be-ing: I think your nit picks where fixed by the optimization Uwe Klotz did. |
|
Oh, you're correct. Sorry for the noise. |
|
The idea of lazily populating the context sub-menus is also mentioned in this bug report: 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. |
|
@uklotzde i totally agree, but the current iteration does not make any more SQL calls then before. It's 2 loops instead of one but fully covered by indexes. |
|
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.
|
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. |
|
Is there anything left to do here? Ready for merge? |
|
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? |
|
@poelzi is already a contributor. |
|
@daschuer it's poelzi on launchpad :) |
| } | ||
|
|
||
| void WTrackTableView::addSelectionToCrate(int iCrateId) { | ||
| void WTrackTableView::addRemoveSelectionInCrate(QWidget* pWidget) { |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
"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) { |
There was a problem hiding this comment.
const auto& trackId to prevent unnecessary copy
| connect(pAction.get(), SIGNAL(triggered()), &m_crateMapper, SLOT(map())); | ||
| pAction->setDefaultWidget(pCheckBox.get()); | ||
|
|
||
| if(crate.getTrackCount() == 0) { |
|
|
||
| if(crate.getTrackCount() == 0) { | ||
| pCheckBox->setChecked(false); | ||
| } else if(crate.getTrackCount() == (uint)trackIds.length()) { |
| return; | ||
| } | ||
|
|
||
| foreach(TrackId trackId, trackIds) { |
There was a problem hiding this comment.
Use modern standard C++ instead of old Qt foreach macro: for (const auto& trackId : trackIds) {
|
@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 👍 |
|
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? |
|
@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 |
|
@uklotzde ready for merge? |
| return trackIds; | ||
| } | ||
|
|
||
| void WTrackTableView::setSelectedTracks(QList<TrackId> trackIds) { |
There was a problem hiding this comment.
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.
|
Almost ready. Just one minor issue that we should fix, now that we know better ;) |
|
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. :) |
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.