Reorder the chain by dragging a row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-24 00:43:51 +02:00
co-authored by Claude Opus 5.5
parent 81ca17463e
commit 536b8620ea
4 changed files with 159 additions and 9 deletions
+92 -7
View File
@@ -1300,6 +1300,12 @@ row.stage-row:selected .stage-action,
row.stage-row:focus-within .stage-action {
opacity: 1;
}
row.stage-row.drop-before {
box-shadow: inset 0 2px @theme_fg_color;
}
row.stage-row.drop-after {
box-shadow: inset 0 -2px @theme_fg_color;
}
)css";
Gtk::Widget& EffectorWindow::buildChainSection() {
@@ -1330,6 +1336,34 @@ Gtk::Widget& EffectorWindow::buildChainSection() {
chainList_.signal_row_selected().connect(
[this](Gtk::ListBoxRow*) { onStageSelected(); });
// A row is dragged by its index (see rebuildChainRows) and dropped between
// two rows, which a line along the edge of the row below or above shows.
auto drop = Gtk::DropTarget::create(Glib::Value<int>::value_type(), Gdk::DragAction::MOVE);
drop->signal_motion().connect(
[this](double, double y) {
showChainDropSlot(chainDropSlot(y));
return Gdk::DragAction::MOVE;
},
false);
drop->signal_leave().connect([this] { showChainDropSlot(-1); });
drop->signal_drop().connect(
[this](const Glib::ValueBase& value, double, double y) {
showChainDropSlot(-1);
Glib::Value<int> from;
from.init(value.gobj());
const int index = from.get();
const int slot = chainDropSlot(y);
// The slot counts the dragged row itself, which leaves its place
// before it takes the new one.
const int to = slot > index ? slot - 1 : slot;
// Not while the drag is still finishing: the move rebuilds the
// rows, the dragged one and its drag source with them.
Glib::signal_idle().connect_once([this, index, to] { moveStage(index, to); });
return true;
},
false);
chainList_.add_controller(drop);
auto* scroller = Gtk::make_managed<Gtk::ScrolledWindow>();
scroller->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
scroller->set_child(chainList_);
@@ -1409,12 +1443,13 @@ void EffectorWindow::removeStage(int index) {
requestRender();
}
void EffectorWindow::moveStage(int index, int delta) {
const int target = index + delta;
if (index < 0 || static_cast<std::size_t>(index) >= stages_.size()) return;
if (target < 0 || static_cast<std::size_t>(target) >= stages_.size()) return;
std::swap(stages_[static_cast<std::size_t>(index)], stages_[static_cast<std::size_t>(target)]);
selectedStage_ = target;
void EffectorWindow::moveStage(int from, int to) {
const auto count = static_cast<int>(stages_.size());
if (from < 0 || from >= count || to < 0 || to >= count || from == to) return;
ChainStage stage = std::move(stages_[static_cast<std::size_t>(from)]);
stages_.erase(stages_.begin() + from);
stages_.insert(stages_.begin() + to, std::move(stage));
selectedStage_ = to;
rebuildChainRows();
buildParamWidgets();
requestRender();
@@ -1488,7 +1523,7 @@ void EffectorWindow::rebuildChainRows() {
if (delta == 0) {
removeStage(index);
} else {
moveStage(index, delta);
moveStage(index, index + delta);
}
});
if (delta == -1) button->set_sensitive(index > 0);
@@ -1497,6 +1532,23 @@ void EffectorWindow::rebuildChainRows() {
}
row->set_child(*box);
// The row itself is what is dragged, under the pointer where it was
// picked up; a press that goes nowhere is still a click on the row.
auto drag = Gtk::DragSource::create();
drag->set_actions(Gdk::DragAction::MOVE);
drag->signal_prepare().connect(
[source = drag.get(), row, index](double x, double y) {
source->set_icon(Gtk::WidgetPaintable::create(*row), static_cast<int>(x),
static_cast<int>(y));
Glib::Value<int> value;
value.init(Glib::Value<int>::value_type());
value.set(index);
return Gdk::ContentProvider::create(value);
},
false);
row->add_controller(drag);
chainList_.append(*row);
}
@@ -1506,6 +1558,39 @@ void EffectorWindow::rebuildChainRows() {
scheduleChainSplitFit();
}
// Where a row dropped at `y` goes: before the row whose upper half `y` is in,
// or after the last row.
int EffectorWindow::chainDropSlot(double y) const {
for (int i = 0;; ++i) {
auto* row = chainList_.get_row_at_index(i);
if (row == nullptr) return i;
graphene_rect_t bounds;
if (!gtk_widget_compute_bounds(GTK_WIDGET(row->gobj()), GTK_WIDGET(chainList_.gobj()),
&bounds))
continue;
if (y < bounds.origin.y + bounds.size.height / 2) return i;
}
}
// Marks the edge a drop into `slot` lands on; -1 clears the mark.
void EffectorWindow::showChainDropSlot(int slot) {
const int count = static_cast<int>(stages_.size());
for (int i = 0; auto* row = chainList_.get_row_at_index(i); ++i) {
const bool before = i == slot;
const bool after = slot == count && i == count - 1;
if (before) {
row->add_css_class("drop-before");
} else {
row->remove_css_class("drop-before");
}
if (after) {
row->add_css_class("drop-after");
} else {
row->remove_css_class("drop-after");
}
}
}
// Once the rows are laid out, not in the middle of a rebuild or of an
// allocation, where moving the split would ask for another one.
void EffectorWindow::scheduleChainSplitFit() {
+5 -1
View File
@@ -91,7 +91,11 @@ private:
Gtk::Widget& buildAddMenu(); // the menu of effects that can be appended
void appendStage(const EffectPtr& effect);
void removeStage(int index);
void moveStage(int index, int delta);
void moveStage(int from, int to); // `to` is the index it ends up at
// Dragging a row: the slot between rows it would drop into, and the line
// that shows it.
int chainDropSlot(double y) const;
void showChainDropSlot(int slot);
// The check on a chain row: the stage stays where it is, and is skipped.
void setStageEnabled(int index, bool enabled);
void rebuildChainRows();