mirror of
https://github.com/fuffc/WeakestAuras.git
synced 2026-11-10 20:07:35 +00:00
ClassicAPI 1.18 is now the only requirement Requirements - Needs ClassicAPI 1.18.0 or newer. SuperWoW is no longer used, and Nampower is optional. - On an older ClassicAPI the addon stays off, and a popup links the download. - Custom triggers on SuperWoW or Nampower events only fire while that mod is installed. Triggers - New Combat Log trigger, with lists of spell IDs, spell names and NPC IDs. WeakAuras2 auras that use one now import. - Custom triggers can listen to the combat log with CLEU or CLEU:SUBEVENT. - Crowd Control matches a specific type (stun, fear, root, silence, disarm and more) or a school lockout, with the effect's real timer. - Saved Crowd Control auras: Root, Silence and Disarm filters carry over; CC and Snare become "any type", and snares are no longer detected. - Cast bars follow pushback and channel updates. Displays - Icon cooldown swipes are drawn by the client's own cooldown frame. - Sound actions play on a chosen channel, and a new Stop sound type ends a looping sound. Fixes - Cooldown auras no longer sweep on every wand shot. - The Global Cooldown trigger ignores wand and Auto Shot shots.
1751 lines
61 KiB
Lua
1751 lines
61 KiB
Lua
-- WeakestAuras -- the custom-option schema and config validator for imported auras.
|
|
-- Mirrors WeakestAurasOptions/AuthorOptions.lua and WeakAuras.lua's validator.
|
|
|
|
if WeakestAuras.disabled then return end
|
|
local WA = WeakestAuras
|
|
|
|
-- Forward declarations. The field builders near the top of this file close over
|
|
-- the read/write helpers defined below them, and in 5.0 a name that is not yet a
|
|
-- visible local compiles as a *global* read -- which resolves to nil at call
|
|
-- time, with the error swallowed. Every user-mode set went through
|
|
-- setOptionValue that way and silently did nothing.
|
|
local optionValue
|
|
local setOptionValue, setOptionValueAt
|
|
|
|
WA.author_option_classes = {
|
|
toggle = "simple",
|
|
input = "simple",
|
|
number = "simple",
|
|
range = "simple",
|
|
color = "simple",
|
|
select = "simple",
|
|
media = "simple",
|
|
multiselect = "simple",
|
|
description = "noninteractive",
|
|
space = "noninteractive",
|
|
header = "noninteractive",
|
|
group = "group",
|
|
}
|
|
|
|
WA.author_option_types = {
|
|
toggle = "Toggle",
|
|
input = "String",
|
|
number = "Number",
|
|
range = "Slider",
|
|
description = "Description",
|
|
color = "Color",
|
|
select = "Dropdown Menu",
|
|
space = "Space",
|
|
multiselect = "Toggle List",
|
|
media = "Media",
|
|
header = "Separator",
|
|
group = "Option Group",
|
|
}
|
|
|
|
WA.author_option_fields = {
|
|
common = {
|
|
type = true,
|
|
name = true,
|
|
useDesc = true,
|
|
desc = true,
|
|
key = true,
|
|
width = true,
|
|
},
|
|
number = {
|
|
min = 0,
|
|
max = 1,
|
|
step = .05,
|
|
default = 0,
|
|
},
|
|
range = {
|
|
min = 0,
|
|
max = 1,
|
|
step = .05,
|
|
default = 0,
|
|
},
|
|
input = {
|
|
default = "",
|
|
useLength = false,
|
|
length = 10,
|
|
multiline = false,
|
|
},
|
|
toggle = {
|
|
default = false,
|
|
},
|
|
description = {
|
|
text = "",
|
|
fontSize = "medium",
|
|
},
|
|
color = {
|
|
default = {1, 1, 1, 1},
|
|
},
|
|
select = {
|
|
values = {"val1"},
|
|
default = 1,
|
|
},
|
|
space = {
|
|
variableWidth = true,
|
|
useHeight = false,
|
|
height = 1,
|
|
},
|
|
media = {
|
|
mediaType = "sound",
|
|
media = "Interface\\AddOns\\WeakestAuras\\sounds\\AirHorn.ogg",
|
|
},
|
|
multiselect = {
|
|
default = {true},
|
|
values = {"val1"},
|
|
},
|
|
header = {
|
|
useName = false,
|
|
text = "",
|
|
},
|
|
group = {
|
|
groupType = "simple",
|
|
useCollapse = true,
|
|
collapse = false,
|
|
limitType = "none",
|
|
size = 10,
|
|
nameSource = 0,
|
|
hideReorder = true,
|
|
entryNames = nil,
|
|
subOptions = {},
|
|
noMerge = false,
|
|
},
|
|
}
|
|
|
|
WA.author_option_media_defaults = {
|
|
sound = "Interface\\AddOns\\WeakestAuras\\sounds\\AirHorn.ogg",
|
|
font = "Friz Quadrata TT",
|
|
border = "1 Pixel",
|
|
background = "None",
|
|
statusbar = "Blizzard",
|
|
}
|
|
|
|
local function clearTable(t)
|
|
for key in pairs(t) do t[key] = nil end
|
|
end
|
|
|
|
local function copyDefault(option)
|
|
if type(option.default) == "table" then return WA.DeepCopy(option.default) end
|
|
return option.default
|
|
end
|
|
|
|
local function valuesEqual(first, second)
|
|
if first == second then return true end
|
|
if type(first) ~= "table" or type(second) ~= "table" then return false end
|
|
for key, value in pairs(first) do
|
|
if not valuesEqual(value, second[key]) then return false end
|
|
end
|
|
for key, value in pairs(second) do
|
|
if not valuesEqual(value, first[key]) then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local significantFieldsForMerge = {
|
|
type = true, name = true, key = true, groupType = true,
|
|
limitType = true, size = true, mediaType = true,
|
|
}
|
|
|
|
local specialCasesForMerge = { nameSource = true }
|
|
|
|
local function round(value)
|
|
return math.floor(value + 0.5)
|
|
end
|
|
|
|
local function customOptionIsValid(option)
|
|
if type(option) ~= "table" or not option.type then
|
|
return false
|
|
elseif WA.author_option_classes[option.type] == "simple" then
|
|
if not option.key or not option.name then return false end
|
|
elseif WA.author_option_classes[option.type] == "group" then
|
|
if not option.key or not option.name or not option.subOptions then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Keyed per index, and never cleared: the option is gone, so a validation pass
|
|
-- after this one finds nothing corrupt and a clear keyed on that would take the
|
|
-- notice away immediately. Deleting the aura is the only thing that drops it, and
|
|
-- losing it on a reload is correct -- the deletion is not repeated.
|
|
local function reportCorruptOption(data, index)
|
|
WA.ReportForAura(data.id, "corruptoption:" .. index, "warning",
|
|
tostring(data.id) .. " Custom Option #" .. index .. " in " .. tostring(data.id)
|
|
.. " has been detected as corrupt, and has been deleted.", true)
|
|
end
|
|
|
|
local function validateUserConfig(data, options, config)
|
|
local authorOptionKeys, corruptOptions = {}, {}
|
|
options = options or {}
|
|
config = config or {}
|
|
|
|
for index = 1, #options do
|
|
local option = options[index]
|
|
if not customOptionIsValid(option) or authorOptionKeys[option.key] then
|
|
reportCorruptOption(data, index)
|
|
corruptOptions[index] = true
|
|
else
|
|
local optionClass = WA.author_option_classes[option.type]
|
|
if option.key then authorOptionKeys[option.key] = index end
|
|
if optionClass == "simple" then
|
|
if config[option.key] == nil then
|
|
config[option.key] = copyDefault(option)
|
|
end
|
|
elseif optionClass == "group" then
|
|
local subOptions = option.subOptions
|
|
if type(config[option.key]) ~= "table" then config[option.key] = {} end
|
|
local subConfig = config[option.key]
|
|
if option.groupType == "array" then
|
|
local invalidArray = false
|
|
for key, value in pairs(subConfig) do
|
|
if type(key) ~= "number" or type(value) ~= "table" then
|
|
invalidArray = true
|
|
break
|
|
end
|
|
end
|
|
if invalidArray then clearTable(subConfig) end
|
|
if option.limitType == "fixed" then
|
|
for i = #subConfig + 1, option.size do subConfig[i] = {} end
|
|
end
|
|
if option.limitType ~= "none" then
|
|
for i = option.size + 1, #subConfig do subConfig[i] = nil end
|
|
end
|
|
for _, toValidate in pairs(subConfig) do
|
|
validateUserConfig(data, subOptions, toValidate)
|
|
end
|
|
else
|
|
local firstKey = next(subConfig)
|
|
if type(firstKey) ~= "string" then clearTable(subConfig) end
|
|
validateUserConfig(data, subOptions, subConfig)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for key, value in pairs(config) do
|
|
if not authorOptionKeys[key] then
|
|
config[key] = nil
|
|
else
|
|
local option = options[authorOptionKeys[key]]
|
|
local optionClass = WA.author_option_classes[option.type]
|
|
if optionClass and optionClass ~= "group" then
|
|
if option.type == "media" then
|
|
if type(value) ~= "string" and (type(value) ~= "number" or option.mediaType ~= "sound") then
|
|
config[key] = copyDefault(option)
|
|
end
|
|
elseif type(value) ~= type(option.default) then
|
|
config[key] = copyDefault(option)
|
|
elseif option.type == "input" and option.useLength then
|
|
config[key] = string.sub(config[key], 1, option.length)
|
|
elseif option.type == "number" or option.type == "range" then
|
|
if (option.max and option.max < value) or (option.min and option.min > value) then
|
|
config[key] = copyDefault(option)
|
|
elseif option.type == "number" and option.step then
|
|
local min = option.min or 0
|
|
config[key] = option.step * round((value - min) / option.step) + min
|
|
end
|
|
elseif option.type == "select" then
|
|
if value < 1 or value > #option.values then config[key] = copyDefault(option) end
|
|
elseif option.type == "multiselect" then
|
|
local multiselect = config[key]
|
|
if type(multiselect) ~= "table" then
|
|
config[key] = {}
|
|
multiselect = config[key]
|
|
end
|
|
for i = 1, #multiselect do
|
|
if option.default[i] ~= nil then
|
|
if type(multiselect[i]) ~= "boolean" then multiselect[i] = option.default[i] end
|
|
else
|
|
multiselect[i] = nil
|
|
end
|
|
end
|
|
if type(option.default) ~= "table" then option.default = {} end
|
|
for i = 1, #option.default do
|
|
if type(multiselect[i]) ~= "boolean" then multiselect[i] = option.default[i] end
|
|
end
|
|
elseif option.type == "color" then
|
|
if type(config[key]) ~= "table" then config[key] = {} end
|
|
for i = 1, 4 do
|
|
local component = config[key][i]
|
|
if type(component) ~= "number" or component < 0 or component > 1 then
|
|
config[key] = copyDefault(option)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for i = #options, 1, -1 do
|
|
if corruptOptions[i] then table.remove(options, i) end
|
|
end
|
|
end
|
|
|
|
function WA.ValidateUserConfig(data)
|
|
if not data then return end
|
|
data.authorOptions = data.authorOptions or {}
|
|
data.config = data.config or {}
|
|
validateUserConfig(data, data.authorOptions, data.config)
|
|
end
|
|
|
|
-- Upstream's `width` is a number from 0.1 to 2 scaling an AceConfig row; ours is
|
|
-- a two-column grid, so everything under a full row lands in the half column.
|
|
-- 1 and 2 -- the two values its own editor offers -- land exactly where upstream
|
|
-- puts them, and a fractional one degrades to the nearer of the two.
|
|
local function customOptionHalf(option)
|
|
return (option.width or 1) < 2
|
|
end
|
|
|
|
-- A range's *track*, as against the hard limits a typed value may reach:
|
|
-- `softMin`/`softMax` where the author gave them, the hard bounds otherwise, and
|
|
-- upstream's 0..100 where there are none of either. The spin box takes both
|
|
-- pairs and keeps them apart; nothing here has to choose between them.
|
|
local function rangeTrack(option)
|
|
local lo = option.softMin or option.min or 0
|
|
local hi = option.softMax or option.max or 100
|
|
if hi < lo then hi = lo end
|
|
return lo, hi
|
|
end
|
|
|
|
-- A number option's bounds applied at the write rather than left to the next
|
|
-- validation pass, so a typed value outside the author's range never reaches the
|
|
-- aura's code at all. Snapped onto the step grid first, as the validator does.
|
|
local function clampAuthorNumber(option, value)
|
|
local min, max, step = option.min, option.max, option.step
|
|
if step and step > 0 then
|
|
local origin = min or 0
|
|
value = origin + step * round((value - origin) / step)
|
|
end
|
|
if min and value < min then value = min end
|
|
if max and value > max then value = max end
|
|
return value
|
|
end
|
|
|
|
-- Values reordered so the user reads them alphabetically while the stored value
|
|
-- stays what it was -- an index for a select, a page number for an array group.
|
|
-- The labels decide the order; the values ride along.
|
|
local function sortedByLabel(values, labels)
|
|
local order = {}
|
|
for i = 1, #values do order[i] = values[i] end
|
|
table.sort(order, function(first, second)
|
|
return tostring((labels and labels[first]) or first)
|
|
< tostring((labels and labels[second]) or second)
|
|
end)
|
|
return order
|
|
end
|
|
|
|
local function setCustomConfig(data, option, value, config)
|
|
config = config or data.config
|
|
setOptionValue(data, option, value, config)
|
|
end
|
|
|
|
local function selectFields(fields, data, option, config)
|
|
local values, labels = {}, {}
|
|
for i = 1, #(option.values or {}) do
|
|
values[i] = i
|
|
labels[i] = option.values[i]
|
|
end
|
|
if option.sortAlphabetically then values = sortedByLabel(values, labels) end
|
|
local field = {
|
|
type = "select", name = option.name, values = values, labels = labels,
|
|
half = customOptionHalf(option),
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(index)
|
|
local value = index
|
|
setCustomConfig(data, option, value, config)
|
|
end,
|
|
}
|
|
table.insert(fields, field)
|
|
end
|
|
|
|
local function mediaField(fields, data, option, config)
|
|
local mediaType = option.mediaType
|
|
if mediaType == "sound" then
|
|
local previews = {}
|
|
for key in pairs(WA.sound_types or {}) do
|
|
if key ~= " custom" and key ~= " KitID" then previews[key] = true end
|
|
end
|
|
table.insert(fields, {
|
|
type = "select", name = option.name, values = WA.SoundValues or {},
|
|
labels = WA.sound_types, previews = previews,
|
|
onPreview = function(value)
|
|
if value ~= " custom" and value ~= " KitID" and WA.PreviewSound then
|
|
WA.PreviewSound(value)
|
|
end
|
|
end,
|
|
half = customOptionHalf(option),
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, value, config) end,
|
|
})
|
|
elseif mediaType == "font" and WA.textCore and WA.textCore.FONTS then
|
|
local values, labels = {}, {}
|
|
for i = 1, #WA.textCore.FONTS do
|
|
local font = WA.textCore.FONTS[i]
|
|
values[i] = font.path
|
|
labels[font.path] = font.name
|
|
end
|
|
table.insert(fields, {
|
|
type = "select", name = option.name, values = values, labels = labels,
|
|
half = customOptionHalf(option),
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, value, config) end,
|
|
})
|
|
elseif mediaType == "statusbar" and WA.Widgets and WA.Widgets.BarTextures then
|
|
local values = WA.Widgets.BarTextures()
|
|
table.insert(fields, {
|
|
type = "select", name = option.name, values = values,
|
|
swatches = WA.Widgets.BarTextureSwatches and WA.Widgets.BarTextureSwatches() or nil,
|
|
half = customOptionHalf(option),
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, value, config) end,
|
|
})
|
|
else
|
|
table.insert(fields, {
|
|
type = "input", name = option.name, half = customOptionHalf(option),
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, value, config) end,
|
|
})
|
|
end
|
|
end
|
|
|
|
local function simpleOptionField(fields, data, option, config)
|
|
config = config or data.config
|
|
local half = customOptionHalf(option)
|
|
if option.type == "toggle" then
|
|
table.insert(fields, {
|
|
type = "toggle", name = option.name, half = half,
|
|
get = function() return optionValue(option, config) and true or false end,
|
|
set = function(value) setCustomConfig(data, option, value and true or false, config) end,
|
|
})
|
|
elseif option.type == "input" then
|
|
table.insert(fields, {
|
|
type = option.multiline and "multiline" or "input", name = option.name,
|
|
half = half, height = option.multiline and 80 or nil,
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, value, config) end,
|
|
})
|
|
elseif option.type == "number" then
|
|
table.insert(fields, {
|
|
type = "input", name = option.name, half = half,
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value)
|
|
local number = tonumber(value)
|
|
if number == nil then return end
|
|
local bounded = clampAuthorNumber(option, number)
|
|
setCustomConfig(data, option, bounded, config)
|
|
-- The box still shows what was typed while the config holds what the
|
|
-- author's bounds allow, so the two only agree again after a repaint.
|
|
if bounded ~= number and WA.RefreshOptions then WA.RefreshOptions() end
|
|
end,
|
|
})
|
|
elseif option.type == "range" then
|
|
local trackLo, trackHi = rangeTrack(option)
|
|
table.insert(fields, {
|
|
type = "range", name = option.name, half = half,
|
|
min = option.min, max = option.max, step = option.step or option.bigStep or 1,
|
|
softMin = trackLo, softMax = trackHi, bigStep = option.bigStep,
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, tonumber(value), config) end,
|
|
})
|
|
elseif option.type == "color" then
|
|
table.insert(fields, {
|
|
type = "color", name = option.name, half = half,
|
|
get = function() return optionValue(option, config) end,
|
|
set = function(value) setCustomConfig(data, option, value, config) end,
|
|
})
|
|
elseif option.type == "select" then
|
|
selectFields(fields, data, option, config)
|
|
elseif option.type == "multiselect" then
|
|
table.insert(fields, {
|
|
type = "description", name = option.name or "",
|
|
})
|
|
-- The toggles may be painted in label order, but each still writes at its
|
|
-- own position: the stored value is a positional array of booleans, so the
|
|
-- sort is a view and never reaches the config.
|
|
local order = {}
|
|
for i = 1, #(option.values or {}) do order[i] = i end
|
|
if option.sortAlphabetically then order = sortedByLabel(order, option.values) end
|
|
for position = 1, #order do
|
|
local i = order[position]
|
|
local index = i
|
|
table.insert(fields, {
|
|
type = "toggle", name = option.values[i], half = half,
|
|
get = function()
|
|
local values = optionValue(option, config)
|
|
return type(values) == "table" and values[index] and true or false
|
|
end,
|
|
set = function(value)
|
|
setOptionValueAt(data, option, index, value and true or false, config)
|
|
end,
|
|
})
|
|
end
|
|
elseif option.type == "media" then
|
|
mediaField(fields, data, option, config)
|
|
end
|
|
end
|
|
|
|
local function placeholderField(fields, option)
|
|
local name = option.name or option.key or "Unnamed option"
|
|
local message
|
|
if option.type == "group" then
|
|
message = "Custom option group '" .. tostring(name) .. "' is not editable here."
|
|
else
|
|
message = "Custom option '" .. tostring(name) .. "' of type '"
|
|
.. tostring(option.type) .. "' is not editable here."
|
|
end
|
|
table.insert(fields, { type = "description", name = message, half = customOptionHalf(option) })
|
|
end
|
|
|
|
local authorOptionTypes = {
|
|
"toggle", "input", "number", "range", "color", "select",
|
|
"description", "space", "header", "multiselect", "media", "group",
|
|
}
|
|
|
|
local authorWidthValues = {1, 2}
|
|
local authorWidthLabels = {[1] = "Half", [2] = "Full"}
|
|
|
|
local function authorField(field, index, action)
|
|
field.authorIndex = index
|
|
field.authorAction = action
|
|
return field
|
|
end
|
|
|
|
local function authorKeyIsAvailable(data, option, key, options)
|
|
if not key or key == "" then return false end
|
|
options = options or data.authorOptions or {}
|
|
for i = 1, #options do
|
|
local other = options[i]
|
|
if other ~= option and other.key == key then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function ensureUniqueAuthorKey(candidate, suffix, options, index)
|
|
index = index or 1
|
|
local key = candidate
|
|
local existingKeys = {}
|
|
local goodKey = true
|
|
for i = 1, #(options or {}) do
|
|
local option = options[i]
|
|
if option and option.key then existingKeys[option.key] = true end
|
|
end
|
|
if existingKeys[key] then goodKey = false end
|
|
if not goodKey then
|
|
local prefix = candidate .. (suffix or "")
|
|
while not goodKey do
|
|
key = prefix .. index
|
|
goodKey = not existingKeys[key]
|
|
index = index + 1
|
|
end
|
|
end
|
|
return key
|
|
end
|
|
|
|
local function authorOptionChanged(data)
|
|
WA.MergeDefaults(data)
|
|
WA.Add(data)
|
|
WA.RefreshAuraEnvConfig(data.id)
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end
|
|
|
|
local function setAuthorOption(data, option, key, value, options)
|
|
if key == "key" and not authorKeyIsAvailable(data, option, value, options) then return end
|
|
option[key] = value
|
|
authorOptionChanged(data)
|
|
end
|
|
|
|
local function authorInputField(index, name, key, data, option, half, options)
|
|
return authorField({
|
|
type = "input", name = name, half = half,
|
|
get = function() return option[key] end,
|
|
set = function(value) setAuthorOption(data, option, key, value, options) end,
|
|
}, index, key)
|
|
end
|
|
|
|
local function authorNumberField(index, name, key, data, option, half, options)
|
|
return authorField({
|
|
type = "input", name = name, half = half,
|
|
get = function() return option[key] end,
|
|
set = function(value)
|
|
local number = tonumber(value)
|
|
if number ~= nil then setAuthorOption(data, option, key, number, options) end
|
|
end,
|
|
}, index, key)
|
|
end
|
|
|
|
-- The same for a field that is genuinely optional -- a range's soft bounds and
|
|
-- its slider step, none of which the schema seeds. Emptying the box means
|
|
-- "unset" rather than "unchanged", which is the only way back off a value the
|
|
-- author typed once; the field above cannot express that, and a range carrying a
|
|
-- softMin it no longer wants exports one.
|
|
local function authorOptionalNumberField(index, name, key, data, option, half)
|
|
return authorField({
|
|
type = "input", name = name, half = half,
|
|
get = function()
|
|
local value = option[key]
|
|
return value ~= nil and tostring(value) or ""
|
|
end,
|
|
set = function(value)
|
|
if value == nil or value == "" then
|
|
setAuthorOption(data, option, key, nil)
|
|
return
|
|
end
|
|
local number = tonumber(value)
|
|
if number ~= nil then setAuthorOption(data, option, key, number) end
|
|
end,
|
|
}, index, key)
|
|
end
|
|
|
|
local function authorSortToggle(fields, data, option, index)
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Sort Alphabetically", half = true,
|
|
get = function() return option.sortAlphabetically and true or false end,
|
|
set = function(value)
|
|
setAuthorOption(data, option, "sortAlphabetically", value and true or false)
|
|
end,
|
|
}, index, "sortAlphabetically"))
|
|
end
|
|
|
|
-- Only the three this client can put a picker behind. Upstream's `border` and
|
|
-- `background` read their lists off LibSharedMedia, which is not here and has
|
|
-- nothing to stand in for it -- the border sub-region draws one hardcoded edge
|
|
-- file and the background one is not a texture at all -- so offering them would
|
|
-- open an empty dropdown, which is worse than not offering the type.
|
|
--
|
|
-- The names stay in author_option_media_defaults and in the labels below: an
|
|
-- imported option already set to one keeps working (its Default falls back to a
|
|
-- plain path box, and authorMediaTypeValues below keeps it selectable so the
|
|
-- dropdown can still show what it is) and exports unchanged.
|
|
local authorMediaTypes = {"sound", "font", "statusbar"}
|
|
local authorMediaLabels = {
|
|
sound = "Sound", font = "Font", border = "Border",
|
|
background = "Background", statusbar = "Status Bar",
|
|
}
|
|
|
|
-- The offered list, plus whatever this option is already set to. Same rule the
|
|
-- array group's Entry Name Source follows: a dropdown must be able to show its
|
|
-- own current value, or the control reads as unset and the first edit silently
|
|
-- changes something the author never touched.
|
|
local function authorMediaTypeValues(option)
|
|
local values = {}
|
|
for i = 1, #authorMediaTypes do values[i] = authorMediaTypes[i] end
|
|
local current = option.mediaType
|
|
if current then
|
|
for i = 1, #values do
|
|
if values[i] == current then return values end
|
|
end
|
|
table.insert(values, current)
|
|
if not authorMediaLabels[current] then authorMediaLabels[current] = current end
|
|
end
|
|
return values
|
|
end
|
|
|
|
local function authorMediaValues(option)
|
|
local mediaType = option.mediaType
|
|
local values, labels = {}, {}
|
|
if mediaType == "sound" then
|
|
values = WA.SoundValues or {}
|
|
labels = WA.sound_types or {}
|
|
elseif mediaType == "font" and WA.textCore and WA.textCore.FONTS then
|
|
for i = 1, #WA.textCore.FONTS do
|
|
local font = WA.textCore.FONTS[i]
|
|
values[i] = font.path
|
|
labels[font.path] = font.name
|
|
end
|
|
elseif mediaType == "statusbar" and WA.Widgets and WA.Widgets.BarTextures then
|
|
values = WA.Widgets.BarTextures()
|
|
labels = {}
|
|
for i = 1, #values do labels[values[i]] = values[i] end
|
|
end
|
|
return values, labels
|
|
end
|
|
|
|
local function authorMediaFields(fields, data, option, index)
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Media Type", values = authorMediaTypeValues(option),
|
|
labels = authorMediaLabels, half = true,
|
|
get = function() return option.mediaType end,
|
|
set = function(value)
|
|
option.mediaType = value
|
|
option.default = WA.author_option_media_defaults[value]
|
|
authorOptionChanged(data)
|
|
end,
|
|
}, index, "mediaType"))
|
|
|
|
local values, labels = authorMediaValues(option)
|
|
if #values > 0 then
|
|
local field = {
|
|
type = "select", name = "Default", values = values, labels = labels,
|
|
half = false,
|
|
get = function() return option.default end,
|
|
set = function(value) setAuthorOption(data, option, "default", value) end,
|
|
}
|
|
if option.mediaType == "sound" then
|
|
field.previews = {}
|
|
for i = 1, #values do
|
|
local value = values[i]
|
|
if value ~= " custom" and value ~= " KitID" then field.previews[value] = true end
|
|
end
|
|
field.onPreview = function(value)
|
|
if value ~= " custom" and value ~= " KitID" and WA.PreviewSound then
|
|
WA.PreviewSound(value)
|
|
end
|
|
end
|
|
elseif option.mediaType == "statusbar" and WA.Widgets.BarTextureSwatches then
|
|
field.swatches = WA.Widgets.BarTextureSwatches()
|
|
end
|
|
table.insert(fields, authorField(field, index, "default"))
|
|
else
|
|
table.insert(fields, authorInputField(index, "Default", "default", data, option, false))
|
|
end
|
|
end
|
|
|
|
local function normalizeMultiselectDefault(option)
|
|
if type(option.default) ~= "table" then option.default = {} end
|
|
local count = #(option.values or {})
|
|
for i = 1, count do
|
|
if type(option.default[i]) ~= "boolean" then option.default[i] = false end
|
|
end
|
|
for i = #option.default, count + 1, -1 do option.default[i] = nil end
|
|
end
|
|
|
|
local function reorderMultiselectDefault(option, fromIndex, before)
|
|
local value = table.remove(option.default, fromIndex)
|
|
local insertAt = (fromIndex < before) and (before - 1) or before
|
|
table.insert(option.default, insertAt, value)
|
|
normalizeMultiselectDefault(option)
|
|
end
|
|
|
|
local function authorMultiselectFields(fields, data, option, index)
|
|
local valuesField = authorField({
|
|
type = "namelist", name = "Values", get = function() return option.values end,
|
|
onChange = function()
|
|
normalizeMultiselectDefault(option)
|
|
authorOptionChanged(data)
|
|
end,
|
|
onReorder = function(fromIndex, before)
|
|
reorderMultiselectDefault(option, fromIndex, before)
|
|
authorOptionChanged(data)
|
|
end,
|
|
onRemove = function(indexValue)
|
|
table.remove(option.default, indexValue)
|
|
authorOptionChanged(data)
|
|
end,
|
|
onAdd = function()
|
|
table.insert(option.default, #option.values, false)
|
|
authorOptionChanged(data)
|
|
end,
|
|
}, index, "values")
|
|
table.insert(fields, valuesField)
|
|
authorSortToggle(fields, data, option, index)
|
|
|
|
normalizeMultiselectDefault(option)
|
|
for valueIndex = 1, #(option.values or {}) do
|
|
local i = valueIndex
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Default: " .. tostring(option.values[i]), half = true,
|
|
get = function() return option.default[i] and true or false end,
|
|
set = function(value)
|
|
option.default[i] = value and true or false
|
|
authorOptionChanged(data)
|
|
end,
|
|
}, index, "default" .. i))
|
|
end
|
|
end
|
|
|
|
local function authorSimpleTypeFields(fields, data, option, index)
|
|
local optionType = option.type
|
|
if optionType == "toggle" then
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Default", values = {0, 1},
|
|
labels = {[0] = "False", [1] = "True"}, half = true,
|
|
get = function() return option.default and 1 or 0 end,
|
|
set = function(value) setAuthorOption(data, option, "default", value == 1) end,
|
|
}, index, "default"))
|
|
elseif optionType == "input" then
|
|
table.insert(fields, authorInputField(index, "Default", "default", data, option, false))
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Limit Length", half = true,
|
|
get = function() return option.useLength and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "useLength", value and true or false) end,
|
|
}, index, "useLength"))
|
|
if option.useLength then
|
|
table.insert(fields, authorNumberField(index, "Length", "length", data, option, true))
|
|
end
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Multiline", half = true,
|
|
get = function() return option.multiline and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "multiline", value and true or false) end,
|
|
}, index, "multiline"))
|
|
elseif optionType == "number" then
|
|
table.insert(fields, authorNumberField(index, "Default", "default", data, option, true))
|
|
table.insert(fields, authorNumberField(index, "Minimum", "min", data, option, true))
|
|
table.insert(fields, authorNumberField(index, "Maximum", "max", data, option, true))
|
|
table.insert(fields, authorNumberField(index, "Step", "step", data, option, true))
|
|
elseif optionType == "range" then
|
|
local trackLo, trackHi = rangeTrack(option)
|
|
table.insert(fields, authorField({
|
|
type = "range", name = "Default",
|
|
min = option.min, max = option.max,
|
|
softMin = trackLo, softMax = trackHi,
|
|
step = option.step or option.bigStep or .05, bigStep = option.bigStep, half = true,
|
|
get = function() return option.default end,
|
|
set = function(value) setAuthorOption(data, option, "default", tonumber(value)) end,
|
|
}, index, "default"))
|
|
table.insert(fields, authorNumberField(index, "Minimum", "min", data, option, true))
|
|
table.insert(fields, authorNumberField(index, "Maximum", "max", data, option, true))
|
|
table.insert(fields, authorNumberField(index, "Step", "step", data, option, true))
|
|
table.insert(fields, authorOptionalNumberField(index, "Soft Min", "softMin", data, option, true))
|
|
table.insert(fields, authorOptionalNumberField(index, "Soft Max", "softMax", data, option, true))
|
|
table.insert(fields, authorOptionalNumberField(index, "Slider Step Size", "bigStep", data, option, true))
|
|
elseif optionType == "color" then
|
|
table.insert(fields, authorField({
|
|
type = "color", name = "Default", half = true,
|
|
get = function() return option.default end,
|
|
set = function(value) setAuthorOption(data, option, "default", value) end,
|
|
}, index, "default"))
|
|
elseif optionType == "select" then
|
|
local values, labels = {}, {}
|
|
for valueIndex = 1, #(option.values or {}) do
|
|
values[valueIndex] = valueIndex
|
|
labels[valueIndex] = option.values[valueIndex]
|
|
end
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Default", values = values, labels = labels, half = true,
|
|
get = function() return option.default end,
|
|
set = function(value) setAuthorOption(data, option, "default", value) end,
|
|
}, index, "default"))
|
|
table.insert(fields, authorField({
|
|
type = "namelist", name = "Values",
|
|
get = function() return option.values end,
|
|
onChange = function() authorOptionChanged(data) end,
|
|
}, index, "values"))
|
|
authorSortToggle(fields, data, option, index)
|
|
elseif optionType == "media" then
|
|
authorMediaFields(fields, data, option, index)
|
|
elseif optionType == "multiselect" then
|
|
authorMultiselectFields(fields, data, option, index)
|
|
elseif optionType == "group" then
|
|
placeholderField(fields, option)
|
|
end
|
|
end
|
|
|
|
local function authorNoninteractiveFields(fields, data, option, index)
|
|
if option.type == "description" then
|
|
table.insert(fields, authorInputField(index, "Text", "text", data, option, false))
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Font Size", values = {"small", "medium", "large"},
|
|
labels = {small = "Small", medium = "Medium", large = "Large"}, half = true,
|
|
get = function() return option.fontSize end,
|
|
set = function(value) setAuthorOption(data, option, "fontSize", value) end,
|
|
}, index, "fontSize"))
|
|
elseif option.type == "header" then
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Use Name", half = true,
|
|
get = function() return option.useName and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "useName", value and true or false) end,
|
|
}, index, "useName"))
|
|
table.insert(fields, authorInputField(index, "Text", "text", data, option, false))
|
|
elseif option.type == "space" then
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Variable Width", half = true,
|
|
get = function() return option.variableWidth and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "variableWidth", value and true or false) end,
|
|
}, index, "variableWidth"))
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Use Height", half = true,
|
|
get = function() return option.useHeight and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "useHeight", value and true or false) end,
|
|
}, index, "useHeight"))
|
|
if option.useHeight then
|
|
table.insert(fields, authorNumberField(index, "Height", "height", data, option, true))
|
|
end
|
|
end
|
|
end
|
|
|
|
local function authorGroupFields(fields, data, option, index)
|
|
local groupTypes = {"simple", "array"}
|
|
local groupTypeLabels = {simple = "Simple", array = "Array"}
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Group Type", values = groupTypes,
|
|
labels = groupTypeLabels, half = true,
|
|
get = function() return option.groupType end,
|
|
set = function(value) setAuthorOption(data, option, "groupType", value) end,
|
|
}, index, "groupType"))
|
|
if option.groupType ~= "array" then return end
|
|
|
|
authorSortToggle(fields, data, option, index)
|
|
|
|
local limitTypes = {"none", "max", "fixed"}
|
|
local limitLabels = {none = "Unlimited", max = "Limited", fixed = "Fixed Size"}
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Limit Type", values = limitTypes,
|
|
labels = limitLabels, half = true,
|
|
get = function() return option.limitType end,
|
|
set = function(value) setAuthorOption(data, option, "limitType", value) end,
|
|
}, index, "limitType"))
|
|
table.insert(fields, authorNumberField(index, "Size", "size", data, option, true))
|
|
|
|
local nameSources, nameSourceLabels = {-1, 0}, {
|
|
[-1] = "Fixed Names", [0] = "Entry Order",
|
|
}
|
|
for subIndex = 1, #(option.subOptions or {}) do
|
|
local subOption = option.subOptions[subIndex]
|
|
if subOption and (subOption.type == "input" or subOption.type == "number"
|
|
or subOption.type == "range" or option.nameSource == subIndex) then
|
|
table.insert(nameSources, subIndex)
|
|
nameSourceLabels[subIndex] = subOption.name or subOption.key or
|
|
("Option " .. tostring(subIndex))
|
|
end
|
|
end
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Entry Name Source", values = nameSources,
|
|
labels = nameSourceLabels, half = true,
|
|
get = function() return option.nameSource or 0 end,
|
|
set = function(value) setAuthorOption(data, option, "nameSource", value) end,
|
|
}, index, "nameSource"))
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Hide Reorder", half = true,
|
|
get = function() return option.hideReorder and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "hideReorder", value and true or false) end,
|
|
}, index, "hideReorder"))
|
|
end
|
|
|
|
local function authorOptionFields(fields, data, option, index, options)
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Option Type", values = authorOptionTypes,
|
|
labels = WA.author_option_types,
|
|
get = function() return option.type end,
|
|
set = function(value)
|
|
local old = option
|
|
local defaults = WA.author_option_fields[value] or {}
|
|
local newOption = WA.DeepCopy(defaults)
|
|
local commonFields = WA.author_option_fields.common
|
|
for key in pairs(commonFields) do
|
|
if old[key] ~= nil then newOption[key] = WA.DeepCopy(old[key]) end
|
|
end
|
|
newOption.type = value
|
|
local optionClass = WA.author_option_classes[value]
|
|
if optionClass == "noninteractive" then
|
|
newOption.name = nil
|
|
newOption.desc = nil
|
|
newOption.key = nil
|
|
newOption.useDesc = nil
|
|
newOption.default = nil
|
|
else
|
|
newOption.name = newOption.name or "Option " .. index
|
|
newOption.key = newOption.key or ensureUniqueAuthorKey(
|
|
"option" .. index, "", options or data.authorOptions, 1)
|
|
end
|
|
(options or data.authorOptions)[index] = newOption
|
|
authorOptionChanged(data)
|
|
end,
|
|
}, index, "type"))
|
|
|
|
local optionClass = WA.author_option_classes[option.type]
|
|
if optionClass ~= "noninteractive" then
|
|
table.insert(fields, authorInputField(index, "Display Name", "name", data, option, true, options))
|
|
table.insert(fields, authorInputField(index, "Option Key", "key", data, option, true, options))
|
|
if optionClass == "simple" then
|
|
table.insert(fields, authorField({
|
|
type = "toggle", name = "Description", half = true,
|
|
get = function() return option.useDesc and true or false end,
|
|
set = function(value) setAuthorOption(data, option, "useDesc", value and true or false) end,
|
|
}, index, "useDesc"))
|
|
if option.useDesc then
|
|
table.insert(fields, authorInputField(index, "Description Text", "desc", data, option, false))
|
|
end
|
|
end
|
|
end
|
|
table.insert(fields, authorField({
|
|
type = "select", name = "Width", values = authorWidthValues,
|
|
labels = authorWidthLabels, half = true,
|
|
get = function() return option.width or 1 end,
|
|
set = function(value) setAuthorOption(data, option, "width", value) end,
|
|
}, index, "width"))
|
|
|
|
if optionClass == "simple" then
|
|
authorSimpleTypeFields(fields, data, option, index)
|
|
elseif optionClass == "noninteractive" then
|
|
authorNoninteractiveFields(fields, data, option, index)
|
|
elseif optionClass == "group" then
|
|
authorGroupFields(fields, data, option, index)
|
|
end
|
|
end
|
|
|
|
local function authorOptionActions(data, options, index, option)
|
|
return WA.Widgets.ListActions(options, index, function()
|
|
authorOptionChanged(data)
|
|
end, {
|
|
duplicate = function(list, itemIndex)
|
|
local copy = WA.DeepCopy(option)
|
|
if copy.key then
|
|
copy.key = ensureUniqueAuthorKey(copy.key .. "copy", "", options, 1)
|
|
end
|
|
if copy.name then copy.name = copy.name .. " - Copy" end
|
|
table.insert(list, itemIndex + 1, copy)
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function optionPathString(path)
|
|
return table.concat(path, ".")
|
|
end
|
|
|
|
local function copyOptionPath(path, value)
|
|
local copy = {}
|
|
for i = 1, #path do copy[i] = path[i] end
|
|
table.insert(copy, value)
|
|
return copy
|
|
end
|
|
|
|
local arrayPages = {}
|
|
|
|
local function arrayPageKey(data, path)
|
|
return tostring(data.id) .. "::config:" .. optionPathString(path)
|
|
end
|
|
|
|
local function getArrayPage(data, path, count)
|
|
local key = arrayPageKey(data, path)
|
|
local page = arrayPages[key] or 1
|
|
if page < 1 then page = 1 end
|
|
if count and count > 0 and page > count then page = count end
|
|
arrayPages[key] = page
|
|
return page
|
|
end
|
|
|
|
local function setArrayPage(data, path, page)
|
|
page = tonumber(page) or 1
|
|
if page < 1 then page = 1 end
|
|
arrayPages[arrayPageKey(data, path)] = page
|
|
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 not mergedOption.subOptions then return end
|
|
local subConfig
|
|
if config then
|
|
if mergedOption.groupType == "simple" then
|
|
subConfig = config[mergedOption.key]
|
|
else
|
|
local configList = config[mergedOption.key]
|
|
local count = type(configList) == "table" and #configList or 0
|
|
local page = getArrayPage(data, path, count)
|
|
subConfig = type(configList) == "table" and configList[page] or nil
|
|
end
|
|
end
|
|
local subOptions = options[index].subOptions
|
|
for i = 1, #mergedOption.subOptions do
|
|
local subPath = {}
|
|
for j = 1, #path do subPath[j] = path[j] end
|
|
table.insert(subPath, i)
|
|
initReferences(mergedOption.subOptions[i], data, subOptions, i,
|
|
subConfig, subPath, mergedOption)
|
|
end
|
|
end
|
|
|
|
local function mergeOptions(mergedOptions, data, options, config, prepath, parent)
|
|
local nextInsert = 1
|
|
prepath = prepath or {}
|
|
options = options or {}
|
|
for i = 1, #options do
|
|
local path = {}
|
|
for j = 1, #prepath do path[j] = prepath[j] end
|
|
table.insert(path, i)
|
|
local nextOption = options[i]
|
|
local shouldMerge = false
|
|
if not nextOption.noMerge then
|
|
for j = nextInsert, #mergedOptions + 1 do
|
|
local mergedOption = mergedOptions[j]
|
|
if not mergedOption then break end
|
|
local validMerge = not mergedOption.noMerge
|
|
if validMerge then
|
|
for field in pairs(significantFieldsForMerge) do
|
|
if nextOption[field] ~= mergedOption[field] then
|
|
validMerge = false
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if validMerge then
|
|
shouldMerge = true
|
|
nextInsert = j
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if shouldMerge then
|
|
local mergedOption = mergedOptions[nextInsert]
|
|
mergedOption.references[data.id] = {
|
|
data = data, options = options, index = i,
|
|
config = config, path = path, parent = parent,
|
|
}
|
|
for key, value in pairs(nextOption) do
|
|
if key == "subOptions" then
|
|
local subConfig
|
|
if config then
|
|
if mergedOption.groupType == "simple" then
|
|
subConfig = config[mergedOption.key]
|
|
else
|
|
local configList = config[mergedOption.key]
|
|
local count = type(configList) == "table" and #configList or 0
|
|
local page = getArrayPage(data, path, count)
|
|
subConfig = type(configList) == "table" and configList[page] or nil
|
|
end
|
|
end
|
|
mergeOptions(mergedOption.subOptions, data, value, subConfig, path, mergedOption)
|
|
if mergedOption.groupType == "array" and mergedOption.nameSource ~= nil then
|
|
if (nextOption.nameSource or 0) < 1 or mergedOption.nameSource < 1 then
|
|
if mergedOption.nameSource ~= nextOption.nameSource then
|
|
mergedOption.nameSource = nil
|
|
end
|
|
else
|
|
local source = mergedOption.subOptions[mergedOption.nameSource]
|
|
local sourceReference = source and source.references[data.id]
|
|
if not sourceReference or sourceReference.index ~= nextOption.nameSource then
|
|
mergedOption.nameSource = nil
|
|
end
|
|
end
|
|
end
|
|
elseif not specialCasesForMerge[key] and not valuesEqual(mergedOption[key], value) then
|
|
mergedOption[key] = nil
|
|
end
|
|
end
|
|
else
|
|
nextInsert = #mergedOptions + 1
|
|
local newOption = WA.DeepCopy(nextOption)
|
|
initReferences(newOption, data, options, i, config, path, parent)
|
|
table.insert(mergedOptions, nextInsert, newOption)
|
|
end
|
|
nextInsert = nextInsert + 1
|
|
end
|
|
end
|
|
|
|
local function firstReference(option)
|
|
if not option.references then return nil end
|
|
for _, reference in pairs(option.references) do return reference end
|
|
return nil
|
|
end
|
|
|
|
local function referenceConfig(reference)
|
|
if reference.config then return reference.config end
|
|
local parent = reference.parent
|
|
if not parent or not parent.references then return nil end
|
|
local parentReference = parent.references[reference.data.id]
|
|
if not parentReference or not parentReference.config then return nil end
|
|
local parentOption = parentReference.options[parentReference.index]
|
|
local list = parentReference.config[parentOption.key]
|
|
if type(list) ~= "table" then
|
|
list = {}
|
|
parentReference.config[parentOption.key] = list
|
|
end
|
|
local page = getArrayPage(reference.data, parentReference.path, #list)
|
|
list[page] = list[page] or {}
|
|
reference.config = list[page]
|
|
return reference.config
|
|
end
|
|
|
|
optionValue = function(option, config)
|
|
if not option.references then return config and config[option.key] end
|
|
local found, value = false, nil
|
|
for _, reference in pairs(option.references) do
|
|
local childConfig = reference.config
|
|
local current = childConfig and childConfig[option.key]
|
|
if not found then
|
|
value, found = current, true
|
|
elseif not valuesEqual(value, current) then
|
|
return nil
|
|
end
|
|
end
|
|
return value
|
|
end
|
|
|
|
local function finishReferenceWrites(option)
|
|
for validationId, validationReference in pairs(option.references) do
|
|
WA.ValidateUserConfig(validationReference.data)
|
|
WA.Add(validationReference.data, true)
|
|
WA.RefreshAuraEnvConfig(validationReference.data.id)
|
|
end
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end
|
|
|
|
setOptionValue = function(data, option, value, config)
|
|
if not option.references then
|
|
config[option.key] = value
|
|
WA.Add(data, true)
|
|
WA.RefreshAuraEnvConfig(data.id)
|
|
return
|
|
end
|
|
for referenceId, reference in pairs(option.references) do
|
|
local childOption = reference.options[reference.index]
|
|
local childConfig = referenceConfig(reference)
|
|
if childConfig then childConfig[childOption.key] = WA.DeepCopy(value) end
|
|
end
|
|
finishReferenceWrites(option)
|
|
end
|
|
|
|
setOptionValueAt = function(data, option, index, value, config)
|
|
if not option.references then
|
|
local values = config[option.key]
|
|
if type(values) ~= "table" then values = {}; config[option.key] = values end
|
|
values[index] = value
|
|
WA.Add(data, true)
|
|
WA.RefreshAuraEnvConfig(data.id)
|
|
return
|
|
end
|
|
for referenceId, reference in pairs(option.references) do
|
|
local childOption = reference.options[reference.index]
|
|
local childConfig = referenceConfig(reference)
|
|
if childConfig then
|
|
local values = childConfig[childOption.key]
|
|
if type(values) ~= "table" then values = {}; childConfig[childOption.key] = values end
|
|
values[index] = value
|
|
end
|
|
end
|
|
finishReferenceWrites(option)
|
|
end
|
|
|
|
local function arrayEntryLabel(option, entry, index)
|
|
local fallback = "Entry " .. tostring(index)
|
|
local source = tonumber(option.nameSource) or 0
|
|
if source == -1 then
|
|
if type(option.entryNames) == "table" and option.entryNames[index] ~= nil then
|
|
return tostring(option.entryNames[index])
|
|
end
|
|
elseif source > 0 then
|
|
local sourceOption = option.subOptions and option.subOptions[source]
|
|
if sourceOption and type(entry) == "table" then
|
|
local value = entry[sourceOption.key]
|
|
if value ~= nil and type(value) ~= "table" then return tostring(value) end
|
|
end
|
|
end
|
|
return fallback
|
|
end
|
|
|
|
local function arrayEntries(config, option)
|
|
local entries = config[option.key]
|
|
if type(entries) ~= "table" then
|
|
entries = {}
|
|
config[option.key] = entries
|
|
end
|
|
return entries
|
|
end
|
|
|
|
local function arrayConfigChanged(data)
|
|
WA.ValidateUserConfig(data)
|
|
WA.Add(data, true)
|
|
WA.RefreshAuraEnvConfig(data.id)
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end
|
|
|
|
local function arrayReferenceChanged(option)
|
|
finishReferenceWrites(option)
|
|
end
|
|
|
|
local function arrayAction(actions, icon, tooltip, onClick)
|
|
table.insert(actions, {icon = icon, tooltip = tooltip, onClick = onClick})
|
|
end
|
|
|
|
local userOptionFields
|
|
|
|
local function arrayGroupFields(fields, data, option, config, path, indent)
|
|
local references = option.references
|
|
local first = firstReference(option)
|
|
local firstOption = first and first.options[first.index] or option
|
|
local firstConfig = first and first.config or config
|
|
local firstEntries = references and firstConfig and firstConfig[firstOption.key]
|
|
if type(firstEntries) ~= "table" then firstEntries = arrayEntries(config, option) end
|
|
local count = #firstEntries
|
|
local values, labels = {}, {}
|
|
|
|
local function referenceEntries(reference)
|
|
local childOption = reference.options[reference.index]
|
|
local childConfig = reference.config or {}
|
|
local entries = childConfig[childOption.key]
|
|
if type(entries) ~= "table" then entries = {} end
|
|
return entries, childOption
|
|
end
|
|
|
|
local function mergedCount()
|
|
local result = count
|
|
if references then
|
|
result = 0
|
|
for _, reference in pairs(references) do
|
|
local entries = referenceEntries(reference)
|
|
if #entries > result then result = #entries end
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
local function mergedLabel(index)
|
|
if not references then return arrayEntryLabel(option, firstEntries[index], index) end
|
|
local label, conflict = nil, false
|
|
for _, reference in pairs(references) do
|
|
local entries, childOption = referenceEntries(reference)
|
|
local current = arrayEntryLabel(childOption, entries[index], index)
|
|
if label == nil then label = current
|
|
elseif label ~= current then conflict = true end
|
|
end
|
|
if conflict or label == nil then return "Entry " .. tostring(index) end
|
|
return label
|
|
end
|
|
|
|
count = mergedCount()
|
|
for i = 1, count do
|
|
values[i] = i
|
|
labels[i] = mergedLabel(i)
|
|
end
|
|
-- The page numbers reordered by entry name; the value stays the page, so
|
|
-- everything downstream of the dropdown is untouched by the sort.
|
|
if option.sortAlphabetically then values = sortedByLabel(values, labels) end
|
|
|
|
local function referencePage(reference)
|
|
local entries = referenceEntries(reference)
|
|
return getArrayPage(reference.data, reference.path, #entries)
|
|
end
|
|
|
|
local function currentPage()
|
|
if not references then return getArrayPage(data, path, #(arrayEntries(config, option))) end
|
|
local page, found = nil, false
|
|
for _, reference in pairs(references) do
|
|
local current = referencePage(reference)
|
|
if not found then page, found = current, true
|
|
elseif page ~= current then return nil end
|
|
end
|
|
return page
|
|
end
|
|
|
|
local actions = {}
|
|
local iconDir = WA.Widgets and WA.Widgets.LIBWIDGETS_TEXTURES or
|
|
"Interface\\AddOns\\WeakestAuras\\libs\\LibWidgets\\textures\\"
|
|
table.insert(fields, {
|
|
type = "select", name = option.name or option.key or "Group",
|
|
values = values, labels = labels, half = customOptionHalf(option),
|
|
actions = actions,
|
|
configPath = optionPathString(path),
|
|
get = function()
|
|
if count == 0 or (references and not currentPage()) then return nil end
|
|
return currentPage()
|
|
end,
|
|
set = function(value)
|
|
if references then
|
|
for _, reference in pairs(references) do setArrayPage(reference.data, reference.path, value) end
|
|
else
|
|
setArrayPage(data, path, value)
|
|
end
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end,
|
|
})
|
|
|
|
if option.limitType ~= "fixed" then
|
|
local canAdd = true
|
|
if references then
|
|
for _, reference in pairs(references) do
|
|
local entries, childOption = referenceEntries(reference)
|
|
if childOption.limitType ~= "none" and #entries >= (tonumber(childOption.size) or 0) then
|
|
canAdd = false
|
|
end
|
|
end
|
|
else
|
|
canAdd = option.limitType == "none" or count < (tonumber(option.size) or 0)
|
|
end
|
|
if canAdd then
|
|
arrayAction(actions, "Interface\\Buttons\\UI-PlusButton-Up", "Add Entry", function()
|
|
if references then
|
|
for _, reference in pairs(references) do
|
|
local current, childOption = referenceEntries(reference)
|
|
if childOption.limitType == "none" or #current < (tonumber(childOption.size) or 0) then
|
|
table.insert(current, {})
|
|
setArrayPage(reference.data, reference.path, #current)
|
|
end
|
|
end
|
|
arrayReferenceChanged(option)
|
|
else
|
|
local current = arrayEntries(config, option)
|
|
if option.limitType ~= "none" and #current >= (tonumber(option.size) or 0) then return end
|
|
table.insert(current, {})
|
|
setArrayPage(data, path, #current)
|
|
arrayConfigChanged(data)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
if count > 1 and not option.hideReorder and option.nameSource ~= -1 then
|
|
arrayAction(actions, iconDir .. "up", "Move Entry Up", function()
|
|
if references then
|
|
for _, reference in pairs(references) do
|
|
local current = referenceEntries(reference)
|
|
local page = referencePage(reference)
|
|
if page > 1 and current[page] then
|
|
current[page], current[page - 1] = current[page - 1], current[page]
|
|
setArrayPage(reference.data, reference.path, page - 1)
|
|
end
|
|
end
|
|
arrayReferenceChanged(option)
|
|
else
|
|
local current = arrayEntries(config, option)
|
|
local page = getArrayPage(data, path, #current)
|
|
if page <= 1 or not current[page] then return end
|
|
current[page], current[page - 1] = current[page - 1], current[page]
|
|
setArrayPage(data, path, page - 1)
|
|
arrayConfigChanged(data)
|
|
end
|
|
end)
|
|
arrayAction(actions, iconDir .. "down", "Move Entry Down", function()
|
|
if references then
|
|
for _, reference in pairs(references) do
|
|
local current = referenceEntries(reference)
|
|
local page = referencePage(reference)
|
|
if page < #current and current[page] then
|
|
current[page], current[page + 1] = current[page + 1], current[page]
|
|
setArrayPage(reference.data, reference.path, page + 1)
|
|
end
|
|
end
|
|
arrayReferenceChanged(option)
|
|
else
|
|
local current = arrayEntries(config, option)
|
|
local page = getArrayPage(data, path, #current)
|
|
if page >= #current or not current[page] then return end
|
|
current[page], current[page + 1] = current[page + 1], current[page]
|
|
setArrayPage(data, path, page + 1)
|
|
arrayConfigChanged(data)
|
|
end
|
|
end)
|
|
end
|
|
|
|
if option.limitType ~= "fixed" and count > 0 then
|
|
arrayAction(actions, LibWidgets.ICON_DELETE, "Delete Entry", function()
|
|
if references then
|
|
for _, reference in pairs(references) do
|
|
local current = referenceEntries(reference)
|
|
local page = referencePage(reference)
|
|
if #current > 0 then
|
|
table.remove(current, page)
|
|
setArrayPage(reference.data, reference.path, math.min(page, #current))
|
|
end
|
|
end
|
|
arrayReferenceChanged(option)
|
|
else
|
|
local current = arrayEntries(config, option)
|
|
local page = getArrayPage(data, path, #current)
|
|
if #current == 0 then return end
|
|
table.remove(current, page)
|
|
local newCount = #current
|
|
setArrayPage(data, path, newCount > 0 and math.min(page, newCount) or 1)
|
|
arrayConfigChanged(data)
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Reset Entry: the selected entry's sub-config emptied, which the validation
|
|
-- that follows refills from the sub-options' defaults. Offered whatever the
|
|
-- limit type, unlike add and delete -- clearing an entry never changes how
|
|
-- many there are, so even a fixed-size array can have one put back.
|
|
if count > 0 then
|
|
arrayAction(actions, WA.STATUS_TEXTURES .. "standby.tga", "Reset Entry", function()
|
|
if references then
|
|
for _, reference in pairs(references) do
|
|
local current = referenceEntries(reference)
|
|
local page = referencePage(reference)
|
|
if current[page] then current[page] = {} end
|
|
end
|
|
arrayReferenceChanged(option)
|
|
else
|
|
local current = arrayEntries(config, option)
|
|
local page = getArrayPage(data, path, #current)
|
|
if not current[page] then return end
|
|
current[page] = {}
|
|
arrayConfigChanged(data)
|
|
end
|
|
end)
|
|
end
|
|
|
|
local page = currentPage() or (references and referencePage(first) or getArrayPage(data, path, count))
|
|
local selected = firstEntries[page]
|
|
if references and not selected then
|
|
for _, reference in pairs(references) do
|
|
local entries = referenceEntries(reference)
|
|
if entries[page] then selected = entries[page]; break end
|
|
end
|
|
end
|
|
if count > 0 and selected then
|
|
userOptionFields(fields, data, option.subOptions, selected, path, indent + 1)
|
|
end
|
|
end
|
|
|
|
local function annotateFields(fields, start, indent, key, value)
|
|
for i = start + 1, #fields do
|
|
if indent and indent > 0 then fields[i].indent = indent end
|
|
if key then fields[i][key] = value end
|
|
end
|
|
end
|
|
|
|
local function optionSubConfig(option, config)
|
|
local reference = firstReference(option)
|
|
if reference then
|
|
local childOption = reference.options[reference.index]
|
|
local childConfig = referenceConfig(reference)
|
|
if childConfig and type(childConfig[childOption.key]) ~= "table" then
|
|
childConfig[childOption.key] = {}
|
|
end
|
|
return childConfig and childConfig[childOption.key] or {}
|
|
end
|
|
if type(config[option.key]) ~= "table" then config[option.key] = {} end
|
|
return config[option.key]
|
|
end
|
|
|
|
local function optionCollapsed(option, data, path, defaultCollapsed)
|
|
if not WA.OptionsState then return defaultCollapsed end
|
|
if not option.references then
|
|
return WA.OptionsState.isCollapsed(data, "config:" .. optionPathString(path), defaultCollapsed)
|
|
end
|
|
for _, reference in pairs(option.references) do
|
|
if WA.OptionsState.isCollapsed(reference.data, "config:" .. optionPathString(reference.path), defaultCollapsed) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function setOptionCollapsed(option, data, path, value)
|
|
if not WA.OptionsState then return end
|
|
if not option.references then
|
|
WA.OptionsState.setCollapsed(data, "config:" .. optionPathString(path), value)
|
|
return
|
|
end
|
|
for _, reference in pairs(option.references) do
|
|
WA.OptionsState.setCollapsed(reference.data, "config:" .. optionPathString(reference.path), value)
|
|
end
|
|
end
|
|
|
|
userOptionFields = function(fields, data, options, config, path, indent)
|
|
for index = 1, #(options or {}) do
|
|
local option = options[index]
|
|
local optionPath = copyOptionPath(path, index)
|
|
local start = #fields
|
|
local optionClass = option and WA.author_option_classes[option.type]
|
|
if optionClass == "simple" then
|
|
simpleOptionField(fields, data, option, config)
|
|
elseif optionClass == "noninteractive" then
|
|
if option.type == "description" then
|
|
table.insert(fields, {
|
|
type = "description",
|
|
name = option.useDesc and option.desc or option.text or "",
|
|
half = customOptionHalf(option),
|
|
})
|
|
elseif option.type == "header" then
|
|
table.insert(fields, {
|
|
type = "header",
|
|
name = option.useName and option.name or option.text or "",
|
|
half = customOptionHalf(option),
|
|
})
|
|
else
|
|
table.insert(fields, {
|
|
type = "space", half = customOptionHalf(option),
|
|
useHeight = option.useHeight, height = option.height,
|
|
})
|
|
end
|
|
elseif optionClass == "group" and option.groupType == "simple" then
|
|
local collapseKey = "config:" .. optionPathString(optionPath)
|
|
local defaultCollapsed = option.collapse
|
|
if defaultCollapsed == nil then defaultCollapsed = true end
|
|
local collapsed = optionCollapsed(option, data, optionPath, defaultCollapsed)
|
|
if option.useCollapse then
|
|
table.insert(fields, {
|
|
type = "header", name = option.name or option.key or "Group",
|
|
collapsed = collapsed, configPath = optionPathString(optionPath),
|
|
onToggle = function()
|
|
setOptionCollapsed(option, data, optionPath, not collapsed)
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end,
|
|
})
|
|
annotateFields(fields, start, indent, "configPath", optionPathString(optionPath))
|
|
end
|
|
if not collapsed or not option.useCollapse then
|
|
local childConfig = optionSubConfig(option, config)
|
|
userOptionFields(fields, data, option.subOptions, childConfig, optionPath, indent + 1)
|
|
end
|
|
elseif optionClass == "group" and option.groupType == "array" then
|
|
local collapseKey = "config:" .. optionPathString(optionPath)
|
|
local defaultCollapsed = option.collapse
|
|
if defaultCollapsed == nil then defaultCollapsed = true end
|
|
local collapsed = optionCollapsed(option, data, optionPath, defaultCollapsed)
|
|
if option.useCollapse then
|
|
table.insert(fields, {
|
|
type = "header", name = option.name or option.key or "Group",
|
|
collapsed = collapsed, configPath = optionPathString(optionPath),
|
|
onToggle = function()
|
|
setOptionCollapsed(option, data, optionPath, not collapsed)
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end,
|
|
})
|
|
annotateFields(fields, start, indent, "configPath", optionPathString(optionPath))
|
|
end
|
|
if not collapsed or not option.useCollapse then
|
|
arrayGroupFields(fields, data, option, config, optionPath, indent)
|
|
end
|
|
elseif optionClass == "group" or not optionClass then
|
|
placeholderField(fields, option or {})
|
|
end
|
|
if not (optionClass == "group" and (option.groupType == "simple" or option.groupType == "array")) then
|
|
annotateFields(fields, start, indent, "configPath", optionPathString(optionPath))
|
|
end
|
|
end
|
|
end
|
|
|
|
local function authorOptionBlock(fields, data, options, index, option, path, indent)
|
|
local optionPath = copyOptionPath(path, index)
|
|
local pathKey = optionPathString(optionPath)
|
|
local collapsed = WA.OptionsState and WA.OptionsState.isCollapsed(
|
|
data, "author:" .. pathKey, true) or false
|
|
local start = #fields
|
|
local title = option.name or option.text or option.key or "Option " .. index
|
|
table.insert(fields, authorField({
|
|
type = "header", name = title, collapsed = collapsed,
|
|
actions = authorOptionActions(data, options, index, option),
|
|
onToggle = function()
|
|
if WA.OptionsState then
|
|
WA.OptionsState.setCollapsed(data, "author:" .. pathKey, not collapsed)
|
|
end
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end,
|
|
}, index, "header"))
|
|
annotateFields(fields, start, indent, "authorPath", pathKey)
|
|
if not collapsed then
|
|
local bodyStart = #fields
|
|
authorOptionFields(fields, data, option, index, options)
|
|
annotateFields(fields, bodyStart, indent, "authorPath", pathKey)
|
|
if WA.author_option_classes[option.type] == "group" then
|
|
for childIndex = 1, #(option.subOptions or {}) do
|
|
authorOptionBlock(fields, data, option.subOptions, childIndex,
|
|
option.subOptions[childIndex], optionPath, indent + 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function authorModeFields(fields, data)
|
|
data.authorOptions = data.authorOptions or {}
|
|
for index = 1, #data.authorOptions do
|
|
authorOptionBlock(fields, data, data.authorOptions, index,
|
|
data.authorOptions[index], {}, 0)
|
|
end
|
|
|
|
table.insert(fields, {type = "header", name = ""})
|
|
table.insert(fields, {
|
|
type = "button", name = "Add Option", authorAction = "add",
|
|
onClick = function()
|
|
local index = #data.authorOptions + 1
|
|
local option = {
|
|
type = "toggle", name = "Option " .. index,
|
|
key = ensureUniqueAuthorKey("option" .. index, "", data.authorOptions, 1),
|
|
default = false, width = 1, useDesc = false,
|
|
}
|
|
table.insert(data.authorOptions, option)
|
|
authorOptionChanged(data)
|
|
end,
|
|
})
|
|
table.insert(fields, {
|
|
type = "button", name = "Enter User Mode", authorAction = "enterUser",
|
|
onClick = function()
|
|
data.authorMode = false
|
|
authorOptionChanged(data)
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function groupLeafData(data)
|
|
local leaves = {}
|
|
if WA.OptionsState and WA.OptionsState.leafDescendants then
|
|
local ids = WA.OptionsState.leafDescendants(data.id)
|
|
for i = 1, #ids do
|
|
local child = WeakestAurasDB.displays[ids[i]]
|
|
if child then table.insert(leaves, child) end
|
|
end
|
|
end
|
|
return leaves
|
|
end
|
|
|
|
-- The merged user-mode option list for a member set -- a group's leaves or a
|
|
-- multi-selection, the list being the only difference.
|
|
local function mergedMemberOptions(members)
|
|
local merged = {}
|
|
for i = 1, #members do
|
|
local child = members[i]
|
|
mergeOptions(merged, child, child.authorOptions or {}, child.config or {}, {}, nil)
|
|
end
|
|
return merged
|
|
end
|
|
|
|
local function userModeFields(fields, data, options, members)
|
|
local merged = options ~= nil
|
|
options = options or data.authorOptions or {}
|
|
userOptionFields(fields, data, options, data.config or {}, {}, 0)
|
|
table.insert(fields, {type = "header", name = ""})
|
|
table.insert(fields, {
|
|
type = "button", name = "Reset to Defaults",
|
|
onClick = function()
|
|
if merged then
|
|
for i = 1, #members do
|
|
local child = members[i]
|
|
child.config = {}
|
|
WA.ValidateUserConfig(child)
|
|
WA.Add(child, true)
|
|
WA.RefreshAuraEnvConfig(child.id)
|
|
end
|
|
else
|
|
data.config = {}
|
|
WA.ValidateUserConfig(data)
|
|
WA.Add(data, true)
|
|
WA.RefreshAuraEnvConfig(data.id)
|
|
end
|
|
if WA.RefreshOptions then WA.RefreshOptions() end
|
|
end,
|
|
})
|
|
if not merged then
|
|
table.insert(fields, {
|
|
type = "button", name = "Enter Author Mode", authorAction = "enterAuthor",
|
|
onClick = function()
|
|
data.authorMode = true
|
|
authorOptionChanged(data)
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
|
|
function WA.CustomOptionsFields(fields, data)
|
|
if not data then return end
|
|
if WA.IsGroup(data) then
|
|
local members = groupLeafData(data)
|
|
userModeFields(fields, data, mergedMemberOptions(members), members)
|
|
elseif data.authorMode then
|
|
authorModeFields(fields, data)
|
|
else
|
|
userModeFields(fields, data)
|
|
end
|
|
end
|
|
|
|
-- The merged Custom Options pane for a multi-selection: the same member
|
|
-- merge the group tab runs, fed the selection instead of a group's leaves.
|
|
-- User mode only, as the group pane is -- Author Mode stays a single-aura
|
|
-- affair.
|
|
function WA.CustomOptionsFieldsMerged(fields, ids)
|
|
local members = {}
|
|
for i = 1, #ids do
|
|
local data = WeakestAurasDB.displays[ids[i]]
|
|
if data then table.insert(members, data) end
|
|
end
|
|
if #members == 0 then return end
|
|
userModeFields(fields, members[1], mergedMemberOptions(members), members)
|
|
end
|