-- Copyright © 2024 @Groick (Twitch). All rights reserved.
local function L(key, ...) return EasyOptions:Localize(key, ...) end

function EasyOptions:HasOptionItem(addonName, optionName) -- hasOptionItem, optionItem
  local optionItem = EasyOptions:GetOptionItem(addonName, optionName)
  return (optionItem ~= nil), optionItem
end
function EasyOptions:GetOptionItem(addonName, optionName)
  if (addonName == nil) or (optionName == nil) then
    return nil
  end
  if (not EasyOptions:IsManagedAddon(addonName)) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to get an option item for the addon \"{1}\" because that addon is not managed by EasyOptions.", EasyOptions:FormatName(addonName)))
    return nil
  end
  local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  return addonInfo.OptionItemsDictionary[optionName]
end
function EasyOptions:GetOptionValue(addonName, optionName) -- value, isValid
  -- See also EasyOptions:GetOptionValueOrDefaultValue.

  if (addonName == nil) or (optionName == nil) then
    return nil, false
  end
  if (not EasyOptions:IsManagedAddon(addonName)) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to get an option value for the addon \"{1}\" because that addon is not managed by EasyOptions.", EasyOptions:FormatName(addonName)))
    return nil, false
  end
  local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  return addonInfo.AddonDB[optionName], true
end
function EasyOptions:GetOptionDefaultValue(addonName, optionName) -- value, isValid
  local optionItem = EasyOptions:GetOptionItem(addonName, optionName)
  if (optionItem == nil) then
    return nil, false
  end
  return optionItem.DefaultValue, true
end
function EasyOptions:SetOptionValue(addonName, optionName, value, allowUpdateControl)
  if (addonName == nil) or (optionName == nil) then
    return
  end
  if (not EasyOptions:IsManagedAddon(addonName)) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to set the value of the option item \"{1}\" in the addon \"{2}\" because that addon is not managed by EasyOptions.", EasyOptions:FormatName(optionName), EasyOptions:FormatName(addonName)))
    return
  end

  local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  local previousValue = addonInfo.AddonDB[optionName]
  -- EasyOptions:PrintItems({"EasyOptions:SetOptionValue", optionName, EasyOptions:Format(value), EasyOptions:Format(previousValue)})
  addonInfo.AddonDB[optionName] = value -- Always set the database value to the specified value (even if the two values are considered equal) in case the specified value is a different instance than the current value.
  if (not EasyOptions:AreEqual(value, previousValue, false)) then -- Pass false to the allowAreApproximatelyEqual parameter when determining whether the ValueChanged event should be raised.
    EasyOptions:HandleOptionValueChanged(addonName, optionName)
    if (EasyOptionsDB["EnableOptionChangedMessages"]) and (optionName ~= nil) and (optionName ~= "") then
      EasyOptions:PrintMessage(L("{1} set to {2}.", EasyOptions:FormatName(optionName), addonInfo.AddonDB[optionName]))
    end
  end

  if (allowUpdateControl) then
    EasyOptions:UpdateControlFromOptionValue(addonName, optionName)
  end
end
function EasyOptions:SetOptionValueToDefaultValue(addonName, optionName, allowUpdateControl)
  -- NOTE: The following have similar functionality:
  --  EasyOptions:ResetOptionValue
  --  EasyOptions:SetOptionValueToDefaultValue
  --  EasyOptions:SetOptionValueToDefaultValueIfCurrentValueEquals

  local defaultValue, isValid = EasyOptions:GetOptionDefaultValue(addonName, optionName)
  if (not isValid) then
    return
  end
  EasyOptions:SetOptionValue(addonName, optionName, defaultValue, allowUpdateControl)
end
function EasyOptions:SetOptionValueToDefaultValueIfCurrentValueEquals(addonName, optionName, value, allowAreApproximatelyEqual, allowUpdateControl)
  -- NOTE: The following have similar functionality:
  --  EasyOptions:ResetOptionValue
  --  EasyOptions:SetOptionValueToDefaultValue
  --  EasyOptions:SetOptionValueToDefaultValueIfCurrentValueEquals

  local currentValue = EasyOptions:GetOptionValue(addonName, optionName)
  if (not EasyOptions:AreEqual(currentValue, value, allowAreApproximatelyEqual)) then
    return
  end

  local defaultValue, isValid = EasyOptions:GetOptionDefaultValue(addonName, optionName)
  if (not isValid) then
    return
  end
  EasyOptions:SetOptionValue(addonName, optionName, defaultValue, allowUpdateControl)
end
function EasyOptions:SetOptionValuesToDefaultValueIfCurrentValueEquals(addonName, optionInfos, value, allowAreApproximatelyEqual, allowUpdateControls) -- optionInfos[i] = {optionName, requireIsHidden}
  local length = EasyOptions:GetLength(optionInfos)
  if (length == 0) then
    return
  end
  local optionItem
  local optionName
  local requireIsHidden
  local allowUpdateControl = allowUpdateControls
  for i = 1, length do
    optionName = optionInfos[i][1] -- optionInfos[i] = {optionName, requireIsHidden}
    requireIsHidden = optionInfos[i][2] -- optionInfos[i] = {optionName, requireIsHidden}
    optionItem = EasyOptions:GetOptionItem(addonName, optionName)
    if (optionItem ~= nil) and ((not requireIsHidden) or (optionItem.Type == "Hidden")) then
      EasyOptions:SetOptionValueToDefaultValueIfCurrentValueEquals(addonName, optionName, value, allowAreApproximatelyEqual, allowUpdateControl)
    end
  end
end
function EasyOptions:SetOptionValuesToDefaultValuesByName(addonName, optionInfos, allowUpdateControls) -- optionInfos[i] = {optionName, requireIsHidden}
  local length = EasyOptions:GetLength(optionInfos)
  if (length == 0) then
    return
  end
  local optionItem
  local optionName
  local requireIsHidden
  local allowUpdateControl = allowUpdateControls
  for i = 1, length do
    optionName = optionInfos[i][1] -- optionInfos[i] = {optionName, requireIsHidden}
    requireIsHidden = optionInfos[i][2] -- optionInfos[i] = {optionName, requireIsHidden}
    optionItem = EasyOptions:GetOptionItem(addonName, optionName)
    if (optionItem ~= nil) and ((not requireIsHidden) or (optionItem.Type == "Hidden")) then
      EasyOptions:SetOptionValueToDefaultValue(addonName, optionName, allowUpdateControl)
    end
  end
end
function EasyOptions:SetOptionValueFromControlInfo(control, value, allowUpdateControl)
  if (control == nil) then
    return
  end
  if (control.AllowSetOptionValue == false) then -- IMPORTANT: We require control.AllowSetOptionValue to be false (rather than false or nil) because we want to assume that setting the OptionValue is allowed if control.AllowSetOptionValue is nil.
    return
  end

  local optionItem = control.OptionItem
  if (optionItem == nil) or (optionItem.AddonName == nil) or (optionItem.Name == nil) then
    return
  end
  
  local addonName = optionItem.AddonName
  local optionName = optionItem.Name
  EasyOptions:SetOptionValue(addonName, optionName, value, allowUpdateControl)
end
function EasyOptions:GetControl(addonName, optionName)
  local optionItem = EasyOptions:GetOptionItem(addonName, optionName)
  if (optionItem == nil) then
    return nil
  end
  return optionItem.Control
end
function EasyOptions:UpdateControlFromOptionValue(addonName, optionName)
  local optionItem = EasyOptions:GetOptionItem(addonName, optionName)
  if (optionItem == nil) then
    return
  end

  local control = optionItem.Control
  if (control == nil) then
    return
  end
  if (control.SetValue == nil) then
    return
  end

  local value, isValid = EasyOptions:GetOptionValue(addonName, optionName)
  if (not isValid) then
    return
  end
  control:SetValue(value)
end

function EasyOptions:GetValueFromCustomValue(valueName, customValue, defaultValue, valueMaxAllowed)
  local value
  local valueNameLocal = valueName or "value"
  if (type(customValue) == "number") then
    value = math.min(customValue, valueMaxAllowed)
  else
    if (customValue ~= nil) then
      EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for a custom {1}. A custom {1} must be expressed as a number.", valueNameLocal))
    end
    value = math.min(defaultValue, valueMaxAllowed)
  end
  return value
end
function EasyOptions:GetSizeFromCustomSize(customSize, ignoreCustomHeight, defaultWidth, defaultHeight, widthMaxAllowed, heightMaxAllowed, allowFillWidthFlag, assumeSingleValueIsHeight) -- width, height, hasWidth, hasHeight, shouldFillWidth
  -- NOTE: The "ignoreCustomHeight" parameter effectively converts to "ignoreCustomWidth" if the assumeSingleValueIsHeight parameter is true.
  --
  local width
  local height
  local hasWidth
  local hasHeight
  local shouldFillWidth
  if (type(customSize) == "number") or (EasyOptions:IsFillWidthFlag(customSize, allowFillWidthFlag)) then
    if (not assumeSingleValueIsHeight) then
      width = math.min(EasyOptions:GetValueIfIsFillWidthFlag(customSize, defaultWidth), widthMaxAllowed)
      height = math.min(defaultHeight, heightMaxAllowed)
      hasWidth = true
      hasHeight = false
      shouldFillWidth = EasyOptions:IsFillWidthFlag(customSize, allowFillWidthFlag)
    else
      width = math.min(defaultWidth, widthMaxAllowed)
      height = math.min(EasyOptions:GetValueIfIsFillWidthFlag(customSize, defaultHeight), heightMaxAllowed)
      hasWidth = false
      hasHeight = true
      shouldFillWidth = false
    end
  elseif (type(customSize) == "table") and (EasyOptions:GetLength(customSize) == 1) and ((type(customSize[1]) == "number") or (EasyOptions:IsFillWidthFlag(customSize[1], allowFillWidthFlag))) then
    if (not assumeSingleValueIsHeight) then
      width = math.min(EasyOptions:GetValueIfIsFillWidthFlag(customSize[1], defaultWidth), widthMaxAllowed)
      height = math.min(defaultHeight, heightMaxAllowed)
      hasWidth = true
      hasHeight = false
      shouldFillWidth = EasyOptions:IsFillWidthFlag(customSize[1], allowFillWidthFlag)
    else
      width = math.min(defaultWidth, widthMaxAllowed)
      height = math.min(EasyOptions:GetValueIfIsFillWidthFlag(customSize[1], defaultHeight), heightMaxAllowed)
      hasWidth = false
      hasHeight = true
      shouldFillWidth = false
    end
  elseif (type(customSize) == "table") and (EasyOptions:GetLength(customSize) == 2) and ((type(customSize[1]) == "number") or (EasyOptions:IsFillWidthFlag(customSize[1], allowFillWidthFlag))) and (type(customSize[2]) == "number") then
    if (not assumeSingleValueIsHeight) then
      width = math.min(EasyOptions:GetValueIfIsFillWidthFlag(customSize[1], defaultWidth), widthMaxAllowed)
      hasWidth = true
      if (ignoreCustomHeight) then
        height = math.min(defaultHeight, heightMaxAllowed)
        hasHeight = false
      else
        height = math.min(customSize[2], heightMaxAllowed)
        hasHeight = true
      end
      shouldFillWidth = EasyOptions:IsFillWidthFlag(customSize[1], allowFillWidthFlag)
    else
      height = math.min(customSize[2], heightMaxAllowed)
      hasHeight = true
      if (ignoreCustomHeight) then -- NOTE: The "ignoreCustomHeight" parameter effectively converts to "ignoreCustomWidth" if the assumeSingleValueIsHeight parameter is true.
        width = math.min(defaultWidth, widthMaxAllowed)
        hasWidth = false
        shouldFillWidth = false
      else
        width = math.min(EasyOptions:GetValueIfIsFillWidthFlag(customSize[1], defaultWidth), widthMaxAllowed)
        hasWidth = true
        shouldFillWidth = EasyOptions:IsFillWidthFlag(customSize[1], allowFillWidthFlag)
      end
    end
  else
    if (customSize ~= nil) then
      if (not assumeSingleValueIsHeight) then
        EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its custom size. Custom sizes must be in one of the following formats: width, {width}, or {width, height}."))
      else
        EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its custom size. Custom sizes must be in one of the following formats: height, {height}, or {width, height}."))
      end
    end
    width = math.min(defaultWidth, widthMaxAllowed)
    height = math.min(defaultHeight, heightMaxAllowed)
    hasWidth = false
    hasHeight = false
    shouldFillWidth = false
  end
  return width, height, hasWidth, hasHeight, shouldFillWidth
end
function EasyOptions:GetSizeInfoFromCustomSize(size, ignoreCustomHeight, allowFillWidthFlag, assumeSingleValueIsHeight) -- hasWidth, hasHeight, shouldFillWidth
  -- NOTE: All parameters that are passed to this method may be nil.
  local customSize = size
  local ignoreCustomHeightLocal = EasyOptions:IIF(ignoreCustomHeight ~= nil, ignoreCustomHeight, false)
  local defaultWidth = 1 -- This value is arbitrary.
  local defaultHeight = 1 -- This value is arbitrary.
  local widthMaxAllowed = EasyOptions.Fields.ControlSizeMaxAllowed_Large -- This value is arbitrary.
  local heightMaxAllowed = EasyOptions.Fields.ControlSizeMaxAllowed_Large -- This value is arbitrary.
  local allowFillWidthFlagLocal = EasyOptions:IIF(allowFillWidthFlag ~= nil, allowFillWidthFlag, true)
  local assumeSingleValueIsHeightLocal = EasyOptions:IIF(assumeSingleValueIsHeight ~= nil, assumeSingleValueIsHeight, false)
  local width, height, hasWidth, hasHeight, shouldFillWidth = EasyOptions:GetSizeFromCustomSize(customSize, ignoreCustomHeightLocal, defaultWidth, defaultHeight, widthMaxAllowed, heightMaxAllowed, allowFillWidthFlagLocal, assumeSingleValueIsHeightLocal)
  return hasWidth, hasHeight, shouldFillWidth
end
function EasyOptions:GetColorFromCustomColor(customColor, hasAlpha)
  local red, green, blue, alpha = 1.0, 1.0, 1.0, 1.0
  if (customColor == nil) then
    -- Do nothing so we return the default values.
  elseif (type(customColor) == "table") and (EasyOptions:GetLength(customColor) == 3) and (type(customColor[1]) == "number") and (type(customColor[2]) == "number") and (type(customColor[3]) == "number") then -- e.g., {1.0, 1.0, 1.0}
    red = customColor[1]
    green = customColor[2]
    blue = customColor[3]
    alpha = 1.0
  elseif (type(customColor) == "table") and (EasyOptions:GetLength(customColor) == 4) and (type(customColor[1]) == "number") and (type(customColor[2]) == "number") and (type(customColor[3]) == "number") and ((customColor[4] == nil) or (type(customColor[4]) == "number")) then -- e.g., {1.0, 1.0, 1.0, 1.0}
    red = customColor[1]
    green = customColor[2]
    blue = customColor[3]
    alpha = EasyOptions:IIF(hasAlpha and (customColor[4] ~= nil), customColor[4], 1.0)
  else
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its color information. Color information must use one of the following formats: {red, green, blue} or {red, green, blue, alpha}."))
  end
  return red, green, blue, alpha
end
function EasyOptions:GetFontFromCustomFontInfo(customFontInfo, defaultFontProvider)
  local fontFilePath, fontSize, fontFlags = defaultFontProvider:GetFont()
  if (customFontInfo == nil) then
    -- Do nothing so we return the default values.
  elseif (type(customFontInfo) == "number") then -- e.g., 16
    fontSize = customFontInfo
  elseif (type(customFontInfo) == "table") and (EasyOptions:GetLength(customFontInfo) == 1) and (type(customFontInfo[1]) == "number") then -- e.g., {16}
    fontSize = customFontInfo[1]
  elseif (type(customFontInfo) == "table") and (EasyOptions:GetLength(customFontInfo) == 2) and (type(customFontInfo[1]) == "string") and (type(customFontInfo[2]) == "number") then -- e.g., {"Fonts\\FRIZQT__.TTF" or "GameFontNormal", 16}
    fontFilePath = customFontInfo[1]
    fontSize = customFontInfo[2]
  elseif (type(customFontInfo) == "table") and (EasyOptions:GetLength(customFontInfo) == 2) and (type(customFontInfo[1]) == "number") and (type(customFontInfo[2]) == "string") then -- e.g., {16, "OUTLINE, THICKOUTLINE, MONOCHROME"}
    fontSize = customFontInfo[2]
    fontFlags = customFontInfo[3]
  elseif (type(customFontInfo) == "table") and (EasyOptions:GetLength(customFontInfo) == 3) and (type(customFontInfo[1]) == "string") and (type(customFontInfo[2]) == "number") and (type(customFontInfo[3]) == "string") then -- e.g., {"Fonts\\FRIZQT__.TTF" or "GameFontNormal", 16, "OUTLINE, THICKOUTLINE, MONOCHROME"}
    fontFilePath = customFontInfo[1]
    fontSize = customFontInfo[2]
    fontFlags = customFontInfo[3]
  elseif (type(customFontInfo) == "string") then -- e.g., "Fonts\\FRIZQT__.TTF" or "GameFontNormal"
    fontFilePath = customFontInfo
  elseif (type(customFontInfo) == "table") and (EasyOptions:GetLength(customFontInfo) == 1) and (type(customFontInfo[1]) == "string") then -- e.g., {"Fonts\\FRIZQT__.TTF" or "GameFontNormal"}
    fontFilePath = customFontInfo[1]
  else
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its font information. Font information must be in one of the following formats: fontSize, fontObjectName, {fontSize}, {fontObjectName}, {fontFilePath, fontSize}, {fontSize, fontFlags}, or {fontFilePath, fontSize, fontFlags}."))
  end
  --
  fontSize = math.floor(fontSize + 0.5) -- Use math.floor to round to the nearest integer because the returned value from the WoW API can have rounding errors.
  --
  local isFontObjectName = EasyOptions:IsFontObjectName(fontFilePath)
  return fontFilePath, fontSize, fontFlags, isFontObjectName
end
function EasyOptions:SetFontFromCustomFontInfo(label, customFontInfo, customTextColor)
  -- See also EasyOptions:SetFont.
  if (customFontInfo ~= nil) then
    local fontFilePath, fontSize, fontFlags, isFontObjectName = EasyOptions:GetFontFromCustomFontInfo(customFontInfo, label)
    if (isFontObjectName) then
      label:SetFontObject(fontFilePath)
    else
      local fontFilePathCurrent, fontSizeCurrent, fontFlagsCurrent = label:GetFont()
      if (fontFilePathCurrent ~= fontFilePath) then -- This is for optimization.
        if (not EasyOptions:IsInGameFont(fontFilePath)) then -- HACK: The following logic was implemented because FontString:SetFont(fontFilePath, fontSize, fontFlags) fails if fontFilePath corresponds to a custom font, but it was found that implementing the following logic acts as a workaround for the issue.
          label:SetFont(fontFilePath, fontSize, fontFlags)
          label:SetFont("Fonts\\ARIALN.TTF", fontSize, fontFlags)
          label:SetFont("Fonts\\Frizqt__.TTF", fontSize, fontFlags)
        end
      end
      local success = label:SetFont(fontFilePath, fontSize, fontFlags)
      -- EasyOptions:Print({"SetFont", fontFilePath, success, "End"})
    end
  end
  if (customTextColor ~= nil) then
    local hasAlpha = (customTextColor[4] ~= nil)
    local red, green, blue, alpha = EasyOptions:GetColorFromCustomColor(customTextColor, hasAlpha)
    label:SetTextColor(red, green, blue, alpha)
  end
  EasyOptions:SetDefaultTextColor(label, false) -- Set the current text color as the default text color.
