You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-11-21 14:57:09 +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:
@@ -660,8 +660,9 @@
|
||||
If [code]true[/code], [MenuBar] will use native menu when supported.
|
||||
[b]Note:[/b] If [PopupMenu] is linked to [StatusIndicator], [MenuBar], or another [PopupMenu] item it can use native menu regardless of this property, use [method is_native_menu] to check it.
|
||||
</member>
|
||||
<member name="submenu_popup_delay" type="float" setter="set_submenu_popup_delay" getter="get_submenu_popup_delay" default="0.3">
|
||||
<member name="submenu_popup_delay" type="float" setter="set_submenu_popup_delay" getter="get_submenu_popup_delay" default="0.2">
|
||||
Sets the delay time in seconds for the submenu item to popup on mouse hovering. If the popup menu is added as a child of another (acting as a submenu), it will inherit the delay time of the parent menu item.
|
||||
[b]Note:[/b] If the mouse is exiting a submenu item with an open submenu and enters a different submenu item, the submenu popup delay time is affected by the direction of the mouse movement toward the open submenu. If the mouse is moving toward the submenu, the open submenu will wait approximately [code]0.5[/code] seconds before closing, which then allows the hovered submenu item to open. This additional delay allows the mouse time to move to the open submenu across other menu items without prematurely closing. If the mouse is not moving toward the open submenu, for example in a downward direction, the open submenu will close immediately.
|
||||
</member>
|
||||
<member name="system_menu_id" type="int" setter="set_system_menu" getter="get_system_menu" enum="NativeMenu.SystemMenus" default="0">
|
||||
If set to one of the values of [enum NativeMenu.SystemMenus], this [PopupMenu] is bound to the special system menu. Only one [PopupMenu] can be bound to each special menu at a time.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -116,10 +116,10 @@ class PopupMenu : public Popup {
|
||||
NativeMenu::SystemMenus system_menu_id = NativeMenu::INVALID_MENU_ID;
|
||||
bool prefer_native = false;
|
||||
|
||||
bool close_allowed = false;
|
||||
bool activated_by_keyboard = false;
|
||||
|
||||
Timer *minimum_lifetime_timer = nullptr;
|
||||
Timer *close_suspended_timer = nullptr;
|
||||
bool close_was_suspended = false;
|
||||
Timer *submenu_timer = nullptr;
|
||||
List<Rect2> autohide_areas;
|
||||
mutable Vector<Item> items;
|
||||
@@ -129,6 +129,16 @@ class PopupMenu : public Popup {
|
||||
int mouse_over = -1;
|
||||
int prev_mouse_over = -1;
|
||||
int submenu_over = -1;
|
||||
int this_submenu_index = -1; // Always -1 for the parent popup, and always a positive int for every open submenu.
|
||||
int active_submenu_index = -1; // A positive int for the parent popup if any submenu is open.
|
||||
bool is_active_submenu_left = false;
|
||||
Vector<Point2> active_submenu_target_line;
|
||||
Point2 last_submenu_mouse_position;
|
||||
int submenu_mouse_exited_ticks_msec = -1;
|
||||
bool mouse_movement_was_tested = false;
|
||||
Point2 panel_offset_start;
|
||||
float submenu_timer_popup_delay = 0.2;
|
||||
const float CLOSE_SUSPENDED_TIMER_DELAY = 0.5;
|
||||
String _get_accel_text(const Item &p_item) const;
|
||||
int _get_mouse_over(const Point2 &p_over) const;
|
||||
void _mouse_over_update(const Point2 &p_over);
|
||||
@@ -144,6 +154,9 @@ class PopupMenu : public Popup {
|
||||
|
||||
void _activate_submenu(int p_over, bool p_by_keyboard = false);
|
||||
void _submenu_timeout();
|
||||
bool _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;
|
||||
void _close_or_suspend();
|
||||
void _close_suspended_timeout();
|
||||
|
||||
uint64_t popup_time_msec = 0;
|
||||
bool hide_on_item_selection = true;
|
||||
@@ -218,7 +231,6 @@ class PopupMenu : public Popup {
|
||||
|
||||
void _draw_items();
|
||||
|
||||
void _minimum_lifetime_timeout();
|
||||
void _close_pressed();
|
||||
void _menu_changed();
|
||||
void _input_from_window_internal(const Ref<InputEvent> &p_event);
|
||||
@@ -227,10 +239,9 @@ class PopupMenu : public Popup {
|
||||
int _get_item_checkable_type(int p_index) const;
|
||||
void _native_popup(const Rect2i &p_rect);
|
||||
String _atr(int p_idx, const String &p_text) const;
|
||||
void _submenu_hidden();
|
||||
|
||||
protected:
|
||||
void _hover_active_submenu_item();
|
||||
|
||||
virtual void _pre_popup() override;
|
||||
virtual Rect2i _popup_adjust_rect() const override;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user