-
-
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
Usability/Keyboard: Make the star ratings in the tracks view editable via keyboard #13717
base: main
Are you sure you want to change the base?
Usability/Keyboard: Make the star ratings in the tracks view editable via keyboard #13717
Conversation
…itor with QAbstractItemDelegatePrivate::_q_commitDataAndCloseEditor ...by more closely adhering to the interface contract of the closeEditor() signal. See also the Qt code here: /~https://github.com/qt/qtbase/blob/faae3dc6b15e3d8754b15a7cfb22b6a797fd2e36/src/widgets/itemviews/qabstractitemdelegate.cpp#L587
The keyboard controls are only active when the eventFilter is installed, which is only the case for non-persistent editors. Persistent editors (i.e. those opened using openPersistentEditor) do not receive keyboard events via their eventFilter.
c162ca7
to
c7e0415
Compare
…r control ...and close the persistent editor in those cases. This is important for keeping the our internal bookkeeping in sync with the Qt framework state - the former is required to work around some quirks related to the two ways of interacting with the StarEditor (by mouse and by keyboard) and how this interacts with the assumptions of QTableView. ==================== = More information = ==================== The StarRating control can be edited via mouse, keyboard, or both at the same time. The following transitions are currently handled correctly (marked by ✓) resp. affected by this change (marked by =>): [✓] "Not editing" -> "Edit via mouse" (on mouseEnter) [✓] "Not editing" -> "Edit via keyboard" (on editKeyPressed) [ ] => [✓] "Edit via mouse" -> "Not editing" (on mouseLeave) [ ] "Edit via mouse" -> "Edit via keyboard+mouse" (on editKeyPressed) [✓] "Edit via keyboard" -> "Not editing" (on exitKeyPressed) [✓] "Edit via keyboard" -> "Edit via keyboard+mouse" (on mouseEnter) [ ] "Edit via keyboard+mouse" -> "Edit via mouse" (on exitKeyPressed) [ ] => [~] "Edit via keyboard+mouse" -> "Edit via keyboard" (on mouseLeave)
c7e0415
to
75836e7
Compare
…tEditorOpen ...and extract common code into separate methods.
75836e7
to
a83e4e4
Compare
…use edit mode" on edit key pressed Correctly handle the case of a user pressing the edit key while the mouse is also hovering over the focused cell. Without this special handling, the pressing the edit key on a cell would simply be ignored if the mouse is hovering over that cell, which would most certainly break user expectations. ==================== = More information = ==================== The StarRating control can be edited via mouse, keyboard, or both at the same time. The following transitions are currently handled correctly resp. affected by this change: [✓] "Not editing" -> "Edit via mouse" (on mouseEnter) [✓] "Not editing" -> "Edit via keyboard" (on editKeyPressed) [✓] "Edit via mouse" -> "Not editing" (on mouseLeave) [ ] => [✓] "Edit via mouse" -> "Edit via keyboard+mouse" (on editKeyPressed) [✓] "Edit via keyboard" -> "Not editing" (on exitKeyPressed) [✓] "Edit via keyboard" -> "Edit via keyboard+mouse" (on mouseEnter) [ ] "Edit via keyboard+mouse" -> "Edit via mouse" (on exitKeyPressed) [~] "Edit via keyboard+mouse" -> "Edit via keyboard" (on mouseLeave)
…mouse edit mode" This is important for the case when all of the following is true: * The user is currently editing a star rating cell via the keyboard. * The mouse cursor is inside that cell. * The user finishes editing by pressing the Return key or the Escape key. Without the special handling in place, the user would have to move the mouse outside the cell and then back in to see the cell react to the movements of the mouse cursor again. ==================== = More information = ==================== The StarRating control can be edited via mouse, keyboard, or both at the same time. The following transitions are currently handled correctly (marked by ✓) resp. affected by this change (marked by =>): [✓] "Not editing" -> "Edit via mouse" (on mouseEnter) [✓] "Not editing" -> "Edit via keyboard" (on editKeyPressed) [✓] "Edit via mouse" -> "Not editing" (on mouseLeave) [✓] "Edit via mouse" -> "Edit via keyboard+mouse" (on editKeyPressed) [✓] "Edit via keyboard" -> "Not editing" (on exitKeyPressed) [✓] "Edit via keyboard" -> "Edit via keyboard+mouse" (on mouseEnter) [ ] => [✓] "Edit via keyboard+mouse" -> "Edit via mouse" (on exitKeyPressed) [~] "Edit via keyboard+mouse" -> "Edit via keyboard" (on mouseLeave)
…ctItemView QAbstractItemView doesn't like it when we try to open an editor while the closing of the previously open editor is not fully finished, because that messes up its internal state. We avoid this race condition by deferring the restore until we have returned to the event loop (using basically the same technique as QObject::deleteLater().
Contrary to its name, the QAbstractItemView::closePersistentEditor() method will also close a non-persistent editor which is present at the specified QModelIndex. If we do not detect this case, moving the mouse outside of a star rating editor that is currently in "keyboard edit mode" would both involuntarily exit the edit mode and break our internal state. ==================== = More information = ==================== The StarRating control can be edited via mouse, keyboard, or both at the same time. The following transitions are currently handled correctly (marked by ✓) resp. affected by this change (marked by =>): [✓] "Not editing" -> "Edit via mouse" (on mouseEnter) [✓] "Not editing" -> "Edit via keyboard" (on editKeyPressed) [✓] "Edit via mouse" -> "Not editing" (on mouseLeave) [✓] "Edit via mouse" -> "Edit via keyboard+mouse" (on editKeyPressed) [✓] "Edit via keyboard" -> "Not editing" (on exitKeyPressed) [✓] "Edit via keyboard" -> "Edit via keyboard+mouse" (on mouseEnter) [✓] "Edit via keyboard+mouse" -> "Edit via mouse" (on exitKeyPressed) [~] => [✓] "Edit via keyboard+mouse" -> "Edit via keyboard" (on mouseLeave)
…edit mode" This matches the behavior of the textual controls, where the border color and background color change depending on whether you are currently editing the text field, or are just focused on the cell.
a83e4e4
to
c59a384
Compare
Just a note
Enter/Return currently triggers the configured double-click action, so in order to use Enter one woud have to select 'ignore' in the preferences, right? |
case Qt::Key_9: { | ||
newRating = 9; | ||
break; | ||
} |
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.
Another quick note:
currently, star ratings range from 0-5, so let's use onyl those keys, and add a note to
Line 78 in 60bdaca
static constexpr int kMaxRating = 5; |
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.
(Edit: Sorry, reply got way longer than intended. TLDR: It causes no issues and is not more complex to maintain than a TODO comment in an "unrelated" file. If you insist, I have no problem with removing those lines though).
I see your point, but I'd argue in favor of keeping it the way it currently is:
- User Interaction: We prevent all key press events from moving up the parent chain anyway while in edit mode (see line 201) to be consistent with the way the other editors work, so the user does not gain something from not handling the keys
6
to9
.
mixxx/src/library/tabledelegates/stareditor.cpp
Lines 192 to 194 in c59a384
// Prevent other keys from being handled as global keyboard shortcuts. // This matches the behavior of the other edit controls. return true;
We clamp to the valid rating range anyway, so I even think supporting all numbers on the numpad makes this feature ever so slightly more discoverable. - API Consistency:
StarRating
has amaxStarCount
method, so I (as a developer) would in principle expectStarEditor
it to work nicely with everyStarRating
object I pass it, including those with max ratings other than 5, if I were to reuse it in some other place.
mixxx/src/library/starrating.h
Lines 23 to 35 in 3c1d44c
explicit StarRating( int starCount = kMinStarCount, int maxStarCount = mixxx::TrackRecord::kMaxRating - mixxx::TrackRecord::kMinRating); void paint(QPainter* painter, const QRect& rect) const; QSize sizeHint() const; int starCount() const { return m_starCount; } int maxStarCount() const { return m_maxStarCount; } - Code complexity: If not for user/developer experience: What is to be gained from replacing a few lines of simple, perfectly working code with a TODO comment in an "unrelated" file?
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.
Sure, your points are valid.
Mine was simply about YAGNI ; )
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.
No offense meant or taken ; )
YAGNI is valid, but as always it's a tradeoff : )
So, ready to merge if there are no other objections?
Good catch, this was an error I made while writing the summary. Yes, it's mapped to the edit action (triggered by |
013c47f
to
05e3874
Compare
Can I do anything to move this PR forward / get it merged? :) Regarding testing, I am running this in my production setup (via the custom Flatpak mentioned below) and have edited a large number of tracks using this method, so I can confirm that it fully works as expected. PS: If you want to quickly try it out without having to build it yourself, you can use the
|
This PR is marked as stale because it has been open 90 days with no activity. |
case QEvent::MouseButtonRelease: { | ||
if (m_starCountToSave != StarRating::kInvalidStarCount) { |
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.
Just set the rating, setStarCount
does verifyStarCount(int)
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.
setStarCount
will trigger a debug assertion on kInvalidStarCount
, but m_starCountToSave == kInvalidStarCount
does not indicate a bug, but is a valid state here.
@@ -83,31 +89,132 @@ void StarEditor::paintEvent(QPaintEvent*) { | |||
cg = QPalette::Inactive; | |||
} | |||
|
|||
painter.save(); |
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.
we have PainterScope() for this
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.
Good point, will refactor.
Just did another shallow review, see my remarks. I probably won't do a more in-depth review soon, someone else needs to do that. |
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.
This is looking good! Just couple of minor feedback on code style we are trying to follow in Mixxx (new) code 😄
connect(pTableView, &QTableView::viewportEntered, this, &StarDelegate::cursorNotOverAnyCell); | ||
|
||
auto* pTrackTableView = qobject_cast<WTrackTableView*>(pTableView); | ||
if (pTrackTableView) { |
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.
Is this supposed to always used in a WTrackTableView
? If yes, it might be used to use a VERIFY_OR_DEBUG_ASSERT
instead of the condition so we can detect misusage of this delegated in the future
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'll do you one better: StarDelegate::StarDelegate()
should be updated to simply take WTrackTableView
in the first place - as its caller already does the mentioned VERIFY_OR_DEBUG_ASSERT
:
mixxx/src/library/basetracktablemodel.cpp
Lines 478 to 481 in 68d3796
auto* pTableView = qobject_cast<WTrackTableView*>(pParent); | |
VERIFY_OR_DEBUG_ASSERT(pTableView) { | |
return nullptr; | |
} |
@@ -43,8 +59,15 @@ QWidget* StarDelegate::createEditor(QWidget* parent, | |||
QStyleOptionViewItem newOption = option; | |||
initStyleOption(&newOption, index); | |||
|
|||
StarEditor* editor = | |||
new StarEditor(parent, m_pTableView, index, newOption, m_focusBorderColor); | |||
StarEditor* editor = new StarEditor(parent, |
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.
When using Qt fat pointer, we usually used our parented_ptr
wrapper to help with visualising the ownership
(Nit: rename editor
and parent
to pEditor
, and pParent
to help distinction to devs)
StarEditor* editor = new StarEditor(parent, | |
auto pEditor = make_parented<StarEditor>(parent, |
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.
Yeah, please use the pointer p
prefix for all lines you touch, and m_p
for pointer members.
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.
Will do. I just didn't have the time to update the code yet 😅
enum PersistentEditorState { | ||
PersistentEditor_NotOpen, | ||
PersistentEditor_Opening, | ||
PersistentEditor_Open, | ||
PersistentEditor_ShouldRestore, | ||
PersistentEditor_InDeferredRestore | ||
}; |
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.
We try not to use bare enums in new codebase, but instead scoped enum
enum PersistentEditorState { | |
PersistentEditor_NotOpen, | |
PersistentEditor_Opening, | |
PersistentEditor_Open, | |
PersistentEditor_ShouldRestore, | |
PersistentEditor_InDeferredRestore | |
}; | |
enum PersistentEditorState { | |
NotOpen, | |
Opening, | |
Open, | |
ShouldRestore, | |
InDeferredRestore | |
}; |
This will allow you to use PersistentEditorState::Open
instead of PersistentEditor_Open
in the implementation code
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.
Yeah, this code was written before I learned about the existence of enum class
...
|
||
// Change rating when certain keys are pressed | ||
// while we are in edit mode. | ||
QKeyEvent* ke = static_cast<QKeyEvent*>(event); |
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.
QKeyEvent* ke = static_cast<QKeyEvent*>(event); | |
QKeyEvent* pKey = static_cast<QKeyEvent*>(event); |
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.
QKeyEvent* ke = static_cast<QKeyEvent*>(event); | |
auto* pKey = static_cast<QKeyEvent*>(event); |
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.
rather pKE
, or pKeyEvent
case Qt::Key_0: { | ||
newRating = 0; | ||
break; | ||
} | ||
case Qt::Key_1: { | ||
newRating = 1; | ||
break; | ||
} | ||
case Qt::Key_2: { | ||
newRating = 2; | ||
break; | ||
} | ||
case Qt::Key_3: { | ||
newRating = 3; | ||
break; | ||
} | ||
case Qt::Key_4: { | ||
newRating = 4; | ||
break; | ||
} | ||
case Qt::Key_5: { | ||
newRating = 5; | ||
break; | ||
} | ||
case Qt::Key_6: { | ||
newRating = 6; | ||
break; | ||
} | ||
case Qt::Key_7: { | ||
newRating = 7; | ||
break; | ||
} | ||
case Qt::Key_8: { | ||
newRating = 8; | ||
break; | ||
} | ||
case Qt::Key_9: { | ||
newRating = 9; | ||
break; | ||
} |
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.
(Nit: the curly bracket in your switch statement are redundant - you should only need them if you have nested control flow)
void WTrackTableView::leaveEvent(QEvent* pEvent) { | ||
Q_UNUSED(pEvent); |
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.
void WTrackTableView::leaveEvent(QEvent* pEvent) { | |
Q_UNUSED(pEvent); | |
void WTrackTableView::leaveEvent(QEvent*) { |
Co-authored-by: Antoine Colombier <7086688+acolombier@users.noreply.github.com>
|
||
StarDelegate::StarDelegate(QTableView* pTableView) | ||
: TableItemDelegate(pTableView), | ||
m_isOneCellInEditMode(false) { | ||
m_persistentEditorState(PersistentEditor_NotOpen) { |
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.
m_persistentEditorState(PersistentEditor_NotOpen) { | |
m_persistentEditorState(PersistentEditorState::NotOpen) { |
Co-authored-by: Antoine Colombier <7086688+acolombier@users.noreply.github.com>
Thank you for the review comments. I'll try to get around integrating them ASAP. |
It is now possible to press
enterthe edit key (F2
, or, once #13148 is merged, 'R` resp. the confiugured edit key) on a selected "star rating" to then change the rating via the keyboard (either left/right arrow keys or numbers). Changing the rating using the mouse is still possible, and should work & interact with the keyboard edits as expected.But why is this PR so large?
The code for handling the actual keyboard edits is quite simple. Nearly all of the complexity relates to handling the interaction between editing via mouse hover, editing via keyboard and the assumptions of the Qt
QTableView
framework code.In short: Cells in the
QTableView
can either be in "edit mode" or "display mode". Pressing the edit key should enter edit mode, canceling or committing the edit should leave edit mode. BUT: The cells should also automatically enter edit mode when the mouse hovers over the cell, and exit it when the mouse leaves the cell. The rating should also temporarily adapt to the position of the mouse when hovering over the cell. But what happens e.g. when the user presses the edit key while already hovering over the cell?All in all, the desired (and hereby implemented) behavior boils down to the following state machine:
PlantUML Diagram
All commits have been extensively documented. If I may, I would recommend reviewing the commits in isolation.