end
function EasyOptions:IsFontObjectName(fontFilePath)
  if (fontFilePath == nil) then
    return false
  end
  return (not string.find(fontFilePath, "\\")) and (not string.find(fontFilePath, "/")) -- e.g., "GameFontNormal" vs. "Fonts\\FRIZQT__.TTF"
end
function EasyOptions:IsInGameFont(fontFilePath)
  -- Description: Indicates whether the specified file path corresponds to an in-game font (e.g., "Fonts\\ARIALN.TTF").
  if (fontFilePath == nil) then
    return false
  end
  local indexOf
  indexOf = string.find(fontFilePath, "Fonts\\")
  if (indexOf == 1) then
    return true
  end
  indexOf = string.find(fontFilePath, "Fonts/")
  if (indexOf == 1) then
    return true
  end
  return false
end
function EasyOptions:CreateFontString(controlName)
  local control = CreateFrame("Frame", controlName, nil)
  local fontObjectName = EasyOptions.Fields.DefaultLabelFontObjectName or "GameFontHighlight" -- GameFontNormal, GameFontNormalMed1, GameFontNormalMed2, GameFontNormalSmall, GameFontHighlightSmall. See also "https://wowpedia.fandom.com/wiki/API_FontInstance_SetFontObject".
  local label = control:CreateFontString(controlName..".Label", "ARTWORK", fontObjectName)
  return label
end
function EasyOptions:SetDefaultTextColor(control, forceUpdate)
  -- NOTE: control will typically be a Label or a TextBox.
  if (control.DefaultTextColor ~= nil) and (not forceUpdate) then
    return
  end

  local red, green, blue, alpha
  if (control.GetTextColor ~= nil) then
    red, green, blue, alpha = control:GetTextColor()
  else
    red, green, blue, alpha = 1.0, 1.0, 1.0, 1.0
  end
  control.DefaultTextColor = {red, green, blue, alpha}
end
function EasyOptions:SetToDefaultTextColor(control)
  if (control == nil) or (control.DefaultTextColor == nil) then
    return
  end
  if (control.SetTextColor ~= nil) then
    local color = control.DefaultTextColor -- {red, green, blue, alpha}
    control:SetTextColor(color[1], color[2], color[3], color[4])
    -- control:SetFontObject(GameFontHighlightLeft)
  end
end
function EasyOptions:SetToDisabledTextColor(control)
  if (control == nil) then
    return
  end
  if (control.SetTextColor == nil) then
    return
  end
  EasyOptions:SetDefaultTextColor(control, false) -- Store the current text color as the default text color if the default text color has not already been set.
  local red, green, blue, alpha
  if (control.GetTextColor ~= nil) then
    red, green, blue, alpha = control:GetTextColor()
  else
    red, green, blue, alpha = 1.0, 1.0, 1.0, 1.0
  end
  if (control.SetTextColor ~= nil) then
    control:SetTextColor(0.5, 0.5, 0.5, alpha)
    -- control.Label:SetFontObject(GameFontNormalLeftGrey)
  end
end
function EasyOptions:GetColorForControlState(enabled, color)
  if (enabled == nil) or (enabled) then -- Assume that the control is enabled if the enabled parameter is nil.
    return color
  else
    return EasyOptions:DesaturateColor(color)
  end
end

function EasyOptions:GetOptionPositionInfo(optionItem)
  -- See also EasyOptions:GetPositionInfoFromOptionItem.
  local optionPositionInfo
  if (optionItem == nil) or (optionItem.ControlType == nil) or (optionItem.ControlType == "Hidden") then
    optionPositionInfo = nil
  else
    optionPositionInfo = optionItem.PositionInfo
  end
  return optionPositionInfo
end
function EasyOptions:GetIndentCountFromOptionPositionInfo(indentCountCurrent, optionPositionInfo, isBuildingRow, useIndentCountCurrentIfIsBuildingRow)
  if (isBuildingRow) and (useIndentCountCurrentIfIsBuildingRow) then
    return indentCountCurrent
  end
  if (optionPositionInfo ~= nil) and (type(optionPositionInfo) == "number") then
    return optionPositionInfo
  end
  return 0
end
function EasyOptions:GetIndentCountFromOptionItem(indentCountCurrent, optionItem, isBuildingRow, useIndentCountCurrentIfIsBuildingRow)
  local optionPositionInfo = EasyOptions:GetOptionPositionInfo(optionItem)
  return EasyOptions:GetIndentCountFromOptionPositionInfo(indentCountCurrent, optionPositionInfo, isBuildingRow, useIndentCountCurrentIfIsBuildingRow)
end
function EasyOptions:GetPositionInfoFromOptionPositionInfo(positionInfoCurrent, optionPositionInfo, isBuildingRow) -- {ChildAnchorPoint, ParentAnchorPoint, x, y}, shouldReplacePositionInfoCurrent, positionIncrementOffsetX, positionIncrementOffsetY, shouldIncrementPositionX, shouldIncrementPositionY
  local requireAnchorPoint = false
  return EasyOptions:GetPositionInfoFromOptionPositionInfo_Internal(positionInfoCurrent, optionPositionInfo, isBuildingRow, requireAnchorPoint)
end
function EasyOptions:GetPositionInfoFromOptionPositionInfo_Internal(positionInfoCurrent, optionPositionInfo, isBuildingRow, requireAnchorPoint) -- {ChildAnchorPoint, ParentAnchorPoint, x, y}, shouldReplacePositionInfoCurrent, positionIncrementOffsetX, positionIncrementOffsetY, shouldIncrementPositionX, shouldIncrementPositionY
  local positionInfo, shouldReplacePositionInfoCurrent, positionIncrementOffsetX, positionIncrementOffsetY, shouldIncrementPositionX, shouldIncrementPositionY
  if (optionPositionInfo == nil) and (not requireAnchorPoint) then
    positionIncrementOffsetX = 0
    positionIncrementOffsetY = 0
    positionInfo = positionInfoCurrent
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = false
    shouldIncrementPositionY = false
  elseif (type(optionPositionInfo) == "number") and (not requireAnchorPoint) then -- NOTE: A single number specifies the number of standard indents. e.g., nil, 0, 1, 2, etc.
    local indentCount = EasyOptions:IIF(optionPositionInfo ~= nil, optionPositionInfo, 0)
    positionIncrementOffsetX = indentCount * EasyOptions:GetDefaultIndentX()
    positionIncrementOffsetY = 0
    positionInfo = {positionInfoCurrent[1], positionInfoCurrent[2], positionInfoCurrent[3] + positionIncrementOffsetX, positionInfoCurrent[4]}
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = isBuildingRow
    shouldIncrementPositionY = (not isBuildingRow)
  elseif (type(optionPositionInfo) == "table") and (EasyOptions:GetLength(optionPositionInfo) == 1) and (type(optionPositionInfo[1]) == "number") and (not requireAnchorPoint) then -- e.g., {20}
    positionIncrementOffsetX = optionPositionInfo[1]
    positionIncrementOffsetY = 0
    positionInfo = {positionInfoCurrent[1], positionInfoCurrent[2], positionInfoCurrent[3] + positionIncrementOffsetX, positionInfoCurrent[4]}
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = isBuildingRow
    shouldIncrementPositionY = (not isBuildingRow)
  elseif (type(optionPositionInfo) == "table") and (EasyOptions:GetLength(optionPositionInfo) == 2) and (type(optionPositionInfo[1]) == "number") and (type(optionPositionInfo[2]) == "number") and (not requireAnchorPoint) then -- e.g., {20, 16}
    positionIncrementOffsetX = optionPositionInfo[1]
    positionIncrementOffsetY = -optionPositionInfo[2]
    positionInfo = {positionInfoCurrent[1], positionInfoCurrent[2], positionInfoCurrent[3] + positionIncrementOffsetX, positionInfoCurrent[4] + positionIncrementOffsetY}
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = isBuildingRow
    shouldIncrementPositionY = (not isBuildingRow)
  elseif (type(optionPositionInfo) == "table") and (EasyOptions:GetLength(optionPositionInfo) == 3) and (type(optionPositionInfo[1]) == "string") and (type(optionPositionInfo[2]) == "number") and (type(optionPositionInfo[3]) == "number") then -- e.g., {"TOPLEFT", 20, -100}
    positionIncrementOffsetX = 0
    positionIncrementOffsetY = 0
    positionInfo = {optionPositionInfo[1], optionPositionInfo[1], optionPositionInfo[2], optionPositionInfo[3]}
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = false
    shouldIncrementPositionY = false
  elseif (type(optionPositionInfo) == "table") and (EasyOptions:GetLength(optionPositionInfo) == 4) and (type(optionPositionInfo[1]) == "string") and (type(optionPositionInfo[2]) == "string") and (type(optionPositionInfo[3]) == "number") and (type(optionPositionInfo[4]) == "number") then -- e.g., {"TOPLEFT", "TOPLEFT", 20, -100}
    positionIncrementOffsetX = 0
    positionIncrementOffsetY = 0
    positionInfo = {optionPositionInfo[1], optionPositionInfo[2], optionPositionInfo[3], optionPositionInfo[4]}
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = false
    shouldIncrementPositionY = false
  else
    if (not requireAnchorPoint) then
      EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its position information. Position information must be specified at OptionItem[4] and must be in one of the following formats: indentCount, {positionIncrementOffsetX}, {positionIncrementOffsetX, positionIncrementOffsetY}, {anchorPoint, x, y}, or {childAnchorPoint, parentAnchorPoint, x, y}."))
      -- EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its position information. Position information must be specified at OptionItem[4] and must either be nil, a number, a table that contains two numbers (e.g., {5, -100}), a table that contains one string that specifies the reference point followed by two numbers (i.e., {\"TOPLEFT\", 5, -100}), or a table that contains two strings that specify the reference points followed by two numbers (i.e., {\"TOPLEFT\", \"TOPLEFT\", 5, -100})."))
    else
      EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for its position information. Position information must be in one of the following formats: {anchorPoint, x, y}, or {childAnchorPoint, parentAnchorPoint, x, y}."))
    end
    positionIncrementOffsetX = 0
    positionIncrementOffsetY = 0
    positionInfo = positionInfoCurrent
    shouldReplacePositionInfoCurrent = false
    shouldIncrementPositionX = isBuildingRow
    shouldIncrementPositionY = (not isBuildingRow)
  end
  return positionInfo, shouldReplacePositionInfoCurrent, positionIncrementOffsetX, positionIncrementOffsetY, shouldIncrementPositionX, shouldIncrementPositionY
end
function EasyOptions:GetPositionInfoFromOptionItem(positionInfoCurrent, optionItem, isBuildingRow) -- {ChildAnchorPoint, ParentAnchorPoint, x, y}, shouldReplacePositionInfoCurrent, offsetY, shouldIncrementY
  -- See also EasyOptions:GetOptionPositionInfo.
  local optionPositionInfo = EasyOptions:GetOptionPositionInfo(optionItem)
  return EasyOptions:GetPositionInfoFromOptionPositionInfo(positionInfoCurrent, optionPositionInfo, isBuildingRow)
end
function EasyOptions:GetFramePositionInfoFromOptionPositionInfo(optionPositionInfo) -- {ChildAnchorPoint, ParentAnchorPoint, x, y}
  local positionInfoDefault = {"CENTER", "CENTER", 0, 0} -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
  local isBuildingRow = false
  local requireAnchorPoint = true
  local positionInfo, shouldReplacePositionInfoCurrent, positionIncrementOffsetX, positionIncrementOffsetY, shouldIncrementPositionX, shouldIncrementPositionY = EasyOptions:GetPositionInfoFromOptionPositionInfo_Internal(positionInfoDefault, optionPositionInfo, isBuildingRow, requireAnchorPoint)
  return positionInfo
end

function EasyOptions:IsLeftAlignedPositionInfo(positionInfo)
  if (positionInfo == nil) then
    return false
  end
  if ((positionInfo[1] == "LEFT") or (positionInfo[1] == "TOPLEFT") or (positionInfo[1] == "BOTTOMLEFT")) and ((positionInfo[2] == "LEFT") or (positionInfo[2] == "TOPLEFT") or (positionInfo[2] == "BOTTOMLEFT")) then -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    return true
  else
    return false
  end
end
function EasyOptions:IsRightAlignedPositionInfo(positionInfo)
  if (positionInfo == nil) then
    return false
  end
  if ((positionInfo[1] == "RIGHT") or (positionInfo[1] == "TOPRIGHT") or (positionInfo[1] == "BOTTOMRIGHT")) and ((positionInfo[2] == "RIGHT") or (positionInfo[2] == "TOPRIGHT") or (positionInfo[2] == "BOTTOMRIGHT")) then -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    return true
  else
    return false
  end
end
function EasyOptions:IsTopAlignedPositionInfo(positionInfo)
  if (positionInfo == nil) then
    return false
  end
  if ((positionInfo[1] == "TOP") or (positionInfo[1] == "TOPLEFT") or (positionInfo[1] == "TOPRIGHT")) and ((positionInfo[2] == "TOP") or (positionInfo[2] == "TOPLEFT") or (positionInfo[2] == "TOPRIGHT")) then -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    return true
  else
    return false
  end
end
function EasyOptions:IsBottomAlignedPositionInfo(positionInfo)
  if (positionInfo == nil) then
    return false
  end
  if ((positionInfo[1] == "BOTTOM") or (positionInfo[1] == "BOTTOMLEFT") or (positionInfo[1] == "BOTTOMRIGHT")) and ((positionInfo[2] == "BOTTOM") or (positionInfo[2] == "BOTTOMLEFT") or (positionInfo[2] == "BOTTOMRIGHT")) then -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    return true
  else
    return false
  end
end
function EasyOptions:IsMiddleAlignedPositionInfo(positionInfo)
  if (positionInfo == nil) then
    return false
  end
  if ((positionInfo[1] == "TOP") or (positionInfo[1] == "BOTTOM") or (positionInfo[1] == "CENTER")) and ((positionInfo[2] == "TOP") or (positionInfo[2] == "BOTTOM") or (positionInfo[2] == "CENTER")) then -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    return true
  else
    return false
  end
end
function EasyOptions:IsLeftAlignedAnchorPoint(frame, anchorPointIndex)
  if (frame == nil) then
    return false
  end
  if (anchorPointIndex == nil) then
    return false
  end
  local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(anchorPointIndex)
  local positionInfo = {point, relativePoint, offsetX, offsetY}
  return EasyOptions:IsLeftAlignedPositionInfo(positionInfo)
end
function EasyOptions:IsRightAlignedAnchorPoint(frame, anchorPointIndex)
  if (frame == nil) then
    return false
  end
  if (anchorPointIndex == nil) then
    return false
  end
  local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(anchorPointIndex)
  local positionInfo = {point, relativePoint, offsetX, offsetY}
  return EasyOptions:IsRightAlignedPositionInfo(positionInfo)
end
function EasyOptions:IsTopAlignedAnchorPoint(frame, anchorPointIndex)
  if (frame == nil) then
    return false
  end
  if (anchorPointIndex == nil) then
    return false
  end
  local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(anchorPointIndex)
  local positionInfo = {point, relativePoint, offsetX, offsetY}
  return EasyOptions:IsTopAlignedPositionInfo(positionInfo)
end
function EasyOptions:IsBottomAlignedAnchorPoint(frame, anchorPointIndex)
  if (frame == nil) then
    return false
  end
  if (anchorPointIndex == nil) then
    return false
  end
  local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(anchorPointIndex)
  local positionInfo = {point, relativePoint, offsetX, offsetY}
  return EasyOptions:IsBottomAlignedPositionInfo(positionInfo)
end
function EasyOptions:IsMiddleAlignedAnchorPoint(frame, anchorPointIndex)
  if (frame == nil) then
    return false
  end
  if (anchorPointIndex == nil) then
    return false
  end
  local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(anchorPointIndex)
  local positionInfo = {point, relativePoint, offsetX, offsetY}
  return EasyOptions:IsMiddleAlignedPositionInfo(positionInfo)
end
function EasyOptions:MoveFrame(frame, deltaLeft, deltaRight, deltaTop, deltaBottom)
  if (frame == nil) then
    return
  end
  local length = frame:GetNumPoints()
  local point, relativeTo, relativePoint, offsetX, offsetY
  local deltaX
  local deltaY
  for i = 1, length do
    point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(i)
    if (point == "LEFT") then
      deltaX = deltaLeft
      deltaY = 0
    elseif (point == "RIGHT") then
      deltaX = deltaRight
      deltaY = 0
    elseif (point == "TOP") then
      deltaX = 0
      deltaY = deltaTop
    elseif (point == "BOTTOM") then
      deltaX = 0
      deltaY = deltaBottom
    elseif (point == "TOPLEFT") then
      deltaX = deltaLeft
      deltaY = deltaTop
    elseif (point == "TOPRIGHT") then
      deltaX = deltaRight
      deltaY = deltaTop
    elseif (point == "BOTTOMLEFT") then
      deltaX = deltaLeft
      deltaY = deltaBottom
    elseif (point == "BOTTOMRIGHT") then
      deltaX = deltaRight
      deltaY = deltaBottom
    elseif (point == "CENTER") then
      -- Reserved.
    else
      deltaX = 0
      deltaY = 0
      EasyOptions:PrintErrorMessageOnce(L("Unable to move the frame's anchor point because \"{1}\" is an unknown anchor point name.", point))
    end
    EasyOptions:MoveFrameByAnchorPointName(frame, point, deltaX, deltaY)
  end
end
function EasyOptions:MoveFrameByAnchorPointName(frame, anchorPointName, deltaX, deltaY)
  if (frame == nil) then
    return
  end
  if (anchorPointName == nil) or (anchorPointName == "") then
    return
  end
  if (deltaX == 0) and (deltaY == 0) then
    return
  end
  local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPointByName(anchorPointName) -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
  if (anchorPointName == "LEFT") then
    offsetX = offsetX + deltaX
  elseif (anchorPointName == "RIGHT") then
    offsetX = offsetX + deltaX
  elseif (anchorPointName == "TOP") then
    offsetY = offsetY + deltaY
  elseif (anchorPointName == "BOTTOM") then
    offsetY = offsetY + deltaY
  elseif (anchorPointName == "TOPLEFT") then
    offsetX = offsetX + deltaX
    offsetY = offsetY + deltaY
  elseif (anchorPointName == "TOPRIGHT") then
    offsetX = offsetX + deltaX
    offsetY = offsetY + deltaY
  elseif (anchorPointName == "BOTTOMLEFT") then
    offsetX = offsetX + deltaX
    offsetY = offsetY + deltaY
  elseif (anchorPointName == "BOTTOMRIGHT") then
    offsetX = offsetX + deltaX
    offsetY = offsetY + deltaY
  elseif (anchorPointName == "CENTER") then
    -- Reserved.
  else
    EasyOptions:PrintErrorMessageOnce(L("Unable to move the frame because \"{1}\" is an unknown anchor point name.", anchorPointName))
    return
  end
  frame:ClearPoint(point)
  frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
