enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
896 lines
35 KiB
Lua
896 lines
35 KiB
Lua
-- Bagshui Category Management UI
|
|
|
|
Bagshui:AddComponent(function()
|
|
local Categories = BsCategories
|
|
|
|
-- Pull the list of valid player class category fields from the Category object skeleton.
|
|
local CLASS_CATEGORY_FIELDS = {}
|
|
for fieldName, _ in pairs(BS_CATEGORY_SKELETON.classes[BsGameInfo.characterClasses]) do
|
|
table.insert(CLASS_CATEGORY_FIELDS, fieldName)
|
|
end
|
|
|
|
--- Subclass override for InitUI() to handle class-specific details.
|
|
function Categories:InitUi()
|
|
-- ObjectList:InitUi() has the same check, but let's avoid doing any unnecessary work
|
|
-- since the superclass function isn't called immediately.
|
|
if self.objectManager then
|
|
return
|
|
end
|
|
|
|
local categories = self
|
|
|
|
-- Custom menus.
|
|
self.menus = Bagshui.prototypes.Menus:New()
|
|
|
|
-- New Category menu.
|
|
self.menus:AddMenu(
|
|
"NewCategory",
|
|
-- Level 1 contains entries for a blank category and creating from a template.
|
|
{
|
|
{
|
|
text = L.Category,
|
|
func = function()
|
|
self.objectManager:NewObject()
|
|
end,
|
|
notCheckable = true,
|
|
},
|
|
{
|
|
text = L.ClassCategory,
|
|
func = function()
|
|
self.objectManager:NewObject(BS_NEW_CLASS_CATEGORY_TEMPLATE)
|
|
end,
|
|
notCheckable = true,
|
|
},
|
|
},
|
|
-- Nothing for level 2/3.
|
|
nil,
|
|
nil
|
|
)
|
|
|
|
-- Character Class dropdown menu.
|
|
|
|
--- Helper function for player class dropdown to prevent UIDropDownMenu_Refresh from adding a check mark.
|
|
---@param dropDown table Dropdown menu WoW UI object.
|
|
local function clearDropDownCheckMarks(dropDown)
|
|
dropDown.selectedID = nil
|
|
dropDown.selectedValue = nil
|
|
dropDown.selectedName = nil
|
|
-- Removing any existing check marks.
|
|
for i = 1, _G.UIDROPDOWNMENU_MAXBUTTONS do
|
|
_G["DropDownList1Button" .. i]:UnlockHighlight()
|
|
_G["DropDownList1Button" .. i .. "Check"]:Hide()
|
|
end
|
|
end
|
|
|
|
--- Helper function for player class dropdown to update the selection when an item is clicked.
|
|
---@param dropDown table Dropdown menu WoW UI object.
|
|
---@param value any Value of the item that was clicked.
|
|
local function dropDownMenuSetSelectedValue(dropDown, value)
|
|
dropDown.bagshuiData.selectedValue = value
|
|
_G.UIDropDownMenu_SetSelectedValue(dropDown, value)
|
|
clearDropDownCheckMarks(dropDown)
|
|
end
|
|
|
|
--- Callback function for clicking an item in the player class menu.
|
|
---@param arg1 table Will always be classMenuArg1 as defined below.
|
|
local function classMenuFunc(arg1)
|
|
dropDownMenuSetSelectedValue(arg1.dropDown, _G.this.value)
|
|
arg1.editor:SetClass(_G.this.value)
|
|
end
|
|
|
|
local classMenu = BsUtil.TableCopy(BsGameInfo.characterClassMenu)
|
|
-- Reusable table for passing values through to the callback function.
|
|
local classMenuArg1 = {}
|
|
|
|
-- Generate the menu items and add the menu.
|
|
for _, menuItem in ipairs(classMenu) do
|
|
menuItem.arg1 = classMenuArg1
|
|
menuItem.notCheckable = true
|
|
menuItem.func = classMenuFunc
|
|
end
|
|
|
|
self.menus:AddMenu(
|
|
"Classes",
|
|
classMenu,
|
|
-- Nothing for level 2/3.
|
|
nil,
|
|
nil,
|
|
-- OpenMenu callback.
|
|
function(menu, dropDown, editor)
|
|
clearDropDownCheckMarks(dropDown)
|
|
classMenuArg1.dropDown = dropDown
|
|
classMenuArg1.editor = editor
|
|
end
|
|
)
|
|
|
|
-- Prepare the list of available bags.
|
|
-- It would be nice to do this in Config\RuleFunctionTemplates.lua,
|
|
-- but that's not happening without a lot of refactoring.
|
|
BsUtil.TableClear(BsRules.ruleFunctionTemplates.Bag)
|
|
for _, inventoryType in ipairs(BS_INVENTORY_TYPE_UI_ORDER) do
|
|
local inventory = Bagshui.components[inventoryType]
|
|
local sequentialBagSlotNum = 1
|
|
for _, containerId in ipairs(inventory.containerIds) do
|
|
local ruleFunctionCode = string.format("Bag(%s)", containerId)
|
|
table.insert(BsRules.ruleFunctionTemplates.Bag, {
|
|
-- Example: "Bank: Bag(10)"
|
|
text = string.format(
|
|
"%s%s:%s %s",
|
|
NORMAL_FONT_COLOR_CODE,
|
|
inventory.inventoryTypeLocalized,
|
|
FONT_COLOR_CODE_CLOSE,
|
|
ruleFunctionCode
|
|
),
|
|
code = ruleFunctionCode,
|
|
description = string.format(
|
|
L.RuleFunction_Bag_ExampleDescription,
|
|
containerId,
|
|
sequentialBagSlotNum,
|
|
inventory.inventoryTypeLocalized
|
|
),
|
|
})
|
|
sequentialBagSlotNum = sequentialBagSlotNum + 1
|
|
end
|
|
end
|
|
-- Need to pick up the multi-bag example and add that too.
|
|
if L_nil["RuleFunction_Bag_ExampleMulti"] then
|
|
table.insert(BsRules.ruleFunctionTemplates.Bag, {
|
|
code = L_nil["RuleFunction_Bag_ExampleMulti"],
|
|
description = L_nil["RuleFunction_Bag_ExampleDescriptionMulti"],
|
|
})
|
|
end
|
|
|
|
-- Construct the Rule Function menu for the category editor.
|
|
-- There's some dumb hackery going on here because I didn't feel like refactoring the auto-split menu code.
|
|
|
|
--- Callback function for the Rule Function menu.
|
|
--- arg1 and arg2 change depending on whether it's called from a normal menu item or an auto-split menu.
|
|
--- ```none
|
|
--- Normal:
|
|
--- arg1 = editBox
|
|
--- arg2 = code to insert
|
|
--- Auto-split:
|
|
--- arg1 = {
|
|
--- templateList = list of templates where arg2 can be found
|
|
--- editBox = editBox
|
|
--- }
|
|
--- arg2 = template Id
|
|
--- ```
|
|
---@param arg1 any See above.
|
|
---@param arg2 any See above.
|
|
local function insertRuleFunctionTemplate(arg1, arg2)
|
|
local codeToInsert, editBox
|
|
if arg1.templateList then
|
|
-- Extra item hack.
|
|
if type(arg2) == "table" and arg2.extra then
|
|
codeToInsert = arg2.code
|
|
else
|
|
codeToInsert = arg1.code or arg1.templateList[arg2].code
|
|
end
|
|
editBox = arg1.editBox
|
|
else
|
|
codeToInsert = arg2
|
|
editBox = arg1
|
|
end
|
|
if type(editBox) ~= "table" or not codeToInsert then
|
|
return
|
|
end
|
|
if not editBox.GetText then
|
|
Bagshui:PrintError("Couldn't find edit box (this shouldn't happen!)")
|
|
return
|
|
end
|
|
local editBoxContent = (editBox:GetText() or "")
|
|
-- Separate functions with " or " unless there's already an and/or present.
|
|
if string.len(editBoxContent) > 0 then
|
|
if not (string.find(editBoxContent, "%s+or%s*$") or string.find(editBoxContent, "%s+and%s*$")) then
|
|
editBoxContent = editBoxContent .. " or"
|
|
end
|
|
if not string.find(editBoxContent, "%s$") then
|
|
editBoxContent = editBoxContent .. " "
|
|
end
|
|
end
|
|
|
|
editBoxContent = editBoxContent .. codeToInsert
|
|
editBox:SetText(editBoxContent)
|
|
end
|
|
|
|
--- Using the given list of Rule Function templates, create a menu table.
|
|
--- This is broken out into a function because there used to be separate
|
|
--- buttons for builtin and 3rd party rule functions. That was changed because
|
|
--- it didn't seem like a necessary distinction. It wasn't worth removing this
|
|
--- function though.
|
|
---@param sortedRuleFunctionTemplateNames string[] Ordered array of rule function template names.
|
|
---@param ruleFunctionTemplates table<string, table> List of `{ <function name> = { { template 1 }, { template N } }`.
|
|
---@param ruleFunctionTemplateExtraEntries table<string, table>? List of `{ <function name> = { { template 1 }, { template N } }` that will be added to the bottom of the template menu.
|
|
---@param ruleFunctionTemplateGenericDescriptions table<string, string> List of `{ <function name> = "Description"] }` Tooltip for the rule function's parent menu item.
|
|
local function buildRuleFunctionMenu(
|
|
ruleFunctionMenus,
|
|
sortedRuleFunctionTemplateNames,
|
|
ruleFunctionTemplates,
|
|
ruleFunctionTemplateExtraEntries,
|
|
ruleFunctionTemplateGenericDescriptions
|
|
)
|
|
BsUtil.TableClear(ruleFunctionMenus)
|
|
local ruleFunctionMenu
|
|
|
|
for _, functionName in ipairs(sortedRuleFunctionTemplateNames) do
|
|
local templates = ruleFunctionTemplates[functionName]
|
|
if templates then
|
|
-- Whenever the menu gets filled up, roll over to a new one.
|
|
if not ruleFunctionMenu or table.getn(ruleFunctionMenu) >= _G.UIDROPDOWNMENU_MAXBUTTONS then
|
|
table.insert(ruleFunctionMenus, {})
|
|
ruleFunctionMenu = ruleFunctionMenus[table.getn(ruleFunctionMenus)]
|
|
end
|
|
|
|
if table.getn(templates) == 1 then
|
|
-- When there's only one template, no submenu is needed.
|
|
table.insert(ruleFunctionMenu, {
|
|
text = templates[1].text or templates[1].code,
|
|
tooltipTitle = templates[1].tooltipTitle or templates[1].text or templates[1].code,
|
|
tooltipText = templates[1].description,
|
|
func = insertRuleFunctionTemplate,
|
|
notCheckable = true,
|
|
arg2 = templates[1].code,
|
|
})
|
|
else
|
|
-- Create a bespoke auto-split menu for each rule function's templates.
|
|
-- With some auto-split menu refactoring this could be cleaner, but it's not a priority.
|
|
|
|
-- Prepare the "extra" examples (usually multi-parameter) as extra menu items
|
|
-- that will be added to the bottom of the menu. When there are enough templates
|
|
-- to force the creation of multiple menus, these will appear at the bottom of each.
|
|
local extraMenuItems = {}
|
|
if type(ruleFunctionTemplateExtraEntries) == "table" and ruleFunctionTemplateExtraEntries[functionName] then
|
|
for i, template in ipairs(ruleFunctionTemplateExtraEntries[functionName]) do
|
|
table.insert(extraMenuItems, {
|
|
text = template.text or template.code,
|
|
tooltipTitle = template.tooltipTitle or template.text or template.code,
|
|
tooltipText = template.description,
|
|
notCheckable = true,
|
|
value = {
|
|
func = insertRuleFunctionTemplate,
|
|
objectId = {
|
|
editBox = nil,
|
|
},
|
|
},
|
|
-- Stupid hack to get the data we need across to `insertRuleFunctionTemplate()`.
|
|
arg2 = {
|
|
extra = true,
|
|
code = template.code,
|
|
},
|
|
})
|
|
end
|
|
end
|
|
-- Need the extra count so we know how much to limit the maximum
|
|
-- entries per menu by.
|
|
local extraMenuItemCount = table.getn(extraMenuItems)
|
|
|
|
local templateNum = 0
|
|
local listNum = 0
|
|
-- We're going to keep looping until we've created enough menus
|
|
-- to account for all the templates. This is usually only necessary
|
|
-- for PeriodicTable when AtlasLoot is loaded.
|
|
repeat
|
|
-- Need to re-check the menu on every loop.
|
|
if table.getn(ruleFunctionMenu) >= _G.UIDROPDOWNMENU_MAXBUTTONS then
|
|
Bagshui:PrintError("Too many rule functions for editor menu")
|
|
break
|
|
end
|
|
|
|
listNum = listNum + 1
|
|
local autoSplitMenuType = "RuleFunction:" .. functionName .. listNum
|
|
local templateList = templates -- Upvalue for nameFunc.
|
|
local ruleFunctionName = functionName -- Upvalue for parentMenuNameTrimFunc.
|
|
local autoSplitMenuIdList = {}
|
|
|
|
-- Populate this menu up to the maximum number of entries we can fit.
|
|
for i = 1, ((_G.UIDROPDOWNMENU_MAXBUTTONS - extraMenuItemCount) * _G.UIDROPDOWNMENU_MAXBUTTONS) do
|
|
templateNum = templateNum + 1
|
|
if not templates[templateNum] then
|
|
break
|
|
end
|
|
table.insert(autoSplitMenuIdList, templateNum)
|
|
end
|
|
|
|
-- Auto-split submenu.
|
|
Bagshui.prototypes.Menus:AddAutoSplitMenu(autoSplitMenuType, {
|
|
defaultIdList = autoSplitMenuIdList,
|
|
parentMenuNameTrimFunc = function(menuName)
|
|
-- Remove FunctionName( from the beginning of the string.
|
|
return string.gsub(menuName, ruleFunctionName .. "%([\"']", "")
|
|
end,
|
|
nameFunc = function(id)
|
|
return templateList[id].text or templateList[id].code
|
|
end,
|
|
tooltipTitleFunc = function(id)
|
|
return templateList[id].tooltipTitle or templateList[id].text or templateList[id].code
|
|
end,
|
|
tooltipTextFunc = function(id)
|
|
return templateList[id].description
|
|
end,
|
|
extraItems = extraMenuItems,
|
|
})
|
|
|
|
-- Parent menu item.
|
|
table.insert(ruleFunctionMenu, {
|
|
text = functionName .. "(...)",
|
|
tooltipTitle = functionName .. "(...)",
|
|
tooltipText = (ruleFunctionTemplateGenericDescriptions[functionName] or "")
|
|
.. (templates.aliasTooltipAddendum or ""),
|
|
hasArrow = true,
|
|
notCheckable = true,
|
|
value = {
|
|
autoSplitMenuType = autoSplitMenuType,
|
|
func = insertRuleFunctionTemplate,
|
|
notCheckable = true,
|
|
objectId = {
|
|
templateList = templateList,
|
|
editBox = nil,
|
|
},
|
|
},
|
|
})
|
|
|
|
until not templates[templateNum]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Create the Rule Function menus.
|
|
|
|
local ruleFunctionMenus = {}
|
|
|
|
buildRuleFunctionMenu(
|
|
ruleFunctionMenus,
|
|
BsRules.sortedRuleFunctionTemplateNames,
|
|
BsRules.ruleFunctionTemplates,
|
|
BsRules.ruleFunctionTemplatesExtra,
|
|
BsRules.ruleFunctionTemplateGenericDescriptions
|
|
)
|
|
|
|
for i, ruleFunctionMenu in ipairs(ruleFunctionMenus) do
|
|
self.menus:AddMenu(
|
|
"RuleFunctionTemplates" .. i,
|
|
ruleFunctionMenu,
|
|
nil,
|
|
nil,
|
|
-- The pre-open callback handles getting information across to the func callback.
|
|
function(menu, editBox, _)
|
|
for _, menuItem in pairs(menu.levels[1]) do
|
|
if type(menuItem.value) == "table" then
|
|
-- Auto-split menus pass arg1 via value.objectId.
|
|
menuItem.value.objectId.editBox = editBox
|
|
else
|
|
-- Normal menus pass editBox through to func as the first argument.
|
|
menuItem.arg1 = editBox
|
|
end
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
-- Custom object editor for Categories.
|
|
local categoryEditor = {}
|
|
|
|
-- Calls ObjectList:InitUi().
|
|
self._super.InitUi(
|
|
self,
|
|
nil, -- No custom Object Manager needed.
|
|
categoryEditor, -- This is our custom Object Editor.
|
|
-- All the overrides.
|
|
{
|
|
-- Basic Object Manager settings.
|
|
|
|
managerWidth = 475,
|
|
managerHeight = 600,
|
|
|
|
managerColumns = {
|
|
{
|
|
field = "name",
|
|
title = L.ObjectManager_Column_Name,
|
|
widthPercent = "65",
|
|
currentSortOrder = "ASC",
|
|
lastSortOrder = "ASC",
|
|
},
|
|
{
|
|
field = "sequence",
|
|
title = L.ObjectManager_Column_Sequence,
|
|
align = "RIGHT",
|
|
widthPercent = "10",
|
|
lastSortOrder = "ASC",
|
|
},
|
|
{
|
|
field = "builtin",
|
|
title = L.ObjectManager_Column_Source,
|
|
widthPercent = "16",
|
|
lastSortOrder = "ASC",
|
|
},
|
|
{
|
|
field = "inUse",
|
|
title = L.ObjectManager_Column_InUse,
|
|
widthPercent = "9",
|
|
lastSortOrder = "ASC",
|
|
},
|
|
},
|
|
|
|
-- Add error indicator to category name.
|
|
managerColumnTextFunc = function(fieldName, categoryId, category, preliminaryDisplayValue)
|
|
if fieldName == "name" and categories.errors[categoryId] then
|
|
return preliminaryDisplayValue .. BS_FONT_COLOR.UI_ORANGE .. " <!>" .. FONT_COLOR_CODE_CLOSE
|
|
end
|
|
return preliminaryDisplayValue
|
|
end,
|
|
|
|
-- Category Manager Add button shows a menu instead of just creating a new category.
|
|
managerAddButtonOnClick = function()
|
|
self.menus:OpenMenu("NewCategory", _G.this, nil, _G.this, 0, 0, "TOPLEFT", "BOTTOMLEFT")
|
|
end,
|
|
|
|
-- Basic Object Editor settings.
|
|
|
|
editorWidth = 550,
|
|
editorHeight = 565,
|
|
|
|
-- Order of fields in editor UI.
|
|
editorFields = {
|
|
"name",
|
|
"nameSort",
|
|
"sequence",
|
|
"rule",
|
|
"list",
|
|
},
|
|
|
|
-- Configuration for editor fields.
|
|
editorFieldProperties = {
|
|
name = {
|
|
required = true,
|
|
},
|
|
sequence = {
|
|
required = true,
|
|
},
|
|
rule = {
|
|
widgetType = "ScrollableEditBox",
|
|
widgetHeight = 150,
|
|
monospace = true,
|
|
},
|
|
list = {
|
|
widgetType = "ItemList",
|
|
widgetWidth = 260,
|
|
widgetHeight = 250,
|
|
multiSelect = true,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
---comment Override NewEditor so we can modify the interface.
|
|
---@return table objectEditorInstance
|
|
function self.objectManager:NewEditor()
|
|
-- Create the ObjectEditor instance.
|
|
local editor = self._super.NewEditor(self)
|
|
local ui = editor.ui
|
|
|
|
-- Assume it's not a class category.
|
|
editor.isClassCategory = false
|
|
|
|
-- Trigger rule validation when the EditBox loses focus
|
|
editor.editBoxes.rule:SetScript("OnEditFocusLost", function()
|
|
editor:UpdateState(true)
|
|
end)
|
|
|
|
-- Place the Rule Function template menu buttons under the Rule label, to the left of the code box.
|
|
local nextAnchorToFrame = editor.labelWidgetPairs.rule.bagshuiData.labelFrame
|
|
local nextButtonSpacing = -BsSkin.toolbarSpacing
|
|
local i = 0
|
|
local menuName
|
|
repeat
|
|
i = i + 1
|
|
menuName = "RuleFunctionTemplates" .. i
|
|
if categories.menus.menuList[menuName] and table.getn(categories.menus.menuList[menuName].levels[1]) > 0 then
|
|
local menuToOpen = menuName -- Upvalue for onClick.
|
|
editor.buttons["ruleFunction" .. i] = ui:CreateIconButton({
|
|
name = editor.nameAndNumber .. "AddRuleFunction" .. i,
|
|
parentFrame = editor.content,
|
|
texture = i == 1 and "RuleFunction" or "Ellipsis",
|
|
anchorPoint = "TOPRIGHT",
|
|
anchorToFrame = nextAnchorToFrame,
|
|
anchorToPoint = "BOTTOMRIGHT",
|
|
xOffset = 0,
|
|
yOffset = nextButtonSpacing,
|
|
tooltipAnchor = "ANCHOR_PRESERVE",
|
|
tooltipAnchorPoint = "TOPRIGHT",
|
|
tooltipAnchorToPoint = "TOPLEFT",
|
|
tooltipTitle = L.CategoryEditor_AddRuleFunction,
|
|
|
|
onClick = function()
|
|
categories.menus:OpenMenu(
|
|
menuToOpen, -- Menu name.
|
|
editor.labelWidgetPairs.rule.bagshuiData.widget.bagshuiData.editBox, -- callback arg1.
|
|
nil, -- callback arg2.
|
|
_G.this, -- anchorFrame.
|
|
0,
|
|
0, -- offsets.
|
|
"TOPRIGHT",
|
|
"BOTTOMLEFT" -- anchor points.
|
|
)
|
|
end,
|
|
})
|
|
nextAnchorToFrame = editor.buttons["ruleFunction" .. i]
|
|
nextButtonSpacing = -BsSkin.toolbarTightSpacing
|
|
end
|
|
until not categories.menus.menuList[menuName]
|
|
|
|
-- Help button goes below the Rule Function buttons.
|
|
ui:CreateIconButton({
|
|
name = editor.nameAndNumber .. "Help",
|
|
parentFrame = editor.content,
|
|
texture = "Question",
|
|
anchorPoint = "TOPRIGHT",
|
|
anchorToFrame = nextAnchorToFrame,
|
|
anchorToPoint = "BOTTOMRIGHT",
|
|
xOffset = 0,
|
|
yOffset = -BsSkin.toolbarGroupSpacing,
|
|
tooltipAnchor = "ANCHOR_PRESERVE",
|
|
tooltipAnchorPoint = "TOPRIGHT",
|
|
tooltipAnchorToPoint = "TOPLEFT",
|
|
tooltipTitle = L["CategoryEditor_RuleFunctionWiki"],
|
|
|
|
onClick = function()
|
|
ui:ShowUrl(BS_WIKI_URL .. BS_WIKI_PAGES.Rules)
|
|
end,
|
|
})
|
|
|
|
-- Validation button / status indicator will be positioned at the bottom left of the code box.
|
|
editor.buttons.ruleValidation = ui:CreateIconButton({
|
|
name = editor.nameAndNumber .. "RuleValidation",
|
|
parentFrame = editor.content,
|
|
texture = "Compile",
|
|
anchorPoint = "RIGHT",
|
|
anchorToFrame = editor.labelWidgetPairs.rule.bagshuiData.label,
|
|
anchorToPoint = "RIGHT",
|
|
xOffset = 0,
|
|
yOffset = 0,
|
|
tooltipTitle = L.CategoryEditor_RuleValidation_Validate,
|
|
tooltipAnchor = "ANCHOR_PRESERVE",
|
|
tooltipAnchorPoint = "BOTTOMRIGHT",
|
|
tooltipAnchorToPoint = "BOTTOMLEFT",
|
|
tooltipXOffset = -5,
|
|
tooltipYOffset = -6,
|
|
noTooltipDelay = true,
|
|
noTooltipTextDelay = true,
|
|
|
|
onClick = function()
|
|
if _G.arg1 == "LeftButton" then
|
|
if editor.ruleInvalid then
|
|
BsLogWindow:Open()
|
|
else
|
|
editor:UpdateState(true)
|
|
_G.this:GetScript("OnEnter")()
|
|
end
|
|
elseif _G.arg1 == "RightButton" then
|
|
BsLogWindow:Open()
|
|
end
|
|
end,
|
|
})
|
|
editor.buttons.ruleValidation:RegisterForClicks("LeftButtonDown", "RightButtonDown")
|
|
editor.buttons.ruleValidation:Disable()
|
|
-- Set the second anchor point for the rule validation button so that it ends up at the bottom left of the edit box
|
|
editor.buttons.ruleValidation:SetPoint(
|
|
"BOTTOM",
|
|
editor.labelWidgetPairs.rule.bagshuiData.widget,
|
|
"BOTTOM",
|
|
0,
|
|
BsSkin.toolbarTightSpacing
|
|
)
|
|
|
|
-- Character class selector for class categories (hidden by default).
|
|
local classSelectorWidth = 85
|
|
local classSelector = ui:CreateDropDownMenuButton(
|
|
editor.nameAndNumber .. "ClassSelector",
|
|
editor.content,
|
|
classSelectorWidth,
|
|
categories.menus,
|
|
"Classes",
|
|
editor -- Pass through to the OpenMenu callback as arg2, which will pass through to func as arg2, which will call editor:SetClass().
|
|
)
|
|
editor.labelWidgetPairs.class = ui:CreateLabeledWidget(
|
|
editor.content, -- Parent.
|
|
string.format(L.Symbol_Colon, L.CategoryEditor_Field_class), -- Label text.
|
|
editor.labelWidth, -- This property is added by InitUi().
|
|
classSelector, -- Widget.
|
|
classSelectorWidth, -- Widget width.
|
|
editor.manager.editorDimensions.widgetHeight,
|
|
"TOPLEFT",
|
|
editor.labelWidgetPairs.sequence, -- Anchor to frame.
|
|
"BOTTOMLEFT" -- Anchor to point.
|
|
)
|
|
editor.labelWidgetPairs.class:Hide()
|
|
|
|
-- Add ID to bottom.
|
|
|
|
local idEditBox = ui:CreateEditBox(
|
|
"objectId",
|
|
editor.content,
|
|
nil,
|
|
"NumberFontNormalSmallGray",
|
|
false, -- multiline.
|
|
true, -- noBackgroundOrBorder.
|
|
true -- selectAllOnFocus.
|
|
)
|
|
-- Ensure text is visible when highlighted.
|
|
local oldOnFocusGained = idEditBox:GetScript("OnEditFocusGained")
|
|
idEditBox:SetScript("OnEditFocusGained", function()
|
|
oldOnFocusGained()
|
|
_G.this:SetTextColor(1, 1, 1)
|
|
end)
|
|
-- Clear highlight on focus lost.
|
|
local oldOnFocusLost = idEditBox:GetScript("OnEditFocusLost")
|
|
idEditBox:SetScript("OnEditFocusLost", function()
|
|
oldOnFocusLost()
|
|
_G.this:HighlightText(0, 0)
|
|
_G.this:SetTextColor(0.35, 0.35, 0.35)
|
|
end)
|
|
|
|
editor.idDisplay = ui:CreateLabeledWidget(
|
|
editor.content, -- Parent.
|
|
string.format(L.Symbol_Colon, L.ID), -- Label text.
|
|
20, -- Label width.
|
|
idEditBox, -- Widget.
|
|
self.editorDimensions.widgetWidth, -- Widget width.
|
|
self.editorDimensions.widgetHeight, -- Widget height.
|
|
"BOTTOMLEFT", -- Anchor point.
|
|
editor.content, -- Anchor to frame.
|
|
"BOTTOMLEFT", -- Anchor to point.
|
|
nil, -- X offset.
|
|
nil, -- Y offset.
|
|
L.CategoryEditor_ID_TooltipTitle,
|
|
L.CategoryEditor_ID_TooltipText
|
|
)
|
|
editor.idDisplay.bagshuiData.labelFrame:SetWidth(editor.idDisplay.bagshuiData.label:GetStringWidth())
|
|
editor.idDisplay.bagshuiData.widget:ClearAllPoints()
|
|
editor.ui:SetPoint(
|
|
editor.idDisplay.bagshuiData.widget,
|
|
"LEFT",
|
|
editor.idDisplay.bagshuiData.labelFrame,
|
|
"RIGHT",
|
|
0,
|
|
0
|
|
)
|
|
editor.idDisplay.bagshuiData.label:SetFontObject("GameFontDisableSmall")
|
|
local font = (editor.idDisplay.bagshuiData.label:GetFont())
|
|
editor.idDisplay.bagshuiData.label:SetTextColor(0.35, 0.35, 0.35)
|
|
editor.idDisplay.bagshuiData.label:SetFont(font, 10)
|
|
font = (editor.idDisplay.bagshuiData.widget:GetFont())
|
|
editor.idDisplay.bagshuiData.widget:SetTextColor(0.35, 0.35, 0.35)
|
|
editor.idDisplay.bagshuiData.widget:SetFont(font, 10)
|
|
|
|
-- Store the editor frame height so it can be adjusted up and down based on whether it's a class category
|
|
editor.uiFrame.bagshuiData.originalHeight = editor.uiFrame:GetHeight()
|
|
|
|
return editor
|
|
end
|
|
|
|
--- Change the Editor interface based on whether it's a class category.
|
|
function categoryEditor:UpdateUiForClassCategories()
|
|
-- Only class categories have a classes property.
|
|
self.isClassCategory = self.referenceObject.classes ~= nil
|
|
|
|
local editorHeight = self.uiFrame.bagshuiData.originalHeight
|
|
|
|
-- Show/hide the player class selector.
|
|
if self.isClassCategory then
|
|
self.labelWidgetPairs.class:Show()
|
|
self.labelWidgetPairs.class.bagshuiData.widget:Show() -- Widgets aren't parented to the holding frame so we need to show/hide them separately.
|
|
self.labelWidgetPairs.rule:SetPoint("TOPLEFT", self.labelWidgetPairs.class, "BOTTOMLEFT", 0, -10)
|
|
editorHeight = editorHeight + self.labelWidgetPairs.class:GetHeight() + 10
|
|
else
|
|
self.labelWidgetPairs.class:Hide()
|
|
self.labelWidgetPairs.class.bagshuiData.widget:Hide() -- Widgets aren't parented to the holding frame so we need to show/hide them separately.
|
|
self.labelWidgetPairs.rule:SetPoint("TOPLEFT", self.labelWidgetPairs.sequence, "BOTTOMLEFT", 0, -10)
|
|
end
|
|
|
|
self.uiFrame:SetHeight(editorHeight)
|
|
end
|
|
|
|
--- For class categories, Update the editor field storage redirect table to point
|
|
--- to the storage for the selected player class.
|
|
---@param class string Player class
|
|
function categoryEditor:SetClass(class)
|
|
if not self.isClassCategory or not class then
|
|
return
|
|
end
|
|
|
|
if not self.updatedObject.classes[class] then
|
|
self.updatedObject.classes[class] = {}
|
|
end
|
|
|
|
-- Redirect all changes to point to this player class storage.
|
|
for _, field in ipairs(CLASS_CATEGORY_FIELDS) do
|
|
self.fieldStorageRedirect[field] = self.updatedObject.classes[class]
|
|
self:PopulateField(field, self.updatedObject.classes[class])
|
|
end
|
|
end
|
|
|
|
--- Category-specific work for loading an object into the editor.
|
|
---@param objectId string|number? See ObjectEditor:Load() definition.
|
|
---@param duplicate boolean? See ObjectEditor:Load() definition.
|
|
---@param template table? See ObjectEditor:Load() definition.
|
|
---@param refresh boolean? See ObjectEditor:Load() definition.
|
|
---@param onFirstSave function? See ObjectEditor:Load() definition.
|
|
---@return string|number # See ObjectEditor:Load() definition.
|
|
function categoryEditor:Load(objectId, duplicate, template, refresh, onFirstSave)
|
|
-- When loading a new category, reset isClassCategory first because the loading
|
|
-- process ends up calling UpdateState(), which needs to know whether it's actually a class category
|
|
if not refresh then
|
|
self.isClassCategory = false
|
|
end
|
|
|
|
local newObjectId = self._super.Load(self, objectId, duplicate, template, refresh, onFirstSave)
|
|
|
|
local classDropDown = self.labelWidgetPairs.class.bagshuiData.widget
|
|
|
|
if refresh then
|
|
-- On refresh, need to update field redirects because tables can change on save
|
|
self:SetClass(classDropDown.bagshuiData.selectedValue)
|
|
else
|
|
-- Do initial adjustments for standard/Class categories
|
|
self:UpdateUiForClassCategories()
|
|
|
|
-- Set the initially selected class to that of the current character
|
|
if self.isClassCategory then
|
|
dropDownMenuSetSelectedValue(classDropDown, Bagshui.currentCharacterInfo.class)
|
|
self:SetClass(Bagshui.currentCharacterInfo.class)
|
|
end
|
|
end
|
|
|
|
-- Update ID.
|
|
self.ui:SetEditBoxTextReadOnly(
|
|
self.idDisplay.bagshuiData.widget,
|
|
type(self.objectId) == "string" and string.format('"%s"', self.objectId) or self.objectId
|
|
)
|
|
|
|
return newObjectId
|
|
end
|
|
|
|
--- Special handling is needed for the Category editor UI state because we
|
|
--- have to keep track of whether the rule function code validates.
|
|
---@param forceRuleValidation boolean Run the rule function code through validation even if it hasn't changed.
|
|
function categoryEditor:UpdateState(forceRuleValidation)
|
|
self._super.UpdateState(self)
|
|
|
|
-- Set supplemental button state based whether the object is read-only.
|
|
local buttonNum = 0
|
|
for i = 1, 10 do
|
|
local button = self.buttons["ruleFunction" .. i]
|
|
if not button then
|
|
break
|
|
end
|
|
button[(self.originalObject.readOnly and "Disable" or "Enable")](button)
|
|
end
|
|
|
|
local ruleEditBox = self.editBoxes.rule
|
|
local ruleValidationButton = self.buttons.ruleValidation
|
|
|
|
-- Prepare some defaults.
|
|
local rule = BsUtil.Trim(ruleEditBox:GetText() or "")
|
|
local ruleValidationButtonEnabled = string.len(rule) > 0 -- Rule can't be validated unless there's code.
|
|
local ruleValidationButtonTexture = "Compile"
|
|
local ruleValidationButtonColor = BS_COLOR.YELLOW
|
|
local ruleValidationButtonTooltipTitle = L.CategoryEditor_RuleValidation_Validate
|
|
local ruleValidationButtonTooltipText
|
|
|
|
-- Rule validation button should only be enabled when there's rule text.
|
|
ruleValidationButton[(ruleValidationButtonEnabled and "Enable" or "Disable")](ruleValidationButton)
|
|
|
|
local ruleValid, errorMessage
|
|
|
|
-- Determine whether the rule code is valid.
|
|
if
|
|
-- Automatically validate the rule if it's not being edited.
|
|
(ruleValidationButtonEnabled and not ruleEditBox.bagshuiData.hasFocus)
|
|
-- Also need to validate if the button was clicked.
|
|
or forceRuleValidation
|
|
then
|
|
ruleValid, errorMessage = BsRules:Validate(rule)
|
|
elseif ruleEditBox.bagshuiData.hasFocus and not self.dirtyFields.rule then
|
|
-- Revert to last saved validation state if the rule hasn't been edited.
|
|
errorMessage = categories.errors[self.originalObject.categoryId]
|
|
ruleValid = not errorMessage
|
|
end
|
|
|
|
-- Only change the validation icon if validation state is known.
|
|
self.ruleInvalid = false
|
|
if ruleValidationButtonEnabled and ruleValid ~= nil then
|
|
if ruleValid then
|
|
ruleValidationButtonTexture = "Check"
|
|
ruleValidationButtonColor = BS_COLOR.UI_GREEN
|
|
ruleValidationButtonTooltipTitle = L.CategoryEditor_RuleValidation_Valid
|
|
else
|
|
self.ruleInvalid = true
|
|
ruleValidationButtonTexture = "Exclamation"
|
|
ruleValidationButtonColor = BS_COLOR.UI_ORANGE
|
|
ruleValidationButtonTooltipTitle = L.CategoryEditor_RuleValidation_Invalid
|
|
ruleValidationButtonTooltipText = errorMessage
|
|
Bagshui:Log(
|
|
Categories:FormatErrorMessage(errorMessage, self.objectId, self.updatedObject.name),
|
|
L.CategoryEditor,
|
|
BS_LOG_MESSAGE_TYPE.ERROR
|
|
)
|
|
end
|
|
end
|
|
|
|
-- Update the validation button.
|
|
self.ui:SetIconButtonTexture(
|
|
ruleValidationButton,
|
|
"Icons\\" .. ruleValidationButtonTexture,
|
|
ruleValidationButtonColor
|
|
)
|
|
ruleValidationButton.bagshuiData.tooltipTitle = ruleValidationButtonTooltipTitle
|
|
ruleValidationButton.bagshuiData.tooltipText = ruleValidationButtonTooltipText
|
|
end
|
|
|
|
--- Helper function for categoryEditor:IsDirty() to determine when a class
|
|
--- category doesn't contain any player class-specific data.
|
|
---@param classObject table Category object to check.
|
|
---@return boolean isPlayerClassDataEmpty
|
|
local function classCategoryObjectIsEmpty(classObject)
|
|
-- A player class object being nil is the same thing as it existing and having empty rule and list properties.
|
|
return (classObject == nil) or (string.len(classObject.rule or "") == 0 and table.getn(classObject.list) == 0)
|
|
end
|
|
|
|
--- In addition to the normal dirty checks, class categories require additional checks.
|
|
---@return boolean
|
|
function categoryEditor:IsDirty()
|
|
local dirty = self._super.IsDirty(self)
|
|
|
|
-- Additional checks for class categories.
|
|
if self.isClassCategory then
|
|
for class, _ in pairs(BsGameInfo.characterClasses) do
|
|
local originalClassObject = self.originalObject.classes and self.originalObject.classes[class]
|
|
local updatedClassObject = self.updatedObject.classes[class]
|
|
|
|
if originalClassObject or updatedClassObject then
|
|
-- When something has changed, we need to know whether it's significant.
|
|
if self:IsObjectDirty(originalClassObject, updatedClassObject) then
|
|
-- Changes can only be meaningful if both the player class object
|
|
-- for both original and updated objects isn't empty.
|
|
if
|
|
not (classCategoryObjectIsEmpty(originalClassObject) and classCategoryObjectIsEmpty(updatedClassObject))
|
|
then
|
|
-- Do deep inspection of each class category field to see if there
|
|
-- are real differences. This is basically the same thing that happens
|
|
-- at the top level, but we need to only do the comparison if the other
|
|
-- checks have been satisfied to avoid false positives.
|
|
for _, fieldName in ipairs(CLASS_CATEGORY_FIELDS) do
|
|
if
|
|
self:IsObjectDirty(
|
|
(originalClassObject and originalClassObject[fieldName] or nil),
|
|
(updatedClassObject and updatedClassObject[fieldName] or nil)
|
|
)
|
|
then
|
|
dirty = true
|
|
self.dirtyFields[fieldName] = _G.GetTime()
|
|
else
|
|
self.dirtyFields[fieldName] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return dirty
|
|
end
|
|
|
|
-- Override for `ObjectEditor:CloseMenusAndClearFocuses()` to deal with the ID display.
|
|
function categoryEditor:CloseMenusAndClearFocuses(menus, editBoxes, listSelections)
|
|
self._super.CloseMenusAndClearFocuses(self, menus, editBoxes, listSelections)
|
|
|
|
if editBoxes ~= false then
|
|
self.idDisplay.bagshuiData.widget:ClearFocus()
|
|
end
|
|
end
|
|
end
|
|
end)
|