Added list widget
This commit is contained in:
parent
8a953d71a3
commit
2b9d10804b
5 changed files with 503 additions and 6 deletions
|
@ -42,6 +42,7 @@ loadWidget("Button");
|
|||
loadWidget("CheckBox");
|
||||
loadWidget("Label");
|
||||
loadWidget("LineEdit");
|
||||
loadWidget("List");
|
||||
loadWidget("MenuBar");
|
||||
loadWidget("PropertyWidgets");
|
||||
loadWidget("ScrollArea");
|
||||
|
|
|
@ -57,8 +57,12 @@ var WidgetsFactoryDialog = {
|
|||
var r2 = gui.widgets.HorizontalRule.new(m.tabsContent, style, {});
|
||||
m.tab_1.addItem(r2);
|
||||
|
||||
m.tab_2 = VBoxLayout.new();
|
||||
m.tab_2 = HBoxLayout.new();
|
||||
m.tabs.addTab("tab-2", "Tab 2", m.tab_2);
|
||||
|
||||
m.button_box = VBoxLayout.new();
|
||||
m.tab_2.addItem(m.button_box);
|
||||
|
||||
m.button = gui.widgets.Button.new(m.tabsContent, style, {})
|
||||
.setText("A button")
|
||||
.setFixedSize(60, 30)
|
||||
|
@ -67,13 +71,13 @@ var WidgetsFactoryDialog = {
|
|||
MessageBox.information("You clicked the button …", "… and entered '" ~ (text != nil ? text : "nothing") ~ "' !");
|
||||
});
|
||||
});
|
||||
m.tab_2.addItem(m.button);
|
||||
m.button_box.addItem(m.button);
|
||||
m.image = gui.widgets.Label.new(m.tabsContent, style, {})
|
||||
.setImage("Textures/Splash1.png")
|
||||
.setVisible(0)
|
||||
.setFixedSize(128, 128);
|
||||
|
||||
m.tab_2.addItem(m.image);
|
||||
m.button_box.addItem(m.image);
|
||||
m.image._view._root.addEventListener("mousedown", func (e) {
|
||||
logprint(LOG_INFO, "Image was clicked at:" ~ e.localX ~ "," ~ e.localY);
|
||||
logprint(LOG_INFO, "Client pos:" ~ e.clientX ~ "," ~ e.clientY);
|
||||
|
@ -97,7 +101,7 @@ var WidgetsFactoryDialog = {
|
|||
.listen("toggled", func (e) {
|
||||
m.image.setVisible(int(e.detail.checked));
|
||||
});
|
||||
m.tab_2.addItem(m.checkable_button);
|
||||
m.button_box.addItem(m.checkable_button);
|
||||
|
||||
m.upsize_button = gui.widgets.Button.new(m.tabsContent, style, {})
|
||||
.setText("Upsize window")
|
||||
|
@ -106,7 +110,7 @@ var WidgetsFactoryDialog = {
|
|||
var s = m.window.getSize();
|
||||
m.window.setSize(s[0] + 100, s[1] + 100);
|
||||
});
|
||||
m.tab_2.addItem(m.upsize_button, 5);
|
||||
m.button_box.addItem(m.upsize_button);
|
||||
|
||||
m.downsize_button = gui.widgets.Button.new(m.tabsContent, style, {})
|
||||
.setText("Downsize window")
|
||||
|
@ -115,11 +119,30 @@ var WidgetsFactoryDialog = {
|
|||
var s = m.window.getSize();
|
||||
m.window.setSize(s[0] - 100, s[1] - 100);
|
||||
});
|
||||
m.tab_2.addItem(m.downsize_button, 5);
|
||||
m.button_box.addItem(m.downsize_button);
|
||||
|
||||
m.list_box = VBoxLayout.new();
|
||||
m.tab_2.addItem(m.list_box);
|
||||
|
||||
m.list = gui.widgets.List.new(m.tabsContent);
|
||||
for (var i = 0; i < 30; i += 1) {
|
||||
m.list.createItem("Item " ~ i);
|
||||
}
|
||||
m.list.listen("selection-changed", func {
|
||||
m.list_selection_label.setText("Selected items: " ~ (string.join(", ", map(func(item) item._text, m.list.getSelectedItems())) or "none"));
|
||||
});
|
||||
m.list.setSizeHint([m.list._MAX_SIZE, m.list._MAX_SIZE]);
|
||||
m.list_box.addItem(m.list);
|
||||
|
||||
m.list_selection_label = gui.widgets.Label.new(m.tabsContent, canvas.style, {})
|
||||
.setText("Selected items: none");
|
||||
m.list_selection_label.setAlignment(canvas.AlignBottom);
|
||||
m.list_box.addItem(m.list_selection_label);
|
||||
|
||||
m.benchmark_tab = VBoxLayout.new();
|
||||
m.tabs.addTab("benchmark", "Benchmark", m.benchmark_tab);
|
||||
m.benchmark_tab_scroll = canvas.gui.widgets.ScrollArea.new(m.tabsContent, canvas.style, {});
|
||||
m.benchmark_tab_scroll.setSizeHint([m.list._MAX_SIZE, m.list._MAX_SIZE]);
|
||||
m.benchmark_tab_scroll_layout = VBoxLayout.new();
|
||||
m.benchmark_tab_scroll.setLayout(m.benchmark_tab_scroll_layout);
|
||||
m.benchmark_tab.addItem(m.benchmark_tab_scroll);
|
||||
|
|
|
@ -964,6 +964,164 @@ DefaultStyle.widgets["menu-bar"] = {
|
|||
return me;
|
||||
},
|
||||
|
||||
update: func(model) {
|
||||
me._bg.set("fill", me._style.getColor("bg_color"));
|
||||
|
||||
return me;
|
||||
}
|
||||
};
|
||||
|
||||
# A button
|
||||
DefaultStyle.widgets["combo-box"] = {
|
||||
new: func(parent, cfg)
|
||||
{
|
||||
me._root = parent.createChild("group", "combo-box");
|
||||
me._bg =
|
||||
me._root.createChild("path");
|
||||
me._border =
|
||||
me._root.createChild("image", "border")
|
||||
.set("slice", "10 6"); #"7")
|
||||
me._buttonBorder =
|
||||
me._root.createChild("image", "border-button")
|
||||
.set("slice", "10 6"); #"7")
|
||||
me._arrowIcon = me._root.createChild("image", "arrow");
|
||||
me._label =
|
||||
me._root.createChild("text")
|
||||
.set("font", "LiberationFonts/LiberationSans-Regular.ttf")
|
||||
.set("character-size", 14)
|
||||
.set("alignment", "left-center");
|
||||
},
|
||||
setSize: func(model, w, h)
|
||||
{
|
||||
var halfWidth = int(w * 0.5);
|
||||
me._bg.reset()
|
||||
.rect(3, 3, w - 6, h - 6, {"border-radius": 5});
|
||||
|
||||
# we split the two pieces
|
||||
me._border.setSize(halfWidth, h);
|
||||
me._buttonBorder.setTranslation(halfWidth, 0);
|
||||
me._buttonBorder.setSize(w - halfWidth, h);
|
||||
|
||||
var arrowSize = me._arrowIcon.imageSize();
|
||||
me._arrowIcon.setTranslation(w - (arrowSize[0] + 20), (h - arrowSize[1]) * 0.5);
|
||||
|
||||
me._label.setTranslation(20, h * 0.5);
|
||||
},
|
||||
setText: func(model, text)
|
||||
{
|
||||
me._label.setText(text);
|
||||
|
||||
var min_width = math.max(80, me._label.maxWidth() + 16 + me._arrowIcon.imageSize()[0]);
|
||||
model.setLayoutMinimumSize([min_width, 16]);
|
||||
model.setLayoutSizeHint([min_width, 28]);
|
||||
|
||||
return me;
|
||||
},
|
||||
update: func(model)
|
||||
{
|
||||
var backdrop = !model._windowFocus();
|
||||
var (w, h) = model._size;
|
||||
var file = me._style._dir_widgets ~ "/";
|
||||
|
||||
# TODO unify color names with image names
|
||||
var bg_color_name = "button_bg_color";
|
||||
if( backdrop )
|
||||
bg_color_name = "button_backdrop_bg_color";
|
||||
else if( !model._enabled )
|
||||
bg_color_name = "button_bg_color_insensitive";
|
||||
else if( model._down )
|
||||
bg_color_name = "button_bg_color_down";
|
||||
else if( model._hover )
|
||||
bg_color_name = "button_bg_color_hover";
|
||||
me._bg.set("fill", me._style.getColor(bg_color_name));
|
||||
|
||||
var arrowIconFile = file ~ "combobox-arrow";
|
||||
|
||||
if( backdrop )
|
||||
{
|
||||
file ~= "backdrop-";
|
||||
me._label.set("fill", me._style.getColor("backdrop_fg_color"));
|
||||
}
|
||||
else
|
||||
me._label.set("fill", me._style.getColor("fg_color"));
|
||||
file ~= "combobox";
|
||||
|
||||
var buttonFile = file ~ "-button";
|
||||
file ~= "-entry";
|
||||
|
||||
var suffix = "";
|
||||
if( model._down )
|
||||
{ # no pressed image for the left half
|
||||
buttonFile ~= "-pressed";
|
||||
}
|
||||
|
||||
if( model._enabled ) {
|
||||
if( model._focused and !backdrop )
|
||||
suffix ~= "-focused";
|
||||
} else {
|
||||
suffix ~= "-disabled";
|
||||
arrowIconFile ~= "-disabled";
|
||||
}
|
||||
|
||||
me._border.set("src", file ~ suffix ~ ".png");
|
||||
me._buttonBorder.set("src", buttonFile ~ suffix ~ ".png");
|
||||
me._arrowIcon.set("src", arrowIconFile ~ ".png");
|
||||
}
|
||||
};
|
||||
|
||||
DefaultStyle.widgets["list-item"] = {
|
||||
new: func(parent, cfg) {
|
||||
me._root = parent.createChild("group", "list-item");
|
||||
me._bg = me._root.createChild("path");
|
||||
|
||||
me._label = me._root.createChild("text")
|
||||
.set("font", "LiberationFonts/LiberationSans-Regular.ttf")
|
||||
.set("character-size", 14)
|
||||
.set("alignment", "left-baseline");
|
||||
},
|
||||
|
||||
setSize: func(model, w, h) {
|
||||
me._bg.reset().rect(0, 0, w, 24);
|
||||
me._label.setTranslation(5, int(h / 2) + 4);
|
||||
return me;
|
||||
},
|
||||
|
||||
_updateLayoutSizes: func(model) {
|
||||
var min_width = 5 + me._label.maxWidth() + 5;
|
||||
model.setLayoutMinimumSize([min_width, 24]);
|
||||
model.setLayoutSizeHint([min_width, 24]);
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
setText: func(model, text) {
|
||||
me._label.setText(text);
|
||||
return me._updateLayoutSizes(model);
|
||||
},
|
||||
|
||||
update: func(model) {
|
||||
me._bg.set("fill", me._style.getColor("list_item_bg" ~ (model._selected ? "_selected" : "")));
|
||||
var text_color_name = "list_item_fg";
|
||||
if (model._selected) {
|
||||
text_color_name ~= "_selected";
|
||||
}
|
||||
me._label.set("fill", me._style.getColor(text_color_name));
|
||||
|
||||
return me;
|
||||
}
|
||||
};
|
||||
|
||||
DefaultStyle.widgets.list = {
|
||||
new: func(parent, cfg) {
|
||||
me._root = parent.createChild("group", "list");
|
||||
me._bg = me._root.createChild("path");
|
||||
},
|
||||
|
||||
setSize: func(model, w, h) {
|
||||
me._bg.reset().rect(0, 0, w, h);
|
||||
return me;
|
||||
},
|
||||
|
||||
update: func(model) {
|
||||
me._bg.set("fill", me._style.getColor("bg_color"));
|
||||
|
||||
|
|
291
Nasal/canvas/gui/widgets/List.nas
Normal file
291
Nasal/canvas/gui/widgets/List.nas
Normal file
|
@ -0,0 +1,291 @@
|
|||
# List.nas - a list widget similar to Qt's QListWidget
|
||||
|
||||
# SPDX-FileCopyrightText: (C) 2022 Frederic Croix <thefgfseagle@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
gui.widgets.ListItem = {
|
||||
new: func(parent, style = nil, cfg = nil) {
|
||||
var m = gui.Widget.new(gui.widgets.ListItem);
|
||||
style = style or canvas.style;
|
||||
m._cfg = Config.new(cfg or {});
|
||||
m._focus_policy = m.NoFocus;
|
||||
|
||||
m._data = m._cfg.get("data", {});
|
||||
m._text = m._cfg.get("text", "");
|
||||
m._selected = m._cfg.get("selected", 0);
|
||||
m._list = nil;
|
||||
|
||||
m._setView(style.createWidget(parent, "list-item", m._cfg));
|
||||
|
||||
m.setLayoutMinimumSize([48, 24]);
|
||||
m.setLayoutMaximumSize([m._MAX_SIZE, 24]);
|
||||
|
||||
m.setText(m._text);
|
||||
m.setSelected(m._selected);
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
# @description Set the data for this item.
|
||||
# @param key Union[scalar, hash] required If @param key is a hash, this item's data is replaced with that hash.
|
||||
# If @param key is a scalar, the data field with that name will be set to value.
|
||||
# If @param key is anything else, an error will be raised.
|
||||
# @param value Any optional The value to set the data field with key @param key to, if @param key is a scalar;
|
||||
# otherwise, this argument is ignored.
|
||||
# @return canvas.gui.widgets.ListItem This list item to support method chaining.
|
||||
setData: func(key, value = nil) {
|
||||
if (isscalar(key)) {
|
||||
me._data[key] = value;
|
||||
} elsif (ishash(key)) {
|
||||
me._data = key;
|
||||
} else {
|
||||
die("cannot set data field with non-scalar key !");
|
||||
}
|
||||
return me;
|
||||
},
|
||||
|
||||
# @description Get the data of this item.
|
||||
# @param key Union[scalar, nil] The scalar key of the data field to return the value of, or nil to return the whole data.
|
||||
# @return Any If @param key is a scalar, the value of the field with key @param key, else the whole data as a hash.
|
||||
getData: func(key = nil) {
|
||||
if (key != nil) {
|
||||
if (!isscalar(key)) {
|
||||
die("cannot get data field with non-scalar key !")
|
||||
}
|
||||
return me._data[key];
|
||||
} else {
|
||||
return me._data;
|
||||
}
|
||||
},
|
||||
|
||||
# @description Clear data
|
||||
# @return canvas.gui.widgets.ListItem This list item to support method chaining.
|
||||
clearData: func {
|
||||
me._data = {};
|
||||
return me;
|
||||
},
|
||||
|
||||
setText: func(text) {
|
||||
me._text = text;
|
||||
me._view.setText(me, text);
|
||||
return me;
|
||||
},
|
||||
|
||||
getText: func {
|
||||
return me._text;
|
||||
},
|
||||
|
||||
setSelected: func(selected = 1) {
|
||||
if (me.getParent() != nil) {
|
||||
me.getParent().setItemSelection(me, selected);
|
||||
}
|
||||
return me;
|
||||
},
|
||||
|
||||
getSelected: func {
|
||||
return me._selected;
|
||||
},
|
||||
|
||||
_setSelected: func(selected) {
|
||||
if (selected != me._selected) {
|
||||
me._selected = selected;
|
||||
me.update();
|
||||
me._trigger("selection-state-changed", {"selected": selected});
|
||||
if (me._selected) {
|
||||
me._trigger("selected");
|
||||
} else {
|
||||
me._trigger("unselected");
|
||||
}
|
||||
}
|
||||
return me;
|
||||
},
|
||||
|
||||
_setView: func(view) {
|
||||
call(gui.Widget._setView, [view], me);
|
||||
|
||||
var el = view._root;
|
||||
el.addEventListener("click", func me.setSelected());
|
||||
},
|
||||
|
||||
update: func {
|
||||
me._view.update(me);
|
||||
return me;
|
||||
},
|
||||
};
|
||||
|
||||
gui.widgets.List = {
|
||||
new: func(parent, style = nil, cfg = nil) {
|
||||
var m = gui.Widget.new(gui.widgets.List);
|
||||
m._style = style = style or canvas.style;
|
||||
m._cfg = Config.new(cfg or {});
|
||||
m._focus_policy = m.NoFocus;
|
||||
|
||||
m._setView(style.createWidget(parent, "list", m._cfg));
|
||||
|
||||
m._scroll = gui.widgets.ScrollArea.new(m._view._root, style, {});
|
||||
m._scrollLayout = VBoxLayout.new();
|
||||
m._scroll.setLayout(m._scrollLayout);
|
||||
m._scrollLayout.setSpacing(0);
|
||||
|
||||
m.setLayoutMinimumSize([48, 24]);
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
setSize: func {
|
||||
if (size(arg) == 1) {
|
||||
var arg = arg[0];
|
||||
}
|
||||
var (x, y) = arg;
|
||||
me._size = [x, y];
|
||||
me._scroll.setSize(x, y);
|
||||
return me.update();
|
||||
},
|
||||
|
||||
_onItemSelectionStateChanged: func {
|
||||
var items = me.getSelectedItems();
|
||||
me._trigger("selection-changed");
|
||||
},
|
||||
|
||||
# @description Add the given item to this list.
|
||||
# @param item canvas.gui.widgets.ListItem required The item to be added to this list.
|
||||
# @return canvas.gui.widgets.List This list to support method chaining.
|
||||
addItem: func(item) {
|
||||
item.listen("selection-state-changed", func me._onItemSelectionStateChanged());
|
||||
me._scrollLayout.addItem(item);
|
||||
item.setParent(me);
|
||||
return me;
|
||||
},
|
||||
|
||||
# @description Create an item with the given text and optionally the given config.
|
||||
# @param text str required Text of the new item.
|
||||
# @param cfg hash optional Additional configuration of the item.
|
||||
# @return canvas.gui.widgets.ListItem The created list item.
|
||||
createItem: func(text, cfg = nil) {
|
||||
cfg = cfg or {};
|
||||
cfg["text"] = text;
|
||||
var item = gui.widgets.ListItem.new(me._scroll.getContent(), me._style, cfg);
|
||||
me.addItem(item);
|
||||
return item;
|
||||
},
|
||||
|
||||
# @description Count the items of this list.
|
||||
# @return int Number of items in this list.
|
||||
count: func {
|
||||
return me._scrollLayout.count();
|
||||
},
|
||||
|
||||
# @description Remove all items from this list.
|
||||
# @return canvas.gui.widgets.List This list to support method chaining.
|
||||
clear: func {
|
||||
me._scrollLayout.clear();
|
||||
return me;
|
||||
},
|
||||
|
||||
# @description Get the item with the given index or text
|
||||
# @param text_or_index Union[int, str] required If text_or_index is an integer, the item at index text_or_index is returned.
|
||||
# If text_or_index is a string, the item with text == text_or_index is returned.
|
||||
# @return canvas.gui.widgets.ListItem The item with the given text or index, or nil if no such item is found.
|
||||
getItem: func(text_or_index) {
|
||||
if (isint(text_or_index)) {
|
||||
return me._scrollLayout.itemAt(text_or_index);
|
||||
} else {
|
||||
for (var i = 0; i < me.count(); i += 1) {
|
||||
if (me._scrollLayout.itemAt(i)._text == text_or_index) {
|
||||
return me.getItem(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
# @description Find the index of the given item or item with the given text.
|
||||
# @param text_or_item Union[str, canvas.gui.widgets.ListItem] required The item or text of the item to return the index of.
|
||||
# @return int The index of the given item or item with the given text, or -1 if the given item or no item with the given text is found.
|
||||
findItem: func(text_or_item) {
|
||||
for (var i = 0; i < me.count(); i += 1) {
|
||||
if (
|
||||
(isstr(text_or_item) and me._scrollLayout.itemAt(i)._text == text_or_index) or
|
||||
text_or_item == me.getItem(i)
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
# @description Remove and return the item with the given text or index.
|
||||
# @param text_or_item Union[str, int] required The text or index of the item to remove and return
|
||||
# @return int The item with the given text or index, or nil if no item with the given text or index is found.
|
||||
takeItem: func(text_or_index) {
|
||||
var index = text_or_index;
|
||||
if (!isint(index)) {
|
||||
index = me.findItem(index);
|
||||
if (index < 0) {
|
||||
return nil;
|
||||
}
|
||||
} elsif (index < 0) {
|
||||
return nil;
|
||||
}
|
||||
return me._scrollLayout.takeAt(index);
|
||||
},
|
||||
|
||||
# @description Remove the item with the given text or index or given item.
|
||||
# @param text_or_index_or_item Union[str, int, canvas.gui.widgets.ListItem] required The text or index of the item, or the item, to remove.
|
||||
# @return canvas.gui.widgets.List This list to support method chaining.
|
||||
removeItem: func(text_or_index_or_item) {
|
||||
if (isscalar(text_or_index_or_item)) {
|
||||
me.takeItem(text_or_index_or_item);
|
||||
} else {
|
||||
me._scrollLayout.removeItem(item);
|
||||
}
|
||||
return me;
|
||||
},
|
||||
|
||||
# @description Unselect all items of this list.
|
||||
# @return canvas.gui.widgets.List This list to support method chaining.
|
||||
clearSelection: func {
|
||||
for (var i = 0; i < me.count(); i += 1) {
|
||||
me.getItem(i)._selected = 0;
|
||||
me.getItem(i).update();
|
||||
}
|
||||
return me;
|
||||
},
|
||||
|
||||
# @description Set the selected state of the item item with the given index or text.
|
||||
# @param text_or_index Union[int, str] required If text_or_index is an integer, the item at index text_or_index is returned.
|
||||
# If text_or_index is a string, the item with text == text_or_index is returned.
|
||||
# @param selected bool optional The selection state of the item, defaults to selected if not given.
|
||||
# @return canvas.gui.widgets.List This list to support method chaining.
|
||||
setItemSelection: func(text_or_index_or_item, selected = 1) {
|
||||
var item = text_or_index_or_item;
|
||||
if (typeof(item) == "scalar") {
|
||||
item = me.getItem(text_or_index_or_item);
|
||||
if (!item) {
|
||||
die("no item found with given text or index '" ~ text_or_index_or_item ~ "' found !");
|
||||
}
|
||||
}
|
||||
me.clearSelection();
|
||||
item._setSelected(selected);
|
||||
return me;
|
||||
},
|
||||
|
||||
# @description Get a vector containing all selected items of this list
|
||||
# @return vector[canvas.gui.widgets.ListItem] Vector containing the selected items
|
||||
getSelectedItems: func {
|
||||
var items = [];
|
||||
for (var i = 0; i < me.count(); i += 1) {
|
||||
var item = me.getItem(i);
|
||||
if (item._selected) {
|
||||
append(items, item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
update: func {
|
||||
me._scroll.update();
|
||||
me._view.update(me);
|
||||
return me;
|
||||
}
|
||||
}
|
||||
|
|
@ -185,5 +185,29 @@
|
|||
<green type="float">0.945</green>
|
||||
<blue type="float">0.941</blue>
|
||||
</menu_item_submenu_indicator_hovered>
|
||||
|
||||
<list_item_fg>
|
||||
<red type="float">0.298</red>
|
||||
<green type="float">0.298</green>
|
||||
<blue type="float">0.298</blue>
|
||||
</list_item_fg>
|
||||
|
||||
<list_item_fg_selected>
|
||||
<red type="float">0.95</red>
|
||||
<green type="float">0.95</green>
|
||||
<blue type="float">0.95</blue>
|
||||
</list_item_fg_selected>
|
||||
|
||||
<list_item_bg>
|
||||
<red type="float">0.949</red>
|
||||
<green type="float">0.945</green>
|
||||
<blue type="float">0.941</blue>
|
||||
</list_item_bg>
|
||||
|
||||
<list_item_bg_selected>
|
||||
<red type="float">0.15</red>
|
||||
<green type="float">0.15</green>
|
||||
<blue type="float">1</blue>
|
||||
</list_item_bg_selected>
|
||||
</colors>
|
||||
</PropertyList>
|
||||
|
|
Loading…
Add table
Reference in a new issue