1
0
Fork 0

Added Popup, Menu and MenuItem classes

This commit is contained in:
TheFGFSEagle 2023-01-05 14:30:22 +01:00 committed by James Turner
parent 581f8d163f
commit 764e6f0ced
6 changed files with 678 additions and 32 deletions

View file

@ -12,6 +12,7 @@
var gui = {
widgets: {},
focused_window: nil,
open_popups: [],
region_highlight: nil,
# Window/dialog stacking order
@ -28,6 +29,8 @@ var loadWidget = func(name) loadGUIFile("widgets/" ~ name ~ ".nas");
var loadDialog = func(name) loadGUIFile("dialogs/" ~ name ~ ".nas");
loadGUIFile("Config.nas");
loadGUIFile("Menu.nas");
loadGUIFile("Popup.nas");
loadGUIFile("Style.nas");
loadGUIFile("Widget.nas");
loadGUIFile("styles/DefaultStyle.nas");
@ -103,8 +106,10 @@ var Window = {
m.setDouble("aspect-ratio", size[0]/size[1]);
m.setBool("lock-aspect-ratio", 0);
# TODO better default position
m.move(0,0);
var desktopSize = [props.globals.getValue("/sim/gui/canvas/size[0]"), props.globals.getValue("/sim/gui/canvas/size[1]")];
var pos = [desktopSize[0] / 2 - size[0] / 2 + 10, desktopSize[1] / 2 - size[1] / 2 + 30];
m.move(pos[0], pos[1]);
if (destroy_on_close) {
m.setFocus();
} else {
@ -351,6 +356,11 @@ var Window = {
_onStateChange: func
{
var event = canvas.CustomEvent.new("wm.focus-" ~ (me._focused ? "in" : "out"));
if (me._focused) {
foreach(var p; gui.open_popups) {
p.hide();
}
}
if( me._getCanvasDecoration() != nil )
{
@ -576,6 +586,12 @@ var Window = {
getDesktop().addEventListener("mousedown", func {
if( gui.focused_window != nil )
gui.focused_window.clearFocus();
if (size(gui.open_popups)) {
foreach (var p; gui.open_popups) {
p.hide();
}
}
});
# Provide old 'Dialog' for backwards compatiblity (should be removed for 3.0)

268
Nasal/canvas/gui/Menu.nas Normal file
View file

@ -0,0 +1,268 @@
# SPDX-FileCopyrightText: (C) 2022 Frederic Croix <thefgfseagle@gmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Usage example for the menu, with submenu:
#
# var m = canvas.gui.Menu.new();
# var i1 = m.createItem("Text");
# var i2 = m.createItem("Text 2");
# var sub = canvas.gui.Menu.new();
# var s1 = sub.createItem("Sub", nil);
# var s2 = sub.createItem("Sub", nil);
# i1.setMenu(sub);
# m.setPosition(200, 200);
# m.show();
gui.MenuItem = {
# @description Create a new menu item widget
# @cfg_field text: str Text of the new menu item
# @cfg_field cb: callable Function / method to call when the item is clicked
# @cfg_field icon: str Path of an icon to be displayed (relative to the path in `canvas.style._dir_widgets`)
# @cfg_field enabled: bool Initial state of the menu item: enabled (1) or disabled (0)
new: func(parent, style, cfg) {
var cfg = Config.new(cfg);
var m = gui.Widget.new(gui.MenuItem);
m._text = cfg.get("text", "Menu item");
m._cb = cfg.get("cb", nil);
m._icon = cfg.get("icon", nil);
m._enabled = cfg.get("enabled", 1);
m._hovered = 0;
m._menu = nil;
m._parent_menu = nil;
m._setView(style.createWidget(parent, cfg.get("type", "menu-item"), cfg));
m.setLayoutMinimumSize([48, 24]);
m.setLayoutSizeHint([64, 24]);
m.setLayoutMaximumSize([1024, 24]);
m._view.setText(m, m._text);
m._view.setIcon(m._icon);
return m;
},
setMenu: func(menu) {
me._menu = menu;
return me.update();
},
onClicked: func(e) {
if (!me._menu and me._cb) {
me._cb(e);
}
},
onMouseEnter: func(e) {
print("entered item ", me._enabled);
for (var i = 0; i < me._parent_menu._layout.count(); i += 1) {
var item = me._parent_menu._layout.itemAt(i);
item._hovered = 0;
if (item._menu) {
item._menu.hide();
}
item.update();
}
if (me._enabled) {
me._hovered = 1;
var x = e.screenX - e.localX + me.geometry()[2];
var y = e.screenY - e.localY;
me._showMenu(x, y);
}
me.update();
},
onMouseLeave: func(e) {
print("left item");
if (me._menu == nil) {
me._hovered = 0;
}
me.update();
},
removeMenu: func() {
me._menu = nil;
return me.update();
},
_showMenu: func(x, y) {
if (me._menu) {
me._menu.setPosition(x, y);
me._menu.show();
me._menu.setFocus();
}
},
setEnabled: func(enabled = 1) {
me._enabled = enabled;
return me.update();
},
setText: func(text) {
me._text = text;
me._view.setText(me, text);
return me.update();
},
setIcon: func(icon) {
me._icon = icon;
me._view.setIcon(icon);
return me.update();
},
setCallback: func(cb = nil) {
me._cb = cb;
return me;
},
update: func {
me._view.update(me);
return me;
},
_setView: func(view) {
call(gui.Widget._setView, [view], me);
var el = view._root;
el.addEventListener("click", func(e) me.onClicked(e));
el.addEventListener("mouseenter", func(e) me.onMouseEnter(e));
el.addEventListener("mouseleave", func(e) me.onMouseLeave(e));
el.addEventListener("drag", func(e) e.stopPropagation());
}
};
gui.Menu = {
new: func(id = nil) {
var m = gui.Popup.new([100, 60], id);
m.parents = [gui.Menu] ~ m.parents;
m.style = style;
m._canvas = m.createCanvas().setColorBackground(style.getColor("bg_color"));
m._root = m._canvas.createGroup();
m._layout = VBoxLayout.new();
m._layout.setSpacing(0);
m.setLayout(m._layout);
m.hide();
return m;
},
# @description Add the given menu item to the menu (normally a `canvas.gui.MenuItem`, but can be any `canvas.gui.Widget` in theory)
# @return canvas.gui.Menu Return me to enable method chaining
addItem: func(item) {
item._parent_menu = me;
me._layout.addItem(item);
me.setSize(me._layout.minimumSize()[0], me._layout.minimumSize()[1]);
return me;
},
# @description Create, insert and return a `canvas.gui.MenuItem` with given text and an optional callback, icon and enabled state.
# @param text: strrequired Text to display on the menu item
# @param cb: callable optional Function / method to call when the item is clicked - if no callback is wanted, nil can be used
# @param icon: str optional Path to the icon (relative to canvas.style._dir_widgets) or nil if none should be displayed
# @param enabled: bool optional Whether the item should be enabled (1) or disabled (0)
# @return canvas.gui.MenuItem The item that was created
createItem: func(text, cb = nil, icon = nil, enabled = 1) {
item = gui.MenuItem.new(me._root, me.style, {"text": text, "cb": cb, "icon": icon, "enabled": enabled});
me.addItem(item);
return item;
},
# @description Create, insert and return a `canvas.gui.MenuItem with the given text and assign the given submenu to it,
# optionally add the given icon and set the given enabled state
# @param text: strrequired Text to display on the menu item
# @param menu: canvas.gui.Menu Submenu that shall be assigned to the new menu item
# @param icon: str optional Path to the icon (relative to canvas.style._dir_widgets) or nil if none should be displayed
# @param enabled: bool optional Whether the item should be enabled (1) or disabled (0)
# @return canvas.gui.MenuItem The item that was created
addMenu: func(text, menu, icon = nil, enabled = 1) {
item = gui.MenuItem.new(me._root, me.style, {"text": text, cb: nil, "icon": icon, "enabled": enabled});
item.setMenu(menu);
me.addItem(item);
return item;
},
# @description Remove all items from the menu
# @return canvas.gui.Menu Return me to enable method chaining
clear: func {
me._layout.clear();
return me;
},
# @description If `item` is a `canvas.gui.Widget`, remove the given `canvas.gui.Widget` from the menu
# Else assume `item` to be a scalar and try to find an item of the menu that has a `getText` method
# and whose result of calling its `getText` method equals `item` and remove that item
# @param item: Union[str, canvas.gui.Widget] required The widget or the text of the menu item to remove
removeItem: func(item) {
if (isa(item, gui.Widget)) {
me._layout.removeItem(item);
} else {
for (var i = 0; i < me._layout.count(); i += 1) {
if (me._layout.itemAt(i)["getText"] != nil and me._layout.itemAt(i).getText() == item) {
me._layout.takeAt(i);
return me;
}
}
die("No menu item with given text '" ~ item ~ "' found, could not remove !");
}
},
# @description If `index` is an integer, remove and return the item at the given `index`
# Else assume `item` to be a scalar and try to find an item of the menu that has a `getText` method
# and whose result of calling its `getText` method equals `item` and remove and return that item
# @param index: Union[int, str] required The index or text of the menu item to remove
# @return canvas.gui.Widget The item with given text `index` or at the given position `index`
takeAt: func(index) {
if (isint(index)) {
return me._layout.takeAt(index);
} else {
for (var i = 0; i < me._layout.count(); i += 1) {
if (me._layout.itemAt(i)["getText"] != nil and me._layout.itemAt(i).getText() == index) {
return me._layout.takeAt(i);
}
}
die("No menu item with given text '" ~ index ~ "' found, could not remove !");
}
},
# @description Count the items of the menu
# @return int Number of items
count: func() {
return me._layout.count();
},
# @description If `index` is an integer, eturn the item at the given `index`
# Else assume `item` to be a scalar and try to find an item of the menu that has a `getText` method
# and whose result of calling its `getText` method equals `item` and eturn that item
# @param index: Union[int, str] required The index or text of the menu item to return
# @return canvas.gui.Widget The item with given text `index` or at the given position `index`
getItem: func(index) {
if (isint(index)) {
return me._layout.itemAt(index);
} else {
for (var i = 0; i < me._layout.count(); i += 1) {
if (me._layout.itemAt(i)["getText"] != nil and me._layout.itemAt(i).getText() == index) {
return me._layout.itemAt(i);
}
}
die("No menu item with given text '" ~ index ~ "' found, could not remove !");
}
},
# @description Destructor
del: func() {
me.hide();
me.clear();
me._canvas.del();
},
# @description Update the menu and its items
update: func() {
me.parents[1].update();
for (var i = 0; i < me._layout.count(); i += 1) {
me._layout.itemAt(i).update();
}
return me;
},
};

285
Nasal/canvas/gui/Popup.nas Normal file
View file

@ -0,0 +1,285 @@
# SPDX-FileCopyrightText: (C) 2022 Frederic Croix <thefgfseagle@gmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later
gui.Popup = {
__used_ids: [],
# Constructor
#
# @param size ([width, height])
new: func(size_, id = nil, parent = nil) {
if (id == nil or contains(gui.Popup.__used_ids, id)) {
id = "popup" ~ size(gui.Popup.__used_ids);
}
append(gui.Popup.__used_ids, id);
var ghost = _newWindowGhost(id);
var m = {
parents: [gui.Popup, PropertyElement, ghost],
_ghost: ghost,
_node: props.wrapNode(ghost._node_ghost),
_focused: 0,
_widgets: [],
_parent: parent,
_canvas: nil,
};
m.setInt("content-size[0]", size_[0]);
m.setInt("content-size[1]", size_[1]);
m.setFocus();
# arg = [child, listener_node, mode, is_child_event]
setlistener(m._node, func m._propCallback(arg[0], arg[2]), 0, 2);
return m;
},
# Destructor
del: func {
me.clearFocus();
if (me["_canvas"] != nil) {
var placements = me._canvas._node.getChildren("placement");
# Do not remove canvas if other placements exist
if (size(placements) > 1) {
foreach (var p; placements) {
if (p.getValue("type") == "window" and p.getValue("id") == me.get("id")) {
p.remove();
}
}
} else {
me._canvas.del();
}
me._canvas = nil;
}
if (me._node != nil) {
me._node.remove();
me._node = nil;
}
},
# Create the canvas to be used for this Window
#
# @return The new canvas
createCanvas: func() {
var size = [
me.get("content-size[0]"),
me.get("content-size[1]")
];
me._canvas = new({
size: [size[0], size[1]],
view: size,
placement: {
type: "window",
id: me.get("id")
},
# Standard alpha blending
"blend-source-rgb": "src-alpha",
"blend-destination-rgb": "one-minus-src-alpha",
# Just keep current alpha (TODO allow using rgb textures instead of rgba?)
"blend-source-alpha": "zero",
"blend-destination-alpha": "one"
});
me._canvas._focused_widget = nil;
me._canvas.data("focused", me._focused);
return me._canvas;
},
# Set an existing canvas to be used for this Window
setCanvas: func(canvas_) {
if (ghosttype(canvas_) != "Canvas") {
return debug.warn("Not a Canvas");
}
canvas_.addPlacement({type: "window", "id": me.get("id")});
me['_canvas'] = canvas_;
canvas_._focused_widget = nil;
canvas_.data("focused", me._focused);
return me;
},
# Get the displayed canvas
getCanvas: func(create = 0) {
if (me['_canvas'] == nil and create) {
me.createCanvas();
}
return me['_canvas'];
},
setLayout: func(l) {
if (me['_canvas'] == nil) {
me.createCanvas();
}
me._canvas.update(); # Ensure placement is applied
me._ghost.setLayout(l);
return me;
},
#
setFocus: func {
if (gui.focused_window != nil) {
gui.focused_window.clearFocus();
}
# me.onFocusIn();
me._focused = 1;
me._onStateChange();
gui.focused_window = me;
setInputFocus(me);
return me;
},
#
clearFocus: func {
# me.onFocusOut();
me._focused = 0;
me._onStateChange();
if (gui.focused_window == me) {
gui.focused_window = nil;
setInputFocus(nil);
}
if (me._parent != nil and contains(gui.open_popups, me._parent)) {
me._parent.setFocus();
}
return me;
},
setPosition: func {
if (size(arg) == 1) {
var arg = arg[0];
}
var (x, y) = arg;
me.setInt("tf/t[0]", x);
me.setInt("tf/t[1]", y);
return me;
},
setSize: func {
if (size(arg) == 1) {
var arg = arg[0];
}
var (w, h) = arg;
me.set("content-size[0]", w);
me.set("content-size[1]", h);
if (me.onResize != nil) {
me.onResize();
}
return me;
},
getSize: func {
var w = me.get("content-size[0]");
var h = me.get("content-size[1]");
return [w,h];
},
# Raise to top of window stack
raise: func() {
# on writing the z-index the window always is moved to the top of all other
# windows with the same z-index.
me.setInt("z-index", me.get("z-index", gui.STACK_INDEX["always-on-top"]));
me.setFocus();
},
hide: func(parents = 0) {
me.clearFocus();
me._ghost.hide();
for (var i = 0; i < size(gui.open_popups); i += 1) {
if (gui.open_popups[i] == me) {
gui.open_popups = subvec(gui.open_popups, 0, i) ~ subvec(gui.open_popups, i);
break;
}
}
if (me._parent != nil) {
me._parent.setFocus();
}
},
show: func() {
me._ghost.show();
me.raise();
if (me._canvas != nil) {
me._canvas.update();
}
for (var i = 0; i < size(gui.open_popups); i += 1) {
gui.open_popups[i].clearFocus();
}
append(gui.open_popups, me);
},
# Hide / show the window based on whether it's currently visible
toggle: func() {
if (me.isVisible()) {
me.hide();
} else {
me.show();
me.raise();
}
},
onResize: func() {
if (me['_canvas'] == nil) {
return;
}
for(var i = 0; i < 2; i += 1) {
var size = me.get("content-size[" ~ i ~ "]");
me._canvas.set("size[" ~ i ~ "]", size);
me._canvas.set("view[" ~ i ~ "]", size);
}
},
# protected:
_onStateChange: func {
var event = canvas.CustomEvent.new("wm.focus-" ~ (me._focused ? "in" : "out"));
if (me.getCanvas() != nil) {
me.getCanvas().data("focused", me._focused).dispatchEvent(event);
}
},
# private:
#mode 0 = value changed, +-1 add/remove node
_propCallback: func(child, mode) {
if (!me._node.equals(child.getParent())) {
return;
}
var name = child.getName();
# support for CSS like position: absolute; with right and/or bottom margin
if (name == "right") {
me._handlePositionAbsolute(child, mode, name, 0);
} elsif (name == "bottom") {
me._handlePositionAbsolute(child, mode, name, 1);
}
},
_handlePositionAbsolute: func(child, mode, name, index) {
# mode
# -1 child removed
# 0 value changed
# 1 child added
if (mode == 0) {
me._updatePos(index, name);
} elsif (mode == 1) {
me["_listener_" ~ name] = [
setlistener(
"/sim/gui/canvas/size[" ~ index ~ "]",
func me._updatePos(index, name)
),
setlistener(
me._node.getNode("content-size[" ~ index ~ "]"),
func me._updatePos(index, name)
)
];
} elsif (mode == -1) {
for (var i = 0; i < 2; i += 1) {
removelistener(me["_listener_" ~ name][i]);
}
}
},
_updatePos: func(index, name) {
me.setInt(
"tf/t[" ~ index ~ "]",
getprop("/sim/gui/canvas/size[" ~ index ~ "]") - me.get(name) - me.get("content-size[" ~ index ~ "]")
);
},
};

View file

@ -853,4 +853,69 @@ DefaultStyle.widgets.slider = {
}
return me;
}
};
DefaultStyle.widgets["menu-item"] = {
new: func(parent, cfg) {
me._root = parent.createChild("group", "menu-item");
me._bg = me._root.createChild("path");
me._icon = me._root.createChild("image");
me._label = me._root.createChild("text")
.set("font", "LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", 14)
.set("alignment", "left-center");
me._submenu_indicator = me._root.createChild("path")
.vert(12).line(6, -7).close();
},
setSize: func(model, w, h) {
me._bg.reset().rect(0, 0, w, h);
me._icon.setTranslation(3, int((h - 12) / 2));
me._label.setTranslation(24, int(h / 2) + 1);
me._submenu_indicator.setTranslation(w - 15, int((h - 12) / 2));
return me;
},
setText: func(model, text) {
me._label.setText(text);
var min_width = me._label.maxWidth() + 6 + 48;
model.setLayoutMinimumSize([min_width, 24]);
model.setLayoutSizeHint([min_width, 24]);
return me;
},
setIcon: func(icon) {
if (!icon) {
me._icon.hide();
} else {
me._icon.show();
var file = me._style._dir_widgets ~ "/" ~ icon;
me._icon.set("src", file);
}
return me;
},
update: func(model) {
me._bg.set("fill", me._style.getColor("menu_item_bg" ~ (model._hovered ? "_hovered" : "")));
var text_color_name = "menu_item_fg";
if (model._hovered) {
text_color_name ~= "_hovered";
} else if (!model._enabled) {
text_color_name ~= "_disabled";
}
me._label.set("fill", me._style.getColor(text_color_name));
me._submenu_indicator.set("fill", me._style.getColor("menu_item_submenu_indicator" ~ (model._hovered ? "_hovered" : "")));
if (model._menu != nil) {
me._submenu_indicator.show();
} else {
me._submenu_indicator.hide();
}
return me;
}
}

View file

@ -1,30 +0,0 @@
# SPDX-FileCopyrightText: (C) 2022 James Turner <james@flightgear.org>
# SPDX-License-Identifier: GPL-2.0-or-later
gui.widgets.Menu = {
new: func(parent, style, cfg)
{
var cfg = Config.new(cfg);
var m = gui.Widget.new(gui.widgets.Menu);
m._focus_policy = m.StrongFocus;
# m._flat = cfg.get("flat", 0);
# m._isDefault = cfg.get("default", 0);
# m._destructive = cfg.get("destructive", 0);
m._setView( style.createWidget(parent, cfg.get("type", "menu"), cfg) );
return m;
},
# protected:
_setView: func(view)
{
call(gui.Widget._setView, [view], me);
var el = view._root;
}
};

View file

@ -137,5 +137,47 @@
<green type="float">0.955</green>
<blue type="float">0.95</blue>
</tab_widget_tab_button_bg_selected>
<menu_item_fg>
<red type="float">0.298</red>
<green type="float">0.298</green>
<blue type="float">0.298</blue>
</menu_item_fg>
<menu_item_fg_disabled>
<red type="float">0.89</red>
<green type="float">0.89</green>
<blue type="float">0.89</blue>
</menu_item_fg_disabled>
<menu_item_fg_hovered>
<red type="float">0.95</red>
<green type="float">0.95</green>
<blue type="float">0.95</blue>
</menu_item_fg_hovered>
<menu_item_bg>
<red type="float">0.949</red>
<green type="float">0.945</green>
<blue type="float">0.941</blue>
</menu_item_bg>
<menu_item_bg_hovered>
<red type="float">0.15</red>
<green type="float">0.15</green>
<blue type="float">1</blue>
</menu_item_bg_hovered>
<menu_item_submenu_indicator>
<red type="float">0</red>
<green type="float">0</green>
<blue type="float">0</blue>
</menu_item_submenu_indicator>
<menu_item_submenu_indicator_hovered>
<red type="float">0.949</red>
<green type="float">0.945</green>
<blue type="float">0.941</blue>
</menu_item_submenu_indicator_hovered>
</colors>
</PropertyList>