Design View: no undo block may open over the reclaim span, and a reapply closes none
Every opener ends the span first, so a tag fired mid-drain no longer hijacks the switch's point. One stated criterion governs both lane sites; no-op actions discard.
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
|
||||
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
|
||||
#include "shell/view/fx_reclaim.h" // fxReclaimEndUndoSpan — no block opens over the span
|
||||
#include "shell/view/view.h" // applyMode (D2 shell)
|
||||
#include "shell/view/view_lanes.h" // mintManagedLanes
|
||||
#include "shell/view/view_park.h" // kViewUndoMask
|
||||
@@ -105,12 +106,16 @@ std::vector<std::string> selectedTrackGuids() {
|
||||
}
|
||||
|
||||
// Reapplies the CURRENT active mode so a membership mutation takes visible effect
|
||||
// immediately (park/unpark/re-derive parents). The return is deliberately discarded: a
|
||||
// refused reapply leaves the membership edit standing and unapplied, and the user's next
|
||||
// edit or mode switch reapplies it. What must NOT be dropped with it is the reclaimer's
|
||||
// queue, which is why applyMode invalidates on its refusal path too (view.cpp).
|
||||
void reapplyActiveMode() {
|
||||
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
|
||||
// immediately (park/unpark/re-derive parents), answering whether it wrote any project
|
||||
// state — the caller's block mints only if something did. `applied` is deliberately
|
||||
// discarded: a refused reapply leaves the membership edit standing and unapplied, and
|
||||
// the user's next edit or mode switch reapplies it (a refusal writes nothing, so it
|
||||
// reports false here and the block discards). What must NOT be dropped with it is the
|
||||
// reclaimer's queue, which is why applyMode invalidates on its refusal path too
|
||||
// (view.cpp).
|
||||
bool reapplyActiveMode() {
|
||||
return applyMode(g_session->view(), g_session->view().activeModeId(), nullptr)
|
||||
.wroteProjectState;
|
||||
}
|
||||
|
||||
constexpr int kFreeModeFixedLanes = 2; // I_FREEMODE value; mirrors the shell's constant
|
||||
@@ -189,15 +194,21 @@ void doToggleMode() {
|
||||
const std::string target =
|
||||
nextModeId(g_session->view().modes(), g_session->view().activeModeId());
|
||||
if (target.empty()) return; // no modes to cycle to (degenerate)
|
||||
if (!applyMode(g_session->view(), target, nullptr)) { reportModeSwitchRefused(); return; }
|
||||
if (!applyMode(g_session->view(), target, nullptr).applied) {
|
||||
reportModeSwitchRefused();
|
||||
return;
|
||||
}
|
||||
persistViewState(PersistScope::ViewOnly);
|
||||
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
|
||||
}
|
||||
|
||||
// Direct jump to a named mode. applyMode is a no-op (returns false, no mutation) if
|
||||
// Direct jump to a named mode. applyMode is a no-op (`applied` false, no mutation) if
|
||||
// the id is unregistered or the transport is running, so both fail safe.
|
||||
void doActivateMode(const std::string& modeId) {
|
||||
if (!applyMode(g_session->view(), modeId, nullptr)) { reportModeSwitchRefused(); return; }
|
||||
if (!applyMode(g_session->view(), modeId, nullptr).applied) {
|
||||
reportModeSwitchRefused();
|
||||
return;
|
||||
}
|
||||
persistViewState(PersistScope::ViewOnly);
|
||||
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
|
||||
}
|
||||
@@ -208,18 +219,34 @@ void doActivateMode(const std::string& modeId) {
|
||||
// actually fired rather than the reapply it happened to trigger. Every one of them
|
||||
// closes the block before persistViewState, which can pop a modal Save-As. Stack RAII
|
||||
// so a throw between the two calls cannot leave the block open in the user's project.
|
||||
//
|
||||
// ENDING THE RECLAIM SPAN IS PART OF OPENING, and every action here inherits it from
|
||||
// this one constructor. A mode switch fired seconds ago has left the span open; a block
|
||||
// opened over it makes each close pair with the other's open, collapsing the switch
|
||||
// into this tag — mechanism and full rule in src/shell/view/CLAUDE.md.
|
||||
//
|
||||
// `markWrote` decides whether this block mints at all: re-tagging tracks that already
|
||||
// sit in that mode writes nothing to the project, and the ("", 0) discard form is real
|
||||
// from a hand-fired action (DAW-measured, same doc). The membership move itself is
|
||||
// never in the point either way — persistViewState runs after the close.
|
||||
class ActionUndoBlock {
|
||||
public:
|
||||
ActionUndoBlock(std::string label, int mask) : label_(std::move(label)), mask_(mask) {
|
||||
fxReclaimEndUndoSpan();
|
||||
Undo_BeginBlock2(nullptr);
|
||||
}
|
||||
~ActionUndoBlock() { Undo_EndBlock2(nullptr, label_.c_str(), mask_); }
|
||||
~ActionUndoBlock() {
|
||||
if (wrote_) Undo_EndBlock2(nullptr, label_.c_str(), mask_);
|
||||
else Undo_EndBlock2(nullptr, "", 0);
|
||||
}
|
||||
void markWrote() { wrote_ = true; }
|
||||
ActionUndoBlock(const ActionUndoBlock&) = delete;
|
||||
ActionUndoBlock& operator=(const ActionUndoBlock&) = delete;
|
||||
|
||||
private:
|
||||
std::string label_;
|
||||
int mask_;
|
||||
bool wrote_ = false;
|
||||
};
|
||||
|
||||
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
|
||||
@@ -231,7 +258,7 @@ void doTag(const std::string& modeId) {
|
||||
{
|
||||
ActionUndoBlock undo("ReaSampler: tag selected tracks -> " + modeId, kViewUndoMask);
|
||||
for (const std::string& g : guids) g_session->view().membership().tag(g, modeId);
|
||||
reapplyActiveMode();
|
||||
if (reapplyActiveMode()) undo.markWrote();
|
||||
}
|
||||
persistViewState(PersistScope::Full);
|
||||
}
|
||||
@@ -245,7 +272,7 @@ void doUntag() {
|
||||
{
|
||||
ActionUndoBlock undo("ReaSampler: untag selected tracks", kViewUndoMask);
|
||||
for (const std::string& g : guids) g_session->view().membership().untag(g);
|
||||
reapplyActiveMode();
|
||||
if (reapplyActiveMode()) undo.markWrote();
|
||||
}
|
||||
persistViewState(PersistScope::Full);
|
||||
}
|
||||
@@ -260,7 +287,7 @@ void doShowBoth() {
|
||||
ActionUndoBlock undo("ReaSampler: toggle show-both on selected tracks", kViewUndoMask);
|
||||
MembershipIndex& m = g_session->view().membership();
|
||||
for (const std::string& g : guids) m.setShowBoth(g, !m.isShowBoth(g));
|
||||
reapplyActiveMode();
|
||||
if (reapplyActiveMode()) undo.markWrote();
|
||||
}
|
||||
persistViewState(PersistScope::Full);
|
||||
}
|
||||
@@ -290,8 +317,11 @@ void doMoveItems(const std::string& targetMode) {
|
||||
}
|
||||
// Re-drive the same minting/apply path auto-tag uses: mint/split lanes for any
|
||||
// track whose items now span modes, then reassert active-mode lane visibility.
|
||||
mintManagedLanes(g_session->view(), nullptr);
|
||||
reapplyActiveMode();
|
||||
// Either half writing is enough to mint; a retag that split no lanes and drove
|
||||
// no flag leaves nothing for a point to hold.
|
||||
const bool minted = mintManagedLanes(g_session->view(), nullptr);
|
||||
const bool reapplied = reapplyActiveMode();
|
||||
if (minted || reapplied) undo.markWrote();
|
||||
}
|
||||
|
||||
persistViewState(PersistScope::Full);
|
||||
|
||||
Reference in New Issue
Block a user