end

function EasyOptions:SetTooltip(control, owner, tooltipTitleText, tooltipText, tooltipTitleColor, tooltipColor, textWrap)
  -- IMPORTANT: Do not exit this method if tooltipText is nil because we always want to add the tooltip framework to the control if this method is called so the control's tooltip can be subsequently modified by setting its control.TooltipInfo.Text property.

  local isMinimapButton = owner.IsMinimapButton
  -- local isMinimapDataObject = control.IsMinimapDataObject

  local tooltipTitleColorLocal
  if (tooltipTitleColor == nil) then
    tooltipTitleColorLocal = {}
  else
    tooltipTitleColorLocal = tooltipTitleColor
  end
  
  local tooltipColorLocal
  if (tooltipColor == nil) then
    tooltipColorLocal = {}
  else
    tooltipColorLocal = tooltipColor
  end

  control.TooltipInfo = {}
  control.TooltipInfo.Owner = owner or control
  control.TooltipInfo.TitleText = tooltipTitleText
  control.TooltipInfo.Text = tooltipText
  control.TooltipInfo.TitleR = tooltipTitleColorLocal[1]
  control.TooltipInfo.TitleG = tooltipTitleColorLocal[2]
  control.TooltipInfo.TitleB = tooltipTitleColorLocal[3]
  control.TooltipInfo.TitleA = tooltipTitleColorLocal[4]
  control.TooltipInfo.R = tooltipColorLocal[1]
  control.TooltipInfo.G = tooltipColorLocal[2]
  control.TooltipInfo.B = tooltipColorLocal[3]
  control.TooltipInfo.A = tooltipColorLocal[4]
  control.TooltipInfo.TextWrap = (textWrap ~= false) -- Set the TextWrap flag to true by default.
  control.TooltipInfo.IsMinimapButton = isMinimapButton

  control.OnEnterTooltipFunction = function(self)
    local control = self
    local info = control.TooltipInfo
    EasyOptions.Fields.TooltipOwner = control
    GameTooltip:ClearLines()
    GameTooltip:Hide()
    if (info == nil) then
      return
    end
    if (not EasyOptions:ShouldEnableTooltip(info.Owner, info.IsMinimapButton)) then
      return
    end
    
    local infoTitleText = info.TitleText or ""
    local infoText = info.Text or ""
    if (control.HasRightButtonDownHandlerForOptionControl) and (EasyOptions:CanResetOptionValue(control.OptionItem)) and (EasyOptionsDB["AutoIncludeRightClickToResetMessageInTooltips"]) then
      if (infoText ~= "") then
        infoText = infoText.."\n\n"
      end
      infoText = infoText..L("Right click to reset this item to its default value.")
    end

    local hasTitleText = (infoTitleText ~= "") and (EasyOptionsDB["ShowTooltipTitle"])
    local hasText = (infoText ~= "")
    local anchorPosition = EasyOptions:IIF(info.IsMinimapButton, EasyOptionsDB["MinimapButtonTooltipAnchorPosition"], EasyOptionsDB["TooltipAnchorPosition"])
    local offsetX = EasyOptions:IIF(info.IsMinimapButton, EasyOptionsDB["MinimapButtonTooltipPositionOffsetX"], EasyOptionsDB["TooltipPositionOffsetX"])
    local offsetY = EasyOptions:IIF(info.IsMinimapButton, EasyOptionsDB["MinimapButtonTooltipPositionOffsetY"], EasyOptionsDB["TooltipPositionOffsetY"])
    local r, g, b, a
    local tooltipTitleColor, tooltipTextColor = EasyOptions:GetTooltipTextColors()
    if (hasText) then
      -- Set the owner of GameTooltip.
      GameTooltip:SetOwner(info.Owner, anchorPosition, offsetX, offsetY)
      --
      -- Determine the width that will fit the tooltip frame to the displayed text.
      --  IMPORTANT: This is required for the following reasons:
      --    The textWrap parameter that is passed to GameTooltip:AddLine seems to be ignored.
      --    This technique is required to prevent the tooltip frame from being very wide if displaying a long single line of text.
      local titleWidthRequired = 0.0
      local textWidthRequired = 0.0
      if (hasTitleText) then -- NOTE: By default, the first line is a larger font (i.e., GameTooltipHeaderText) than any subsequent lines (i.e., GameTooltipText).
        GameTooltip:ClearLines()
        r, g, b, a = 1.0, 1.0, 1.0, 1.0 -- The colors are arbitrary.
        GameTooltip:SetText(infoTitleText, r, g, b, a, false)
        titleWidthRequired = GameTooltip:GetWidth()
        GameTooltip:ClearLines()
      end
      if (hasText) then
        GameTooltip:ClearLines()
        r, g, b, a = 1.0, 1.0, 1.0, 1.0 -- The colors are arbitrary.
        GameTooltip:SetText(infoText, r, g, b, a, false)
        textWidthRequired = GameTooltip:GetWidth()
        if (hasTitleText) then
          local tooltipTextWidthPadding = 0 -- 2 * 6 -- Account for the fixed width of the frame's border and padding.
          textWidthRequired = 0.85 * (textWidthRequired - tooltipTextWidthPadding) + tooltipTextWidthPadding -- IMPORTANT: We must scale the width because the first line is a larger font (i.e., GameTooltipHeaderText) than any subsequent lines (i.e., GameTooltipText).
        end
        GameTooltip:ClearLines()
      end
      local widthRequired = math.max(titleWidthRequired, textWidthRequired)
      local widthMinAllowed = 32 -- This value is arbitrary.
      local widthMaxAllowed = 400 -- This value is arbitrary.
      local widthDesired = EasyOptions:LimitValue(widthRequired, widthMinAllowed, widthMaxAllowed, false)
      GameTooltip:SetMinimumWidth(widthDesired)
      -- GameTooltip:SetShrinkToFitWrapped()
      --
      -- Set the tooltip text and show the GameTooltip frame.
      if (hasTitleText) then -- NOTE: By default, the first line is a larger font (i.e., GameTooltipHeaderText) than any subsequent lines (i.e., GameTooltipText).
        r = info.TitleR or tooltipTitleColor[1] or EasyOptions.Fields.DefaultTitleColor[1]
        g = info.TitleG or tooltipTitleColor[2] or EasyOptions.Fields.DefaultTitleColor[2]
        b = info.TitleB or tooltipTitleColor[3] or EasyOptions.Fields.DefaultTitleColor[3]
        a = info.TitleA or tooltipTitleColor[4] or 1.0
        GameTooltip:AddLine(infoTitleText, r, g, b, a, info.TextWrap)
        -- GameTooltip:AddDoubleLine(leftText, rightText, r, g, b, r, g, b)
        -- GameTooltip:SetText(infoTitleText, r, g, b, a, info.TextWrap)
      end
      if (hasText) then
        r = info.R or tooltipTextColor[1] or 1.0
        g = info.G or tooltipTextColor[2] or 1.0
        b = info.B or tooltipTextColor[3] or 1.0
        a = info.A or tooltipTextColor[4] or 1.0
        GameTooltip:AddLine(infoText, r, g, b, a, info.TextWrap)
        -- GameTooltip:AddDoubleLine(leftText, rightText, r, g, b, r, g, b)
        -- GameTooltip:SetText(infoText, r, g, b, a, info.TextWrap)
      end
      GameTooltip:Show()
    end
  end
  control.OnLeaveTooltipFunction = function(self)
    local control = self
    local info = control.TooltipInfo
    if (EasyOptions:IsTooltipOwner(control)) then
      EasyOptions.Fields.TooltipOwner = nil
    end
    if (info == nil) then
      return
    end
    if (GameTooltip:IsOwned(info.Owner)) then -- GameTooltip:GetOwner() == self
      GameTooltip:Hide()
    end
  end

  if (not control.HasInitializedTooltipEvents) then
    control.HasInitializedTooltipEvents = true
    if (isMinimapButton) then
      local controlLocal = control
      local onEnterBase = control.OnEnter
      local onLeaveBase = control.OnLeave
      control.OnEnter = function(self) -- Override the method.
        local control = controlLocal -- self
        control:OnEnterTooltipFunction()
        if (onEnterBase ~= nil) then
          onEnterBase(self)
        end
      end
      control.OnLeave = function(self) -- Override the method.
        local control = controlLocal -- self
        control:OnLeaveTooltipFunction()
        if (onLeaveBase ~= nil) then
          onLeaveBase(self)
        end
      end
    else
      control:HookScript("OnEnter", control.OnEnterTooltipFunction) -- control:SetScript
      control:HookScript("OnLeave", control.OnLeaveTooltipFunction) -- control:SetScript
    end
  end
end
function EasyOptions:IsTooltipOwner(control)
  if (control == nil) then
    return false
  end
  local owner = GameTooltip:GetOwner()
  if (owner ~= nil) then
    return (owner == control)
  else
    return (EasyOptions.Fields.TooltipOwner == control)
  end
end
function EasyOptions:ShouldEnableTooltip(control, isMinimapButton)
  -- Determine whether tooltips are enabled in Options.
  local isEnabled = EasyOptions:IIF(isMinimapButton, EasyOptionsDB["EnableMinimapButtonTooltips"], EasyOptionsDB["EnableStandardTooltips"])
  if (not isEnabled) then
    return false
  end

  -- Verify the enabled state of the control.
  if (not EasyOptions:GetEnabledStateOfControl(control)) then
    return false
  end

  -- Determine whether tooltips are enabled in Options for the control type that is under the mouse cursor.
  local topLevelOwner = EasyOptions:GetTopLevelOwner(control)
  local allowOwnerType = false
  local useBaseType = true
  if (EasyOptions:IsControlType(topLevelOwner, "DropDown", allowOwnerType, useBaseType)) then
    if (EasyOptions:IsControlType(control, "Label", allowOwnerType, useBaseType)) then
      if (not EasyOptionsDB["EnableTooltips_DropDown_Label"]) then
        return false
      end
    else
      if (not EasyOptionsDB["EnableTooltips_DropDown_Control"]) then
        return false
      end
    end
  end
  if (EasyOptions:IsControlType(topLevelOwner, "TextBox", allowOwnerType, useBaseType)) then -- or (EasyOptions:IsControlType(topLevelOwner, "TextBoxBorderFrame", allowOwnerType, false)) then
    if (EasyOptions:IsControlType(control, "Label", allowOwnerType, useBaseType)) then
      if (not EasyOptionsDB["EnableTooltips_TextBox_Label"]) then
        return false
      end
    else
      if (not EasyOptionsDB["EnableTooltips_TextBox_Control"]) then
        return false
      end
    end
  end
  if (EasyOptions:IsControlType(topLevelOwner, "Slider", allowOwnerType, useBaseType)) then
    if (EasyOptions:IsControlType(control, "Label", allowOwnerType, useBaseType)) then
      if (not EasyOptionsDB["EnableTooltips_Slider_Label"]) then
        return false
      end
    else
      if (not EasyOptionsDB["EnableTooltips_Slider_Control"]) then
        return false
      end
    end
  end

  return true
end
function EasyOptions:GetTooltipTextColors() -- tooltipTitleColor, tooltipTextColor
  if (not EasyOptionsDB["SwapTooltipTextColors"]) then
    return EasyOptionsDB["TooltipTitleColor"], EasyOptionsDB["TooltipTextColor"]
  else
    return EasyOptionsDB["TooltipTextColor"], EasyOptionsDB["TooltipTitleColor"]
  end
end
function EasyOptions:IsControlOwnerOfTooltip(control, requireIsTooltipShown)
  if (control == nil) then
    return false
  end
  if (requireIsTooltipShown) and (not GameTooltip:IsShown()) then
    return false
  end

  if (EasyOptions:IsTooltipOwner(control)) then
    return true
  end

  local owner
  if (control.TooltipInfo == nil) or (control.TooltipInfo.Owner == nil) then
    owner = control
  else
    owner = control.TooltipInfo.Owner
  end
  return GameTooltip:IsOwned(owner)
  -- local ownerFrame, anchor = GameTooltip:GetOwner()
end
function EasyOptions:ShouldRefreshTooltipAfterOptionValueChanged(optionItem)
  if (not EasyOptions.Fields.EnableRefreshTooltipAfterControlValueChanged) then
    return false
  end

  if (optionItem == nil) then
    return false
  end

  local control = optionItem.Control
  if (control == nil) or (control.OnEnterTooltipFunction == nil) then
    return false
  end

  -- The following code was removed because it will cause all OptionItems to refresh GameTooltip if their option value changes when multiple options are reset.
  --  See also EasyOptions:IsTooltipOwner().
  -- local ownerFrame, anchor = GameTooltip:GetOwner()
  -- if (not GameTooltip:IsShown()) and (ownerFrame == nil) then
  --   return true
  -- end

  local requireIsTooltipShown = false -- IMPORTANT: We must not require GameTooltip to be shown in case we want to show GameTooltip after a control's value is changed (e.g., a control does not have a default tooltip, but we want to show the "Right click to reset" tooltip after the user changes the value of the control).
  return EasyOptions:IsControlOwnerOfTooltip(control, requireIsTooltipShown)
end
function EasyOptions:RefreshTooltipAfterOptionValueChanged(optionItem)
  if (optionItem == nil) then
    return
  end

  local control = optionItem.Control
  if (control ~= nil) and (control.OnEnterTooltipFunction ~= nil) and (EasyOptions:IsTooltipOwner(control)) then
    control:OnEnterTooltipFunction()
  end
end

function EasyOptions:SetVisibleState(control, isVisible)
  if (control == nil) then
    return
  end
  if (isVisible) then
    control:Show()
  else
    control:Hide()
  end
end

function EasyOptions:IsOwnerOf(owner, ownedControl, includeGrandparentControls)
  if (owner == nil) then
    return false
  end
  if (ownedControl == nil) then
    return false
  end
  
  if (ownedControl.Owner == owner) then
    return true
  end
  if (includeGrandparentControls) then
    return EasyOptions:IsOwnerOf(owner, ownedControl.Owner, includeGrandparentControls)
  else
    return false
  end
end
function EasyOptions:IsParentOf(parent, child, includeGrandparentControls)
  if (parent == nil) then
    return false
  end
  if (child == nil) then
    return false
  end
  
  if (child.Parent == parent) then
    return true
  end
  if (includeGrandparentControls) then
    return EasyOptions:IsParentOf(parent, child.Parent, includeGrandparentControls)
  else
    return false
  end
end
function EasyOptions:GetTopLevelOwner(control)
  if (control == nil) then
    return nil
  end
  if (control.Owner == nil) then
    return control
  end
  return EasyOptions:GetTopLevelOwner(control.Owner)
end
function EasyOptions:GetTopLevelParent(control)
  if (control == nil) then
    return nil
  end
  if (control.Parent == nil) then
    return control
  end
  return EasyOptions:GetTopLevelParent(control.Parent)
end
function EasyOptions:IsFrame(frame, allowUIParent)
  if (frame == nil) then
    return false
  end
  if (allowUIParent) and ((frame == UIParent) or (frame == "UIParent")) then
    return true
  end
  if (frame.GetFrameLevel == nil) or (frame.GetFrameStrata == nil) or (frame.SetClampedToScreen == nil) or (frame.GetChildren == nil) then -- Check for methods that are expected to be supported by a frame.
    return false
  end
  return true
end
function EasyOptions:IsControlType(control, controlType, allowOwnerType, useBaseType)
  if (control == nil) then
    return false
  end
  if (controlType == nil) or (controlType == "") then
    return false
  end

  local controlTypeOfControl = EasyOptions:IIF(useBaseType, control.BaseType, control.Type)
  if (controlTypeOfControl == controlType) then
    return true
  elseif (allowOwnerType) and (control.Owner ~= nil) then
    return EasyOptions:IsControlType(control.Owner, controlType, allowOwnerType, useBaseType)
  else
    return false
  end
end
function EasyOptions:IsStartContainerControlType(controlType)
  return (controlType == "StartGroupBox") or (controlType == "StartTabControl") or (controlType == "StartTab") or (controlType == "StartListView") or (controlType == "StartListViewItem")
end
function EasyOptions:IsEndContainerControlType(controlType)
  return (controlType == "EndGroupBox") or (controlType == "EndTabControl") or (controlType == "EndTab") or (controlType == "EndListView") or (controlType == "EndListViewItem")
end
function EasyOptions:GetStartContainerControlTypeFromEndContainerControlType(controlType)
  if (controlType == "EndGroupBox") then
    return "StartGroupBox"
  elseif (controlType == "EndTabControl") then
    return "StartTabControl"
  elseif (controlType == "EndTab") then
    return "StartTab"
  elseif (controlType == "EndListView") then
    return "StartListView"
  elseif (controlType == "EndListViewItem") then
    return "StartListViewItem"
  else
    EasyOptions:PrintErrorMessageOnce(L("The control type \"{1}\" is not a known container type.", controlType))
    return nil
  end
end
function EasyOptions:GetEndContainerControlTypeFromStartContainerControlType(controlType)
  if (controlType == "StartGroupBox") then
    return "EndGroupBox"
  elseif (controlType == "StartTabControl") then
    return "EndTabControl"
  elseif (controlType == "StartTab") then
    return "EndTab"
  elseif (controlType == "StartListView") then
    return "EndListView"
  elseif (controlType == "StartListViewItem") then
    return "EndListViewItem"
  else
    EasyOptions:PrintErrorMessageOnce(L("The control type \"{1}\" is not a known container type.", controlType))
    return nil
  end
end

function EasyOptions:GetFullNameOfOptionItems(optionItems)
  if (optionItems == nil) then
    return nil
  end

  local result = optionItems.Name
  local parentName = EasyOptions:GetFullNameOfOptionItems(optionItems.Parent)
  if (parentName ~= nil) and (parentName ~= "") then
    result = parentName.."."..result
  end
  return result
end

