1
0
Fork 0

Added menu bar widget and global menu bar, menu item improvements

This commit is contained in:
TheFGFSEagle 2023-01-10 20:42:01 +01:00 committed by James Turner
parent 2cf388e674
commit a5e30023bd
9 changed files with 370 additions and 36 deletions

View file

@ -91,5 +91,6 @@ var unload = func
{ {
unloadTooltips(); unloadTooltips();
unloadErrorNotification(); unloadErrorNotification();
unloadGUI();
logprint(LOG_INFO, "Unloaded canvas Nasal module"); logprint(LOG_INFO, "Unloaded canvas Nasal module");
}; };

View file

@ -33,7 +33,7 @@ var Element = {
} }
append(me._bindings, keyboard.Binding.new(s, f)); append(me._bindings, keyboard.Binding.new(s, f));
if (size(me._bindings) == 1) { if (size(me._bindings) == 1) {
obj.addEventListener("keydown", func(e) obj.onKeyPressed(e)); me.addEventListener("keydown", func(e) me.onKeyPressed(e));
} }
}, },

View file

@ -30,6 +30,7 @@ var loadDialog = func(name) loadGUIFile("dialogs/" ~ name ~ ".nas");
loadGUIFile("Config.nas"); loadGUIFile("Config.nas");
loadGUIFile("Menu.nas"); loadGUIFile("Menu.nas");
loadGUIFile("MenuBar.nas");
loadGUIFile("Popup.nas"); loadGUIFile("Popup.nas");
loadGUIFile("Style.nas"); loadGUIFile("Style.nas");
loadGUIFile("Widget.nas"); loadGUIFile("Widget.nas");
@ -40,6 +41,7 @@ loadWidget("Button");
loadWidget("CheckBox"); loadWidget("CheckBox");
loadWidget("Label"); loadWidget("Label");
loadWidget("LineEdit"); loadWidget("LineEdit");
loadWidget("MenuBar");
loadWidget("PropertyWidgets"); loadWidget("PropertyWidgets");
loadWidget("ScrollArea"); loadWidget("ScrollArea");
loadWidget("Rule"); loadWidget("Rule");
@ -584,16 +586,17 @@ var Window = {
# Clear focus on click outside any window # Clear focus on click outside any window
getDesktop().addEventListener("mousedown", func { getDesktop().addEventListener("mousedown", func {
if( gui.focused_window != nil ) if (gui.focused_window != nil) {
gui.focused_window.clearFocus(); gui.focused_window.clearFocus();
}
if (size(gui.open_popups)) {
foreach (var p; gui.open_popups) { foreach (var p; gui.open_popups) {
p.hide(); p.hide();
} }
}
}); });
gui.menubar = gui.MenuBar.new();
# Provide old 'Dialog' for backwards compatiblity (should be removed for 3.0) # Provide old 'Dialog' for backwards compatiblity (should be removed for 3.0)
var Dialog = { var Dialog = {
new: func(size, type = nil, id = nil) new: func(size, type = nil, id = nil)
@ -602,3 +605,7 @@ var Dialog = {
return Window.new(size, type, id); return Window.new(size, type, id);
} }
}; };
var unloadGUI = func() {
gui.menubar.del();
}

View file

