1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-20 14:45:44 +00:00

Merge pull request #110256 from Koyper/fix_popup_menu_item_wont_open

[PopupMenu] Fix submenu item not popping on mouse enter
This commit is contained in:
Thaddeus Crews
2025-11-18 14:00:45 -06:00
3 changed files with 244 additions and 145 deletions

View File

@@ -346,68 +346,67 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const {
}
void PopupMenu::_activate_submenu(int p_over, bool p_by_keyboard) {
ERR_FAIL_INDEX_MSG(p_over, items.size(), vformat("Invalid submenu index %d in _activate_submenu.", p_over));
PopupMenu *submenu_popup = items[p_over].submenu;
if (submenu_popup->is_visible()) {
return; // Already visible.
}
ERR_FAIL_COND_MSG(submenu_popup->is_visible(), vformat("_activate_submenu should not be called on an open submenu - index: %d.", p_over));
submenu_popup->this_submenu_index = p_over;
active_submenu_index = p_over;
submenu_popup->get_window()->set_exclusive(false); // Ensure mouse inputs to parent menu are not inhibited by the submenu in exclusive mode.
const float win_scale = get_content_scale_factor();
const Point2 panel_ofs_start = Point2(panel->get_offset(SIDE_LEFT), panel->get_offset(SIDE_TOP)) * win_scale;
const Point2 panel_ofs_end = Point2(-panel->get_offset(SIDE_RIGHT), -panel->get_offset(SIDE_BOTTOM)) * win_scale;
const Point2 this_pos = get_position() + Point2(0, panel_ofs_start.y + theme_cache.panel_style->get_margin(SIDE_TOP) * win_scale);
Rect2 this_rect(this_pos, get_size());
const float scroll_offset = control->get_position().y;
const float scaled_ofs_cache = items[p_over]._ofs_cache * win_scale;
const float scaled_height_cache = items[p_over]._height_cache * win_scale;
const Point2 this_pos = get_position();
Rect2 this_rect = Rect2(this_pos, panel->get_size());
submenu_popup->reset_size(); // Shrink the popup size to its contents.
const Size2 submenu_size = submenu_popup->get_size();
// Calculate the submenu's position.
Point2 submenu_pos(0, -submenu_popup->get_theme_stylebox(SceneStringName(panel))->get_margin(SIDE_TOP) * submenu_popup->get_content_scale_factor());
Point2 submenu_pos = Point2(0, 0);
Rect2i screen_rect = is_embedded() ? Rect2i(get_embedder()->get_visible_rect()) : get_parent_rect();
active_submenu_target_line.clear();
panel_offset_start = Point2(panel->get_offset(SIDE_LEFT), panel->get_offset(SIDE_TOP)) * win_scale;
const Point2 panel_offset_end = Point2(-panel->get_offset(SIDE_RIGHT), -panel->get_offset(SIDE_BOTTOM)) * win_scale;
const Vector2 scaled_this_size = this_rect.size * win_scale;
const float scaled_theme_v_separation = theme_cache.v_separation * win_scale;
const float scroll_offset = control->get_position().y;
const float scaled_ofs_cache = items[p_over]._ofs_cache * win_scale;
const float scaled_height_cache = items[p_over]._height_cache * win_scale;
if (is_layout_rtl()) {
submenu_pos += this_pos + Point2(-submenu_size.width + panel_ofs_end.x, scaled_ofs_cache + scroll_offset - theme_cache.v_separation / 2);
is_active_submenu_left = true;
submenu_pos += this_pos + Point2(-submenu_size.width + panel_offset_end.x, scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + panel_offset_start.y);
if (submenu_pos.x < screen_rect.position.x) {
submenu_pos.x = this_pos.x + this_rect.size.width - panel_ofs_start.x;
submenu_pos.x = this_pos.x + this_rect.size.width - panel_offset_start.x;
is_active_submenu_left = false;
}
this_rect.position.x += panel_ofs_end.x;
} else {
submenu_pos += this_pos + Point2(this_rect.size.width - panel_ofs_end.x, scaled_ofs_cache + scroll_offset - theme_cache.v_separation / 2);
is_active_submenu_left = false;
submenu_pos += this_pos + Point2(scaled_this_size.x + panel_offset_start.x, scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + panel_offset_start.y);
if (submenu_pos.x + submenu_size.width > screen_rect.position.x + screen_rect.size.width) {
submenu_pos.x = this_pos.x - submenu_size.width + panel_ofs_start.x;
submenu_pos.x = this_pos.x - submenu_size.width + panel_offset_end.x;
is_active_submenu_left = true;
}
this_rect.position.x += panel_ofs_start.x;
}
submenu_popup->set_position(submenu_pos);
PopupMenu *submenu_pum = Object::cast_to<PopupMenu>(submenu_popup);
if (!submenu_pum) {
submenu_popup->popup();
return;
}
submenu_pum->activated_by_keyboard = p_by_keyboard;
submenu_popup->activated_by_keyboard = p_by_keyboard;
// If not triggered by the mouse, start the popup with its first enabled item focused.
if (p_by_keyboard) {
for (int i = 0; i < submenu_pum->get_item_count(); i++) {
if (!submenu_pum->is_item_disabled(i)) {
submenu_pum->set_focused_item(i);
for (int i = 0; i < submenu_popup->get_item_count(); i++) {
if (!submenu_popup->is_item_disabled(i)) {
submenu_popup->set_focused_item(i);
break;
}
}
}
submenu_pum->popup();
// Set autohide areas.
submenu_popup->popup();
// The autohide areas are set on the submenu, but are aligned over the parent menu,
// so we spoof `this_rect` position as the negative relative offset of the parent from the submenu.
this_rect.position = -(submenu_popup->get_position() - this_pos);
const Rect2 safe_area(get_position(), get_size());
Viewport *vp = submenu_popup->get_embedder();
@@ -416,21 +415,29 @@ void PopupMenu::_activate_submenu(int p_over, bool p_by_keyboard) {
} else {
DisplayServer::get_singleton()->window_set_popup_safe_rect(submenu_popup->get_window_id(), safe_area);
}
this_rect.position -= submenu_pum->get_position(); // Make the position of the parent popup relative to submenu popup.
this_rect.size.width -= panel_ofs_start.x + panel_ofs_end.x;
this_rect.size.height -= panel_ofs_end.y + (theme_cache.panel_style->get_margin(SIDE_TOP) + theme_cache.panel_style->get_margin(SIDE_BOTTOM)) * win_scale;
// Autohide area above the submenu item.
submenu_pum->clear_autohide_areas();
submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y - theme_cache.panel_style->get_margin(SIDE_TOP) * win_scale,
this_rect.size.x, scaled_ofs_cache + scroll_offset + theme_cache.panel_style->get_margin(SIDE_TOP) * win_scale - theme_cache.v_separation / 2));
// If there is an area below the submenu item, add an autohide area there.
if (scaled_ofs_cache + scaled_height_cache + scroll_offset <= control->get_size().height * win_scale) {
const int from = scaled_ofs_cache + scaled_height_cache + scroll_offset + theme_cache.v_separation / 2;
submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from));
// Set the mouse movement target line at the top and bottom points of the submenu vertical side abutting the parent menu.
if (is_active_submenu_left) {
active_submenu_target_line.push_back(Point2(submenu_popup->get_position().x + submenu_popup->get_size().x, submenu_popup->get_position().y));
} else {
active_submenu_target_line.push_back(submenu_popup->get_position());
}
active_submenu_target_line.push_back(Point2(active_submenu_target_line[0].x, active_submenu_target_line[0].y + submenu_popup->get_size().y));
submenu_popup->clear_autohide_areas();
// Add an autohide area above the submenu item unless it's the top item.
// This avoids a narrow strip of area that can trigger the submenu to reload when reentering the parent item from the top.
const int y_to_item_top = scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + theme_cache.panel_style->get_margin(SIDE_TOP) * win_scale;
Rect2 top_rect = Rect2(this_rect.position.x, this_rect.position.y, scaled_this_size.width, y_to_item_top);
if (active_submenu_index != 0) {
submenu_popup->add_autohide_area(top_rect);
}
// If there is an area below the submenu item, add an autohide area there unless it's the last item.
if (active_submenu_index != items.size() - 1) {
const int y_to_item_bottom = y_to_item_top + scaled_height_cache + scaled_theme_v_separation;
submenu_popup->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + y_to_item_bottom, scaled_this_size.x, scaled_this_size.y - y_to_item_bottom));
}
queue_accessibility_update();
control->queue_redraw();
}
void PopupMenu::_parent_focused() {
@@ -460,8 +467,6 @@ void PopupMenu::_submenu_timeout() {
if (mouse_over == submenu_over) {
_activate_submenu(mouse_over);
}
submenu_over = -1;
}
void PopupMenu::_input_from_window(const Ref<InputEvent> &p_event) {
@@ -572,7 +577,8 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
} else if (p_event->is_action("ui_left", true) && p_event->is_pressed()) {
Node *n = get_parent();
if (n) {
if (Object::cast_to<PopupMenu>(n)) {
if (PopupMenu *parent_popup = Object::cast_to<PopupMenu>(n)) {
parent_popup->activated_by_keyboard = true;
hide();
set_input_as_handled();
} else if (Object::cast_to<MenuBar>(n)) {
@@ -650,8 +656,12 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
if (!item_clickable_area.has_point(b->get_position())) {
return;
}
_mouse_over_update(b->get_position());
int over = _get_mouse_over(b->get_position());
if (over < 0 || items[over].separator || items[over].disabled || (items[over].submenu && items[over].submenu->is_visible())) {
return;
} else {
_mouse_over_update(b->get_position());
}
} else {
if (is_scrolling) {
is_scrolling = false;
@@ -683,7 +693,9 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
}
if (items[over].submenu) {
_activate_submenu(over);
if (!items[over].submenu->is_visible()) {
_activate_submenu(over);
}
return;
}
activate_item(over);
@@ -694,28 +706,40 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseMotion> m = p_event;
if (m.is_valid()) {
if (m->get_velocity().is_zero_approx()) {
if (m->get_velocity().is_zero_approx() || m->get_relative() == Vector2(0, 0)) {
return;
}
activated_by_keyboard = false;
for (const Rect2 &E : autohide_areas) {
if (!scroll_container->get_global_rect().has_point(m->get_position()) && E.has_point(m->get_position())) {
// The mouse left the safe area, prepare to close.
_close_pressed();
return;
if (this_submenu_index != -1) { // Is a submenu.
PopupMenu *parent_popup = Object::cast_to<PopupMenu>(get_parent());
Point2 areas_mouse_pos = get_mouse_position() - parent_popup->panel_offset_start;
for (const Rect2 &E : autohide_areas) {
if (!scroll_container->get_global_rect().has_point(m->get_position()) && E.has_point(areas_mouse_pos)) {
_close_or_suspend();
return;
}
}
}
if (!minimum_lifetime_timer->is_stopped()) {
if (this_submenu_index != -1) { // Is a submenu.
// The mouse left the safe area, but came back again, so cancel the auto-closing.
minimum_lifetime_timer->stop();
if (PopupMenu *parent_pum = Object::cast_to<PopupMenu>(get_parent())) {
parent_pum->_hover_active_submenu_item();
if (PopupMenu *parent_popup = Object::cast_to<PopupMenu>(get_parent())) {
if (!parent_popup->close_suspended_timer->is_stopped()) {
parent_popup->close_suspended_timer->stop();
}
}
}
if (mouse_over == -1 && !item_clickable_area.has_point(m->get_position())) {
Vector2 m_relative = m->get_relative();
if (is_embedded() && m_relative != Vector2()) { // Only if the mouse has moved relative.
if (PopupMenu *parent_popup = Object::cast_to<PopupMenu>(get_parent())) { // Is a submenu.
parent_popup->last_submenu_mouse_position = m->get_position();
} else {
last_submenu_mouse_position = m->get_position();
}
}
return;
}
_mouse_over_update(m->get_position());
@@ -764,27 +788,50 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
}
}
void PopupMenu::_mouse_over_update(const Point2 &p_over) {
int over = _get_mouse_over(p_over);
int id = (over < 0 || items[over].separator || items[over].disabled) ? -1 : (items[over].id >= 0 ? items[over].id : over);
bool PopupMenu::_is_mouse_moving_toward_submenu(const Vector2 &p_relative, bool p_is_submenu_left, const Vector2 &p_mouse_position, const Vector<Point2> &p_active_submenu_target_line) const {
Vector2 top_target = (p_active_submenu_target_line[0] - p_mouse_position).rotated(p_is_submenu_left ? -Math::PI * 0.5 : Math::PI * 0.5);
Vector2 bottom_target = (p_active_submenu_target_line[1] - p_mouse_position).rotated(p_is_submenu_left ? Math::PI * 0.5 : -Math::PI * 0.5);
// The top_target vector is perpendicular to the vector between the mouse position and the top point of the submenu target line.
// The dot product is > 0 if the relative vector is +/- 90 degrees of the top_vector.
// The bottom_target vector is perpendicular to the vector between the mouse position and the bottom point of the submenu target line,
// but pointing in the opposite direction of the top_target vector. Thus, the intersection of top and bottom test semicircles is the area
// in which the relative vector is deemed to be moving toward the submenu.
// Without normalization, the comparison is correct by testing only the sign, but would not work against a range of +/- 1.
return bottom_target.dot(p_relative) > 0 && top_target.dot(p_relative) > 0;
}
void PopupMenu::_mouse_over_update(const Point2 &p_over) {
int over_index = _get_mouse_over(p_over);
int id = (over_index < 0 || items[over_index].separator || items[over_index].disabled) ? -1 : (items[over_index].id >= 0 ? items[over_index].id : over_index);
if (id < 0) {
// Only remove the hover if there's no open submenu, or the mouse is in an item that can't be hovered.
if (over >= 0 || !(mouse_over >= 0 && items[mouse_over].submenu && items[mouse_over].submenu->is_visible())) {
mouse_over = -1;
queue_accessibility_update();
control->queue_redraw();
}
mouse_over = -1;
submenu_over = -1;
submenu_timer->stop();
queue_accessibility_update();
control->queue_redraw();
return;
}
if (!is_scrolling && items[over].submenu && submenu_over != over) {
submenu_over = over;
submenu_timer->start();
if (items[over_index].submenu) {
if (items[over_index].submenu->is_visible()) {
if (submenu_over == over_index) {
last_submenu_mouse_position = is_embedded() ? p_over : Point2(DisplayServer::get_singleton()->mouse_get_position() - get_position());
}
if (items[over_index].submenu->active_submenu_index != -1) { // Close any secondary popup open on the visible submenu when the mouse is on the parent item.
PopupMenu *secondary_popup = Object::cast_to<PopupMenu>(items[over_index].submenu);
ERR_FAIL_INDEX_MSG(secondary_popup->active_submenu_index, secondary_popup->items.size(), vformat("Invalid active_submenu_index index %d in _mouse_over_update.", secondary_popup->active_submenu_index));
secondary_popup->items[secondary_popup->active_submenu_index].submenu->_close_pressed();
}
} else if (!is_scrolling && submenu_over != over_index && !close_was_suspended && active_submenu_index == -1) {
submenu_timer->start();
submenu_over = over_index;
}
} else {
submenu_timer->stop();
submenu_over = -1;
}
if (over != mouse_over) {
mouse_over = over;
if (over_index != mouse_over) {
mouse_over = over_index;
queue_accessibility_update();
control->queue_redraw();
}
@@ -846,8 +893,7 @@ void PopupMenu::_draw_items() {
Point2 item_ofs = ofs;
Size2 icon_size = _get_item_icon_size(i);
float h = _get_item_height(i);
if (i == mouse_over) {
if ((active_submenu_index == -1 && i == mouse_over) || i == active_submenu_index) {
theme_cache.hover_style->draw(ci, Rect2(item_ofs + Point2(0, -theme_cache.v_separation / 2), Size2(display_width, h + theme_cache.v_separation)));
}
@@ -963,13 +1009,13 @@ void PopupMenu::_draw_items() {
if (theme_cache.font_outline_size > 0 && theme_cache.font_outline_color.a > 0) {
items[i].text_buf->draw_outline(ci, text_pos, theme_cache.font_outline_size, theme_cache.font_outline_color);
}
items[i].text_buf->draw(ci, text_pos, items[i].disabled ? theme_cache.font_disabled_color : (i == mouse_over ? theme_cache.font_hover_color : theme_cache.font_color));
items[i].text_buf->draw(ci, text_pos, items[i].disabled ? theme_cache.font_disabled_color : ((active_submenu_index == -1 && i == mouse_over) || i == active_submenu_index ? theme_cache.font_hover_color : theme_cache.font_color));
} else {
Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0));
if (theme_cache.font_outline_size > 0 && theme_cache.font_outline_color.a > 0) {
items[i].text_buf->draw_outline(ci, text_pos, theme_cache.font_outline_size, theme_cache.font_outline_color);
}
items[i].text_buf->draw(ci, text_pos, items[i].disabled ? theme_cache.font_disabled_color : (i == mouse_over ? theme_cache.font_hover_color : theme_cache.font_color));
items[i].text_buf->draw(ci, text_pos, items[i].disabled ? theme_cache.font_disabled_color : ((active_submenu_index == -1 && i == mouse_over) || i == active_submenu_index ? theme_cache.font_hover_color : theme_cache.font_color));
}
}
@@ -995,28 +1041,52 @@ void PopupMenu::_draw_items() {
}
}
void PopupMenu::_minimum_lifetime_timeout() {
close_allowed = true;
// If the mouse still isn't in this popup after timer expires, close.
if (!activated_by_keyboard && !get_visible_rect().has_point(get_mouse_position())) {
_close_pressed();
void PopupMenu::_close_pressed() {
if (this_submenu_index != -1 && active_submenu_index != -1) { // Close secondary submenus on first submenu close.
ERR_FAIL_INDEX_MSG(active_submenu_index, items.size(), vformat("Invalid active_submenu_index index %d in _close_pressed.", active_submenu_index));
items[active_submenu_index].submenu->_close_pressed();
}
Popup::_close_pressed();
}
void PopupMenu::_close_pressed() {
// Only apply minimum lifetime to submenus.
PopupMenu *parent_pum = Object::cast_to<PopupMenu>(get_parent());
if (!parent_pum) {
Popup::_close_pressed();
return;
}
// If the timer has expired, close. If timer is still running, do nothing.
if (close_allowed) {
close_allowed = false;
Popup::_close_pressed();
} else if (minimum_lifetime_timer->is_stopped()) {
minimum_lifetime_timer->start();
void PopupMenu::_close_or_suspend() {
if (this_submenu_index != -1) { // Is a submenu.
PopupMenu *parent_popup = Object::cast_to<PopupMenu>(get_parent());
Point2 mouse_pos = is_embedded() ? parent_popup->get_mouse_position() : Point2(DisplayServer::get_singleton()->mouse_get_position() - parent_popup->get_position());
if (parent_popup->_get_mouse_over(mouse_pos) == this_submenu_index) {
parent_popup->submenu_mouse_exited_ticks_msec = -1;
parent_popup->mouse_movement_was_tested = false;
parent_popup->close_suspended_timer->stop();
parent_popup->close_was_suspended = false;
parent_popup->submenu_timer->stop();
return;
}
if (parent_popup->submenu_mouse_exited_ticks_msec == -1) { // ticks are reset to -1 when any open submenu is hidden.
parent_popup->submenu_mouse_exited_ticks_msec = OS::get_singleton()->get_ticks_msec();
}
if (!parent_popup->close_was_suspended && parent_popup->close_suspended_timer->is_stopped()) { // Provisionally suspend submenu close until the check below after several msecs.
parent_popup->close_suspended_timer->start();
parent_popup->close_was_suspended = true; // Stays true until the submenu is closed so it can only be suspended once.
parent_popup->submenu_timer->stop();
} else if (!parent_popup->mouse_movement_was_tested && OS::get_singleton()->get_ticks_msec() - parent_popup->submenu_mouse_exited_ticks_msec > 30) {
// Allow the mouse to move away for several msecs before calculating the relative vector to the mouse exit point out of the submenu item.
parent_popup->mouse_movement_was_tested = true;
if (is_embedded()) { // Correct for relative positioning of embedded subwindows.
parent_popup->last_submenu_mouse_position += get_position() - parent_popup->get_position();
}
const Vector2 screen_mouse_position = mouse_pos + parent_popup->get_position();
Vector2 mouse_relative_to_submenu_exit = mouse_pos - parent_popup->last_submenu_mouse_position;
// If the mouse is not moving toward the submenu target line, force the submenu closed.
if (!_is_mouse_moving_toward_submenu(mouse_relative_to_submenu_exit, parent_popup->is_active_submenu_left, screen_mouse_position, parent_popup->active_submenu_target_line)) {
parent_popup->close_suspended_timer->stop();
parent_popup->close_was_suspended = false;
_close_pressed();
}
} else if (parent_popup->mouse_movement_was_tested && OS::get_singleton()->get_ticks_msec() - parent_popup->submenu_mouse_exited_ticks_msec > CLOSE_SUSPENDED_TIMER_DELAY * 1000) {
_close_pressed(); // This is called when the mouse is moved off an open submenu onto an autohide area.
}
} else { // Is the parent popup menu.
_close_pressed();
}
}
@@ -1097,19 +1167,6 @@ Rect2i PopupMenu::_popup_adjust_rect() const {
return current;
}
void PopupMenu::_hover_active_submenu_item() {
for (int i = 0; i < items.size(); i++) {
if (items[i].submenu && items[i].submenu->is_visible()) {
if (mouse_over != i) {
mouse_over = i;
queue_accessibility_update();
control->queue_redraw();
}
return;
}
}
}
void PopupMenu::add_child_notify(Node *p_child) {
Window::add_child_notify(p_child);
@@ -1299,11 +1356,9 @@ void PopupMenu::_notification(int p_what) {
} break;
case NOTIFICATION_WM_MOUSE_EXIT: {
if (mouse_over >= 0 && (!items[mouse_over].submenu || !items[mouse_over].submenu->is_visible())) {
mouse_over = -1;
queue_accessibility_update();
control->queue_redraw();
}
mouse_over = -1;
queue_accessibility_update();
control->queue_redraw();
} break;
case NOTIFICATION_WM_SIZE_CHANGED: {
@@ -1403,14 +1458,15 @@ void PopupMenu::_notification(int p_what) {
}
}
// Only used when using operating system windows.
if (!activated_by_keyboard && !is_embedded() && autohide_areas.size()) {
Point2 mouse_pos = DisplayServer::get_singleton()->mouse_get_position();
mouse_pos -= get_position();
// Only used when using operating system windows, and only on submenus.
if (!activated_by_keyboard && !is_embedded() && autohide_areas.size() && this_submenu_index != -1) {
PopupMenu *parent_popup = Object::cast_to<PopupMenu>(get_parent());
const float win_scale = get_content_scale_factor();
Point2 mouse_pos = DisplayServer::get_singleton()->mouse_get_position() - get_position();
Point2 areas_mouse_pos = mouse_pos - parent_popup->panel_offset_start;
for (const Rect2 &E : autohide_areas) {
if (!Rect2(Point2(), get_size()).has_point(mouse_pos) && E.has_point(mouse_pos)) {
_close_pressed();
if (!Rect2(Point2(), control->get_size() * win_scale).has_point(areas_mouse_pos) && E.has_point(areas_mouse_pos)) {
_close_or_suspend();
return;
}
}
@@ -1869,6 +1925,7 @@ void PopupMenu::add_submenu_node_item(const String &p_label, PopupMenu *p_submen
queue_accessibility_update();
control->queue_redraw();
p_submenu->connect("popup_hide", callable_mp(this, &PopupMenu::_submenu_hidden));
child_controls_changed();
notify_property_list_changed();
_menu_changed();
@@ -2159,12 +2216,43 @@ void PopupMenu::set_item_submenu_node(int p_idx, PopupMenu *p_submenu) {
items.write[p_idx].submenu_bound = true;
}
}
p_submenu->connect("popup_hide", callable_mp(this, &PopupMenu::_submenu_hidden));
control->queue_redraw();
child_controls_changed();
_menu_changed();
}
void PopupMenu::_close_suspended_timeout() {
if (submenu_over != -1 && is_embedded()) {
ERR_FAIL_INDEX_MSG(submenu_over, items.size(), vformat("Invalid submenu index %d in _close_suspended_timeout.", submenu_over));
if (items[submenu_over].submenu->is_visible()) {
items[submenu_over].submenu->_close_or_suspend();
}
}
if (active_submenu_index != -1 && items[active_submenu_index].submenu->is_visible()) {
// Closes the submenu if the mouse is moved off the parent item toward the submenu,
// but comes to a stop before reaching the submenu and the timeout is reached.
items[active_submenu_index].submenu->_close_pressed();
}
}
void PopupMenu::_submenu_hidden() {
// Ensure the submenu_timer is not running to avoid any race conditions between opening and closing submenus.
ERR_FAIL_COND_MSG(!submenu_timer->is_stopped(), "The submenu_timer should never be running when the _submenu_hidden signal is emitted.");
ERR_FAIL_COND_MSG(active_submenu_index == -1, "The active_submenu_index should never be -1 when _submenu_hidden is entered.");
active_submenu_index = -1;
submenu_over = -1;
submenu_mouse_exited_ticks_msec = -1;
mouse_movement_was_tested = false;
close_was_suspended = false;
queue_accessibility_update();
control->queue_redraw();
if (!activated_by_keyboard) {
Point2 mouse_pos = is_embedded() ? get_mouse_position() : Point2(DisplayServer::get_singleton()->mouse_get_position() - get_position());
_mouse_over_update(mouse_pos);
}
}
void PopupMenu::toggle_item_checked(int p_idx) {
ERR_FAIL_INDEX(p_idx, items.size());
items.write[p_idx].checked = !items[p_idx].checked;
@@ -2917,7 +3005,7 @@ void PopupMenu::set_submenu_popup_delay(float p_time) {
if (p_time <= 0) {
p_time = 0.01;
}
submenu_timer_popup_delay = p_time;
submenu_timer->set_wait_time(p_time);
}
@@ -3298,7 +3386,6 @@ void PopupMenu::set_visible(bool p_visible) {
PopupMenu::PopupMenu() {
set_flag(FLAG_TRANSPARENT, true);
// The panel used to draw the panel style.
panel = memnew(PanelContainer);
panel->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
@@ -3319,16 +3406,16 @@ PopupMenu::PopupMenu() {
control->connect(SceneStringName(draw), callable_mp(this, &PopupMenu::_draw_items));
submenu_timer = memnew(Timer);
submenu_timer->set_wait_time(0.3);
submenu_timer->set_wait_time(submenu_timer_popup_delay); // Default is 0.2.
submenu_timer->set_one_shot(true);
submenu_timer->connect("timeout", callable_mp(this, &PopupMenu::_submenu_timeout));
add_child(submenu_timer, false, INTERNAL_MODE_FRONT);
minimum_lifetime_timer = memnew(Timer);
minimum_lifetime_timer->set_wait_time(0.3);
minimum_lifetime_timer->set_one_shot(true);
minimum_lifetime_timer->connect("timeout", callable_mp(this, &PopupMenu::_minimum_lifetime_timeout));
add_child(minimum_lifetime_timer, false, INTERNAL_MODE_FRONT);
close_suspended_timer = memnew(Timer);
close_suspended_timer->set_wait_time(CLOSE_SUSPENDED_TIMER_DELAY);
close_suspended_timer->set_one_shot(true);
close_suspended_timer->connect("timeout", callable_mp(this, &PopupMenu::_close_suspended_timeout));
add_child(close_suspended_timer, false, INTERNAL_MODE_FRONT);
property_helper.setup_for_instance(base_property_helper, this);