mirror of
https://github.com/slint-ui/slint.git
synced 2025-07-23 04:55:28 +00:00
258 lines
7.7 KiB
Text
258 lines
7.7 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 } from "std-widgets-impl.slint";
|
|
|
|
component MenuItemImpl {
|
|
in property <bool> is-current;
|
|
in property <MenuEntry> entry;
|
|
|
|
callback set-current();
|
|
callback clear-current();
|
|
callback activate(entry : MenuEntry, y: length);
|
|
|
|
background-layer := Rectangle {
|
|
background: Palette.background;
|
|
border-radius: 3px;
|
|
border-width: 1px;
|
|
|
|
HorizontalLayout {
|
|
spacing: 7px;
|
|
padding-left: 11px;
|
|
padding-top: 4px;
|
|
padding-bottom: 6px;
|
|
|
|
menu-text := Text {
|
|
text: entry.title;
|
|
horizontal-stretch: 1;
|
|
color: Palette.foreground;
|
|
}
|
|
|
|
if entry.has-sub-menu : Image {
|
|
source: @image-url("../fluent/_arrow_forward.svg");
|
|
colorize: menu-text.color;
|
|
accessible-role: none;
|
|
}
|
|
}
|
|
|
|
TouchArea {
|
|
pointer-event(event) => {
|
|
if event.kind == PointerEventKind.move && !root.is-current {
|
|
root.set-current()
|
|
} else if event.kind == PointerEventKind.down && entry.has-sub-menu {
|
|
activate(entry, self.absolute-position.y);
|
|
} else if event.kind == PointerEventKind.up
|
|
&& self.mouse-y > 0 && self.mouse-y < self.height
|
|
&& self.mouse-x > 0 && self.mouse-x < self.width {
|
|
// can't put this in `clicked` because then the menu would close causing a panic in the pointer-event
|
|
activate(entry, self.absolute-position.y);
|
|
}
|
|
}
|
|
|
|
changed has-hover => {
|
|
if !self.has-hover && root.is-current {
|
|
root.clear-current();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
states [
|
|
is-current when root.is-current: {
|
|
// FIXME: note that the fluent style uses the secondary color for highlighted item
|
|
background-layer.background: Palette.accent-background;
|
|
menu-text.color: Palette.accent-foreground;
|
|
}
|
|
]
|
|
}
|
|
|
|
export component PopupMenuImpl inherits Window {
|
|
in property <[MenuEntry]> entries: [];
|
|
|
|
property <int> current: -1;
|
|
property <length> px: 1rem / 14;
|
|
|
|
callback sub-menu(menu-entry: MenuEntry) -> [MenuEntry];
|
|
callback activated(menu-entry: MenuEntry);
|
|
callback close();
|
|
|
|
forward-focus: focus-scope;
|
|
|
|
Rectangle {
|
|
border-radius: 7px;
|
|
border-color: Palette.border;
|
|
border-width: 1px;
|
|
background: Palette.background;
|
|
drop-shadow-blur: 2px;
|
|
drop-shadow-color: Palette.foreground.transparentize(0.5);
|
|
min-width: 10rem;
|
|
}
|
|
|
|
focus-scope := FocusScope {
|
|
layout := VerticalLayout {
|
|
padding: 5px;
|
|
|
|
for entry[index] in entries: MenuItemImpl {
|
|
entry: entry;
|
|
is-current: current == index;
|
|
|
|
set-current => {
|
|
focus-scope.focus();
|
|
root.current = index;
|
|
open-sub-menu-after-timeout.running = true;
|
|
}
|
|
|
|
clear-current => {
|
|
root.current = -1;
|
|
}
|
|
|
|
activate(entry, y) => {
|
|
root.activate(entry, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
open-sub-menu-after-timeout := Timer {
|
|
interval: 500ms;
|
|
running: false;
|
|
|
|
triggered => {
|
|
self.running = false;
|
|
|
|
if current >= 0 {
|
|
if entries[current].has-sub-menu {
|
|
activate(entries[current], y-pos(current));
|
|
} else {
|
|
sub-menu.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function y-pos(idx: int) -> length {
|
|
layout.padding + idx * (layout.height - 2 * layout.padding) / entries.length
|
|
}
|
|
|
|
key-pressed(event) => {
|
|
open-sub-menu-after-timeout.running = false;
|
|
|
|
if event.text == Key.UpArrow {
|
|
if current >= 1 {
|
|
current -= 1;
|
|
} else {
|
|
current = entries.length - 1;
|
|
}
|
|
return accept;
|
|
} else if event.text == Key.DownArrow {
|
|
if current < entries.length - 1 {
|
|
current += 1;
|
|
} else {
|
|
current = 0;
|
|
}
|
|
return accept;
|
|
} else if event.text == Key.Return {
|
|
if current >= 0 && current < entries.length {
|
|
activate(entries[current], y-pos(current));
|
|
}
|
|
return accept;
|
|
} else if event.text == Key.RightArrow {
|
|
if current >= 0 && current < entries.length && entries[current].has-sub-menu {
|
|
activate(entries[current], y-pos(current));
|
|
}
|
|
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) => {
|
|
root.activated(entry);
|
|
root.close();
|
|
}
|
|
}
|
|
|
|
function activate(entry : MenuEntry, y: length) {
|
|
open-sub-menu-after-timeout.running = false;
|
|
if entry.has-sub-menu {
|
|
sub-menu.entries = root.sub-menu(entry);
|
|
sub-menu.show({
|
|
x: root.width,
|
|
y: y - sub-menu.absolute-position.y,
|
|
});
|
|
} else {
|
|
activated(entry);
|
|
close();
|
|
}
|
|
}
|
|
}
|
|
|
|
export component MenuBarImpl {
|
|
callback activated(MenuEntry);
|
|
callback sub-menu(MenuEntry) -> [MenuEntry];
|
|
|
|
property <[MenuEntry]> entries;
|
|
property <length> px: 1rem / 14;
|
|
|
|
preferred-width: 100%;
|
|
height: layout.preferred-height;
|
|
|
|
layout := HorizontalLayout {
|
|
padding: 5px;
|
|
alignment: start;
|
|
spacing: 1px;
|
|
|
|
for entry in entries: e := Rectangle {
|
|
// FIXME: note that the fluent style uses the secondary color for highlighted item
|
|
background: touch-area.has-hover || touch-area.pressed ? Palette.accent-background : transparent;
|
|
border-radius: 3px;
|
|
|
|
HorizontalLayout {
|
|
padding: 11px;
|
|
padding-top: 4px;
|
|
padding-bottom: 6px;
|
|
|
|
Text {
|
|
text: entry.title;
|
|
color: touch-area.has-hover || touch-area.pressed ? Palette.accent-foreground : Palette.foreground;
|
|
}
|
|
}
|
|
|
|
touch-area := TouchArea {
|
|
clicked => {
|
|
context-menu.entries = root.sub-menu(entry);
|
|
context-menu.show({ x: e.x, y: root.height });
|
|
}
|
|
}
|
|
}
|
|
|
|
// For the default size when there is no entries
|
|
Rectangle {
|
|
HorizontalLayout {
|
|
padding-top: 0.4rem;
|
|
padding-bottom: 0.6rem;
|
|
|
|
Text {
|
|
text: "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
context-menu := ContextMenuInternal {
|
|
activated(entry) => { root.activated(entry); self.close(); }
|
|
sub-menu(entry) => { root.sub-menu(entry); }
|
|
}
|
|
}
|
|
|