function EasyOptions:AddControlToOwner(control, owner)
  if (control == nil) then
    return
  end
  
  local ownerType
  if (owner == nil) or (owner.BaseType == nil) then
    ownerType = "nil"
  else
    ownerType = owner.BaseType
  end

  if (control.Owner ~= nil) then
    local controlType = EasyOptions:IIF((control.Type ~= nil) and (control.Type ~= ""), control.Type, "control")
    local ownerName = EasyOptions:IIF((control.Owner.Name ~= nil) and (control.Owner.Name ~= ""), control.Owner.Name, "an owner")
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has a {1} that cannot be assigned to an owner of type \"{2}\" because the {1} for that item has already been assigned to {3}.", controlType, ownerType, ownerName))
    -- if (control.Owner == owner) then
    --   EasyOptions:PrintErrorMessage(L("control.Owner is already set to the desired owner:"))
    --   EasyOptions:Print("  control = {1}", EasyOptions:Format(control.OptionItem[1]))
    --   EasyOptions:Print("  control.Owner = {1}", EasyOptions:Format(control.Owner.OptionItem[1]))
    --   EasyOptions:Print("  owner = {1}", EasyOptions:Format(owner.OptionItem[1]))
    -- elseif (control.Owner ~= nil) then
    --   EasyOptions:PrintErrorMessage(L("control.Owner has already been set:"))
    --   EasyOptions:Print("  control = {1}", EasyOptions:Format(control.OptionItem[1]))
    --   EasyOptions:Print("  control.Owner = {1}", EasyOptions:Format(control.Owner.OptionItem[1]))
    --   EasyOptions:Print("  owner = {1}", EasyOptions:Format(owner.OptionItem[1]))
    -- end
    return
  end
  if (EasyOptions:IsParentOf(control, owner, true)) then
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("cannot be assigned to an owner of type \"{1}\" because the control for that item is a child of the desired owner.", ownerType))
    return
  end
  if (owner.OwnedControls == nil) then
    owner.OwnedControls = {}
  end
  table.insert(owner.OwnedControls, control)
  control.Owner = owner
end
function EasyOptions:AddControlsToOwner(controls, owner)
  if (controls == nil) then
    return
  end
  local length = EasyOptions:GetLength(controls)
  for i = 1, length do
    EasyOptions:AddControlToOwner(controls[i], owner)
  end
end
function EasyOptions:AddControlToParent(control, parent)
  if (control == nil) then
    return
  end
  if (control.Parent == parent) then
    return
  end
  if (control.Parent ~= nil) then
    local controlInfo
    local parentCurrentInfo
    local parentDesiredInfo
    if (control.Name == nil) or (control.Name == "") then
      controlInfo = ""
    else
      controlInfo = EasyOptions:Format(" ({1})", control.Name)
    end
    if (control.Parent.Name == nil) or (control.Parent.Name == "") then
      parentCurrentInfo = ""
    else
      parentCurrentInfo = EasyOptions:Format(" ({1})", control.Parent.Name)
    end
    if (parent == nil) or (parent.Name == nil) or (parent.Name == "") then
      parentDesiredInfo = ""
    else
      parentDesiredInfo = EasyOptions:Format(" ({1})", parent.Name)
    end
    if (EasyOptions.Current.IsBuildingOptionFrame) then
      EasyOptions:PrintErrorMessageWithOptionItemInfo(L("is associated with a control{1} that cannot be assigned to a parent control{2} because the control has already been assigned to a parent control{3}.", controlInfo, parentDesiredInfo, parentCurrentInfo))
    else
      EasyOptions:PrintErrorMessageOnce(L("A control{1} cannot be assigned to a parent control{2} because the control has already been assigned to a parent control{3}.", controlInfo, parentDesiredInfo, parentCurrentInfo))
    end
    -- if (control.Parent == parent) then
    --   EasyOptions:PrintErrorMessage(L("control.Parent is already set to the desired parent:"))
    --   EasyOptions:Print("  control = {1}", EasyOptions:Format(control.OptionItem[1]))
    --   EasyOptions:Print("  control.Parent = {1}", EasyOptions:Format(control.Parent.OptionItem[1]))
    --   EasyOptions:Print("  parent = {1}", EasyOptions:Format(parent.OptionItem[1]))
    -- elseif (control.Parent ~= nil) then
    --   EasyOptions:PrintErrorMessage(L("control.Parent has already been set:"))
    --   EasyOptions:Print("  control = {1}", EasyOptions:Format(control.OptionItem[1]))
    --   EasyOptions:Print("  control.Parent = {1}", EasyOptions:Format(control.Parent.OptionItem[1]))
    --   EasyOptions:Print("  parent = {1}", EasyOptions:Format(parent.OptionItem[1]))
    -- end
    return
  end
  if (parent.Children == nil) then
    parent.Children = {}
  end
  table.insert(parent.Children, control)
  control.Parent = parent
end
function EasyOptions:AddControlsToParent(controls, parent)
  if (controls == nil) then
    return
  end
  local length = EasyOptions:GetLength(controls)
  for i = 1, length do
    EasyOptions:AddControlToParent(controls[i], parent)
  end
end

function EasyOptions:GetAutoFitWidthAndHeightEffective(customSize, autoFitWidth, autoFitHeight, ignoreCustomHeight, allowFillWidthFlag, assumeSingleValueIsHeight) -- autoFitWidth, autoFitHeight
  -- Description: Returns appropriate values (true or false) for autoFitWidth and autoFitHeight based on the specified values (which can be nil) and a specified customSize (which can be nil).
  --  NOTE: All parameters that are passed to this method may be nil.

  if (autoFitWidth ~= nil) and (autoFitHeight ~= nil) then
    return autoFitWidth, autoFitHeight -- Use the specified values if neither of the values are nil.
  end

  -- Determine the appropriate values when accounting for whether customSize contains a width or height.
  local hasWidth, hasHeight, shouldFillWidth = EasyOptions:GetSizeInfoFromCustomSize(customSize, ignoreCustomHeight, allowFillWidthFlag, assumeSingleValueIsHeight)
  local autoFitWidthLocal = EasyOptions:IIF(autoFitWidth ~= nil, autoFitWidth, not hasWidth) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
  local autoFitHeightLocal = EasyOptions:IIF(autoFitHeight ~= nil, autoFitHeight, not hasHeight) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
  
  return autoFitWidthLocal, autoFitHeightLocal
end
function EasyOptions:ResizeContainerControlAfterAddChildControls(control, containerInfo)
  local controlType = control.BaseType
  local marginX
  local marginY
  local autoFitWidth
  local autoFitHeight
  local allowAutoFitWidth = containerInfo.AutoFitWidth -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
  local allowAutoFitHeight = containerInfo.AutoFitHeight -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
  if (controlType == "GroupBox") then
    marginX = EasyOptions:GetDefaultControlMarginX()
    marginY = EasyOptions:GetDefaultControlMarginY()
    width = math.max(containerInfo.ControlRegionWidth + marginX + 8, 20) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    height = math.max(containerInfo.ControlRegionHeight + marginY + 10, 20) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    autoFitWidth = allowAutoFitWidth
    autoFitHeight = allowAutoFitHeight
  elseif (controlType == "TabControl") then
    marginX = EasyOptions:GetDefaultControlMarginX()
    marginY = EasyOptions:GetDefaultControlMarginY()
    width = math.max(containerInfo.ControlRegionWidth + marginX + 8, 20) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    height = math.max(containerInfo.ControlRegionHeight + marginY + 10, 20) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    autoFitWidth = allowAutoFitWidth
    autoFitHeight = allowAutoFitHeight
  elseif (controlType == "TabButtonFrame") then
    marginX = 0
    marginY = 0
    width = math.max(containerInfo.ControlRegionWidth + marginX + 0, 1) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    height = math.max(containerInfo.ControlRegionHeight + marginY + 0, 1) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    autoFitWidth = true
    autoFitHeight = true
  elseif (controlType == "ListView") then
    -- Set the width of the ListViewPanel (if required).
    EasyOptions:UpdateListViewAfterAddListViewItems(control)
    --
    -- Determine the desired size of the ListView.
    marginX = EasyOptions:GetDefaultControlMarginX()
    marginY = EasyOptions:GetDefaultControlMarginY()
    width = control.ListViewPanelBorderFrame:GetWidth() + EasyOptions:IIF(containerInfo.ControlRegionWidth > 0, containerInfo.ControlRegionWidth + marginX + 2, 0) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    height = math.max(containerInfo.ControlRegionHeight + marginY + 8, control:GetHeight()) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    autoFitWidth = allowAutoFitWidth
    autoFitHeight = allowAutoFitHeight
  elseif (controlType == "ListViewItemFrame") then
    marginX = 0
    marginY = 0
    width = math.max(containerInfo.ControlRegionWidth + marginX + 0, 1) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    height = math.max(containerInfo.ControlRegionHeight + marginY + 0, 1) -- ContainerControl, ParentControlPrevious, IndentCountCurrentPrevious, PositionInfoCurrentPrevious, PositionInfoInitialWithinContainerControl, ControlRegionWidth, ControlRegionHeight, CustomSize, AutoFitWidth, AutoFitHeight, ControlsInContainerControl
    autoFitWidth = true
    autoFitHeight = true
  else
    autoFitWidth = false
    autoFitHeight = false
    EasyOptions:PrintErrorMessageOnce(L("Unable to resize the container control because \"{1}\" is an unknown container control type.", EasyOptions:FormatName(controlType)))
  end
  
  -- Resize the container control (if required).
  if (autoFitWidth) then
    local previousWidth = control:GetWidth()
    control:SetWidth(width)
    control.ControlRegionWidth = control.ControlRegionWidth + (control:GetWidth() - previousWidth)
    if (controlType == "TabControl") then
      EasyOptions:UpdateTabControlAfterAddTabButtons(control) -- Call this method so the control's width will be set to fit the TabButton region width if appropriate.
    elseif (controlType == "ListView") then
      -- Limit the width of the ListViewPanel (if required).
      local widthMaxAllowed = EasyOptions:GetOptionPanelWidthExcludingMargins()
      local deltaWidth = width - widthMaxAllowed
      if (control.AllowResizeListViewPanelWidthToFitListViewItems) and (deltaWidth > 0) then
        local listViewPanelWidthPrevious = control.ListViewPanelBorderFrame:GetWidth()
        local listViewPanelWidth = math.max(control.ListViewPanelBorderFrame:GetWidth() - deltaWidth, EasyOptions:GetDefaultListViewPanelWidthMin(0))
        control.ListViewPanelBorderFrame:SetWidth(listViewPanelWidth)
        width = width - (listViewPanelWidthPrevious - control.ListViewPanelBorderFrame:GetWidth())
        control:SetWidth(width)
        control.AllowResizeListViewPanelWidthToFitListViewItems = false -- Prevent the ListViewPanel's width from being changed (e.g., increased to fit the width of the ListViewItems).
        EasyOptions:UpdateListViewAfterAddListViewItems(control) -- Call this method to update the visible state of the scroll bar if required.
      end
    end
  end
  if (autoFitHeight) then
    local previousHeight = control:GetHeight()
    control:SetHeight(height)
    control.ControlRegionHeight = control.ControlRegionHeight + (control:GetHeight() - previousHeight)
  end
  return control.ControlRegionWidth, control.ControlRegionHeight
end

function EasyOptions:ShowOptionsFrame(frame)
  if (frame == nil) then
    return
  end
  if (EasyOptions:UseLegacyOptionsMode()) then
    InterfaceOptionsFrame_OpenToCategory(frame) -- getglobal(addonName.."Options")
    InterfaceOptionsFrame_OpenToCategory(frame) -- We must call InterfaceOptionsFrame_OpenToCategory twice to prevent a known WoW API bug.
    -- InterfaceOptionsFrame_OpenToFrame(frame)
  else
    -- EasyOptions:PrintItems({"ShowOptionsFrame", frame.name, frame.OptionsCategoryID})
	  Settings.OpenToCategory(frame.OptionsCategoryID)
	  Settings.OpenToCategory(frame.OptionsCategoryID) -- We must call Settings.OpenToCategory twice to prevent a known WoW API bug.
  end
end
function EasyOptions:ShowOptions(addonName) -- , categoryName
  if (addonName == nil) then
    return
  end
  if (not EasyOptions:IsManagedAddon(addonName)) then
    EasyOptions:PrintErrorMessage(L("Unable to show options for the addon \"{1}\" because that addon is not managed by EasyOptions.", EasyOptions:FormatName(addonName)))
    return
  end

  local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  local optionPanelFrame = addonInfo.MainOptionPanelFrame
  if (optionPanelFrame == nil) then
    EasyOptions:PrintErrorMessage(L("Unable to show options for the addon \"{1}\" because EasyOptions did not create an options frame for that addon.", EasyOptions:FormatName(addonName)))
    return
  end

  EasyOptions:ShowOptionsFrame(optionPanelFrame)

  -- SampleCode: This code was removed because the WoW API does not seem to support showing subcategory options frames.
  -- local categoryNameLocal = categoryName or addonName -- Use the addon name as the category name by default.
  -- if (addonName == nil) then
  --   return
  -- end
  -- if (not EasyOptions:IsManagedAddon(addonName)) then
  --   EasyOptions:PrintErrorMessage(L("Unable to show the options category \"{1}\" for the addon \"{2}\" because that addon is not managed by EasyOptions.", categoryNameLocal, addonName))
  --   return
  -- end
  -- 
  -- local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  -- local optionPanelFrame = addonInfo.OptionPanelFrames[categoryNameLocal]
  -- if (optionPanelFrame == nil) then
  --   if (categoryNameLocal == addonName) then
  --     EasyOptions:PrintErrorMessage(L("Unable to show options for the addon \"{1}\" because EasyOptions did not create a category with that addon's name.", addonName))
  --   else
  --     EasyOptions:PrintErrorMessage(L("Unable to show the options category \"{1}\" for the addon \"{2}\" because EasyOptions did not create a category with that name for the addon.", categoryNameLocal, addonName))
  --   end
  --   return
  -- end
  -- 
  -- EasyOptions:ShowOptionsFrame(optionPanelFrame)
end
function EasyOptions:ShowAddonManager(addonName)
  -- Show the Addon Manager frame.
  --  NOTE: We assume that the Addon Manager frame is the parent category for EasyOptions' options.
  EasyOptions:ShowOptions("EasyOptions")
  
  -- Set the specified addon as EasyOptions' active addon (if possible).
  if (EasyOptions:IsManagedAddon(addonName)) then
    local allowUpdateControl = true
    EasyOptions:SetOptionValue("EasyOptions", "ActiveAddon", addonName, allowUpdateControl)
  end
end

function EasyOptions:GetAddonVersion(addonName, allowNoVersionString)
  local addonVersion
  if (addonName == nil) or (addonName == "") then
    addonVersion = nil
  else
    addonVersion = C_AddOns.GetAddOnMetadata(addonName, "Version") -- GetAddOnMetadata(addonName, "Version")
  end
  return EasyOptions:GetAddonVersionString(addonVersion, allowNoVersionString)
end
function EasyOptions:GetAddonVersionString(addonVersion, allowNoVersionString)
  local defaultResult = EasyOptions:IIF(allowNoVersionString, L("[No Version]"), "")
  if (addonVersion == nil) or (addonVersion == "") then
    return defaultResult
  end

  return EasyOptions:Format(addonVersion)
end

function EasyOptions:IsManagedAddon(addonName)
  if (addonName == nil) then
    return false
  end
  return (EasyOptions.Fields.AddonInfo[addonName] ~= nil)
end
function EasyOptions:GetManagedAddonNames(sortResult, ignoreCase)
  local addonInfo = EasyOptions.Fields.AddonInfo
  return EasyOptions:GetKeys(addonInfo, sortResult, ignoreCase)
end

function EasyOptions:AddRightButtonDownHandlerForOptionControl(control)
  if (control == nil) then
    return
  end
  if (control.HasRightButtonDownHandlerForOptionControl) then
    return
  end
  control.HasRightButtonDownHandlerForOptionControl = true
  control:HookScript("OnMouseDown", function(self, button)
    if (button == "RightButton") and (EasyOptions:GetEnabledStateOfControl(self)) then
      EasyOptions:ResetOptionValue(self.OptionItem, self.IsSubOptionItemControl, true)
    end
  end)
end
function EasyOptions:SetIsSubOptionItemControl(controls)
  if (controls == nil) then
    return
  end
  local length = EasyOptions:GetLength(controls)
  local control
  for i = 1, length do
    control = controls[i]
    control.IsSubOptionItemControl = true
    EasyOptions:SetIsSubOptionItemControl(control.OwnedControls) -- Set the IsSubOptionItemControl property of controls that are owned by this control (e.g., the ValueBox of a Slider control).
  end
end

function EasyOptions:GetValuesFromTabInfo(tabInfo)
  local text, customWidth = nil, nil
  if (tabInfo == nil) then
    -- Do nothing so we return the default values.
  elseif (type(tabInfo) == "string") then -- e.g., "Tab 1"
    text = tabInfo
    customWidth = nil
  elseif (type(tabInfo) == "table") and (EasyOptions:GetLength(tabInfo) == 1) and ((type(tabInfo[1]) == "string") or (type(tabInfo) == "nil")) then -- e.g., {"Tab 1"}
    text = tabInfo[1]
    customWidth = nil
  elseif (type(tabInfo) == "table") and (EasyOptions:GetLength(tabInfo) == 2) and ((type(tabInfo[1]) == "string") or (type(tabInfo) == "nil")) and (type(tabInfo[2]) == "number") then -- e.g., {"Tab 1", 100}
    text = tabInfo[1]
    customWidth = tabInfo[2]
  else
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has an invalid entry for a TabInfo. TabInfo must use one of the following formats: text, {text} or {text, customWidth}."))
  end
  return text, customWidth
end

