// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 //! This file contains a generic implementation of the MenuBar and ContextMenu import { Palette, MenuBarItem, MenuBar, MenuFrame, MenuItem } from "std-widgets-impl.slint"; export component PopupMenuImpl inherits Window { in property <[MenuEntry]> entries: []; property current-highlight: -1; property current-open: -1; property px: 1rem / 14; callback sub-menu(menu-entry: MenuEntry) -> [MenuEntry]; callback activated(menu-entry: MenuEntry); callback close(); forward-focus: focus-scope; background: transparent; focus-scope := FocusScope { frame := MenuFrame { for entry[index] in entries: MenuItem { entry: entry; is-current: current-highlight == index; set-current => { focus-scope.focus(); root.current-highlight = index; open-sub-menu-after-timeout.running = true; } clear-current => { root.current-highlight = -1; } activate(entry, y) => { root.activate(entry, y, index); } } } open-sub-menu-after-timeout := Timer { interval: 500ms; running: false; triggered => { self.running = false; if current-highlight >= 0 { if entries[current-highlight].has-sub-menu { activate(entries[current-highlight], y-pos(current-highlight), current-highlight); } else { current-open = -1; sub-menu.close(); } } } } function y-pos(idx: int) -> length { frame.padding + idx * (frame.height - 2 * frame.padding) / entries.length } key-pressed(event) => { open-sub-menu-after-timeout.running = false; if event.text == Key.UpArrow { if current-highlight < 1 { current-highlight = entries.length - 1; } else if entries[current-highlight - 1].is_separator { current-highlight -= 2; } else { current-highlight -= 1; } return accept; } else if event.text == Key.DownArrow { if current-highlight >= entries.length - 1 { current-highlight = 0; } else if entries[current-highlight + 1].is_separator { current-highlight += 2; } else { current-highlight += 1; } return accept; } else if event.text == Key.Return { if current-highlight >= 0 && current-highlight < entries.length && entries[current-highlight].enabled { activate(entries[current-highlight], y-pos(current-highlight), current-highlight); } return accept; } else if event.text == Key.RightArrow { if current-highlight >= 0 && current-highlight < entries.length && entries[current-highlight].has-sub-menu && entries[current-highlight].enabled { activate(entries[current-highlight], y-pos(current-highlight), current-highlight); } return accept; } else if event.text == Key.LeftArrow { // TODO: should close only if this menu is a sub menu root.close(); } reject } } sub-menu := ContextMenuInternal { x: 0; y: 0; width: 0; height: 0; sub-menu(entry) => { root.sub-menu(entry); } activated(entry) => { current-open = -1; root.activated(entry); root.close(); } } function activate(entry : MenuEntry, y: length, index: int) { open-sub-menu-after-timeout.running = false; if entry.has-sub-menu { if current-open != index || !sub-menu.is-open() { current-open = index; sub-menu.entries = root.sub-menu(entry); sub-menu.show({ x: root.width, y: y - sub-menu.absolute-position.y, }); } } else { current-open = -1; activated(entry); close(); } } } export component MenuBarImpl { callback activated(menu-entry: MenuEntry); callback sub-menu(menu-entry: MenuEntry) -> [MenuEntry]; property <[MenuEntry]> entries; preferred-width: 100%; height: base.min-height; private property last-open-entry-index : -1; base := MenuBar { width: 100%; for entry[idx] in entries: e := MenuBarItem { entry: entry; clicked => { last-open-entry-index = idx; context-menu.entries = root.sub-menu(entry); context-menu.show({ x: e.x, y: root.height }); } hovered => { if last-open-entry-index != idx && context-menu.is-open() { self.clicked(); } } } } context-menu := ContextMenuInternal { // Only manual calls to `show` should open the menu, and we shouldn't react to clicks. enabled: false; activated(entry) => { root.activated(entry); self.close(); } sub-menu(entry) => { root.sub-menu(entry); } } }