Added hysteresis for popup sub-menus

This adds a small lag effect when opening submenus which allow the user to move directly to an item on the submenu without worrying about avoiding the autohide regions.
This commit is contained in:
Eric M 2020-09-07 22:57:50 +10:00
parent 30b6db99a9
commit c482e8ec85
4 changed files with 66 additions and 11 deletions

View file

@ -93,7 +93,7 @@ void Popup::_notification(int p_what) {
}
void Popup::_parent_focused() {
if (popped_up) {
if (popped_up && close_on_parent_focus) {
_close_pressed();
}
}
@ -112,7 +112,19 @@ void Popup::set_as_minsize() {
set_size(get_contents_minimum_size());
}
void Popup::set_close_on_parent_focus(bool p_close) {
close_on_parent_focus = p_close;
}
bool Popup::get_close_on_parent_focus() {
return close_on_parent_focus;
}
void Popup::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_close_on_parent_focus", "close"), &Popup::set_close_on_parent_focus);
ClassDB::bind_method(D_METHOD("get_close_on_parent_focus"), &Popup::get_close_on_parent_focus);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "close_on_parent_focus"), "set_close_on_parent_focus", "get_close_on_parent_focus");
ADD_SIGNAL(MethodInfo("popup_hide"));
}

View file

@ -40,6 +40,7 @@ class Popup : public Window {
LocalVector<Window *> visible_parents;
bool popped_up = false;
bool close_on_parent_focus = true;
void _input_from_window(const Ref<InputEvent> &p_event);
@ -57,6 +58,10 @@ protected:
public:
void set_as_minsize();
void set_close_on_parent_focus(bool p_close);
bool get_close_on_parent_focus();
Popup();
~Popup();
};

View file

@ -173,11 +173,11 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const {
return -1;
}
void PopupMenu::_activate_submenu(int over) {
Node *n = get_node(items[over].submenu);
ERR_FAIL_COND_MSG(!n, "Item subnode does not exist: " + items[over].submenu + ".");
void PopupMenu::_activate_submenu(int p_over) {
Node *n = get_node(items[p_over].submenu);
ERR_FAIL_COND_MSG(!n, "Item subnode does not exist: " + items[p_over].submenu + ".");
Popup *submenu_popup = Object::cast_to<Popup>(n);
ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[over].submenu + ".");
ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[p_over].submenu + ".");
if (submenu_popup->is_visible()) {
return; //already visible!
}
@ -190,7 +190,7 @@ void PopupMenu::_activate_submenu(int over) {
float scroll_offset = control->get_position().y;
Point2 submenu_pos = this_pos + Point2(this_rect.size.width, items[over]._ofs_cache + scroll_offset);
Point2 submenu_pos = this_pos + Point2(this_rect.size.width, items[p_over]._ofs_cache + scroll_offset);
Size2 submenu_size = submenu_popup->get_size();
// Fix pos if going outside parent rect
@ -198,6 +198,7 @@ void PopupMenu::_activate_submenu(int over) {
submenu_pos.x = this_pos.x - submenu_size.width;
}
submenu_popup->set_close_on_parent_focus(false);
submenu_popup->set_position(submenu_pos);
submenu_popup->set_as_minsize(); // Shrink the popup size to it's contents.
submenu_popup->popup();
@ -210,11 +211,11 @@ void PopupMenu::_activate_submenu(int over) {
// 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, this_rect.size.x, items[over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2));
submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[p_over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2));
// If there is an area below the submenu item, add an autohide area there.
if (items[over]._ofs_cache + items[over]._height_cache + scroll_offset <= control->get_size().height) {
int from = items[over]._ofs_cache + items[over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height;
if (items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset <= control->get_size().height) {
int from = items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height;
submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from));
}
}
@ -547,6 +548,31 @@ void PopupMenu::_draw_background() {
style->draw(ci2, Rect2(Point2(), margin_container->get_size()));
}
void PopupMenu::_minimum_lifetime_timeout() {
close_allowed = true;
// If the mouse still isn't in this popup after timer expires, close.
if (!get_visible_rect().has_point(get_mouse_position())) {
_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::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
@ -566,7 +592,7 @@ void PopupMenu::_notification(int p_what) {
control->update();
} break;
case NOTIFICATION_WM_MOUSE_ENTER: {
//grab_focus();
grab_focus();
} break;
case NOTIFICATION_WM_MOUSE_EXIT: {
if (mouse_over >= 0 && (items[mouse_over].submenu == "" || submenu_over != -1)) {
@ -1484,6 +1510,12 @@ PopupMenu::PopupMenu() {
submenu_timer->set_one_shot(true);
submenu_timer->connect("timeout", callable_mp(this, &PopupMenu::_submenu_timeout));
add_child(submenu_timer);
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);
}
PopupMenu::~PopupMenu() {

View file

@ -86,6 +86,9 @@ class PopupMenu : public Popup {
}
};
bool close_allowed = false;
Timer *minimum_lifetime_timer = nullptr;
Timer *submenu_timer;
List<Rect2> autohide_areas;
Vector<Item> items;
@ -102,7 +105,7 @@ class PopupMenu : public Popup {
void _scroll_to_item(int p_item);
void _gui_input(const Ref<InputEvent> &p_event);
void _activate_submenu(int over);
void _activate_submenu(int p_over);
void _submenu_timeout();
uint64_t popup_time_msec = 0;
@ -130,6 +133,9 @@ class PopupMenu : public Popup {
void _draw_items();
void _draw_background();
void _minimum_lifetime_timeout();
void _close_pressed();
protected:
friend class MenuButton;
void _notification(int p_what);