function EasyOptions:UpdateCheckBoxGroupInfo(control, controlType, iCheckBoxGroupInfo, checkBoxGroupInfo, indentCountCurrent, enableAutoCheckBoxGrouping, isAddingControlsToContainer, isBuildingRow)
  -- Determine parameters.
  local checkBox
  local iCheckBoxGroupInfoResult = iCheckBoxGroupInfo -- init
  --
  -- local isCheckBoxType = EasyOptions:IsCheckBoxType(control)
  local isStartCheckBoxGroup = (controlType == "StartCheckBoxGroup")
  
  -- End current CheckBoxGroups (if required).
  local shouldEndCheckBoxGroup, shouldContinue
  for i = iCheckBoxGroupInfoResult, 1, -1 do -- Loop in reverse order.
    shouldEndCheckBoxGroup, shouldContinue = EasyOptions:UpdateCheckBoxGroupInfo_ShouldEndCheckBoxGroup(control, controlType, i, checkBoxGroupInfo, indentCountCurrent, enableAutoCheckBoxGrouping, isAddingControlsToContainer, isBuildingRow)
    if (shouldEndCheckBoxGroup) then
      checkBoxGroupInfo[i] = nil -- Clear the item.
      iCheckBoxGroupInfoResult = i - 1
    end
    if (not shouldContinue) then
      break
    end
  end
  
  -- Determine whether we should increment or decrement GroupBoxCount or RowCount for each item in checkBoxGroupInfo.
  --  IMPORTANT: This must be done after we potentially add control to the checkBox.OwnedControls list of CheckBoxGroup (see above).
  local length = EasyOptions:GetLength(checkBoxGroupInfo)
  if (EasyOptions:IsStartContainerControlType(controlType)) then
    for i = 1, length do
      checkBoxGroupInfo[i][4] = checkBoxGroupInfo[i][4] + 1 -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    end
  end
  if (EasyOptions:IsEndContainerControlType(controlType)) then
    for i = 1, length do
      checkBoxGroupInfo[i][4] = math.max(checkBoxGroupInfo[i][4] - 1, 0) -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    end
  end
  if (controlType == "StartRow") then
    for i = 1, length do
      checkBoxGroupInfo[i][5] = checkBoxGroupInfo[i][5] + 1 -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    end
  end
  if (controlType == "EndRow") then
    for i = 1, length do
      checkBoxGroupInfo[i][5] = math.max(checkBoxGroupInfo[i][5] - 1, 0) -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    end
  end
  
  -- Add control to the checkBox.OwnedControls list of CheckBoxGroup (if required).
  --  IMPORTANT: This must be done before we potentially start a new CheckBoxGroup (see below).
  --  IMPORTANT: This must be done before we potentially increment the GroupBoxCount of the CheckBoxGroups (see below).
  --  IMPORTANT: We don't require (indentCountCurrent > indentCountOfCheckBoxCurrent) because if the IsManualCheckBoxGroup flag of checkBoxGroupInfoCurrent is true (i.e., the CheckBoxGroup was started due to "StartCheckBoxGroup" command) then we want to add subsequent controls to the checkBox.OwnedControls list regardless of the indentCountCurrent that is associated with each control.
  local checkBoxGroupInfoCurrent, indentCountOfCheckBoxCurrent, isManualCheckBoxGroupCurrent = EasyOptions:UpdateCheckBoxGroupInfo_GetCheckBoxGroupInfoParameters(iCheckBoxGroupInfoResult, checkBoxGroupInfo)
  if (control ~= nil) and (checkBoxGroupInfoCurrent ~= nil) and (checkBoxGroupInfoCurrent[4] == 0) then -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    checkBox = checkBoxGroupInfoCurrent[1] -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    EasyOptions:AddControlToOwner(control, checkBox)
  end
  
  -- Start a new CheckBoxGroup (if required).
  --  IMPORTANT: This must be done after we potentially add control to the checkBox.OwnedControls list of CheckBoxGroup (see above).
  if (EasyOptions:UpdateCheckBoxGroupInfo_ShouldStartCheckBoxGroup(control, controlType, iCheckBoxGroupInfoResult, checkBoxGroupInfo, indentCountCurrent, enableAutoCheckBoxGrouping, isAddingControlsToContainer, isBuildingRow)) then
    local isManualCheckBoxGroup = isStartCheckBoxGroup
    iCheckBoxGroupInfoResult = iCheckBoxGroupInfoResult + 1
    checkBoxGroupInfo[iCheckBoxGroupInfoResult] = {control, indentCountCurrent, isManualCheckBoxGroup, 0, 0, {}} -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
  end
  
  return iCheckBoxGroupInfoResult
end
function EasyOptions:UpdateCheckBoxGroupInfo_ShouldEndCheckBoxGroup(control, controlType, iCheckBoxGroupInfo, checkBoxGroupInfo, indentCountCurrent, enableAutoCheckBoxGrouping, isAddingControlsToContainer, isBuildingRow) -- shouldEndCheckBoxGroup, shouldContinue
  -- Determine parameters.
  local checkBoxGroupInfoCurrent, indentCountOfCheckBoxCurrent, isManualCheckBoxGroupCurrent = EasyOptions:UpdateCheckBoxGroupInfo_GetCheckBoxGroupInfoParameters(iCheckBoxGroupInfo, checkBoxGroupInfo)
  --
  -- local isCheckBoxType = EasyOptions:IsCheckBoxType(control)
  local isEndCheckBoxGroup = (controlType == "EndCheckBoxGroup")
  local isEndAllCheckBoxGroups = (controlType == "EndAllCheckBoxGroups")
  local isStartContainer = EasyOptions:IsStartContainerControlType(controlType)
  local isEndContainer = EasyOptions:IsEndContainerControlType(controlType)
  local isStartRow = (controlType == "StartRow")
  local isEndRow = (controlType == "EndRow")
  --
  local isControlTypeValidForEndCheckBoxGroup = (control ~= nil) or (isStartRow) -- IMPORTANT: We don't require a control for the "StartRow" command because no control is associated with that command, but we determine whether the current CheckBoxGroup should be ended or the controls on the row should be added to the current CheckBoxGroup based on IndentCount that is specified with the "StartRow" command.

  local allowEndCheckBoxGroup = (isEndCheckBoxGroup) or (isEndAllCheckBoxGroups) or ((not isManualCheckBoxGroupCurrent) and (enableAutoCheckBoxGrouping) and (isControlTypeValidForEndCheckBoxGroup))
  -- local allowEndCheckBoxGroup = (isEndCheckBoxGroup) or (isEndAllCheckBoxGroups) or ((not isManualCheckBoxGroupCurrent) and (enableAutoCheckBoxGrouping) and (control ~= nil))
  local shouldEndCheckBoxGroup = false -- init
  local shouldContinue = false -- init
  if (allowEndCheckBoxGroup) and (checkBoxGroupInfoCurrent ~= nil) then
    if (isEndCheckBoxGroup) or (isEndAllCheckBoxGroups) then
      shouldEndCheckBoxGroup = true -- End the current CheckBoxGroup if isEndCheckBoxGroup or isEndAllCheckBoxGroups are true (i.e., controlType == "EndCheckBoxGroup" or controlType == "EndAllCheckBoxGroups").
      shouldContinue = (shouldEndCheckBoxGroup) and ((isEndAllCheckBoxGroups) or (not isManualCheckBoxGroupCurrent))
    -- elseif (isBuildingRow) then
    --   shouldEndCheckBoxGroup = false -- Don't end a CheckBoxGroup while we are building a Row.
    elseif (checkBoxGroupInfoCurrent[4] == 0) and (isEndContainer) then -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
      shouldEndCheckBoxGroup = true -- End the current CheckBoxGroup if control is contained in a GroupBox and we are done adding controls to that GroupBox.
      shouldContinue = shouldEndCheckBoxGroup
    elseif (checkBoxGroupInfoCurrent[5] == 0) and (isEndRow) then -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
      shouldEndCheckBoxGroup = true -- End the current CheckBoxGroup if control is contained on a Row and we are done adding controls to that Row.
      shouldContinue = shouldEndCheckBoxGroup
    elseif ((checkBoxGroupInfoCurrent[4] == 1) and (not isEndContainer)) or (checkBoxGroupInfoCurrent[4] >= 2) then -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
      shouldEndCheckBoxGroup = false -- Don't end the current CheckBoxGroup if control is contained in a GroupBox that is a child of checkBoxGroupInfoCurrent.
      shouldContinue = shouldEndCheckBoxGroup
    elseif (checkBoxGroupInfoCurrent[5] >= 1) then -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
      shouldEndCheckBoxGroup = false -- Don't end the current CheckBoxGroup if control is contained in a Row that is a child of checkBoxGroupInfoCurrent.
      shouldContinue = shouldEndCheckBoxGroup
    elseif (indentCountCurrent <= indentCountOfCheckBoxCurrent) then
      shouldEndCheckBoxGroup = true -- End the current CheckBoxGroup if indentCountCurrent has decreased or is the same as the current CheckBoxGroup.
      shouldContinue = shouldEndCheckBoxGroup
    end
  end
  
  return shouldEndCheckBoxGroup, shouldContinue
end
function EasyOptions:UpdateCheckBoxGroupInfo_ShouldStartCheckBoxGroup(control, controlType, iCheckBoxGroupInfo, checkBoxGroupInfo, indentCountCurrent, enableAutoCheckBoxGrouping, isAddingControlsToContainer, isBuildingRow)
  -- Determine parameters.
  local checkBoxGroupInfoCurrent, indentCountOfCheckBoxCurrent, isManualCheckBoxGroupCurrent = EasyOptions:UpdateCheckBoxGroupInfo_GetCheckBoxGroupInfoParameters(iCheckBoxGroupInfo, checkBoxGroupInfo)
  --
  local isCheckBoxType = EasyOptions:IsCheckBoxType(control)
  local isStartCheckBoxGroup = (controlType == "StartCheckBoxGroup")
  
  local allowStartCheckBoxGroup = ((isStartCheckBoxGroup) or ((enableAutoCheckBoxGrouping) and (not isBuildingRow))) and (isCheckBoxType)
  -- local allowStartCheckBoxGroup = (not isManualCheckBoxGroupCurrent) and ((isStartCheckBoxGroup) or ((enableAutoCheckBoxGrouping) and (not isBuildingRow))) and (isCheckBoxType)
  local shouldStartCheckBoxGroup = false -- init
  if (allowStartCheckBoxGroup) then
    if (isStartCheckBoxGroup) then
      shouldStartCheckBoxGroup = true -- Start a new CheckBoxGroup if control is a CheckBox and isStartCheckBoxGroup is true (i.e., controlType == "StartCheckBoxGroup").
    elseif (isBuildingRow) then
      shouldStartCheckBoxGroup = false -- Don't start a CheckBoxGroup while we are building a Row.
    elseif (checkBoxGroupInfoCurrent == nil) or (isManualCheckBoxGroupCurrent) then
      shouldStartCheckBoxGroup = true -- Start a new CheckBoxGroup if control is a CheckBox and there are no existing CheckBoxGroups or if isManualCheckBoxGroupCurrent is true (i.e., the CheckBoxGroup was started due to "StartCheckBoxGroup" command).
    elseif (checkBoxGroupInfoCurrent[4] >= 1) or (checkBoxGroupInfoCurrent[5] >= 1) then -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
      shouldStartCheckBoxGroup = true -- Start a new CheckBoxGroup if control is a CheckBox that is contained in a GroupBox or a Row that is a child of checkBoxGroupInfoCurrent.
    elseif (indentCountCurrent > indentCountOfCheckBoxCurrent) then
      shouldStartCheckBoxGroup = true -- Start a new CheckBoxGroup if control is a CheckBox and indentCountCurrent has increased.
    end
  end
  
  return shouldStartCheckBoxGroup
end
function EasyOptions:UpdateCheckBoxGroupInfo_GetCheckBoxGroupInfoParameters(iCheckBoxGroupInfo, checkBoxGroupInfo) -- checkBoxGroupInfoCurrent, indentCountOfCheckBoxCurrent, isManualCheckBoxGroupCurrent
  -- Determine parameters.
  local checkBoxGroupInfoCurrent -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
  local indentCountOfCheckBoxCurrent
  local isManualCheckBoxGroupCurrent
  if (iCheckBoxGroupInfo == 0) then
    checkBoxGroupInfoCurrent = nil
    indentCountOfCheckBoxCurrent = -1
    isManualCheckBoxGroupCurrent = false
  else
    checkBoxGroupInfoCurrent = checkBoxGroupInfo[iCheckBoxGroupInfo] -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    indentCountOfCheckBoxCurrent = checkBoxGroupInfoCurrent[2] -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
    isManualCheckBoxGroupCurrent = checkBoxGroupInfoCurrent[3] -- [1] CheckBox, [2] IndentCountOfCheckBox, [3] IsManualCheckBoxGroup, [4] GroupBoxCount, [5] RowCount
  end
  return checkBoxGroupInfoCurrent, indentCountOfCheckBoxCurrent, isManualCheckBoxGroupCurrent
end

function EasyOptions:CreateStandardFrame(name, parent, customFrameType, createBackgroundTexture, backgroundTextureColor, setBackdrop)
  -- Description: Returns a frame that is compatible with EasyOptions.
  local frameType = customFrameType or "Frame"
  local frame = CreateFrame(frameType, name, parent, "BackdropTemplateMixin" and "BackdropTemplate")
  frame.Name = name
  if (createBackgroundTexture) then
    local frameBackgroundName = EasyOptions:GetChildControlName(frame, ".Background")
    frame.Background = frame:CreateTexture(frameBackgroundName, "BACKGROUND", nil, 1) -- Create a texture for the background. -- IMPORTANT: We set the sublayer to 1 to ensure that the frame's Background texture is always drawn on top of the frame's Backdrop.
    frame.Background:SetAllPoints(true)
    if (backgroundTextureColor ~= nil) then
      frame.Background:SetColorTexture(backgroundTextureColor[1], backgroundTextureColor[2], backgroundTextureColor[3], backgroundTextureColor[4] or 1.0)
    else
      frame.Background:SetColorTexture(0.0, 0.0, 0.0, 0.0)
    end
  end
  if (setBackdrop) then
    local texture = "Interface\\Tooltips\\UI-Tooltip-Background" -- "Interface\\ChatFrame\\ChatFrameBackground"
    frame:SetBackdrop({bgFile = texture, tile = true, tileSize = 32})
    EasyOptions:SetFrameBackdropColorForLockedState(frame)
  end
  return frame
end
function EasyOptions:CreateEventFrame(name, onUpdateHandler)
  -- Description: Returns a frame that is intended to listen for events.
  local frame = CreateFrame("Frame", name, UIParent, "BackdropTemplateMixin" and "BackdropTemplate")
  frame.Events = {}
  frame:SetScript("OnEvent", function(self, event, ...) -- Sets the function to handle events, passing all parameters.
    frame.Events[event](...)
  end)
  if (onUpdateHandler ~= nil) then
    frame:SetScript("OnUpdate", onUpdateHandler)
  end
  
  -- Set properties.
  frame:ClearAllPoints()
  frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -1, 1) -- Move the frame off screen. "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
  frame:SetSize(0, 0)
  frame:SetBackdropColor(0, 0, 0, 0)
  -- frame:Hide() -- IMPORTANT: Do not hide the frame because some events (e.g., OnUpdate) don't occur if the frame is hidden.

  return frame
end
function EasyOptions:CreateContainerFrame(name, parent, showBackground, showBorder, addHideGameTooltipScripts, backdropInfo)
  local frame = CreateFrame("Frame", name, parent, "BackdropTemplateMixin" and "BackdropTemplate")
  frame.Type = "ContainerFrame"
  frame.BaseType = "Frame"

  EasyOptions:SetBackdropForContainerFrame(frame, showBackground, showBorder, backdropInfo)

  if (addHideGameTooltipScripts) then
    EasyOptions:AddHideGameTooltipScripts(frame)
  end

  return frame
end
function EasyOptions:SetBackdropForContainerFrame(frame, showBackground, showBorder, backdropInfo)
  local controlStyle = EasyOptions.Fields.TextureInfos.DefaultControlStyle -- Use the default control style.
  local backdropInfoLocal = backdropInfo or EasyOptions:GetDefaultBorderBackdropInfo(controlStyle)
  frame:SetBackdrop(backdropInfoLocal) -- Saves the table to frame.backdropInfo and calls frame:ApplyBackdrop(), or calls frame:ClearBackdrop() if the backdropInfo argument is nil.
  frame:SetBackdropColor(0.0, 0.0, 0.0, EasyOptions:IIF(showBackground, 0.4, 0.0))
  frame:SetBackdropBorderColor(0.6, 0.6, 0.6, EasyOptions:IIF(showBorder, 1.0, 0.0))
end
function EasyOptions:GetDefaultBorderBackdropInfo(controlStyle)
  -- Reference: https://warcraft.wiki.gg/wiki/BackdropTemplate
  local textureInfos = EasyOptions:GetTextureInfos(controlStyle)
  local inset = textureInfos.BorderBackdropInset -- 3 -- Specifies how much each side of a frame is indented from the corresponding border edge.
  local backdropInfo = {
    bgFile = textureInfos.DefaultBackgroundFilePath, -- "Interface\\Tooltips\\UI-Tooltip-Background"
    tile = true, 
    -- tileSize = 16, 
    edgeFile = textureInfos.DefaultBorderFilePath, -- "Interface\\Tooltips\\UI-Tooltip-Border", 
    edgeSize = 14, -- NOTE: Using a value less than 16 helps prevent gaps or overlaps where the side and corner textures join. -- IMPORTANT: The edgeSize must be <= half the width or height of the control (whichever is smaller) to prevent overlaps where the side and corner textures join.
    insets = {left = inset, right = inset, top = inset, bottom = inset}, 
  }
  return backdropInfo
end
function EasyOptions:GetDefaultBorderBackdropInset(controlStyle)
  -- Description: Specifies how much each side of a frame is indented from the corresponding border edge.
  local textureInfos = EasyOptions:GetTextureInfos(controlStyle)
  return textureInfos["BorderBackdropInset"]
end
function EasyOptions:GetTextureInfo(key, controlStyle)
  if (key == nil) or (key == "") then
    return nil
  end
  local textureInfos = EasyOptions:GetTextureInfos(controlStyle)
  return textureInfos[key]
end
function EasyOptions:GetTextureInfos(controlStyle)
  local controlStyleLocal = controlStyle or EasyOptionsDB["ControlStyle"] -- Use the active control style by default.
  local result = EasyOptions.Fields.TextureInfos[controlStyleLocal]
  return result or EasyOptions.Fields.TextureInfos.Classic