@ -14,6 +14,12 @@
# m.show(); # m.show();
gui.MenuItem = { gui.MenuItem = {
MenuPosition: {
Above: 0x0,
Right: 0x1,
Below: 0x2,
Left: 0x4
},
# @description Create a new menu item widget # @description Create a new menu item widget
# @cfg_field text: str Text of the new menu item # @cfg_field text: str Text of the new menu item
# @cfg_field shortcut: str String representation of the keyboard shortcut for the 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._cb = cfg.get("cb", nil);
m._icon = cfg.get("icon", nil); m._icon = cfg.get("icon", nil);
m._enabled = cfg.get("enabled", 1); m._enabled = cfg.get("enabled", 1);
m._menu_position = cfg.get("menu_position", gui.MenuItem.MenuPosition.Right);
m._hovered = 0; m._hovered = 0;
m._menu = nil; m._menu = nil;
m._parent_menu = nil; m._parent_menu = nil;
m._is_menubar_item = 0;
m._setView(style.createWidget(parent, cfg.get("type", "menu-item"), cfg)); m._setView(style.createWidget(parent, cfg.get("type", "menu-item"), cfg));
@ -45,32 +53,34 @@ gui.MenuItem = {
}, },
setMenu: func(menu) { setMenu: func(menu) {
menu._parent_item = me;
menu._canvas_item = me._parent_menu._canvas_item;
me._menu = menu; me._menu = menu;
return me.update(); return me.update();
}, },
onClicked: func(e) { onClicked: func(e) {
print("clicked", me._menu == nil, me._cb != nil);
if (!me._menu and me._cb) { if (!me._menu and me._cb) {
me._cb(e); me._cb(e);
} }
if (me._parent_menu != nil) {
me._parent_menu.hide(); me._parent_menu.hide();
}
}, },
onMouseEnter: func(e) { onMouseEnter: func(e) {
print("entered item ", me._enabled);
for (var i = 0; i < me._parent_menu._layout.count(); i += 1) { for (var i = 0; i < me._parent_menu._layout.count(); i += 1) {
var item = me._parent_menu._layout.itemAt(i); var item = me._parent_menu._layout.itemAt(i);
item._hovered = 0; item._hovered = 0;
if (item._menu) { if (item._menu != nil) {
item._menu.hide(); item._menu.hide();
} }
item.update(); item.update();
} }
if (me._enabled) { if (me._enabled) {
me._hovered = 1; me._hovered = 1;
var x = e.screenX - e.localX + me.geometry()[2]; var x = e.screenX - e.localX;
var y = e.screenY - e.localY; var y = e.screenY - e.localY;
me._showMenu(x, y); me._showMenu(x, y);
} }
@ -78,7 +88,6 @@ gui.MenuItem = {
}, },
onMouseLeave: func(e) { onMouseLeave: func(e) {
print("left item");
if (me._menu == nil) { if (me._menu == nil) {
me._hovered = 0; me._hovered = 0;
} }
@ -92,10 +101,29 @@ gui.MenuItem = {
_showMenu: func(x, y) { _showMenu: func(x, y) {
if (me._menu) { 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.show();
me._menu.setFocus(); me._menu.setFocus();
} }
me._hovered = 1;
},
_hideMenu: func {
if (me._menu) {
me._menu.clearFocus();
me._menu.hide();
}
me._hovered = 0;
}, },
setEnabled: func(enabled = 1) { setEnabled: func(enabled = 1) {
@ -121,8 +149,15 @@ gui.MenuItem = {
_setParentMenu: func(m) { _setParentMenu: func(m) {
me._parent_menu = m; me._parent_menu = m;
if (me._parent_menu != nil and me._parent_menu._canvas_item != nil and me._cb != nil) { if (me._parent_menu != nil and me._parent_menu._canvas_item != nil and me._cb != nil) {
if (me._shortcut != nil) {
me._parent_menu._canvas_item.bindShortcut(me._shortcut, me._cb); 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);
}
}
}
}, },
setIcon: func(icon) { setIcon: func(icon) {
@ -137,7 +172,9 @@ gui.MenuItem = {
}, },
update: func { update: func {
if (me._view != nil) {
me._view.update(me); me._view.update(me);
}
return me; return me;
}, },
@ -158,6 +195,7 @@ gui.Menu = {
var m = gui.Popup.new([100, 60], id); var m = gui.Popup.new([100, 60], id);
m.parents = [gui.Menu] ~ m.parents; m.parents = [gui.Menu] ~ m.parents;
m.style = style; m.style = style;
m._parent_item = nil;
m._canvas = m.createCanvas().setColorBackground(style.getColor("bg_color")); m._canvas = m.createCanvas().setColorBackground(style.getColor("bg_color"));
m._root = m._canvas.createGroup(); m._root = m._canvas.createGroup();
@ -182,7 +220,7 @@ gui.Menu = {
addItem: func(item) { addItem: func(item) {
item._setParentMenu(me); item._setParentMenu(me);
me._layout.addItem(item); 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; 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}); var item = gui.MenuItem.new(me._root, me.style, {text: text, cb: cb, shortcut: shortcut, icon: icon, enabled: enabled});
me.addItem(item); 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, # @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 # optionally add the given icon and set the given enabled state
# @param text: str required Text to display on the menu item # @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 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) # @param enabled: bool optional Whether the item should be enabled (1) or disabled (0)
# @return canvas.gui.MenuItem The item that was created # @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) { if (text == nil) {
die("cannot create a menu item without text"); die("cannot create a menu item without text");
} }
if (menu == nil) { if (menu == nil) {
die("cannot create a submenu item without submenu"); 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); item.setMenu(menu);
me.addItem(item); 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 # @description Remove all items from the menu
@ -297,6 +347,14 @@ gui.Menu = {
call(me.parents[1].show, [], me); 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 # @description Destructor
del: func() { del: func() {
me.hide(); me.hide();

View file

@ -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();
},
};

View file

@ -7,10 +7,6 @@ gui.Popup = {
# #
# @param size ([width, height]) # @param size ([width, height])
new: func(size_, id = nil, parent = nil) { 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 ghost = _newWindowGhost(id);
var m = { var m = {
parents: [gui.Popup, PropertyElement, ghost], parents: [gui.Popup, PropertyElement, ghost],
@ -127,7 +123,6 @@ gui.Popup = {
me._focused = 1; me._focused = 1;
me._onStateChange(); me._onStateChange();
gui.focused_window = me; gui.focused_window = me;
setInputFocus(me);
return me; return me;
}, },
# #
@ -137,7 +132,6 @@ gui.Popup = {
me._onStateChange(); me._onStateChange();
if (gui.focused_window == me) { if (gui.focused_window == me) {
gui.focused_window = nil; gui.focused_window = nil;
setInputFocus(nil);
} }
if (me._parent != nil and contains(gui.open_popups, me._parent)) { if (me._parent != nil and contains(gui.open_popups, me._parent)) {
me._parent.setFocus(); me._parent.setFocus();

View file

@ -866,12 +866,12 @@ DefaultStyle.widgets["menu-item"] = {
me._label = me._root.createChild("text") me._label = me._root.createChild("text")
.set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("font", "LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", 14) .set("character-size", 14)
.set("alignment", "left-center"); .set("alignment", "left-baseline");
me._shortcut = me._root.createChild("text") me._shortcut = me._root.createChild("text")
.set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("font", "LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", 14) .set("character-size", 14)
.set("alignment", "right-center"); .set("alignment", "right-baseline");
me._submenu_indicator = me._root.createChild("path") me._submenu_indicator = me._root.createChild("path")
.vert(12).line(6, -7).close(); .vert(12).line(6, -7).close();
@ -879,15 +879,29 @@ DefaultStyle.widgets["menu-item"] = {
setSize: func(model, w, h) { setSize: func(model, w, h) {
me._bg.reset().rect(0, 0, w, h); me._bg.reset().rect(0, 0, w, h);
me._icon.setTranslation(3, int((h - 12) / 2)); var offset = 0;
me._label.setTranslation(24, int(h / 2) + 1); if (!model._is_menubar_item) {
me._shortcut.setTranslation(w - 3, int(h / 2) + 1); offset += 5 + 18;
me._submenu_indicator.setTranslation(w - 15, int((h - 12) / 2)); }
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; return me;
}, },
_updateLayoutSizes: func(model) { _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.setLayoutMinimumSize([min_width, 24]);
model.setLayoutSizeHint([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._shortcut.set("fill", me._style.getColor(text_color_name));
me._submenu_indicator.set("fill", me._style.getColor("menu_item_submenu_indicator" ~ (model._hovered ? "_hovered" : ""))); me._submenu_indicator.set("fill", me._style.getColor("menu_item_submenu_indicator" ~ (model._hovered ? "_hovered" : "")));
if (model._menu != nil) { if (model._menu != nil) {
if (!model._is_menubar_item) {
me._submenu_indicator.show(); me._submenu_indicator.show();
}
me._shortcut.hide(); me._shortcut.hide();
} else { } else {
me._submenu_indicator.hide(); me._submenu_indicator.hide();
me._shortcut.show(); 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; return me;
} }
} }

View file

@ -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 <thefgfseagle@gmail.com>
# 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;
},
};

View file

@ -433,6 +433,7 @@ var Binding = {
}, },
fire: func(e) { fire: func(e) {
debug.dump(e);
if (isfunc(me.f)) { if (isfunc(me.f)) {
me.f(e); me.f(e);
} }