aka.uikit is a collection of different automation functions for the ease of creating UI in Aegisub dialog.
Imports aka.uikit to the scope:
local uikit = require("aka.uikit")
local adialog, abuttons, adisplay = uikit.dialog, uikit.buttons, uikit.display
local adialog, abuttons, adisplay
with require "aka.uikit"
adialog = .dialog
abuttons = .buttons
adisplay = .display
In this tutorial, what's otherwise known as widgets or controls in Aegisub dialog will be referred to as classes, including vanilla classes such as edit
, floatedit
and dropdown
as well as classes unique to aka.uikit such as separator
and columns
. All the options for classes such as x
, y
, name
, label
, value
will be referred to as keys.
Some examples using aka.uikit include a small dialog in aka.99PercentTags (Unfortunately the main dialog of 99%Tags is written prior to aka.uikit), the main dialog of aka.Sandbox, and a series of config dialogs and dependency install dialogs in zah.autoclip.
– Basic components
– aka.uikit.dialog
: Autofill x
, y
and width
key
– aka.uikit.dialog
: A list of all classes
– aka.uikit.dialog
: Automatically filling data into dialog
– aka.uikit.buttons
: Create buttons with button_ids
– aka.uikit.display
: Basic use and display.repeatUntil()
– aka.uikit.dialog
: Group classes together and create reusable templates
– aka.uikit.dialog
: Explain structure of adialog and create custom classes
– aka.uikit.dialog
: Access data in generator functions
aka.uikit has three main components, aka.uikit.dialog
, aka.uikit.buttons
and aka.uikit.display
:
dialog = adialog.new({ width = 6 })
:label({ label = "Hello World!" })
buttons = abuttons.new()
:ok("OK")
:close("Cancel")
display = adisplay(dialog, buttons)
with dialog = adialog { width: 6 }
\label { label: "Hello World!" }
with buttons = abuttons!
\ok "OK"
\close "Cancel"
display = adisplay dialog, buttons
- In the first two lines, a dialog is created with width of 6, containing only one class, a label that will display "Hello World!".
- In the following three lines, an
"OK"
and a"Cancel"
button are created. - In the last line, the dialog and buttons are (ready to be) displayed to the user.
For dialog
, buttons
, you can call resolve()
to get a result in vanilla Aegisub.
vanilla_dialog = dialog:resolve()
vanilla_buttons, vanilla_button_ids = buttons:resolve()
vanilla_dialog = dialog\resolve!
vanilla_buttons, vanilla_button_ids = buttons\resolve!
Or use it on display
to display the dialog to the user:
button, result = adisplay(dialog, buttons):resolve()
button, result = (adisplay dialog, buttons)\resolve!
The usage and functions of each component will be explained separately in following sections.
The first feature of aka.uikit.dialog
is to autofill x
, y
and width
key when creating dialog:
dialog = adialog.new({ width = 5 })
:label({ label = "AAE data:" })
:textbox({ height = 4, name = "aae_data" })
:checkbox({ name = "expand", label = "Expand", value = true })
with dialog = adialog { width: 5 }
\label { label: "AAE data:" }
\textbox { height: 4, name: "aae_data" }
\checkbox { name: "expand", label: "Expand", value: true }
adialog.new({ width = 5 })
: A new dialog table is created with the width of 5. All three child classes will thus have awidth
of 5 and arranged top down in the order they are called.adialog.new({ width = 5 })
is the same asadialog({ width = 5 })
.dialog:label({ label = "AAE data:" })
creates a label, displaying text"AAE data:"
.dialog:label
and all other class methods do not create a new copy and only returns itself. Bothdialog:label()
anddialog = dialog:label()
yields the same result.dialog:textbox({ height = 4, name = "aae_data" })
creates a textbox, with the data name"aae_data"
. Keys for all vanilla classes are the same as in vanilla dialog table. This also includesheight
, which by default is 1 for all classes. With a multiline class liketextbox
, you need to manually set theheight
key to the intended height.dialog:checkbox({ name = "expand", label = "Expand", value = true })
creates a checkbox with data nameexpand
,label
as"Expand data"
, and default value set totrue
.
The code in the example creates the same dialog as following dialog table created in vanilla:
dialog = { { class = "label", x = 0, y = 0, width = 5, label = "AAE data" },
{ class = "textbox", x = 0, y = 1, width = 5, height = 4, name = "aae_data" },
{ class = "checkbox", x = 0, y = 5, width = 5, name = "expand", label = "Expand", value = true } }
You can, however, manually override the x
, y
or width
key if needed:
dialog:checkbox({ x = 1, width = 4, name = "expand", label = "Expand", value = true })
dialog\checkbox { x: 1, width: 4 name: "expand", label: "Expand", value: true }
You can also pass a generator function to modify the x
, y
or width
key. The function will receive the key's natural or default value as parameter:
dialog:checkbox({ x = function(x) return x + 1 end,
width = function(width) return width - 1 end,
name = "expand",
label = "Expand",
value = true })
dialog\checkbox
x: (x) -> x + 1
width: (width) -> width - 1
name: "expand"
label: "Expand"
value: true
Note that even if you have modified x
or y
key, the space the class originally occupies will still be left for it. If you want the class to be floating without occupying space, you could use dialog:floatable
class.
dialog.edit
, dialog.intedit
, dialog.floatedit
, dialog.textbox
, dialog.color
, dialog.coloralpha
, dialog.alpha
For vanilla classes, keys used in aka.uikit.dialog
are the same as in dialog table:
dialog.edit
:
-- Create an edit.
--
-- This method receives parameters in a table.
--
-- All vanilla keys are avilable.
-- `x`, `y`, and `width` keys are optional and will be generated by
-- aka.uikit.
--
-- Additionally, to dynamically modify the dialog:
-- Use the vanilla `name` key.
-- `x`, `y`, `width`, `height`, and `text` keys also accept generator
-- functions.
--
-- @return self
dialog.floatedit
:
-- Create a floatedit.
--
-- This method receives parameters in a table.
--
-- All vanilla keys are avilable.
-- `x`, `y`, and `width` keys are optional and will be generated by
-- aka.uikit.
--
-- Additionally, to dynamically modify the dialog:
-- Use the vanilla `name` key.
-- `x`, `y`, `width`, `height`, and `value` keys also accept generator
-- functions.
--
-- @return self
For these three classes, in addition to vanilla keys, new name
keys are available to dynamically change the label or item list in dropdown.
dialog.label
:
-- Create a label.
--
-- This method receives parameters in a table.
--
-- All vanilla keys are avilable.
-- `x`, `y`, and `width` keys are optional and will be generated by
-- aka.uikit.
--
-- Additionally, to dynamically modify the dialog:
-- @key name_label Change the label dynamically
-- `x`, `y`, `width`, `height`, and `label` keys also accept generator
-- functions.
--
-- @return self
dialog.dropdown
:
-- Create a dropdown.
--
-- This method receives parameters in a table.
--
-- All vanilla keys are avilable.
-- `x`, `y`, and `width` keys are optional and will be generated by
-- aka.uikit.
--
-- Additionally, to dynamically modify the dialog:
-- Use the vanilla `name` key for `value` and the following key:
-- @key name_items Change items dynamically
-- `x`, `y`, `width`, `height`, `value` and `items` keys also accept
-- generator functions.
--
-- @return self
dialog.checkbox
:
-- Create a checkbox.
--
-- This method receives parameters in a table.
--
-- All vanilla keys are avilable.
-- `x`, `y`, and `width` keys are optional and will be generated by
-- aka.uikit.
--
-- Additionally, to dynamically modify the dialog:
-- Use the vanilla `name` key for `value` and the following key:
-- @key name_label Change the label dynamically
-- `x`, `y`, `width`, `height`, `value` and `label` keys also accept
-- generator functions.
--
-- @return self
-- Create a separator or an empty space on the dialog.
-- Note that if there is no more classes below separator, the separator
-- will not have any effect. To create an empty space at the bottom of
-- dialog, use an empty label.
--
-- This method receives parameters in a table.
-- @key [height=1] vertical height of the separator
--
-- @return self
Example:
dialog:separator({ height = 2 })
dialog\separator { height: 2 }
Classes inside a floatable class does not occupy spaces in the main dialog.
-- Create a subdialog for floating classes
--
-- @return subdialog Call methods such as `label` from this
-- subdialog to add floating classes.
-- All floating classes should specify their `x`,
-- `y` and `width` keys.
Example:
subdialog = dialog:floatable()
subdialog:label({ x = 10, y = 10, width = 3, label = "Floating" })
subdialog:label({ x = 10, y = 11, width = 3, label = "Floating" })
subdialog = dialog\floatable!
subdialog\label { x: 10, y: 10, width: 3, label: "Floating" }
subdialog\label { x: 10, y: 11, width: 3, label: "Floating" }
Classes inside an ifable class will only display if the value specified by the name in ifable is truthy or equal to the value provided.
-- Create a subdialog only when value with the name in dialog data is
-- truthy or equal to the value provided.
--
-- This method receives parameters in a table.
-- @key name The name for the value in the dialog data.
-- @key value If this key is not provided, classes in the
-- subdialog will be displayed if value for the
-- name is truthy.
-- If this key is provided and not a function,
-- classes in the subdialog will be displayed if
-- value for the name equals to this key.
-- If this key is a generator function, the
-- function will be called with the current value
-- in the dialog data. Classes in the subdialog
-- will be displayed if the return of the function
-- is truthy.
--
-- @return subdialog Call methods such as `label` from this
-- subdialog to add to ifable.
Classes inside an unlessable class will only display if the value specified by the name in unlessable is falsy or not equal to the value provided.
-- Create a subdialog only when value with the name in dialog data is
-- falsy or not equal to the value provided.
--
-- This method receives parameters in a table.
-- @key name The name for the value in the dialog data.
-- @key value If this key is not provided, classes in the
-- subdialog will be displayed if value for the
-- name is falsey.
-- If this key is provided and not a function,
-- classes in the subdialog will be displayed if
-- value for the name doesn't equal to this key.
-- If this key is a generator function, the
-- function will be called with the current value
-- in the dialog data. Classes in the subdialog
-- will be displayed if the return of the function
-- is falsey.
--
-- @return subdialog Call methods such as `label` from this
-- subdialog to add to unlessable.
An example of using dialog.ifable
with display.repeatUntil()
is as below:
dialog = adialog.new({ width = 5 })
:load_data(previous_data)
-- If err_msg is truely, the label_edit will be displayed with text of err_msg
subdialog = dialog:ifable({ name = "err_msg" })
subdialog = subdialog:label_edit({ label = "Error occuried", name = "err_msg" })
dialog = dialog:label_edit({ label = "URL", name = "url" })
buttons = abuttons.ok("Connect"):close("Cancel")
result = adisplay(dialog, buttons)
:repeatUntil(function(button, result)
response, err, msg = request.send(result["url"], { method = "GET" })
if not response then
result["err_msg"] = "No response"
return err(result)
elseif response.code ~= 200 then
result["err_msg"] = "Target responded with code " .. tostring(response.code)
return err(result)
else
return ok(result)
end end)
local dialog, buttons, result
with dialog = adialog.new { width: 5 }
\load_data previous_data
-- If err_msg is truely, the label_edit will be displayed with text of err_msg
with subdialog = \ifable { name: "err_msg" }
\label_edit { label: "Error occuried", name: "err_msg" }
\label_edit { label: "URL", name: "url" }
with buttons = abuttons.ok "Connect"
\close "Cancel"
with result = adisplay dialog, buttons
\repeatUntil (button, result) ->
-- If the user clicked "Cancel", `repeatUntil()`` will return
-- without calling this function
response = request.send result["url"], { method: "GET" }
if not response
result["err_msg"] = "No response"
return err result
elseif response.code ~= 200
result["err_msg"] = "Target responded with code " .. tostring response.code
return err result
else
return ok result
-- Join another dialog
--
-- @param dialog Note that only classes in the dialog will be joined
-- and other information such as data and width will
-- be discarded.
-- The dialog will be copied inside the function so
-- later modification of the parameter dialog won't
-- affect the dialog joined.
--
-- @return self
By default, aka.uikit.dialog
arranges classes from top to bottom. This method creates columns and enables arranging classes side by side.
-- Create columns to arrange classes side by side
--
-- This method receives parameters in a table.
-- @key widths A table of widths for each columns. The number
-- of width's in this table determines the number
-- of columns created.
-- For example, to create three equally divided
-- columns in a dialog with a total width of 12:
-- dialog:columns({ widths = { 4, 4, 4 } })
-- Also accepts generator function for individual
-- width's. The generator function will received
-- the width of the whole dialog.columns as
-- parameter.
--
-- @return subdialogs For each width in widths param, return a
-- subdialog. Call methods such as `label` from
-- these subdialog to add classes to each column.
Example:
left, middle, right = dialog:columns({ widths = { 2, 2, 3 } })
left:label({ label = "Left" })
middle:label({ label = "Middle" })
right:edit({ name = "edit", text = "Right" })
left, middle, right = dialog\columns { widths = { 2, 2, 3 } }
left\label { label: "Left" }
middle\label { label: "Middle" }
right\edit { name: "edit", text: "Right" }
label_edit
, label_intedit
, label_floatedit
, label_textbox
, label_dropdown
, label_checkbox
, label_color
, label_coloralpha
and label_alpha
It's common for automation scripts to have a edit, intedit, floatedit, etc with a label on the left. This class is a oneliner combining dialog:columns
and respected classes.
dialog.label_edit
:
-- Create an edit with a label on the left.
--
-- This method receives parameters in a table.
-- All keys for edit are the same as in vanilla Aegisub.
-- `x`, `y`, and `width` are optional.
-- Additionally:
-- @key label Text to display for the label.
-- @key name_label Change the label dynamically.
-- @key widths By default, label and edit each takes up half
-- of the width available. Change the widths of
-- two classes using this key.
-- All keys except for `name`s, presumably, also accept generator
-- functions.
--
-- To create this dialog:
-- \fn [ Arial ]
-- Calls:
-- dialog:label_edit({ label = "\\fn", name = "fn", text = "Arial" })
--
-- @return self
dialog.label_floatedit
:
-- Create a floatedit with a label on the left.
--
-- This method receives parameters in a table.
-- All keys for floatedit are the same as in vanilla Aegisub.
-- `x`, `y`, and `width` are optional.
-- Additionally:
-- @key label Text to display for the label.
-- @key name_label Change the label dynamically.
-- @key widths By default, label and edit each takes up half
-- of the width available. Change the widths of
-- two classes using this key.
-- All keys except for `name`s, presumably, also accept generator
-- functions.
--
-- To create this dialog:
-- \frz [ 0. ]
-- Calls:
-- dialog:label_floatedit({ label = "\\frz", name = "frz", value = 0 })
--
-- @return self
dialog.label_dropdown
:
-- Create a dropdown with a label on the left.
--
-- This method receives parameters in a table.
-- All keys for dropdown are the same as in vanilla Aegisub.
-- `x`, `y`, and `width` are optional.
-- Additionally:
-- @key label Text to display for the label.
-- @key name_label Change the label dynamically.
-- @key name_items Change the item list dynamically
-- @key widths By default, label and edit each takes up half
-- of the width available. Change the widths of
-- two classes using this key.
-- All keys except for `name`s, presumably, also accept generator
-- functions.
--
-- @return self
Examples:
dialog:label_edit({ label = "\\fn", name = "fn", text = "Arial" })
dialog:label_floatedit({ label = "\\frz", name = "frz", value = 0 })
dialog:label_textbox({ label = "Data:", height = 2, name = "data", text = "Multiline\nContent" })
dialog:label_checkbox({ label = "Expand", name = "expand", value = true })
dialog\label_edit { label: "\\fn", name: "fn", text: "Arial" }
dialog\label_floatedit { label: "\\frz", name: "frz", value: 0 }
dialog\label_textbox { label: "Data:", height: 2, name: "data", text: "Multiline\nContent" }
dialog\label_checkbox { label: "Expand", name: "expand", value: true }
It's common for automation scripts to prefill the dialog with data, either data from the active subtitle line, or settings from previous run. In vanilla, this is often performed as below:
dialog = { { class = "floatedit", x = 0, y = 0, width = 5,
name = "frz", value = line_data["frz"] },
{ class = "textbox", x = 0, y = 1, width = 5, height = 3,
name = "command", value = previous_data["command"] } }
aka.uikit.dialog
makes this process easy using the dialog:load_data()
method:
dialog = adialog.new({ width = 5 }
:load_data(line_data)
:load_data(previous_data)
:floatedit({ name = "frz" })
:textbox({ height = 3, name = "command" })
)
with dialog = adialog { width: 5 }
\load_data line_data
\load_data previous_data
\floatedit { name: "frz" }
\textbox { height: 3, name: "command" }
The data should be in key-value pairs, in the same format as the second return from aegisub.dialog.display
.
dialog:load_data()
can be called before or after the creation of classes in the dialog, and data in dialog:load_data()
is applied in the resolve()
function.
dialog:load_data()
overrides the default values set in the dialog or values from previous call of dialog:load_data()
. This way if you want to use values from previous run but also need a default value when the user runs the script for the first time, you can write the default value directly to each classes:
dialog = adialog.new({ width = 4 })
:load_data(previous_data) -- Override default value
:label_floatedit({ label = "Strength", name = "strength", min = 0, value = 2 }) -- Default value
dialog = adialog.new { width: 4 }
dialog\load_data previous_data -- Override default value
dialog\label_floatedit { label: "Strength", name: "strength", min: 0, value: 2 } -- Default value
Or have a separate default table:
default_data = { "strength" = 2 }
dialog = adialog.new({ width = 4 })
:load_data(default_data) -- Override default value
:load_data(previous_data) -- Override default_data and default value
:label_floatedit({ label = "Strength", name = "strength", min = 0, value = 2 }) -- Default value
default_data =
strength: 2
dialog = adialog.new { width: 4 }
dialog\load_data default_data -- Override default value
dialog\load_data previous_data -- Override default_data and default value
dialog\label_floatedit { label: "Strength", name: "strength", min: 0, value: 2 } -- Default value
aka.uikit.buttons
automates the process of writing button_ids. Instead of this code in vanilla:
buttons = { "Apply", "&Validate", "&Help", "Close" }
button_ids = { ["ok"] = "Apply", ["close"] = "Close", ["help"] = "&Help" }
You can create buttons with aka.uikit.buttons
as below:
buttons = abuttons.ok("Apply")
:regular("&Validate")
:help("&Help")
:close("Close")
with buttons = abuttons!
\ok "Apply"
\regular "&Validate"
\help "&Help"
\close "Close"
To create a aka.uikit.buttons
instance, either call new
, call the object itself, or directly call any of the buttons using .
instead of :
or \
:
instance1 = abuttons.new()
instance2 = abuttons()
instance3 = abuttons.ok("Apply")
instance4 = abuttons.regular("L&eft")
instance1 = abuttons.new!
instance2 = abuttons!
instance3 = abuttons.ok "Apply"
instance4 = abuttons.regular "L&eft"
These are special buttons in Aegisub that they have button_ids
and will be triggers when the user presses keys such as Enter, return or escape on keyboard. For main functional buttons, these should be preferred over regular buttons (which will be introduced in the next section).
– buttons.ok()
triggers when the user presses Enter or return.
– buttons.close()
triggers when the user presses escape.
– buttons.cancel()
is mutually exclusive with buttons.close()
. The difference between the two is that when the user presses escape, in buttons.close()
the display returns the name of the close button, but in buttons.cancel()
the display returns false
.
– buttons.help()
displays a special help button on Mac.
Buttons are arranged in the order the methods are called.
buttons:ok("Apply")
buttons:close("Close")
buttons\ok "Apply"
buttons\close "Close"
To create a regular button, call buttons.regular()
, buttons.extra()
, or just call the instance itself.
buttons:regular("Configurate")
buttons:extra("Configurate")
buttons("Configurate")
buttons\regular "Configurate"
buttons\extra "Configurate"
buttons "Configurate"
buttons.is_ok()
, buttons.is_close_cancel()
, buttons.is_close()
, buttons.is_cancel()
, and buttons.is_help()
These are functions that will be helpful to process button
return after display:
As an example:
button, result = adisplay(dialog, buttons):resolve()
if buttons:is_ok(button) then
-- ...
elseif button == "Extra button" then
-- ...
elseif buttons:is_close_cancel(button) then
-- ...
end
button, result = (adisplay dialog, buttons)\resolve!
if buttons\is_ok button
-- ...
elseif button == "Extra button"
-- ...
elseif buttons\is_close_cancel button
-- ...
buttons.is_close()
and buttons.is_cancel()
are only aliases for function buttons.is_close_cancel()
and it makes no distinction between close
and cancel
buttons.
To start a display, call aka.uikit.display
or aka.uikit.display.new
:
-- Start a display
--
-- @param dialog dialog from aka.uikit.dialog
-- @param buttons buttons from aka.uikit.buttons
--
-- @return display
To get a button
and result
similar to vanilla in aka.uikit.display
, use display.resolve()
:
-- Display and dialog and get button and result
--
-- @return button Same as in vanilla aegisub.dialog.display
-- @return result Same as in vanilla aegisub.dialog.display
Examples:
button, result = adisplay(dialog, buttons):resolve()
button, result = (adisplay dialog, buttons)\resolve!
aka.uikit.display
also offers methods for more complex situations:
-- Repeatly display the dialog until f returns ok(result)
--
-- @param f function that takes in button and result.
-- It shall returns ok() if the dialog is accepted.
-- Any contents in the ok() is returns out of
-- `repeatUntil()` so you could possibly preprocess
-- the data inside this function.
-- It shall returns err() if the dialog is rejected
-- and the dialog is redisplayed to the user.
-- If you want to display an error message or modify
-- the dialog, you can pass data inside err() and it
-- will be loaded using `dialog:loadData()`.
--
-- @return Result Ok if the dialog is accepted by f. Contains the
-- data returned from f.
-- Err if the user cancel the operation.
Examples:
dialog = adialog.new({ width = 5 })
:load_data(previous_data)
subdialog = dialog:ifable({ name = "err_msg" })
subdialog = subdialog:label_edit({ label = "Error occuried", name = "err_msg" })
dialog = dialog:label_edit({ label = "URL", name = "url" })
buttons = abuttons.ok("Connect"):close("Cancel")
result = adisplay(dialog, buttons)
:repeatUntil(function(button, result)
-- If the user clicked "Cancel", `repeatUntil()`` will return
-- without calling this function
response, err, msg = request.send(result["url"], { method = "GET" })
if not response then
result["err_msg"] = "No response"
return err(result)
elseif response.code ~= 200 then
result["err_msg"] = "Target responded with code " .. tostring(response.code)
return err(result)
else
return ok(result)
end end)
with dialog = adialog.new { width: 5 }
\load_data previous_data
with subdialog = \ifable { name: "err_msg" }
\label_edit { label: "Error occuried", name: "err_msg" }
\label_edit { label: "URL", name: "url" }
with buttons = abuttons.ok "Connect"
\close "Cancel"
with result = adisplay dialog, buttons
\repeatUntil (button, result) ->
-- If the user clicked "Cancel", `repeatUntil()`` will return
-- without calling this function
response, err, msg = request.send result["url"], { method: "GET" }
if not response
result["err_msg"] = "No response"
return err result
elseif response.code ~= 200
result["err_msg"] = "Target responded with code " .. tostring response.code
return err result
else
return ok result
-- Load the content from previous run, display the dialog and save
-- the content for next run.
-- This will only save the content if a button other than close or
-- cancel is triggered.
--
-- @param [name] The subfolder you would want to put the config
-- @param name_supp The name for the config file without the file
-- extension.
-- The subfolder name is an optional parameter and
-- can be ommited in place. Calling the method as
-- `display\loadResolveAndSave filename` is A-OK.
--
-- @return button Same as in vanilla aegisub.dialog.display
-- @return result Same as in vanilla aegisub.dialog.display
This will automatically „Recall last“ in every run. However, this won't work if the dialog contains data unique to active or selected lines since the previous data is loaded at the very last, in which case you should write it manually with dialog.load_data()
. You won't need this function either if you want to write an advanced preset system as in a lot of lyger's scripts.
-- Load the contents from previous run, repeatly display the dialog
-- until f returns ok(result), and then save the result for next run.
--
-- @param [name] The subfolder you would want to put the config
-- @param name_supp The name for the config file without the file
-- extension.
-- The subfolder name is an optional parameter and
-- can be ommited in place. Calling the method as
-- `display\loadRepeatUntilAndSave filename, f` is
-- A-OK.
-- @param f Function that takes in button and result.
-- It shall returns ok(result) if the dialog is
-- You may preprocess the data for further use since
-- the contents inside ok() will be returned out of
-- `loadRepeatUntilAndSave`. However, you also need
-- to return the key-value pairs necessay for the
-- next dialog run.
-- It shall returns err() if the dialog is rejected
-- and the dialog is redisplayed to the user.
-- If you want to display an error message or modify
-- the dialog, you can pass data inside err() and it
-- will be loaded using `dialog:loadData()`.
--
-- @return Result Ok if the dialog is accepted by f. Contains the
-- data returned from f.
-- Err if the user cancel the operation.
Grouping classes together and creating reusable templates is exactly the idea behind adialog.label_xxx
methods such as adialog.label_edit
and adialog.label_dropdown
. It is simply packing the function calls to real classes into a new method:
-----------------------------------------------------------------------
-- Create an edit with a label on the left.
--
-- This method receives parameters in a table.
-- All keys for edit are the same as in vanilla Aegisub.
-- `x`, `y`, and `width` are optional.
-- Additionally:
-- @key label Text to display for the label.
-- @key name_label Change the label dynamically.
-- @key widths By default, label and edit each takes up half
-- of the width available. Change the widths of
-- two classes using this key.
-- All keys except for `name`s, presumably, also accept generator
-- functions.
--
-- To create this dialog:
-- \fn [ Arial ]
-- Calls:
-- dialog:label_edit({ label = "\\fn", name = "fn", text = "Arial" })
--
-- @return self
-----------------------------------------------------------------------
dialog.label_edit = function(self, item)
if item.widths == nil then
item.widths = { function(width, _) return math.ceil(width / 2) end,
function(width, _) return math.floor(width / 2) end }
end
local left, right = self:columns({ x = item.x, y = item.y, width = item.width, widths = item.widths })
item.x = nil item.y = nil item.width = nil item.widths = nil
left:label({ label = item.label })
item.label = nil
right:edit(item)
return self
end
To create your own templates, simply inherit aka.uikit.dialog
and add a new method. The following example creates a position edit with a label
and two floatedit
:
new_dialog = setmetatable({}, { __index = adialog })
new_dialog.new = function(...)
local self = adialog.new(...)
setmetatable(self, { __index = new_dialog })
end
-----------------------------------------------------------------------
-- @key label
-- @key name_label
-- @key name_x
-- @key name_y
-- @key value_x
-- @key value_y
-- @key label_width
-- @key edit_width
--
-- @return self
-----------------------------------------------------------------------
new_dialog.pos_edit = function(self, item)
left, middle, right = self:columns({ widths = { item.label_width, item.edit_width, item.edit_width } })
left:label({ label = item.label, name = item.name_label })
middle:floatedit({ name = item.name_x, value = item.value_x })
right:floatedit({ name = item.name_y, value = item.value_y })
end
class new_dialog extends adialog
-----------------------------------------------------------------------
-- @key label
-- @key name_x
-- @key name_y
-- @key value_x
-- @key value_y
-- @key label_width
-- @key edit_width
--
-- @return self
-----------------------------------------------------------------------
pos_edit: (item) =>
with item
left, middle, right = @columns { widths: { .label_width, .edit_width, .edit_width } }
left\label { label: .label }
middle\floatedit { name: .name_x, value: .value_x }
right\floatedit { name: .name_y, value: .value_y }
Here is a brief insight of how aka.uikit.dialog
works.
Note that in this section, aka.uikit.dialog
is called adialog
instance and the vanilla Aegisub table is called dialog table.
The idea of aka.uikit.dialog
is that the dialog will be represented by a tree shaped structure inside adialog
instance for the ease of adding classes, until some resolve
functions clamp this tree down and add in the x
, y
and width
value to generate Aegisub dialog table.
For example this simple dialog:
dialog = adialog.new({ width = 10 })
:load_data({ ["edit"] = "EDIT" })
:label({ label = "LABEL_A" })
subdialog_L, subdialog_R = dialog:columns({ widths = { 5, 5 } })
subdialog_L:label({ label = "LABEL_B" })
subdialog_R:label({ label = "LABEL_C" })
dialog:label({ label = "LABEL_D" })
local dialog
with dialog = adialog { width = 10 }
\load_data { ["edit"] = "EDIT" }
\label { label = "LABEL_A" }
subdialog_L, subdialog_R = dialog\columns { widths = { 5, 5 } }
subdialog_L\label { label = "LABEL_B" }
subdialog_R\label { label = "LABEL_C" }
dialog\label { label = "LABEL_D" }
Creates a structure like this:
dialog → dialog_mt → dialog_resolver
├╶["resolve"] function
├╶["width"] 10
├╶["data"] { ["edit"] = "EDIT" }
├╶[1] vanilla_label_resolver → vanilla_base_resolver
│ ├╶["resolve"] function
│ └╶["label"] "LABEL_A"
├╶[2] columns_resolver
│ ├╶["resolve"] function
│ ├╶["widths"] {5, 5}
│ └╶["columns"]
│ ├╶[1] subdialog → subdialog_mt → subdialog_resolver
│ │ ├╶["resolve"] function
│ │ └╶[1] vanilla_label_resolver → vanilla_base_resolver
│ │ ├╶["resolve"] function
│ │ └╶["label"] "LABEL_B"
│ └╶[2] subdialog → subdialog_mt → subdialog_resolver
│ ├╶["resolve"] function
│ └╶[1] vanilla_label_resolver → vanilla_base_resolver
│ ├╶["resolve"] function
│ └╶["label"] "LABEL_C"
└╶[3] vanilla_label_resolver → vanilla_base_resolver
├╶["resolve"] function
└╶["label"] "LABEL_D"
Note that every level of tables here all have a resolve function. For dialog_resolver.resolve
and subdialog_resolver.resolve
, the only thing they need to do is to call the resolve
function of every one of its ipairs
members.
subdialog_resolver.resolve = function(self, dialog, x, y, width)
for i, v in ipairs(self) do
y = v:resolve(dialog, x, y, width)
end
return y
end
In aka.uikit.dialog
, classes are arranged from top to bottom. That means all classes in the same dialog or subdialog will be fed with the same x
and width
. For y
, every resolvers will take the current y
, decide how many height they will need, and return y
plus height
for the next class down the line.
The vanilla_label_resolver
has a lot to do. Although the label
key has been provided by the user, the user may use name_label
key to dynamically updating the label. It also need to sort out x
, y
, width
and insert it to the resulting dialog table:
vanilla_label_resolver.resolve = function(item, dialog, x, y, width)
item = Table.copy(item)
item.class = last_class:match(item.class)[2]["str"]
vanilla_base_resovler.x_y_width_height_resolve(item, dialog, "x", x)
vanilla_base_resovler.x_y_width_height_resolve(item, dialog, "y", y)
vanilla_base_resovler.x_y_width_height_resolve(item, dialog, "width", width)
vanilla_base_resovler.x_y_width_height_resolve(item, dialog, "height", 1)
vanilla_base_resovler.value_resolve(item, dialog, "name_label", "label")
table.insert(dialog, item)
return y + item.height
end
-- For the two function it calls:
vanilla_base_resovler.x_y_width_height_resolve = function(item, dialog, key, default_value)
if item[key] == nil then
item[key] = default_value
elseif type(item[key]) == "function" then
item[key] = item[key](default_value, dialog["data"])
else
do end
end end
vanilla_base_resovler.value_resolve = function(item, dialog, name_key, value_key)
if item[name_key] ~= nil and dialog["data"][item[name_key]] ~= nil then
if type(dialog["data"][item[name_key]]) == "function" then
item[value_key] = dialog["data"][item[name_key]](item[value_key], dialog["data"])
else
item[value_key] = dialog["data"][item[name_key]]
end
else
if type(item[value_key]) == "function" then
item[value_key] = item[value_key](nil, dialog["data"])
end end
item[name_key] = nil
end
-- ...
The item
parameter in this function is self
, vanilla_label_resolver
instance with label
as string and resolve
as function.
Pay special attention that we copied item
before making any changes to it. This is important as the adialog
instance and every classes in it should be considered immutable. This is because in aka.uikit.display.repeatUtil
and many other usages, the same adialog
instance will be used to generate dialog table repeatly.
The dialog
parameter is the Aegisub dialog table that the instance is resolving to, as in the example, item
is inserted into the dialog
table after processing.
However, to open access to dialog["data"]
from adialog.load_data()
and other potential information, the dialog
table inherits the original adialog
instance. This means dialog["data"]
is not a copy of data
in original adialog
instance, but through __index
metamethod they are the same table. For the same reason as item
parameter, everything that is inherited in this dialog table should be considered immutable.
To insert such resolver into adialog
:
dialog.label = function(self, item)
if item == nil then item = {} end
setmetatable(item, { __index = vanilla_label_resolver })
item.class = "_au_label"
table.insert(self, item)
return self
end
Custom classes can be created similarly and implementations for other aka.uikit.dialog
classes are available in the source.
In conclusion, a class in aka.uikit.dialog
is a Lua table with a resolve()
function which when called, prepares and inserts the results as vanilla dialog classes into dialog table. resolve()
function accepts self
, the resulting dialog
table with __index
metamethod set to original adialog
instance and x
, y
and width
for the class, and returns y
for the next class to resolve.
Many classes in adialog
accepts generator functions. b
For generator functions on x
, y
and width
key, the function will receive the normal x, y, or width value generated by aka.uikit
in its first parameter.
For generator functions on height
key, the function will receive 1
in its first parameter.
For generator functions on value
, text
, label
or equivalent keys, if the function is passed in at the time of setting up the dialog through adialog
method calls, the function will receive nil
as its first parameter. However, if the function is passed in through adialog.load_data
and thereby picked up by name
, name_label
or equivalent, it will receive the value passed in at the time of setting up the dialog.
As an example:
rotate = function(value) return value + 90 end
data = { ["degree"] = rotate }
dialog = adialog.new({ width = 5 })
:label_floatedit({ label = "Degree:" name = "degree", value = 90 })
:load_data(data)
rotate = (value) -> value + 90
data = { degree: rotate }
with dialog = adialog { width: 5 }
\label_floatedit { label: "Degree:", name: "degree", value: 90 }
\load_data data
When this example dialog is displayed, it will show 180 in the floatedit.
However, something that has not be introduced by now is that all generator functions receive dialog["data"]
as the second parameter. As is exaplained in previous section, this dialog["data"]
should considered immutable.
As an example:
err_dialog:textbox({ height = 4,
value = function(_, data)
return "Error occurred\n" .. data["err"] end })
err_dialog\textbox
height: 4
value: (_, data) -> "Error occurred\n" .. data["err"]