end
function EasyOptions:CreateScrollFrame(parent, customControlName, showBackground, showBorder, width, height, positionScrollFrameInsideBorderFrame, customScrollFramePadding, onScrollHandler) -- borderFrame, scrollFrame
  -- customScrollFramePadding = {topLeftPaddingX, topLeftPaddingY, bottomRightPaddingX, bottomRightPaddingY}

  local controlNameDefault = "ScrollFrameOwner"
  local controlName = EasyOptions:GetControlName(controlNameDefault, customControlName)

  local borderFrame = CreateFrame("Frame", controlName..".BorderFrame", parent, "BackdropTemplateMixin" and "BackdropTemplate")
  borderFrame.Name = borderFrame:GetName()
  borderFrame.Type = "BorderFrame"
  borderFrame.BaseType = "Frame"
  borderFrame:SetSize(width, height)
  EasyOptions:SetBackdropForContainerFrame(borderFrame, showBackground, showBorder)
  EasyOptions:AddHideGameTooltipScripts(borderFrame)

  local customScrollFramePaddingLocal = customScrollFramePadding or {}
  local topLeftPaddingX = customScrollFramePaddingLocal[1] or 0
  local topLeftPaddingY = customScrollFramePaddingLocal[2] or 0
  local bottomRightPaddingX = (customScrollFramePaddingLocal[3] or 0) + EasyOptions:IIF(positionScrollFrameInsideBorderFrame, 26, 4)
  local bottomRightPaddingY = customScrollFramePaddingLocal[4] or 0
  local scrollFrame = CreateFrame("ScrollFrame", controlName..".ScrollFrame", borderFrame, "UIPanelScrollFrameTemplate") -- Create a ScrollFrame to contain the TextBox.
  scrollFrame.Name = scrollFrame:GetName()
  scrollFrame.Type = "ScrollFrame"
  scrollFrame.BaseType = "ScrollFrame"
  scrollFrame.BorderFrame = borderFrame
  scrollFrame.IsVerticalScrollBarVisible = true
  scrollFrame.ScrollBarRegionWidthWhenVisible = EasyOptions:IIF(positionScrollFrameInsideBorderFrame, 16, EasyOptions.Fields.DefaultScrollBarRegionWidth)
  scrollFrame.ScrollBarRegionWidth = scrollFrame.ScrollBarRegionWidthWhenVisible
  scrollFrame.ScrollFramePadding = {topLeftPaddingX, topLeftPaddingY, bottomRightPaddingX, bottomRightPaddingY}
  EasyOptions:AddHideGameTooltipScripts(scrollFrame)
  
  scrollFrame.RequiresVerticalScrollBar = function(self)
    scrollFrame = self
    return (scrollFrame:GetVerticalScrollRange() > 0)
  end
  scrollFrame.AutoSetVerticalScrollBarVisibleState = function(self)
    scrollFrame = self
    local requiresVerticalScrollBar = scrollFrame:RequiresVerticalScrollBar()
    scrollFrame:SetVerticalScrollBarVisibleState(requiresVerticalScrollBar)
    return requiresVerticalScrollBar
  end
  scrollFrame.SetVerticalScrollBarVisibleState = function(self, isVisible)
    scrollFrame = self
    if (isVisible) then
      scrollFrame:ShowVerticalScrollBar()
    else
      scrollFrame:HideVerticalScrollBar()
    end
  end
  scrollFrame.ShowVerticalScrollBar = function(self)
    scrollFrame = self
    scrollFrame.IsVerticalScrollBarVisible = true
    scrollFrame.ScrollBarRegionWidth = scrollFrame.ScrollBarRegionWidthWhenVisible
    scrollFrame:SetPoint("RIGHT", scrollFrame.BorderFrame, "RIGHT", -scrollFrame.ScrollFramePadding[3], 0) -- ScrollFramePadding = {topLeftPaddingX, topLeftPaddingY, bottomRightPaddingX, bottomRightPaddingY}
    scrollFrame:EnableMouse(true) -- control:IsEnabled()
  end
  scrollFrame.HideVerticalScrollBar = function(self)
    scrollFrame = self
    scrollFrame.IsVerticalScrollBarVisible = false
    scrollFrame.ScrollBarRegionWidth = 0
    scrollFrame:SetPoint("RIGHT", scrollFrame.BorderFrame, "RIGHT", EasyOptions.Fields.DefaultOffScreenPosition, 0) -- Move the scroll bar off-screen.
    scrollFrame:EnableMouse(false)
  end

  scrollFrame:SetPoint("TOPLEFT", borderFrame, "TOPLEFT", topLeftPaddingX, -EasyOptions.Fields.ScrollBarVerticalMargin - topLeftPaddingY)
  scrollFrame:SetPoint("BOTTOM", borderFrame, "BOTTOM", 0, EasyOptions.Fields.ScrollBarVerticalMargin + bottomRightPaddingY)
  -- scrollFrame:SetPoint("BOTTOMRIGHT", borderFrame, "BOTTOMRIGHT", -bottomRightPaddingX, EasyOptions.Fields.ScrollBarVerticalMargin + bottomRightPaddingY)
  scrollFrame:ShowVerticalScrollBar()

  if (onScrollHandler ~= nil) then
    local onScrollHandlerLocal = onScrollHandler
    scrollFrame:HookScript("OnHorizontalScroll", function(self, offset) -- IMPORTANT: We must use HookScript rather than SetScript to ensure that we do not overwrite an event handler in the WoW API.
      onScrollHandlerLocal(self, offset, true)
    end)
    scrollFrame:HookScript("OnVerticalScroll", function(self, offset) -- IMPORTANT: We must use HookScript rather than SetScript to ensure that we do not overwrite an event handler in the WoW API.
      onScrollHandlerLocal(self, offset, false)
    end)
  end

  scrollFrame.OnEnabledChangedHandler = function(self)
    -- Reserved.
  end
  EasyOptions:AddSupportForEnabledState(scrollFrame)

  return borderFrame, scrollFrame
end
function EasyOptions:CreateControlContainedInScrollFrame(frameType, parent, controlName, showBackground, showBorder, width, height, positionScrollFrameInsideBorderFrame, customScrollFramePadding, onScrollHandler) -- control, borderFrame, scrollFrame
  local borderFrame, scrollFrame = EasyOptions:CreateScrollFrame(parent, controlName, showBackground, showBorder, width, height, positionScrollFrameInsideBorderFrame, customScrollFramePadding, onScrollHandler)
  local control = CreateFrame(frameType, controlName, scrollFrame, "BackdropTemplateMixin" and "BackdropTemplate")
  EasyOptions:AddControlToOwner(borderFrame, control)
  EasyOptions:AddControlToOwner(scrollFrame, control)
  control.BorderFrame = borderFrame
  control.ScrollFrame = scrollFrame
  control.ContainerControl = scrollFrame -- Specifies the frame that receives mouse events in the space that appears to be owned by the control when the control is too small to fill that space.
  borderFrame.Control = control
  scrollFrame.Control = control
  scrollFrame:SetScrollChild(control)
  control:SetPoint("TOPLEFT", control.ScrollFrame, "TOPLEFT", 0, 0)
  -- control:SetPoint("BOTTOMRIGHT", control.ScrollFrame, "BOTTOMRIGHT", 0, 0)
  return control, borderFrame, scrollFrame
end
function EasyOptions:AddHideGameTooltipScripts(frame)
  -- Logic in this method was implemented because some control types (e.g., EditBox) fail to raise the OnLeave event which prevents their tooltip from being hidden when the user moves the mouse out of the control.
  frame:HookScript("OnEnter", function(self)
    EasyOptions.Fields.TooltipOwner = nil
    GameTooltip:Hide()
  end)
  frame:HookScript("OnLeave", function(self)
    EasyOptions.Fields.TooltipOwner = nil
    GameTooltip:Hide()
  end)
