Reorder the chain by dragging a row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,7 @@ both the style and the stage for that reason. `renderChain` takes the pool as a
|
|||||||
trailing argument; the command line builds no styles and passes none.
|
trailing argument; the command line builds no styles and passes none.
|
||||||
|
|
||||||
The interface builds the chain in the sidebar ("Effect chain": add from the
|
The interface builds the chain in the sidebar ("Effect chain": add from the
|
||||||
menu, reorder, remove, switch off with the check on the row, one stage selected
|
menu, reorder by dragging a row or with its arrows, remove, switch off with the check on the row, one stage selected
|
||||||
at a time with its params below), the CLI builds it from repeated `-e`:
|
at a time with its params below), the CLI builds it from repeated `-e`:
|
||||||
`-e scanlines --strip-height 6 -e blur --radius 5`, with `--disable NAME`
|
`-e scanlines --strip-height 6 -e blur --radius 5`, with `--disable NAME`
|
||||||
leaving a stage in place but skipping it. Flags share one namespace across all effects, so `buildChain`
|
leaving a stage in place but skipping it. Flags share one namespace across all effects, so `buildChain`
|
||||||
|
|||||||
+92
-7
@@ -1300,6 +1300,12 @@ row.stage-row:selected .stage-action,
|
|||||||
row.stage-row:focus-within .stage-action {
|
row.stage-row:focus-within .stage-action {
|
||||||
opacity: 1;
|
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";
|
)css";
|
||||||
|
|
||||||
Gtk::Widget& EffectorWindow::buildChainSection() {
|
Gtk::Widget& EffectorWindow::buildChainSection() {
|
||||||
@@ -1330,6 +1336,34 @@ Gtk::Widget& EffectorWindow::buildChainSection() {
|
|||||||
chainList_.signal_row_selected().connect(
|
chainList_.signal_row_selected().connect(
|
||||||
[this](Gtk::ListBoxRow*) { onStageSelected(); });
|
[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>();
|
auto* scroller = Gtk::make_managed<Gtk::ScrolledWindow>();
|
||||||
scroller->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
|
scroller->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
|
||||||
scroller->set_child(chainList_);
|
scroller->set_child(chainList_);
|
||||||
@@ -1409,12 +1443,13 @@ void EffectorWindow::removeStage(int index) {
|
|||||||
requestRender();
|
requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EffectorWindow::moveStage(int index, int delta) {
|
void EffectorWindow::moveStage(int from, int to) {
|
||||||
const int target = index + delta;
|
const auto count = static_cast<int>(stages_.size());
|
||||||
if (index < 0 || static_cast<std::size_t>(index) >= stages_.size()) return;
|
if (from < 0 || from >= count || to < 0 || to >= count || from == to) return;
|
||||||
if (target < 0 || static_cast<std::size_t>(target) >= stages_.size()) return;
|
ChainStage stage = std::move(stages_[static_cast<std::size_t>(from)]);
|
||||||
std::swap(stages_[static_cast<std::size_t>(index)], stages_[static_cast<std::size_t>(target)]);
|
stages_.erase(stages_.begin() + from);
|
||||||
selectedStage_ = target;
|
stages_.insert(stages_.begin() + to, std::move(stage));
|
||||||
|
selectedStage_ = to;
|
||||||
rebuildChainRows();
|
rebuildChainRows();
|
||||||
buildParamWidgets();
|
buildParamWidgets();
|
||||||
requestRender();
|
requestRender();
|
||||||
@@ -1488,7 +1523,7 @@ void EffectorWindow::rebuildChainRows() {
|
|||||||
if (delta == 0) {
|
if (delta == 0) {
|
||||||
removeStage(index);
|
removeStage(index);
|
||||||
} else {
|
} else {
|
||||||
moveStage(index, delta);
|
moveStage(index, index + delta);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (delta == -1) button->set_sensitive(index > 0);
|
if (delta == -1) button->set_sensitive(index > 0);
|
||||||
@@ -1497,6 +1532,23 @@ void EffectorWindow::rebuildChainRows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
row->set_child(*box);
|
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);
|
chainList_.append(*row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1506,6 +1558,39 @@ void EffectorWindow::rebuildChainRows() {
|
|||||||
scheduleChainSplitFit();
|
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
|
// 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.
|
// allocation, where moving the split would ask for another one.
|
||||||
void EffectorWindow::scheduleChainSplitFit() {
|
void EffectorWindow::scheduleChainSplitFit() {
|
||||||
|
|||||||
+5
-1
@@ -91,7 +91,11 @@ private:
|
|||||||
Gtk::Widget& buildAddMenu(); // the menu of effects that can be appended
|
Gtk::Widget& buildAddMenu(); // the menu of effects that can be appended
|
||||||
void appendStage(const EffectPtr& effect);
|
void appendStage(const EffectPtr& effect);
|
||||||
void removeStage(int index);
|
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.
|
// The check on a chain row: the stage stays where it is, and is skipped.
|
||||||
void setStageEnabled(int index, bool enabled);
|
void setStageEnabled(int index, bool enabled);
|
||||||
void rebuildChainRows();
|
void rebuildChainRows();
|
||||||
|
|||||||
@@ -122,6 +122,30 @@ bool addFromMenu(Gtk::MenuButton& add, const std::string& title) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The drop target the chain list reorders its rows with.
|
||||||
|
GtkDropTarget* chainDropTarget(Gtk::Widget& list) {
|
||||||
|
GListModel* controllers = gtk_widget_observe_controllers(list.gobj());
|
||||||
|
GtkDropTarget* found = nullptr;
|
||||||
|
for (guint i = 0; i < g_list_model_get_n_items(controllers) && found == nullptr; ++i) {
|
||||||
|
auto* controller = g_list_model_get_item(controllers, i);
|
||||||
|
if (GTK_IS_DROP_TARGET(controller)) found = GTK_DROP_TARGET(controller);
|
||||||
|
g_object_unref(controller);
|
||||||
|
}
|
||||||
|
g_object_unref(controllers);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What dropping the row of stage `index` at `y` does; a real drag needs a
|
||||||
|
// pointer, which the tests do not have.
|
||||||
|
void dropStage(GtkDropTarget* drop, int index, double y) {
|
||||||
|
GValue value = G_VALUE_INIT;
|
||||||
|
g_value_init(&value, G_TYPE_INT);
|
||||||
|
g_value_set_int(&value, index);
|
||||||
|
gboolean handled = FALSE;
|
||||||
|
g_signal_emit_by_name(drop, "drop", &value, 1.0, y, &handled);
|
||||||
|
g_value_unset(&value);
|
||||||
|
}
|
||||||
|
|
||||||
void runTests(Gtk::Window& window) {
|
void runTests(Gtk::Window& window) {
|
||||||
const auto& effects = effector::effects();
|
const auto& effects = effector::effects();
|
||||||
|
|
||||||
@@ -239,6 +263,43 @@ void runTests(Gtk::Window& window) {
|
|||||||
"Remove drops the stage and renumbers the rest");
|
"Remove drops the stage and renumbers the rest");
|
||||||
check(!sources->get_visible(), "and the sources hide again");
|
check(!sources->get_visible(), "and the sources hide again");
|
||||||
|
|
||||||
|
std::cout << "drag and drop\n";
|
||||||
|
addFromMenu(*add, "Scanlines");
|
||||||
|
for (const auto& effect : effects) {
|
||||||
|
if (effect->title() != "Blur" && effect->title() != "Scanlines") {
|
||||||
|
addFromMenu(*add, effect->title());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GtkDropTarget* drop = chainDropTarget(*chainList);
|
||||||
|
check(drop != nullptr, "the chain list takes drops");
|
||||||
|
if (drop != nullptr && rowCount(*chainList) == 3) {
|
||||||
|
const std::string first = rowTitle(*chainList, 0).substr(3);
|
||||||
|
const std::string second = rowTitle(*chainList, 1).substr(3);
|
||||||
|
const std::string third = rowTitle(*chainList, 2).substr(3);
|
||||||
|
const double bottom = chainList->get_row_at_index(2)->get_height() * 3.0;
|
||||||
|
GdkDragAction action = GDK_ACTION_NONE;
|
||||||
|
g_signal_emit_by_name(drop, "motion", 1.0, bottom, &action);
|
||||||
|
check(chainList->get_row_at_index(2)->has_css_class("drop-after"),
|
||||||
|
"dragging past the last row marks its bottom edge");
|
||||||
|
dropStage(drop, 0, bottom);
|
||||||
|
pump();
|
||||||
|
check(rowTitle(*chainList, 0) == "1. " + second && rowTitle(*chainList, 1) == "2. " + third &&
|
||||||
|
rowTitle(*chainList, 2) == "3. " + first,
|
||||||
|
"dropping the first stage past the last moves it to the end");
|
||||||
|
check(chainList->get_selected_row() == chainList->get_row_at_index(2),
|
||||||
|
"and selects it");
|
||||||
|
check(!chainList->get_row_at_index(2)->has_css_class("drop-after"),
|
||||||
|
"and clears the mark");
|
||||||
|
dropStage(drop, 2, 1.0);
|
||||||
|
pump();
|
||||||
|
check(rowTitle(*chainList, 0) == "1. " + first && rowTitle(*chainList, 1) == "2. " + second,
|
||||||
|
"dropping on the top edge of the first row moves it to the front");
|
||||||
|
}
|
||||||
|
for (int i = rowCount(*chainList) - 1; i > 0; --i) {
|
||||||
|
if (auto* removeRow = withTooltip(*chainList->get_row_at_index(i), "Remove")) press(*removeRow);
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "many stages\n";
|
std::cout << "many stages\n";
|
||||||
for (int i = 0; i < 4; ++i) addFromMenu(*add, effects[static_cast<std::size_t>(i) % effects.size()]->title());
|
for (int i = 0; i < 4; ++i) addFromMenu(*add, effects[static_cast<std::size_t>(i) % effects.size()]->title());
|
||||||
pump();
|
pump();
|
||||||
|
|||||||
Reference in New Issue
Block a user