Compare commits

..
Author SHA1 Message Date
Lam Wei Lun df38b0351d fix(gui): scroll the main preset list while its submenu is open
The filament list groups presets by vendor: hovering a vendor in the main
popup opens a side popup with that vendor's materials. On GTK the side popup
holds the popup grab, and gtk_main_do_event hands every GDK_SCROLL of the popup
chain to the grab widget. GDK names the event for the window under the cursor,
so the side popup received the wheel events raised over the main list and
consumed them: the main list could not be scrolled while a vendor was hovered.
The problem shows on both X11 and Wayland, because the rerouting happens in
GTK, not in the compositor.

The side popup now inspects the event's GDK window and, when the event belongs
to the main popup, replays it there. The replay goes through
gtk_propagate_event(GetConnectWidget()): wxGTK connects its mouse handlers to
the inner client widget of a popup (a wxPizza child of the outer GtkWindow), so
a signal emitted on the outer GtkWindow reaches no handler at all.

The widget reference in .claude/skills/orca-wxwidgets records the mechanism and
the connect-widget fact.
2026-10-14 11:04:49 +08:00
8 changed files with 75 additions and 73 deletions
@@ -570,6 +570,11 @@ Details [source] (`Widgets/PopupWindow.cpp`).
- On GTK the sub binds an idle handler. While both are shown and the cursor is over main but not over the
sub, it builds a `wxMouseEvent(wxEVT_MOTION)` from `wxGetMousePosition()`, calls `mainDropDown->mouseMove(ev)`
**directly** (nothing is posted), and calls `e.RequestMore()` to keep polling.
- On GTK the sub also receives the wheel events the user raises over main: GDK sends a `wl_pointer.axis` only
to the surface the pointer is over, and GTK then hands that `GDK_SCROLL` to the chain's grab widget, which is
the sub. The sub installs a `GtkWidget::event` handler that replays the event on main with
`gtk_propagate_event(main->GetConnectWidget(), event)` when the event's GDK window belongs to main
(`DropDown::messureSize`).
- **GTK wrapper sizing.** `messureSize` calls `gtk_window_resize` on `m_widget` after `SetSize`, because wxGTK's
`DoSetSize` only uses `gtk_widget_set_size_request` ("gtk_window_resize does not work for GTK_WINDOW_POPUP",
`src/gtk/popupwin.cpp:181-188`).
@@ -607,6 +612,32 @@ Details [source] (`Widgets/PopupWindow.cpp`).
```
Cite: c0ae2bda99, b9ff15054f (`DropDown::mouseMove`, `DropDown::messureSize`, `DropDown::ShouldDismissOnTopWindowDeactivate`,
`PopupWindow::ShouldDismissOnTopWindowDeactivate`).
- **Rule:** On GTK, route a chained popup's wheel event by the GDK window it names, not by the grab widget and not
by the global cursor position.
**Why:** While the sub holds the grab, `gtk_main_do_event` hands every `GDK_SCROLL` of the chain to the grab
widget (the sub) with its original, un-rebased coordinates [source] (`gtkmain.c:1789-1792`, 1923-1924), while
GDK Wayland sends the axis to the surface under the pointer only (`pointer_handle_axis` → `create_scroll_event`,
`gdkdevice-wayland.c:1929, 1527`). The sub therefore sees and consumes wheel events raised over main, so the
main list never scrolls, on X11 and Wayland alike. A hit test against `wxGetMousePosition()` cannot replace the
window test: global coordinates are unreliable on Wayland (`mouse-keyboard-focus.md`). `DropDown` connects a
`GtkWidget::event` handler on the sub's `GtkWindow` (its `m_widget`), which is emitted before wx's
`::scroll-event` handler on the same widget and suppresses it when it returns TRUE (`gtkwidget.c:7696`), then
delivers the event with `gtk_propagate_event(main->GetConnectWidget(), event)`. Returning TRUE there also stops
the parent-chain propagation.
```cpp
// Wrong: emitted on the popup's outer GtkWindow, where wx has no handlers at all
g_signal_emit_by_name(main->m_widget, "scroll-event", event, &handled);
// Right (wxGTK): replay GTK's own delivery on the widget wx listens to
gtk_propagate_event(main->GetConnectWidget(), event);
```
**Where wx listens [source].** `wxPopupWindow::Create` builds a popup from two widgets: `m_widget` is the outer
`GtkWindow` and `m_wxwindow` is a `wxPizza` child added to it. `wxWindowGTK::GetConnectWidget()` is
`m_wxwindow ? m_wxwindow : m_widget`, and `ConnectWidget()` — which binds `button_press_event`, `motion_notify_event`
and `scroll_event` — is called on that widget (`src/gtk/window.cpp` `Create`, `GetConnectWidget`). A signal emitted
on the outer `GtkWindow` therefore reaches no wx handler; only events GTK delivers to the inner widget, or to a
descendant of the grab widget, are seen by wx. `GTKGetConnectWindow()`/`GTKGetMainWindow()` are the matching
GdkWindows.
Cite: `DropDown::messureSize`, `DropDown::mouseWheelMoved`.
- **Rule:** When shrinking a dropdown to fit above the display bottom, give up if fewer than two rows would
remain.
**Why:** On Wayland the popup can land where almost no vertical space is left. Clamping to that sliver made an
+2 -3
View File
@@ -2602,13 +2602,12 @@ void GCodeProcessorResult::reset() {
//BBS: add mutex for protection of gcode result
lock();
// moves and lines_ends grow with the print: free them, as no reset result reuses the capacity.
clear_and_shrink(moves);
moves.clear();
plate_mass = {};
object_masses.clear();
body_masses.clear();
support_masses.clear();
clear_and_shrink(lines_ends);
lines_ends.clear();
printable_area = Pointfs();
//BBS: add bed exclude area
bed_exclude_area = Pointfs();
+7 -43
View File
@@ -3905,8 +3905,14 @@ void PresetBundle::export_selections(AppConfig &config)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1%, print %2%, filaments[0] %3% ")%printers.get_selected_preset_name() % prints.get_selected_preset_name() %filament_presets[0];
}
size_t PresetBundle::resize_project_filament_slots(size_t n)
void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
{
unsigned old_filament_count = this->filament_presets.size();
if (n > old_filament_count && old_filament_count != 0)
filament_presets.resize(n, filament_presets.back());
else {
filament_presets.resize(n);
}
ConfigOptionStrings* filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
@@ -3932,25 +3938,9 @@ size_t PresetBundle::resize_project_filament_slots(size_t n)
ams_multi_color_filment.resize(n);
resize_mixed_filament_metadata(project_config, old_slot_count, n);
return old_slot_count;
}
void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
{
unsigned old_filament_count = this->filament_presets.size();
if (n > old_filament_count && old_filament_count != 0)
filament_presets.resize(n, filament_presets.back());
else {
filament_presets.resize(n);
}
const size_t old_slot_count = resize_project_filament_slots(n);
//BBS set new filament color to new_color
if (!new_color.empty()) {
ConfigOptionStrings* filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
for (size_t i = old_slot_count; i < n; i++) {
filament_color->values[i] = new_color;
filament_multi_color->values[i] = new_color;
@@ -5387,23 +5377,6 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers");
out.option<ConfigOptionStrings>("extruder_ams_count", true)->values = save_extruder_ams_count_to_string(this->extruder_ams_counts);
// The slice config is consumed by code that indexes per-slot arrays by material id
// (MultiMaterialSegmentation sizes its state count from filament_colour). After the
// producer-side lockstep in update_multi_material_filament_presets() these project-scoped
// arrays are expected to be exactly num_filaments; the property the pipeline relies on is
// that none is shorter (a short filament_colour was the OOB in #16432). Pad if needed.
for (const char* per_slot_key : { "filament_colour", "filament_colour_type", "filament_multi_colour" }) {
auto* per_slot_opt = dynamic_cast<ConfigOptionVectorBase*>(out.option(per_slot_key, true));
if (per_slot_opt == nullptr)
continue;
if (per_slot_opt->size() < num_filaments) {
BOOST_LOG_TRIVIAL(warning) << "full_fff_config: per-slot array " << per_slot_key
<< " has " << per_slot_opt->size() << " entries, padding to num_filaments=" << num_filaments;
per_slot_opt->resize(num_filaments, FullPrintConfig::defaults().option(per_slot_key));
}
assert(per_slot_opt->size() >= num_filaments);
}
out.option<ConfigOptionEnumGeneric>("printer_technology", true)->value = ptFFF;
return out;
}
@@ -7938,15 +7911,6 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
this->filament_presets.resize(num_extruders, this->filament_presets.empty() ? this->filaments.first_visible().name :
this->filament_presets.back());
num_filaments = this->filament_presets.size();
// Keep the project-scoped per-slot arrays in lockstep with the padded preset list, so
// consumers that size from filament_colour (MultiMaterialSegmentation) agree with those
// that size from the preset list / filament_diameter (PrintObjectSlice). Grow only: a
// colour array longer than the preset list is a valid state (a project with more slots
// than nozzles) and must not be truncated here.
const auto* filament_colour = this->project_config.option<ConfigOptionStrings>("filament_colour");
if (filament_colour == nullptr || filament_colour->values.size() < num_filaments)
this->resize_project_filament_slots(num_filaments);
}
if (to_delete_filament_id == -1)
to_delete_filament_id = num_filaments;
-6
View File
@@ -394,12 +394,6 @@ public:
// slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and
// then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does.
void set_num_filaments(unsigned int n, std::string new_col = "");
// Grow or shrink the project-scoped per-slot arrays (filament_colour, filament_colour_type,
// filament_multi_colour, the filament maps and the mixed metadata) to n, without touching
// filament_presets and without rebuilding flush volumes. Callers that resize filament_presets
// must keep these arrays in lockstep with it. Returns the array size before the resize so the
// caller can seed the new tail.
size_t resize_project_filament_slots(size_t n);
void update_num_filaments(unsigned int to_del_flament_id);
void get_ams_cobox_infos(AMSComboInfo &combox_info);
+2 -9
View File
@@ -1073,14 +1073,8 @@ static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCance
const auto &layer_ranges = print_object.shared_regions()->layer_ranges;
double z = print_object.get_layer(int(range.begin()))->slice_z;
auto it_layer_range = layer_range_first(layer_ranges, z);
// The per-layer row width of the segmentation is the number of material channels it
// was built with (filament_colour.size()). Size the bound from the data itself rather
// than from filament_diameter.size(): those two arrays can diverge (the project's
// colour array can end up shorter than the padded preset list), and trusting the
// larger count reads past the end of every row.
// BBS
const size_t num_extruders = segmentation.empty() ? 0 : segmentation.front().size();
assert(num_extruders == print_object.print()->config().filament_diameter.size());
const size_t num_extruders = print_object.print()->config().filament_diameter.size();
struct ByExtruder {
ExPolygons expolygons;
@@ -1103,8 +1097,7 @@ static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCance
by_extruder.assign(num_extruders, ByExtruder());
by_region.assign(layer.region_count(), ByRegion());
bool layer_split = false;
const size_t layer_channels = segmentation[layer_id].size();
for (size_t extruder_id = 0; extruder_id < std::min(num_extruders, layer_channels); ++ extruder_id) {
for (size_t extruder_id = 0; extruder_id < num_extruders; ++ extruder_id) {
ByExtruder &region = by_extruder[extruder_id];
append(region.expolygons, std::move(segmentation[layer_id][extruder_id]));
if (! region.expolygons.empty()) {
+3 -6
View File
@@ -927,17 +927,15 @@ void ViewerImpl::reset()
m_used_extruders.clear();
m_total_time = { 0.0f, 0.0f };
m_travels_time = { 0.0f, 0.0f };
m_vertices.clear();
m_vertices_colors.clear();
// swap rather than clear: these are sized by the print, and a reset means the memory
// should go back, not sit reserved until the next load
std::vector<PathVertex>().swap(m_vertices);
std::vector<float>().swap(m_vertices_colors);
for (std::vector<float>& times : m_layer_start_times)
std::vector<float>().swap(times);
std::vector<uint32_t>().swap(m_layer_first_vertex);
m_layers_in_vertex_order = false;
// BitSet::clear() only zeroes the bits, it keeps the blocks allocated; load() builds a new
// bitset anyway and it is never read while m_vertices is empty
m_valid_lines_bitset = BitSet<>();
m_valid_lines_bitset.clear();
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
m_cog_marker.reset();
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
@@ -1836,7 +1834,6 @@ size_t ViewerImpl::get_used_cpu_memory() const
ret += sizeof(m_extrusion_roles_colors);
ret += sizeof(m_options_colors);
ret += STDVEC_MEMSIZE(m_vertices, PathVertex);
ret += STDVEC_MEMSIZE(m_vertices_colors, float);
for (const std::vector<float>& times : m_layer_start_times)
ret += STDVEC_MEMSIZE(times, float);
ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t);
-6
View File
@@ -1610,12 +1610,6 @@ void PlaterPresetComboBox::show_default_color_picker()
{
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
// The combo can outlive the project's colour array (e.g. a slot padded onto the printer's
// nozzle count); guard before indexing so a stale slot index cannot read past the end.
if (m_filament_idx < 0 || m_filament_idx >= static_cast<int>(colors->values.size())) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": m_filament_idx %1% out of range %2%") % m_filament_idx % colors->values.size();
return;
}
wxColour current_clr(colors->values[m_filament_idx]);
if (!current_clr.IsOk())
current_clr = wxColour(0, 0, 0); // Don't set alfa to transparence
+30
View File
@@ -19,6 +19,7 @@
#include <wx/stattext.h>
#include <wx/gdicmn.h>
#include <wx/window.h>
#include <wx/utils.h>
#ifdef __WXGTK__
#include <gtk/gtk.h>
@@ -665,6 +666,35 @@ void DropDown::messureSize()
sub->mainDropDown->mouseMove(ev);
e.RequestMore();
});
// Orca: the submenu holds the GTK grab, so GTK gives it the wheel events of the whole
// chain. GDK names the window under the cursor, so re-emit main's events on main.
g_signal_connect(sub->m_widget, "event", G_CALLBACK(+[](GtkWidget *, GdkEvent *event, gpointer data) -> gboolean {
auto *sub = static_cast<DropDown *>(data);
DropDown *main = sub->mainDropDown;
if (event->type != GDK_SCROLL || !main || !main->IsShown() || !sub->IsShown())
return FALSE;
gpointer owner = nullptr;
if (event->any.window)
gdk_window_get_user_data(event->any.window, &owner);
GtkWidget *widget = (owner && GTK_IS_WIDGET(owner)) ? GTK_WIDGET(owner) : nullptr;
bool for_main = widget && (widget == main->m_widget || gtk_widget_is_ancestor(widget, main->m_widget));
bool for_sub = widget && (widget == sub->m_widget || gtk_widget_is_ancestor(widget, sub->m_widget));
if (!for_main && for_sub) {
// The compositor may keep the pointer focus on the submenu while the cursor is over main.
wxPoint pt((int) event->scroll.x, (int) event->scroll.y);
if (sub->GetClientRect().Contains(pt))
return FALSE;
wxPoint off = sub->GetScreenRect().GetTopLeft() - main->GetScreenRect().GetTopLeft();
event->scroll.x += off.x;
event->scroll.y += off.y;
for_main = true;
}
if (!for_main)
return FALSE;
// wx connects its mouse handlers to GetConnectWidget(), the inner widget of the popup.
gtk_propagate_event(main->GetConnectWidget(), event);
return TRUE;
}), sub);
#endif
subDropDown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) {
e.SetEventObject(this);