end
function EasyOptions:UpdateFrameVisibleState(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  if (value) then
    frame:Show()
  else
    frame:Hide()
  end
end
function EasyOptions:UpdateFrameLockedState(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  frame:SetMovable(not value)
  frame:EnableMouse(not value)
  EasyOptions:SetFrameBackdropColorForLockedState(frame, value)
end
function EasyOptions:UpdateFrameClampedToScreenState(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  frame:SetClampedToScreen(value)
end
function EasyOptions:UpdateFrameAnchoredToMouseState(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  if (frame.IsAnchoredToMouse == value) then -- Exit the method if the value has not changed.
    return
  end

  frame.IsAnchoredToMouse = false -- Disable the state while we update properties.
  if (value) then
    -- Store the current points.
    frame:SetFixedAnchorPointInfos(EasyOptions:GetFrameAnchorPointInfos(frame))
  else
    -- Restore the saved points.
    local clearAllPoints = true
    EasyOptions:ApplyFrameAnchorPointInfos(frame, frame.FixedAnchorPointInfos, clearAllPoints)
    -- frame:SetFixedAnchorPointInfos(nil)
  end
  frame.IsAnchoredToMouse = value
end
function EasyOptions:UpdateFrameSize(frame, value, setWidth, setHeight)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  local widthCurrent, heightCurrent = frame:GetSize()
  if ((not setWidth) or (value == widthCurrent)) and ((not setHeight) or (value == heightCurrent)) then
    return
  end

  local width = EasyOptions:IIF(setWidth, value, widthCurrent)
  local height = EasyOptions:IIF(setHeight, value, heightCurrent)
  frame:SetSize(width, height)
end
function EasyOptions:UpdateFrameScale(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  if (value == frame:GetScale()) then
    return
  end

  frame:SetScale(value)

  -- Update the frame's position from the corresponding controls after the scale is changed so the frame's relative position on the screen is independent of the frame's scale.
  local framePositionControl = frame.FramePositionControl
  local framePositionOptionItem = frame.FramePositionOptionItem
  if (framePositionControl ~= nil) and (framePositionControl.UpdateFramePositionFromControls ~= nil) and (not EasyOptions:IsResettingOptionValuesOfOptionItemParent(framePositionOptionItem)) then
    local updateFramePositionOptionItem = (not EasyOptions.Current.IsBuildingOptionFrame)
    framePositionControl:UpdateFramePositionFromControls(updateFramePositionOptionItem)
  end
end
function EasyOptions:UpdateFrameStrata(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  if (value == frame:GetFrameStrata()) then
    return
  end

  frame:SetFrameStrata(value) -- "BACKGROUND", "LOW", "MEDIUM", "HIGH", "DIALOG", "FULLSCREEN", "FULLSCREEN_DIALOG", "TOOLTIP"
end
function EasyOptions:UpdateFrameLevel(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  if (value == frame:GetFrameLevel()) then
    return
  end

  frame:SetFrameLevel(value) -- 0 to 10000
end
function EasyOptions:UpdateFrameBackgroundColor(frame, value)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  if (frame.Background == nil) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to set the background color of the frame \"{1}\" because a standard background texture was not created for the frame.", EasyOptions:FormatName(frame.Name)))
    return
  end
  
  frame.Background:SetColorTexture(value[1], value[2], value[3], value[4] or 1.0)
end
function EasyOptions:SetFrameBackdropColorForLockedState(frame, isLocked)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  if (frame:IsMovable()) then
    frame:SetBackdropColor(0, 0, 0, 0.4)
  else
    frame:SetBackdropColor(0, 0, 0, 0)
  end
end
function EasyOptions:UpdateFramePositionOptionItemFromActualPosition(frame, printCoordinates)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  local control = frame.FramePositionControl
  local returnDisplayedFramePositions = true -- Store the displayed values in Options so the values in Options are always the same as the displayed values.
  local framePositionInfo = EasyOptions:GetPositionInfoFromFramePoint(frame, returnDisplayedFramePositions, nil)
  local allowUpdateControl = false
  EasyOptions:SetOptionValueFromControlInfo(control, framePositionInfo, allowUpdateControl)
  if (printCoordinates) then
    EasyOptions:Print(L("Frame Position = ({1}, {2})", string.format(EasyOptions.Fields.FramePositionRoundFormatString, framePositionInfo[3]), string.format(EasyOptions.Fields.FramePositionRoundFormatString, framePositionInfo[4])))
    -- EasyOptions:Print("Point, Relative Point: "..point..", "..relativePoint)
  end
end
function EasyOptions:GetPositionInfoFromFramePoint(frame, returnDisplayedFramePositions, pointIndex) -- {ChildAnchorPoint, ParentAnchorPoint, x, y}
  local positionInfoDefault = {"CENTER", "CENTER", 0, 0} -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
  if (frame == nil) then
    return positionInfoDefault
  end
  local pointIndexLocal = pointIndex or 1
  local point, relativeTo, relativePoint, x, y = frame:GetPoint(pointIndexLocal)
  if (point == nil) or (relativePoint == nil) or (x == nil) or (y == nil) then
    return positionInfoDefault
  end
  x = EasyOptions:Round(x, EasyOptions.Fields.FramePositionRoundIncrement)
  y = EasyOptions:Round(y, EasyOptions.Fields.FramePositionRoundIncrement)
  if (returnDisplayedFramePositions) then
    x, y = EasyOptions:GetDisplayedFramePositionsFromActualFramePositions(frame, x, y) -- Convert from actual offsets to displayed offsets.
  end
  return {point, relativePoint, x, y}
end
function EasyOptions:GetActualFramePositionsFromDisplayedFramePositions(frame, x, y) -- x, y
  local frameScale = EasyOptions:GetFrameScaleFromFrame(frame)
  local scaleFactor = EasyOptions:IIF(frameScale <= 0.0, 1.0, 1.0 / frameScale) -- IMPORTANT: This must be consistent with logic in EasyOptions:GetDisplayedFramePositionsFromActualFramePositions.
  return scaleFactor * x, scaleFactor * y
end
function EasyOptions:GetDisplayedFramePositionsFromActualFramePositions(frame, x, y) -- x, y
  local frameScale = EasyOptions:GetFrameScaleFromFrame(frame)
  local scaleFactor = EasyOptions:IIF(frameScale <= 0.0, 1.0, frameScale) -- IMPORTANT: This must be consistent with logic in EasyOptions:GetActualFramePositionsFromDisplayedFramePositions.
  return scaleFactor * x, scaleFactor * y
end
function EasyOptions:SetFramePosition(frame, childAnchorPoint, parentAnchorPoint, x, y, updateFramePositionControls, updateFramePositionOptionItem, printCoordinates)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  local optionItem = frame.FramePositionOptionItem
  local control = optionItem.Control
  local parentFrame = frame.ParentFrame
  if (parentFrame == nil) then
    parentFrame = UIParent
  end
  
  local point, relativeTo, relativePoint, xCurrent, yCurrent = frame:GetPoint(1)
  local roundIncrementForPositionChanged = 0.1 * EasyOptions.Fields.FramePositionRoundIncrement
  local hasPositionChanged = (childAnchorPoint ~= point) or (parentAnchorPoint ~= relativePoint) or (EasyOptions:Round(x, roundIncrementForPositionChanged) ~= EasyOptions:Round(xCurrent, roundIncrementForPositionChanged)) or (EasyOptions:Round(y, roundIncrementForPositionChanged) ~= EasyOptions:Round(yCurrent, roundIncrementForPositionChanged))
  if (not hasPositionChanged) then -- IMPORTANT: We verify that the position has changed in order to avoid a WoW API bug that can cause frame:GetPoint to return values for x and y that are slightly different than the values that were passed to frame:SetPoint.
    return
  end
  frame:ClearAllPoints()
  frame:SetPoint(childAnchorPoint, parentFrame, parentAnchorPoint, x, y)
  if (updateFramePositionControls) and (control ~= nil) and (control.UpdateControlsFromFrame ~= nil) then
    control:UpdateControlsFromFrame(false)
  end
  if (updateFramePositionOptionItem) then
    EasyOptions:UpdateFramePositionOptionItemFromActualPosition(frame, printCoordinates)
  end

  if (frame.IsAnchoredToMouse) and (frame.SetFixedAnchorPointInfos ~= nil) then
    local fixedAnchorPointInfo = EasyOptions:GetFrameAnchorPointInfo(childAnchorPoint, parentFrame, parentAnchorPoint, x, y)
    frame:SetFixedAnchorPointInfos({fixedAnchorPointInfo}) -- Update frame.FixedAnchorPointInfos.
  end
end
function EasyOptions:ResetFramePosition(frame)
  -- Validate frame.
  if (frame == nil) then
    return
  end
  
  local promptText = L("Reset the frame to its default position?")
  local uniqueID = "EasyOptions.ResetFramePosition"
  EasyOptions.Fields.FrameForResetFramePosition = frame -- IMPORTANT: We must store frame as a field. Otherwise, the WoW API will use the first frame that is passed to this method when the onAcceptHandler method is called by the WoW API for all subsequent calls to this method. The reason for this behavior is unknown. Storing the frame parameter as a local variable in this method does not fix the issue. It was verified during testing that the appropriate frame is passed to this method.
  local onAcceptHandler = function(userText)
    local framePositionControl = EasyOptions.Fields.FrameForResetFramePosition.FramePositionControl
    local framePositionInfo = EasyOptions:GetFramePositionInfoFromOptionPositionInfo(EasyOptions.Fields.FrameForResetFramePosition.DefaultPositionInfo)
    framePositionControl:SetFramePositionControlsFromFramePositionInfo(framePositionInfo, true)
    -- local updateFramePositionControls = true
    -- local updateFramePositionOptionItem = true
    -- local printCoordinates = false
    -- EasyOptions:SetFramePosition(EasyOptions.Fields.FrameForResetFramePosition, framePositionInfo[1], framePositionInfo[2], framePositionInfo[3], framePositionInfo[4], updateFramePositionControls, updateFramePositionOptionItem, printCoordinates)
    EasyOptions.Fields.FrameForResetFramePosition = nil
  end
  local onCancelHandler = nil
  EasyOptions:ShowDialogOkCancel(promptText, uniqueID, onAcceptHandler, onCancelHandler)
end
function EasyOptions:GetFrameSizeFromFrame(frame) -- width, height
  if (frame == nil) then
    return 0.0, 0.0
  end
  local width, height = frame:GetSize()
  return EasyOptions:Round(width, EasyOptions.Fields.FrameSizeRoundIncrement), EasyOptions:Round(height, EasyOptions.Fields.FrameSizeRoundIncrement)
end
function EasyOptions:GetFrameScaleFromFrame(frame)
  if (frame == nil) then
    return 1.0
  end
  return EasyOptions:Round(frame:GetScale(), EasyOptions.Fields.FrameScaleRoundIncrement)
end
function EasyOptions:GetFrameLevelFromFrame(frame)
  if (frame == nil) then
    return 0
  end
  return EasyOptions:Round(frame:GetFrameLevel(), EasyOptions.Fields.FrameLevelRoundIncrement)
end

function EasyOptions:GetOptionValueOrDefaultValue(optionName, defaultValue)
  -- See also EasyOptions:GetOptionValue.
  if (EasyOptions.Current.AddonDB == nil) then
    EasyOptions:PrintErrorMessageOnce("EasyOptions:GetOptionValueOrDefaultValue was called when EasyOptions.Current.AddonDB is nil.")
  end

  if (optionName == nil) then
    return defaultValue
  else
    return EasyOptions.Current.AddonDB[optionName]
  end
end

function EasyOptions:GetControlText(optionItem)
  if (optionItem.Text ~= nil) then
    return optionItem.Text
  else
    return ""
  end
end
function EasyOptions:GetControlLabelText(control, returnEmptyStringIfNil)
  -- Description: Returns the text of control.Label.
  local result = nil -- init
  if (control ~= nil) and (control.Label ~= nil) then
    result = control.Label:GetText()
  end
  if (returnEmptyStringIfNil) and (result == nil) then
    result = ""
  end
  return result
end
function EasyOptions:GetControlName(controlType, customControlName)
  if (customControlName ~= nil) then
    return customControlName
  end
  if (controlType == nil) then
    controlType = ""
  end
  if (EasyOptions.Current.OptionControlCounts == nil) then -- EasyOptions.Current.OptionControlCounts can be nil if this method is called after options are built for all of the addons.
    EasyOptions.Current.OptionControlCounts = {}
  end
  if (EasyOptions.Current.OptionControlCounts[controlType] == nil) then
    EasyOptions.Current.OptionControlCounts[controlType] = 1
  else
    EasyOptions.Current.OptionControlCounts[controlType] = EasyOptions.Current.OptionControlCounts[controlType] + 1
  end

  local count = EasyOptions.Current.OptionControlCounts[controlType]
  local controlName = ""
  if (EasyOptions.Current.AddonName ~= nil) and (EasyOptions.Current.AddonName ~= "") then
    controlName = EasyOptions.Current.AddonName.."."
  end
  controlName = controlName..controlType..EasyOptions:FormatInteger(count)
  -- EasyOptions:Print({"ControlName", controlType, count, controlName})
  return controlName
end
function EasyOptions:GetChildControlName(parentControl, childNameSuffix)
  if (parentControl == nil) then
    return nil
  end
  if (childNameSuffix == nil) then
    return nil
  end

  local parentName = parentControl.Name or parentControl:GetName()
  if (parentName == nil) then
    return nil
  end

  return parentName..childNameSuffix
end

function EasyOptions:GetNumberOfDimensionsOfTextBoxValue(textBoxType, delimiterInfo)
  if (delimiterInfo == nil) then
    return 0
  end
  
  local numberOfDimensionsMaxAllowed
  if (textBoxType == "ValueBox") then
    numberOfDimensionsMaxAllowed = 0 -- ValueBoxes can only support a single value.
  elseif (textBoxType == "MultiLineTextBox") then
    numberOfDimensionsMaxAllowed = 2 -- MultiLineTextBoxes can support 2D data.
  else
    numberOfDimensionsMaxAllowed = 1 -- TextBoxes can support 1D data.
  end
  
  return EasyOptions:GetNumberOfDimensionsOfDelimiterInfo(delimiterInfo, numberOfDimensionsMaxAllowed)
end
function EasyOptions:ConvertTextBoxValueToAppropriateNumberOfDimensions(value, textBoxType, delimiterInfo, allowNil)
  if (allowNil) and (value == nil) then
    return nil
  end
  
  local numberOfDimensions = EasyOptions:GetNumberOfDimensionsOfTextBoxValue(textBoxType, delimiterInfo)
  return EasyOptions:ConvertValueToNumberOfDimensions(value, numberOfDimensions)
end

function EasyOptions:GetDefaultControlMarginX()
  -- Description: Specifies the default margin inside of parent controls (e.g., a GroupBox).
  return 8 -- IMPORTANT: Modify the value of EasyOptions.Fields.DefaultWidthMaxAvailable_NoScrollBars if this value is changed.
end
function EasyOptions:GetDefaultControlMarginY()
  -- Description: Specifies the default margin inside of parent controls (e.g., a GroupBox).
  return 6
end
function EasyOptions:GetDefaultControlPaddingX()
  -- Description: Specifies the default distance between controls.
  return 8
end
function EasyOptions:GetDefaultControlPaddingY()
  -- Description: Specifies the default distance between controls.
  return 6
end
function EasyOptions:GetDefaultIndentX()
  -- Description: Specifies the default amount to indent child OptionItems.
  return 20
end
function EasyOptions:GetBlankLineRegionHeight()
  -- Description: Returns the default height of a blank line.
  return 16
end
function EasyOptions:GetOptionPanelSize() -- {width, height}
  -- Description: Specifies the size of the panel that contains the option item controls.
  return {650, 547} -- IMPORTNAT: This size effectively determines the position of the right and bottom edges of the frame that contains the option item controls. The dimensions of this size must be > 0. Also, this size determines the minimum range over which the scrollbar will scroll so if this size is larger than the size of scrollAreaPanel then the user will be able to scroll the scrollbar even if scrollAreaPanel is able to contain all of the controls in scrollAreaPanel.Child (i.e., the range of the scrollbar is math.max(scrollAreaPanel.Child:GetHeight(), [Maximum bottom position of all controls in scrollAreaPanel.Child])).
  -- return {641, 546} -- IMPORTNAT: This size effectively determines the position of the right and bottom edges of the frame that contains the option item controls. The dimensions of this size must be > 0. Also, this size determines the minimum range over which the scrollbar will scroll so if this size is larger than the size of scrollAreaPanel then the user will be able to scroll the scrollbar even if scrollAreaPanel is able to contain all of the controls in scrollAreaPanel.Child (i.e., the range of the scrollbar is math.max(scrollAreaPanel.Child:GetHeight(), [Maximum bottom position of all controls in scrollAreaPanel.Child])).
end
function EasyOptions:GetOptionPanelSizeExcludingMargins() -- {width, height}
  local size = EasyOptions:GetOptionPanelSize()
  return {size[1] - 2 * EasyOptions:GetDefaultControlMarginX(), size[2] - 2 * EasyOptions:GetDefaultControlMarginY()}
end
function EasyOptions:GetOptionPanelWidthExcludingMargins()
  return EasyOptions:GetOptionPanelSizeExcludingMargins()[1]
end
function EasyOptions:GetOptionPanelHeightExcludingMargins()
  return EasyOptions:GetOptionPanelSizeExcludingMargins()[2]
end
function EasyOptions:GetOptionPanelMiddle() -- {x, y}
  local positionOfColumn1 = EasyOptions:GetPositionOfColumn1Start()
  local size = EasyOptions:GetOptionPanelSize()
  return {positionOfColumn1[1] + 0.5 * size[1], positionOfColumn1[2] + 0.5 * size[2]}
end
function EasyOptions:GetPositionOfColumn1Start() -- {x, y}
  -- Description: Returns the default position for the first control in the options frame.
  local marginX = EasyOptions:GetDefaultControlMarginX()
  local marginY = EasyOptions:GetDefaultControlMarginY()
  return {marginX - 0, -marginY + 4}
end
function EasyOptions:GetPositionInfoOfColumn1Start()
  local position = EasyOptions:GetPositionOfColumn1Start()
  return {"TOPLEFT", "TOPLEFT", position[1], position[2]}
end
function EasyOptions:GetPositionInfoOfChildControlInContainerStart()
  local positionInfo = EasyOptions:GetPositionInfoOfColumn1Start()
  positionInfo[3] = positionInfo[3] + 4
  positionInfo[4] = positionInfo[4] - 5
  return positionInfo
end
function EasyOptions:GetPositionOfColumnStart(columnNumber, numberOfColumns) -- {x, y}
  -- Description: Returns the default position for the first control in the specified column when building the specified number of columns.
  local positionOfColumn1 = EasyOptions:GetPositionOfColumn1Start()
  local columnWidth = EasyOptions:GetWidthForNumberOfColumns(numberOfColumns)
  local paddingX = EasyOptions:GetDefaultControlPaddingX()
  local positionX = math.max(columnNumber - 1, 0) * (columnWidth + paddingX)
  return {positionX, positionOfColumn1[2]}
end
function EasyOptions:GetPositionInfoOfColumnStart(columnNumber, numberOfColumns)
  local positionInfo = EasyOptions:GetPositionInfoOfColumn1Start()
  local position = EasyOptions:GetPositionOfColumnStart(columnNumber, numberOfColumns)
  return {positionInfo[1], positionInfo[2], position[1], position[2]}
end
function EasyOptions:GetWidthForNumberOfColumns(numberOfColumns)
  -- Description: Returns the width that is available for each column within the options frame for the specified number of columns.
  local panelSizeExcludingMargins = EasyOptions:GetOptionPanelSizeExcludingMargins()
  local panelWidthExcludingMargins = panelSizeExcludingMargins[1]
  local paddingX = EasyOptions:GetDefaultControlPaddingX()
  local widthAvailable = math.max(panelWidthExcludingMargins - math.max(numberOfColumns - 1, 0) * paddingX, 0) -- Exclude the margin around the parent control and subtract the gaps between the columns.
  return widthAvailable / math.max(numberOfColumns, 1)
end

function EasyOptions:SetControlPointToFillWidth(control, parent, positionInfo, offsetLeft, offsetRight)
  local isValidPositionInfo = false -- init
  if (EasyOptions:IsLeftAlignedPositionInfo(positionInfo)) or (EasyOptions:IsMiddleAlignedPositionInfo(positionInfo)) then
    control:SetPoint("RIGHT", parent, "RIGHT", -(EasyOptions:GetDefaultControlMarginX() + offsetRight), 0) -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    isValidPositionInfo = true
  end
  if (EasyOptions:IsRightAlignedPositionInfo(positionInfo)) or (EasyOptions:IsMiddleAlignedPositionInfo(positionInfo)) then
    control:SetPoint("LEFT", parent, "LEFT", (EasyOptions:GetDefaultControlMarginX() + offsetLeft), 0) -- "LEFT", "RIGHT", "TOP", "BOTTOM", "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", "CENTER"
    isValidPositionInfo = true
  end
  if (not isValidPositionInfo) then
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has its anchor position set to \"{1}\", \"{2}\" which is not compatible with a width of \"{3}\".", EasyOptions:Format(positionInfo[1]), EasyOptions:Format(positionInfo[2]), EasyOptions.Fields.FillWidthFlag))
  end
end
function EasyOptions:GetValueIfIsFillWidthFlag(value, defaultValue)
  if (EasyOptions:IsFillWidthFlag(value, true)) then
    return defaultValue
  else
    return value
  end
end
function EasyOptions:IsFillWidthFlag(value, allowFillWidthFlag)
  if (allowFillWidthFlag ~= nil) and (not allowFillWidthFlag) then
    return false
  end
  if (value == EasyOptions.Fields.FillWidthFlag) then
    return true
  end
  if (value == nil) or (type(value) ~= "number") then
    return false
  end
  if (EasyOptions.Fields.AssumeNegativeWidthIsFillWidthFlag) and (value < 0) then
    return true
  end
  return false
end

function EasyOptions:GetFrameAnchorPointInfos(frame)
  -- Description: Returns all of the frame's anchor points.
  local length = frame:GetNumPoints()
  if (length == 0) then
    return nil
  end

  local point, relativeTo, relativePoint, offsetX, offsetY
  local result = {}
  for i = 1, length do
    result[i] = EasyOptions:GetFrameAnchorPointInfo(frame:GetPoint(i))
  end
  return result
end
function EasyOptions:GetFrameAnchorPointInfo(point, relativeTo, relativePoint, offsetX, offsetY)
  local info = {}
  info.point = point
  info.relativeTo = relativeTo
  info.relativePoint = relativePoint
  info.offsetX = offsetX
  info.offsetY = offsetY
  return info
end
function EasyOptions:ApplyFrameAnchorPointInfos(frame, anchorPointInfos, clearAllPoints)
  if (frame == nil) then
    return
  end

  if (clearAllPoints) then
    frame:ClearAllPoints()
  end

  local length = EasyOptions:GetLength(anchorPointInfos)
  local info
  for i = 1, length do
    info = anchorPointInfos[i]
    frame:SetPoint(info.point, info.relativeTo, info.relativePoint, info.offsetX, info.offsetY)
  end
end

function EasyOptions:CopyLayoutProperties(sourceFrame, destinationFrame, copySize, copyScale, copyAnchorPoints)
  -- Description: Copies the layout properties (i.e., Size, Scale, and AnchorPoints) of a source frame to a destination frame.
  if (copySize) then
    destinationFrame:SetSize(sourceFrame:GetSize())
  end
  if (copyScale) then
    destinationFrame:SetScale(sourceFrame:GetScale())
  end
  if (copyAnchorPoints) then
    destinationFrame:ClearAllPoints()
    local length = sourceFrame:GetNumPoints()
    local point, relativeTo, relativePoint, offsetX, offsetY
    for i = 1, length do
      point, relativeTo, relativePoint, offsetX, offsetY = sourceFrame:GetPoint(i)
      destinationFrame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
    end
  end
end
function EasyOptions:CopyLabelProperties(sourceLabel, destinationLabel, copyText, copyTextColor, copyFrameLayoutProperties)
  -- Description: Copies the properties of a source FontString to a destination FontString.
  -- Alias for search: EasyOptions:CopyFontStringProperties()
  destinationLabel:SetFontObject(sourceLabel:GetFontObject())
  destinationLabel:SetFont(sourceLabel:GetFont())
  destinationLabel:SetIndentedWordWrap(sourceLabel:GetIndentedWordWrap())
  destinationLabel:SetJustifyH(sourceLabel:GetJustifyH())
  destinationLabel:SetJustifyV(sourceLabel:GetJustifyV())
  destinationLabel:SetMaxLines(sourceLabel:GetMaxLines())
  destinationLabel:SetNonSpaceWrap(sourceLabel:CanNonSpaceWrap())
  destinationLabel:SetRotation(sourceLabel:GetRotation())
  destinationLabel:SetShadowColor(sourceLabel:GetShadowColor())
  destinationLabel:SetShadowOffset(sourceLabel:GetShadowOffset())
  destinationLabel:SetSpacing(sourceLabel:GetSpacing())
  destinationLabel:SetTextHeight(sourceLabel:GetLineHeight())
  destinationLabel:SetTextScale(sourceLabel:GetTextScale())
  destinationLabel:SetWordWrap(sourceLabel:CanWordWrap())
  if (copyText) then
    destinationLabel:SetText(sourceLabel:GetText())
  end
  if (copyTextColor) then
    destinationLabel:SetTextColor(sourceLabel:GetTextColor())
  end
  if (copyFrameLayoutProperties) then
    local copyFrameSize = true
    local copyFrameScale = true
    local copyFrameAnchorPoints = true
    EasyOptions:CopyLayoutProperties(sourceLabel, destinationLabel, copyFrameSize, copyFrameScale, copyFrameAnchorPoints)
  end
end
function EasyOptions:CreateCopyOfLabel(labelName, sourceLabel, control, drawLayer, copyText, copyTextColor, copyFrameLayoutProperties)
  local drawLayerLocal = drawLayer or sourceLabel:GetDrawLayer()
  local fontObjectName = nil
  local destinationLabel = control:CreateFontString(labelName, drawLayer, fontObjectName)
  EasyOptions:CopyLabelProperties(sourceLabel, destinationLabel, copyText, copyTextColor, copyFrameLayoutProperties)
  return destinationLabel
end

function EasyOptions:DisableHandler(control)
  -- Desaturate the control's text color and/or the control's label's text color.
  if (control.Label ~= nil) and ((control.EnableAutoSetLabelTextColorOnEnabledChanged == nil) or (control.EnableAutoSetLabelTextColorOnEnabledChanged)) then -- NOTE: We assume that control.EnableAutoSetLabelTextColorOnEnabledChanged is true by default.
    EasyOptions:SetToDisabledTextColor(control.Label)
  end
  if (control.OnEnabledChangedHandler ~= nil) then
    control:OnEnabledChangedHandler()
  end
  
  EasyOptions:UpdateIconTextureColor(control)
  if (control.SupportsDesaturationTexture) and (control.DesaturationTexture ~= nil) then
    control.DesaturationTexture:Show()
  end

  -- Update the enabled state of controls (if required).
  if (not EasyOptions.Current.IsBuildingOptionFrame) then -- NOTE: We don't update the enabled state of controls if EasyOptions.Current.IsBuildingOptionFrame is true because we assume that the enabled state of controls will be set to the appropriate value when each Option frame is built.
    local includeChildControls = false -- Don't update grandchild controls because we assume that each child control will update the enabled state of its children if appropriate.
    EasyOptions:UpdateEnabledStateOfChildControlsFromEnabledStateOfOwners(control, includeChildControls)
  end

  -- Hide the tooltip (if required).
  if (EasyOptions:IsTooltipOwner(control)) and (GameTooltip:IsShown()) then
    GameTooltip:Hide()
    -- EasyOptions.Fields.TooltipOwner = nil -- Do not clear EasyOptions.Fields.TooltipOwner until the mouse cursor leaves the control.
  end
end
function EasyOptions:EnableHandler(control)
  -- Restore the control's text color and/or the control's label's text color.
  if (control.Label ~= nil) and ((control.EnableAutoSetLabelTextColorOnEnabledChanged == nil) or (control.EnableAutoSetLabelTextColorOnEnabledChanged)) then -- NOTE: We assume that control.EnableAutoSetLabelTextColorOnEnabledChanged is true by default.
    EasyOptions:SetToDefaultTextColor(control.Label)
  end
  if (control.OnEnabledChangedHandler ~= nil) then
    control:OnEnabledChangedHandler()
  end
  
  EasyOptions:UpdateIconTextureColor(control)
  if (control.SupportsDesaturationTexture) and (control.DesaturationTexture ~= nil) then
    control.DesaturationTexture:Hide()
  end

  -- Update the enabled state of controls (if required).
  if (not EasyOptions.Current.IsBuildingOptionFrame) then -- NOTE: We don't update the enabled state of controls if EasyOptions.Current.IsBuildingOptionFrame is true because we assume that the enabled state of controls will be set to the appropriate value when each Option frame is built.
    local includeChildControls = false -- Don't update grandchild controls because we assume that each child control will update the enabled state of its children if appropriate.
    EasyOptions:UpdateEnabledStateOfChildControlsFromEnabledStateOfOwners(control, includeChildControls)
  end

  -- Show the tooltip (if required).
  if (control.OnEnterTooltipFunction ~= nil) and (EasyOptions:IsTooltipOwner(control)) and (not GameTooltip:IsShown()) then
    control:OnEnterTooltipFunction()
  end
end
function EasyOptions:UpdateEnabledStateOfControlsFromEnabledStateOfOwners(controls, includeChildControls)
  if (controls == nil) then
    return
  end

  local length = EasyOptions:GetLength(controls)
  local control
  local enabledAllowed
  for i = 1, length do
    control = controls[i]
    if (control.SetEnabledAllowed ~= nil) then
      enabledAllowed = EasyOptions:GetDesiredEnabledStateOfControlFromEnabledStateOfOwners(control)
      control:SetEnabledAllowed(enabledAllowed)
    end
    --
    if (includeChildControls) then
      EasyOptions:UpdateEnabledStateOfChildControlsFromEnabledStateOfOwners(control, includeChildControls)
    end
  end
end
function EasyOptions:UpdateEnabledStateOfChildControlsFromEnabledStateOfOwners(control, includeGrandchildControls)
  if (control == nil) then
    return
  end
  EasyOptions:UpdateEnabledStateOfControlsFromEnabledStateOfOwners(control.OwnedControls, includeGrandchildControls)
  EasyOptions:UpdateEnabledStateOfControlsFromEnabledStateOfOwners(control.Children, includeGrandchildControls)
end
function EasyOptions:GetDesiredEnabledStateOfControlFromEnabledStateOfOwners(control)
  local result = true -- init
  if (control == nil) then
    return result
  end
  
  local owner = control.Owner
  if (result) and (owner ~= nil) and (not EasyOptions:IsParentOf(control, owner, true)) then -- IMPORTANT: We must ignore the state of owner if control is a parent of owner (e.g., if control is a GroupBox, owner is a CheckBox, and the GroupBox contains the CheckBox) in order to prevent a possible stack overflow that could occur because this is a recursive method. Also, a control should never be allowed to disable its parent because doing so would prevent the user from being able to enable the control or its parent again because disabling a control's parent will also disable the control.
    local ownerState = EasyOptions:GetEnabledStateOfControl(owner) -- init
    if (EasyOptions:IsCheckBoxType(owner)) then
      ownerState = (ownerState) and (owner:GetValue())
    end
    result = (result) and (ownerState) and (EasyOptions:GetDesiredEnabledStateOfControlFromEnabledStateOfOwners(owner))
  end

  local parent = control.Parent
  if (result) and (parent ~= nil) then
    local parentState = EasyOptions:GetEnabledStateOfControl(parent) -- init
    result = (result) and (parentState) and (EasyOptions:GetDesiredEnabledStateOfControlFromEnabledStateOfOwners(parent))
  end
  
  return result
end
function EasyOptions:SupportsEnabledState(control)
  if (control == nil) then
    return false
  end
  
  if (control.SupportsEnabledState) or (EasyOptions:HasNativeSupportForEnabledState(control)) then
    return true
  end

  return false
end
function EasyOptions:GetEnabledStateOfControl(control)
  if (not EasyOptions:SupportsEnabledState(control)) then
    return true
  end
  return control:IsEnabled()
end
function EasyOptions:SetEnabledStateOfControl(enabled, control)
  if (not EasyOptions:SupportsEnabledState(control)) then
    return
  end
  
  if (not enabled) then
    control:Disable()
  else
    control:Enable()
  end
end
function EasyOptions:SetEnabledStateOfControls(enabled, controls)
  if (controls == nil) then
    return
  end

  local length = EasyOptions:GetLength(controls)
  local control
  for i = 1, length do
    control = controls[i]
    EasyOptions:SetEnabledStateOfControl(enabled, control)
  end
end
function EasyOptions:SetEnabledAllowedOfControl(enabledAllowed, control)
  if (not EasyOptions:SupportsEnabledState(control)) then
    return
  end

  control:SetEnabledAllowed(enabledAllowed)
end
function EasyOptions:SetEnabledAllowedOfControls(enabledAllowed, controls)
  if (controls == nil) then
    return
  end

  local length = EasyOptions:GetLength(controls)
  local control
  for i = 1, length do
    control = controls[i]
    EasyOptions:SetEnabledAllowedOfControl(enabled, control)
  end
end
function EasyOptions:AddSupportForEnabledState(control)
  if (control.SupportsEnabledState) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to add EnabledState support for the specified control because the control already supports that state."))
    return
  end
  control.SupportsEnabledState = true

  -- EnabledAllowed
  control.EnabledAllowed = true -- NOTE: The EnabledAllowed property is only intended to be set by EasyOptions via control:SetEnabledAllowed or EasyOptions:SetSetEnabledAllowedOfControl. External addons may use control:Enable, control:Disable, EasyOptions:SetEnabledStateOfControl, or EasyOptions:SetEnabledStateOfControls.
  control.SetEnabledAllowed = function(self, enabledAllowed)
    local control = self
    local isEnabledPrevious = control:IsEnabled() -- Store the current value.
    if (control.EnabledAllowed ~= enabledAllowed) then -- Determine whether the property changed.
      control.EnabledAllowed = enabledAllowed -- Set the property.
      if (control.OnEnabledAllowedChangedHandler ~= nil) then
        control:OnEnabledAllowedChangedHandler() -- Handle the property changed event.
      end
    end
    if (control:IsEnabled() ~= isEnabledPrevious) then -- Determine whether the value changed.
      control:HandleEnabledChanged()
    end
  end

  -- IsEnabled
  control.EnabledInternal = true -- NOTE: The EnabledAllowed property should only be modified by methods that are declared in this method.
  if (control.IsEnabled ~= nil) then -- Check to see if the control has native support for the IsEnabled method.
    control.EnabledInternal = control:IsEnabled() -- Initialize control.EnabledInternal to the control's current enabled state.
    control.IsEnabledBase = control.IsEnabled -- Store the base method with a different name so it can still be accessed after we overwrite the method.
  end
  control.IsEnabled = function(self) -- Override the method.
    local control = self
    return (control.EnabledInternal ~= false) and (control.EnabledAllowed ~= false) -- NOTE: We assume that the Enabled properties are true if they have not been assigned a value (i.e., they are set to nil).
  end
  control.HandleEnabledChanged = function(self)
    local control = self
    if (not control:IsEnabled()) then
      if (control.DisableBase ~= nil) then
        control:DisableBase() -- Call the base method.
      end
      EasyOptions:DisableHandler(control)
    else
      if (control.EnableBase ~= nil) then
        control:EnableBase() -- Call the base method.
      end
      EasyOptions:EnableHandler(control)
    end
    local includeGrandchildControls = false -- Don't update grandchild controls because we assume that each child control will update the enabled state of its children if appropriate.
    EasyOptions:UpdateEnabledStateOfChildControlsFromEnabledStateOfOwners(control, includeGrandchildControls)
  end

  -- SetEnabled
  control.SetEnabled = function(self, enabled)
    local control = self
    if (not enabled) then
      control:Disable()
    else
      control:Enable()
    end
  end

  -- Disable
  if (control.Disable ~= nil) then -- Check to see if the control has native support for the Disable method.
    control.DisableBase = control.Disable -- Store the base method with a different name so it can still be accessed after we overwrite the method.
  end
  control.Disable = function(self) -- Override the method.
    local control = self
    local isEnabledPrevious = control:IsEnabled() -- Store the current value.
    if (control.EnabledInternal ~= false) then -- Determine whether the property changed.
      control.EnabledInternal = false -- Set the property.
      if (control.OnEnabledInternalChangedHandler ~= nil) then
        control:OnEnabledInternalChangedHandler() -- Handle the property changed event.
      end
    end
    if (control:IsEnabled() ~= isEnabledPrevious) then -- Determine whether the value changed.
      control:HandleEnabledChanged()
    end
  end

  -- Enable
  if (control.Enable ~= nil) then -- Check to see if the control has native support for the Enable method.
    control.EnableBase = control.Enable -- Store the base method with a different name so it can still be accessed after we overwrite the method.
  end
  control.Enable = function(self) -- Override the method.
    local control = self
    local isEnabledPrevious = control:IsEnabled() -- Store the current value.
    if (control.EnabledInternal ~= true) then -- Determine whether the property changed.
      control.EnabledInternal = true -- Set the property.
      if (control.OnEnabledInternalChangedHandler ~= nil) then
        control:OnEnabledInternalChangedHandler() -- Handle the property changed event.
      end
    end
    if (control:IsEnabled() ~= isEnabledPrevious) then -- Determine whether the value changed.
      control:HandleEnabledChanged()
    end
  end
end
function EasyOptions:HasNativeSupportForEnabledState(control)
  local baseType = control.BaseType
  return (baseType == "Button") or (baseType == "CheckBox") or (baseType == "RadioButton") or (baseType == "TextBox") or (baseType == "Slider")
end
function EasyOptions:AddSupportForDesaturationTexture(control, textureLayer, paddingLeft, paddingRight, paddingTop, paddingBottom)
  -- Description: Adds a texture that overlays the specified control to cause the control to appear to be desaturated while the texture is shown.
  if (control.SupportsDesaturationTexture) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to add DesaturationTexture support for the specified control because the control already supports that state."))
    return
  end
  control.SupportsDesaturationTexture = true

  local textureName = EasyOptions:GetChildControlName(control, ".DesaturationTexture")
  local textureLayerLocal = textureLayer or "ARTWORK"
  local desaturationTexture = control:CreateTexture(textureName, textureLayerLocal)
  desaturationTexture:SetTexture("Interface\\AddOns\\EasyOptions\\Textures\\SolidColorAlpha255.tga")
  desaturationTexture:SetPoint("TOPLEFT", control, "TOPLEFT", paddingLeft, -paddingTop) -- NOTE: We set the TOPLEFT and BOTTOMRIGHT rather than the CENTER so this texture will automatically resize if the control is resized.
  desaturationTexture:SetPoint("BOTTOMRIGHT", control, "BOTTOMRIGHT", -paddingRight, paddingBottom)
  desaturationTexture:SetVertexColor(0.5, 0.5, 0.5, 0.5)
  desaturationTexture:EnableMouse(false)
  desaturationTexture:Hide()
  control.DesaturationTexture = desaturationTexture
end
function EasyOptions:CreateDisabledTextLabel(label, control, drawLayer, isVisible)
  if (control.DisabledTextLabel ~= nil) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to create a DisabledTextLabel for the specified control because the control already contains a DisabledTextLabel."))
    return
  end
  
  local labelName = EasyOptions:GetChildControlName(control, ".DisabledTextLabel")
  local copyText = true
  local copyTextColor = false
  local copyFrameLayoutProperties = true
  local disabledTextLabel = EasyOptions:CreateCopyOfLabel(labelName, label, control, drawLayer, copyText, copyTextColor, copyFrameLayoutProperties)
  disabledTextLabel:SetTextColor(0.5, 0.5, 0.5, 1.0)
  EasyOptions:SetVisibleState(disabledTextLabel, isVisible)

  control.DisabledTextLabel = disabledTextLabel
  return disabledTextLabel
end

function EasyOptions:HandleOptionValueChanged(addonName, optionName)
  if (not EasyOptions:IsManagedAddon(addonName)) then
    return
  end
  if (optionName == nil) then
    return
  end

  -- Raise events.
  local optionItem = EasyOptions:GetOptionItem(addonName, optionName)
  if (optionItem.OnOptionValueChangedHandler ~= nil) then
    optionItem:OnOptionValueChangedHandler()
  end
  --
  local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  if (addonInfo.OnOptionValueChangedHandler ~= nil) then
    addonInfo:OnOptionValueChangedHandler(optionName)
  end

  -- Update the displayed tooltip (if required).
  if (EasyOptions:ShouldRefreshTooltipAfterOptionValueChanged(optionItem)) then
    EasyOptions:RefreshTooltipAfterOptionValueChanged(optionItem)
  end
end
function EasyOptions:SetAreOptionValuesChanging(addonName, value)
  if (not EasyOptions:IsManagedAddon(addonName)) then
    return
  end

  -- Initialize (if required).
  if (EasyOptions.Fields.AreOptionValuesChanging[addonName] == nil) then
    EasyOptions.Fields.AreOptionValuesChanging[addonName] = false -- init
  end
  
  -- Determine whether the value has changed.
  if (EasyOptions.Fields.AreOptionValuesChanging[addonName] == value) then
    return
  end

  -- Call the appropriate method in the addon.
  local addonInfo = EasyOptions.Fields.AddonInfo[addonName] -- [AddonName] = {AddonName, AddonDB, OptionItems, OptionItemsDictionary, MainOptionPanelFrame, OptionPanelFrames, OptionControls, OptionControlCounts, OnOptionValueChangedHandler (Optional), OnOptionValuesChangingHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionValuesChangedHandler (Optional), OnOptionPanelFrameShowHandler (Optional), IsValidSaveDataVersionProvider (Optional)} -- Stores all data for each addon.
  EasyOptions.Fields.AreOptionValuesChanging[addonName] = value
  if (value) then
    if (addonInfo.OnOptionValuesChangingHandler ~= nil) then
      addonInfo:OnOptionValuesChangingHandler()
    end
  else
    if (addonInfo.OnOptionValuesChangedHandler ~= nil) then
      addonInfo:OnOptionValuesChangedHandler()
    end
  end
end
function EasyOptions:SetIsResettingOptionValues(addonName, value, optionItems)
  if (not EasyOptions:IsManagedAddon(addonName)) then
    return
  end

  EasyOptions.Fields.IsResettingOptionValues[addonName] = {value, optionItems} -- [AddonName] = [1] IsResetting, [2] OptionItems
end
function EasyOptions:IsResettingOptionValuesOfOptionItemParent(optionItem)
  if (optionItem == nil) then
    return false
  end

  local addonName = optionItem.AddonName
  if (not EasyOptions:IsManagedAddon(addonName)) then
    return false
  end

  local items = EasyOptions.Fields.IsResettingOptionValues[addonName] -- [AddonName] = [1] IsResetting, [2] OptionItems
  if (items == nil) then
    return false
  end

  local isResetting = items[1]
  local optionItems = items[2]
  if (not isResetting) then
    return false
  end
  if (optionItem.Parent ~= optionItems) then
    return false
  end
  return true
end

function EasyOptions:AddMinimapIcon(addonName, optionName, text, tooltipTitleText, tooltipText, iconFilePath, visible, onClickHandler) -- name, dataObject, minimapButton
  if (not EasyOptions:AddMinimapIcon_CanAddIcon(addonName, optionName)) then
    return nil, nil, nil
  end

  local name = addonName
  if (optionName ~= nil) and (optionName ~= "") then
    name = name.."."..optionName
  end
  if (EasyOptions.Libs.DBIcon:GetMinimapButton(name)) then
    EasyOptions:PrintErrorMessage(L("Unable to add a minimap button named \"{1}\" because a minimap button with that name already exists.", name))
    return nil, nil, nil
  end
  
  local addonNameLocal = addonName
  local textLocal = text -- text or addonName
  local tooltipTitleTextLocal = tooltipTitleText
  local tooltipTextLocal = tooltipText
  local onClickHandlerLocal = onClickHandler
  -- if (onClickHandler ~= nil) then
  --   onClickHandlerLocal = onClickHandler
  -- else
  --   onClickHandlerLocal = function(self)
  --     EasyOptions:ShowOptions(addonNameLocal)
  --   end
  --   tooltipTextLocal = tooltipTextLocal or L("Show options for {1}.", addonName)
  -- end

  -- Create the table to store the minimap icon data (if required).
  if (EasyOptionsMinimapIconSettingsDB[addonName] == nil) then
    EasyOptionsMinimapIconSettingsDB[addonName] = {}
  end
  if (EasyOptionsMinimapIconSettingsDB[addonName][name] == nil) then
    EasyOptionsMinimapIconSettingsDB[addonName][name] = {}
  end

  local dataObject = EasyOptions.Libs.DataBroker:NewDataObject(name, {
    type = "data source", 
    text = textLocal, 
    icon = iconFilePath, 
    OnClick = onClickHandlerLocal, 
  })

  EasyOptions.Libs.DBIcon:Register(name, dataObject, EasyOptionsMinimapIconSettingsDB[addonName][name])
  dataObject.Name = name
  dataObject.IsMinimapDataObject = true
  dataObject.MinimapButton = EasyOptions.Libs.DBIcon:GetMinimapButton(name)
  if (dataObject.MinimapButton ~= nil) then
    dataObject.MinimapButton.IsMinimapButton = true
  end

  dataObject.SetVisible = function(self, visible)
    local dataObject = self
    if (dataObject.MinimapButton.db == nil) then
      dataObject.MinimapButton.db = {}
    end
    dataObject.MinimapButton.db.hide = not visible -- We must set the database value that is used by EasyOptions.Libs.DBIcon because the database value is not restored when addons are loaded or after a Reload UI.
    if (visible) then
      EasyOptions.Libs.DBIcon:Show(dataObject.Name)
      -- dataObject.MinimapButton:Show()
    else
      EasyOptions.Libs.DBIcon:Hide(dataObject.Name)
      -- dataObject.MinimapButton:Hide()
    end
  end
  dataObject:SetVisible(visible)

  if (tooltipTextLocal ~= nil) and (tooltipTextLocal ~= "") and (dataObject.MinimapButton ~= nil) then
    local owner = dataObject.MinimapButton
    EasyOptions:SetTooltip(dataObject, owner, tooltipTitleTextLocal, tooltipTextLocal, nil, nil, nil)
  end
  return name, dataObject, dataObject.MinimapButton
end
function EasyOptions:AddMinimapIcon_CanAddIcon(addonName, optionName)
  local allowPrintMessage = true
  local allowShowDialog = false
  if (addonName == nil) or (addonName == "") then
    EasyOptions:PrintErrorMessageOnce(L("Unable to add a minimap icon because because the specified addon name is nil or an empty string."))
    return false
  end
  if (type(addonName) ~= "string") then
    EasyOptions:PrintErrorMessageOnce(L("Unable to add a minimap icon because the specified addon name is of type {1}. The addon name must be a string.", type(addonName)))
    return false
  end
  if (optionName ~= nil) and (type(optionName) ~= "string") then
    EasyOptions:PrintErrorMessageOnce(L("Unable to add a minimap icon because the specified option item name is of type {1}. The option item name must be nil or a string.", type(optionName)))
    return false
  end
  if (not EasyOptions:IsManagedAddon(addonName)) then
    EasyOptions:PrintErrorMessageOnce(L("Unable to add a minimap icon for the addon {1} because that addon is not managed by EasyOptions.", addonName))
    return false
  end
  return true
end

function EasyOptions:PrintErrorMessageWithOptionItemInfo(messageSuffix)
  local message = EasyOptions:GetErrorMessageWithOptionItemInfo(messageSuffix)
  EasyOptions:PrintErrorMessageOnce(message)
end
function EasyOptions:PrintErrorMessageWithOptionCategoryInfo(messageSuffix)
  local message = EasyOptions:GetErrorMessageWithOptionCategoryInfo(messageSuffix)
  EasyOptions:PrintErrorMessageOnce(message)
end
function EasyOptions:GetFrameOrPrintErrorMessageWithOptionItemInfoIfIsInvalidFrame(frameTypeName, frame, allowNil)
  -- Description: Prints an error message if the specified frame is invalid and returns a value that is appropriate to use as a frame, or nil if the specified frame is invalid.
  if (frame == nil) then
    if (not allowNil) then
      EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has its {1} entry set to nil.", frameTypeName))
    end
    return nil
  elseif (not EasyOptions:IsFrame(frame, false)) then
    EasyOptions:PrintErrorMessageWithOptionItemInfo(L("has its {1} entry set to a value that does not appear to be a frame.", frameTypeName))
    return nil
  else
    return frame
  end
end
function EasyOptions:GetErrorMessageWithOptionItemInfo(messageSuffix)
  local optionName = nil -- init
  if (EasyOptions.Current.OptionItem ~= nil) then
    optionName = EasyOptions.Current.OptionItem[1]
  end
  local optionCategoryName = EasyOptions:GetFullNameOfOptionItems(EasyOptions.Current.OptionItems)

  local message = ""
  if (EasyOptions.Current.OptionItems == nil) then
    if (not EasyOptions.Current.IsBuildingOptionFrame) then
      message = message..L("An item")
    else
      message = message..L("An unknown option item")
    end
  else
    if (EasyOptions.Current.OptionItemIndex == nil) or (EasyOptions.Current.OptionItemIndex == 0) then
      message = message..L("An unknown option item")
    elseif (optionName == nil) or (optionName == "") then
      message = message..L("The option item at index {1}", EasyOptions.Current.OptionItemIndex)
    else
      message = message..L("The option item \"{1}\" at index {2}", optionName, EasyOptions.Current.OptionItemIndex)
    end
    if (optionCategoryName ~= nil) and (optionCategoryName ~= "") then
      message = message..L(" in the {1} options table", optionCategoryName)
    end
  end

  local messageSuffixLocal = messageSuffix
  if (messageSuffixLocal == nil) or (messageSuffixLocal == "") then
    messageSuffixLocal = L("is in an invalid format.")
  end
  message = message.." "..messageSuffixLocal

  return message
end
function EasyOptions:GetErrorMessageWithOptionCategoryInfo(messageSuffix)
  local message = ""
  local optionCategoryName = EasyOptions:GetFullNameOfOptionItems(EasyOptions.Current.OptionItems)
  if (EasyOptions.Current.OptionItems == nil) or (optionCategoryName == nil) or (optionCategoryName == "") then
    message = message..L("An options table in an unknown option category")
  else
    message = message..L("The {1} options table", optionCategoryName)
  end

  local messageSuffixLocal = messageSuffix
  if (messageSuffixLocal == nil) or (messageSuffixLocal == "") then
    messageSuffixLocal = L("is in an invalid format.")
  end
  message = message.." "..messageSuffixLocal

  return message
end
