Difference between revisions of "Sandbox/GUI/Getting Started"
m (→Simple) |
m (→Adding a little flavor) |
||
(114 intermediate revisions by the same user not shown) | |||
Line 3: | Line 3: | ||
==Introduction== | ==Introduction== | ||
− | This guide is designed to help you get a simple Wesnoth GUI, implemented in lua, up and running while describing the basic building blocks along the way. | + | This guide is designed to help you get a simple Wesnoth GUI, implemented in lua, up and running while describing the basic building blocks along the way. It is written in a narrative format, where most examples build on previous examples, and therefore while not always necessary it may be desirable to read from start to finish. The reader, probably a UMC author, should have a basic knowledge of working with lua within Wesnoth. |
Some would find creating a GUI in part or in full using WML simpler to follow, and those alternatives are available, for example [[LuaAPI/gui/example]], but we're using lua here. If you find WML easier to follow, you can always convert the lua tables that define the GUIs to WML using [[LuaAPI/wml#wml.tostring|wml.tostring]], for example: | Some would find creating a GUI in part or in full using WML simpler to follow, and those alternatives are available, for example [[LuaAPI/gui/example]], but we're using lua here. If you find WML easier to follow, you can always convert the lua tables that define the GUIs to WML using [[LuaAPI/wml#wml.tostring|wml.tostring]], for example: | ||
Line 12: | Line 12: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | In some examples, instead of defining the entire | + | In the comments of our first GUI, below, is an example which loads a WML GUI configuration using [[LuaAPI/wml#wml.load|wml.load()]], validating against the GUI2 window schema. |
+ | |||
+ | In some examples, instead of defining the entire GUI at once, we'll break out some parts into separate variables. If you try to view one in WML and get an error from wml.tostring() about expecting a WML table and not a table, try passing your variable(/table) inside a table: | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
Line 19: | Line 21: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | ===What is GUI2?=== | |
+ | Once upon a time, Wesnoth had a GUI system which is now commonly referred to as GUI1. As of 1.19, GUI1 has been almost completely replaced by GUI2. UMC authors will probably never encounter GUI1 and can simply use the terms GUI and GUI2 interchangeably, at least until GUI3 comes along. | ||
+ | |||
+ | Instead of spending a lot of time here giving an overview of what GUI2 is and what makes it great, and not so great, let's charge ahead so we can see it in action, and let readers who are interested look elsewhere(TODO: link) for a more formal definition. (TODO: is this the right approach for most readers?). | ||
==Getting Started== | ==Getting Started== | ||
Line 27: | Line 32: | ||
===A most basic GUI=== | ===A most basic GUI=== | ||
− | [[File:Most basic gui.png|thumb]] | + | {| |
− | + | |[[File:Most basic gui.png|center|thumb|I can't believe this worked]] | |
− | In our prestart event, we create a simple menu item, which executes a single line of lua which calls the lua function most_basic_gui() which is found in gui_tutorial.lua: | + | |- |
+ | |In our prestart event, we create a simple menu item, which executes a single line of lua which calls the lua function most_basic_gui() which is found in gui_tutorial.lua: | ||
+ | |} | ||
<syntaxhighlight lang=wml> | <syntaxhighlight lang=wml> | ||
Line 59: | Line 66: | ||
wml.tag.row { -- a grid must include at least one row | wml.tag.row { -- a grid must include at least one row | ||
wml.tag.column { -- a row needs a column | wml.tag.column { -- a row needs a column | ||
− | wml.tag.image { -- | + | wml.tag.image { -- a column includes exactly one widget |
label = "units/trolls/grunt.png" | label = "units/trolls/grunt.png" | ||
} | } | ||
Line 74: | Line 81: | ||
-- Or, if you want to define the gui in WML, something like: | -- Or, if you want to define the gui in WML, something like: | ||
− | -- local dialog_wml = wml.load "~add-ons/GUI_Tutorial/most_basic_gui.cfg" | + | -- local dialog_wml = wml.load("~add-ons/GUI_Tutorial/most_basic_gui.cfg", |
+ | -- true, "schema/gui_window.cfg") | ||
-- gui.show_dialog(wml.get_child(dialog_wml, 'resolution')) | -- gui.show_dialog(wml.get_child(dialog_wml, 'resolution')) | ||
end | end | ||
Line 104: | Line 112: | ||
Our function creates a table containing the definition of a "dialog", and then passes that to [[LuaAPI/gui#gui.show_dialog|gui.show_dialog()]]. It should be noted that gui.show_dialog does not provide synchronization, so if your GUI makes changes to the game state, you'll need to jump through some hoops or you'll break replays and multiplayer, but that's not something we need to care about at this point, so just be aware that you may need to deal with it in the future. | Our function creates a table containing the definition of a "dialog", and then passes that to [[LuaAPI/gui#gui.show_dialog|gui.show_dialog()]]. It should be noted that gui.show_dialog does not provide synchronization, so if your GUI makes changes to the game state, you'll need to jump through some hoops or you'll break replays and multiplayer, but that's not something we need to care about at this point, so just be aware that you may need to deal with it in the future. | ||
− | Our dialog definition at this point includes three parts. The first two are a tooltip and a helptip, which are required and define how tooltips (use id = "tooltip_large" or id = "tooltip" or id = "tooltip_transparent"), and helptips (rarely used, but must be included here, and yes their definitions are "tooltip" not "helptip") will look. The third part is a grid, which is basically a table, like in HTML or MySQL, with rows and columns. A grid always contains at least one row. A row contains at least one column (note that a column does NOT span rows, it is completely contained within a row). Inside a column is exactly one item, a cell which contains a [[GUIWidgetDefinitionWML|widget]], in this case we'll use an image (later we'll see what else we can put in a cell). Note that we don't actually define a cell in our code, it's just a term used to refer to the contents of a column. | + | Our dialog definition at this point includes three parts. The first two are a tooltip and a helptip, which are required and define how tooltips (use id = "tooltip_large" or id = "tooltip" or id = "tooltip_transparent"), and helptips (rarely used, but must be included here, and yes their definitions are "tooltip" not "helptip") will look (as we will see later, this really should be definition = "tooltip", but in this case it's id = "tooltip"). The third part is a grid, which is basically a table, like in HTML or MySQL, with rows and columns. A grid always contains at least one row. A row contains at least one column (note that a column does NOT span rows, it is completely contained within a row). Inside a column is exactly one item, a cell which contains a [[GUIWidgetDefinitionWML|widget]], in this case we'll use an image (later we'll see what else we can put in a cell). Note that we don't actually define a cell in our code, it's just a term used to refer to the contents of a column. |
The preshow function is optional. If included in the call to [[LuaAPI/gui#gui.show_dialog|gui.show_dialog()]], it is run before the GUI is displayed. In this case, we include it to display the dialog we created with lua in WML format. Near the end, in comments, we show how you would call [[LuaAPI/gui#gui.show_dialog|gui.show_dialog()]] if you chose to define your dialog using WML. Note that what is shown here is the WML equivalent of our lua dialogDefinition, and not the complete WML you would need to use if you were to configure your GUI layout in WML. | The preshow function is optional. If included in the call to [[LuaAPI/gui#gui.show_dialog|gui.show_dialog()]], it is run before the GUI is displayed. In this case, we include it to display the dialog we created with lua in WML format. Near the end, in comments, we show how you would call [[LuaAPI/gui#gui.show_dialog|gui.show_dialog()]] if you chose to define your dialog using WML. Note that what is shown here is the WML equivalent of our lua dialogDefinition, and not the complete WML you would need to use if you were to configure your GUI layout in WML. | ||
Line 111: | Line 119: | ||
===Adding a little flavor=== | ===Adding a little flavor=== | ||
− | + | ||
− | You may have noticed our GUI is missing a header and a way to close it when we're done admiring our work. Here we add a new first row to our grid, while demonstrating a couple formatting options: a border to put some distance between our grid cell and its neighbor, and the "use_markup = true" option to enable Pango support in our text. | + | {| |
+ | |You may have noticed our GUI is missing a header and a way to close it when we're done admiring our work. Here we add a new first row to our grid, while demonstrating a couple formatting options: a border to put some distance between our grid cell and its neighbor, and the "use_markup = true" option to enable Pango support in our text. | ||
Our third row adds an OK button at the bottom. You can associate actions with buttons to do all kinds of things, but here we just exploit the default behaviour to close the GUI. | Our third row adds an OK button at the bottom. You can associate actions with buttons to do all kinds of things, but here we just exploit the default behaviour to close the GUI. | ||
Of course, we'll also have to modify our return to support the new function, and update our menu item(s) accordingly. | Of course, we'll also have to modify our return to support the new function, and update our menu item(s) accordingly. | ||
− | + | |- | |
+ | |[[File:Most basic gui2.png|center|thumb|Adding header and a footer]] | ||
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local function most_basic_gui2() | local function most_basic_gui2() | ||
Line 145: | Line 156: | ||
wml.tag.row { | wml.tag.row { | ||
wml.tag.column { -- An "OK" button, with no action assigned for now, | wml.tag.column { -- An "OK" button, with no action assigned for now, | ||
− | -- but it will close the | + | -- but it will close the GUI |
wml.tag.button { | wml.tag.button { | ||
id = "ok", | id = "ok", | ||
Line 154: | Line 165: | ||
} | } | ||
} | } | ||
+ | wml.parse(wml.tostring({wml.tag.resolution(dialogDefinition)}), "schema/gui_window.cfg") -- schema validation errors will go to log | ||
gui.show_dialog(dialogDefinition) | gui.show_dialog(dialogDefinition) | ||
end | end | ||
return { most_basic_gui = most_basic_gui, most_basic_gui2 = most_basic_gui2 } | return { most_basic_gui = most_basic_gui, most_basic_gui2 = most_basic_gui2 } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | Note that in this example, just before we invoke gui.show_dialog(), we use [[LuaAPI/wml#wml.parse|wml.parse()]] to validate our input against the GUI2 window schema. If we've misspelled something, or placed a key in the wrong place (like placing a border in a widget instead of a column), etc, an error such as '''error validation: Invalid key 'garbage=' in tag [resolution]''' will be written to the wesnoth log. | ||
===And a bit more=== | ===And a bit more=== | ||
− | + | {| | |
− | |||
+ | |And now a minor, but important change. We want to add a new text field next to the image in the body of our GUI. Obviously, we want to add a new column, but this is more difficult than when we added new rows in the previous example. The problem is that all rows (at a given level) in a grid must contain the same number of columns. We can't have two columns in the row that constitutes the body of our GUI, but only one in the header and one in the footer. To solve this problem, we replace our image widget with a new grid, which can use as many columns as we like (as long as they are the same within each row of our new grid). This new grid contains a row with two columns, but that is okay because the new grid itself is placed in a single column, which matches our single column header and footer (ok button), keeping the rows balanced. | ||
+ | |- | ||
+ | |[[File:Most basic gui3.png|center|thumb|Rows with different numbers of columns]] | ||
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local function most_basic_gui3() | local function most_basic_gui3() | ||
Line 185: | Line 202: | ||
-- to match the number of columns in | -- to match the number of columns in | ||
-- the rows of our header and footer | -- the rows of our header and footer | ||
− | wml.tag.grid { -- A new grid, so we can use a different number of columns | + | wml.tag.grid { -- A new grid, so we can use a different |
+ | -- number of columns | ||
wml.tag.row { | wml.tag.row { | ||
wml.tag.column { | wml.tag.column { | ||
Line 203: | Line 221: | ||
wml.tag.row { -- A footer | wml.tag.row { -- A footer | ||
wml.tag.column { -- An "OK" button, with no action assigned for now, | wml.tag.column { -- An "OK" button, with no action assigned for now, | ||
− | -- but it will close the | + | -- but it will close the GUI |
wml.tag.button { | wml.tag.button { | ||
id = "ok", | id = "ok", | ||
Line 215: | Line 233: | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | While we see here just the simplest of examples, you can create many levels of grids inside grids, rows with multiple columns some of which containing grids, etc, to build complicated dialogs to your liking. | ||
==Containers== | ==Containers== | ||
Line 223: | Line 243: | ||
===Listbox=== | ===Listbox=== | ||
− | + | {| | |
− | Now we'd like to add some more monsters. Obviously, we could just add more rows, but what if we won't know until runtime how many and which ones? We could break up the definition of our dialog and add new rows dynamically, it's just a table after all, but fortunately we have a widget which handles this for us, a listbox. A listbox is kind of like an array, a collection of similar objects where the number of items can vary. We will tell the GUI where we want the box, and what each entry in the box will look like, and then at runtime we can add entries to the box. | + | | Now we'd like to add some more monsters. Obviously, we could just add more rows, but what if we won't know until runtime how many and which ones? We could break up the definition of our dialog and add new rows dynamically, it's just a table after all, but fortunately we have a widget which handles this for us, a listbox. A listbox is kind of like an array, a collection of similar objects where the number of items can vary. We will tell the GUI where we want the box, and what each entry in the box will look like, and then at runtime we can add entries to the box. |
+ | |||
+ | Note that listbox items are added from top to bottom. Another container, the horizontal_listbox, works just like a listbox except the items are added from left to right. With a grid listbox, items are added horizontally with the list wrapping to a new line as necessary. | ||
+ | |- | ||
+ | | [[File:Gui with listbox.png|center|thumb|Listbox with three items. Each item is an image and a label. The third item has been selected.]] | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
Line 325: | Line 349: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Which we can read as "In listbox element i of our listbox, for the image with identifier monster_image which we defined in our listboxItem, use the data from the corresponding index i in our example data table". If you were to step through all of the variable substitutions, you'd see that for i=1, our dialogDefinition is basically the same thing as it was in the earlier examples, just supplied from a table instead of hardcoded (note that you can hardcode entries in a listbox in your dialog definition using the [list_data] tag, aka wml.tag.list_data. | + | Which we can read as "In listbox element i of our listbox, for the image with identifier monster_image which we defined in our listboxItem, use the data from the corresponding index i in our example data table" (you don't see the index i for the monsters table here, but remember we're iterating using ipairs, we could have just as easily used listbox[i].monster_image.label = monsters[i].image). If you were to step through all of the variable substitutions, you'd see that for i=1, our dialogDefinition is basically the same thing as it was in the earlier examples, just supplied from a table instead of hardcoded (note that you can hardcode entries in a listbox in your dialog definition using the [list_data] tag, aka wml.tag.list_data, which we do not demonstrate here). |
− | You will probably notice that our data does not line up nicely in the GUI. We will fix that later. We'll also demonstrate how the user can select | + | You will probably notice that our data does not line up nicely in the GUI. We will fix that later. We'll also demonstrate how the user can select an item in a listbox, and how you can identify which item that was using the selected_index variable of the listbox. |
===Tree View=== | ===Tree View=== | ||
====Simple Tree View==== | ====Simple Tree View==== | ||
− | {| | + | {| |
− | | | + | | You may have noticed that clicking on a listbox item in our listbox caused it to be highlighted with a box around the contents. This is because the items in a listbox are selectable. This will be useful when we want to add actions, but it looks a bit odd if we just want to present a list of data. |
− | You may have noticed that clicking on a listbox item in our listbox caused it to be highlighted with a box around the contents. This is because the items in a listbox are selectable. This will be useful when we want to add actions, but it looks a bit odd if we just want to present a list of data. | ||
Also, most of the data had to be formatted the same for each item in the listbox. We could, perhaps, include custom markup in our labels, but the actual layout, like the number of columns per row, had to be the same for every item. | Also, most of the data had to be formatted the same for each item in the listbox. We could, perhaps, include custom markup in our labels, but the actual layout, like the number of columns per row, had to be the same for every item. | ||
Another way to present a list of data is the tree view. Since a tree view supports multiple data types, we may need to create multiple definitions for the elements in our list. These are known as nodes. It will also be necessary to explicitly define which node to use for each element when we populate our tree view. | Another way to present a list of data is the tree view. Since a tree view supports multiple data types, we may need to create multiple definitions for the elements in our list. These are known as nodes. It will also be necessary to explicitly define which node to use for each element when we populate our tree view. | ||
− | | | + | |- |
− | [[File:Basic tree view.png|thumb]] | + | | [[File:Basic tree view.png|center|thumb|Tree_views can hold multiple data types (only trolls have names here)]] |
|} | |} | ||
<syntaxhighlight lang=lua line> | <syntaxhighlight lang=lua line> | ||
Line 473: | Line 496: | ||
====Building a Tree==== | ====Building a Tree==== | ||
− | + | {| | |
− | + | | colspan="2" |At this point, one may wonder about the name "tree view", since what he have seen doesn't look much like a tree. To make a tree-like structure, we'll demonstrate adding children to nodes. At the top level our (upside-down) tree will have two nodes, one for trolls and one for everything else. A toggle_button (a type of button which changes states when you push it) will allow us to expand the trolls node to expose its children, which are themselves nodes, though they only contain a label at this point. | |
− | + | |- | |
− | At this point, one may wonder about the name "tree view", since what he have seen doesn't look much like a tree. To make a tree-like structure, we'll demonstrate adding children to nodes. At the top level our (upside-down) tree will have two nodes, one for trolls and one for everything else. A toggle_button (a type of button which changes states when you push it) will allow us to expand the trolls node to expose its children, which are themselves nodes, though they only contain a label at this point. | + | | [[File:Basic tree view2a.png|center|thumb|Tree_view with Trolls folded]] || [[File:Basic tree view2b.png|center|thumb|Tree_view with Trolls unfolded]] |
− | + | |} | |
<syntaxhighlight lang=lua line> | <syntaxhighlight lang=lua line> | ||
− | local function | + | local function building_a_tree() |
local tree_view = wml.tag.tree_view { | local tree_view = wml.tag.tree_view { | ||
id = "monsters_tv", | id = "monsters_tv", | ||
Line 545: | Line 568: | ||
local function preshow(dialog) -- Prepare the GUI before display | local function preshow(dialog) -- Prepare the GUI before display | ||
-- You can refer to an object by its position | -- You can refer to an object by its position | ||
+ | |||
-- dialog.monsters_tv:add_item_of_type("race_node") | -- dialog.monsters_tv:add_item_of_type("race_node") | ||
-- dialog.monsters_tv[1].race_label.label = "Trolls" | -- dialog.monsters_tv[1].race_label.label = "Trolls" | ||
− | -- dialog.monsters_tv[1].race_button.on_modified = function()dialog.monsters_tv[1].unfolded = | + | -- dialog.monsters_tv[1].race_button.on_modified = |
+ | -- function()dialog.monsters_tv[1].unfolded = | ||
-- dialog.monsters_tv[1].race_button.selected end | -- dialog.monsters_tv[1].race_button.selected end | ||
Line 561: | Line 586: | ||
-- dialog.monsters_tv[2].race_button.visible = "hidden" | -- dialog.monsters_tv[2].race_button.visible = "hidden" | ||
− | -- ... or you can refer to that object using the return value from add_item_of_type | + | -- ... or you can refer to that object using the return value |
+ | -- from add_item_of_type | ||
local troll_node = dialog.monsters_tv:add_item_of_type("race_node") | local troll_node = dialog.monsters_tv:add_item_of_type("race_node") | ||
troll_node.race_label.label = "Trolls" | troll_node.race_label.label = "Trolls" | ||
− | troll_node.race_button.on_modified = function()troll_node.unfolded = | + | troll_node.race_button.on_modified = |
− | + | function() | |
− | + | troll_node.unfolded = troll_node.race_button.selected | |
− | + | end | |
− | + | local item = troll_node:add_item_of_type("details_node") | |
− | + | item.monster_type.label = "Whelp" | |
− | + | item = troll_node:add_item_of_type("details_node") | |
− | + | item.monster_type.label = "Shaman" | |
− | + | item = troll_node:add_item_of_type("details_node") | |
+ | item.monster_type.label = "Troll" | ||
− | + | local not_troll_node = | |
− | + | dialog.monsters_tv:add_item_of_type("race_node") | |
− | + | not_troll_node.race_label.label = "Other scary things" | |
+ | not_troll_node.race_button.visible = "hidden" | ||
end | end | ||
Line 587: | Line 615: | ||
We demonstrate two different ways of creating and accessing items, the first (commented out) just using the order in which they are created, while in the second we capture the result of add_item_of_type() in a variable we can use to refer to the newly defined object. The first method is shown primarily to demonstrate the structure of the resulting objects. The second method is perhaps easier to follow, and is almost necessary when you start doing things like dynamically deleting nodes, so it is in most cases a better practice (of course, you probably won't want to use the same variable for each node like we did, and perhaps not one local to the preview function). | We demonstrate two different ways of creating and accessing items, the first (commented out) just using the order in which they are created, while in the second we capture the result of add_item_of_type() in a variable we can use to refer to the newly defined object. The first method is shown primarily to demonstrate the structure of the resulting objects. The second method is perhaps easier to follow, and is almost necessary when you start doing things like dynamically deleting nodes, so it is in most cases a better practice (of course, you probably won't want to use the same variable for each node like we did, and perhaps not one local to the preview function). | ||
− | We add a node, label it "Trolls", and add a callback to the button such that | + | We add a node, label it "Trolls", and add a callback to the button such that the children of the node will be visible (the node is "unfolded", a boolean value which defaults to false) when the button is checked (selected == true). Then we add three "details_node" nodes as children of this node. |
Our second node, "Other scary things" will remain empty for now, so we'll set the visible attribute on its button to "hidden" (which is like false, but with hidden the widget still takes up space). | Our second node, "Other scary things" will remain empty for now, so we'll set the visible attribute on its button to "hidden" (which is like false, but with hidden the widget still takes up space). | ||
− | This example is rather ugly, both in the hardwired code and the appearance of the resulting GUI, but it addresses a couple important aspects of how tree views work and how we might use them. Let's clean it up a bit. We'll add an indentation_step_size to the tree view, along with a spacer and [[#Alignment|horizontal_alignment]] to our details node, to make the labels line up nicely, and change the button [[#Definitions|definition]] to replace the checkbox with something that looks like it belongs there. | + | {| |
− | + | | This example is rather ugly, both in the hardwired code and the appearance of the resulting GUI, but it addresses a couple important aspects of how tree views work and how we might use them. Let's clean it up a bit. We'll add an indentation_step_size to the tree view, along with a spacer and [[#Alignment|horizontal_alignment]] to our details node, to make the labels line up nicely, and change the button [[#Definitions|definition]] to replace the checkbox with something that looks like it belongs there. We will look at methods for handling layout in more depth [[#Appearance|later]]. | |
− | [[File:Basic tree view2c.png|thumb]] | + | |- |
+ | | [[File:Basic tree view2c.png|center|thumb|Our tree_view with proper alignment]] | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
Line 642: | Line 671: | ||
===Multi_page=== | ===Multi_page=== | ||
− | ====Simple==== | + | ====Simple Multi_page==== |
+ | {| | ||
+ | |- | ||
Like a tree_view, a multi_page is a heterogeneous container which is dynamically populated, however, while a multi_page contains multiple elements (pages), only one page is displayed at any one time. Its purpose is perhaps best described by a couple of examples. | Like a tree_view, a multi_page is a heterogeneous container which is dynamically populated, however, while a multi_page contains multiple elements (pages), only one page is displayed at any one time. Its purpose is perhaps best described by a couple of examples. | ||
− | + | |- | |
− | [[File:Basic multipage.png|thumb]] | + | [[File:Basic multipage.png|center|thumb|Multi_page showing active page]] |
− | + | |} | |
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local function basic_multipage() | local function basic_multipage() | ||
− | + | local monsters = { | |
− | + | { image = "units/trolls/grunt.png", | |
− | + | label = "A troll", | |
− | + | name = _"Bob", type = "Trolls" }, | |
− | + | { image = "units/trolls/whelp.png", | |
− | + | label = "A troll whelp", | |
− | + | name = "Junior", type = "Trolls" }, | |
+ | { image = "units/trolls/shaman.png", | ||
+ | label = "A troll shaman", | ||
+ | name = _"Alice", type = "Trolls" }, | ||
+ | { image = "units/monsters/cuttlefish.png", | ||
+ | label = "A cuttlefish", | ||
+ | type = "Seamonsters" }, | ||
+ | { image = "units/monsters/yeti.png", | ||
+ | label = "A yeti", | ||
+ | type = "Coolers" } | ||
+ | } | ||
− | + | local multi_page = wml.tag.multi_page { | |
− | + | id = "monsters_mp", | |
− | + | wml.tag.page_definition { | |
− | + | id = "trolls_page", | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.label { | |
− | + | id = "monster_name" | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
+ | }, | ||
+ | wml.tag.column { | ||
+ | wml.tag.image { | ||
+ | id = "monster_image" | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.column { | ||
+ | wml.tag.label { | ||
+ | id = "monster_label" | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.page_definition { | ||
+ | id = "nottrolls_page", | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.image { | ||
+ | id = "monster_image", | ||
+ | linked_group = "monster_image" | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.column { | ||
+ | wml.tag.label { | ||
+ | id = "monster_label", | ||
+ | linked_group = "monster_label" | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | local dialogDefinition = { | ||
+ | wml.tag.tooltip { id = "tooltip_large" }, | ||
+ | wml.tag.helptip { id = "tooltip_large" }, | ||
+ | wml.tag.grid { | ||
+ | wml.tag.row { -- A header | ||
+ | wml.tag.column { | ||
+ | border = "bottom", | ||
+ | border_size = 10, | ||
+ | wml.tag.label { | ||
+ | use_markup = true, | ||
+ | label = "<span size='large'>" .. _"Here there be " .. | ||
+ | "<span color='yellow'>" .. | ||
+ | _"MONSTERS!" .. "</span></span>" | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.row { -- The body of our GUI | ||
+ | wml.tag.column { | ||
+ | multi_page | ||
} | } | ||
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { -- An "OK" button | |
− | + | wml.tag.button { | |
− | + | id = "ok", | |
− | + | label = _"OK" | |
− | + | }, | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
− | + | } | |
+ | } | ||
+ | } | ||
− | + | local function preshow(dialog) -- Prepare the GUI before display | |
− | + | for i, monster in ipairs(monsters) do | |
− | + | if monster.type == "Trolls" then | |
− | + | dialog.monsters_mp:add_item_of_type("trolls_page") | |
− | + | dialog.monsters_mp[i].monster_name.label = monster.name | |
− | + | else | |
− | + | dialog.monsters_mp:add_item_of_type("nottrolls_page") | |
− | + | end | |
− | + | dialog.monsters_mp[i].monster_image.label = monster.image | |
− | + | dialog.monsters_mp[i].monster_label.label = monster.label | |
− | + | end | |
− | + | --dialog.monsters_mp.selected_index = 5 | |
− | + | end | |
− | + | gui.show_dialog(dialogDefinition,preshow) | |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 751: | Line 792: | ||
====A More Useful Multi_page==== | ====A More Useful Multi_page==== | ||
− | |||
− | |||
− | |||
+ | {| | ||
+ | | colspan="2" | To demonstrate the usefulness of a multi_page, and highlight its difference from a tree_view, we'll add a listbox to allow us to select the page to view. We'll also need to add a little action to our GUI. | ||
+ | |- | ||
+ | | style="align:centered" |[[File:More useful multipage.png|center|thumb|User selected Troll]] || [[File:More useful multipage2.png|center|thumb|User selected Cuttlefish]] | ||
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local function more_useful_multi_page() | local function more_useful_multi_page() | ||
− | + | local monsters = { | |
− | + | { image = "units/trolls/grunt.png", | |
− | + | label = "A troll", name = _"Bob", | |
− | + | race = "Trolls", tip = "Your uncle" }, | |
− | + | { image = "units/trolls/whelp.png", | |
− | + | label = "A troll whelp", name = "Junior", | |
− | + | race = "Trolls", tip = "Your nephew" }, | |
+ | { image = "units/trolls/shaman.png", | ||
+ | label = "A troll shaman", name = _"Alice", | ||
+ | race = "Trolls", tip = "Your auntie" }, | ||
+ | { image = "units/monsters/cuttlefish.png", | ||
+ | label = "A cuttlefish", race = "Seamonsters", | ||
+ | tip = "Not a fish" }, | ||
+ | { image = "units/monsters/yeti.png", | ||
+ | label = "A yeti", race = "Coolers", | ||
+ | tip = "<span size='large' weight='bold'>ROAR!</span>" } | ||
+ | } | ||
− | + | local listbox_id = "monsters" | |
− | + | local listboxItem = wml.tag.grid { | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.image { | |
− | + | id = "monster_image" | |
− | |||
− | |||
} | } | ||
+ | } | ||
} | } | ||
+ | } | ||
− | + | local listboxDefinition = wml.tag.listbox { | |
− | + | id = listbox_id, | |
− | + | wml.tag.list_definition { | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.toggle_panel { | |
− | + | listboxItem | |
− | + | } | |
− | + | } | |
− | + | } | |
} | } | ||
+ | } | ||
− | + | local multi_page = wml.tag.multi_page { | |
− | + | id = "monsters_mp", | |
− | + | wml.tag.page_definition { | |
− | + | id = "trolls_page", | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.label { | |
− | + | id = "monster_name" | |
− | + | } | |
− | + | }, | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.image { | |
− | + | id = "monster_image" | |
− | + | } | |
− | } | + | }, |
+ | }, | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.label { | ||
+ | id = "monster_label" | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.page_definition { | ||
+ | id = "nottrolls_page", | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.image { | ||
+ | id = "monster_image" | ||
+ | } | ||
}, | }, | ||
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.label { | |
− | + | id = "monster_label" | |
− | + | } | |
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
+ | }, | ||
} | } | ||
+ | } | ||
− | + | local dialogDefinition = { | |
− | + | wml.tag.tooltip { id = "tooltip_large" }, | |
− | + | wml.tag.helptip { id = "tooltip_large" }, | |
− | + | wml.tag.grid { | |
− | + | wml.tag.row { -- A header | |
− | + | wml.tag.column { | |
− | + | border = "bottom", | |
− | + | border_size = 10, | |
− | + | wml.tag.label { | |
− | + | use_markup = true, | |
− | + | label = "<span size='large'>" .. _"Here there be " .. | |
− | + | "<span color='yellow'>" .. | |
− | + | _"MONSTERS!" .. "</span></span>" | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { -- The body of our GUI | |
− | + | wml.tag.column { | |
− | + | wml.tag.grid { | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
wml.tag.row { | wml.tag.row { | ||
− | wml.tag.column { | + | wml.tag.column { |
− | + | listboxDefinition | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.spacer { | |
+ | width = 30 | ||
} | } | ||
+ | }, | ||
+ | wml.tag.column { | ||
+ | multi_page | ||
+ | }, | ||
} | } | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { -- An "OK" button | ||
+ | wml.tag.button { | ||
+ | id = "ok", | ||
+ | label = _"OK" | ||
+ | }, | ||
} | } | ||
+ | } | ||
} | } | ||
− | + | } | |
− | + | local function preshow(dialog) -- Prepare the GUI before display | |
− | + | local listbox = dialog[listbox_id] | |
− | + | for i, monster in ipairs(monsters) do | |
− | + | listbox[i].monster_image.label = monster.image | |
− | + | listbox[i].monster_image.tooltip = monster.tip | |
− | + | if monster.type == "Trolls" then | |
− | + | dialog.monsters_mp:add_item_of_type("trolls_page") | |
− | + | dialog.monsters_mp[i].monster_name.label = monster.name | |
− | + | else | |
− | + | dialog.monsters_mp:add_item_of_type("nottrolls_page") | |
− | + | end | |
− | + | dialog.monsters_mp[i].monster_image.label = monster.image | |
− | + | dialog.monsters_mp[i].monster_image.tooltip = monster.tip | |
− | + | dialog.monsters_mp[i].monster_label.label = monster.label | |
− | + | end | |
− | + | local function switch_page() | |
− | + | dialog.monsters_mp.selected_index = listbox.selected_index | |
− | |||
end | end | ||
− | gui.show_dialog(dialogDefinition,preshow) | + | listbox.on_modified = switch_page |
+ | end | ||
+ | gui.show_dialog(dialogDefinition,preshow) | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 902: | Line 969: | ||
===Buttons=== | ===Buttons=== | ||
− | In this example, we'll create a couple buttons which provide the user a choice, and demonstrate how we get the results back where we can put them to use. | + | {| |
− | [[File:Basic return value.png|thumb]] | + | | colspan=2 | In this example, we'll create a couple buttons which provide the user a choice, use a handy confirmation popup to confirm that choice, and demonstrate how we get the results back where we can put them to use. |
− | [[File:Basic return value_confirm.png|thumb|The user selected Alice, let's confirm this critical choice]] | + | |- |
+ | | [[File:Basic return value.png|center|thumb|There can be only one]] || [[File:Basic return value_confirm.png|center|thumb|The user selected Alice, let's confirm this critical choice]] | ||
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local function basic_return_value() | local function basic_return_value() | ||
− | + | local dialogDefinition = { | |
− | + | wml.tag.tooltip { id = "tooltip_large" }, | |
− | + | wml.tag.helptip { id = "tooltip_large" }, | |
− | + | wml.tag.linked_group { | |
− | + | id = "leader_lg", | |
− | + | fixed_height = true, | |
− | + | }, | |
− | + | wml.tag.grid { | |
− | + | wml.tag.row { -- A header | |
− | + | wml.tag.column { | |
− | + | border = "bottom", | |
− | + | border_size = 10, | |
− | + | wml.tag.label { | |
− | + | use_markup = true, | |
− | + | label = "<span size='large'>" .. | |
+ | _"Which unit shall lead your army?" .. "</span>" | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.row { -- The body of our GUI | ||
+ | wml.tag.column { | ||
+ | wml.tag.grid { | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.grid { | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.label { label = "Alice" } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.image { | ||
+ | linked_group = "leader_lg", | ||
+ | label = "units/elves-wood/sylph.png" | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | wml.tag.row { | ||
+ | wml.tag.column { | ||
+ | wml.tag.button { | ||
+ | label = "Select", | ||
+ | return_value = 1 | ||
+ | } | ||
} | } | ||
+ | }, | ||
} | } | ||
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.spacer { width = 20 } | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.grid { | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.label { label = "Bob" } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.image { | |
− | + | linked_group = "leader_lg", | |
− | + | label = "units/human-loyalists/marshal.png" | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.button { | |
− | + | label = "Select", | |
− | + | return_value = 2 | |
− | + | } | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
+ | } | ||
} | } | ||
+ | } | ||
} | } | ||
+ | } | ||
} | } | ||
+ | } | ||
} | } | ||
− | + | } | |
− | + | local user_chose = gui.show_dialog(dialogDefinition) | |
+ | local leader | ||
+ | if user_chose == -1 then -- User closed the dialog by hitting the Enter key | ||
+ | leader = nil | ||
+ | end | ||
+ | if user_chose == -2 then -- User closed the dialog by hitting the Escape key | ||
+ | leader = nil | ||
+ | end | ||
− | + | if user_chose == 1 then | |
− | + | leader = "Alice" | |
− | end | + | end |
+ | if user_chose == 2 then | ||
+ | leader = "Bob" | ||
+ | end | ||
− | + | local confirmed_leader_choice | |
− | |||
− | |||
− | + | if leader ~= nil then | |
− | + | local query = _"You want " .. leader .. _" as your leader?" | |
− | + | confirmed_leader_choice = gui.confirm(query) | |
− | + | end | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if confirmed_leader_choice then | |
− | + | wesnoth.message("Great!") | |
− | + | else | |
− | + | wesnoth.message("Fine, lead them yourself!") | |
− | + | end | |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 1,028: | Line 1,096: | ||
===Slider and Textbox=== | ===Slider and Textbox=== | ||
+ | {| | ||
+ | | Here we see a couple methods of getting more dynamic input from the user, a slider and a textbox presented in one silly example. | ||
+ | |- | ||
+ | | [[File:Slider and textbox.png|center|thumb|Two ways of getting input from the user]] | ||
+ | |} | ||
− | |||
− | |||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
− | function | + | function slider_and_textbox() |
− | + | local dialogDefinition = { | |
− | + | wml.tag.tooltip { id = "tooltip_large" }, | |
− | + | wml.tag.helptip { id = "tooltip_large" }, | |
− | + | wml.tag.grid { | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.label { | |
− | + | label = _"How much gold will you pay for this old rusty sword?" | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.slider { | |
− | + | id = "gold_sl", | |
− | + | minimum_value = 0, | |
− | + | maximum_value = | |
− | + | wesnoth.sides[wesnoth.current.side].gold, | |
− | + | value = math.floor( | |
− | + | wesnoth.sides[wesnoth.current.side].gold/2) | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.text_box { | |
− | + | id = "gold_tb", | |
− | + | --hint_text = _ "Enter gold here", | |
− | + | --hint_image = "images/gold_pile.png", | |
− | + | label = tostring(math.floor( | |
− | + | wesnoth.sides[wesnoth.current.side].gold/2)) | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.button { | |
+ | id = "ok", | ||
+ | label = _"OK" | ||
+ | } | ||
} | } | ||
+ | } | ||
} | } | ||
− | + | } | |
− | + | local function preshow(dialog) | |
− | + | local function show_input() | |
− | + | wesnoth.interface.add_chat_message(_"You chose " .. | |
− | + | dialog.gold_sl.value .. _" with the slider") | |
− | + | wesnoth.interface.add_chat_message(_"You entered " .. | |
+ | tostring(dialog.gold_tb.text) .. _" in the text_box") | ||
+ | end | ||
+ | -- this is a button, so we use on_button_click, not on_left_click | ||
+ | dialog.ok.on_button_click = show_input | ||
− | + | dialog.gold_sl.on_modified = function() | |
+ | dialog.gold_tb.text = tostring(dialog.gold_sl.value) | ||
end | end | ||
− | + | end | |
+ | gui.show_dialog(dialogDefinition,preshow) | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 1,098: | Line 1,179: | ||
===Borders=== | ===Borders=== | ||
+ | |||
+ | Borders allow you to force unused space around a column, for example so that your widgets don't runtogether. You can specify the border to be one or more of top, bottom, left, right, or all. The border_size sets the amount of padding, in pixels. | ||
+ | |||
+ | <syntaxhighlight lang=lua> | ||
+ | wml.tag.column{ | ||
+ | border = "left, right" | ||
+ | border_size = 25 | ||
+ | } | ||
+ | </syntaxhighlight> | ||
===Alignment=== | ===Alignment=== | ||
− | By default, the GUI will usually center widgets in their cells, which is not always what we want. In this example, we use horizontal_alignment to move widgets around a little. | + | {| |
+ | | colspan=2 |By default, the GUI will usually center widgets in their cells, which is not always what we want. In this example, we use horizontal_alignment to move widgets around a little. | ||
In more complex cases, it may be useful to add [[GUIWidgetDefinitionWML#Spacer|spacers]] to help force alignment, or use linked_groups to force consistent alignment for objects within a grid. | In more complex cases, it may be useful to add [[GUIWidgetDefinitionWML#Spacer|spacers]] to help force alignment, or use linked_groups to force consistent alignment for objects within a grid. | ||
− | + | |- | |
− | [[File:Gui tutorial alignment without.png|thumb]] | + | | [[File:Gui tutorial alignment without.png|center|thumb|Default alignment]] ||[[File:Gui tutorial alignment with.png|center|thumb|Showing off some alignment options]] |
− | [[File:Gui tutorial alignment with.png|thumb]] | + | |} |
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local function basic_alignment() | local function basic_alignment() | ||
− | + | local dialogDefinition = { | |
− | + | wml.tag.tooltip { id = "tooltip_large" }, | |
− | + | wml.tag.helptip { id = "tooltip_large" }, | |
− | + | wml.tag.grid { | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.label { | |
− | + | label = "Ralph" | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.image { | |
− | + | label = "units/trolls/grunt.png" | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | horizontal_alignment = "right", | |
− | + | wml.tag.label { | |
− | + | label = "Sam" | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.image { | |
− | + | label = "units/trolls/troll-hero-attack-se-4.png" -- 122x102 | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | horizontal_alignment = "left", | |
− | + | wml.tag.label { | |
− | + | label = "Jr" | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | horizontal_alignment = "right", | |
− | + | wml.tag.image { | |
− | + | label = "units/trolls/whelp.png" -- 72x72 | |
− | + | } | |
− | + | } | |
− | |||
} | } | ||
+ | } | ||
} | } | ||
− | + | } | |
+ | gui.show_dialog(dialogDefinition) | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 1,159: | Line 1,251: | ||
The screenshots show the output of this example with and without the horizontal_alignment lines included above. | The screenshots show the output of this example with and without the horizontal_alignment lines included above. | ||
− | + | A label allows you to set the text_alignment field (e.g. text_alignment = "left"). Note, however, this only aligns the text within the label. The label itself will probably be centered in the column, giving the false appearance that your text alignment is not working. | |
+ | |||
+ | ===Growth=== | ||
+ | |||
+ | To keep your dialog nicely balanced, the GUI2 engine may need to grow rows and/or columns. You can control which columns, for example, grow and which do not, by setting some with grow_factor = 1, and others with grow_factor = 0 (do not grow). While grow factors of 0 and 1 are the most common, you can use other values to adjust the relative growth if you really need to, see [[GUILayout]] for the gory details. | ||
− | + | It it sometimes useful to include a column with a [spacer] and a grow_factor (often the only column with grow_factor != 0) whose sole purpose is to keep your GUI aligned nicely as the layout engine does its evil. | |
− | = | + | To force a column to stretch to fit available space, use horizontal_grow = true. Note that horizontal_grow is not compatible with horizontal_alignment. There is also a vertical_grow parameter. |
− | |||
− | + | If you need to see every little detail about the layout process, start wesnoth with --log-debug=gui/layout. Good luck with that. | |
===Definitions=== | ===Definitions=== | ||
Line 1,181: | Line 1,276: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | There are many other definitions for buttons and other widget types in that directory. It would be nice to have a list (linked here), but for now you'll just have to look around. | + | {| |
+ | |There are many other definitions for buttons and other widget types in that directory. It would be nice to have a list (linked here), but for now you'll just have to look around. | ||
− | We can even create our own custom definitions using [[LuaAPI/gui#gui.add_widget_definition|gui.add_widget_definition()]]. In this example, we copy from .../data/gui/widget/toggle_button_tree_view_node.cfg with a slight change so that our button turns red when focused. | + | We can even create our own custom definitions using [[LuaAPI/gui#gui.add_widget_definition|gui.add_widget_definition()]]. In this example, we copy from .../data/gui/widget/toggle_button_tree_view_node.cfg with a slight change so that our button turns red ( '''~BLEND(255,0,0,1)''' ) when focused. |
− | In this example, we will use wesnoth.wml_actions to create the WML [ | + | In this example, we will use wesnoth.wml_actions to create the WML tag [add_widget_def_demo]. |
− | [[File:Gui tutorial custom widget.png|thumb]] | + | Note the first line, a common convention for creating an alias for wml.tag. |
+ | |- | ||
+ | |[[File:Gui tutorial custom widget.png|center|thumb|Different definition of buttons, including one of our own (right)]] | ||
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
+ | local T = wml.tag -- save ourselves some typing | ||
function wesnoth.wml_actions.add_widget_def_demo() | function wesnoth.wml_actions.add_widget_def_demo() | ||
− | + | local definition = { | |
− | + | id = "tree_view_node_custom", | |
− | + | description = "Fold/unfold status indicator of a tree view node (MODIFIED).", | |
− | + | T.resolution { | |
− | + | min_width = 25, | |
− | + | min_height = 19, | |
− | + | default_width = 25, | |
− | + | default_height = 19, | |
− | + | max_width = 25, | |
− | + | max_height = 19, | |
− | + | -- Unselected - note there's no tags for unselected/selected, that's simply determined by the order | |
− | + | T.state { | |
− | + | T.enabled { | |
− | + | T.draw { | |
− | + | T.image { name = "buttons/fold-arrow.png" } | |
− | + | } | |
− | + | }, | |
− | + | T.disabled { | |
− | + | T.draw { | |
− | + | T.image { name = "buttons/fold-arrow.png~GS()" } | |
− | + | } | |
− | + | }, | |
− | + | T.focused { | |
− | + | T.draw { | |
− | + | T.image { name = "buttons/fold-arrow.png~BLEND(255,0,0,1)" } | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | -- Selected | |
− | + | T.state { | |
− | + | T.enabled { | |
− | + | T.draw { | |
− | + | T.image { name = "buttons/unfold-arrow.png" } | |
− | + | } | |
− | + | }, | |
− | + | T.disabled { | |
− | + | T.draw { | |
− | + | T.image { name = "buttons/unfold-arrow.png~GS()" } | |
− | + | } | |
− | + | }, | |
− | + | T.focused { | |
− | + | T.draw { | |
− | + | T.image { name = "buttons/unfold-arrow.png~BLEND(255,0,0,1)" } | |
− | + | } | |
− | |||
− | |||
} | } | ||
+ | }, | ||
} | } | ||
− | + | } | |
+ | gui.add_widget_definition("toggle_button", "tree_view_node_custom", definition) | ||
− | + | local dialogDefinition = { | |
− | + | T.tooltip { id = "tooltip_large" }, | |
− | + | T.helptip { id = "tooltip_large" }, | |
− | + | T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | T.toggle_button { | |
− | + | definition = "default" | |
− | + | } | |
− | + | }, | |
− | + | T.column { | |
− | + | T.toggle_button { | |
− | + | definition = "tree_view_node" | |
− | + | } | |
− | + | }, | |
− | + | T.column { | |
− | + | T.toggle_button { | |
− | + | definition = "tree_view_node_custom" | |
− | + | } | |
− | + | } | |
− | + | } | |
− | |||
} | } | ||
− | + | } | |
+ | gui.show_dialog(dialogDefinition) | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | See [[GUIWidgetDefinitionWML]] for more details on widget definitions. | |
+ | |||
+ | You can also give the whole dialog a definition, like definition = "tooltip_large". There is no gui.add_window_definition(), however a window is technically a type of widget so it may be possible to create your own window definitions (please update this if you do). | ||
===Panels=== | ===Panels=== | ||
Line 1,293: | Line 1,395: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Here we take our [[#Progress Bar|progress bar]], and add a text overlay. | + | {| |
+ | |Here we take our [[#Progress Bar|progress bar]], and add a text overlay. | ||
Note: either using text on a canvas is rather limited, or I just couldn't figure it out. For example, I could not get the text size any smaller, and any attempts to do so simply resulted in very blocky text. And the spaces were necessary so that the text wasn't stretched out. I also find it surprising that the progress bar does not have this feature inherently. So it's not a great example, but it should be enough to get you started using canvases. Be sure to visit the [[LuaAPI/gui/widget#set_canvas|link]] mentioned above for more options. | Note: either using text on a canvas is rather limited, or I just couldn't figure it out. For example, I could not get the text size any smaller, and any attempts to do so simply resulted in very blocky text. And the spaces were necessary so that the text wasn't stretched out. I also find it surprising that the progress bar does not have this feature inherently. So it's not a great example, but it should be enough to get you started using canvases. Be sure to visit the [[LuaAPI/gui/widget#set_canvas|link]] mentioned above for more options. | ||
− | + | |- | |
− | [[File:Gui tutorial canvas text.png|thumb]] | + | |[[File:Gui tutorial canvas text.png|center|thumb|A progress bar in the background, with text in the foreground]] |
− | + | |} | |
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
function wesnoth.wml_actions.progress_bar_with_overlay() | function wesnoth.wml_actions.progress_bar_with_overlay() | ||
− | + | local dialogDefinition = { | |
− | + | T.tooltip { id = "tooltip" }, | |
− | + | T.helptip { id = "tooltip" }, | |
− | + | T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | T.panel { id = "panel", | |
− | + | T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | T.button { id = "button", | |
− | + | label =_ "Press me"} | |
− | + | }, | |
− | + | T.column { | |
− | + | T.progress_bar { id = "progress" } | |
− | + | }, | |
− | + | T.column { | |
− | + | T.tag.button { id = "ok", | |
+ | label = _"Done" | ||
+ | } | ||
} | } | ||
+ | } | ||
} | } | ||
− | } } | + | } |
− | + | } | |
− | + | } | |
− | + | } | |
− | + | } | |
− | + | local function preshow(dialog) | |
− | + | dialog.ok.enabled = false | |
− | + | dialog.button.on_button_click = function() | |
− | + | dialog.progress.percentage = dialog.progress.percentage + 10 | |
− | + | dialog.panel:set_canvas(2, { T.text { text_alignment = "center", font_size = 48, | |
− | + | text_markup = true, text = "<span color='yellow'> " .. | |
− | + | dialog.progress.percentage .. "% </span>" } } ) | |
− | + | if dialog.progress.percentage > 99 then | |
+ | dialog.button.enabled = false | ||
+ | dialog.ok.enabled = true | ||
end | end | ||
− | + | end | |
+ | gui.show_dialog(dialogDefinition,preshow) | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | You may notice we added a panel and placed our text on the foreground canvas(2) on it, since the progress_bar does not support canvases. | + | You may notice we added a panel and placed our text on the foreground canvas(2) on it, since the progress_bar itself does not support canvases. |
===Placement=== | ===Placement=== | ||
− | + | ||
− | + | You may have noticed that all of our dialogs are centered on the screen. This is the default. You can override this and place the dialog wherever you like by setting automatic_placement = false, along with x, y, width, and height at the top level of your dialog definition (next to tooltip =, etc). | |
− | + | ||
− | + | '''This part about placement needs review''' | |
− | + | If you prefer, you may replace x and/or y with horizontal_placement and vertical_placement, such as horizontal_placement = "left". You can even set the placement values using WFL, for example horizontal_placement = "(gamemap_width / 3)" -- see [[#Using WFL with GUI2|Using WFL with GUI2]]. | |
==Miscellaneous== | ==Miscellaneous== | ||
+ | TODO: This stuff doesn't belong in a tutorial. It's worth documenting, but not here. Better to save these here for now than keep them on my laptop. | ||
===Progress Bar=== | ===Progress Bar=== | ||
− | + | {| | |
− | A progress_bar is a graphical representation of a percent. In this example, we present the user with a puzzle. They must hit the button repeatedly before they can close the | + | |A progress_bar is a graphical representation of a percent. In this example, we present the user with a puzzle. They must hit the button repeatedly before they can close the GUI. A progress_bar displays their current progress toward completion. |
− | + | |- | |
− | [[File:Gui tutorial progress bar.png|thumb|upright=2]] | + | |[[File:Gui tutorial progress bar.png|center|thumb|upright=2]] |
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
function wesnoth.wml_actions.progress_bar() | function wesnoth.wml_actions.progress_bar() | ||
− | + | local dialogDefinition = { | |
− | + | wml.tag.tooltip { id = "tooltip_large" }, | |
− | + | wml.tag.helptip { id = "tooltip_large" }, | |
− | + | wml.tag.grid { | |
− | + | wml.tag.row { | |
− | + | wml.tag.column { | |
− | + | wml.tag.button { id = "button", | |
− | + | label =_ "Press me" | |
− | + | } | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.progress_bar { id = "progress" } | |
− | + | }, | |
− | + | wml.tag.column { | |
− | + | wml.tag.button { id = "ok", | |
− | + | label = _"Done" | |
− | + | } | |
− | |||
} | } | ||
+ | } | ||
} | } | ||
− | + | } | |
− | + | local function preshow(dialog) | |
− | + | dialog.ok.enabled = false | |
− | + | dialog.button.on_button_click = function() | |
− | + | dialog.progress.percentage = dialog.progress.percentage + 10 | |
− | + | if dialog.progress.percentage > 99 then | |
− | + | dialog.button.enabled = false | |
− | + | dialog.ok.enabled = true | |
− | |||
end | end | ||
− | + | end | |
+ | |||
+ | gui.show_dialog(dialogDefinition,preshow) | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 1,395: | Line 1,507: | ||
===Unit Preview Pane=== | ===Unit Preview Pane=== | ||
− | A unit_preview_pane takes a unit (or a unit type), and presents a graphical representation of the unit and its more important attributes, along with tooltips for additional details, in the same way that the recall menu does. Here we see an example of a unit which has picked up some items, including a weapon, which are affecting its stats. | + | {| |
− | + | |A unit_preview_pane takes a unit (or a unit type), and presents a graphical representation of the unit and its more important attributes, along with tooltips for additional details, in the same way that the recall menu does. Here we see an example of a unit which has picked up some items, including a weapon, which are affecting its stats. | |
− | [[File:Gui tutorial unit preview pane.png|thumb]] | + | |- |
+ | |[[File:Gui tutorial unit preview pane.png|center|thumb]] | ||
+ | |} | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
local T = wml.tag | local T = wml.tag | ||
function wesnoth.wml_actions.recall_from_variable(cfg) | function wesnoth.wml_actions.recall_from_variable(cfg) | ||
− | + | local from_array = cfg.from or wml.error( | |
− | + | "[recall_from_variable]: missing required from= ") | |
− | + | local to_var = cfg.to or wml.error( | |
+ | "[recall_from_variable]: missing required to= ") | ||
+ | local unit_list = wml.array_access.get(from_array) or wml.error( | ||
+ | string.format("[recall_from_variable]: failed to fetch wml array %s",from_array)) | ||
− | + | local listboxItem = T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | T.label { id = "available_unit", | |
− | + | linked_group = "available_unit" | |
− | |||
− | |||
} | } | ||
+ | } | ||
} | } | ||
+ | } | ||
− | + | local listbox_id = "available_units" | |
− | + | local listboxDefinition = T.listbox { id = listbox_id, | |
− | + | T.list_definition { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | T.toggle_panel { listboxItem } | |
− | |||
− | |||
} | } | ||
+ | } | ||
} | } | ||
− | + | } | |
− | + | local dialogDefinition = { | |
− | + | T.tooltip { id = "tooltip" }, | |
− | + | T.helptip { id = "tooltip" }, | |
− | + | T.linked_group { id = "available_unit", fixed_width = true }, | |
− | + | T.grid { | |
− | + | T.row { -- header | |
− | + | T.column { | |
− | + | T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | border = "bottom", | |
− | + | border_size = 10, | |
− | + | T.label { | |
− | + | use_markup = true, | |
− | + | label = "<span size='large' color='yellow' weight='bold'>" | |
− | + | .. _"Select unit to recall" .. "</span>" | |
− | |||
− | |||
− | |||
} | } | ||
− | + | } | |
− | + | } | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | T.row { -- Body | |
− | + | T.column { | |
− | + | T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | border = "right", | |
− | + | border_size = 40, | |
− | + | listboxDefinition | |
− | + | }, | |
− | + | T.column { | |
− | + | T.unit_preview_pane { id = "unit_preview" } | |
− | + | } | |
− | + | } | |
− | + | } | |
− | + | } | |
− | + | }, | |
− | + | T.row { -- Footer | |
− | + | T.column { | |
− | + | T.grid { | |
− | + | T.row { | |
− | + | T.column { | |
− | + | T.spacer { width = 400 } | |
− | + | }, | |
− | + | T.column { | |
− | + | border = "top,right", | |
− | + | border_size = 10, | |
− | + | T.button { id = "ok", | |
− | + | label = _"OK" | |
− | + | } | |
− | + | } | |
− | + | } | |
− | + | } | |
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
− | + | local picked = 1 | |
− | + | local function preshow(dialog) | |
− | + | local listbox = dialog[listbox_id] | |
− | + | for i,unit in ipairs(unit_list) do | |
− | + | listbox[i].available_unit.label = unit.name .. "(" .. | |
− | + | unit.language_name .. ")" | |
− | + | end | |
− | + | local function draw_unit() | |
− | + | wesnoth.units.to_recall(unit_list[listbox.selected_index]) | |
− | + | local tmp = wesnoth.units.find_on_recall{ id = | |
− | + | unit_list[listbox.selected_index].id }[1] | |
− | + | dialog.unit_preview.unit = tmp | |
− | + | wesnoth.units.extract(tmp) | |
− | + | picked = listbox.selected_index | |
− | |||
end | end | ||
+ | draw_unit() | ||
+ | listbox.on_modified = draw_unit | ||
+ | end | ||
− | + | gui.show_dialog(dialogDefinition,preshow) | |
− | + | wml.variables[to_var] = picked - 1 | |
end | end | ||
Line 1,520: | Line 1,639: | ||
===Explore Your Options=== | ===Explore Your Options=== | ||
− | We've seen a lot of options that can be set to modify the behaviour of our dialogs, some on columns, some on | + | We've seen a lot of options that can be set to modify the behaviour of our dialogs, some on columns, some on widgets, etc. These are in the process of being documented [[GUIWidgetInstanceWML|here]], but if you would like to go straight to the source, the data Wesnoth uses to validate your configuration can be found in [https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui data/schema/gui] under your install directory. |
− | Let's look at a scroll_label, for example. A scroll_label is a widget, so we look in [https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui widget_instances.cfg] and find the following. Here we can see five parameters we can provide to our scroll_label (in addition to the ones available to all widgets, like id and definition), along with their types and default values. For example, we could set "link_aware = true", and if we do not we can assume that link_aware will be false. While this does not explain what these parameters do, the information provided in the schema directory is often helpful nonetheless. | + | Let's look at a scroll_label, for example. A scroll_label is a widget, so we look in [https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui/widget_instances.cfg widget_instances.cfg] and find the following. Here we can see five parameters we can provide to our scroll_label (in addition to the ones available to all widgets, like id and definition, see the entry super=... which refers you to the widget_instance in the file [https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui/generic.cfg data/schema/gui/generic.cfg]), along with their types and default values. For example, we could set "link_aware = true", and if we do not we can assume that link_aware will be false. While this does not explain what these parameters do, the information provided in the schema directory is often helpful nonetheless. |
<syntaxhighlight lang=wml> | <syntaxhighlight lang=wml> | ||
Line 1,546: | Line 1,665: | ||
[/type] | [/type] | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | The variable types found in the schema files are described at [[GUIVariable]]. | ||
+ | |||
+ | Note: The keys in the schema file correspond to the attributes used when configuring your widgets. They will not always align perfectly with keys used by the Lua API. For example, the slider uses maximum_value in its configuration, and max_value when using the Lua API: | ||
+ | |||
+ | <syntaxhighlight lang=wml> | ||
+ | [slider] | ||
+ | id="my_slider" | ||
+ | maximum_value = 100 | ||
+ | [/slider] | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang=lua> | ||
+ | dialog.my_slider.max_value = 90 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ===Using WFL with GUI2=== | ||
+ | |||
+ | If you look at the variable type descriptions at [[GUIVariable]] you may notice that many of them start with "f_" and have "or formula" in the description. This means that you can use [[Wesnoth_Formula_Language|Wesnoth Formula Language (WFL)]] formulas in these fields, with certain variables made available to you (which variables and what they mean can be challenging to ascertain, but you can generally figure it out by example). | ||
+ | |||
+ | Looking at [https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui/window.cfg data/schema/gui/window.cfg] we see that 'height' has type f_unsigned, which tells us that height is an unsigned integer, and that we can use a WFL formula to define it in a window_definition. In [https://github.com/wesnoth/wesnoth/tree/master/data/gui/themes/default/window/wml_message.cfg data/gui/window/wml_message.cfg] (data/gui/themes/default/window/wml_message.cfg starting around 1.19.2), we find the line '''height = "(screen_height - 30)"'''. This does exactly what it looks like, it sets the height of our window to 30 pixels less than the height of the screen. | ||
===Useful Links=== | ===Useful Links=== | ||
− | + | [[GUIToolkitWML]] - Documents the keys/values available on various objects (e.g. "what attributes can I set on a column?") <br> | |
− | [[LuaAPI/gui|LuaAPI/gui - Mostly about opening windows | + | [[LuaAPI/gui|LuaAPI/gui]] - Mostly about opening windows<br> |
− | [[LuaAPI/types/widget | + | [[LuaAPI/types/widget]] - Widget attributes and callbacks<br> |
[https://wiki.wesnoth.org/Category:GUI_WML_Reference GUI_WML_Reference]<br> | [https://wiki.wesnoth.org/Category:GUI_WML_Reference GUI_WML_Reference]<br> | ||
[https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui .../data/schema/gui/*.cfg] - Lists of valid options for various GUI objects<br> | [https://github.com/wesnoth/wesnoth/tree/master/data/schema/gui .../data/schema/gui/*.cfg] - Lists of valid options for various GUI objects<br> | ||
+ | [https://devdocs.wesnoth.org/layout_algorithm.html Layout Algorithm] - For windows, at least | ||
===Credits=== | ===Credits=== |
Latest revision as of 23:42, 1 November 2024
So, it looks like I can't exclude this page/section from the search engine as I had hoped. I wanted to put this in a sandbox so that it wouldn't be published without some proper review.
Contents
Introduction
This guide is designed to help you get a simple Wesnoth GUI, implemented in lua, up and running while describing the basic building blocks along the way. It is written in a narrative format, where most examples build on previous examples, and therefore while not always necessary it may be desirable to read from start to finish. The reader, probably a UMC author, should have a basic knowledge of working with lua within Wesnoth.
Some would find creating a GUI in part or in full using WML simpler to follow, and those alternatives are available, for example LuaAPI/gui/example, but we're using lua here. If you find WML easier to follow, you can always convert the lua tables that define the GUIs to WML using wml.tostring, for example:
print(wml.tostring(dialogDefinition))
gui.show_lua_console()
In the comments of our first GUI, below, is an example which loads a WML GUI configuration using wml.load(), validating against the GUI2 window schema.
In some examples, instead of defining the entire GUI at once, we'll break out some parts into separate variables. If you try to view one in WML and get an error from wml.tostring() about expecting a WML table and not a table, try passing your variable(/table) inside a table:
print(wml.tostring({listboxItem}))
gui.show_lua_console()
What is GUI2?
Once upon a time, Wesnoth had a GUI system which is now commonly referred to as GUI1. As of 1.19, GUI1 has been almost completely replaced by GUI2. UMC authors will probably never encounter GUI1 and can simply use the terms GUI and GUI2 interchangeably, at least until GUI3 comes along.
Instead of spending a lot of time here giving an overview of what GUI2 is and what makes it great, and not so great, let's charge ahead so we can see it in action, and let readers who are interested look elsewhere(TODO: link) for a more formal definition. (TODO: is this the right approach for most readers?).
Getting Started
For example purposes, we'll create a directory in our campaign directory called lua containing a file called gui_tutorial.lua, and add a command in the prestart event for a scenario which will create a right-click menu option to invoke our new GUI. Of course there are other methods to invoke lua from WML, but this one will get us started. After all, we really just want to get something up on the screen ASAP, right?
A most basic GUI
In our prestart event, we create a simple menu item, which executes a single line of lua which calls the lua function most_basic_gui() which is found in gui_tutorial.lua: |
[set_menu_item]
id=most_basic_gui
description="Our first GUI"
[command]
[lua]
code=<<
wesnoth.require("~add-ons/<OUR_CAMPAIGN>/lua/gui_tutorial.lua").most_basic_gui()
>>
[/lua]
[/command]
[/set_menu_item]
And create gui_tutorial.lua:
The WML equivalent of dialogDefinition | |
---|---|
local function most_basic_gui()
local dialogDefinition = {
--click_dismiss = true, -- allow user to close dialog with click of a button
wml.tag.tooltip { id = "tooltip_large" }, -- required
wml.tag.helptip { id = "tooltip_large" }, -- required
wml.tag.grid { -- our most basic gui
wml.tag.row { -- a grid must include at least one row
wml.tag.column { -- a row needs a column
wml.tag.image { -- a column includes exactly one widget
label = "units/trolls/grunt.png"
}
}
}
}
}
local function preshow(dialog)
print(wml.tostring(dialogDefinition))
gui.show_lua_console()
end
gui.show_dialog(dialogDefinition,preshow)
-- Or, if you want to define the gui in WML, something like:
-- local dialog_wml = wml.load("~add-ons/GUI_Tutorial/most_basic_gui.cfg",
-- true, "schema/gui_window.cfg")
-- gui.show_dialog(wml.get_child(dialog_wml, 'resolution'))
end
return { most_basic_gui = most_basic_gui}
|
[tooltip]
id="tooltip_large"
[/tooltip]
[helptip]
id="tooltip_large"
[/helptip]
[grid]
[row]
[column]
[image]
label="units/trolls/grunt.png"
[/image]
[/column]
[/row]
[/grid]
|
In the above file, we have just the single function to create our GUI, followed by a return command which makes our function most_basic_gui() available to the wesnoth.require() function as most_basic_gui().
Our function creates a table containing the definition of a "dialog", and then passes that to gui.show_dialog(). It should be noted that gui.show_dialog does not provide synchronization, so if your GUI makes changes to the game state, you'll need to jump through some hoops or you'll break replays and multiplayer, but that's not something we need to care about at this point, so just be aware that you may need to deal with it in the future.
Our dialog definition at this point includes three parts. The first two are a tooltip and a helptip, which are required and define how tooltips (use id = "tooltip_large" or id = "tooltip" or id = "tooltip_transparent"), and helptips (rarely used, but must be included here, and yes their definitions are "tooltip" not "helptip") will look (as we will see later, this really should be definition = "tooltip", but in this case it's id = "tooltip"). The third part is a grid, which is basically a table, like in HTML or MySQL, with rows and columns. A grid always contains at least one row. A row contains at least one column (note that a column does NOT span rows, it is completely contained within a row). Inside a column is exactly one item, a cell which contains a widget, in this case we'll use an image (later we'll see what else we can put in a cell). Note that we don't actually define a cell in our code, it's just a term used to refer to the contents of a column.
The preshow function is optional. If included in the call to gui.show_dialog(), it is run before the GUI is displayed. In this case, we include it to display the dialog we created with lua in WML format. Near the end, in comments, we show how you would call gui.show_dialog() if you chose to define your dialog using WML. Note that what is shown here is the WML equivalent of our lua dialogDefinition, and not the complete WML you would need to use if you were to configure your GUI layout in WML.
Note: if you should try out the above, you'll have to hit escape or enter to close the GUI. Uncomment the "click_dismiss" line, and the user will be able to close the GUI with a mouse click.
Adding a little flavor
You may have noticed our GUI is missing a header and a way to close it when we're done admiring our work. Here we add a new first row to our grid, while demonstrating a couple formatting options: a border to put some distance between our grid cell and its neighbor, and the "use_markup = true" option to enable Pango support in our text.
Our third row adds an OK button at the bottom. You can associate actions with buttons to do all kinds of things, but here we just exploit the default behaviour to close the GUI. Of course, we'll also have to modify our return to support the new function, and update our menu item(s) accordingly. |
local function most_basic_gui2()
local dialogDefinition = {
wml.tag.tooltip { id = "tooltip_large" },
wml.tag.helptip { id = "helptip_large" },
wml.tag.grid {
wml.tag.row { -- A header
wml.tag.column {
border = "bottom",
border_size = 10,
wml.tag.label {
use_markup = true,
label = "<span size='large'>" .. _"Here there be " ..
"<span color='yellow'>" ..
_"MONSTERS!" .. "</span></span>"
}
}
},
wml.tag.row { -- The body of our GUI
wml.tag.column {
wml.tag.image {
label = "units/trolls/grunt.png"
}
}
},
wml.tag.row {
wml.tag.column { -- An "OK" button, with no action assigned for now,
-- but it will close the GUI
wml.tag.button {
id = "ok",
label = _"OK"
},
}
}
}
}
wml.parse(wml.tostring({wml.tag.resolution(dialogDefinition)}), "schema/gui_window.cfg") -- schema validation errors will go to log
gui.show_dialog(dialogDefinition)
end
return { most_basic_gui = most_basic_gui, most_basic_gui2 = most_basic_gui2 }
Note that in this example, just before we invoke gui.show_dialog(), we use wml.parse() to validate our input against the GUI2 window schema. If we've misspelled something, or placed a key in the wrong place (like placing a border in a widget instead of a column), etc, an error such as error validation: Invalid key 'garbage=' in tag [resolution] will be written to the wesnoth log.
And a bit more
And now a minor, but important change. We want to add a new text field next to the image in the body of our GUI. Obviously, we want to add a new column, but this is more difficult than when we added new rows in the previous example. The problem is that all rows (at a given level) in a grid must contain the same number of columns. We can't have two columns in the row that constitutes the body of our GUI, but only one in the header and one in the footer. To solve this problem, we replace our image widget with a new grid, which can use as many columns as we like (as long as they are the same within each row of our new grid). This new grid contains a row with two columns, but that is okay because the new grid itself is placed in a single column, which matches our single column header and footer (ok button), keeping the rows balanced. |
local function most_basic_gui3()
local dialogDefinition = {
wml.tag.tooltip { id = "tooltip_large" },
wml.tag.helptip { id = "helptip_large" },
wml.tag.grid {
wml.tag.row { -- A header
wml.tag.column {
border = "bottom",
border_size = 10,
wml.tag.label {
use_markup = true,
label = "<span size='large'>" .. _"Here there be " ..
"<span color='yellow'>" ..
_"MONSTERS!" .. "</span></span>"
}
}
},
wml.tag.row { -- The body of our GUI
wml.tag.column { -- This is the only column in this row,
-- to match the number of columns in
-- the rows of our header and footer
wml.tag.grid { -- A new grid, so we can use a different
-- number of columns
wml.tag.row {
wml.tag.column {
wml.tag.image {
label = "units/trolls/grunt.png"
}
},
wml.tag.column {
wml.tag.label {
label = "A troll"
}
}
}
}
}
},
wml.tag.row { -- A footer
wml.tag.column { -- An "OK" button, with no action assigned for now,
-- but it will close the GUI
wml.tag.button {
id = "ok",
label = "Ok"
}
}
}
}
}
gui.show_dialog(dialogDefinition)
end
While we see here just the simplest of examples, you can create many levels of grids inside grids, rows with multiple columns some of which containing grids, etc, to build complicated dialogs to your liking.
Containers
Stacked Widget
TODO
Listbox
Now we'd like to add some more monsters. Obviously, we could just add more rows, but what if we won't know until runtime how many and which ones? We could break up the definition of our dialog and add new rows dynamically, it's just a table after all, but fortunately we have a widget which handles this for us, a listbox. A listbox is kind of like an array, a collection of similar objects where the number of items can vary. We will tell the GUI where we want the box, and what each entry in the box will look like, and then at runtime we can add entries to the box.
Note that listbox items are added from top to bottom. Another container, the horizontal_listbox, works just like a listbox except the items are added from left to right. With a grid listbox, items are added horizontally with the list wrapping to a new line as necessary. | ||||||||||||||||||||||||||||||
local function gui_with_listbox()
local monsters = {
{ image = "units/trolls/grunt.png",
string = "A troll" },
{ image = "units/monsters/cuttlefish.png",
string = "A cuttlefish" },
{ image = "units/monsters/yeti.png",
string = "A yeti" }
}
local listbox_id = "monsters"
local listboxItem = wml.tag.grid {
wml.tag.row {
wml.tag.column {
wml.tag.image {
id = "monster_image"
}
},
wml.tag.column {
wml.tag.label {
id = "monster_label"
}
}
}
}
local listboxDefinition = wml.tag.listbox { id = listbox_id,
wml.tag.list_definition {
wml.tag.row {
wml.tag.column {
wml.tag.toggle_panel {
listboxItem
}
}
}
}
}
local dialogDefinition = {
wml.tag.tooltip { id = "tooltip_large" },
wml.tag.helptip { id = "tooltip_large" },
wml.tag.grid {
wml.tag.row { -- A header
wml.tag.column {
border = "bottom",
border_size = 10,
wml.tag.label {
use_markup = true,
label = "<span size='large'>" .. _"Here there be " ..
"<span color='yellow'>" ..
_"MONSTERS!" .. "</span></span>"
}
}
},
wml.tag.row { -- The body of our GUI
wml.tag.column {
listboxDefinition
}
},
wml.tag.row {
wml.tag.button {
id = "ok",
label = _"OK"
}
}
}
}
local function preshow(dialog) -- Prepare the GUI before display
local listbox = dialog[listbox_id]
for i, monster in ipairs(monsters) do
listbox[i].monster_image.label = monster.image
listbox[i].monster_label.label = monster.string
end
end
gui.show_dialog(dialogDefinition,preshow)
end
We begin by creating a simple table which represents the data which will be presented in our listbox. In most cases, we probably wouldn't create that monster table here, but we need it for our example. The variable listbox_id gives us an identifier for our listbox so we can reference it when we need to. We don't really need to use a variable here, it's just convenient. We define the structure for the elements in our listbox, stored in listboxItem. Note that we've replaced the actual data with identifiers (e.g. 'units/trolls/grunt.png' becomes 'id = "monster_image"), since each element may have different data. We define the listbox itself, specifying the identifier, and the definition of our listbox element (which looks a lot like a grid). The cell inside the column contains a variable which is just the listbox element definition we created above; it's not necessary to do this, we could have used one big table, but it's easier to read this way (IMO). We replace the hardcoded data in our GUI body with our new listbox definition, again using a variable to make it easier to read. We create a function, often called preshow(), which will be called for us as part of the drawing the GUI (see the new optional argument in gui.show_dialog() -- there's also an optional postshow(), but we don't need it here). This is where we pull the data from our example table into the listbox. We create a listbox variable associated with the listbox, identified by the listbox_id we assigned earlier, inside the dialog which gui.show_dialog() passes to preshow(). Then we iterate through our data table, using each entry from that table to populate a listbox item. Let's look at that last action a little more closely using an example. listbox[i].monster_image.label = monster.image
Which we can read as "In listbox element i of our listbox, for the image with identifier monster_image which we defined in our listboxItem, use the data from the corresponding index i in our example data table" (you don't see the index i for the monsters table here, but remember we're iterating using ipairs, we could have just as easily used listbox[i].monster_image.label = monsters[i].image). If you were to step through all of the variable substitutions, you'd see that for i=1, our dialogDefinition is basically the same thing as it was in the earlier examples, just supplied from a table instead of hardcoded (note that you can hardcode entries in a listbox in your dialog definition using the [list_data] tag, aka wml.tag.list_data, which we do not demonstrate here). You will probably notice that our data does not line up nicely in the GUI. We will fix that later. We'll also demonstrate how the user can select an item in a listbox, and how you can identify which item that was using the selected_index variable of the listbox. Tree ViewSimple Tree View
1local function basic_tree_view()
2 local monsters = {
3 { image = "units/trolls/grunt.png",
4 label = "A troll", name = _"Bob",
5 type = "Trolls" },
6 { image = "units/trolls/whelp.png",
7 label = "A troll whelp", name = "Junior",
8 type = "Trolls" },
9 { image = "units/trolls/shaman.png",
10 label = "A troll shaman", name = _"Alice",
11 type = "Trolls" },
12 { image = "units/monsters/cuttlefish.png",
13 label = "A cuttlefish", type = "Seamonsters" },
14 { image = "units/monsters/yeti.png", label = "A yeti",
15 type = "Coolers" }
16 }
17
18 local tree_view = wml.tag.tree_view {
19 id = "monsters_tv",
20 wml.tag.node {
21 id = "trolls_node",
22 wml.tag.node_definition {
23 wml.tag.row {
24 wml.tag.column {
25 wml.tag.label {
26 id = "monster_name",
27 linked_group = "monster_name"
28 }
29 },
30 wml.tag.column {
31 wml.tag.image {
32 id = "monster_image",
33 linked_group = "monster_image"
34 }
35 },
36 wml.tag.column {
37 wml.tag.label {
38 id = "monster_label",
39 linked_group = "monster_label"
40 }
41 }
42 }
43 }
44 },
45 wml.tag.node {
46 id = "nottrolls_node",
47 wml.tag.node_definition {
48 wml.tag.row {
49 wml.tag.column {
50 wml.tag.image {
51 id = "monster_image",
52 linked_group = "monster_image"
53 }
54 },
55 wml.tag.column {
56 wml.tag.label {
57 id = "monster_label",
58 linked_group = "monster_label"
59 }
60 }
61 }
62 }
63 }
64 }
65
66 local dialogDefinition = {
67 wml.tag.tooltip { id = "tooltip_large" },
68 wml.tag.helptip { id = "tooltip_large" },
69 wml.tag.linked_group {
70 id = "monster_name",
71 fixed_width = true
72 },
73 wml.tag.linked_group {
74 id = "monster_image",
75 fixed_width = true
76 },
77 wml.tag.linked_group {
78 id = "monster_label",
79 fixed_width = true
80 },
81 wml.tag.grid {
82 wml.tag.row { -- A header
83 wml.tag.column {
84 border = "bottom",
85 border_size = 10,
86 wml.tag.label {
87 use_markup = true,
88 label = "<span size='large'>" .. _"Here there be " ..
89 "<span color='yellow'>" ..
90 _"MONSTERS!" .. "</span></span>"
91 }
92 }
93 },
94 wml.tag.row { -- The body of our GUI
95 wml.tag.column {
96 tree_view
97 }
98 },
99 wml.tag.row {
100 wml.tag.column {
101 wml.tag.button {
102 id = "ok",
103 label = _"OK"
104 }
105 }
106 }
107 }
108 }
109 local function preshow(dialog)
110 for i, monster in ipairs(monsters) do
111 if monster.type == "Trolls" then
112 dialog.monsters_tv:add_item_of_type("trolls_node")
113 dialog.monsters_tv[i].monster_name.label = monster.name -- only trolls have a name
114 else
115 dialog.monsters_tv:add_item_of_type("nottrolls_node")
116 end
117 -- All of our monsters have an image and a label
118 dialog.monsters_tv[i].monster_image.label = monster.image
119 dialog.monsters_tv[i].monster_label.label = monster.label
120 end
121 end
122 gui.show_dialog(dialogDefinition,preshow)
123end
We have expanded our list of monsters to include three types of trolls. We have also given each troll a name. This is of course a rather simplistic example, we could have simply given each monster that is not a troll an empty name, but it as presented here our approach serves the purpose of demonstrating how we deal with multiple data types. Our new tree view contains a node for each type of data we will present. In preshow, we explicitly define each element in our list using add_item_of_type(), and populate the item accordingly. [Note: in our listbox example, we could have used add_item() to add items to the listbox, but chose not to since that section was already introducing a number of new concepts. But here, since we have multiple types of items we need to be able to specify the type of the item when we add one, hence we need to use add_item_of_type()]. You may also note the addition of a few linked_group lines. We'll cover linked groups in more detail later, but as used here they instruct the GUI that each column using the same node type needs to be aligned with the others. For example, all of our trolls line up nicely. Building a Tree
1local function building_a_tree()
2 local tree_view = wml.tag.tree_view {
3 id = "monsters_tv",
4 wml.tag.node {
5 id = "race_node",
6 wml.tag.node_definition {
7 wml.tag.row {
8 wml.tag.column {
9 wml.tag.toggle_button {
10 id = "race_button",
11 }
12 },
13 wml.tag.column {
14 wml.tag.label {
15 id = "race_label",
16 }
17 }
18 }
19 }
20 },
21 wml.tag.node {
22 id = "details_node",
23 wml.tag.node_definition {
24 wml.tag.row {
25 wml.tag.column {
26 wml.tag.label {
27 id = "monster_type",
28 }
29 }
30 }
31 }
32 }
33 }
34 local dialogDefinition = {
35 wml.tag.tooltip { id = "tooltip_large" },
36 wml.tag.helptip { id = "tooltip_large" },
37 wml.tag.grid {
38 wml.tag.row { -- A header
39 wml.tag.column {
40 border = "bottom",
41 border_size = 10,
42 wml.tag.label {
43 use_markup = true,
44 label = "<span size='large'>" .. _"Show me the " ..
45 "<span color='yellow'>" ..
46 _"MONSTERS!" .. "</span></span>"
47 }
48 }
49 },
50 wml.tag.row { -- The body of our GUI
51 wml.tag.column {
52 tree_view
53 }
54 },
55 wml.tag.row {
56 wml.tag.column { -- An "OK" button
57 wml.tag.button {
58 id = "ok",
59 label = _"OK"
60 },
61 }
62 }
63 }
64 }
65 local function preshow(dialog) -- Prepare the GUI before display
66 -- You can refer to an object by its position
67
68 -- dialog.monsters_tv:add_item_of_type("race_node")
69 -- dialog.monsters_tv[1].race_label.label = "Trolls"
70 -- dialog.monsters_tv[1].race_button.on_modified =
71 -- function()dialog.monsters_tv[1].unfolded =
72 -- dialog.monsters_tv[1].race_button.selected end
73
74 -- dialog.monsters_tv[1]:add_item_of_type("details_node")
75 -- dialog.monsters_tv[1][1].monster_type.label = "Whelp"
76 -- dialog.monsters_tv[1]:add_item_of_type("details_node")
77 -- dialog.monsters_tv[1][2].monster_type.label = "Shaman"
78 -- dialog.monsters_tv[1]:add_item_of_type("details_node")
79 -- dialog.monsters_tv[1][3].monster_type.label = "Troll"
80
81 -- dialog.monsters_tv:add_item_of_type("race_node")
82 -- dialog.monsters_tv[2].race_label.label = "Other scary things"
83 -- dialog.monsters_tv[2].race_button.visible = "hidden"
84
85 -- ... or you can refer to that object using the return value
86 -- from add_item_of_type
87 local troll_node = dialog.monsters_tv:add_item_of_type("race_node")
88 troll_node.race_label.label = "Trolls"
89 troll_node.race_button.on_modified =
90 function()
91 troll_node.unfolded = troll_node.race_button.selected
92 end
93 local item = troll_node:add_item_of_type("details_node")
94 item.monster_type.label = "Whelp"
95 item = troll_node:add_item_of_type("details_node")
96 item.monster_type.label = "Shaman"
97 item = troll_node:add_item_of_type("details_node")
98 item.monster_type.label = "Troll"
99
100 local not_troll_node =
101 dialog.monsters_tv:add_item_of_type("race_node")
102 not_troll_node.race_label.label = "Other scary things"
103 not_troll_node.race_button.visible = "hidden"
104
105 end
106 gui.show_dialog(dialogDefinition,preshow)
107end
We define two nodes in our tree, a race_node for the top level, and a details_node for the children of a race_node. The rest of the interesting bits are in preshow(). We demonstrate two different ways of creating and accessing items, the first (commented out) just using the order in which they are created, while in the second we capture the result of add_item_of_type() in a variable we can use to refer to the newly defined object. The first method is shown primarily to demonstrate the structure of the resulting objects. The second method is perhaps easier to follow, and is almost necessary when you start doing things like dynamically deleting nodes, so it is in most cases a better practice (of course, you probably won't want to use the same variable for each node like we did, and perhaps not one local to the preview function). We add a node, label it "Trolls", and add a callback to the button such that the children of the node will be visible (the node is "unfolded", a boolean value which defaults to false) when the button is checked (selected == true). Then we add three "details_node" nodes as children of this node. Our second node, "Other scary things" will remain empty for now, so we'll set the visible attribute on its button to "hidden" (which is like false, but with hidden the widget still takes up space).
|