mirror of
				https://github.com/slint-ui/slint.git
				synced 2025-10-31 12:04:33 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			179 lines
		
	
	
	
		
			5.8 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			179 lines
		
	
	
	
		
			5.8 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| // Copyright © SixtyFPS GmbH <info@slint.dev>
 | |
| // 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 <int> current-highlight: -1;
 | |
|     // `y` position of the currently highlighted menu entry
 | |
|     property <length> current-highlight-y-pos;
 | |
|     property <int> current-open: -1;
 | |
|     property <length> 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;
 | |
| 
 | |
|                 changed is-current => {
 | |
|                     if self.is-current {
 | |
|                         current-highlight-y-pos = self.absolute-position.y - root.absolute-position.y;
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 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], current-highlight-y-pos, current-highlight);
 | |
|                     } else {
 | |
|                         current-open = -1;
 | |
|                         sub-menu.close();
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         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], current-highlight-y-pos, 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], current-highlight-y-pos, 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 <int> 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); }
 | |
|     }
 | |
| }
 | |
| 
 | 
