CueControl: reject hotcue position writes when unset - #16787
CueControl: reject hotcue position writes when unset#16787gary-gdbsystems wants to merge 2 commits into
Conversation
The hotcue_X_position control was a plain writable ControlObject, so a script or controller could write to it even when no hotcue was set. The underlying cue never moved (CueControl::hotcuePositionChanged rejects a null cue), but the raw set() still left a bogus value in the control, which was then reported back on read. Route external writes through connectValueChangeRequest, mirroring the existing hotcue_X_color pattern. The new handler rejects the request unless a hotcue is set, otherwise it delegates to the existing validated move path; the resulting cue change syncs the control back to the cue's actual position. Because the control is now confirm-required, engine-side updates use setAndConfirm(). Note: unlike the earlier approach in mixxxdj#13446, this does not call setReadOnly() (which would log a spurious "is read-only" warning on every legitimate move) and guards on the m_pCue pointer rather than the control value (which misfires at position 0). Resolves the request in mixxxdj#10409. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cr7pt0gr4ph7
left a comment
There was a problem hiding this comment.
The core logic looks good, but should also be extended to cover hotcue end position.
| // Use setAndConfirm() because m_hotcuePosition is confirm-required: a plain | ||
| // set() would be intercepted as a change request instead of updating the | ||
| // engine-authoritative value. | ||
| m_hotcuePosition->setAndConfirm(position.toEngineSamplePosMaybeInvalid()); |
There was a problem hiding this comment.
The verbose comment can be removed:
| // Use setAndConfirm() because m_hotcuePosition is confirm-required: a plain | |
| // set() would be intercepted as a change request instead of updating the | |
| // engine-authoritative value. | |
| m_hotcuePosition->setAndConfirm(position.toEngineSamplePosMaybeInvalid()); | |
| m_hotcuePosition->setAndConfirm(position.toEngineSamplePosMaybeInvalid()); |
| // The position may only be moved by dragging an existing hotcue, so route | ||
| // write requests through a handler that rejects them unless the hotcue is | ||
| // set. This also makes the control confirm-required, so engine-side updates | ||
| // must use setAndConfirm() (see HotcueControl::setPosition()). |
There was a problem hiding this comment.
The verbose comment can be removed:
| // The position may only be moved by dragging an existing hotcue, so route | |
| // write requests through a handler that rejects them unless the hotcue is | |
| // set. This also makes the control confirm-required, so engine-side updates | |
| // must use setAndConfirm() (see HotcueControl::setPosition()). |
| m_hotcueEndPosition = std::make_unique<ControlObject>( | ||
| keyForControl(QStringLiteral("endposition"))); | ||
| connect(m_hotcueEndPosition.get(), | ||
| &ControlObject::valueChanged, | ||
| this, | ||
| &HotcueControl::slotHotcueEndPositionChanged, | ||
| Qt::DirectConnection); |
There was a problem hiding this comment.
The hotcue end position should also reject writes when the hotcue is not set:
| m_hotcueEndPosition = std::make_unique<ControlObject>( | |
| keyForControl(QStringLiteral("endposition"))); | |
| connect(m_hotcueEndPosition.get(), | |
| &ControlObject::valueChanged, | |
| this, | |
| &HotcueControl::slotHotcueEndPositionChanged, | |
| Qt::DirectConnection); | |
| m_hotcuePosition->connectValueChangeRequest( | |
| this, | |
| &HotcueControl::slotHotcueEndPositionChangeRequest, | |
| Qt::DirectConnection); |
| // Reject the change if no hotcue is set: the position can only be moved by | ||
| // dragging an existing hotcue, not to create one (see issue #10409). |
There was a problem hiding this comment.
The overly verbose comment can be reduced to:
| // Reject the change if no hotcue is set: the position can only be moved by | |
| // dragging an existing hotcue, not to create one (see issue #10409). | |
| // Reject the change if no hotcue is set |
| } | ||
|
|
||
| void HotcueControl::slotHotcueEndPositionChanged(double newEndPosition) { | ||
| emit hotcueEndPositionChanged(this, newEndPosition); |
There was a problem hiding this comment.
The hotcue end position should also reject writes if the hotcue is not set (and maybe also if the current hotcue type doesn't support an end position?):
| emit hotcueEndPositionChanged(this, newEndPosition); | |
| // Reject the change if no hotcue is set | |
| if (!m_pCue) { | |
| return; | |
| } | |
| emit hotcueEndPositionChanged(this, newEndPosition); |
| @@ -138,6 +138,7 @@ class HotcueControl : public QObject { | |||
| void slotHotcueSwap(double v); | |||
| void slotHotcueEndPositionChanged(double newPosition); | |||
There was a problem hiding this comment.
The hotcue end position should also reject change requests:
| void slotHotcueEndPositionChanged(double newPosition); | |
| void slotHotcueEndPositionChanged(double newPosition); | |
| void slotHotcueEndPositionChangeRequest(double newPosition); |
| .isValid()); | ||
| } | ||
|
|
||
| TEST_F(HotcueControlTest, PositionChangeRequestRejectedWhenUnset) { |
There was a problem hiding this comment.
This should be extended to also test that the hotcue end position rejects changes when not set.
There was a problem hiding this comment.
| m_hotcueEndPosition->setAndConfirm(endPosition.toEngineSamplePosMaybeInvalid()); |
Address review feedback on the hotcue-position guard: apply the same confirm-required + reject-when-unset treatment to hotcue_endposition, so writing it with no hotcue set no longer stores a bogus value. Route endposition writes through slotHotcueEndPositionChangeRequest (rejects when m_pCue is null), and use setAndConfirm() in setEndPosition() since the control is now confirm-required. Also trim the verbose comments on the position path per review.
|
Thanks. Extended the reject-when-unset guard to Only saved loops have an end position, so I can also reject |
Just for completeness: Jump hotcues also have an endposition, which is used to store the position that is being jumped to. |
Summary
Fixes #10409.
hotcue_X_positionwas a plain writableControlObject, so a script or controller could write to it even when no hotcue was set. The underlying cue never moved —CueControl::hotcuePositionChanged()already rejects a null cue — but the rawset()still stored a bogus value in the control, which was then reported back on read.Approach
Route external writes through
connectValueChangeRequest, mirroring the existinghotcue_X_colorpattern:slotHotcuePositionChangeRequest()rejects the request unless a hotcue is set (guards on them_pCuepointer).CueControl::hotcuePositionChanged), so moving an existing hotcue still works; the resulting cue change syncs the control back to the cue's actual (validated) position vialoadCuesFromTrack().HotcueControl::setPosition()usessetAndConfirm().This supersedes the earlier stalled attempt in #13446. It intentionally does not call
setReadOnly()(which, combined with a request handler, would log a spuriousis read-only. Ignoringwarning on every legitimate hotcue move) and guards onm_pCuerather than the control value (the old!m_hotcuePosition->get()check misfires at position 0).Changes
src/engine/controls/cuecontrol.cpp— wire the change-request handler, addslotHotcuePositionChangeRequest(), usesetAndConfirm()insetPosition().src/engine/controls/cuecontrol.h— slot declaration.src/test/hotcuecontrol_test.cpp— two tests: a write is rejected when no hotcue is set; a write moves a set hotcue.Testing
HotcueControlTest(32 tests) and all cue/loop suites (113 tests) pass.