Design View: drop the undo blocks around writes that mint nothing; the membership actions own theirs

Flag writes are silent from a timer, so a block around a reapply or the reconcile
repair could only manufacture a point. The real switch's span is unchanged.
This commit is contained in:
2026-08-07 14:41:17 -04:00
parent 67d9ec3da9
commit 39529a5ac4
8 changed files with 141 additions and 144 deletions
+63 -27
View File
@@ -27,6 +27,7 @@
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
#include "shell/view/view.h" // applyMode (D2 shell)
#include "shell/view/view_lanes.h" // mintManagedLanes
#include "shell/view/view_park.h" // kViewUndoMask
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountSelectedTracks
@@ -201,40 +202,76 @@ void doActivateMode(const std::string& modeId) {
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
}
// The block each membership-edit action below owns. The reapply they drive opens none
// of its own — nothing it writes mints a point unaided — so a user-fired tag stays one
// Ctrl-Z only because the ACTION spans it, and the label names the act the user
// 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.
class ActionUndoBlock {
public:
ActionUndoBlock(std::string label, int mask) : label_(std::move(label)), mask_(mask) {
Undo_BeginBlock2(nullptr);
}
~ActionUndoBlock() { Undo_EndBlock2(nullptr, label_.c_str(), mask_); }
ActionUndoBlock(const ActionUndoBlock&) = delete;
ActionUndoBlock& operator=(const ActionUndoBlock&) = delete;
private:
std::string label_;
int mask_;
};
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
// cross-mode case is show-both).
void doTag(const std::string& modeId) {
for (const std::string& g : selectedTrackGuids())
g_session->view().membership().tag(g, modeId);
reapplyActiveMode();
const std::vector<std::string> guids = selectedTrackGuids();
if (guids.empty()) return; // nothing selected — no act, no undo point (cf. doMoveItems)
{
ActionUndoBlock undo("ReaSampler: tag selected tracks -> " + modeId, kViewUndoMask);
for (const std::string& g : guids) g_session->view().membership().tag(g, modeId);
reapplyActiveMode();
}
persistViewState(PersistScope::Full);
}
// Shared body behind "Untag selected" and "Tag -> Arrange" — Arrange is the absence
// of a tag, so the two actions are the same act.
void doUntag() {
for (const std::string& g : selectedTrackGuids())
g_session->view().membership().untag(g);
reapplyActiveMode();
const std::vector<std::string> guids = selectedTrackGuids();
if (guids.empty()) return;
{
ActionUndoBlock undo("ReaSampler: untag selected tracks", kViewUndoMask);
for (const std::string& g : guids) g_session->view().membership().untag(g);
reapplyActiveMode();
}
persistViewState(PersistScope::Full);
}
// Flips each track's pin independently — the honest semantics of a toggle on a
// multi-selection (a mixed selection converges toward uniform only if it already was).
void doShowBoth() {
MembershipIndex& m = g_session->view().membership();
for (const std::string& g : selectedTrackGuids())
m.setShowBoth(g, !m.isShowBoth(g));
reapplyActiveMode();
const std::vector<std::string> guids = selectedTrackGuids();
if (guids.empty()) return;
{
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();
}
persistViewState(PersistScope::Full);
}
// Retag the current ITEM selection to `targetMode` (empty => untag -> Arrange
// default). planItemRetag decides which items to retag (manual-lane items are
// EXEMPT), upholding the managed-lanes-only invariant. Wrapped in ONE Undo block.
// EXEMPT), upholding the managed-lanes-only invariant.
//
// UNDO/SAVE ordering: persistViewState may pop a Save-As dialog, which must NOT sit
// inside the Undo block, so we close the block first, then persist.
// The mask stays UNDO_STATE_ALL rather than the view mask the three track actions
// close on: this one can mint lanes, and the fixed-lane properties are only ASSUMED to
// ride UNDO_STATE_ITEMS (see kViewUndoMask).
void doMoveItems(const std::string& targetMode) {
const std::vector<RetagItem> selected = selectedRetagItems();
const std::vector<ItemRetagOp> ops = planItemRetag(selected, targetMode);
@@ -242,21 +279,20 @@ void doMoveItems(const std::string& targetMode) {
MembershipIndex& membership = g_session->view().membership();
Undo_BeginBlock2(nullptr);
for (const ItemRetagOp& op : ops) {
if (op.untag) membership.untag(op.guid);
else membership.tag(op.guid, op.modeId);
{
ActionUndoBlock undo(targetMode.empty()
? std::string("ReaSampler: untag selected items")
: "ReaSampler: move selected items -> " + targetMode,
-1);
for (const ItemRetagOp& op : ops) {
if (op.untag) membership.untag(op.guid);
else membership.tag(op.guid, op.modeId);
}
// 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();
}
// 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();
const std::string label =
targetMode.empty()
? std::string("ReaSampler: untag selected items")
: std::string("ReaSampler: move selected items -> ") + targetMode;
Undo_EndBlock2(nullptr, label.c_str(), -1);
persistViewState(PersistScope::Full);
}