UI Framework - meldavy/ipf-documentation GitHub Wiki
As discussed in my LUA vs XML writeup, creating UI elements through LUA is much easier to experiment with and debug, thus the rest of this page is explained using controls created through LUA.
Refer to https://github.com/meldavy/ipf-documentation/wiki/Drawing-UI-XML-vs-LUA for Frame documentation
| Operation | Description |
|---|---|
frame:GetChild('controlName') |
returns a control that is a direct child of the parent control |
frame:GetChildRecursively('controlName') |
returns a control that is a recursive child of the parent control |
ui.GetFrame('frameName') |
returns a frame that matches a given name. Most Addon events provide frame as the first argument. But when implementing hook methods, asyncronous functions, or other control scripts, you usually don't have a reference to the parent frame. Thus in those case, you can use ui.GetFrame() to get a reference to your addon's frame. |
When accessing or setting properties of a control (such as a control's text or width) after retrieving it through GetChild() or GetChildRecursively(), it needs to be casted to the right type. It has to do with polymorphism; GetChild() and GetChildRecursively() returns CObject type (check global dump) but to access functions specific to each control, ie the IsChecked() function of CCheckBox type, we need to cast CObject to the right type. The easiest way to cast is to call AUTO_CAST():
local frame = ui.GetFrame('myFrame');
local ctrl = frame:GetChildRecursively('myControl');
AUTO_CAST(ctrl); -- this does an in-place cast, no need to create a new variableUsually, it is good practice to AUTO_CAST every control after a Get call.
Controls by default only holds basic metadata of how the control should be displayed on the screen. However, there are times when we want to give additional contextual data to each control. For instance, let's say I want to create a daily content checklist, where I can keep track of my daily dungeon progress. Let's say I have a checkbox for each dungeon type, such as challenge mode, Auto-matching raid, etc. And my goal is to associate each checkbox with a content ID.
The way to do this is using UserValue.
ctrl:SetUserValue("contentID", id)
...
local id = ctrl:GetUserValue("contentID");You can store any number of key-value pairs using this method.
Most controls expose a way to set a text label, using ctrl:SetText(strarg). However, in most cases, the string formatting processor allows you to set color and size.
"{@st42b}{s16}{#f0dcaa}HelloWorld"- Exact structure of
{@font}is unknown. -
{s16}is font size.{s8}for instance is size 8. -
{#ffffff}is font color. Can also be 4 byte hex to include alpha value.
When you want your control to respond to certain UI events (such as drag and drop, click, slide, etc), you need to set the EventScript.
sharePartyCheck:SetEventScript(ui.LBUTTONUP, "SCR_QUEST_SHARE_PARTY_MEMBER");
The first argument to SetEventScript() is the event type, which can be of the following:
ui.DROP,
ui.ENTERKEY,
ui.LBUTTONDBLCLICK,
ui.LBUTTONDOWN,
ui.LBUTTONPRESSED,
ui.LBUTTONUP,
ui.LOST_FOCUS,
ui.MOUSEMOVE,
ui.MOUSEOFF,
ui.MOUSEON,
ui.MOUSEWHEEL,
ui.POP,
ui.RBUTTONDBLCLICK,
ui.RBUTTONDOWN,
ui.RBUTTONUP,
ui.SCROLLand the second argument is the function to call.
Note that some not all controls support all of the above Event types, and I recommend you refer to first-party code examples to see what is supported and what is not supported per control.
The function that is called has the following function signature:
function MY_FUNC(frame, ctrl, strarg, numarg)
endThe function cannot have more than these 4 parameters.
-
frameis the parent frame -
ctrlis the control that the event happened on
By default strarg and numarg are empty. They can be set using the following methods:
ctrl:SetEventScriptArgNumber(ui.LBUTTONUP, questIES.ClassID); -- sets the 4th parameter (numarg)
ctrl:SetEventScriptArgString(ui.LBUTTONUP, "Item"); -- sets the 3rd parameter (strarg)
Each event type (LBUTTONUP, LBUTTONDOWN, SLIDE, etc) has its own EventScript and EventScriptArgs.
...
local timer = frame:CreateOrGetControl("timer", "addontimer", 10, 10);
AUTO_CAST(timer);
timer:Stop();
timer:SetUpdateScript("AUTOAWAKENING_ON_AUTO_ATTEMPT_TIMER_TICK");
timer:Start(1);
end
function MY_FUNCTION(frame)
print("HI");
end | Function | Description |
|---|---|
Stop() |
Stops the timer. |
Start(numarg) |
Starts the timer. numarg is values in seconds, and can be as low as 0.01. When Start() is called, the UpdateScript is immediately invoked, and then is invoked on every next interval determined by numarg until the timer is stopped or the frame is closed. |
SetUpdateScript(strarg) |
Sets the function that is invoked on timer tick. arg must be string name of the function. Function must have single frame parameter. |
SetUpdateScript(strarg) must be called before Start(). Usually, you want to set the update script when you create the Timer control.
The timer stops ticket if the frame it belongs in does not exist. Thus, its parent frame must alive for the Timer to continue ticking. However, even if parent frame is closed, timer:Start() will invoke the UpdateScript once, but will not tick afterwards.
...
local casterInfo = countselectgroup:CreateOrGetControl("richtext", "casterInfo", 40, 40, ui.LEFT, ui.TOP, 0, 7, 0, 0);
casterInfo:SetText("");
casterInfo:SetFormat("%s %s");
casterInfo:AddParamInfo("caster", "");
casterInfo:AddParamInfo("count", "");
casterInfo:SetTextByKey("caster", casterName)
casterInfo:SetTextByKey("count", count)
end| Function | Description |
|---|---|
SetText(strarg) |
Sets the text of the RichText control |
SetFormat(strarg) |
Sets a string formatter for the RichText control. |
AddParamInfo(strarg, vararg) |
Gives a named key for each format placeholder (%s, %d, etc). vararg is I think default value |
SetTextByKey(strarg, vararg) |
Sets the value of a formatter placeholder with a key. |
All RichText must be initialized with SetText() or else behavior can be weird. If you don't know what text it will hold, always initialize with ctrl:SetText("") as an empty string placeholder.
local sharePartyCheck = ctrlset:CreateOrGetControl('checkbox', ...);
AUTO_CAST(sharePartyCheck);
sharePartyCheck:SetCheck(0);
sharePartyCheck:SetCheck(1);
sharePartyCheck:ToggleCheck();
local isChecked = sharePartyCheck:IsChecked(); | Function | Description |
|---|---|
SetCheck(numarg) |
Sets the boolean checked value, either 0 or 1
|
ToggleCheck() |
Toggles the Check value |
IsChecked() |
returns 1 or 0 based on current check value |
Usually, a CheckBox is used together with a ui.LBUTTONDOWN event script, where the event script will check the current value through ctrl:IsChecked() and toggle the checkbox.
[CRadioButton]: table {
[__name]: string = ui::CRadioButton
[tolua_ubox]: table {
} // [tolua_ubox]
[SetCheck]: function()
[SetGroupID]: function()
[InitGroupID]: function()
[IsChecked]: function()
[Select]: function()
[EnableGroup]: function()
[AddToGroup]: function()
[GetGroupID]: function()
[GetSelectedButton]: function()
} // [CRadioButton]RadioButton is used by group. Meaning that if one radiobutton of a group gets checked, all others become unchecked.
It is convention to index the radio buttons by "<controlname>_<index>", ie: "myradiobutton_0", "myradiobutton_1", where all radio buttons of a group share the same name but different index. If that naming convention is followed, you can call the following function to get the index subtext:
GET_RADIOBTN_NUMBER(ctrl); Slots are kindof special type of control because rather than directly manipulating the slot using CSlot functions, it is usually manipulated through the Slot lib or through imcSlot class.
Usually, slots are set with items:
imcSlot:SetItemInfo(slot, iteminfo, count); -- https://github.com/meldavy/ipf-documentation/wiki/Item,-Icon,-and-SlotIt can also be set with an item classID:
SET_SLOT_ITEM_CLS(slot, itemClassID)A caption can be added through ctrl:SetText() but often times the SET_SLOT_COUNT_TEXT function from the slot library is used:
SET_SLOT_COUNT_TEXT(slot, count)
But usually, the goal is to set an image on the slot. (Setting the slot to reflect an item, or a skill icon) At the low level, imcSlot:SetImage() is used.
SET_SLOT_IMG(slot, image)
-- snippet of slot lib
function SET_SLOT_IMG(slot, img)
imcSlot:SetImage(slot, img);
end
-- setting image of an item
itemIES = GetIES(invenItemInfo:GetObject());
SET_SLOT_IMG(slot, GET_ITEM_ICON_IMAGE(itemIES));
-- setting image of a buff
local imageName = GET_BUFF_ICON_NAME(class);
SET_SLOT_IMG(slot, imageName);
local slotset = trackerFrame:GetChild("slotset");
AUTO_CAST(slotset);
slotset:SetColRow(5, 4); -- (5x4 grid)
local col = slotset:GetCol();
local row = slotset:GetRow();
local slot = slotset:GetSlotByIndex(index); [CSlotSet]: table {
[__name]: string = ui::CSlotSet
[tolua_ubox]: table {
} // [tolua_ubox]
[GetSlotWidth]: function()
[ClearSlotAndPullNextSlots]: function()
[GetSelectedSlot]: function()
[GetSlotByIndex]: function()
[AddSlot]: function()
[ClearSelectedSlot]: function()
[SetColRow]: function()
[GetSpcY]: function()
[SetSlotCount]: function()
[GetSelectedSlotCount]: function()
[SwapSlot]: function()
[SetMaxSelectionCount]: function()
[ExpandRow]: function()
[SetSlotSize]: function()
[GetCol]: function()
[GetSlotCount]: function()
[EnableSelection]: function()
[MakeSelectionList]: function()
[SetStartIndex]: function()
[EnableDrop]: function()
[AutoAdjustRow]: function()
[AutoCheckDecreaseRow]: function()
[CreateSlots]: function()
[SetSpc]: function()
[GetSlotHeight]: function()
[GetSpcX]: function()
[CheckSize]: function()
[EnableDrag]: function()
[EnableShowEmpty]: function()
[EnablePop]: function()
[GetRow]: function()
[GetSlotByRowCol]: function()
[GetSelectedIndex]: function()
[GetIconByIndex]: function()
[GetIcon]: function()
[ClearIconAll]: function()
[GetSlot]: function()
} // [CSlotSet]When creating my farm tracker addon, I was trying to get auto adjusting rows working (increase row on demand, reduce rows on demand)
To understand how slotsets work, we need to understand that slotCount ~= row * col. What I mean is that even if we have a row of 1 and col of 5, (a 5*1 grid), there can actually be 20 slots. Or 100. Or less.
ROW AND COLUMN IS ONLY THE DIMENSION OF THE SLOTSET. They are "arbitrary" values that help other functions do their job, but does not actually determine the number of slots. Thus, if we want to increase or decrease rows, we need to manipulate the slotset slightly unintuitively.
Given a slotset with n rows and y columns, you have a slotset with 0 slots, just a bunch of rows and columns. You need to "fill" those rows and columns with slots:
slotset:CreateSlots();Thus, every time you want to expand a row, you must call CreateSlots():
-- recommended
slotset:ExpandRow();
-- verbose, but does same thing
slotset:SetColRow(slotset:GetCol(), slotset:GetRow() + 1);
-- draw the slots
slotset:CreateSlots();Unlike adding new rows, removing rows is slightly different:
slotset:SetSlotCount(slotset:GetSlotCount() - slotset:GetCol()); -- subtracts the slot count of 1 row
slotset:SetColRow(slotset:GetCol(), slotset:GetRow() - 1);Usually, when you want to remove rows, the best thing you can do is just clear the slotset and redraw it complete.
slotset:RemoveAllChild();
slotset:SetColRow(x, y);
slotset:CreateSlots();
... refill your slots with your own logic local droplist = bgbox:CreateOrGetControl("droplist", "droplist", 190, 30, ui.LEFT, ui.TOP, 110, 74, 0, 0);
AUTO_CAST(droplist);
droplist:SetSkinName('droplist_normal'); -- Important
droplist:ClearItems();
droplist:GetSelItemIndex();
droplist:AddItem(index, strarg);
droplist:AddItem(index2, strarg);
droplist:AddItem(index3, strarg); [CDropList]: table {
[__name]: string = ui::CDropList
[GetSelItemIndex]: function()
[tolua_ubox]: table {
} // [tolua_ubox]
[AddItem]: function()
[RemoveItem]: function()
[SetVisibleLine]: function()
[GetSelItemKey]: function()
[SetItemTextByKey]: function()
[SetFrameScrollBarSkinName]: function()
[SetFrameScrollBarOffset]: function()
[GetSelItemCaption]: function()
[SetDropListDefaultOffset]: function()
[SetSelectedScp]: function()
[GetSelItemValue]: function()
[SelectItem]: function()
[SetFrameOffset]: function()
[GetItemCount]: function()
[SelectItemByKey]: function()
[ClearItems]: function()
} // [CDropList]Setting SkinName is quite important, or else droplist is not functional.
Usually, you add items through a loop. Index starts at 0.
local okBtn = box:CreateControl('button', 'ok_btn', 140, 195, 100, 30);
AUTO_CAST(okBtn);
okBtn:SetText(ScpArgMsg('Auto_{@st41b}Hwagin{/}'));
okBtn:SetTooltipType('texthelp');
okBtn:SetTooltipArg(ScpArgMsg('Auto_{@st59}SilBeoLeul_SoMoHayeo_HaeDang_JiyeogeuLo_iDongHapNiDa.{/}'));
okBtn:SetEventScriptArgNumber(ui.LBUTTONDOWN, camp_warp_class.ClassID);
okBtn:SetEventScript(ui.LBUTTONUP, 'WARP_TO_AREA');
okBtn:SetOverSound('button_over');
okBtn:SetClickSound('button_click_stats_ok'); [CButton]: table {
[__name]: string = ui::CButton
[tolua_ubox]: table {
} // [tolua_ubox]
[SetText]: function()
[SetImage]: function()
[SetForceClicked]: function()
[GetImageName]: function()
[EnableResizeByText]: function()
[EnableImageStretch]: function()
[SetIsUpCheckBtn]: function()
} // [CButton]A button is usually incredibly simple. Most buttons operate by setting SetEventScript(ui.LBUTTONUP, func) and processing an on-click event.
A lot of issues that happen in the software industry is when a button is rapidly clicked twice in succession. In the good ol' days, iOS and Android apps crashed if successive button clicks were not handled properly. Similar thing happens in TOS too. If you have a script that runs on button click but you don't want it to happen too quickly in rapid succession, the best tip is to set hittest to 0, then do ReserveScript() to set hittest of the button back to 1 after a short delay to prevent the button from being clicked twice really fast.
picture:SetImage("questinfo_return");
picture:SetEventScript(ui.LBUTTONUP, "QUESTION_QUEST_WARP");
picture:SetEventScriptArgNumber(ui.LBUTTONUP, questIES.ClassID);
picture:EnableHitTest(1);
picture:SetAngleLoop(-3); -- I think this accomplishes a constant spinning effect
picture:SetAngle(angle); -- rotates the pictureA Picture control is most likely set with an image. An image is defined under /ui.ipf container. See baseskinset.xml for examples. Image can be png or tga.
I recommend checking out my Banderilla addon to see how custom images are packaged into an addon.
[CSlideBar]: table {
[SetLevel]: function()
[tolua_ubox]: table {
} // [tolua_ubox]
[SetMinSlideLevel]: function()
[GetMaxLevel]: function()
[__name]: string = ui::CSlideBar
[SetMaxSlideLevel]: function()
[GetLevel]: function()
} // [CSlideBar]Slidebar is usually declared in XML with a SlideScp attribute, a function that is called when the slider value changes. But I don't see an equivalent event in lua. You might be able to use a combination of mouse.IsLBtnPressed() and ui.MOUSEMOVE (not sure if this will work tho)
To get something like the camcon addon working, I think you will need to declare your slidebars through xml unfortunately.
local gauge = ctrlset:CreateOrGetControl('gauge', 'gauge_'..i, startx, y, 200, 40);
AUTO_CAST(gauge);
local curcnt = sObj["QuestInfoValue" .. i];
local needcnt = sObj["QuestInfoMaxCount" .. i];
gauge:SetPoint(curcnt, needcnt);
gauge:AddStat('%v/%m');
gauge:SetStatFont(0, 'white_12_ol');
gauge:SetStatOffset(0, -3, -2);
gauge:SetStatAlign(0, ui.CENTER_HORZ, ui.CENTER_VERT); [CGauge]: table {
[__name]: string = ui::CGauge
[tolua_ubox]: table {
} // [tolua_ubox]
[SetCellPoint]: function()
[SetDrawFillPoint]: function()
[IsBlinking]: function()
[SetPointWithTime]: function()
[SetPoint]: function()
[GetCurPoint]: function()
[SetCautionBlink]: function()
[GetStat]: function()
[IsTimeProcessing]: function()
[SetBlink]: function()
[SetTotalTime]: function()
[StopTimeProcess]: function()
[SetBarImageName]: function()
[SetModeByExport]: function()
[SetInverse]: function()
[ReleaseBlink]: function()
[SetProgressStyle]: function()
[AddTotalTime]: function()
[ReleaseCautionBlink]: function()
[SetStatAlign]: function()
[SetStatOffset]: function()
[SetBarColor]: function()
[SetStatFont]: function()
[GetDestPoint]: function()
[SetProgressSound]: function()
[SetProcessMode]: function()
[SetDrawStyle]: function()
[AddStat]: function()
[SetMaxPointWithTime]: function()
[SetMaxPoint]: function()
[ShowStat]: function()
[SetTextAlign]: function()
[GetMaxPoint]: function()
[SetTextStat]: function()
[SetCurPoint]: function()
} // [CGauge]gauge:SetPoint(currentVal, maxVal);The most important part of a gauge is the SetPoint() method. The rest of the functions are primarily used for visually displaying the gauge. But for simple gauges, all of the gauge functions are pointless (no pun intended) except for SetPoint().
You'll most likely be using gauges with skins, and I recommend you check out my Banderilla addon source for an example.
For usage of other controls not listed above, the best thing you can do is search for usage examples in the ipf dump.