1/**************************************************************************/
2/* popup_menu.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "popup_menu.h"
32#include "popup_menu.compat.inc"
33
34#include "core/config/project_settings.h"
35#include "core/input/input.h"
36#include "core/os/keyboard.h"
37#include "core/os/os.h"
38#include "core/string/print_string.h"
39#include "core/string/translation.h"
40#include "scene/gui/menu_bar.h"
41#include "scene/theme/theme_db.h"
42
43String PopupMenu::_get_accel_text(const Item &p_item) const {
44 if (p_item.shortcut.is_valid()) {
45 return p_item.shortcut->get_as_text();
46 } else if (p_item.accel != Key::NONE) {
47 return keycode_get_string(p_item.accel);
48 }
49 return String();
50}
51
52Size2 PopupMenu::_get_item_icon_size(int p_idx) const {
53 const PopupMenu::Item &item = items[p_idx];
54 Size2 icon_size = item.get_icon_size();
55
56 int max_width = 0;
57 if (theme_cache.icon_max_width > 0) {
58 max_width = theme_cache.icon_max_width;
59 }
60 if (item.icon_max_width > 0 && (max_width == 0 || item.icon_max_width < max_width)) {
61 max_width = item.icon_max_width;
62 }
63
64 if (max_width > 0 && icon_size.width > max_width) {
65 icon_size.height = icon_size.height * max_width / icon_size.width;
66 icon_size.width = max_width;
67 }
68
69 return icon_size;
70}
71
72Size2 PopupMenu::_get_contents_minimum_size() const {
73 Size2 minsize = theme_cache.panel_style->get_minimum_size(); // Accounts for margin in the margin container
74 minsize.x += scroll_container->get_v_scroll_bar()->get_size().width * 2; // Adds a buffer so that the scrollbar does not render over the top of content
75
76 float max_w = 0.0;
77 float icon_w = 0.0;
78 int check_w = MAX(theme_cache.checked->get_width(), theme_cache.radio_checked->get_width()) + theme_cache.h_separation;
79 int accel_max_w = 0;
80 bool has_check = false;
81
82 for (int i = 0; i < items.size(); i++) {
83 Size2 item_size;
84 const_cast<PopupMenu *>(this)->_shape_item(i);
85
86 Size2 icon_size = _get_item_icon_size(i);
87 item_size.height = _get_item_height(i);
88 icon_w = MAX(icon_size.width, icon_w);
89
90 item_size.width += items[i].indent * theme_cache.indent;
91
92 if (items[i].checkable_type && !items[i].separator) {
93 has_check = true;
94 }
95
96 item_size.width += items[i].text_buf->get_size().x;
97 item_size.height += theme_cache.v_separation;
98
99 if (items[i].accel != Key::NONE || (items[i].shortcut.is_valid() && items[i].shortcut->has_valid_event())) {
100 int accel_w = theme_cache.h_separation * 2;
101 accel_w += items[i].accel_text_buf->get_size().x;
102 accel_max_w = MAX(accel_w, accel_max_w);
103 }
104
105 if (!items[i].submenu.is_empty()) {
106 item_size.width += theme_cache.submenu->get_width();
107 }
108
109 max_w = MAX(max_w, item_size.width);
110
111 minsize.height += item_size.height;
112 }
113
114 int item_side_padding = theme_cache.item_start_padding + theme_cache.item_end_padding;
115 minsize.width += max_w + icon_w + accel_max_w + item_side_padding;
116
117 if (has_check) {
118 minsize.width += check_w;
119 }
120
121 if (is_inside_tree()) {
122 int height_limit = get_usable_parent_rect().size.height;
123 if (minsize.height > height_limit) {
124 minsize.height = height_limit;
125 }
126 }
127
128 return minsize;
129}
130
131int PopupMenu::_get_item_height(int p_idx) const {
132 ERR_FAIL_INDEX_V(p_idx, items.size(), 0);
133
134 Size2 icon_size = _get_item_icon_size(p_idx);
135 int icon_height = icon_size.height;
136 if (items[p_idx].checkable_type && !items[p_idx].separator) {
137 icon_height = MAX(icon_height, MAX(theme_cache.checked->get_height(), theme_cache.radio_checked->get_height()));
138 }
139
140 int text_height = items[p_idx].text_buf->get_size().height;
141 if (text_height == 0 && !items[p_idx].separator) {
142 text_height = theme_cache.font->get_height(theme_cache.font_size);
143 }
144
145 int separator_height = 0;
146 if (items[p_idx].separator) {
147 separator_height = MAX(theme_cache.separator_style->get_minimum_size().height, MAX(theme_cache.labeled_separator_left->get_minimum_size().height, theme_cache.labeled_separator_right->get_minimum_size().height));
148 }
149
150 return MAX(separator_height, MAX(text_height, icon_height));
151}
152
153int PopupMenu::_get_items_total_height() const {
154 // Get total height of all items by taking max of icon height and font height
155 int items_total_height = 0;
156 for (int i = 0; i < items.size(); i++) {
157 items_total_height += _get_item_height(i) + theme_cache.v_separation;
158 }
159
160 // Subtract a separator which is not needed for the last item.
161 return items_total_height - theme_cache.v_separation;
162}
163
164int PopupMenu::_get_mouse_over(const Point2 &p_over) const {
165 if (p_over.x < 0 || p_over.x >= get_size().width) {
166 return -1;
167 }
168
169 // Accounts for margin in the margin container
170 Point2 ofs = theme_cache.panel_style->get_offset() + Point2(0, theme_cache.v_separation / 2);
171
172 if (ofs.y > p_over.y) {
173 return -1;
174 }
175
176 for (int i = 0; i < items.size(); i++) {
177 ofs.y += i > 0 ? theme_cache.v_separation : (float)theme_cache.v_separation / 2;
178
179 ofs.y += _get_item_height(i);
180
181 if (p_over.y - control->get_position().y < ofs.y) {
182 return i;
183 }
184 }
185
186 return -1;
187}
188
189void PopupMenu::_activate_submenu(int p_over, bool p_by_keyboard) {
190 Node *n = get_node_or_null(items[p_over].submenu);
191 ERR_FAIL_NULL_MSG(n, "Item subnode does not exist: '" + items[p_over].submenu + "'.");
192 Popup *submenu_popup = Object::cast_to<Popup>(n);
193 ERR_FAIL_NULL_MSG(submenu_popup, "Item subnode is not a Popup: '" + items[p_over].submenu + "'.");
194 if (submenu_popup->is_visible()) {
195 return; // Already visible.
196 }
197
198 Point2 this_pos = get_position();
199 Rect2 this_rect(this_pos, get_size());
200
201 float scroll_offset = control->get_position().y;
202
203 submenu_popup->reset_size(); // Shrink the popup size to its contents.
204 Size2 submenu_size = submenu_popup->get_size();
205
206 Point2 submenu_pos;
207 if (control->is_layout_rtl()) {
208 submenu_pos = this_pos + Point2(-submenu_size.width, items[p_over]._ofs_cache + scroll_offset - theme_cache.v_separation / 2);
209 } else {
210 submenu_pos = this_pos + Point2(this_rect.size.width, items[p_over]._ofs_cache + scroll_offset - theme_cache.v_separation / 2);
211 }
212
213 // Fix pos if going outside parent rect.
214 if (submenu_pos.x < get_parent_rect().position.x) {
215 submenu_pos.x = this_pos.x + submenu_size.width;
216 }
217
218 if (submenu_pos.x + submenu_size.width > get_parent_rect().position.x + get_parent_rect().size.width) {
219 submenu_pos.x = this_pos.x - submenu_size.width;
220 }
221
222 submenu_popup->set_position(submenu_pos);
223
224 PopupMenu *submenu_pum = Object::cast_to<PopupMenu>(submenu_popup);
225 if (!submenu_pum) {
226 submenu_popup->popup();
227 return;
228 }
229
230 submenu_pum->activated_by_keyboard = p_by_keyboard;
231
232 // If not triggered by the mouse, start the popup with its first enabled item focused.
233 if (p_by_keyboard) {
234 for (int i = 0; i < submenu_pum->get_item_count(); i++) {
235 if (!submenu_pum->is_item_disabled(i)) {
236 submenu_pum->set_focused_item(i);
237 break;
238 }
239 }
240 }
241
242 submenu_pum->popup();
243
244 // Set autohide areas.
245
246 Rect2 safe_area = this_rect;
247 safe_area.position.y += items[p_over]._ofs_cache + scroll_offset + theme_cache.panel_style->get_offset().height - theme_cache.v_separation / 2;
248 safe_area.size.y = items[p_over]._height_cache + theme_cache.v_separation;
249 Viewport *vp = submenu_popup->get_embedder();
250 if (vp) {
251 vp->subwindow_set_popup_safe_rect(submenu_popup, safe_area);
252 } else {
253 DisplayServer::get_singleton()->window_set_popup_safe_rect(submenu_popup->get_window_id(), safe_area);
254 }
255
256 // Make the position of the parent popup relative to submenu popup.
257 this_rect.position = this_rect.position - submenu_pum->get_position();
258
259 // Autohide area above the submenu item.
260 submenu_pum->clear_autohide_areas();
261 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 + theme_cache.panel_style->get_offset().height - theme_cache.v_separation / 2));
262
263 // If there is an area below the submenu item, add an autohide area there.
264 if (items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset <= control->get_size().height) {
265 int from = items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset + theme_cache.v_separation / 2 + theme_cache.panel_style->get_offset().height;
266 submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from));
267 }
268}
269
270void PopupMenu::_parent_focused() {
271 if (is_embedded()) {
272 Point2 mouse_pos_adjusted;
273 Window *window_parent = Object::cast_to<Window>(get_parent()->get_viewport());
274 while (window_parent) {
275 if (!window_parent->is_embedded()) {
276 mouse_pos_adjusted += window_parent->get_position();
277 break;
278 }
279
280 window_parent = Object::cast_to<Window>(window_parent->get_parent()->get_viewport());
281 }
282
283 Rect2 safe_area = get_embedder()->subwindow_get_popup_safe_rect(this);
284 Point2 pos = DisplayServer::get_singleton()->mouse_get_position() - mouse_pos_adjusted;
285 if (safe_area == Rect2i() || !safe_area.has_point(pos)) {
286 Popup::_parent_focused();
287 } else {
288 grab_focus();
289 }
290 }
291}
292
293void PopupMenu::_submenu_timeout() {
294 if (mouse_over == submenu_over) {
295 _activate_submenu(mouse_over);
296 }
297
298 submenu_over = -1;
299}
300
301void PopupMenu::gui_input(const Ref<InputEvent> &p_event) {
302 ERR_FAIL_COND(p_event.is_null());
303
304 if (!items.is_empty()) {
305 Input *input = Input::get_singleton();
306 Ref<InputEventJoypadMotion> joypadmotion_event = p_event;
307 Ref<InputEventJoypadButton> joypadbutton_event = p_event;
308 bool is_joypad_event = (joypadmotion_event.is_valid() || joypadbutton_event.is_valid());
309
310 if (p_event->is_action("ui_down", true) && p_event->is_pressed()) {
311 if (is_joypad_event) {
312 if (!input->is_action_just_pressed("ui_down", true)) {
313 return;
314 }
315 set_process_internal(true);
316 }
317 int search_from = mouse_over + 1;
318 if (search_from >= items.size()) {
319 search_from = 0;
320 }
321
322 bool match_found = false;
323 for (int i = search_from; i < items.size(); i++) {
324 if (!items[i].separator && !items[i].disabled) {
325 mouse_over = i;
326 emit_signal(SNAME("id_focused"), i);
327 scroll_to_item(i);
328 control->queue_redraw();
329 set_input_as_handled();
330 match_found = true;
331 break;
332 }
333 }
334
335 if (!match_found) {
336 // If the last item is not selectable, try re-searching from the start.
337 for (int i = 0; i < search_from; i++) {
338 if (!items[i].separator && !items[i].disabled) {
339 mouse_over = i;
340 emit_signal(SNAME("id_focused"), i);
341 scroll_to_item(i);
342 control->queue_redraw();
343 set_input_as_handled();
344 break;
345 }
346 }
347 }
348 } else if (p_event->is_action("ui_up", true) && p_event->is_pressed()) {
349 if (is_joypad_event) {
350 if (!input->is_action_just_pressed("ui_up", true)) {
351 return;
352 }
353 set_process_internal(true);
354 }
355 int search_from = mouse_over - 1;
356 if (search_from < 0) {
357 search_from = items.size() - 1;
358 }
359
360 bool match_found = false;
361 for (int i = search_from; i >= 0; i--) {
362 if (!items[i].separator && !items[i].disabled) {
363 mouse_over = i;
364 emit_signal(SNAME("id_focused"), i);
365 scroll_to_item(i);
366 control->queue_redraw();
367 set_input_as_handled();
368 match_found = true;
369 break;
370 }
371 }
372
373 if (!match_found) {
374 // If the first item is not selectable, try re-searching from the end.
375 for (int i = items.size() - 1; i >= search_from; i--) {
376 if (!items[i].separator && !items[i].disabled) {
377 mouse_over = i;
378 emit_signal(SNAME("id_focused"), i);
379 scroll_to_item(i);
380 control->queue_redraw();
381 set_input_as_handled();
382 break;
383 }
384 }
385 }
386 } else if (p_event->is_action("ui_left", true) && p_event->is_pressed()) {
387 Node *n = get_parent();
388 if (n) {
389 if (Object::cast_to<PopupMenu>(n)) {
390 hide();
391 set_input_as_handled();
392 } else if (Object::cast_to<MenuBar>(n)) {
393 Object::cast_to<MenuBar>(n)->gui_input(p_event);
394 set_input_as_handled();
395 return;
396 }
397 }
398 } else if (p_event->is_action("ui_right", true) && p_event->is_pressed()) {
399 if (mouse_over >= 0 && mouse_over < items.size() && !items[mouse_over].separator && !items[mouse_over].submenu.is_empty() && submenu_over != mouse_over) {
400 _activate_submenu(mouse_over, true);
401 set_input_as_handled();
402 } else {
403 Node *n = get_parent();
404 if (n && Object::cast_to<MenuBar>(n)) {
405 Object::cast_to<MenuBar>(n)->gui_input(p_event);
406 set_input_as_handled();
407 return;
408 }
409 }
410 } else if (p_event->is_action("ui_accept", true) && p_event->is_pressed()) {
411 if (mouse_over >= 0 && mouse_over < items.size() && !items[mouse_over].separator) {
412 if (!items[mouse_over].submenu.is_empty() && submenu_over != mouse_over) {
413 _activate_submenu(mouse_over, true);
414 } else {
415 activate_item(mouse_over);
416 }
417 set_input_as_handled();
418 }
419 }
420 }
421
422 // Make an area which does not include v scrollbar, so that items are not activated when dragging scrollbar.
423 Rect2 item_clickable_area = scroll_container->get_rect();
424 if (scroll_container->get_v_scroll_bar()->is_visible_in_tree()) {
425 if (is_layout_rtl()) {
426 item_clickable_area.position.x += scroll_container->get_v_scroll_bar()->get_size().width;
427 } else {
428 item_clickable_area.size.width -= scroll_container->get_v_scroll_bar()->get_size().width;
429 }
430 }
431
432 Ref<InputEventMouseButton> b = p_event;
433
434 if (b.is_valid()) {
435 if (!item_clickable_area.has_point(b->get_position())) {
436 return;
437 }
438
439 MouseButton button_idx = b->get_button_index();
440 if (!b->is_pressed()) {
441 // Activate the item on release of either the left mouse button or
442 // any mouse button held down when the popup was opened.
443 // This allows for opening the popup and triggering an action in a single mouse click.
444 if (button_idx == MouseButton::LEFT || initial_button_mask.has_flag(mouse_button_to_mask(button_idx))) {
445 bool was_during_grabbed_click = during_grabbed_click;
446 during_grabbed_click = false;
447 initial_button_mask.clear();
448
449 // Disable clicks under a time threshold to avoid selection right when opening the popup.
450 uint64_t now = OS::get_singleton()->get_ticks_msec();
451 uint64_t diff = now - popup_time_msec;
452 if (diff < 150) {
453 return;
454 }
455
456 int over = _get_mouse_over(b->get_position());
457 if (over < 0) {
458 if (!was_during_grabbed_click) {
459 hide();
460 }
461 return;
462 }
463
464 if (items[over].separator || items[over].disabled) {
465 return;
466 }
467
468 if (!items[over].submenu.is_empty()) {
469 _activate_submenu(over);
470 return;
471 }
472 activate_item(over);
473 }
474 }
475 }
476
477 Ref<InputEventMouseMotion> m = p_event;
478
479 if (m.is_valid()) {
480 if (m->get_velocity().is_zero_approx()) {
481 return;
482 }
483 activated_by_keyboard = false;
484
485 for (const Rect2 &E : autohide_areas) {
486 if (!Rect2(Point2(), get_size()).has_point(m->get_position()) && E.has_point(m->get_position())) {
487 _close_pressed();
488 return;
489 }
490 }
491
492 if (!item_clickable_area.has_point(m->get_position())) {
493 return;
494 }
495
496 int over = _get_mouse_over(m->get_position());
497 int id = (over < 0 || items[over].separator || items[over].disabled) ? -1 : (items[over].id >= 0 ? items[over].id : over);
498
499 if (id < 0) {
500 mouse_over = -1;
501 control->queue_redraw();
502 return;
503 }
504
505 if (!items[over].submenu.is_empty() && submenu_over != over) {
506 submenu_over = over;
507 submenu_timer->start();
508 }
509
510 if (over != mouse_over) {
511 mouse_over = over;
512 control->queue_redraw();
513 }
514 }
515
516 Ref<InputEventKey> k = p_event;
517
518 if (allow_search && k.is_valid() && k->get_unicode() && k->is_pressed()) {
519 uint64_t now = OS::get_singleton()->get_ticks_msec();
520 uint64_t diff = now - search_time_msec;
521 uint64_t max_interval = uint64_t(GLOBAL_GET("gui/timers/incremental_search_max_interval_msec"));
522 search_time_msec = now;
523
524 if (diff > max_interval) {
525 search_string = "";
526 }
527
528 if (String::chr(k->get_unicode()) != search_string) {
529 search_string += String::chr(k->get_unicode());
530 }
531
532 for (int i = mouse_over + 1; i <= items.size(); i++) {
533 if (i == items.size()) {
534 if (mouse_over <= 0) {
535 break;
536 } else {
537 i = 0;
538 }
539 }
540
541 if (i == mouse_over) {
542 break;
543 }
544
545 if (items[i].text.findn(search_string) == 0) {
546 mouse_over = i;
547 emit_signal(SNAME("id_focused"), i);
548 scroll_to_item(i);
549 control->queue_redraw();
550 set_input_as_handled();
551 break;
552 }
553 }
554 }
555}
556
557void PopupMenu::_draw_items() {
558 control->set_custom_minimum_size(Size2(0, _get_items_total_height()));
559 RID ci = control->get_canvas_item();
560
561 Size2 margin_size;
562 margin_size.width = margin_container->get_margin_size(SIDE_LEFT) + margin_container->get_margin_size(SIDE_RIGHT);
563 margin_size.height = margin_container->get_margin_size(SIDE_TOP) + margin_container->get_margin_size(SIDE_BOTTOM);
564
565 // Space between the item content and the sides of popup menu.
566 bool rtl = control->is_layout_rtl();
567 // In Item::checkable_type enum order (less the non-checkable member), with disabled repeated at the end.
568 Ref<Texture2D> check[] = { theme_cache.checked, theme_cache.radio_checked, theme_cache.checked_disabled, theme_cache.radio_checked_disabled };
569 Ref<Texture2D> uncheck[] = { theme_cache.unchecked, theme_cache.radio_unchecked, theme_cache.unchecked_disabled, theme_cache.radio_unchecked_disabled };
570 Ref<Texture2D> submenu;
571 if (rtl) {
572 submenu = theme_cache.submenu_mirrored;
573 } else {
574 submenu = theme_cache.submenu;
575 }
576
577 float scroll_width = scroll_container->get_v_scroll_bar()->is_visible_in_tree() ? scroll_container->get_v_scroll_bar()->get_size().width : 0;
578 float display_width = control->get_size().width - scroll_width;
579
580 // Find the widest icon and whether any items have a checkbox, and store the offsets for each.
581 float icon_ofs = 0.0;
582 bool has_check = false;
583 for (int i = 0; i < items.size(); i++) {
584 if (items[i].separator) {
585 continue;
586 }
587
588 Size2 icon_size = _get_item_icon_size(i);
589 icon_ofs = MAX(icon_size.width, icon_ofs);
590
591 if (items[i].checkable_type) {
592 has_check = true;
593 }
594 }
595 if (icon_ofs > 0.0) {
596 icon_ofs += theme_cache.h_separation;
597 }
598
599 float check_ofs = 0.0;
600 if (has_check) {
601 for (int i = 0; i < 4; i++) {
602 check_ofs = MAX(check_ofs, check[i]->get_width());
603 check_ofs = MAX(check_ofs, uncheck[i]->get_width());
604 }
605 check_ofs += theme_cache.h_separation;
606 }
607
608 Point2 ofs;
609
610 // Loop through all items and draw each.
611 for (int i = 0; i < items.size(); i++) {
612 // For the first item only add half a separation. For all other items, add a whole separation to the offset.
613 ofs.y += i > 0 ? theme_cache.v_separation : (float)theme_cache.v_separation / 2;
614
615 _shape_item(i);
616
617 Point2 item_ofs = ofs;
618 Size2 icon_size = _get_item_icon_size(i);
619 float h = _get_item_height(i);
620
621 if (i == mouse_over) {
622 if (rtl) {
623 theme_cache.hover_style->draw(ci, Rect2(item_ofs + Point2(scroll_width, -theme_cache.v_separation / 2), Size2(display_width, h + theme_cache.v_separation)));
624 } else {
625 theme_cache.hover_style->draw(ci, Rect2(item_ofs + Point2(0, -theme_cache.v_separation / 2), Size2(display_width, h + theme_cache.v_separation)));
626 }
627 }
628
629 String text = items[i].xl_text;
630
631 // Separator
632 item_ofs.x += items[i].indent * theme_cache.indent;
633 if (items[i].separator) {
634 if (!text.is_empty() || !items[i].icon.is_null()) {
635 int content_size = items[i].text_buf->get_size().width + theme_cache.h_separation * 2;
636 if (!items[i].icon.is_null()) {
637 content_size += icon_size.width + theme_cache.h_separation;
638 }
639
640 int content_center = display_width / 2;
641 int content_left = content_center - content_size / 2;
642 int content_right = content_center + content_size / 2;
643 if (content_left > item_ofs.x) {
644 int sep_h = theme_cache.labeled_separator_left->get_minimum_size().height;
645 int sep_ofs = Math::floor((h - sep_h) / 2.0);
646 theme_cache.labeled_separator_left->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(MAX(0, content_left - item_ofs.x), sep_h)));
647 }
648 if (content_right < display_width) {
649 int sep_h = theme_cache.labeled_separator_right->get_minimum_size().height;
650 int sep_ofs = Math::floor((h - sep_h) / 2.0);
651 theme_cache.labeled_separator_right->draw(ci, Rect2(Point2(content_right, item_ofs.y + sep_ofs), Size2(MAX(0, display_width - content_right), sep_h)));
652 }
653 } else {
654 int sep_h = theme_cache.separator_style->get_minimum_size().height;
655 int sep_ofs = Math::floor((h - sep_h) / 2.0);
656 theme_cache.separator_style->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(display_width, sep_h)));
657 }
658 }
659
660 Color icon_color(1, 1, 1, items[i].disabled && !items[i].separator ? 0.5 : 1);
661
662 icon_color *= items[i].icon_modulate;
663
664 // For non-separator items, add some padding for the content.
665 item_ofs.x += theme_cache.item_start_padding;
666
667 // Checkboxes
668 if (items[i].checkable_type && !items[i].separator) {
669 int disabled = int(items[i].disabled) * 2;
670 Texture2D *icon = (items[i].checked ? check[items[i].checkable_type - 1 + disabled] : uncheck[items[i].checkable_type - 1 + disabled]).ptr();
671 if (rtl) {
672 icon->draw(ci, Size2(control->get_size().width - item_ofs.x - icon->get_width(), item_ofs.y) + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color);
673 } else {
674 icon->draw(ci, item_ofs + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color);
675 }
676 }
677
678 int separator_ofs = (display_width - items[i].text_buf->get_size().width) / 2;
679
680 // Icon
681 if (!items[i].icon.is_null()) {
682 const Point2 icon_offset = Point2(0, Math::floor((h - icon_size.height) / 2.0));
683 Point2 icon_pos;
684
685 if (items[i].separator) {
686 separator_ofs -= (icon_size.width + theme_cache.h_separation) / 2;
687
688 if (rtl) {
689 icon_pos = Size2(control->get_size().width - item_ofs.x - separator_ofs - icon_size.width, item_ofs.y);
690 } else {
691 icon_pos = item_ofs + Size2(separator_ofs, 0);
692 }
693 } else {
694 if (rtl) {
695 icon_pos = Size2(control->get_size().width - item_ofs.x - check_ofs - icon_size.width, item_ofs.y);
696 } else {
697 icon_pos = item_ofs + Size2(check_ofs, 0);
698 }
699 }
700
701 items[i].icon->draw_rect(ci, Rect2(icon_pos + icon_offset, icon_size), false, icon_color);
702 }
703
704 // Submenu arrow on right hand side.
705 if (!items[i].submenu.is_empty()) {
706 if (rtl) {
707 submenu->draw(ci, Point2(scroll_width + theme_cache.panel_style->get_margin(SIDE_LEFT) + theme_cache.item_end_padding, item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color);
708 } else {
709 submenu->draw(ci, Point2(display_width - theme_cache.panel_style->get_margin(SIDE_RIGHT) - submenu->get_width() - theme_cache.item_end_padding, item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color);
710 }
711 }
712
713 // Text
714 if (items[i].separator) {
715 if (!text.is_empty()) {
716 Vector2 text_pos = Point2(separator_ofs, item_ofs.y + Math::floor((h - items[i].text_buf->get_size().y) / 2.0));
717 if (!rtl && !items[i].icon.is_null()) {
718 text_pos.x += icon_size.width + theme_cache.h_separation;
719 }
720
721 if (theme_cache.font_separator_outline_size > 0 && theme_cache.font_separator_outline_color.a > 0) {
722 items[i].text_buf->draw_outline(ci, text_pos, theme_cache.font_separator_outline_size, theme_cache.font_separator_outline_color);
723 }
724 items[i].text_buf->draw(ci, text_pos, theme_cache.font_separator_color);
725 }
726 } else {
727 item_ofs.x += icon_ofs + check_ofs;
728
729 if (rtl) {
730 Vector2 text_pos = Size2(control->get_size().width - items[i].text_buf->get_size().width - item_ofs.x, item_ofs.y) + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0));
731 if (theme_cache.font_outline_size > 0 && theme_cache.font_outline_color.a > 0) {
732 items[i].text_buf->draw_outline(ci, text_pos, theme_cache.font_outline_size, theme_cache.font_outline_color);
733 }
734 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));
735 } else {
736 Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0));
737 if (theme_cache.font_outline_size > 0 && theme_cache.font_outline_color.a > 0) {
738 items[i].text_buf->draw_outline(ci, text_pos, theme_cache.font_outline_size, theme_cache.font_outline_color);
739 }
740 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));
741 }
742 }
743
744 // Accelerator / Shortcut
745 if (items[i].accel != Key::NONE || (items[i].shortcut.is_valid() && items[i].shortcut->has_valid_event())) {
746 if (rtl) {
747 item_ofs.x = scroll_width + theme_cache.panel_style->get_margin(SIDE_LEFT) + theme_cache.item_end_padding;
748 } else {
749 item_ofs.x = display_width - theme_cache.panel_style->get_margin(SIDE_RIGHT) - items[i].accel_text_buf->get_size().x - theme_cache.item_end_padding;
750 }
751 Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0));
752 if (theme_cache.font_outline_size > 0 && theme_cache.font_outline_color.a > 0) {
753 items[i].accel_text_buf->draw_outline(ci, text_pos, theme_cache.font_outline_size, theme_cache.font_outline_color);
754 }
755 items[i].accel_text_buf->draw(ci, text_pos, i == mouse_over ? theme_cache.font_hover_color : theme_cache.font_accelerator_color);
756 }
757
758 // Cache the item vertical offset from the first item and the height.
759 items.write[i]._ofs_cache = ofs.y;
760 items.write[i]._height_cache = h;
761
762 ofs.y += h;
763 }
764}
765
766void PopupMenu::_draw_background() {
767 RID ci2 = margin_container->get_canvas_item();
768 theme_cache.panel_style->draw(ci2, Rect2(Point2(), margin_container->get_size()));
769}
770
771void PopupMenu::_minimum_lifetime_timeout() {
772 close_allowed = true;
773 // If the mouse still isn't in this popup after timer expires, close.
774 if (!activated_by_keyboard && !get_visible_rect().has_point(get_mouse_position())) {
775 _close_pressed();
776 }
777}
778
779void PopupMenu::_close_pressed() {
780 // Only apply minimum lifetime to submenus.
781 PopupMenu *parent_pum = Object::cast_to<PopupMenu>(get_parent());
782 if (!parent_pum) {
783 Popup::_close_pressed();
784 return;
785 }
786
787 // If the timer has expired, close. If timer is still running, do nothing.
788 if (close_allowed) {
789 close_allowed = false;
790 Popup::_close_pressed();
791 } else if (minimum_lifetime_timer->is_stopped()) {
792 minimum_lifetime_timer->start();
793 }
794}
795
796void PopupMenu::_shape_item(int p_idx) {
797 if (items.write[p_idx].dirty) {
798 items.write[p_idx].text_buf->clear();
799
800 Ref<Font> font = items[p_idx].separator ? theme_cache.font_separator : theme_cache.font;
801 int font_size = items[p_idx].separator ? theme_cache.font_separator_size : theme_cache.font_size;
802
803 if (items[p_idx].text_direction == Control::TEXT_DIRECTION_INHERITED) {
804 items.write[p_idx].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
805 } else {
806 items.write[p_idx].text_buf->set_direction((TextServer::Direction)items[p_idx].text_direction);
807 }
808 items.write[p_idx].text_buf->add_string(items.write[p_idx].xl_text, font, font_size, items[p_idx].language);
809
810 items.write[p_idx].accel_text_buf->clear();
811 items.write[p_idx].accel_text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
812 items.write[p_idx].accel_text_buf->add_string(_get_accel_text(items.write[p_idx]), font, font_size);
813 items.write[p_idx].dirty = false;
814 }
815}
816
817void PopupMenu::_menu_changed() {
818 emit_signal(SNAME("menu_changed"));
819}
820
821void PopupMenu::add_child_notify(Node *p_child) {
822 Window::add_child_notify(p_child);
823
824 PopupMenu *pm = Object::cast_to<PopupMenu>(p_child);
825 if (!pm) {
826 return;
827 }
828 p_child->connect("menu_changed", callable_mp(this, &PopupMenu::_menu_changed));
829 _menu_changed();
830}
831
832void PopupMenu::remove_child_notify(Node *p_child) {
833 Window::remove_child_notify(p_child);
834
835 PopupMenu *pm = Object::cast_to<PopupMenu>(p_child);
836 if (!pm) {
837 return;
838 }
839 p_child->disconnect("menu_changed", callable_mp(this, &PopupMenu::_menu_changed));
840 _menu_changed();
841}
842
843void PopupMenu::_notification(int p_what) {
844 switch (p_what) {
845 case NOTIFICATION_ENTER_TREE: {
846 PopupMenu *pm = Object::cast_to<PopupMenu>(get_parent());
847 if (pm) {
848 // Inherit submenu's popup delay time from parent menu.
849 float pm_delay = pm->get_submenu_popup_delay();
850 set_submenu_popup_delay(pm_delay);
851 }
852 if (!is_embedded()) {
853 set_flag(FLAG_NO_FOCUS, true);
854 }
855 } break;
856
857 case NOTIFICATION_THEME_CHANGED:
858 case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
859 case NOTIFICATION_TRANSLATION_CHANGED: {
860 for (int i = 0; i < items.size(); i++) {
861 items.write[i].xl_text = atr(items[i].text);
862 items.write[i].dirty = true;
863 _shape_item(i);
864 }
865
866 child_controls_changed();
867 _menu_changed();
868 control->queue_redraw();
869 } break;
870
871 case NOTIFICATION_WM_MOUSE_ENTER: {
872 grab_focus();
873 } break;
874
875 case NOTIFICATION_WM_MOUSE_EXIT: {
876 if (mouse_over >= 0 && (items[mouse_over].submenu.is_empty() || submenu_over != -1)) {
877 mouse_over = -1;
878 control->queue_redraw();
879 }
880 } break;
881
882 case NOTIFICATION_POST_POPUP: {
883 initial_button_mask = Input::get_singleton()->get_mouse_button_mask();
884 during_grabbed_click = (bool)initial_button_mask;
885 } break;
886
887 case NOTIFICATION_INTERNAL_PROCESS: {
888 Input *input = Input::get_singleton();
889
890 if (input->is_action_just_released("ui_up") || input->is_action_just_released("ui_down")) {
891 gamepad_event_delay_ms = DEFAULT_GAMEPAD_EVENT_DELAY_MS;
892 set_process_internal(false);
893 return;
894 }
895 gamepad_event_delay_ms -= get_process_delta_time();
896 if (gamepad_event_delay_ms <= 0) {
897 if (input->is_action_pressed("ui_down")) {
898 gamepad_event_delay_ms = GAMEPAD_EVENT_REPEAT_RATE_MS + gamepad_event_delay_ms;
899 int search_from = mouse_over + 1;
900 if (search_from >= items.size()) {
901 search_from = 0;
902 }
903
904 bool match_found = false;
905 for (int i = search_from; i < items.size(); i++) {
906 if (!items[i].separator && !items[i].disabled) {
907 mouse_over = i;
908 emit_signal(SNAME("id_focused"), i);
909 scroll_to_item(i);
910 control->queue_redraw();
911 match_found = true;
912 break;
913 }
914 }
915
916 if (!match_found) {
917 // If the last item is not selectable, try re-searching from the start.
918 for (int i = 0; i < search_from; i++) {
919 if (!items[i].separator && !items[i].disabled) {
920 mouse_over = i;
921 emit_signal(SNAME("id_focused"), i);
922 scroll_to_item(i);
923 control->queue_redraw();
924 break;
925 }
926 }
927 }
928 }
929
930 if (input->is_action_pressed("ui_up")) {
931 gamepad_event_delay_ms = GAMEPAD_EVENT_REPEAT_RATE_MS + gamepad_event_delay_ms;
932 int search_from = mouse_over - 1;
933 if (search_from < 0) {
934 search_from = items.size() - 1;
935 }
936
937 bool match_found = false;
938 for (int i = search_from; i >= 0; i--) {
939 if (!items[i].separator && !items[i].disabled) {
940 mouse_over = i;
941 emit_signal(SNAME("id_focused"), i);
942 scroll_to_item(i);
943 control->queue_redraw();
944 match_found = true;
945 break;
946 }
947 }
948
949 if (!match_found) {
950 // If the first item is not selectable, try re-searching from the end.
951 for (int i = items.size() - 1; i >= search_from; i--) {
952 if (!items[i].separator && !items[i].disabled) {
953 mouse_over = i;
954 emit_signal(SNAME("id_focused"), i);
955 scroll_to_item(i);
956 control->queue_redraw();
957 break;
958 }
959 }
960 }
961 }
962 }
963
964 // Only used when using operating system windows.
965 if (!activated_by_keyboard && !is_embedded() && autohide_areas.size()) {
966 Point2 mouse_pos = DisplayServer::get_singleton()->mouse_get_position();
967 mouse_pos -= get_position();
968
969 for (const Rect2 &E : autohide_areas) {
970 if (!Rect2(Point2(), get_size()).has_point(mouse_pos) && E.has_point(mouse_pos)) {
971 _close_pressed();
972 return;
973 }
974 }
975 }
976 } break;
977
978 case NOTIFICATION_VISIBILITY_CHANGED: {
979 if (!is_visible()) {
980 if (mouse_over >= 0) {
981 mouse_over = -1;
982 control->queue_redraw();
983 }
984
985 for (int i = 0; i < items.size(); i++) {
986 if (items[i].submenu.is_empty()) {
987 continue;
988 }
989
990 Node *n = get_node(items[i].submenu);
991 if (!n) {
992 continue;
993 }
994
995 PopupMenu *pm = Object::cast_to<PopupMenu>(n);
996 if (!pm || !pm->is_visible()) {
997 continue;
998 }
999
1000 pm->hide();
1001 }
1002
1003 set_process_internal(false);
1004 } else {
1005 if (!is_embedded()) {
1006 set_process_internal(true);
1007 }
1008
1009 // Set margin on the margin container
1010 margin_container->add_theme_constant_override("margin_left", theme_cache.panel_style->get_margin(Side::SIDE_LEFT));
1011 margin_container->add_theme_constant_override("margin_top", theme_cache.panel_style->get_margin(Side::SIDE_TOP));
1012 margin_container->add_theme_constant_override("margin_right", theme_cache.panel_style->get_margin(Side::SIDE_RIGHT));
1013 margin_container->add_theme_constant_override("margin_bottom", theme_cache.panel_style->get_margin(Side::SIDE_BOTTOM));
1014 }
1015 } break;
1016 }
1017}
1018
1019/* Methods to add items with or without icon, checkbox, shortcut.
1020 * Be sure to keep them in sync when adding new properties in the Item struct.
1021 */
1022
1023#define ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel) \
1024 item.text = p_label; \
1025 item.xl_text = atr(p_label); \
1026 item.id = p_id == -1 ? items.size() : p_id; \
1027 item.accel = p_accel;
1028
1029void PopupMenu::add_item(const String &p_label, int p_id, Key p_accel) {
1030 Item item;
1031 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1032 items.push_back(item);
1033
1034 _shape_item(items.size() - 1);
1035 control->queue_redraw();
1036
1037 child_controls_changed();
1038 notify_property_list_changed();
1039 _menu_changed();
1040}
1041
1042void PopupMenu::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id, Key p_accel) {
1043 Item item;
1044 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1045 item.icon = p_icon;
1046 items.push_back(item);
1047
1048 _shape_item(items.size() - 1);
1049 control->queue_redraw();
1050
1051 child_controls_changed();
1052 notify_property_list_changed();
1053 _menu_changed();
1054}
1055
1056void PopupMenu::add_check_item(const String &p_label, int p_id, Key p_accel) {
1057 Item item;
1058 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1059 item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
1060 items.push_back(item);
1061
1062 _shape_item(items.size() - 1);
1063 control->queue_redraw();
1064
1065 child_controls_changed();
1066 _menu_changed();
1067}
1068
1069void PopupMenu::add_icon_check_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id, Key p_accel) {
1070 Item item;
1071 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1072 item.icon = p_icon;
1073 item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
1074 items.push_back(item);
1075
1076 _shape_item(items.size() - 1);
1077 control->queue_redraw();
1078
1079 child_controls_changed();
1080}
1081
1082void PopupMenu::add_radio_check_item(const String &p_label, int p_id, Key p_accel) {
1083 Item item;
1084 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1085 item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
1086 items.push_back(item);
1087
1088 _shape_item(items.size() - 1);
1089 control->queue_redraw();
1090
1091 child_controls_changed();
1092 _menu_changed();
1093}
1094
1095void PopupMenu::add_icon_radio_check_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id, Key p_accel) {
1096 Item item;
1097 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1098 item.icon = p_icon;
1099 item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
1100 items.push_back(item);
1101
1102 _shape_item(items.size() - 1);
1103 control->queue_redraw();
1104
1105 child_controls_changed();
1106 _menu_changed();
1107}
1108
1109void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int p_default_state, int p_id, Key p_accel) {
1110 Item item;
1111 ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel);
1112 item.max_states = p_max_states;
1113 item.state = p_default_state;
1114 items.push_back(item);
1115
1116 _shape_item(items.size() - 1);
1117 control->queue_redraw();
1118
1119 child_controls_changed();
1120 _menu_changed();
1121}
1122
1123#define ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, p_allow_echo) \
1124 ERR_FAIL_COND_MSG(p_shortcut.is_null(), "Cannot add item with invalid Shortcut."); \
1125 _ref_shortcut(p_shortcut); \
1126 item.text = p_shortcut->get_name(); \
1127 item.xl_text = atr(item.text); \
1128 item.id = p_id == -1 ? items.size() : p_id; \
1129 item.shortcut = p_shortcut; \
1130 item.shortcut_is_global = p_global; \
1131 item.allow_echo = p_allow_echo;
1132
1133void PopupMenu::add_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_global, bool p_allow_echo) {
1134 Item item;
1135 ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, p_allow_echo);
1136 items.push_back(item);
1137
1138 _shape_item(items.size() - 1);
1139 control->queue_redraw();
1140
1141 child_controls_changed();
1142 _menu_changed();
1143}
1144
1145void PopupMenu::add_icon_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortcut> &p_shortcut, int p_id, bool p_global, bool p_allow_echo) {
1146 Item item;
1147 ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, p_allow_echo);
1148 item.icon = p_icon;
1149 items.push_back(item);
1150
1151 _shape_item(items.size() - 1);
1152 control->queue_redraw();
1153
1154 child_controls_changed();
1155 _menu_changed();
1156}
1157
1158void PopupMenu::add_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_global) {
1159 Item item;
1160 ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, false); // Echo for check shortcuts doesn't make sense.
1161 item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
1162 items.push_back(item);
1163
1164 _shape_item(items.size() - 1);
1165 control->queue_redraw();
1166
1167 child_controls_changed();
1168 _menu_changed();
1169}
1170
1171void PopupMenu::add_icon_check_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortcut> &p_shortcut, int p_id, bool p_global) {
1172 Item item;
1173 ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, false);
1174 item.icon = p_icon;
1175 item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX;
1176 items.push_back(item);
1177
1178 _shape_item(items.size() - 1);
1179 control->queue_redraw();
1180
1181 child_controls_changed();
1182 _menu_changed();
1183}
1184
1185void PopupMenu::add_radio_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_global) {
1186 Item item;
1187 ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, false);
1188 item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
1189 items.push_back(item);
1190
1191 _shape_item(items.size() - 1);
1192 control->queue_redraw();
1193
1194 child_controls_changed();
1195 _menu_changed();
1196}
1197
1198void PopupMenu::add_icon_radio_check_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortcut> &p_shortcut, int p_id, bool p_global) {
1199 Item item;
1200 ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, false);
1201 item.icon = p_icon;
1202 item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON;
1203 items.push_back(item);
1204
1205 _shape_item(items.size() - 1);
1206 control->queue_redraw();
1207
1208 child_controls_changed();
1209 _menu_changed();
1210}
1211
1212void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu, int p_id) {
1213 Item item;
1214 item.text = p_label;
1215 item.xl_text = atr(p_label);
1216 item.id = p_id == -1 ? items.size() : p_id;
1217 item.submenu = p_submenu;
1218 items.push_back(item);
1219
1220 _shape_item(items.size() - 1);
1221 control->queue_redraw();
1222
1223 child_controls_changed();
1224 _menu_changed();
1225}
1226
1227#undef ITEM_SETUP_WITH_ACCEL
1228#undef ITEM_SETUP_WITH_SHORTCUT
1229
1230/* Methods to modify existing items. */
1231
1232void PopupMenu::set_item_text(int p_idx, const String &p_text) {
1233 if (p_idx < 0) {
1234 p_idx += get_item_count();
1235 }
1236 ERR_FAIL_INDEX(p_idx, items.size());
1237 if (items[p_idx].text == p_text) {
1238 return;
1239 }
1240 items.write[p_idx].text = p_text;
1241 items.write[p_idx].xl_text = atr(p_text);
1242 items.write[p_idx].dirty = true;
1243 _shape_item(p_idx);
1244
1245 control->queue_redraw();
1246 child_controls_changed();
1247 _menu_changed();
1248}
1249
1250void PopupMenu::set_item_text_direction(int p_idx, Control::TextDirection p_text_direction) {
1251 if (p_idx < 0) {
1252 p_idx += get_item_count();
1253 }
1254 ERR_FAIL_INDEX(p_idx, items.size());
1255 ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3);
1256 if (items[p_idx].text_direction != p_text_direction) {
1257 items.write[p_idx].text_direction = p_text_direction;
1258 items.write[p_idx].dirty = true;
1259 control->queue_redraw();
1260 }
1261}
1262
1263void PopupMenu::set_item_language(int p_idx, const String &p_language) {
1264 if (p_idx < 0) {
1265 p_idx += get_item_count();
1266 }
1267 ERR_FAIL_INDEX(p_idx, items.size());
1268 if (items[p_idx].language != p_language) {
1269 items.write[p_idx].language = p_language;
1270 items.write[p_idx].dirty = true;
1271 control->queue_redraw();
1272 }
1273}
1274
1275void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) {
1276 if (p_idx < 0) {
1277 p_idx += get_item_count();
1278 }
1279 ERR_FAIL_INDEX(p_idx, items.size());
1280
1281 if (items[p_idx].icon == p_icon) {
1282 return;
1283 }
1284
1285 items.write[p_idx].icon = p_icon;
1286
1287 control->queue_redraw();
1288 child_controls_changed();
1289 _menu_changed();
1290}
1291
1292void PopupMenu::set_item_icon_max_width(int p_idx, int p_width) {
1293 if (p_idx < 0) {
1294 p_idx += get_item_count();
1295 }
1296 ERR_FAIL_INDEX(p_idx, items.size());
1297
1298 if (items[p_idx].icon_max_width == p_width) {
1299 return;
1300 }
1301
1302 items.write[p_idx].icon_max_width = p_width;
1303
1304 control->queue_redraw();
1305 child_controls_changed();
1306 _menu_changed();
1307}
1308
1309void PopupMenu::set_item_icon_modulate(int p_idx, const Color &p_modulate) {
1310 if (p_idx < 0) {
1311 p_idx += get_item_count();
1312 }
1313 ERR_FAIL_INDEX(p_idx, items.size());
1314
1315 if (items[p_idx].icon_modulate == p_modulate) {
1316 return;
1317 }
1318
1319 items.write[p_idx].icon_modulate = p_modulate;
1320 control->queue_redraw();
1321}
1322
1323void PopupMenu::set_item_checked(int p_idx, bool p_checked) {
1324 if (p_idx < 0) {
1325 p_idx += get_item_count();
1326 }
1327 ERR_FAIL_INDEX(p_idx, items.size());
1328
1329 if (items[p_idx].checked == p_checked) {
1330 return;
1331 }
1332
1333 items.write[p_idx].checked = p_checked;
1334
1335 control->queue_redraw();
1336 child_controls_changed();
1337 _menu_changed();
1338}
1339
1340void PopupMenu::set_item_id(int p_idx, int p_id) {
1341 if (p_idx < 0) {
1342 p_idx += get_item_count();
1343 }
1344 ERR_FAIL_INDEX(p_idx, items.size());
1345
1346 if (items[p_idx].id == p_id) {
1347 return;
1348 }
1349
1350 items.write[p_idx].id = p_id;
1351
1352 control->queue_redraw();
1353 child_controls_changed();
1354 _menu_changed();
1355}
1356
1357void PopupMenu::set_item_accelerator(int p_idx, Key p_accel) {
1358 if (p_idx < 0) {
1359 p_idx += get_item_count();
1360 }
1361 ERR_FAIL_INDEX(p_idx, items.size());
1362
1363 if (items[p_idx].accel == p_accel) {
1364 return;
1365 }
1366
1367 items.write[p_idx].accel = p_accel;
1368 items.write[p_idx].dirty = true;
1369
1370 control->queue_redraw();
1371 child_controls_changed();
1372 _menu_changed();
1373}
1374
1375void PopupMenu::set_item_metadata(int p_idx, const Variant &p_meta) {
1376 if (p_idx < 0) {
1377 p_idx += get_item_count();
1378 }
1379 ERR_FAIL_INDEX(p_idx, items.size());
1380
1381 if (items[p_idx].metadata == p_meta) {
1382 return;
1383 }
1384
1385 items.write[p_idx].metadata = p_meta;
1386 control->queue_redraw();
1387 child_controls_changed();
1388 _menu_changed();
1389}
1390
1391void PopupMenu::set_item_disabled(int p_idx, bool p_disabled) {
1392 if (p_idx < 0) {
1393 p_idx += get_item_count();
1394 }
1395 ERR_FAIL_INDEX(p_idx, items.size());
1396
1397 if (items[p_idx].disabled == p_disabled) {
1398 return;
1399 }
1400
1401 items.write[p_idx].disabled = p_disabled;
1402 control->queue_redraw();
1403 child_controls_changed();
1404 _menu_changed();
1405}
1406
1407void PopupMenu::set_item_submenu(int p_idx, const String &p_submenu) {
1408 if (p_idx < 0) {
1409 p_idx += get_item_count();
1410 }
1411 ERR_FAIL_INDEX(p_idx, items.size());
1412
1413 if (items[p_idx].submenu == p_submenu) {
1414 return;
1415 }
1416
1417 items.write[p_idx].submenu = p_submenu;
1418 control->queue_redraw();
1419 child_controls_changed();
1420 _menu_changed();
1421}
1422
1423void PopupMenu::toggle_item_checked(int p_idx) {
1424 ERR_FAIL_INDEX(p_idx, items.size());
1425 items.write[p_idx].checked = !items[p_idx].checked;
1426 control->queue_redraw();
1427 child_controls_changed();
1428 _menu_changed();
1429}
1430
1431String PopupMenu::get_item_text(int p_idx) const {
1432 ERR_FAIL_INDEX_V(p_idx, items.size(), "");
1433 return items[p_idx].text;
1434}
1435
1436String PopupMenu::get_item_xl_text(int p_idx) const {
1437 ERR_FAIL_INDEX_V(p_idx, items.size(), "");
1438 return items[p_idx].xl_text;
1439}
1440
1441Control::TextDirection PopupMenu::get_item_text_direction(int p_idx) const {
1442 ERR_FAIL_INDEX_V(p_idx, items.size(), Control::TEXT_DIRECTION_INHERITED);
1443 return items[p_idx].text_direction;
1444}
1445
1446String PopupMenu::get_item_language(int p_idx) const {
1447 ERR_FAIL_INDEX_V(p_idx, items.size(), "");
1448 return items[p_idx].language;
1449}
1450
1451int PopupMenu::get_item_idx_from_text(const String &text) const {
1452 for (int idx = 0; idx < items.size(); idx++) {
1453 if (items[idx].text == text) {
1454 return idx;
1455 }
1456 }
1457
1458 return -1;
1459}
1460
1461Ref<Texture2D> PopupMenu::get_item_icon(int p_idx) const {
1462 ERR_FAIL_INDEX_V(p_idx, items.size(), Ref<Texture2D>());
1463 return items[p_idx].icon;
1464}
1465
1466int PopupMenu::get_item_icon_max_width(int p_idx) const {
1467 ERR_FAIL_INDEX_V(p_idx, items.size(), 0);
1468 return items[p_idx].icon_max_width;
1469}
1470
1471Color PopupMenu::get_item_icon_modulate(int p_idx) const {
1472 ERR_FAIL_INDEX_V(p_idx, items.size(), Color());
1473 return items[p_idx].icon_modulate;
1474}
1475
1476Key PopupMenu::get_item_accelerator(int p_idx) const {
1477 ERR_FAIL_INDEX_V(p_idx, items.size(), Key::NONE);
1478 return items[p_idx].accel;
1479}
1480
1481Variant PopupMenu::get_item_metadata(int p_idx) const {
1482 ERR_FAIL_INDEX_V(p_idx, items.size(), Variant());
1483 return items[p_idx].metadata;
1484}
1485
1486bool PopupMenu::is_item_disabled(int p_idx) const {
1487 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1488 return items[p_idx].disabled;
1489}
1490
1491bool PopupMenu::is_item_checked(int p_idx) const {
1492 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1493 return items[p_idx].checked;
1494}
1495
1496int PopupMenu::get_item_id(int p_idx) const {
1497 ERR_FAIL_INDEX_V(p_idx, items.size(), 0);
1498 return items[p_idx].id;
1499}
1500
1501int PopupMenu::get_item_index(int p_id) const {
1502 for (int i = 0; i < items.size(); i++) {
1503 if (items[i].id == p_id) {
1504 return i;
1505 }
1506 }
1507
1508 return -1;
1509}
1510
1511String PopupMenu::get_item_submenu(int p_idx) const {
1512 ERR_FAIL_INDEX_V(p_idx, items.size(), "");
1513 return items[p_idx].submenu;
1514}
1515
1516String PopupMenu::get_item_tooltip(int p_idx) const {
1517 ERR_FAIL_INDEX_V(p_idx, items.size(), "");
1518 return items[p_idx].tooltip;
1519}
1520
1521Ref<Shortcut> PopupMenu::get_item_shortcut(int p_idx) const {
1522 ERR_FAIL_INDEX_V(p_idx, items.size(), Ref<Shortcut>());
1523 return items[p_idx].shortcut;
1524}
1525
1526int PopupMenu::get_item_indent(int p_idx) const {
1527 ERR_FAIL_INDEX_V(p_idx, items.size(), 0);
1528 return items[p_idx].indent;
1529}
1530
1531int PopupMenu::get_item_max_states(int p_idx) const {
1532 ERR_FAIL_INDEX_V(p_idx, items.size(), -1);
1533 return items[p_idx].max_states;
1534}
1535
1536int PopupMenu::get_item_state(int p_idx) const {
1537 ERR_FAIL_INDEX_V(p_idx, items.size(), -1);
1538 return items[p_idx].state;
1539}
1540
1541void PopupMenu::set_item_as_separator(int p_idx, bool p_separator) {
1542 if (p_idx < 0) {
1543 p_idx += get_item_count();
1544 }
1545 ERR_FAIL_INDEX(p_idx, items.size());
1546
1547 if (items[p_idx].separator == p_separator) {
1548 return;
1549 }
1550
1551 items.write[p_idx].separator = p_separator;
1552 control->queue_redraw();
1553}
1554
1555bool PopupMenu::is_item_separator(int p_idx) const {
1556 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1557 return items[p_idx].separator;
1558}
1559
1560void PopupMenu::set_item_as_checkable(int p_idx, bool p_checkable) {
1561 if (p_idx < 0) {
1562 p_idx += get_item_count();
1563 }
1564 ERR_FAIL_INDEX(p_idx, items.size());
1565
1566 int type = (int)(p_checkable ? Item::CHECKABLE_TYPE_CHECK_BOX : Item::CHECKABLE_TYPE_NONE);
1567 if (type == items[p_idx].checkable_type) {
1568 return;
1569 }
1570
1571 items.write[p_idx].checkable_type = p_checkable ? Item::CHECKABLE_TYPE_CHECK_BOX : Item::CHECKABLE_TYPE_NONE;
1572 control->queue_redraw();
1573 _menu_changed();
1574}
1575
1576void PopupMenu::set_item_as_radio_checkable(int p_idx, bool p_radio_checkable) {
1577 if (p_idx < 0) {
1578 p_idx += get_item_count();
1579 }
1580 ERR_FAIL_INDEX(p_idx, items.size());
1581
1582 int type = (int)(p_radio_checkable ? Item::CHECKABLE_TYPE_RADIO_BUTTON : Item::CHECKABLE_TYPE_NONE);
1583 if (type == items[p_idx].checkable_type) {
1584 return;
1585 }
1586
1587 items.write[p_idx].checkable_type = p_radio_checkable ? Item::CHECKABLE_TYPE_RADIO_BUTTON : Item::CHECKABLE_TYPE_NONE;
1588 control->queue_redraw();
1589 _menu_changed();
1590}
1591
1592void PopupMenu::set_item_tooltip(int p_idx, const String &p_tooltip) {
1593 if (p_idx < 0) {
1594 p_idx += get_item_count();
1595 }
1596 ERR_FAIL_INDEX(p_idx, items.size());
1597
1598 if (items[p_idx].tooltip == p_tooltip) {
1599 return;
1600 }
1601
1602 items.write[p_idx].tooltip = p_tooltip;
1603 control->queue_redraw();
1604 _menu_changed();
1605}
1606
1607void PopupMenu::set_item_shortcut(int p_idx, const Ref<Shortcut> &p_shortcut, bool p_global) {
1608 if (p_idx < 0) {
1609 p_idx += get_item_count();
1610 }
1611 ERR_FAIL_INDEX(p_idx, items.size());
1612
1613 if (items[p_idx].shortcut == p_shortcut && items[p_idx].shortcut_is_global == p_global && items[p_idx].shortcut.is_valid() == p_shortcut.is_valid()) {
1614 return;
1615 }
1616
1617 if (items[p_idx].shortcut.is_valid()) {
1618 _unref_shortcut(items[p_idx].shortcut);
1619 }
1620 items.write[p_idx].shortcut = p_shortcut;
1621 items.write[p_idx].shortcut_is_global = p_global;
1622 items.write[p_idx].dirty = true;
1623
1624 if (items[p_idx].shortcut.is_valid()) {
1625 _ref_shortcut(items[p_idx].shortcut);
1626 }
1627
1628 control->queue_redraw();
1629 _menu_changed();
1630}
1631
1632void PopupMenu::set_item_indent(int p_idx, int p_indent) {
1633 if (p_idx < 0) {
1634 p_idx += get_item_count();
1635 }
1636 ERR_FAIL_INDEX(p_idx, items.size());
1637
1638 if (items.write[p_idx].indent == p_indent) {
1639 return;
1640 }
1641 items.write[p_idx].indent = p_indent;
1642
1643 control->queue_redraw();
1644 child_controls_changed();
1645 _menu_changed();
1646}
1647
1648void PopupMenu::set_item_multistate(int p_idx, int p_state) {
1649 if (p_idx < 0) {
1650 p_idx += get_item_count();
1651 }
1652 ERR_FAIL_INDEX(p_idx, items.size());
1653
1654 if (items[p_idx].state == p_state) {
1655 return;
1656 }
1657
1658 items.write[p_idx].state = p_state;
1659 control->queue_redraw();
1660 _menu_changed();
1661}
1662
1663void PopupMenu::set_item_shortcut_disabled(int p_idx, bool p_disabled) {
1664 if (p_idx < 0) {
1665 p_idx += get_item_count();
1666 }
1667 ERR_FAIL_INDEX(p_idx, items.size());
1668
1669 if (items[p_idx].shortcut_is_disabled == p_disabled) {
1670 return;
1671 }
1672
1673 items.write[p_idx].shortcut_is_disabled = p_disabled;
1674 control->queue_redraw();
1675 _menu_changed();
1676}
1677
1678void PopupMenu::toggle_item_multistate(int p_idx) {
1679 ERR_FAIL_INDEX(p_idx, items.size());
1680 if (0 >= items[p_idx].max_states) {
1681 return;
1682 }
1683
1684 ++items.write[p_idx].state;
1685 if (items.write[p_idx].max_states <= items[p_idx].state) {
1686 items.write[p_idx].state = 0;
1687 }
1688
1689 control->queue_redraw();
1690 _menu_changed();
1691}
1692
1693bool PopupMenu::is_item_checkable(int p_idx) const {
1694 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1695 return items[p_idx].checkable_type;
1696}
1697
1698bool PopupMenu::is_item_radio_checkable(int p_idx) const {
1699 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1700 return items[p_idx].checkable_type == Item::CHECKABLE_TYPE_RADIO_BUTTON;
1701}
1702
1703bool PopupMenu::is_item_shortcut_global(int p_idx) const {
1704 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1705 return items[p_idx].shortcut_is_global;
1706}
1707
1708bool PopupMenu::is_item_shortcut_disabled(int p_idx) const {
1709 ERR_FAIL_INDEX_V(p_idx, items.size(), false);
1710 return items[p_idx].shortcut_is_disabled;
1711}
1712
1713void PopupMenu::set_focused_item(int p_idx) {
1714 if (p_idx != -1) {
1715 ERR_FAIL_INDEX(p_idx, items.size());
1716 }
1717
1718 if (mouse_over == p_idx) {
1719 return;
1720 }
1721
1722 mouse_over = p_idx;
1723 if (mouse_over != -1) {
1724 scroll_to_item(mouse_over);
1725 }
1726
1727 control->queue_redraw();
1728}
1729
1730int PopupMenu::get_focused_item() const {
1731 return mouse_over;
1732}
1733
1734void PopupMenu::set_item_count(int p_count) {
1735 ERR_FAIL_COND(p_count < 0);
1736 int prev_size = items.size();
1737
1738 if (prev_size == p_count) {
1739 return;
1740 }
1741
1742 items.resize(p_count);
1743
1744 if (prev_size < p_count) {
1745 for (int i = prev_size; i < p_count; i++) {
1746 items.write[i].id = i;
1747 }
1748 }
1749
1750 control->queue_redraw();
1751 child_controls_changed();
1752 notify_property_list_changed();
1753 _menu_changed();
1754}
1755
1756int PopupMenu::get_item_count() const {
1757 return items.size();
1758}
1759
1760void PopupMenu::scroll_to_item(int p_idx) {
1761 ERR_FAIL_INDEX(p_idx, items.size());
1762
1763 // Scroll item into view (upwards).
1764 if (items[p_idx]._ofs_cache - scroll_container->get_v_scroll() < -control->get_position().y) {
1765 scroll_container->set_v_scroll(items[p_idx]._ofs_cache + control->get_position().y);
1766 }
1767
1768 // Scroll item into view (downwards).
1769 if (items[p_idx]._ofs_cache + items[p_idx]._height_cache - scroll_container->get_v_scroll() > -control->get_position().y + scroll_container->get_size().height) {
1770 scroll_container->set_v_scroll(items[p_idx]._ofs_cache + items[p_idx]._height_cache + control->get_position().y);
1771 }
1772}
1773
1774bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only) {
1775 Key code = Key::NONE;
1776 Ref<InputEventKey> k = p_event;
1777
1778 if (k.is_valid()) {
1779 code = k->get_keycode();
1780 if (code == Key::NONE) {
1781 code = (Key)k->get_unicode();
1782 }
1783 if (k->is_ctrl_pressed()) {
1784 code |= KeyModifierMask::CTRL;
1785 }
1786 if (k->is_alt_pressed()) {
1787 code |= KeyModifierMask::ALT;
1788 }
1789 if (k->is_meta_pressed()) {
1790 code |= KeyModifierMask::META;
1791 }
1792 if (k->is_shift_pressed()) {
1793 code |= KeyModifierMask::SHIFT;
1794 }
1795 }
1796
1797 for (int i = 0; i < items.size(); i++) {
1798 if (is_item_disabled(i) || items[i].shortcut_is_disabled || (!items[i].allow_echo && p_event->is_echo())) {
1799 continue;
1800 }
1801
1802 if (items[i].shortcut.is_valid() && items[i].shortcut->matches_event(p_event) && (items[i].shortcut_is_global || !p_for_global_only)) {
1803 activate_item(i);
1804 return true;
1805 }
1806
1807 if (code != Key::NONE && items[i].accel == code) {
1808 activate_item(i);
1809 return true;
1810 }
1811
1812 if (!items[i].submenu.is_empty()) {
1813 Node *n = get_node(items[i].submenu);
1814 if (!n) {
1815 continue;
1816 }
1817
1818 PopupMenu *pm = Object::cast_to<PopupMenu>(n);
1819 if (!pm) {
1820 continue;
1821 }
1822
1823 if (pm->activate_item_by_event(p_event, p_for_global_only)) {
1824 return true;
1825 }
1826 }
1827 }
1828 return false;
1829}
1830
1831void PopupMenu::activate_item(int p_idx) {
1832 ERR_FAIL_INDEX(p_idx, items.size());
1833 ERR_FAIL_COND(items[p_idx].separator);
1834 int id = items[p_idx].id >= 0 ? items[p_idx].id : p_idx;
1835
1836 //hide all parent PopupMenus
1837 Node *next = get_parent();
1838 PopupMenu *pop = Object::cast_to<PopupMenu>(next);
1839 while (pop) {
1840 // We close all parents that are chained together,
1841 // with hide_on_item_selection enabled
1842
1843 if (items[p_idx].checkable_type) {
1844 if (!hide_on_checkable_item_selection || !pop->is_hide_on_checkable_item_selection()) {
1845 break;
1846 }
1847 } else if (0 < items[p_idx].max_states) {
1848 if (!hide_on_multistate_item_selection || !pop->is_hide_on_multistate_item_selection()) {
1849 break;
1850 }
1851 } else if (!hide_on_item_selection || !pop->is_hide_on_item_selection()) {
1852 break;
1853 }
1854
1855 pop->hide();
1856 next = next->get_parent();
1857 pop = Object::cast_to<PopupMenu>(next);
1858 }
1859
1860 // Hides popup by default; unless otherwise specified
1861 // by using set_hide_on_item_selection and set_hide_on_checkable_item_selection
1862
1863 bool need_hide = true;
1864
1865 if (items[p_idx].checkable_type) {
1866 if (!hide_on_checkable_item_selection) {
1867 need_hide = false;
1868 }
1869 } else if (0 < items[p_idx].max_states) {
1870 if (!hide_on_multistate_item_selection) {
1871 need_hide = false;
1872 }
1873 } else if (!hide_on_item_selection) {
1874 need_hide = false;
1875 }
1876
1877 if (need_hide) {
1878 hide();
1879 }
1880
1881 emit_signal(SNAME("id_pressed"), id);
1882 emit_signal(SNAME("index_pressed"), p_idx);
1883}
1884
1885void PopupMenu::remove_item(int p_idx) {
1886 ERR_FAIL_INDEX(p_idx, items.size());
1887
1888 if (items[p_idx].shortcut.is_valid()) {
1889 _unref_shortcut(items[p_idx].shortcut);
1890 }
1891
1892 items.remove_at(p_idx);
1893 control->queue_redraw();
1894 child_controls_changed();
1895 _menu_changed();
1896}
1897
1898void PopupMenu::add_separator(const String &p_text, int p_id) {
1899 Item sep;
1900 sep.separator = true;
1901 sep.id = p_id;
1902 if (!p_text.is_empty()) {
1903 sep.text = p_text;
1904 sep.xl_text = atr(p_text);
1905 }
1906 items.push_back(sep);
1907 control->queue_redraw();
1908 _menu_changed();
1909}
1910
1911void PopupMenu::clear() {
1912 for (int i = 0; i < items.size(); i++) {
1913 if (items[i].shortcut.is_valid()) {
1914 _unref_shortcut(items[i].shortcut);
1915 }
1916 }
1917 items.clear();
1918 mouse_over = -1;
1919 control->queue_redraw();
1920 child_controls_changed();
1921 notify_property_list_changed();
1922 _menu_changed();
1923}
1924
1925void PopupMenu::_ref_shortcut(Ref<Shortcut> p_sc) {
1926 if (!shortcut_refcount.has(p_sc)) {
1927 shortcut_refcount[p_sc] = 1;
1928 p_sc->connect_changed(callable_mp(this, &PopupMenu::_shortcut_changed));
1929 } else {
1930 shortcut_refcount[p_sc] += 1;
1931 }
1932}
1933
1934void PopupMenu::_unref_shortcut(Ref<Shortcut> p_sc) {
1935 ERR_FAIL_COND(!shortcut_refcount.has(p_sc));
1936 shortcut_refcount[p_sc]--;
1937 if (shortcut_refcount[p_sc] == 0) {
1938 p_sc->disconnect_changed(callable_mp(this, &PopupMenu::_shortcut_changed));
1939 shortcut_refcount.erase(p_sc);
1940 }
1941}
1942
1943void PopupMenu::_shortcut_changed() {
1944 for (int i = 0; i < items.size(); i++) {
1945 items.write[i].dirty = true;
1946 }
1947 control->queue_redraw();
1948}
1949
1950// Hide on item selection determines whether or not the popup will close after item selection
1951void PopupMenu::set_hide_on_item_selection(bool p_enabled) {
1952 hide_on_item_selection = p_enabled;
1953}
1954
1955bool PopupMenu::is_hide_on_item_selection() const {
1956 return hide_on_item_selection;
1957}
1958
1959void PopupMenu::set_hide_on_checkable_item_selection(bool p_enabled) {
1960 hide_on_checkable_item_selection = p_enabled;
1961}
1962
1963bool PopupMenu::is_hide_on_checkable_item_selection() const {
1964 return hide_on_checkable_item_selection;
1965}
1966
1967void PopupMenu::set_hide_on_multistate_item_selection(bool p_enabled) {
1968 hide_on_multistate_item_selection = p_enabled;
1969}
1970
1971bool PopupMenu::is_hide_on_multistate_item_selection() const {
1972 return hide_on_multistate_item_selection;
1973}
1974
1975void PopupMenu::set_submenu_popup_delay(float p_time) {
1976 if (p_time <= 0) {
1977 p_time = 0.01;
1978 }
1979
1980 submenu_timer->set_wait_time(p_time);
1981}
1982
1983float PopupMenu::get_submenu_popup_delay() const {
1984 return submenu_timer->get_wait_time();
1985}
1986
1987void PopupMenu::set_allow_search(bool p_allow) {
1988 allow_search = p_allow;
1989}
1990
1991bool PopupMenu::get_allow_search() const {
1992 return allow_search;
1993}
1994
1995String PopupMenu::get_tooltip(const Point2 &p_pos) const {
1996 int over = _get_mouse_over(p_pos);
1997 if (over < 0 || over >= items.size()) {
1998 return "";
1999 }
2000 return items[over].tooltip;
2001}
2002
2003void PopupMenu::add_autohide_area(const Rect2 &p_area) {
2004 autohide_areas.push_back(p_area);
2005}
2006
2007void PopupMenu::clear_autohide_areas() {
2008 autohide_areas.clear();
2009}
2010
2011void PopupMenu::take_mouse_focus() {
2012 ERR_FAIL_COND(!is_inside_tree());
2013
2014 if (get_parent()) {
2015 get_parent()->get_viewport()->pass_mouse_focus_to(this, control);
2016 }
2017}
2018
2019bool PopupMenu::_set(const StringName &p_name, const Variant &p_value) {
2020 Vector<String> components = String(p_name).split("/", true, 2);
2021 if (components.size() >= 2 && components[0].begins_with("item_") && components[0].trim_prefix("item_").is_valid_int()) {
2022 int item_index = components[0].trim_prefix("item_").to_int();
2023 String property = components[1];
2024 if (property == "text") {
2025 set_item_text(item_index, p_value);
2026 return true;
2027 } else if (property == "icon") {
2028 set_item_icon(item_index, p_value);
2029 return true;
2030 } else if (property == "checkable") {
2031 bool radio_checkable = (int)p_value == Item::CHECKABLE_TYPE_RADIO_BUTTON;
2032 if (radio_checkable) {
2033 set_item_as_radio_checkable(item_index, true);
2034 } else {
2035 bool checkable = p_value;
2036 set_item_as_checkable(item_index, checkable);
2037 }
2038 return true;
2039 } else if (property == "checked") {
2040 set_item_checked(item_index, p_value);
2041 return true;
2042 } else if (property == "id") {
2043 set_item_id(item_index, p_value);
2044 return true;
2045 } else if (property == "disabled") {
2046 set_item_disabled(item_index, p_value);
2047 return true;
2048 } else if (property == "separator") {
2049 set_item_as_separator(item_index, p_value);
2050 return true;
2051 }
2052 }
2053#ifndef DISABLE_DEPRECATED
2054 // Compatibility.
2055 if (p_name == "items") {
2056 Array arr = p_value;
2057 ERR_FAIL_COND_V(arr.size() % 10, false);
2058 clear();
2059
2060 for (int i = 0; i < arr.size(); i += 10) {
2061 String text = arr[i + 0];
2062 Ref<Texture2D> icon = arr[i + 1];
2063 // For compatibility, use false/true for no/checkbox and integers for other values
2064 bool checkable = arr[i + 2];
2065 bool radio_checkable = (int)arr[i + 2] == Item::CHECKABLE_TYPE_RADIO_BUTTON;
2066 bool checked = arr[i + 3];
2067 bool disabled = arr[i + 4];
2068
2069 int id = arr[i + 5];
2070 int accel = arr[i + 6];
2071 Variant meta = arr[i + 7];
2072 String subm = arr[i + 8];
2073 bool sep = arr[i + 9];
2074
2075 int idx = get_item_count();
2076 add_item(text, id);
2077 set_item_icon(idx, icon);
2078 if (checkable) {
2079 if (radio_checkable) {
2080 set_item_as_radio_checkable(idx, true);
2081 } else {
2082 set_item_as_checkable(idx, true);
2083 }
2084 }
2085 set_item_checked(idx, checked);
2086 set_item_disabled(idx, disabled);
2087 set_item_id(idx, id);
2088 set_item_metadata(idx, meta);
2089 set_item_as_separator(idx, sep);
2090 set_item_accelerator(idx, (Key)accel);
2091 set_item_submenu(idx, subm);
2092 }
2093 }
2094#endif
2095 return false;
2096}
2097
2098bool PopupMenu::_get(const StringName &p_name, Variant &r_ret) const {
2099 Vector<String> components = String(p_name).split("/", true, 2);
2100 if (components.size() >= 2 && components[0].begins_with("item_") && components[0].trim_prefix("item_").is_valid_int()) {
2101 int item_index = components[0].trim_prefix("item_").to_int();
2102 String property = components[1];
2103 if (property == "text") {
2104 r_ret = get_item_text(item_index);
2105 return true;
2106 } else if (property == "icon") {
2107 r_ret = get_item_icon(item_index);
2108 return true;
2109 } else if (property == "checkable") {
2110 if (item_index >= 0 && item_index < items.size()) {
2111 r_ret = items[item_index].checkable_type;
2112 return true;
2113 } else {
2114 r_ret = Item::CHECKABLE_TYPE_NONE;
2115 ERR_FAIL_V(true);
2116 }
2117 } else if (property == "checked") {
2118 r_ret = is_item_checked(item_index);
2119 return true;
2120 } else if (property == "id") {
2121 r_ret = get_item_id(item_index);
2122 return true;
2123 } else if (property == "disabled") {
2124 r_ret = is_item_disabled(item_index);
2125 return true;
2126 } else if (property == "separator") {
2127 r_ret = is_item_separator(item_index);
2128 return true;
2129 }
2130 }
2131 return false;
2132}
2133
2134void PopupMenu::_get_property_list(List<PropertyInfo> *p_list) const {
2135 for (int i = 0; i < items.size(); i++) {
2136 p_list->push_back(PropertyInfo(Variant::STRING, vformat("item_%d/text", i)));
2137
2138 PropertyInfo pi = PropertyInfo(Variant::OBJECT, vformat("item_%d/icon", i), PROPERTY_HINT_RESOURCE_TYPE, "Texture2D");
2139 pi.usage &= ~(get_item_icon(i).is_null() ? PROPERTY_USAGE_STORAGE : 0);
2140 p_list->push_back(pi);
2141
2142 pi = PropertyInfo(Variant::INT, vformat("item_%d/checkable", i), PROPERTY_HINT_ENUM, "No,As checkbox,As radio button");
2143 pi.usage &= ~(!is_item_checkable(i) ? PROPERTY_USAGE_STORAGE : 0);
2144 p_list->push_back(pi);
2145
2146 pi = PropertyInfo(Variant::BOOL, vformat("item_%d/checked", i));
2147 pi.usage &= ~(!is_item_checked(i) ? PROPERTY_USAGE_STORAGE : 0);
2148 p_list->push_back(pi);
2149
2150 pi = PropertyInfo(Variant::INT, vformat("item_%d/id", i), PROPERTY_HINT_RANGE, "0,10,1,or_greater");
2151 p_list->push_back(pi);
2152
2153 pi = PropertyInfo(Variant::BOOL, vformat("item_%d/disabled", i));
2154 pi.usage &= ~(!is_item_disabled(i) ? PROPERTY_USAGE_STORAGE : 0);
2155 p_list->push_back(pi);
2156
2157 pi = PropertyInfo(Variant::BOOL, vformat("item_%d/separator", i));
2158 pi.usage &= ~(!is_item_separator(i) ? PROPERTY_USAGE_STORAGE : 0);
2159 p_list->push_back(pi);
2160 }
2161}
2162
2163void PopupMenu::_bind_methods() {
2164 ClassDB::bind_method(D_METHOD("activate_item_by_event", "event", "for_global_only"), &PopupMenu::activate_item_by_event, DEFVAL(false));
2165
2166 ClassDB::bind_method(D_METHOD("add_item", "label", "id", "accel"), &PopupMenu::add_item, DEFVAL(-1), DEFVAL(0));
2167 ClassDB::bind_method(D_METHOD("add_icon_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_item, DEFVAL(-1), DEFVAL(0));
2168 ClassDB::bind_method(D_METHOD("add_check_item", "label", "id", "accel"), &PopupMenu::add_check_item, DEFVAL(-1), DEFVAL(0));
2169 ClassDB::bind_method(D_METHOD("add_icon_check_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_check_item, DEFVAL(-1), DEFVAL(0));
2170 ClassDB::bind_method(D_METHOD("add_radio_check_item", "label", "id", "accel"), &PopupMenu::add_radio_check_item, DEFVAL(-1), DEFVAL(0));
2171 ClassDB::bind_method(D_METHOD("add_icon_radio_check_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_radio_check_item, DEFVAL(-1), DEFVAL(0));
2172
2173 ClassDB::bind_method(D_METHOD("add_multistate_item", "label", "max_states", "default_state", "id", "accel"), &PopupMenu::add_multistate_item, DEFVAL(0), DEFVAL(-1), DEFVAL(0));
2174
2175 ClassDB::bind_method(D_METHOD("add_shortcut", "shortcut", "id", "global", "allow_echo"), &PopupMenu::add_shortcut, DEFVAL(-1), DEFVAL(false), DEFVAL(false));
2176 ClassDB::bind_method(D_METHOD("add_icon_shortcut", "texture", "shortcut", "id", "global", "allow_echo"), &PopupMenu::add_icon_shortcut, DEFVAL(-1), DEFVAL(false), DEFVAL(false));
2177 ClassDB::bind_method(D_METHOD("add_check_shortcut", "shortcut", "id", "global"), &PopupMenu::add_check_shortcut, DEFVAL(-1), DEFVAL(false));
2178 ClassDB::bind_method(D_METHOD("add_icon_check_shortcut", "texture", "shortcut", "id", "global"), &PopupMenu::add_icon_check_shortcut, DEFVAL(-1), DEFVAL(false));
2179 ClassDB::bind_method(D_METHOD("add_radio_check_shortcut", "shortcut", "id", "global"), &PopupMenu::add_radio_check_shortcut, DEFVAL(-1), DEFVAL(false));
2180 ClassDB::bind_method(D_METHOD("add_icon_radio_check_shortcut", "texture", "shortcut", "id", "global"), &PopupMenu::add_icon_radio_check_shortcut, DEFVAL(-1), DEFVAL(false));
2181
2182 ClassDB::bind_method(D_METHOD("add_submenu_item", "label", "submenu", "id"), &PopupMenu::add_submenu_item, DEFVAL(-1));
2183
2184 ClassDB::bind_method(D_METHOD("set_item_text", "index", "text"), &PopupMenu::set_item_text);
2185 ClassDB::bind_method(D_METHOD("set_item_text_direction", "index", "direction"), &PopupMenu::set_item_text_direction);
2186 ClassDB::bind_method(D_METHOD("set_item_language", "index", "language"), &PopupMenu::set_item_language);
2187 ClassDB::bind_method(D_METHOD("set_item_icon", "index", "icon"), &PopupMenu::set_item_icon);
2188 ClassDB::bind_method(D_METHOD("set_item_icon_max_width", "index", "width"), &PopupMenu::set_item_icon_max_width);
2189 ClassDB::bind_method(D_METHOD("set_item_icon_modulate", "index", "modulate"), &PopupMenu::set_item_icon_modulate);
2190 ClassDB::bind_method(D_METHOD("set_item_checked", "index", "checked"), &PopupMenu::set_item_checked);
2191 ClassDB::bind_method(D_METHOD("set_item_id", "index", "id"), &PopupMenu::set_item_id);
2192 ClassDB::bind_method(D_METHOD("set_item_accelerator", "index", "accel"), &PopupMenu::set_item_accelerator);
2193 ClassDB::bind_method(D_METHOD("set_item_metadata", "index", "metadata"), &PopupMenu::set_item_metadata);
2194 ClassDB::bind_method(D_METHOD("set_item_disabled", "index", "disabled"), &PopupMenu::set_item_disabled);
2195 ClassDB::bind_method(D_METHOD("set_item_submenu", "index", "submenu"), &PopupMenu::set_item_submenu);
2196 ClassDB::bind_method(D_METHOD("set_item_as_separator", "index", "enable"), &PopupMenu::set_item_as_separator);
2197 ClassDB::bind_method(D_METHOD("set_item_as_checkable", "index", "enable"), &PopupMenu::set_item_as_checkable);
2198 ClassDB::bind_method(D_METHOD("set_item_as_radio_checkable", "index", "enable"), &PopupMenu::set_item_as_radio_checkable);
2199 ClassDB::bind_method(D_METHOD("set_item_tooltip", "index", "tooltip"), &PopupMenu::set_item_tooltip);
2200 ClassDB::bind_method(D_METHOD("set_item_shortcut", "index", "shortcut", "global"), &PopupMenu::set_item_shortcut, DEFVAL(false));
2201 ClassDB::bind_method(D_METHOD("set_item_indent", "index", "indent"), &PopupMenu::set_item_indent);
2202 ClassDB::bind_method(D_METHOD("set_item_multistate", "index", "state"), &PopupMenu::set_item_multistate);
2203 ClassDB::bind_method(D_METHOD("set_item_shortcut_disabled", "index", "disabled"), &PopupMenu::set_item_shortcut_disabled);
2204
2205 ClassDB::bind_method(D_METHOD("toggle_item_checked", "index"), &PopupMenu::toggle_item_checked);
2206 ClassDB::bind_method(D_METHOD("toggle_item_multistate", "index"), &PopupMenu::toggle_item_multistate);
2207
2208 ClassDB::bind_method(D_METHOD("get_item_text", "index"), &PopupMenu::get_item_text);
2209 ClassDB::bind_method(D_METHOD("get_item_text_direction", "index"), &PopupMenu::get_item_text_direction);
2210 ClassDB::bind_method(D_METHOD("get_item_language", "index"), &PopupMenu::get_item_language);
2211 ClassDB::bind_method(D_METHOD("get_item_icon", "index"), &PopupMenu::get_item_icon);
2212 ClassDB::bind_method(D_METHOD("get_item_icon_max_width", "index"), &PopupMenu::get_item_icon_max_width);
2213 ClassDB::bind_method(D_METHOD("get_item_icon_modulate", "index"), &PopupMenu::get_item_icon_modulate);
2214 ClassDB::bind_method(D_METHOD("is_item_checked", "index"), &PopupMenu::is_item_checked);
2215 ClassDB::bind_method(D_METHOD("get_item_id", "index"), &PopupMenu::get_item_id);
2216 ClassDB::bind_method(D_METHOD("get_item_index", "id"), &PopupMenu::get_item_index);
2217 ClassDB::bind_method(D_METHOD("get_item_accelerator", "index"), &PopupMenu::get_item_accelerator);
2218 ClassDB::bind_method(D_METHOD("get_item_metadata", "index"), &PopupMenu::get_item_metadata);
2219 ClassDB::bind_method(D_METHOD("is_item_disabled", "index"), &PopupMenu::is_item_disabled);
2220 ClassDB::bind_method(D_METHOD("get_item_submenu", "index"), &PopupMenu::get_item_submenu);
2221 ClassDB::bind_method(D_METHOD("is_item_separator", "index"), &PopupMenu::is_item_separator);
2222 ClassDB::bind_method(D_METHOD("is_item_checkable", "index"), &PopupMenu::is_item_checkable);
2223 ClassDB::bind_method(D_METHOD("is_item_radio_checkable", "index"), &PopupMenu::is_item_radio_checkable);
2224 ClassDB::bind_method(D_METHOD("is_item_shortcut_disabled", "index"), &PopupMenu::is_item_shortcut_disabled);
2225 ClassDB::bind_method(D_METHOD("get_item_tooltip", "index"), &PopupMenu::get_item_tooltip);
2226 ClassDB::bind_method(D_METHOD("get_item_shortcut", "index"), &PopupMenu::get_item_shortcut);
2227 ClassDB::bind_method(D_METHOD("get_item_indent", "index"), &PopupMenu::get_item_indent);
2228
2229 ClassDB::bind_method(D_METHOD("set_focused_item", "index"), &PopupMenu::set_focused_item);
2230 ClassDB::bind_method(D_METHOD("get_focused_item"), &PopupMenu::get_focused_item);
2231 ClassDB::bind_method(D_METHOD("set_item_count", "count"), &PopupMenu::set_item_count);
2232 ClassDB::bind_method(D_METHOD("get_item_count"), &PopupMenu::get_item_count);
2233
2234 ClassDB::bind_method(D_METHOD("scroll_to_item", "index"), &PopupMenu::scroll_to_item);
2235
2236 ClassDB::bind_method(D_METHOD("remove_item", "index"), &PopupMenu::remove_item);
2237
2238 ClassDB::bind_method(D_METHOD("add_separator", "label", "id"), &PopupMenu::add_separator, DEFVAL(String()), DEFVAL(-1));
2239 ClassDB::bind_method(D_METHOD("clear"), &PopupMenu::clear);
2240
2241 ClassDB::bind_method(D_METHOD("set_hide_on_item_selection", "enable"), &PopupMenu::set_hide_on_item_selection);
2242 ClassDB::bind_method(D_METHOD("is_hide_on_item_selection"), &PopupMenu::is_hide_on_item_selection);
2243
2244 ClassDB::bind_method(D_METHOD("set_hide_on_checkable_item_selection", "enable"), &PopupMenu::set_hide_on_checkable_item_selection);
2245 ClassDB::bind_method(D_METHOD("is_hide_on_checkable_item_selection"), &PopupMenu::is_hide_on_checkable_item_selection);
2246
2247 ClassDB::bind_method(D_METHOD("set_hide_on_state_item_selection", "enable"), &PopupMenu::set_hide_on_multistate_item_selection);
2248 ClassDB::bind_method(D_METHOD("is_hide_on_state_item_selection"), &PopupMenu::is_hide_on_multistate_item_selection);
2249
2250 ClassDB::bind_method(D_METHOD("set_submenu_popup_delay", "seconds"), &PopupMenu::set_submenu_popup_delay);
2251 ClassDB::bind_method(D_METHOD("get_submenu_popup_delay"), &PopupMenu::get_submenu_popup_delay);
2252
2253 ClassDB::bind_method(D_METHOD("set_allow_search", "allow"), &PopupMenu::set_allow_search);
2254 ClassDB::bind_method(D_METHOD("get_allow_search"), &PopupMenu::get_allow_search);
2255
2256 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_on_item_selection"), "set_hide_on_item_selection", "is_hide_on_item_selection");
2257 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_on_checkable_item_selection"), "set_hide_on_checkable_item_selection", "is_hide_on_checkable_item_selection");
2258 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_on_state_item_selection"), "set_hide_on_state_item_selection", "is_hide_on_state_item_selection");
2259 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submenu_popup_delay", PROPERTY_HINT_NONE, "suffix:s"), "set_submenu_popup_delay", "get_submenu_popup_delay");
2260 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_search"), "set_allow_search", "get_allow_search");
2261
2262 ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "item_");
2263
2264 ADD_SIGNAL(MethodInfo("id_pressed", PropertyInfo(Variant::INT, "id")));
2265 ADD_SIGNAL(MethodInfo("id_focused", PropertyInfo(Variant::INT, "id")));
2266 ADD_SIGNAL(MethodInfo("index_pressed", PropertyInfo(Variant::INT, "index")));
2267 ADD_SIGNAL(MethodInfo("menu_changed"));
2268
2269 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, PopupMenu, panel_style, "panel");
2270 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, PopupMenu, hover_style, "hover");
2271
2272 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, PopupMenu, separator_style, "separator");
2273 BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, PopupMenu, labeled_separator_left);
2274 BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, PopupMenu, labeled_separator_right);
2275
2276 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, PopupMenu, v_separation);
2277 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, PopupMenu, h_separation);
2278 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, PopupMenu, indent);
2279 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, PopupMenu, item_start_padding);
2280 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, PopupMenu, item_end_padding);
2281 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, PopupMenu, icon_max_width);
2282
2283 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, checked);
2284 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, checked_disabled);
2285 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, unchecked);
2286 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, unchecked_disabled);
2287 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, radio_checked);
2288 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, radio_checked_disabled);
2289 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, radio_unchecked);
2290 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, radio_unchecked_disabled);
2291
2292 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, submenu);
2293 BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, submenu_mirrored);
2294
2295 BIND_THEME_ITEM(Theme::DATA_TYPE_FONT, PopupMenu, font);
2296 BIND_THEME_ITEM(Theme::DATA_TYPE_FONT_SIZE, PopupMenu, font_size);
2297 BIND_THEME_ITEM(Theme::DATA_TYPE_FONT, PopupMenu, font_separator);
2298 BIND_THEME_ITEM(Theme::DATA_TYPE_FONT_SIZE, PopupMenu, font_separator_size);
2299
2300 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_color);
2301 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_hover_color);
2302 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_disabled_color);
2303 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_accelerator_color);
2304 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_CONSTANT, PopupMenu, font_outline_size, "outline_size");
2305 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_outline_color);
2306
2307 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_separator_color);
2308 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_CONSTANT, PopupMenu, font_separator_outline_size, "separator_outline_size");
2309 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, PopupMenu, font_separator_outline_color);
2310}
2311
2312void PopupMenu::popup(const Rect2i &p_bounds) {
2313 moved = Vector2();
2314 popup_time_msec = OS::get_singleton()->get_ticks_msec();
2315 Popup::popup(p_bounds);
2316}
2317
2318PopupMenu::PopupMenu() {
2319 // Margin Container
2320 margin_container = memnew(MarginContainer);
2321 margin_container->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2322 add_child(margin_container, false, INTERNAL_MODE_FRONT);
2323 margin_container->connect("draw", callable_mp(this, &PopupMenu::_draw_background));
2324
2325 // Scroll Container
2326 scroll_container = memnew(ScrollContainer);
2327 scroll_container->set_clip_contents(true);
2328 margin_container->add_child(scroll_container);
2329
2330 // The control which will display the items
2331 control = memnew(Control);
2332 control->set_clip_contents(false);
2333 control->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2334 control->set_h_size_flags(Control::SIZE_EXPAND_FILL);
2335 control->set_v_size_flags(Control::SIZE_EXPAND_FILL);
2336 scroll_container->add_child(control, false, INTERNAL_MODE_FRONT);
2337 control->connect("draw", callable_mp(this, &PopupMenu::_draw_items));
2338
2339 connect("window_input", callable_mp(this, &PopupMenu::gui_input));
2340
2341 submenu_timer = memnew(Timer);
2342 submenu_timer->set_wait_time(0.3);
2343 submenu_timer->set_one_shot(true);
2344 submenu_timer->connect("timeout", callable_mp(this, &PopupMenu::_submenu_timeout));
2345 add_child(submenu_timer, false, INTERNAL_MODE_FRONT);
2346
2347 minimum_lifetime_timer = memnew(Timer);
2348 minimum_lifetime_timer->set_wait_time(0.3);
2349 minimum_lifetime_timer->set_one_shot(true);
2350 minimum_lifetime_timer->connect("timeout", callable_mp(this, &PopupMenu::_minimum_lifetime_timeout));
2351 add_child(minimum_lifetime_timer, false, INTERNAL_MODE_FRONT);
2352}
2353
2354PopupMenu::~PopupMenu() {
2355}
2356