6 Popups Menus and Dialogs
dxtr edited this page 2026-10-20 01:43:24 +02:00

Popups, Menus and Dialogs

A popup is a window of its own for menus, lists to choose from and tooltips: its controls draw into it while it is shown, laid out in its size. show-popup shows it against a rectangle of the viewport it is in, in that viewport's coordinates, such as a button's from control-viewport-rectangle, which goes through the button's canvas, the viewport's zoom and pan or its canvas layer's, to where the button is drawn: below it by default, moved to the other side or along the screen's edge where it would not fit, as large as its controls need unless given :width and :height. A popup-panel draws a panel behind them. The examples here add to the settings form of Widgets, each part a node of the form, found by its path, and each function connected to a signal given the form. Tell the settings were saved:

(sc:call-in-tree *tree*
                 (lambda ()
                   (let ((save (sc:find-node (form *tree*) "box/save"))
                         (saved (make-instance 'ui:popup-panel :name "saved" :grab nil)))
                     (sc:add-child save saved)
                     (sc:add-child saved (make-instance 'ui:label :text "Saved"))
                     (ui:show-popup saved (ui:control-viewport-rectangle save)))))

hide-popup hides it again, and it emits popup-shown and popup-hidden. A popup that grabs, as :grab t by default makes a menu's, takes the keyboard while it is shown, and hides on Escape, when the user clicks outside it, and when the window system dismisses it, as on a click outside the program; a press outside it then does nothing else. Its controls take its window's input, with a focus of their own.

A popup is a window of the window system's, or it is embedded, drawn inside the window it is shown in, over everything else, as Godot's embedded subwindows are: where the viewport it is shown on has viewport-embed-subwindows, as a project's game does by default, so that in fullscreen it keeps its menus in its frame; and always in a tree whose engine draws offscreen, that takes no window, or that has no engine, as in tests. Embedded, it is placed by the same rules within the window rather than the screen, takes the window's input first, the pointer's over it and the keys while it grabs, and popup-embedded-p is true.

A control's control-tooltip-text, given as :tooltip-text, shows in a tooltip once the pointer has rested on it for (tooltip-delay) seconds, half a second as in Godot unless set, beside the pointer; it goes when the pointer leaves the control and on a press. It shows under the control, as Godot's does, so it draws with the themes that control finds: tooltip-panel's :panel and tooltip-label's :font-color. A control with parts gives each its own text with a control-tooltip method, which gets where the pointer is.

A popup-menu is a popup of items to choose from, Godot's PopupMenu. Its items are menu-item objects, which make-menu-item and make-menu-separator make: a text; an icon, an image drawn before the text, tinted with its menu-item-icon-modulate; an identifier, its index unless it gives one; whether it is disabled, checkable, :check or :radio, and checked; whether it is a separator; the submenu it opens, the name of a popup menu among the menu's children; a shortcut, an action or a binding that chooses it from the keyboard, whose text it shows; and a tooltip. Items are value objects, saved inside the scene with their control, and each belongs to one control: changing one, as with (setf menu-item-checked), draws its menu again. The pointer or the arrow keys highlight an item, and a click, Return or Space chooses it, which hides the menu and emits id-pressed and index-pressed; so does its shortcut, while the menu shows and, for a menu bar's or a menu button's menu, while it does not. A submenu opens as the pointer rests on its item, or by Right, and Left closes it. A menu-bar opens the popup menus among its children from a row of their titles, its items' shortcuts choosing them only while the keyboard focus is in its menu-bar-shortcut-context, if it has one, as the menus of Godot's script editor choose theirs while the focus is in it; a menu-button the one among its children, its items' shortcuts choosing them only while the focus is in its button-shortcut-context, if it has one, as Godot's controls take shortcuts; and an option-button shows one choice of a list it opens, its items option-item objects, as make-option-item and make-option-separator make them. Add presets and a quality to the form:

(defun choose-preset (form volume)
  (setf (ui:range-value (sc:find-node form "box/volume-row/volume")) volume))

(defun choose-quality (form index)
  (declare (ignore form))
  (format t "~&Quality ~D.~%" index))

(sc:call-in-tree *tree*
                 (lambda ()
                   (let* ((form (form *tree*))
                          (presets (make-instance 'ui:menu-button :name "presets" :text "Presets"))
                          (menu (make-instance
                                 'ui:popup-menu
                                 :name "menu"
                                 :items (list (ui:make-menu-item "Quiet" :id 20)
                                              (ui:make-menu-item "Loud" :id 100)
                                              (ui:make-menu-separator)
                                              (ui:make-menu-item "Default" :id 70
                                                                 :shortcut '(:symbol :d :control)))))
                          (quality (make-instance 'ui:option-button
                                                  :name "quality"
                                                  :items (mapcar #'ui:make-option-item
                                                                 '("Low" "Medium" "High"))
                                                  :selected 1)))
                     (sc:connect menu 'ui:id-pressed form 'choose-preset)
                     (sc:connect quality 'ui:item-selected form 'choose-quality)
                     (sc:add-child presets menu)
                     (add (sc:find-node form "box") presets quality))))

Control+D chooses Default, the menu open or not. Choosing a checkable item does not check it, as in Godot: check it with (setf (ui:menu-item-checked (ui:popup-menu-item menu index)) t). add-menu-item, add-menu-separator and clear-menu change a menu's items, and popup-menu-item gives one. To give a control an item another holds, give it a copy-item of it.

A dialog tells the user something, as Godot's AcceptDialog, and a confirmation-dialog asks them to confirm, with a Cancel button beside OK. show-dialog shows one over the middle of the window it is in, as large as its message, its controls and its buttons need, its OK button taking the keyboard focus. OK confirms it, emitting confirmed; its Cancel button, Escape and closing its window cancel it, emitting canceled; either hides it. add-dialog-button adds a button that emits custom-action with an action, and the controls added to a dialog go above its buttons. Ask before resetting the volume:

(defun ask-to-reset (form)
  (ui:show-dialog (sc:find-node form "box/reset/confirm")))

(defun reset-volume (form)
  (setf (ui:range-value (sc:find-node form "box/volume-row/volume")) 70))

(sc:call-in-tree *tree*
                 (lambda ()
                   (let ((form (form *tree*))
                         (reset (make-instance 'ui:button :name "reset" :text "Reset"))
                         (confirm (make-instance 'ui:confirmation-dialog
                                                 :name "confirm" :title "Reset"
                                                 :text "Reset the volume?")))
                     (sc:connect reset 'ui:pressed form 'ask-to-reset)
                     (sc:connect confirm 'ui:confirmed form 'reset-volume)
                     (add (sc:find-node form "box") reset)
                     (sc:add-child reset confirm))))

A dialog is modal unless made with :modal nil: the window it is shown on takes no input until it hides. Like a popup it is a window of the window system's, a dialog of the window it is shown on, which dialog-window gives; or embedded where popups are, drawn inside that window over everything else with a border and a title bar of its own, by which it is dragged within the window and closed, as Godot's embedded windows, and dialog-embedded-p is true. An embedded dialog that is not modal gets the window's keys while it has the focus, which a press on it gives and a press elsewhere in the window takes back.

A file-dialog, Godot's FileDialog, chooses files and folders: it shows a folder in a line edit, the folder's folders and files in a list, and under them the file's name and filters of what the list shows. Its file-dialog-file-mode says what it chooses, :open-file, :open-files, :open-dir, :open-any, or by default :save-file, and it emits file-selected, files-selected or dir-selected with absolute pathnames. Its theme's icons :folder and :file show before its folders and files, and :parent-folder and :reload on its Up and Refresh buttons, as Godot's: the default theme gives Pyssel's own, SVG files drawn at the window's scale in the dialog's font colour; where a theme's cannot be drawn, the buttons show words and folders a slash after their names. Its file-dialog-filters are file-filter objects, which make-file-filter makes of a description and patterns. Save the settings to a file:

(defun ask-where-to-save (form)
  (ui:show-dialog (sc:find-node form "box/save-as/file")))

(defun save-settings-to (form path)
  (with-open-file (*standard-output* path :direction :output :if-exists :supersede)
    (save-settings form)))

(sc:call-in-tree *tree*
                 (lambda ()
                   (let ((form (form *tree*))
                         (save-as (make-instance 'ui:button :name "save-as" :text "Save as..."))
                         (file (make-instance 'ui:file-dialog
                                              :name "file"
                                              :filters (list (ui:make-file-filter
                                                              "Settings" "*.settings")))))
                     (sc:connect save-as 'ui:pressed form 'ask-where-to-save)
                     (sc:connect file 'ui:file-selected form 'save-settings-to)
                     (add (sc:find-node form "box") save-as)
                     (sc:add-child save-as file))))

A name typed without the filter's extension gets it, .settings, and saving over a file asks first.