Files
OrcaSlicer/src/slic3r/GUI/Widgets/DropDown.cpp
T
Lam Wei Lun 84768da6a1 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 12:00:40 +08:00

961 lines
32 KiB
C++

#include "DropDown.hpp"
#include "Label.hpp"
#include <cassert>
#include <algorithm>
#include <boost/date_time/posix_time/posix_time_types.hpp>
#include <cstdio>
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include <vector>
#include <utility>
#include <iterator>
#include <wx/dc.h>
#include <wx/dcclient.h>
#include <wx/display.h>
#include <wx/dcbuffer.h>
#include <wx/dcgraph.h>
#include <wx/event.h>
#include <wx/popupwin.h>
#include <wx/stattext.h>
#include <wx/gdicmn.h>
#include <wx/window.h>
#include <wx/utils.h>
#ifdef __WXGTK__
#include <gtk/gtk.h>
#endif
#include <set>
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include "slic3r/GUI/Widgets/StateHandler.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
EVT_LEFT_DOWN(DropDown::mouseDown)
EVT_LEFT_UP(DropDown::mouseReleased)
EVT_MOUSE_CAPTURE_LOST(DropDown::mouseCaptureLost)
EVT_MOTION(DropDown::mouseMove)
EVT_MOUSEWHEEL(DropDown::mouseWheelMoved)
// catch paint events
EVT_PAINT(DropDown::paintEvent)
END_EVENT_TABLE()
/*
* Called by the system of by wxWidgets when the panel needs
* to be redrawn. You can also trigger this call by
* calling Refresh()/Update().
*/
DropDown::DropDown(std::vector<Item> &items)
: items(items)
, state_handler(this)
, border_color(0xDBDBDB)
, text_color(std::make_pair(0x909090, (int) StateColor::Disabled),
std::make_pair(0x363636, (int) StateColor::Normal))
, selector_border_color(std::make_pair(0x009688, (int) StateColor::Hovered),
std::make_pair(*wxWHITE, (int) StateColor::Normal))
, selector_background_color(std::make_pair(0xBFE1DE, (int) StateColor::Checked), // ORCA updated background color for checked item
std::make_pair(*wxWHITE, (int) StateColor::Normal))
{
}
DropDown::DropDown(wxWindow *parent, std::vector<Item> &items, long style)
: DropDown(items)
{
Create(parent, style);
}
void DropDown::Create(wxWindow *parent, long style)
{
PopupWindow::Create(parent, wxPU_CONTAINS_CONTROLS);
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetBackgroundColour(*wxWHITE);
state_handler.attach({&border_color, &text_color, &selector_border_color, &selector_background_color});
state_handler.update_binds();
if ((style & DD_NO_CHECK_ICON) == 0)
check_bitmap = ScalableBitmap(this, "checked", 16);
arrow_bitmap = ScalableBitmap(this, "hms_arrow", 16);
text_off = style & DD_NO_TEXT;
// BBS set default font
SetFont(Label::Body_14);
#ifdef __WXOSX__
// PopupWindow releases mouse on idle, which may cause various problems,
// such as losting mouse move, and dismissing soon on first LEFT_DOWN event.
Bind(wxEVT_IDLE, [] (wxIdleEvent & evt) {});
#endif
}
void DropDown::Invalidate(bool clear)
{
if (clear) {
selection = hover_item = -1;
offset = wxPoint();
}
assert(selection < (int) items.size());
need_sync = true;
}
void DropDown::SetSelection(int n)
{
if (n >= (int) items.size())
n = -1;
if (selection == n) return;
selection = n;
if (need_sync) { // for icon Size
messureSize();
need_sync = true;
}
if (subDropDown)
subDropDown->SetSelection(n);
paintNow();
}
wxString DropDown::GetValue() const
{
return selection >= 0 ? items[selection].text : wxString();
}
void DropDown::SetValue(const wxString &value)
{
auto i = std::find_if(items.begin(), items.end(), [&value](Item & item) { return item.text == value; });
selection = i == items.end() ? -1 : std::distance(items.begin(), i);
}
void DropDown::SetCornerRadius(double radius)
{
this->radius = radius;
paintNow();
}
void DropDown::SetBorderColor(StateColor const &color)
{
border_color = color;
state_handler.update_binds();
paintNow();
}
void DropDown::SetSelectorBorderColor(StateColor const &color)
{
selector_border_color = color;
state_handler.update_binds();
paintNow();
}
void DropDown::SetTextColor(StateColor const &color)
{
text_color = color;
state_handler.update_binds();
paintNow();
}
void DropDown::SetSelectorBackgroundColor(StateColor const &color)
{
selector_background_color = color;
state_handler.update_binds();
paintNow();
}
void DropDown::SetUseContentWidth(bool use, bool limit_max_content_width)
{
if (use_content_width == use)
return;
use_content_width = use;
this->limit_max_content_width = limit_max_content_width;
need_sync = true;
messureSize();
}
void DropDown::SetAlignIcon(bool align) { align_icon = align; }
void DropDown::Rescale()
{
need_sync = true;
}
bool DropDown::HasDismissLongTime()
{
auto now = boost::posix_time::microsec_clock::universal_time();
return !IsShown() &&
(now - dismissTime).total_milliseconds() >= 20;
}
void DropDown::Popup(wxWindow *focus)
{
#ifdef __WXGTK__
if (!mainDropDown && m_widget) {
// Data-view cell editors can receive focus before wxGTK infers a
// native popup parent, so provide the current toplevel explicitly.
GtkWindow *transient_parent = nullptr;
for (wxWindow *win = GetParent(); win; win = win->GetParent()) {
GtkWidget *widget = static_cast<GtkWidget *>(win->GetHandle());
if (!widget)
continue;
GtkWidget *top = gtk_widget_get_toplevel(widget);
if (GTK_IS_WINDOW(top)) {
transient_parent = GTK_WINDOW(top);
break;
}
}
if (transient_parent)
gtk_window_set_transient_for(GTK_WINDOW(m_widget), transient_parent);
}
#endif
PopupWindow::Popup(focus);
}
void DropDown::paintEvent(wxPaintEvent& evt)
{
// depending on your system you may need to look at double-buffered dcs
wxBufferedPaintDC dc(this);
render(dc);
}
/*
* Alternatively, you can use a clientDC to paint on the panel
* at any time. Using this generally does not free you from
* catching paint events, since it is possible that e.g. the window
* manager throws away your drawing when the window comes to the
* background, and expects you will redraw it when the window comes
* back (by sending a paint event).
*/
void DropDown::paintNow()
{
// depending on your system you may need to look at double-buffered dcs
//wxClientDC dc(this);
//render(dc);
Refresh();
}
static wxSize GetBmpSize(wxBitmap & bmp)
{
if (!bmp.IsOk())
return wxSize(0, 0);
#ifdef __APPLE__
return bmp.GetScaledSize();
#else
return bmp.GetSize();
#endif
}
// The x-range [first, second) of the column at the right of the rows that holds the items' action
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
// clicks against it, so the drawn icon and its clickable area always match.
std::pair<int, int> DropDown::actionColumn() const
{
if (actionIconWidth <= 0)
return {0, 0};
int right = rowSize.x;
if (rowSize.y * count > GetSize().y) // position bar
right -= 6;
return {right - actionIconWidth - FromDIP(16), right};
}
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
{
// save dc
auto pre_clr = dc.GetTextForeground();
auto pre_pen = dc.GetPen();
dc.SetTextForeground(wxColour(172, 172, 172));//GRAY 500
dc.SetPen(wxColour(166, 169, 170));//GRAY 400
// miner font
auto font = w->GetFont();
font.SetPointSize(font.GetPointSize() - 3);
dc.SetFont(font);
int spacing = w->FromDIP(8);
if (!split_text.empty())// Paiting: text + spacing + line + spacing
{
int max_content_width = item_width - start_pt.x - 2 * spacing;
wxSize tSize = dc.GetMultiLineTextExtent(split_text);
if (tSize.x > max_content_width)
{
split_text = wxControl::Ellipsize(split_text, dc, wxELLIPSIZE_END, max_content_width);
tSize = dc.GetMultiLineTextExtent(split_text);
}
dc.SetFont(font);
dc.DrawText(split_text, start_pt);
int line_width = item_width - start_pt.x - tSize.x - 2 * spacing;
int line_y = start_pt.y + (tSize.GetHeight() / 2);
dc.DrawLine(start_pt.x + tSize.x + spacing, line_y, start_pt.x + tSize.x + line_width + spacing, line_y);// draw right line
}
else// Paiting: line + spacing
{
int line_y = start_pt.y + (item_height / 2);
int line_width = item_width - start_pt.x - spacing;
dc.DrawLine(start_pt.x, line_y, start_pt.x + line_width, line_y);// draw line
}
// restore dc
dc.SetTextForeground(pre_clr);
dc.SetPen(pre_pen);
dc.SetFont(w->GetFont());
}
/*
* Here we do the actual rendering. I put it in a separate
* method so that it can work no matter what type of DC
* (e.g. wxPaintDC or wxClientDC) is used.
*/
void DropDown::render(wxDC &dc)
{
if (items.size() == 0) return;
int states = state_handler.states();
if (subDropDown)
states |= subDropDown->state_handler.states();
dc.SetPen(wxPen(border_color.colorForStates(states)));
dc.SetBrush(wxBrush(StateColor::darkModeColorFor(GetBackgroundColour())));
// if (GetWindowStyle() & wxBORDER_NONE)
// dc.SetPen(wxNullPen);
// draw background
wxSize size = GetSize();
if (radius == 0)
dc.DrawRectangle(0, 0, size.x, size.y);
else
dc.DrawRoundedRectangle(0, 0, size.x, size.y, radius);
int selected_item = selectedItem();
int hover_index = hoverIndex();
// draw hover rectangle
wxRect rcContent = {{0, offset.y}, rowSize};
if (hover_item >= 0 && (states & StateColor::Hovered) && (hover_index < 0 || !(items[hover_index].style & DD_ITEM_STYLE_SPLIT_ITEM))) {
rcContent.y += rowSize.y * hover_item;
if (rcContent.GetBottom() > 0 && rcContent.y < size.y) {
if (selected_item == hover_item)
dc.SetBrush(wxBrush(selector_background_color.colorForStates(states | StateColor::Checked)));
dc.SetPen(wxPen(selector_border_color.colorForStates(states)));
rcContent.Deflate(4, 1);
dc.DrawRectangle(rcContent);
rcContent.Inflate(4, 1);
}
rcContent.y = offset.y;
}
// draw checked rectangle
if (selected_item >= 0 && (selected_item != hover_item || (states & StateColor::Hovered) == 0)) {
rcContent.y += rowSize.y * selected_item;
if (rcContent.GetBottom() > 0 && rcContent.y < size.y) {
dc.SetBrush(wxBrush(selector_background_color.colorForStates(states | StateColor::Checked)));
dc.SetPen(wxPen(selector_background_color.colorForStates(states)));
rcContent.Deflate(4, 1);
dc.DrawRectangle(rcContent);
rcContent.Inflate(4, 1);
}
rcContent.y = offset.y;
}
dc.SetBrush(*wxTRANSPARENT_BRUSH);
{
wxSize offset = (rowSize - textSize) / 2;
rcContent.Deflate(0, offset.y);
}
// draw position bar
if (rowSize.y * count > size.y) {
int height = rowSize.y * count;
wxRect rect = {size.x - 6, -offset.y * size.y / height, 4,
size.y * size.y / height};
dc.SetPen(wxPen(border_color.defaultColor()));
dc.SetBrush(wxBrush(*wxLIGHT_GREY));
dc.DrawRoundedRectangle(rect, 2);
rcContent.width -= 6;
}
// draw check icon
rcContent.x += 5;
rcContent.width -= 5;
if (check_bitmap.bmp().IsOk()) {
auto szBmp = check_bitmap.GetBmpSize();
if (selected_item >= 0) {
wxPoint pt = rcContent.GetLeftTop();
pt.y += (rcContent.height - szBmp.y) / 2;
pt.y += rowSize.y * selected_item;
if (pt.y + szBmp.y > 0 && pt.y < size.y)
dc.DrawBitmap(check_bitmap.bmp(), pt);
}
rcContent.x += szBmp.x + 5;
rcContent.width -= szBmp.x + 5;
}
const auto action_column = actionColumn();
if (action_column.second > action_column.first)
rcContent.width = action_column.first - rcContent.x;
std::set<wxString> groups;
// draw texts & icons
int index = 0;
for (int i = 0; i < items.size(); ++i) {
auto &item = items[i];
int states2 = states;
if ((item.style & DD_ITEM_STYLE_DISABLED) != 0)
states2 &= ~StateColor::Enabled;
// Skip by group
if (group.IsEmpty()) {
if (!item.group_key.IsEmpty()) {
if (groups.find(item.group_key) != groups.end())
continue;
groups.insert(item.group_key);
if (!item.group_key.IsEmpty()) {
bool disabled = true;
for (int j = i + 1; j < items.size(); ++j) {
if (items[j].group_key == item.group_key && (items[j].style & DD_ITEM_STYLE_DISABLED) == 0) {
disabled = false;
break;
}
}
if (!disabled)
states2 |= StateColor::Enabled;
}
}
} else {
if (item.group_key != group)
continue;
}
bool is_hover = index == hover_item;
++index;
if (rcContent.GetBottom() < 0) {
rcContent.y += rowSize.y;
continue;
}
if (rcContent.y > size.y) break;
wxPoint pt = rcContent.GetLeftTop();
if (item.style & DD_ITEM_STYLE_SPLIT_ITEM) {
_DrawSplitItem(this, dc, item.text, pt, rowSize.GetWidth(), rowSize.GetHeight());
rcContent.y += rowSize.GetHeight();
continue;
}
auto & icon = item.icon;
auto size2 = GetBmpSize(icon);
if (iconSize.x > 0) {
if (icon.IsOk()) {
pt.y += (rcContent.height - size2.y) / 2;
dc.DrawBitmap(icon, pt);
}
pt.x += iconSize.x + 5;
pt.y = rcContent.y;
} else if (icon.IsOk()) {
pt.y += (rcContent.height - size2.y) / 2;
dc.DrawBitmap(icon, pt);
pt.x += size2.x + 5;
pt.y = rcContent.y;
}
auto text = group.IsEmpty()
? (item.group_key.IsEmpty() ? item.text : item.group_label)
: item.text;
if (item.action_icon.IsOk()) {
auto action_size = GetBmpSize(item.action_icon);
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
rcContent.y + (rcContent.height - action_size.y) / 2);
dc.DrawBitmap(item.action_icon, action_pt);
}
if (!text_off && !text.IsEmpty()) {
wxSize tSize = dc.GetMultiLineTextExtent(text);
if (pt.x + tSize.x > rcContent.GetRight()) {
int max_text_width = rcContent.GetRight() - pt.x;
if (max_text_width <= 0) {
text.clear();
} else {
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
}
}
pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont());
// Dimmed items stay selectable, so they only borrow the disabled text tone rather
// than taking the disabled state itself.
const int text_states = (item.style & DD_ITEM_STYLE_DIMMED) ? (states2 & ~StateColor::Enabled) : states2;
dc.SetTextForeground(text_color.colorForStates(text_states));
dc.DrawText(text, pt);
if (group.IsEmpty() && !item.group_key.IsEmpty()) {
auto szBmp = arrow_bitmap.GetBmpSize();
pt.x = rcContent.GetRight() - szBmp.x - 5;
pt.y = rcContent.y + (rcContent.height - szBmp.y) / 2;
dc.DrawBitmap(arrow_bitmap.bmp(), pt);
}
}
rcContent.y += rowSize.y;
}
}
int DropDown::hoverIndex()
{
if (hover_item < 0)
return -1;
// BUG FIX: Can't take the shortcut if subDropDown exists (which means there are groups)
// because we need to detect group headers which return negative indices
if (count == items.size() && subDropDown == nullptr)
return hover_item;
int index = -1;
std::set<wxString> groups;
for (int i = 0; i < items.size(); ++i) {
auto &item = items[i];
// Skip by group
if (group.IsEmpty()) {
if (!item.group_key.IsEmpty()) {
if (groups.find(item.group_key) == groups.end())
groups.insert(item.group_key);
else
continue;
}
} else {
if (item.group_key != group)
continue;
}
if (++index == hover_item)
return (item.group_key.IsEmpty() || !group.IsEmpty()) ? i : -i - 2;
}
return -1;
}
int DropDown::selectedItem()
{
if (selection < 0)
return -1;
// BUG FIX: Can't take the shortcut if subDropDown exists (which means there are groups)
// because the visual position differs from the actual item index when groups are shown
if (count == items.size() && subDropDown == nullptr)
return selection;
auto & sel = items[selection];
if (group.IsEmpty() ? !sel.group_key.IsEmpty() : sel.group_key != group)
return -1;
if (selection == 0)
return 0;
int index = 0;
std::set<wxString> groups;
for (size_t i = 0; i < selection; ++i) {
auto &item = items[i];
// Skip by group
if (group.IsEmpty()) {
if (!item.group_key.IsEmpty()) {
if (groups.find(item.group_key) == groups.end())
groups.insert(item.group_key);
else
continue;
}
} else {
if (item.group_key != group)
continue;
}
++index;
}
return index;
}
void DropDown::messureSize()
{
if (!need_sync) return;
textSize = wxSize();
iconSize = wxSize();
actionIconWidth = 0;
count = 0;
wxClientDC dc(GetParent() ? GetParent() : this);
dc.SetFont(GetFont());
std::set<wxString> groups;
for (size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
// Skip by group
if (group.IsEmpty()) {
if (!item.group_key.IsEmpty()) {
if (groups.find(item.group_key) == groups.end())
groups.insert(item.group_key);
else
continue;
}
} else {
if (item.group_key != group)
continue;
}
++count;
wxSize size1;
if (!text_off) {
auto text = group.IsEmpty()
? (item.group_key.IsEmpty() ? item.text : item.group_label)
: item.text;
size1 = dc.GetMultiLineTextExtent(text);
if (group.IsEmpty() && !item.group_key.IsEmpty())
size1.x += 5 + arrow_bitmap.GetBmpWidth();
}
if (item.icon.IsOk()) {
wxSize size2 = GetBmpSize(item.icon);
if (size2.x > iconSize.x)
iconSize = size2;
if (!align_icon) {
size1.x += size2.x + (text_off ? 0 : 5);
}
}
if (item.action_icon.IsOk())
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
if (size1.x > textSize.x) textSize = size1;
}
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
if (actionIconWidth > 0)
textSize.x += actionIconWidth + FromDIP(16);
if (!align_icon) iconSize.x = 0;
wxSize szContent = textSize;
if (szContent.x < FromDIP(120) && !use_content_width)
szContent.x = FromDIP(120);
szContent.x += 10;
if (check_bitmap.bmp().IsOk()) {
auto szBmp = check_bitmap.GetBmpSize();
szContent.x += szBmp.x + 5;
}
if (iconSize.x > 0) szContent.x += iconSize.x + (text_off ? 0 : 5);
if (iconSize.y > szContent.y) szContent.y = iconSize.y;
szContent.y += 10;
if (count > 15) szContent.x += 6;
if (GetParent() && group.IsEmpty()) {
auto x = GetParent()->GetSize().x;
if (x > 0 && (!use_content_width || x > szContent.x))
szContent.x = x;
}
rowSize = szContent;
if (limit_max_content_width) {
wxSize parent_size = GetParent()->GetSize();
if (rowSize.x > parent_size.x * 2) {
rowSize.x = 2 * parent_size.x;
szContent = rowSize;
}
}
szContent.y *= std::min((size_t) 15, std::max(count, (size_t) 1));
szContent.y += items.size() > 15 ? rowSize.y / 2 : 0;
wxWindow::SetSize(szContent);
#ifdef __WXGTK__
// Gtk has a wrapper window for popup widget
if (szContent.x > 0 && szContent.y > 0)
gtk_window_resize (GTK_WINDOW (m_widget), szContent.x, szContent.y);
#endif
if (!groups.empty() && subDropDown == nullptr) {
subDropDown = new DropDown(items);
subDropDown->mainDropDown = this;
subDropDown->check_bitmap = check_bitmap;
subDropDown->text_off = text_off;
subDropDown->use_content_width = true;
subDropDown->Create(GetParent());
#ifdef __WXGTK__
// Orca: On Wayland, while the sub holds an xdg_popup grab, motion events for
// the cursor over main may not be delivered (Mutter drops motion
// outside the grabbing surface). Poll on idle and synthesize a
// mouseMove on main so its hover highlight tracks and it can dismiss
// the sub when the cursor leaves the parent (group) item.
DropDown* sub = subDropDown;
sub->Bind(wxEVT_IDLE, [sub](wxIdleEvent& e) {
e.Skip();
if (!sub->IsShown() || !sub->mainDropDown->IsShown())
return;
wxPoint screen_pt = wxGetMousePosition();
if (sub->GetScreenRect().Contains(screen_pt) || !sub->mainDropDown->GetScreenRect().Contains(screen_pt))
return;
wxPoint main_pt = sub->mainDropDown->ScreenToClient(screen_pt);
wxMouseEvent ev(wxEVT_MOTION);
ev.SetEventObject(sub->mainDropDown);
ev.m_x = main_pt.x;
ev.m_y = main_pt.y;
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);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
}
need_sync = false;
}
void DropDown::autoPosition()
{
messureSize();
wxPoint pos;
wxSize off;
if (mainDropDown) {
pos = mainDropDown->ClientToScreen(wxPoint(0, 0));
off = mainDropDown->GetSize();
pos.x += 6;
pos.y += mainDropDown->hover_item * rowSize.y + rowSize.y + mainDropDown->offset.y;
off.x -= 12;
off.y = -rowSize.y;
} else {
pos = GetParent()->ClientToScreen(wxPoint(0, 0));
off = GetParent()->GetSize();
pos.y -= 6;
off.x = 0;
off.y += 12;
}
wxPoint old = GetPosition();
wxSize size = GetSize();
Position(pos, off);
if (old != GetPosition()) {
size = rowSize;
size.y *= std::min((size_t) 15, count);
size.y += count > 15 ? rowSize.y / 2 : 0;
if (size != GetSize()) {
wxWindow::SetSize(size);
offset = wxPoint();
Position(pos, off);
}
}
if (GetPosition().y > pos.y) {
// may exceed
auto drect = wxDisplay(GetParent()).GetGeometry();
if (GetPosition().y + size.y + 10 > drect.GetBottom()) {
int available_height = drect.GetBottom() - GetPosition().y - 10;
if (available_height < rowSize.y * 2)
return;
if (use_content_width && count <= 15) size.x += 6;
size.y = available_height;
wxWindow::SetSize(size);
if (selection >= 0) {
if (offset.y + rowSize.y * (selection + 1) > size.y)
offset.y = size.y - rowSize.y * (selection + 1);
else if (offset.y + rowSize.y * selection < 0)
offset.y = -rowSize.y * selection;
}
}
}
}
void DropDown::mouseDown(wxMouseEvent& event)
{
// Receivce unexcepted LEFT_DOWN on Mac after OnDismiss
if (!IsShown())
return;
// force calc hover item again
mouseMove(event);
pressedDown = true;
CaptureMouse();
dragStart = event.GetPosition();
}
void DropDown::mouseReleased(wxMouseEvent& event)
{
if (pressedDown) {
dragStart = wxPoint();
pressedDown = false;
if (HasCapture())
ReleaseMouse();
if (hover_item < 0)
return;
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
int index = hoverIndex();
if (index >= 0 && items[index].action_icon.IsOk()) {
const auto action_column = actionColumn();
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
sendDropDownActionEvent(index);
paintNow();
return;
}
}
sendDropDownEvent();
if (mainDropDown)
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
DismissAndNotify();
} else if (subDropDown)
subDropDown->Popup(subDropDown);
}
}
void DropDown::mouseCaptureLost(wxMouseCaptureLostEvent &event)
{
wxMouseEvent evt;
mouseReleased(evt);
}
void DropDown::mouseMove(wxMouseEvent &event)
{
wxPoint pt = event.GetPosition();
#ifdef __WXOSX__
if (mainDropDown) {
auto size = GetSize();
if (pt.x < 0 || pt.y < 0 || pt.x >= size.x || pt.y >= size.y) {
auto diff = GetPosition() - mainDropDown->GetPosition();
event.SetX(pt.x + diff.x);
event.SetY(pt.y + diff.y);
mainDropDown->mouseMove(event);
return;
}
}
#endif
if (pressedDown) {
wxPoint pt2 = offset + pt - dragStart;
wxSize size = GetSize();
dragStart = pt;
if (pt2.y > 0)
pt2.y = 0;
else if (pt2.y + rowSize.y * int(count) < size.y)
pt2.y = size.y - rowSize.y * int(count);
if (pt2.y != offset.y) {
offset = pt2;
hover_item = -1; // moved
} else {
return;
}
}
if (!pressedDown || hover_item >= 0) {
int hover = (pt.y - offset.y) / rowSize.y;
if (hover >= (int) count) hover = -1;
if (hover == hover_item) return;
hover_item = hover;
int index = hoverIndex();
if (index < -1) {
auto & drop = *subDropDown;
drop.group = items[-index - 2].group_key;
drop.need_sync = true;
drop.messureSize();
#ifdef __WXGTK__
// wxGTK wraps popup contents in a native GtkWindow. Make the submenu
// transient for the currently mapped parent popup window before
// positioning/showing it, so wlroots/Hyprland sees the topmost parent.
if (m_widget && drop.m_widget)
gtk_window_set_transient_for(GTK_WINDOW(drop.m_widget), GTK_WINDOW(m_widget));
#endif
drop.autoPosition();
drop.paintNow();
if (!drop.IsShown())
drop.Popup(&drop);
} else if (index >= 0) {
if (subDropDown) {
subDropDown->group.clear();
if (subDropDown->IsShown())
subDropDown->Dismiss();
}
SetToolTip(items[index].tip);
}
}
paintNow();
}
void DropDown::mouseWheelMoved(wxMouseEvent &event)
{
auto delta = event.GetWheelRotation();
wxSize size = GetSize();
wxPoint pt2 = offset + wxPoint{0, delta};
if (pt2.y > 0)
pt2.y = 0;
else if (pt2.y + rowSize.y * int(count) < size.y)
pt2.y = size.y - rowSize.y * int(count);
if (pt2.y != offset.y) {
offset = pt2;
} else {
return;
}
int hover = (event.GetPosition().y - offset.y) / rowSize.y;
if (hover >= (int) count) hover = -1;
if (hover != hover_item) {
hover_item = hover;
if (auto index = hoverIndex(); index >= 0)
SetToolTip(items[index].tip);
}
paintNow();
}
// currently unused events
void DropDown::sendDropDownEvent()
{
int index = hoverIndex();
if (index < 0 || (items[index].style & DD_ITEM_STYLE_DISABLED))
return;
wxCommandEvent event(wxEVT_COMBOBOX, GetId());
event.SetEventObject(this);
event.SetInt(index);
event.SetString(items[index].text);
GetEventHandler()->ProcessEvent(event);
}
void DropDown::sendDropDownActionEvent(int index)
{
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
return;
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
event.SetEventObject(this);
event.SetInt(index);
event.SetString(items[index].text);
GetEventHandler()->ProcessEvent(event);
}
void DropDown::Dismiss()
{
if (subDropDown && subDropDown->IsShown())
return;
PopupWindow::Dismiss();
}
bool DropDown::ShouldDismissOnTopWindowDeactivate()
{
// On Wayland, mapping a chained xdg_popup with grab makes the parent
// toplevel inactive, which would otherwise cascade-dismiss the whole
// chain. Skip when our chain peer is shown.
return !((mainDropDown && mainDropDown->IsShown()) ||
(subDropDown && subDropDown->IsShown()));
}
void DropDown::OnDismiss()
{
if (mainDropDown) {
const wxPoint& mouse_pos = wxGetMousePosition();
if (!mainDropDown->GetScreenRect().Contains(mouse_pos))
mainDropDown->DismissAndNotify();
else
#ifdef __WIN32__
SetActiveWindow(mainDropDown->GetHandle());
#else
;
#endif
return;
}
if (subDropDown && subDropDown->IsShown())
return;
dismissTime = boost::posix_time::microsec_clock::universal_time();
hover_item = -1;
wxCommandEvent e(EVT_DISMISS);
GetEventHandler()->ProcessEvent(e);
}