diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas index 3d868141a..0362bd510 100644 --- a/Nasal/canvas/api.nas +++ b/Nasal/canvas/api.nas @@ -91,5 +91,6 @@ var unload = func { unloadTooltips(); unloadErrorNotification(); + unloadGUI(); logprint(LOG_INFO, "Unloaded canvas Nasal module"); -}; \ No newline at end of file +}; diff --git a/Nasal/canvas/api/element.nas b/Nasal/canvas/api/element.nas index ef43f0e5d..08c142aa4 100644 --- a/Nasal/canvas/api/element.nas +++ b/Nasal/canvas/api/element.nas @@ -33,7 +33,7 @@ var Element = { } append(me._bindings, keyboard.Binding.new(s, f)); if (size(me._bindings) == 1) { - obj.addEventListener("keydown", func(e) obj.onKeyPressed(e)); + me.addEventListener("keydown", func(e) me.onKeyPressed(e)); } }, diff --git a/Nasal/canvas/gui.nas b/Nasal/canvas/gui.nas index f697509ae..40742504d 100644 --- a/Nasal/canvas/gui.nas +++ b/Nasal/canvas/gui.nas @@ -30,6 +30,7 @@ var loadDialog = func(name) loadGUIFile("dialogs/" ~ name ~ ".nas"); loadGUIFile("Config.nas"); loadGUIFile("Menu.nas"); +loadGUIFile("MenuBar.nas"); loadGUIFile("Popup.nas"); loadGUIFile("Style.nas"); loadGUIFile("Widget.nas"); @@ -40,6 +41,7 @@ loadWidget("Button"); loadWidget("CheckBox"); loadWidget("Label"); loadWidget("LineEdit"); +loadWidget("MenuBar"); loadWidget("PropertyWidgets"); loadWidget("ScrollArea"); loadWidget("Rule"); @@ -584,16 +586,17 @@ var Window = { # Clear focus on click outside any window getDesktop().addEventListener("mousedown", func { - if( gui.focused_window != nil ) + if (gui.focused_window != nil) { gui.focused_window.clearFocus(); + } - if (size(gui.open_popups)) { - foreach (var p; gui.open_popups) { - p.hide(); - } + foreach (var p; gui.open_popups) { + p.hide(); } }); +gui.menubar = gui.MenuBar.new(); + # Provide old 'Dialog' for backwards compatiblity (should be removed for 3.0) var Dialog = { new: func(size, type = nil, id = nil) @@ -602,3 +605,7 @@ var Dialog = { return Window.new(size, type, id); } }; + +var unloadGUI = func() { + gui.menubar.del(); +} diff --git a/Nasal/canvas/gui/Menu.nas b/Nasal/canvas/gui/Menu.nas index 21e6fd002..7a095d9a3 100644 --- a/Nasal/canvas/gui/Menu.nas +++ b/Nasal/canvas/gui/Menu.nas @@ -14,6 +14,12 @@ # m.show(); gui.MenuItem = { + MenuPosition: { + Above: 0x0, + Right: 0x1, + Below: 0x2, + Left: 0x4 + }, # @description Create a new menu item widget # @cfg_field text: str Text of the new menu item # @cfg_field shortcut: str String representation of the keyboard shortcut for the item @@ -28,9 +34,11 @@ gui.MenuItem = { m._cb = cfg.get("cb", nil); m._icon = cfg.get("icon", nil); m._enabled = cfg.get("enabled", 1); + m._menu_position = cfg.get("menu_position", gui.MenuItem.MenuPosition.Right); m._hovered = 0; m._menu = nil; m._parent_menu = nil; + m._is_menubar_item = 0; m._setView(style.createWidget(parent, cfg.get("type", "menu-item"), cfg)); @@ -45,32 +53,34 @@ gui.MenuItem = { }, setMenu: func(menu) { + menu._parent_item = me; + menu._canvas_item = me._parent_menu._canvas_item; me._menu = menu; return me.update(); }, onClicked: func(e) { - print("clicked", me._menu == nil, me._cb != nil); if (!me._menu and me._cb) { me._cb(e); } - me._parent_menu.hide(); + if (me._parent_menu != nil) { + me._parent_menu.hide(); + } }, 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) { + if (item._menu != nil) { item._menu.hide(); } item.update(); } if (me._enabled) { me._hovered = 1; - var x = e.screenX - e.localX + me.geometry()[2]; + var x = e.screenX - e.localX; var y = e.screenY - e.localY; me._showMenu(x, y); } @@ -78,7 +88,6 @@ gui.MenuItem = { }, onMouseLeave: func(e) { - print("left item"); if (me._menu == nil) { me._hovered = 0; } @@ -92,10 +101,29 @@ gui.MenuItem = { _showMenu: func(x, y) { if (me._menu) { - me._menu.setPosition(x, y); + var pos = [0, 0]; + if (me._menu_position == gui.MenuItem.MenuPosition.Right) { + pos = [x + me.geometry()[2], y]; + } elsif (me._menu_position == gui.MenuItem.MenuPosition.Below) { + pos = [x, me.geometry()[3] + y]; + } elsif (me._menu_position == gui.MenuItem.MenuPosition.Above) { + pos = [x, y - me._menu.getSize()[1]]; + } elsif (me._menu_position == gui.MenuItem.MenuPosition.Left) { + pos = [x - me._menu.getSize()[0], y]; + } + me._menu.setPosition(pos[0], pos[1]); me._menu.show(); me._menu.setFocus(); } + me._hovered = 1; + }, + + _hideMenu: func { + if (me._menu) { + me._menu.clearFocus(); + me._menu.hide(); + } + me._hovered = 0; }, setEnabled: func(enabled = 1) { @@ -121,7 +149,14 @@ gui.MenuItem = { _setParentMenu: func(m) { me._parent_menu = m; if (me._parent_menu != nil and me._parent_menu._canvas_item != nil and me._cb != nil) { - me._parent_menu._canvas_item.bindShortcut(me._shortcut, me._cb); + if (me._shortcut != nil) { + me._parent_menu._canvas_item.bindShortcut(me._shortcut, me._cb); + } + if (me._menu != nil) { + for (var i = 0; i < me._menu.count(); i += 1) { + me._menu.getItem(i).setCanvasItem(me._parent_menu._canvas_item); + } + } } }, @@ -137,7 +172,9 @@ gui.MenuItem = { }, update: func { - me._view.update(me); + if (me._view != nil) { + me._view.update(me); + } return me; }, @@ -158,6 +195,7 @@ gui.Menu = { var m = gui.Popup.new([100, 60], id); m.parents = [gui.Menu] ~ m.parents; m.style = style; + m._parent_item = nil; m._canvas = m.createCanvas().setColorBackground(style.getColor("bg_color")); m._root = m._canvas.createGroup(); @@ -182,7 +220,7 @@ gui.Menu = { addItem: func(item) { item._setParentMenu(me); me._layout.addItem(item); - me.setSize(me._layout.minimumSize()[0], me._layout.minimumSize()[1]); + me.setSize(math.max(me._layout.minimumSize()[0], 64), math.max(me._layout.minimumSize()[1], 24)); return me; }, @@ -199,27 +237,39 @@ gui.Menu = { } var item = gui.MenuItem.new(me._root, me.style, {text: text, cb: cb, shortcut: shortcut, icon: icon, enabled: enabled}); me.addItem(item); - return item; + return me; }, # @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: str required 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 = nil, menu = nil, icon = nil, enabled = 1) { + addMenu: func(text = nil, menu = nil, enabled = 1) { if (text == nil) { die("cannot create a menu item without text"); } if (menu == nil) { die("cannot create a submenu item without submenu"); } - var item = gui.MenuItem.new(me._root, me.style, {text: text, cb: nil, shortcut: nil, icon: icon, enabled: enabled}); + var item = gui.MenuItem.new(me._root, me.style, {text: text, cb: nil, shortcut: nil, icon: nil, enabled: enabled}); + menu._parent_item = item; item.setMenu(menu); me.addItem(item); - return item; + return me; + }, + + createMenu: func(text = nil, enabled = 1) { + if (text == nil) { + die("cannot create a submenu item without text"); + } + var menu = gui.Menu.new(); + var item = gui.MenuItem.new(me._root, me.style, {text: text, cb: nil, shortcut: nil, icon: nil, enabled: enabled}); + menu._parent_item = item; + item.setMenu(menu); + me.addItem(item); + return menu; }, # @description Remove all items from the menu @@ -297,6 +347,14 @@ gui.Menu = { call(me.parents[1].show, [], me); }, + hide: func { + if (me._parent_item != nil) { + me._parent_item._hovered = 0; + me._parent_item.update(); + } + call(me.parents[1].hide, [], me); + }, + # @description Destructor del: func() { me.hide(); diff --git a/Nasal/canvas/gui/MenuBar.nas b/Nasal/canvas/gui/MenuBar.nas new file mode 100644 index 000000000..ffa598039 --- /dev/null +++ b/Nasal/canvas/gui/MenuBar.nas @@ -0,0 +1,73 @@ +gui.MenuBar = { + new: func(id = nil) { + var m = canvas.Window.new([64, 24]); + m.parents = [gui.MenuBar] ~ m.parents; + + m._canvas = m.createCanvas().setColorBackground(style.getColor("bg_color")); + m._root = m._canvas.createGroup(); + m._layout = VBoxLayout.new(); + m.setLayout(m._layout); + m._menubar = gui.widgets.MenuBar.new(m._root, style, {}); + m._menubar.setCanvasItem(getDesktop()); + m._layout.addItem(m._menubar); + m.setPosition(0, 0); + + return m; + }, + + addMenu: func(text = nil, menu = nil, enabled = 1) { + var item = me._menubar.addMenu(text, menu, enabled); + me.setSize(math.max(me._layout.sizeHint()[0], 64), math.max(me._layout.sizeHint()[1], 24)); + return me; + }, + + createMenu: func(text = nil, enabled = 1) { + var menu = me._menubar.createMenu(text, enabled); + me.setSize(math.max(me._layout.sizeHint()[0], 64), math.max(me._layout.sizeHint()[1], 24)); + return menu; + }, + + clear: func { + me._menubar.clear(); + return me; + }, + + removeMenu: func(item) { + me._menubar.removeMenu(item); + return me; + }, + + takeAt: func(index) { + return me._menubar.takeAt(index); + }, + + count: func() { + return me._menubar.count(); + }, + + getItem: func(index) { + return me._menubar.getItem(index); + }, + + getMenu: func(index) { + return me._menubar.getMenu(index); + }, + + show: func(x = nil, y = nil) { + if (x != nil and y != nil) { + me.setPosition(x, y); + } + me._ghost.show(); + me.raise(); + if (me._canvas != nil) { + me._canvas.update(); + } + }, + + del: func() { + me.hide(); + me._menubar.clear(); + me._canvas.del(); + }, +}; + diff --git a/Nasal/canvas/gui/Popup.nas b/Nasal/canvas/gui/Popup.nas index 945afd353..5132f88bd 100644 --- a/Nasal/canvas/gui/Popup.nas +++ b/Nasal/canvas/gui/Popup.nas @@ -7,10 +7,6 @@ gui.Popup = { # # @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], @@ -127,7 +123,6 @@ gui.Popup = { me._focused = 1; me._onStateChange(); gui.focused_window = me; - setInputFocus(me); return me; }, # @@ -137,7 +132,6 @@ gui.Popup = { 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(); diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas index 8a819fe1a..6d043c827 100644 --- a/Nasal/canvas/gui/styles/DefaultStyle.nas +++ b/Nasal/canvas/gui/styles/DefaultStyle.nas @@ -866,12 +866,12 @@ DefaultStyle.widgets["menu-item"] = { me._label = me._root.createChild("text") .set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) - .set("alignment", "left-center"); + .set("alignment", "left-baseline"); me._shortcut = me._root.createChild("text") .set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) - .set("alignment", "right-center"); + .set("alignment", "right-baseline"); me._submenu_indicator = me._root.createChild("path") .vert(12).line(6, -7).close(); @@ -879,15 +879,29 @@ DefaultStyle.widgets["menu-item"] = { 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._shortcut.setTranslation(w - 3, int(h / 2) + 1); - me._submenu_indicator.setTranslation(w - 15, int((h - 12) / 2)); + var offset = 0; + if (!model._is_menubar_item) { + offset += 5 + 18; + } + me._icon.setTranslation(5, int((h - 12) / 2)); + me._label.setTranslation(offset + 5, int(h / 2) + 4); + me._shortcut.setTranslation(w - 5, int(h / 2) + 4); + me._submenu_indicator.setTranslation(w - 12, int((h - 12) / 2)); return me; }, _updateLayoutSizes: func(model) { - var min_width = 3 + 18 + 3 + me._label.maxWidth() + 3 + me._shortcut.maxWidth() + 3 + 12 + 3; + var min_width = 5 + me._label.maxWidth() + 5; + if (!model._is_menubar_item) { + # add icon space + min_width += 5 + 18; + # add shortcut space + min_width += me._shortcut.maxWidth() + 10; + if (model._menu != nil) { + # add submenu indicator space + min_width += 12; + } + } model.setLayoutMinimumSize([min_width, 24]); model.setLayoutSizeHint([min_width, 24]); @@ -927,13 +941,35 @@ DefaultStyle.widgets["menu-item"] = { me._shortcut.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(); + if (!model._is_menubar_item) { + me._submenu_indicator.show(); + } me._shortcut.hide(); } else { me._submenu_indicator.hide(); me._shortcut.show(); } + return me; + } +}; + +DefaultStyle.widgets["menu-bar"] = { + new: func(parent, cfg) { + me._root = parent.createChild("group", "menu-bar"); + me._bg = me._root.createChild("path"); + me._items = me._root.createChild("group", "tab-widget-content"); + }, + + setSize: func(model, w, h) { + me._bg.reset().rect(0, 0, w, h); + me._items.setTranslation(0, 0); + return me; + }, + + update: func(model) { + me._bg.set("fill", me._style.getColor("bg_color")); + return me; } } diff --git a/Nasal/canvas/gui/widgets/MenuBar.nas b/Nasal/canvas/gui/widgets/MenuBar.nas new file mode 100644 index 000000000..f19341afa --- /dev/null +++ b/Nasal/canvas/gui/widgets/MenuBar.nas @@ -0,0 +1,164 @@ +# MenuBar.nas - a menu bar that can be added as a normal widget to a layout + +# SPDX-FileCopyrightText: (C) 2022 Frederic Croix +# SPDX-License-Identifier: GPL-2.0-or-later + +gui.widgets.MenuBar = { + new: func(parent, style, cfg) { + var m = gui.Widget.new(gui.widgets.MenuBar); + m._cfg = Config.new(cfg); + m._focus_policy = m.NoFocus; + + m._setView(style.createWidget(parent, "menu-bar", m._cfg)); + + m._layout = HBoxLayout.new(); + m._layout.setSpacing(0); + m._layout.setCanvas(m._view._root.getCanvas()); + m._canvas_item = nil; + + m.setLayoutMinimumSize([48, 24]); + m.setLayoutSizeHint([64, 24]); + + return m; + }, + + setCanvasItem: func(item) { + me._canvas_item = item; + for (var i = 0; i < me._layout.count(); i += 1) { + me._layout.itemAt(i)._setParentMenu(me); + } + }, + + # @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: str required Text to display on the menu item + # @param menu: canvas.gui.Menu Submenu that shall be assigned to the new menu item + # @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 = nil, menu = nil, enabled = 1) { + if (text == nil) { + die("cannot create a menu item without text"); + } + if (menu == nil) { + die("cannot create a submenu item without submenu"); + } + var item = gui.MenuItem.new(me._view._items, style, + { + text: text, cb: nil, shortcut: nil, icon: nil, enabled: enabled, + menu_position: gui.MenuItem.MenuPosition.Below, + } + ); + item._is_menubar_item = 1; + item._view._updateLayoutSizes(item); + item._setParentMenu(me); + item.setMenu(menu); + me._layout.addItem(item); + me.setSize(math.max(me._layout.minimumSize()[0], 64), math.max(me._layout.minimumSize()[1], 24)); + return me; + }, + + createMenu: func(text = nil, enabled = 1) { + if (text == nil) { + die("cannot create a submenu item without text"); + } + var menu = gui.Menu.new(); + me.addMenu(text, menu, enabled); + return menu; + }, + + # @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 + removeMenu: 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 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)._menu; + } + } + die("No menu 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)._menu; + } + } + die("No menu item with given text '" ~ index ~ "' found, could not remove !"); + } + }, + + getMenu: func(index) { + return me.getItem(index)._menu; + }, + + setSize: func { + if (size(arg) == 1) { + var arg = arg[0]; + } + var (x, y) = arg; + me._size = [x, y]; + me.setLayoutMinimumSize([x, y]); + me.setLayoutSizeHint([x, y]); + return me.update(); + }, + + # @description Update the menu and its items + update: func() { + if(me._layout.getParent() == nil) { + me._layout.setParent(me); + } + + me._layout.setGeometry([0, 0, me._size[0], me._size[1]]); + me._view.setSize(me, me._size[0], me._size[1]); + me._view.update(me); + + return me; + }, +}; + diff --git a/Nasal/keyboard.nas b/Nasal/keyboard.nas index 10fea5ea2..ce06812d4 100644 --- a/Nasal/keyboard.nas +++ b/Nasal/keyboard.nas @@ -433,6 +433,7 @@ var Binding = { }, fire: func(e) { + debug.dump(e); if (isfunc(me.f)) { me.f(e); }