--[[ writes to options field of aura data, which is then read to construct the user config panel Values in aura data: authorOptions -> array of options. config -> key/value hash table. key, and format of value are defined by author, and the precise value is defined by the user. This table gets copied into the aura's script environment via aura_env.config. authorMode -> bool, used to determine if author or user mode is displayed in Custom Options tab. option -> table with fields: type (required) -> string such as "toggle", "slider", "string", "number", "color", etc. key (required) -> string which custom scripts can use to read the selected option default (required) -> default value of the option name (required) -> displayed name in the user config panel width (required) -> number between 0.1 and 2 (softMin of 0.5). Determines the width of the option. useDesc (optional) -> bool. If false, then the tooltip will not be used. desc (optional) -> string to be displayed in the option tooltip When options are merged together (i.e. when the user multiselects and then opens the custom options tab), there is one additional field: references -> childID <=> optionID map, used to dereference to the proper option table in setters Supported option types, and additional fields that each type supports/requires: group -> represents a group of options. useCollapse (optional) -> if true, then group will have a collapsible header in user mode. collapse (optional) -> whether or not the collapsible header begins collapsed when the user begins a session. subOptions (required) -> array of options groupType (required) -> type of group: simple -> group is for organizational purposes only. config value is a sub config array -> group represents an array of entries from the user, with similar information between them: config value is arranged as an array of sub configs, one for each entry in the array. limitType (required) -> Specifies if user can add or remove entries from the array freely size (optional) -> required if the limitType is not "none". description -> dummy option which can be used to display some text. Not interactive, and so key/default/name are not set or required. text (required) -> text displayed on the panel fontSize (optional) -> fontSize. Default is medium. space -> dummy option which acts as a spacer. Not interactive, and so key/default/name are not set or required. useWidth (required) -> bool. If false, then the space is given full width in AceConfig. Else, option.width is used. useHeight (required) -> bool. If false, then the space covers only the line it renders on. Else, it covers the number of lines specified. height (optional) -> number. Height of space, in lines. separator -> AceConfig header widget. useName (required) -> bool. If true, then option.text is used as the name. test (optional) -> string. Text to be shown on the header widget. input -> text field which the user can input a string. length (optional) -> allowed length of the string. If set, then input longer than the allowed length will be trimmed number -> text field which the user can type in a number. Input is converted to a number value. max (optional) -> maximum allowed value. If set, then input greater than the maximum will be clamped min (optional) -> minimum allowed value. If set, then input lesser than the minimum will be clamped step (optional) -> stepsize select -> dropdown menu with author-specified strings to select. values (required) -> array of strings to select. config value will be the index corresponding to the string. toggle -> checkbutton which can be in a state of true or false, corresponding to checked and unchecked color -> color selector. Color is delivered as an {r, g, b, a} array. range -> slider element which allows the user to select a number value. max (optional) -> maximum allowed value. If set, then input greater than the maximum will be clamped min (optional) -> minimum allowed value. If set, then input lesser than the minimum will be clamped softmax (optional) -> Like max, but the manual entry will accept values up to the softmax. softmin (optional) -> Like min, but the manual entry will accept values down to the softmin. bigStep (optional) -> step size of the slider. Defaults to 0.05 step (optional) -> like bigStep, but applies to number input as well ]] if not WeakAuras.IsLibsOK() then return end ---@type string local AddonName = ... ---@class OptionsPrivate local OptionsPrivate = select(2, ...) ---@class WeakAuras local WeakAuras = WeakAuras local L = WeakAuras.L local tinsert, tremove, tconcat = table.insert, table.remove, table.concat local conflictBlue = "|cFF4080FF" local conflict = {} -- magic value local function atLeastOneSet(references, key) for _, optionData in pairs(references) do local childOption = optionData.options[optionData.index] if childOption[key] ~= nil then return true end end end local function neq(a, b) if type(a) == "table" and type(b) == "table" then for k, v in pairs(a) do if neq(v, b[k]) then return true end end for k, v in pairs(b) do if neq(v, a[k]) then return true end end else return a ~= b end end -- blues the name if there are conflicts between the references for this value local function name(option, key, name, phrase) local header = name or phrase if option[key] ~= nil or not atLeastOneSet(option.references, key) then return header else return conflictBlue .. header end end -- blue if at least one member of the group does not have an option in the references local function nameHead(data, option, phrase) if not data.controlledChildren then return phrase else for child in OptionsPrivate.Private.TraverseLeafs(data) do if not option.references[child.id] then return conflictBlue .. phrase end end end return phrase end local function nameUser(option) local firstValue for id, optionData in pairs(option.references) do local childConfig = optionData.config if not childConfig then return option.name elseif firstValue == nil then firstValue = childConfig[option.key] elseif neq(firstValue, childConfig[option.key]) then return conflictBlue .. option.name end end return option.name end local function nameUserDesc(option) if option.text then return option.text else local text = {} for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] if childOption.text and childOption.text ~= nil then tinsert(text, childOption.text) end end return conflictBlue .. tconcat(text, "\n") end end local function nameArray(option, array, index, phrase) local value for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] if not childOption[array] then return conflictBlue .. phrase elseif childOption[array][index] == nil then return conflictBlue .. phrase elseif value == nil then value = childOption[array][index] elseif value ~= childOption[array][index] then return conflictBlue .. phrase end end return phrase end -- provides a tooltip showing all the conflicting values if there are any local function desc(option, key, phrase) if option[key] or not atLeastOneSet(option.references, key) then return phrase else local desc = {} if phrase then desc[1] = phrase end tinsert(desc, L["Values:"]) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data if childOption[key] ~= nil then tinsert( desc, ("%s #%i: %s"):format(childData.id, optionData.path[#optionData.path], tostring(childOption[key])) ) end end return tconcat(desc, "\n") end end local function descType(option) local desc = { L["This setting controls what widget is generated in user mode."], L["Used in Auras:"] } for id, optionData in pairs(option.references) do tinsert(desc, ("%s - Option %i"):format(id, optionData.path[#optionData.path])) end return tconcat(desc, "\n") end local function descSelect(option, key) if option.values then return "" else local desc = {L["Values:"]} for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] if childOption.values[key] ~= nil then tinsert( desc, ("%s %i: - %s"):format(id, optionData.path[#optionData.path], tostring(childOption.values[key])) ) end end return tconcat(desc, "\n") end end local function descColor(option, key) if option[key] or not atLeastOneSet(option.references, key) then return L["Values are in normalized rgba format."] else local desc = { L["Values are in normalized rgba format."], L["Values:"] } for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] if childOption[key] ~= nil then tinsert( desc, ("%s #%i: %.2f %.2f %.2f %.2f"):format( id, childOption.path[#childOption.path], unpack(childOption[key]) ) ) end end return tconcat(desc, "\n") end end local function descUser(option) if option.useDesc ~= nil and option.desc ~= nil then return option.useDesc and option.desc or nil else local desc = {} for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] if childOption.useDesc and childOption.desc and childOption.desc ~= "" then tinsert(desc, ("%s - %s"):format(id, childOption.desc)) end end return tconcat(desc, "\n") end end local function descArray(option, array, index, phrase) local desc, values, isConflict = {phrase}, {}, false local initialValue = nil for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] values[id] = tostring(childOption[array][index]) if initialValue == nil then initialValue = values[id] elseif values[id] ~= initialValue then isConflict = true end end if isConflict then for id, value in pairs(values) do tinsert(desc, ("%s - %s"):format(id, value)) end end return tconcat(desc, "\n") end -- getters for AceConfig local function get(option, key) return function() return option[key] end end local function getUser(option) return function() local value for _, optionData in pairs(option.references) do if not optionData.config then return elseif value == nil then value = optionData.config[option.key] elseif neq(value, optionData.config[option.key]) then return end end return value end end local function getStr(option, key) return function() local str = option[key] or "" return str:gsub("|", "||") end end local function getNumAsString(option, key) return function() if option[key] ~= nil then return tostring(option[key]) end end end local function getUserNumAsString(option) return function() local value for id, optionData in pairs(option.references) do if value == nil then value = optionData.config[option.key] elseif neq(value, optionData.config[option.key]) then return "" end end if value ~= nil then return tostring(value) end end end local function getValues(option) local values = {} local firstChild = true for _, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childValues = childOption.values local i = 1 while i <= #values or i <= #childValues do if firstChild then values[i] = childValues[i] elseif values[i] ~= childValues[i] then values[i] = conflict end i = i + 1 end firstChild = false end return values end local function getUserValues(option) local values = getValues(option) for i, v in ipairs(values) do if v == conflict then values[i] = conflictBlue .. L["Value %i"]:format(i) end end return values end local function getColor(option, key) return function() if option[key] then return unpack(option[key]) end end end local function getUserColor(option) return function() local firstValue for id, optionData in pairs(option.references) do local childConfig = optionData.config if firstValue == nil then firstValue = childConfig[option.key] elseif neq(firstValue, childConfig[option.key]) then return end if firstValue then return unpack(firstValue) end end end end local function getArrayStr(option, array, index) return function() if option[array][index] then return option[array][index]:gsub("|","||") else return "" end end end -- setters for AceConfig local function set(data, option, key) return function(_, value) for _, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption[key] = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setUser(data, option) return function(_, value) for _, optionData in pairs(option.references) do local childData = optionData.data local childConfig = optionData.config childConfig[option.key] = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setStr(data, option, key) return function(_, value) value = value:gsub("||", "|") for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption[key] = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setNum(data, option, key, required) return function(_, value) if value ~= "" then local num = tonumber(value) if not num or math.abs(num) == math.huge or tostring(num) == "nan" then return end for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption[key] = num WeakAuras.Add(childData) end elseif not required then for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption[key] = nil WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setUserNum(data, option) return function(_, value) if value ~= "" then local num = tonumber(value) if not num or math.abs(num) == math.huge or tostring(num) == "nan" then return end for _, optionData in pairs(option.references) do local childData = optionData.data local childConfig = optionData.config childConfig[option.key] = num WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end end local function setColor(data, option, key) return function(_, r, g, b, a) local color = {r, g, b, a} for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption[key] = color WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setUserColor(data, option) return function(_, r, g, b, a) local color = {r, g, b, a} for id, optionData in pairs(option.references) do local childData = optionData.data local childConfig = optionData.config childConfig[option.key] = color WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setSelectDefault(data, option, key) return function(_, value) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.default = min(value, #childOption.values) WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function setArrayStr(data, option, array, index) return function(_, value) value = value:gsub("||","|") for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption[array][index] = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function ensureUniqueKey(candidate, suffix, options, index) index = index or 1 local goodKey = true local key = candidate local existingKeys = {} for _, option in ipairs(options) do if option.key then if option.key == key then goodKey = false end existingKeys[option.key] = true end end if not goodKey then local prefix = candidate .. suffix while not goodKey do key = prefix .. index goodKey = not existingKeys[key] index = index + 1 end end return key end local function generateKey(prefix, options, index) return ensureUniqueKey(prefix, "", options, index) end local typeControlAdders, addAuthorModeOption typeControlAdders = { toggle = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "default"] = { type = "select", width = WeakAuras.normalWidth, name = name(option, "default", L["Default"]), desc = desc(option, "default"), order = order(), values = OptionsPrivate.Private.bool_types, get = function() if option.default == nil then return end return option.default and 1 or 0 end, set = function(_, value) local val = value == 1 for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.default = val WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } end, input = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "default"] = { type = "input", width = WeakAuras.normalWidth, name = name(option, "default", L["Default"]), desc = desc(option, "default"), order = order(), get = get(option, "default"), set = set(data, option, "default") } args[prefix .. "useLength"] = { type = "toggle", width = WeakAuras.normalWidth, name = name(option, "useLength", L["Max Length"]), desc = desc(option, "useLength"), order = order(), get = get(option, "useLength"), set = set(data, option, "useLength") } args[prefix .. "length"] = { type = "range", control = "WeakAurasSpinBox", width = WeakAuras.normalWidth, name = name(option, "length", L["Length"]), desc = desc(option, "length"), order = order(), min = 1, step = 1, softMax = 20, get = get(option, "length"), set = set(data, option, "length"), disabled = function() return not option.useLength end } args[prefix .. "multiline"] = { type = "toggle", width = WeakAuras.doubleWidth, name = name(option, "multiline", L["Large Input"]), desc = desc(option, "multiline", L["If checked, then the user will see a multi line edit box. This is useful for inputting large amounts of text."]), order = order(), get = get(option, "multiline"), set = set(data, option, "multiline"), } end, number = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "default"] = { type = "input", width = WeakAuras.normalWidth, name = name(option, "default", L["Default"]), desc = desc(option, "default"), order = order(), get = getNumAsString(option, "default"), set = setNum(data, option, "default", true) } args[prefix .. "min"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "min", L["Min"]), desc = desc(option, "min"), order = order(), get = getNumAsString(option, "min"), set = setNum(data, option, "min") } args[prefix .. "max"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "max", L["Max"]), desc = desc(option, "min"), order = order(), get = getNumAsString(option, "max"), set = setNum(data, option, "max") } args[prefix .. "step"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "step", L["Step Size"]), desc = desc(option, "step"), order = order(), get = getNumAsString(option, "step"), set = setNum(data, option, "step") } end, range = function(options, args, data, order, prefix, i) local option = options[i] local min, max, softMin, softMax, step, bigStep softMax = option.softMax softMin = option.softMin bigStep = option.bigStep min = option.min max = option.max local effectiveMin = softMin or min or 0 local effectiveMax = softMax or max or 100 if (effectiveMin > effectiveMax) then -- This will cause a error inside the slider -- Fix up either softMax or max, depending on which one is the effective one if softMax then softMax = effectiveMin elseif max then max = effectiveMin else softMax = effectiveMin end end step = option.step args[prefix .. "default"] = { type = "range", control = "WeakAurasSpinBox", width = WeakAuras.normalWidth, name = name(option, "default", L["Default"]), desc = desc(option, "default"), order = order(), get = get(option, "default"), set = set(data, option, "default"), min = min, max = max, step = step, softMin = softMin, softMax = softMax, bigStep = bigStep, } args[prefix .. "min"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "min", L["Min"]), desc = desc(option, "min"), order = order(), get = getNumAsString(option, "min"), set = setNum(data, option, "min") } args[prefix .. "max"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "max", L["Max"]), desc = desc(option, "max"), order = order(), get = getNumAsString(option, "max"), set = setNum(data, option, "max") } args[prefix .. "step"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "step", L["Step Size"]), desc = desc(option, "step"), order = order(), get = getNumAsString(option, "step"), set = setNum(data, option, "step") } args[prefix .. "softmin"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "softMin", L["Soft Min"]), desc = desc(option, "softMin"), order = order(), get = getNumAsString(option, "softMin"), set = setNum(data, option, "softMin") } args[prefix .. "softmax"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "softMax", L["Soft Max"]), desc = desc(option, "softMax"), order = order(), get = getNumAsString(option, "softMax"), set = setNum(data, option, "softMax") } args[prefix .. "bigstep"] = { type = "input", width = WeakAuras.normalWidth * 2 / 3, name = name(option, "bigStep", L["Slider Step Size"]), desc = desc(option, "bigStep"), order = order(), get = getNumAsString(option, "bigStep"), set = setNum(data, option, "bigStep") } end, description = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "key"] = nil args[prefix .. "name"] = nil args[prefix .. "fontsize"] = { type = "select", width = WeakAuras.normalWidth, name = name(option, "fontSize", L["Font Size"]), desc = desc(option, "fontSize"), order = order(), values = OptionsPrivate.Private.font_sizes, get = get(option, "fontSize"), set = set(data, option, "fontSize") } args[prefix .. "descinput"] = { type = "input", width = WeakAuras.doubleWidth, name = name(option, "text", L["Description Text"]), desc = desc(option, "text"), order = order(), multiline = true, get = getStr(option, "text"), set = setStr(data, option, "text") } end, color = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "default"] = { type = "color", width = WeakAuras.normalWidth, hasAlpha = true, name = name(option, "default", L["Default"]), desc = descColor(option, "default"), order = order(), get = getColor(option, "default"), set = setColor(data, option, "default") } end, select = function(options, args, data, order, prefix, i) local option = options[i] local values = getValues(option) local defaultValues = {} for i, v in ipairs(values) do if v == conflict then defaultValues[i] = conflictBlue .. L["Value %i"]:format(i) else defaultValues[i] = v end end args[prefix .. "default"] = { type = "select", width = WeakAuras.normalWidth, name = name(option, "default", L["Default"]), desc = desc(option, "default"), order = order(), values = defaultValues, get = get(option, "default"), set = setSelectDefault(data, option) } for j, value in ipairs(values) do args[prefix .. "space" .. j] = { type = "toggle", width = WeakAuras.normalWidth, name = L["Value %i"]:format(j), order = order(), disabled = function() return true end, get = function() return true end, set = function() end } args[prefix .. "value" .. j] = { type = "input", width = WeakAuras.normalWidth - 0.15, name = (value == conflict and conflictBlue or "") .. L["Value %i"]:format(j), desc = descSelect(option, j), order = order(), get = function() if value ~= conflict then return value:gsub("|", "||") end end, set = function(_, value) value = value:gsub("||", "|") for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data local insertPoint = math.min(j, #childOption.values + 1) if value == "" then tremove(childOption.values, insertPoint) else childOption.values[insertPoint] = value end WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } args[prefix .. "valdelete" .. j] = { type = "execute", width = 0.15, name = L["Delete"], order = order(), func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data tremove(childOption.values, j) WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } end args[prefix .. "newvaluespace"] = { type = "toggle", width = WeakAuras.normalWidth, name = L["New Value"], order = order(), disabled = function() return true end, get = function() return true end, set = function() end } args[prefix .. "newvalue"] = { type = "input", width = WeakAuras.normalWidth, name = L["New Value"], order = order(), get = function() return "" end, set = function(_, value) value = value:gsub("||", "|") for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.values[#childOption.values + 1] = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } end, space = function(options, args, data, order, prefix, i) local option = options[i] -- this option should be just useWidth but no need to do a migration in the data just for that. args[prefix .. "variableWidth"] = { type = "toggle", width = WeakAuras.normalWidth, order = order(), name = name(option, "variableWidth", L["Width"]), desc = desc( option, "variableWidth", L["If unchecked, then this space will fill the entire line it is on in User Mode."] ), get = get(option, "variableWidth"), set = set(data, option, "variableWidth") } args[prefix .. "widthSpace"] = nil local widthOption = args[prefix .. "width"] widthOption.name = name(option, "width", L["Width"]) widthOption.disabled = function() return not option.variableWidth end widthOption.order = order() args[prefix .. "useHeight"] = { type = "toggle", width = WeakAuras.normalWidth, order = order(), name = name(option, "useHeight", L["Height"]), desc = desc(option, "useHeight", L["If checked, then this space will span across multiple lines."]), get = get(option, "useHeight"), set = set(data, option, "useHeight") } args[prefix .. "height"] = { type = "range", control = "WeakAurasSpinBox", width = WeakAuras.normalWidth, order = order(), name = name(option, "height", L["Height"]), desc = desc(option, "height"), get = get(option, "height"), set = set(data, option, "height"), disabled = function() return not option.useHeight end, min = 1, softMax = 10, step = 1 } end, media = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "mediaType"] = { type = "select", width = WeakAuras.normalWidth, name = name(option, "mediaType", L["Media Type"]), desc = desc(option, "mediaType"), values = OptionsPrivate.Private.shared_media_types, order = order(), get = get(option, "mediaType"), set = function(_, value) for _, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.mediaType = value childOption.default = OptionsPrivate.Private.author_option_media_defaults[value] WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } args[prefix .. "default"] = { type = "select", width = WeakAuras.doubleWidth, name = name(option, "default", L["Default"]), desc = desc(option, "default"), values = function() if option.mediaType == "sound" then return OptionsPrivate.Private.sound_file_types else return AceGUIWidgetLSMlists[option.mediaType] end end, sorting = function() if option.mediaType == "sound" then return OptionsPrivate.Private.SortOrderForValues(OptionsPrivate.Private.sound_file_types) else return nil end end, dialogControl = OptionsPrivate.Private.author_option_media_controls[option.mediaType], itemControl = OptionsPrivate.Private.author_option_media_itemControls[option.mediaType], order = order(), get = get(option, "default"), set = function(_, value) if option.mediaType == "sound" then -- do this outside the deref loop, so we don't play the sound a million times PlaySoundFile(value, "Master") end for _, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.default = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } end, multiselect = function(options, args, data, order, prefix, i) local option = options[i] local values = getValues(option) local defaultValues = {} for i, v in ipairs(values) do if v == conflict then defaultValues[i] = conflictBlue .. L["Value %i"]:format(i) else defaultValues[i] = v end end args[prefix .. "default"] = { type = "multiselect", width = WeakAuras.normalWidth * 0.9, name = L["Default"], order = order(), values = defaultValues, get = function(_, k) return option.default and option.default[k] end, set = function(_, k, v) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.default[k] = v WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } for j, value in ipairs(values) do args[prefix .. "space" .. j] = { type = "toggle", width = WeakAuras.normalWidth, name = L["Value %i"]:format(j), order = order(), disabled = function() return true end, get = function() return true end, set = function() end } args[prefix .. "value" .. j] = { type = "input", width = WeakAuras.normalWidth - 0.15, name = (value == conflict and conflictBlue or "") .. L["Value %i"]:format(j), desc = descSelect(option, j), order = order(), get = function() if value ~= conflict then return value:gsub("|", "||") end end, set = function(_, value) value = value:gsub("||", "|") for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data local insertPoint = math.min(j, #childOption.values + 1) if value == "" then tremove(childOption.values, insertPoint) tremove(childOption.default, insertPoint) else childOption.values[insertPoint] = value end WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } args[prefix .. "valdelete" .. j] = { type = "execute", width = 0.15, name = "", order = order(), func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data tremove(childOption.values, j) tremove(childOption.default, j) WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete", imageWidth = 24, imageHeight = 24 } end args[prefix .. "newvaluespace"] = { type = "toggle", width = WeakAuras.normalWidth, name = L["New Value"], order = order(), disabled = function() return true end, get = function() return true end, set = function() end } args[prefix .. "newvalue"] = { type = "input", width = WeakAuras.normalWidth, name = L["New Value"], order = order(), get = function() return "" end, set = function(_, value) value = value:gsub("||", "|") for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.values[#childOption.values + 1] = value childOption.default[#childOption.default + 1] = false WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } end, header = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "width"] = nil args[prefix .. "useName"] = { type = "toggle", name = name(option, "useName", L["Separator text"]), desc = desc( option, "useName", L["If checked, then this separator will include text. Otherwise, it will be just a horizontal line."] ), order = order(), width = WeakAuras.normalWidth, get = get(option, "useName"), set = set(data, option, "useName") } args[prefix .. "text"] = { type = "input", name = name(option, "text", L["Separator Text"]), desc = desc(option, "text"), order = order(), width = WeakAuras.normalWidth, get = getStr(option, "text"), set = setStr(data, option, "text"), disabled = function() return not option.useName end } args[prefix .. "noMerge"] = { type = "toggle", name = name(option, "noMerge", L["Prevent Merging"]), desc = desc(option, "noMerge", L["If checked, then this separator will not merge with other separators when selecting multiple auras."]), order = order(), width = WeakAuras.doubleWidth, get = get(option, "noMerge"), set = set(data, option, "noMerge"), } end, group = function(options, args, data, order, prefix, i) local option = options[i] args[prefix .. "width"] = nil args[prefix .. "groupType"] = { type = "select", name = name(option, "groupType", L["Group Type"]), order = order(), width = WeakAuras.doubleWidth, values = OptionsPrivate.Private.group_option_types, get = get(option, "groupType"), set = function(_, value) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.groupType = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } args[prefix .. "useCollapse"] = { type = "toggle", name = name(option, "useCollapse", L["Collapsible Group"]), desc = desc(option, "useCollapse", L["If checked, then this option group can be temporarily collapsed by the user."]), order = order(), width = WeakAuras.normalWidth, get = get(option, "useCollapse"), set = set(data, option, "useCollapse"), } args[prefix .. "collapseDefault"] = { type = "toggle", name = name(option, "collapse", L["Start Collapsed"]), desc = desc(option, "collapse", L["If checked, then this option group will start collapsed."]), order = order(), width = WeakAuras.normalWidth, get = get(option, "collapse"), set = function(_, value) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data childOption.collapse = value OptionsPrivate.SetCollapsed(id, "config", optionData.path, value) WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() return not option.useCollapse end } args[prefix .. "noMerge"] = { type = "toggle", name = WeakAuras.newFeatureString .. name(option, "noMerge", L["Prevent Merging"]), desc = desc(option, "noMerge", L["If checked, then this group will not merge with other group when selecting multiple auras."]), order = order(), width = option.groupType =="simple" and WeakAuras.doubleWidth or WeakAuras.normalWidth, get = get(option, "noMerge"), set = set(data, option, "noMerge"), } if option.groupType ~="simple" then args[prefix .. "sortAlphabetically"] = { type = "toggle", name = WeakAuras.newFeatureString .. name(option, "sortAlphabetically", L["Sort"]), desc = desc(option, "sortAlphabetically", L["If checked, then the combo box in the User settings will be sorted."]), order = order(), width = WeakAuras.normalWidth, get = get(option, "sortAlphabetically"), set = set(data, option, "sortAlphabetically"), } end if option.groupType ~="simple" then args[prefix .. "limitType"] = { type = "select", name = name(option, "limitType", L["Number of Entries"]), desc = desc(option, "limitType", L["Determines how many entries can be in the table."]), order = order(), width = WeakAuras.normalWidth, values = OptionsPrivate.Private.group_limit_types, get = get(option, "limitType"), set = function(_, value) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data if childOption.limitType == "fixed" and childOption.nameSource == -1 and value ~= "fixed" then childOption.entryNames = nil childOption.nameSource = 0 end childOption.limitType = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, } args[prefix .. "size"] = { type = "range", control = "WeakAurasSpinBox", name = name(option, "limitType", option.limitType == "max" and L["Entry limit"] or L["Number of Entries"]), desc = desc(option, "limitType"), order = order(), width = WeakAuras.normalWidth, min = 1, -- no point in a table with no entries softMax = 20, -- 20 people in a mythic raid group step = 1, get = get(option, "size"), set = function(_, value) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data if childOption.nameSource == -1 then if value < childOption.size then for i = value + 1, childOption.size do childOption.entryNames[i] = nil end else for i = childOption.size + 1, value do childOption.entryNames[i] = L["Entry %i"]:format(i) end end end childOption.size = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() return option.limitType == "none" end, } args[prefix .. "hideReorder"] = { type = "toggle", name = name(option, "hideReorder", L["Disallow Entry Reordering"]), desc = desc(option, "hideReorder"), order = order(), width = WeakAuras.normalWidth, get = function() return option.hideReorder or option.nameSource == -1 end, set = set(data, option, "hideReorder"), disabled = function() return option.nameSource == -1 end, } local nameSources = CopyTable(OptionsPrivate.Private.array_entry_name_types) local validNameSourceTypes = OptionsPrivate.Private.name_source_option_types if option.limitType ~= "fixed" then nameSources[-1] = nil end for subIndex, subOption in ipairs(option.subOptions) do if validNameSourceTypes[subOption.type] then local allShareThisOption = true for id in pairs(option.references) do if not subOption.references[id] then allShareThisOption = false break end end if allShareThisOption then nameSources[subIndex] = subOption.key end end end args[prefix .. "nameSource"] = { type = "select", name = name(option, "nameSource", L["Entry Name Source"]), desc = desc(option, "nameSource"), order = order(), values = nameSources, width = WeakAuras.doubleWidth, get = function() return option.nameSource or 0 end, set = function(_, value) for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data if (value == -1) ~= (childOption.nameSource == -1) then if value == -1 then local entryNames = {} for i = 1, childOption.size do entryNames[i] = L["Entry %i"]:format(i) end childOption.entryNames = entryNames else childOption.entryNames = nil end end if value > 0 then childOption.nameSource = option.subOptions[value].references[id].index else childOption.nameSource = value end WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, } if option.nameSource == -1 then for i = 1, option.size do args[prefix .. "entry" .. i .. "name"] = { type = "input", name = nameArray(option, "entryNames", i, L["Entry %i"]:format(i)), desc = descArray(option, "entryNames", i), order = order(), width = WeakAuras.doubleWidth, get = getArrayStr(option, "entryNames", i), set = setArrayStr(data, option, "entryNames", i), } end end end args[prefix .. "groupStart"] = { type = "header", name = L["Start of %s"]:format(option.name), order = order() } local subPrefix = prefix .. "option" for subIndex, subOption in ipairs(option.subOptions) do local addControlsForType = typeControlAdders[subOption.type] if addControlsForType then addAuthorModeOption(option.subOptions, args, data, order, subPrefix .. subIndex, subIndex) end end args[prefix .. "addSubOption"] = { type = "execute", name = L["Add Sub Option"], order = order(), width = WeakAuras.normalWidth, func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data local path = optionData.path local j = #childOption.subOptions + 1 path[#path + 1] = j childOption.subOptions[j] = { type = "toggle", key = generateKey("subOption", childOption.subOptions, j), name = L["Sub Option %i"]:format(j), default = false, width = 1, useDesc = false, } OptionsPrivate.SetCollapsed(id, "author", path, false) WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } args[prefix .. "groupEnd"] = { type = "header", name = L["End of %s"]:format(option.name), order = order() } end } local function up(data, options, index) local option = options[index] return function() for id, optionData in pairs(option.references) do if optionData.path[#optionData.path] <= 1 then return true end end end, function() for id, optionData in pairs(option.references) do -- move the option up in the subOptions local path = optionData.path local optionID = optionData.index local childData = optionData.data local childOptions = optionData.options local parent = optionData.parent if parent and parent.groupType == "array" then local dereferencedParent = parent.references[id].options[parent.references[id].index] if dereferencedParent.nameSource == optionID then dereferencedParent.nameSource = optionID - 1 elseif dereferencedParent.nameSource == optionID - 1 then dereferencedParent.nameSource = optionID end end OptionsPrivate.MoveCollapseDataUp(id, "author", path) childOptions[optionID], childOptions[optionID - 1] = childOptions[optionID - 1], childOptions[optionID] WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function down(data, options, index) local option = options[index] return function() for id, optionData in pairs(option.references) do if optionData.path[#optionData.path] >= #optionData.options then return true end end end, function() for id, optionData in pairs(option.references) do -- move the option down in the subOptions local path = optionData.path local optionID = optionData.index local childData = optionData.data local parent = optionData.parent if parent and parent.groupType == "array" then local dereferencedParent = parent.references[id].options[parent.references[id].index] if dereferencedParent.nameSource == optionID then dereferencedParent.nameSource = optionID + 1 elseif dereferencedParent.nameSource == optionID + 1 then dereferencedParent.nameSource = optionID end end local childOptions = optionData.options OptionsPrivate.MoveCollapseDataDown(id, "author", path) childOptions[optionID], childOptions[optionID + 1] = childOptions[optionID + 1], childOptions[optionID] WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function duplicate(data, options, index) local option = options[index] return function() for id, optionData in pairs(option.references) do local optionID = optionData.index local childOptions = optionData.options local childData = optionData.data local path = optionData.path path[#path] = path[#path] + 1 -- this data is being regenerated very soon OptionsPrivate.InsertCollapsed(id, "author", optionData.path, false) local newOption = CopyTable(childOptions[optionID]) if newOption.key then local existingKeys = {} for _, option in ipairs(childOptions) do if option.key then existingKeys[option.key] = true end end while existingKeys[newOption.key] do newOption.key = generateKey(newOption.key .. "copy", childOptions, 1) end end if newOption.name then newOption.name = newOption.name .. " - " .. L["Copy"] end tinsert(childOptions, optionID + 1, newOption) WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end local function validateNonDuplicateKey(option) -- note: this has some unintuitive behavior -- e.g. if aura A has option keys "foo", "bar" -- and aura B has option keys "foo", "baz", -- then you still cannot change the merged option with key "foo" to "bar" -- unless you unselect aura A, even though aura B would be fine with that. return function(_, newKey) for id, optionData in pairs(option.references) do for index, otherOption in ipairs(optionData.options) do if index ~= optionData.index and otherOption.key == newKey then return L["%s - Option #%i has the key %s. Please choose a different option key."]:format(id, index, newKey) end end end return true end end function addAuthorModeOption(options, args, data, order, prefix, i) -- add header controls local option = options[i] local collapsed = false for id, optionData in pairs(option.references) do if OptionsPrivate.IsCollapsed(id, "author", optionData.path, true) then collapsed = true break end end local _, optionData = next(option.references) local isInGroup = optionData.parent ~= nil local buttonWidth = 0.6 if isInGroup then buttonWidth = buttonWidth + 0.3 end local optionBelow = options[i + 1] local isAboveGroup = optionBelow and OptionsPrivate.Private.author_option_classes[optionBelow.type] == "group" if isAboveGroup then buttonWidth = buttonWidth + 0.15 end local optionAbove = options[i - 1] local isBelowGroup = optionAbove and OptionsPrivate.Private.author_option_classes[optionAbove.type] == "group" if isBelowGroup then buttonWidth = buttonWidth + 0.15 end local optionClass = OptionsPrivate.Private.author_option_classes[option.type] local optionName = optionClass == "noninteractive" and OptionsPrivate.Private.author_option_types[option.type] or option.name args[prefix .. "collapse"] = { type = "execute", name = nameHead(data, option, optionName), order = order(), width = WeakAuras.doubleWidth - buttonWidth, func = function() for id, optionData in pairs(option.references) do OptionsPrivate.SetCollapsed(id, "author", optionData.path, not collapsed) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = collapsed and "Interface\\AddOns\\WeakAuras\\Media\\Textures\\expand" or "Interface\\AddOns\\WeakAuras\\Media\\Textures\\collapse", imageWidth = 18, imageHeight = 18, control = "WeakAurasExpand" } args[prefix .. "upAndIn"] = { type = "execute", width = 0.15, name = L["Move Into Above Group"], order = order(), hidden = function() return not isBelowGroup end, func = function() for id, optionData in pairs(option.references) do local groupData = optionAbove.references[id] if groupData then local childGroup = groupData.options[groupData.index] local childCollapsed = OptionsPrivate.IsCollapsed(id, "author", optionData.path, true) OptionsPrivate.RemoveCollapsed(id, "author", optionData.path) local newPath = groupData.path tinsert(newPath, #childGroup.subOptions + 1) OptionsPrivate.InsertCollapsed(id, "author", newPath, childCollapsed) local childOption = tremove(optionData.options, optionData.index) childOption.key = ensureUniqueKey(childOption.key, "In", childGroup.subOptions) local childData = optionData.data tinsert(childGroup.subOptions, childOption) WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\upright", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } args[prefix .. "downAndIn"] = { type = "execute", width = 0.15, name = L["Move Into Below Group"], order = order(), hidden = function() return not isAboveGroup end, func = function() for id, optionData in pairs(option.references) do local groupData = optionBelow.references[id] if groupData then local childGroup = groupData.options[groupData.index] local childCollapsed = OptionsPrivate.IsCollapsed(id, "author", optionData.path, true) OptionsPrivate.RemoveCollapsed(id, "author", optionData.path) local newPath = groupData.path tinsert(newPath, 1) OptionsPrivate.InsertCollapsed(id, "author", newPath, childCollapsed) local childOption = tremove(optionData.options, optionData.index) childOption.key = ensureUniqueKey(childOption.key, "In", childGroup.subOptions) local childData = optionData.data tinsert(childGroup.subOptions, 1, childOption) WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\downright", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } args[prefix .. "upAndOut"] = { type = "execute", width = 0.15, name = L["Move Above Group"], order = order(), hidden = function() return not isInGroup end, func = function() for id, optionData in pairs(option.references) do local path = optionData.path local parent = optionData.parent local parentOptions = parent and parent.references[id].options or optionData.data.authorOptions local childOption = tremove(optionData.options, optionData.index) if parent and parent.groupType == "array" then local dereferencedParent = parent.references[id].options[parent.references[id].index] if dereferencedParent.nameSource == optionData.index then dereferencedParent.nameSource = 0 elseif dereferencedParent.nameSource > optionData.index then dereferencedParent.nameSource = dereferencedParent.nameSource - 1 end end OptionsPrivate.RemoveCollapsed(id, "author", optionData.path) childOption.key = ensureUniqueKey(childOption.key, "Out", parentOptions) tinsert(parentOptions, path[#path - 1], childOption) path[#path] = nil OptionsPrivate.InsertCollapsed(id, "author", path) WeakAuras.Add(optionData.data) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\upleft", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } args[prefix .. "downAndOut"] = { type = "execute", width = 0.15, name = L["Move Below Group"], order = order(), hidden = function() return not isInGroup end, func = function() for id, optionData in pairs(option.references) do local path = optionData.path local parent = optionData.parent local parentOptions = parent and parent.references[id].options or optionData.data.authorOptions local childOption = tremove(optionData.options, optionData.index) if parent and parent.groupType == "array" then local dereferencedParent = parent.references[id].options[parent.references[id].index] if dereferencedParent.nameSource == optionData.index then dereferencedParent.nameSource = 0 elseif dereferencedParent.nameSource > optionData.index then dereferencedParent.nameSource = dereferencedParent.nameSource - 1 end end OptionsPrivate.RemoveCollapsed(id, "author", optionData.path) childOption.key = ensureUniqueKey(childOption.key, "Out", parentOptions) tinsert(parentOptions, path[#path - 1] + 1, childOption) path[#path] = nil path[#path] = path[#path] + 1 OptionsPrivate.InsertCollapsed(id, "author", path) WeakAuras.Add(optionData.data) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\downleft", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } local upDisable, upFunc = up(data, options, i) args[prefix .. "up"] = { type = "execute", width = 0.15, name = L["Move Up"], order = order(), disabled = upDisable, func = upFunc, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\moveup", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } local downDisable, downFunc = down(data, options, i) args[prefix .. "down"] = { type = "execute", width = 0.15, name = L["Move Down"], order = order(), disabled = downDisable, func = downFunc, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\movedown", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } args[prefix .. "duplicate"] = { type = "execute", width = 0.15, name = L["Duplicate"], order = order(), func = duplicate(data, options, i), image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\duplicate", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } args[prefix .. "delete"] = { type = "execute", width = 0.15, name = L["Delete"], order = order(), func = function() for id, optionData in pairs(option.references) do local childOptions = optionData.options local optionIndex = optionData.index local childData = optionData.data local parent = optionData.parent OptionsPrivate.RemoveCollapsed(id, "author", optionData.path) tremove(childOptions, optionIndex) if parent and parent.groupType == "array" then local dereferencedParent = parent.references[id].options[parent.references[id].index] if dereferencedParent.nameSource == optionData.index then dereferencedParent.nameSource = 0 elseif dereferencedParent.nameSource > optionData.index then dereferencedParent.nameSource = dereferencedParent.nameSource - 1 end end WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete", imageWidth = 24, imageHeight = 24, control = "WeakAurasIcon" } if collapsed then return end args[prefix .. "type"] = { type = "select", width = WeakAuras.doubleWidth, name = L["Option Type"], desc = descType(option), order = order(), values = OptionsPrivate.Private.author_option_types, get = get(option, "type"), set = function(_, value) if value == option.type then return end local author_option_fields = OptionsPrivate.Private.author_option_fields local commonFields, newFields = author_option_fields.common, author_option_fields[value] local newClass = OptionsPrivate.Private.author_option_classes[value] for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data local parentOption = optionData.parent for k in pairs(childOption) do if not commonFields[k] then childOption[k] = nil end end for k, v in pairs(newFields) do if type(v) == "table" then childOption[k] = CopyTable(v) else childOption[k] = v end end childOption.type = value if newClass == "noninteractive" then childOption.name = nil childOption.desc = nil childOption.key = nil childOption.useDesc = nil childOption.default = nil else -- don't use the option index here if switching from a noninteractive type -- mostly because it would have a very non-intuitive effect -- the names and keys would likely not match anymore, and so -- the merged display would basically explode into a bunch of separate options childOption.name = childOption.name or (L["Option %i"]):format(i) if not childOption.key then local newKey = "option" .. i local existingKeys = {} for index, option in pairs(optionData.options) do if index ~= optionData.index and option.key then existingKeys[option.key] = true end end while existingKeys[newKey] do newKey = newKey .. "copy" end childOption.key = newKey end end if parentOption and parentOption.groupType == "array" and not OptionsPrivate.Private.array_entry_name_types[value] then local dereferencedParent = parentOption.references[id] if dereferencedParent.nameSource == optionData.index then dereferencedParent.nameSource = 0 end end WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } if optionClass ~= "noninteractive" then args[prefix .. "name"] = { type = "input", width = WeakAuras.normalWidth, name = name(option, "name", L["Display Name"]), desc = desc(option, "name"), order = order(), get = getStr(option, "name"), set = setStr(data, option, "name") } args[prefix .. "key"] = { type = "input", width = WeakAuras.normalWidth, name = name(option, "key", optionClass == "group" and L["Group key"] or L["Option key"]), order = order(), validate = validateNonDuplicateKey(option), get = get(option, "key"), set = set(data, option, "key") } end if optionClass == "simple" then args[prefix .. "tooltipSpace"] = { type = "description", width = WeakAuras.doubleWidth, name = "", order = order } args[prefix .. "usetooltip"] = { type = "toggle", name = name(option, "useDesc", L["Tooltip"]), order = order(), width = WeakAuras.halfWidth, get = get(option, "useDesc"), set = set(data, option, "useDesc") } args[prefix .. "tooltip"] = { type = "input", name = name(option, "desc", L["Tooltip Text"]), desc = desc(option, "desc"), order = order(), width = WeakAuras.normalWidth * 1.5, get = getStr(option, "desc"), set = setStr(data, option, "desc"), disabled = function() return not option.useDesc end } end args[prefix .. "width"] = { type = "range", control = "WeakAurasSpinBox", width = WeakAuras.normalWidth, name = name(option, "width", L["Width"]), desc = desc(option, "width"), order = order(), min = 0.1, max = 2, step = 0.05, get = get(option, "width"), set = set(data, option, "width") } local addControlsForType = typeControlAdders[option.type] if addControlsForType then addControlsForType(options, args, data, order, prefix, i) end end local groupPages = {} local function getPage(id, path, max) max = max or math.huge groupPages[id] = groupPages[id] or {} local base = groupPages[id] for _, index in ipairs(path) do if not base[index] then base[index] = {} end base = base[index] end if not base.page or (max and base.page > max) then base.page = 1 end return base.page end local function setPage(id, path, page) groupPages[id] = groupPages[id] or {} local base = groupPages[id] for _, index in ipairs(path) do if not base[index] then base[index] = {} end base = base[index] end base.page = page end local function addUserModeOption(options, args, data, order, prefix, i) local option = options[i] local optionType = option.type local optionClass = OptionsPrivate.Private.author_option_classes[optionType] local userOption if optionClass == "simple" then userOption = { type = optionType, name = nameUser(option), desc = descUser(option), width = (option.width or 1) * WeakAuras.normalWidth, order = order(), get = getUser(option), set = setUser(data, option) } elseif optionClass == "noninteractive" then userOption = { type = "description", order = order(), name = "", width = (option.width or 1) * WeakAuras.normalWidth } elseif optionClass == "group" then local collapsed = false if option.useCollapse then local defaultCollapsed = true if option.collapse ~= nil then defaultCollapsed = option.collapse end for id, optionData in pairs(option.references) do if OptionsPrivate.IsCollapsed(id, "config", optionData.path, defaultCollapsed) then collapsed = true break end end args[prefix .. "collapse"] = { type = "execute", name = option.name, order = order(), width = WeakAuras.doubleWidth, func = function() for id, optionData in pairs(option.references) do OptionsPrivate.SetCollapsed(id, "config", optionData.path, not collapsed) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, image = collapsed and "Interface\\AddOns\\WeakAuras\\Media\\Textures\\expand" or "Interface\\AddOns\\WeakAuras\\Media\\Textures\\collapse", imageWidth = 18, imageHeight = 18, control = "WeakAurasExpand" } end if not collapsed then local skipSubOptions = false if option.groupType == "array" then local values, firstChild = {}, true local nameSource = option.nameSource or 0 if nameSource > 0 then nameSource = option.subOptions[option.nameSource].key end if nameSource == -1 then for id, optionData in pairs(option.references) do local i = 1 local childOption = optionData.options[optionData.index] local entryNames = childOption.entryNames while i <= #values or i <= childOption.size do if firstChild then values[i] = entryNames[i] elseif values[i] ~= entryNames[i] then values[i] = conflictBlue .. L["Entry %i"]:format(i) end i = i + 1 end firstChild = false end elseif nameSource == 0 then for id, optionData in pairs(option.references) do local i = 1 local childOption = optionData.options[optionData.index] local childValues = optionData.config[childOption.key] while i <= #values or i <= #childValues do if firstChild then values[i] = L["Entry %i"]:format(i) elseif values[i] == nil or childValues[i] == nil then values[i] = conflictBlue .. L["Entry %i"]:format(i) end i = i + 1 end firstChild = false end else for id, optionData in pairs(option.references) do local i = 1 local childOption = optionData.options[optionData.index] local childValues = optionData.config[option.key] while i <= #values or i <= #childValues do if firstChild then values[i] = childValues[i][nameSource] or conflictBlue .. L["Entry %i"]:format(i) elseif not childValues[i] or childValues[i][nameSource] ~= values[i] then values[i] = conflictBlue .. L["Entry %i"]:format(i) end i = i + 1 end firstChild = false end end skipSubOptions = #values == 0 local buttonWidth = 0.75 if option.limitType == "fixed" then buttonWidth = buttonWidth - 0.30 end if option.hideReorder or option.nameSource == -1 then buttonWidth = buttonWidth - 0.30 end args[prefix .. "entryChoice"] = { type = "select", name = nameUser(option), order = order(), width = WeakAuras.doubleWidth - buttonWidth, values = values, get = function() if skipSubOptions then return 1 -- show the "create" prompt, which is at index 1 end local value for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] if value == nil then value = getPage(id, optionData.path, #childConfigList) elseif value ~= getPage(id, optionData.path, #childConfigList) then return "" end end return value end, set = function(_, value) for id, optionData in pairs(option.references) do setPage(id, optionData.path, value) -- XXX: mergeOptions will reset this to the maximum value if it's too big end WeakAuras.ClearAndUpdateOptions(data.id, true) end, sorting = option.sortAlphabetically and OptionsPrivate.Private.SortOrderForValues(values) or nil } args[prefix .. "resetEntry"] = { type = "execute", name = L["Reset Entry"], order = order(), func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childData = optionData.data local childPage = getPage(id, optionData.path) local childConfigList = optionData.config[childOption.key] childConfigList[childPage] = {} WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, width = 0.15, image = "Interface\\Addons\\WeakAuras\\Media\\Textures\\reset", imageWidth = 18, imageHeight = 18, control = "WeakAurasIcon" } if option.limitType ~= "fixed" then args[prefix .. "createEntry"] = { type = "execute", name = L["Add Entry"], order = order(), func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] local childData = optionData.data if childOption.limitType == "none" or #childConfigList < childOption.size then tinsert(childConfigList, {}) setPage(id, optionData.path, #childConfigList) -- we do need to Add here, so that the new entry can get its default values WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() if option.limitType == "none" then return false else for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] if #childConfigList >= childOption.size then return true end end end end, width = 0.15, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\add", imageWidth = 18, imageHeight = 18, control = "WeakAurasIcon" } args[prefix .. "deleteEntry"] = { type = "execute", name = L["Delete Entry"], order = order(), confirm = true, func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] local childData = optionData.data local page = getPage(id, optionData.path) if #childConfigList ~= 0 then tremove(childConfigList, page) setPage(id, optionData.path, min(#childConfigList, page)) WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() return skipSubOptions end, width = 0.15, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete", imageWidth = 18, imageHeight = 18, control = "WeakAurasIcon" } end if option.nameSource ~= -1 and not option.hideReorder then args[prefix .. "moveEntryUp"] = { type = "execute", name = L["Move Entry Up"], order = order(), func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] local childData = optionData.data local childPage = getPage(id, optionData.path, #childConfigList) if childConfigList[childPage] then childConfigList[childPage], childConfigList[childPage - 1] = childConfigList[childPage - 1], childConfigList[childPage] setPage(id, optionData.path, childPage - 1) WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() for id, optionData in pairs(option.references) do if getPage(id, optionData.path) <= 1 then return true end end end, width = 0.15, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\moveup", imageWidth = 18, imageHeight = 18, control = "WeakAurasIcon" } args[prefix .. "moveEntryDown"] = { type = "execute", name = L["Move Entry Down"], order = order(), func = function() for id, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] local childData = optionData.data local childPage = getPage(id, optionData.path, #childConfigList) if childConfigList[childPage] then childConfigList[childPage], childConfigList[childPage + 1] = childConfigList[childPage + 1], childConfigList[childPage] setPage(id, optionData.path, childPage + 1) WeakAuras.Add(childData) end end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() for id, optionData in pairs(option.references) do local childPage = getPage(id, optionData.path) local childOption = optionData.options[optionData.index] local childConfigList = optionData.config[childOption.key] if childPage >= #childConfigList then return true end end end, width = 0.15, image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\movedown", imageWidth = 18, imageHeight = 18, control = "WeakAurasIcon" } end end if not skipSubOptions then local subPrefix = prefix .. "subOption" for j = 1, #option.subOptions do addUserModeOption(option.subOptions, args, data, order, subPrefix .. j, j) end end end end args[prefix] = userOption -- convert from weakauras option type to ace option type if optionClass == "simple" then -- toggle and input don't need any extra love if optionType == "input" then userOption.multiline = option.multiline elseif optionType == "number" then userOption.type = "input" userOption.get = getUserNumAsString(option) userOption.set = setUserNum(data, option) elseif optionType == "range" then userOption.softMax = option.softMax userOption.softMin = option.softMin userOption.bigStep = option.bigStep userOption.min = option.min userOption.max = option.max local effectiveMin = userOption.softMin or userOption.min or 0 local effectiveMax = userOption.softMax or userOption.max or 100 if (effectiveMin > effectiveMax) then -- This will cause a error inside the slider -- Fix up either softMax or max, depending on which one is the effective one if userOption.softMax then userOption.softMax = effectiveMin elseif userOption.max then userOption.max = effectiveMin else userOption.softMax = effectiveMin end end userOption.step = option.step elseif optionType == "color" then userOption.hasAlpha = true userOption.get = getUserColor(option) userOption.set = setUserColor(data, option) elseif optionType == "select" then userOption.values = getUserValues(option) elseif optionType == "multiselect" then userOption.values = getUserValues(option) userOption.get = function(_, k) local value for id, optionData in pairs(option.references) do if value == nil then value = optionData.config[option.key][k] elseif value ~= optionData.config[option.key][k] then return end end return value end userOption.set = function(_, k, v) for _, optionData in pairs(option.references) do optionData.config[option.key][k] = v WeakAuras.Add(optionData.data) end WeakAuras.ClearAndUpdateOptions(data.id, true) end elseif optionType == "media" then userOption.type = "select" userOption.dialogControl = OptionsPrivate.Private.author_option_media_controls[option.mediaType] userOption.itemControl = OptionsPrivate.Private.author_option_media_itemControls[option.mediaType] userOption.values = function() if option.mediaType == "sound" then return OptionsPrivate.Private.sound_file_types else return AceGUIWidgetLSMlists[option.mediaType] end end userOption.sorting = function() if option.mediaType == "sound" then return OptionsPrivate.Private.SortOrderForValues(OptionsPrivate.Private.sound_file_types) else return nil end end userOption.set = function(_, value) if option.mediaType == "sound" then PlaySoundFile(value, "Master") end for _, optionData in pairs(option.references) do local childData = optionData.data local childConfig = optionData.config childConfig[option.key] = value WeakAuras.Add(childData) end WeakAuras.ClearAndUpdateOptions(data.id, true) end end elseif optionClass == "noninteractive" then if optionType == "header" then userOption.type = "header" local name = {} local firstName = nil local conflict = false for _, optionData in pairs(option.references) do local childOption = optionData.options[optionData.index] if childOption.useName and #childOption.text > 0 then if firstName == nil then firstName = childOption.text tinsert(name, (childOption.text:gsub("||", "|"))) elseif childOption.text ~= firstName then conflict = true tinsert(name, (childOption.text:gsub("||", "|"))) end end end userOption.name = (conflict and conflictBlue or "") .. tconcat(name, " / ") elseif optionType == "description" then userOption.name = nameUserDesc(option) userOption.fontSize = option.fontSize elseif optionType == "space" then if not option.variableWidth then userOption.width = "full" end if option.useHeight and (option.height or 1) > 1 then userOption.name = string.rep("\n", option.height - 1) else userOption.name = " " end end end end local function initReferences(mergedOption, data, options, index, config, path, parent) mergedOption.references = { [data.id] = { data = data, options = options, index = index, config = config, path = path, parent = parent, } } if mergedOption.subOptions then local subConfig if config then if mergedOption.groupType == "simple" then subConfig = config[mergedOption.key] else local configList = config[mergedOption.key] local page = getPage(data.id, path, #configList) subConfig = configList[page] end end local subOptions = options[index].subOptions local subPath local subParent = mergedOption for i, submergedOption in ipairs(mergedOption.subOptions) do -- ha, submerged subPath = CopyTable(path) subPath[#subPath + 1] = i initReferences(submergedOption, data, subOptions, i, subConfig, subPath, subParent) end end end -- all of these fields must be identical for an option to be merged -- sometimes this just means that they are both nil, e.g. descriptions have no key local significantFieldsForMerge = { type = true, name = true, key = true, groupType = true, limitType = true, size = true, mediaType = true, } -- these fields are special cases, generally reserved for when the UI displays something based on the merged options -- e.g. array name source displays options in merged order, so the dereferenced source is not useful at that level. local specialCasesForMerge = { nameSource = true } local function mergeOptions(mergedOptions, data, options, config, prepath, parent) local nextInsert = 1 for i = 1, #options do local path = CopyTable(prepath) path[#path + 1] = i -- find the best place to start inserting the next option to merge local nextToMerge = options[i] local shouldMerge = false if not nextToMerge.noMerge then for j = nextInsert, #mergedOptions + 1 do local mergedOption = mergedOptions[j] if not mergedOption then break end -- no more options to check, so must insert local validMerge = not mergedOption.noMerge if validMerge then for field in pairs(significantFieldsForMerge) do if nextToMerge[field] ~= mergedOption[field] then validMerge = false break end end end if validMerge then shouldMerge = true nextInsert = j break end end else nextInsert = #mergedOptions + 1 end -- now we know at what point to add nextToMerge if shouldMerge then local mergedOption = mergedOptions[nextInsert] -- nil out all fields which aren't the same mergedOption.references[data.id] = { data = data, options = options, index = i, config = config, path = path, parent = parent, } for k, v in pairs(nextToMerge) do if k == "subOptions" then local subConfig if config then if mergedOption.groupType == "simple" then subConfig = config[mergedOption.key] else local configList = config[mergedOption.key] local page = getPage(data.id, path, #configList) subConfig = configList[page] end end local subParent = mergedOption mergeOptions(mergedOption.subOptions, data, v, subConfig, path, subParent) if mergedOption.groupType == "array" and mergedOption.nameSource ~= nil then -- special case merge of nameSource -- nameSource can be an optionID of the array's subOptions. -- Since the optionIDs are normally hidden away in references and options with different optionIDs can be merged together, -- we can't simply use nilmerge like we do with most other fields. -- Obviously, if the nameSource has already been set to nil then we do not need to do any more checks, -- as newly merged options can never resolve conflicts. Otherwise, we need to examine the semantic value of the nameSource. if nextToMerge.nameSource < 1 or mergedOption.nameSource < 1 then -- either the names are fixed, or they are auto-generated as "Entry #" -- in this case, nilmerge is the appropriate strategy if mergedOption.nameSource ~= nextToMerge.nameSource then mergedOption.nameSource = nil end else -- entry names are sourced from config of a particular subOption -- check if nextToMerge.nameSource was merged in the same spot as mergedOption.nameSource local subMergedOption = mergedOption.subOptions[mergedOption.nameSource] local optionData = subMergedOption.references[data.id] if not optionData or optionData.index ~= nextToMerge.nameSource then -- either an option was not merged at the name source's index, or the wrong option was. -- in both cases, the name source is conflicted. Fallback to "Entry #" as entry names mergedOption.nameSource = nil end end end elseif not specialCasesForMerge[k] and neq(mergedOption[k], v) then mergedOption[k] = nil end end else -- can't merge, should insert instead local newOption = CopyTable(nextToMerge) initReferences(newOption, data, options, i, config, path, parent) tinsert(mergedOptions, nextInsert, newOption) end -- never merge 2 options from the same child nextInsert = nextInsert + 1 end end local function valuesAreEqual(t1, t2) if t1 == t2 then return true end local ty1 = type(t1) local ty2 = type(t2) if ty1 ~= ty2 then return false end if ty1 == "number" then return abs(t1 - t2) < 1e-9 end if ty1 ~= "table" then return false end for k1, v1 in pairs(t1) do local v2 = t2[k1] if v2 == nil or not valuesAreEqual(v1, v2) then return false end end for k2, v2 in pairs(t2) do local v1 = t1[k2] if v1 == nil or not valuesAreEqual(v1, v2) then return false end end return true end local function allChoicesAreDefault(option, config, id, path) local optionClass = OptionsPrivate.Private.author_option_classes[option.type] if optionClass == "simple" then return valuesAreEqual(option.default, config[option.key]) elseif optionClass == "group" then if option.groupType == "simple" then local subConfig = config[option.key] path[#path + 1] = 0 for i, subOption in ipairs(option.subOptions) do path[#path] = i if not allChoicesAreDefault(subOption, subConfig, id, path) then return false end end path[#path] = nil elseif option.groupType == "array" then path[#path + 1] = 0 for _, subConfig in ipairs(config[option.key]) do for i, subOption in ipairs(option.subOptions) do path[#path] = i if not allChoicesAreDefault(subOption, subConfig, id, path) then return false end end end path[#path] = nil end if option.useCollapse then local isCollapsed = OptionsPrivate.IsCollapsed(id, "config", path, option.collapse) if isCollapsed ~= option.collapse then return false end end end return true end local function createorder(startorder) local order = startorder or 1 return function() order = order + 1 return order end end function OptionsPrivate.GetAuthorOptions(data) -- initialize the process local authorOptions = { type = "group", name = L["Custom Options"], order = 100, args = {} } local args = authorOptions.args local isAuthorMode = true local options = {} local order = createorder(1) for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do mergeOptions(options, child, child.authorOptions, child.config, {}) isAuthorMode = isAuthorMode and child.authorMode end if isAuthorMode then args["enterUserMode"] = { type = "execute", width = WeakAuras.normalWidth, name = L["Enter User Mode"], desc = L["Enter user mode."], order = order(), func = function() for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do child.authorMode = nil -- no need to add, author mode is picked up by ClearAndUpdateOptions end WeakAuras.ClearAndUpdateOptions(data.id, true) end } args["enterUserModeSpacer"] = { type = "description", name = "", order = order() } for i = 1, #options do addAuthorModeOption(options, args, data, order, "option" .. i, i) end args["addOption"] = { type = "execute", width = WeakAuras.normalWidth, name = L["Add Option"], order = order(), func = function() for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do local i = #child.authorOptions + 1 child.authorOptions[i] = { type = "toggle", key = generateKey("option", child.authorOptions, i), name = L["Option %i"]:format(i), default = false, width = 1, useDesc = false, } OptionsPrivate.SetCollapsed(child.id, "author", i, false) WeakAuras.Add(child) end WeakAuras.ClearAndUpdateOptions(data.id, true) end } else for i = 1, #options do addUserModeOption(options, args, data, order, "userOption" .. i, i) end args["userConfigFooter"] = { type = "header", name = "", order = order() } args["resetToDefault"] = { type = "execute", width = WeakAuras.normalWidth, name = L["Reset to Defaults"], desc = L["Reset all options to their default values."], order = order(), func = function() for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do child.config = {} -- config validation in Add() will set all the needed keys to their defaults OptionsPrivate.ResetCollapsed(child.id, "config") WeakAuras.Add(child) end WeakAuras.ClearAndUpdateOptions(data.id, true) end, disabled = function() local path = {} for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do local config = child.config for i, option in ipairs(child.authorOptions) do path[1] = i local result = allChoicesAreDefault(option, config, child.id, path) if result == false then return false end end end return true end } args["enterAuthorMode"] = { type = "execute", width = WeakAuras.normalWidth, name = L["Enter Author Mode"], desc = L["Configure what options appear on this panel."], order = order(), func = function() for configData in OptionsPrivate.Private.TraverseLeafsOrAura(data) do -- no need to add, author mode is picked up by ClearAndUpdateOptions configData.authorMode = true end WeakAuras.ClearAndUpdateOptions(data.id, true) end } end return authorOptions end