view Editor.lua @ 275:4957aeb0d3a4

fix up .hgtags
author Flick
date Wed, 11 May 2011 11:00:42 -0700
parents c27596828276
children 36a29870bf34
line wrap: on
line source
local addonName, addonTable = ...
local ReAction = addonTable.ReAction
local L = ReAction.L
local _G = _G
local wipe = wipe
local format = string.format
local InCombatLockdown = InCombatLockdown
local tfetch = addonTable.tfetch
local tbuild = addonTable.tbuild

local AceConfigReg = LibStub("AceConfigRegistry-3.0")
local AceConfigDialog = LibStub("AceConfigDialog-3.0")


local pointTable = {
  NONE        = " ",
  CENTER      = L["Center"], 
  LEFT        = L["Left"],
  RIGHT       = L["Right"],
  TOP         = L["Top"],
  BOTTOM      = L["Bottom"],
  TOPLEFT     = L["Top Left"],
  TOPRIGHT    = L["Top Right"],
  BOTTOMLEFT  = L["Bottom Left"],
  BOTTOMRIGHT = L["Bottom Right"],
}

local Editor = { 
  buttonHandlers = { }
}

function Editor:New()
  -- create new self
  self = setmetatable( { }, { __index = self } )

  self.barOptMap = setmetatable({},{__mode="v"})
  self.tmp = { }
  self.configID = "ReAction-Editor"

  self.gui = LibStub("AceGUI-3.0"):Create("Frame")
  
  local frame = self.gui.frame
  frame:SetClampedToScreen(true)
  frame:Hide()

  self.title = ("%s - %s"):format(L["ReAction"],L["Bar Editor"])
  self.gui:SetTitle(self.title)

  self.options = {
    type = "group",
    name = self.title,
    handler = self,
    childGroups = "select",
    args = {
      launchConfig = {
        type = "execute",
        name = L["Global Config"],
        desc = L["Opens ReAction global configuration settings panel"],
        func = function() 
            -- AceConfigDialog calls :Open() after every selection, making it
            -- generally not possible to cleanly close from a menu item.
            -- If you don't use a custom frame, you can use ACD:Close(), but since
            -- we're using a custom frame, we have to do the work of closing later in an
            -- OnUpdate script.
            ReAction:ShowOptions();
            frame:SetScript("OnUpdate",
              function()
                self:Close()
                frame:SetScript("OnUpdate",nil)
              end)
          end,
        order = 1
      },
      desc = {
        type = "description",
        name = L["Use the mouse to arrange and resize the bars on screen. Tooltips on bars indicate additional functionality."],
        order = 2,
      },
      hdr = {
        type = "header",
        name = L["Select Bar"],
        order = 3,
      },
      _new = {
        type = "group",
        name = L["New Bar..."],
        order = 4,
        args = { 
          desc = {
            type = "description",
            name = L["Choose a name, type, and initial grid for your new action bar:"],
            order = 1,
          },
          name = {
            type = "input",
            name = L["Bar Name"],
            desc = L["Enter a name for your new action bar"],
            get  = function() return self.tmp.barName or "" end,
            set  = function(info, val) self.tmp.barName = val end,
            order = 2,
          },
          type = {
            type = "select",
            name = L["Button Type"],
            get  = function() return self.tmp.barType or ReAction:GetDefaultBarType() or "" end,
            set  = function(info, val) 
                     local c = ReAction:GetDefaultBarConfig(val)
                     self.tmp.barType = val 
                     self.tmp.barSize = c.btnWidth or self.tmp.barSize
                     self.tmp.barRows = c.btnRows or self.tmp.barRows
                     self.tmp.barCols = c.btnColumns or self.tmp.barCols
                     self.tmp.barSpacing = c.spacing or self.tmp.barSpacing
                   end,
            values = "GetBarTypes",
            order = 3,
          },
          go = {
            type = "execute",
            name = L["Create Bar"],
            func = "CreateBar",
            order = 4,
          },
          grid = {
            type = "group",
            name = L["Button Grid"],
            inline = true,
            order = 5,
            args = {
              rows = {
                type = "range",
                name = L["Rows"],
                get  = function() return self.tmp.barRows or 1 end,
                set  = function(info, val) self.tmp.barRows = val end,
                width = "double",
                min = 1,
                max = 32,
                step = 1,
                order = 2,
              },
              cols = {
                type = "range",
                name = L["Columns"],
                get  = function() return self.tmp.barCols or 12 end,
                set  = function(info, val) self.tmp.barCols = val end,
                width = "double",
                min = 1, 
                max = 32,
                step = 1,
                order = 3,
              },
              sz = {
                type = "range",
                name = L["Size"],
                get  = function() return self.tmp.barSize or 36 end,
                set  = function(info, val) self.tmp.barSize = val end,
                width = "double",
                min = 10,
                max = 72,
                step = 1,
                order = 4,
              },
              spacing = {
                type = "range",
                name = L["Spacing"],
                get  = function() return self.tmp.barSpacing or 3 end,
                set  = function(info, val) self.tmp.barSpacing = val end,
                width = "double",
                min = 0,
                max = 24,
                step = 1,
                order = 5,
              }
            }
          }
        }
      }
    }
  }

  self.gui:SetCallback("OnClose", 
    function() 
      ReAction:SetConfigMode(false)
    end )

  AceConfigReg:RegisterOptionsTable(self.configID, self.options)
  AceConfigDialog:SetDefaultSize(self.configID, 700, 540)

  ReAction.RegisterCallback(self,"OnCreateBar")
  ReAction.RegisterCallback(self,"OnDestroyBar")
  ReAction.RegisterCallback(self,"OnRenameBar")

  self:RefreshBarOptions()

  return self
end


function Editor:Open(bar, ...)
  if bar then
    AceConfigDialog:SelectGroup(self.configID, self.barOptMap[bar:GetName()], ...)
  end
  AceConfigDialog:Open(self.configID,self.gui)
  self.gui:SetTitle(self.title)
end

function Editor:Close()
  if self.gui then
    self.gui:ReleaseChildren()
    self.gui:Hide()
  end
end

function Editor:Refresh()
  AceConfigReg:NotifyChange(self.configID)
end

function Editor:UpdateBarOptions(bar)
  local name = bar:GetName()
  local key  = self.barOptMap[name]
  local args = self.options.args

  if not key then
    -- AceConfig doesn't allow spaces, etc, in arg key names, and they must be
    -- unique strings. So generate a unique key (it can be whatever) for the bar
    local i = 1
    repeat
      key = ("bar%s"):format(i)
      i = i+1
    until args[key] == nil
    self.barOptMap[name] = key

    args[key] = { 
      type = "group",
      name = name,
      childGroups = "tab",
      order = i+100,
      args = {
        general = {
          type = "group",
          name = L["General"],
          order = 1,
          args = {
            name = {
              type = "input",
              name = L["Rename Bar"],
              get  = function() return bar:GetName() end,
              set  = function(info, value) return ReAction:RenameBar(bar, value) end,
              order = 1,
            },
            delete = {
              type = "execute",
              name = L["Delete Bar"],
              desc = function() return bar:GetName() end,
              confirm = true,
              func = function() ReAction:EraseBar(bar) end,
              order = 2
            },
            optionsHdr = {
              type = "header",
              name = "",
              order = 3,
            },
            clickDown = {
              type = "toggle",
              name = L["Activate on Down"],
              desc = L["Activate the button when the key or mouse button is pressed down instead of when it is released"],
              order = 4,
              width = "full",
              set  = function(info, value) bar:GetConfig().clickDown = value; ReAction:RebuildAll() end,
              get  = function() return bar:GetConfig().clickDown end,
            },
            alpha = {
              type = "range",
              name = L["Transparency"],
              get  = function() return bar:GetAlpha() end,
              set  = function(info, val) bar:SetAlpha(val) end,
              min = 0, 
              max = 1,
              isPercent = true,
              step = 0.01,
              bigStep = 0.05,
              order = 5,
            },
            anchor = {
              type = "group",
              name = L["Anchor"],
              inline = true,
              order = 6,
              args = {
                frame = {
                  type = "input",
                  name = L["Frame"],
                  desc = L["The frame that the bar is anchored to"],
                  get  = function() local _, f = bar:GetAnchor(); return f end,
                  set  = function(info, val) bar:SetAnchor(nil,val) end,
                  validate = function(info, name) 
                      if name then
                        local f = ReAction:GetBar(name)
                        if f then
                          return true
                        else
                          f = _G[name]
                          if f and type(f) == "table" and f.IsObjectType and f:IsObjectType("Frame") then
                            local _, explicit = f:IsProtected()
                            return explicit
                          end
                        end
                      end
                      return false
                    end,
                  width = "double",
                  order = 1
                },
                point = {
                  type = "select",
                  name = L["Point"],
                  desc = L["Anchor point on the bar frame"],
                  style = "dropdown",
                  get  = function() return bar:GetAnchor() end,
                  set  = function(info, val) bar:SetAnchor(val) end,
                  values = pointTable,
                  order = 2,
                },
                relativePoint = {
                  type = "select",
                  name = L["Relative Point"],
                  desc = L["Anchor point on the target frame"],
                  style = "dropdown",
                  get  = function() local p,f,r = bar:GetAnchor(); return r end,
                  set  = function(info, val) bar:SetAnchor(nil,nil,val) end,
                  values = pointTable,
                  order = 3,
                },
                x = {
                  type = "input",
                  pattern = "\-?%d+",
                  name = L["X offset"],
                  get = function() local p,f,r,x = bar:GetAnchor(); return ("%d"):format(x) end,
                  set = function(info,val) bar:SetAnchor(nil,nil,nil,val) end,
                  order = 4
                },
                y = {
                  type = "input",
                  pattern = "\-?%d+",
                  name = L["Y offset"],
                  get = function() local p,f,r,x,y = bar:GetAnchor(); return ("%d"):format(y) end,
                  set = function(info,val) bar:SetAnchor(nil,nil,nil,nil,val) end,
                  order = 5
                },
              },
            },
          },
        },
        buttonOpts = self:CreateButtonOptions(bar),
        stateOpts  = self:CreateStateOptions(bar)
      }
    }
  end

end

function Editor:CreateButtonOptions(bar)
  local buttonClass = bar:GetButtonClass()
  local classID = buttonClass:GetButtonTypeID()
  local handler = self.buttonHandlers[classID]

  if handler then
    local h = handler:New(bar)
    return h:GetOptions()
  end
end

function Editor:RefreshBarOptions()
  for name, key in pairs(self.barOptMap) do
    if not ReAction:GetBar(name) then
      self.barOptMap[name] = nil
      self.options.args[key] = nil
    end
  end
  for name, bar in ReAction:IterateBars() do
    self:UpdateBarOptions(bar)
  end
  self:Refresh()
end

function Editor:OnCreateBar(evt, bar)
  self:UpdateBarOptions(bar)
  self:Refresh()
end

function Editor:OnDestroyBar(evt, bar, name)
  local key = self.barOptMap[name]
  if key then
    self.barOptMap[name] = nil
    self.options.args[key] = nil
    self:Refresh()
  end
end

function Editor:OnRenameBar(evt, bar, oldname, newname)
  local key = self.barOptMap[oldname]
  if key then
    self.barOptMap[oldname], self.barOptMap[newname] = nil, key
    self.options.args[key].name = newname
    self:Refresh()
  end
end

local _scratch = { }
function Editor:GetBarTypes()
  wipe(_scratch)
  return ReAction:GetBarTypeOptions(_scratch)
end

function Editor:CreateBar()
  if self.tmp.barName and self.tmp.barName ~= "" then
    local bar = ReAction:CreateBar(self.tmp.barName, self.tmp.barType or ReAction:GetDefaultBarType(), self.tmp.barRows, self.tmp.barCols, self.tmp.barSize, self.tmp.barSpacing)
    if bar then
      AceConfigDialog:SelectGroup(self.configID, self.barOptMap[self.tmp.barName])
      self.tmp.barName = nil
    end
  end
end

-------------------------------
---- Action button handler ----
-------------------------------

do
  local ActionHandler = {
    buttonClass = ReAction.Button.Action,
    options = {
      pages = {
        name  = L["# Pages"],
        desc  = L["Use the Dynamic State tab to specify page transitions"],
        order = 1,
        type  = "range",
        min   = 1,
        max   = 10,
        step  = 1,
        get   = "GetNumPages",
        set   = "SetNumPages",
      },
      mindcontrol = {
        name = L["Mind Control Support"],
        desc = L["When possessing a target (e.g. via Mind Control), map the first 12 buttons of this bar to the possessed target's actions."],
        order = 2,
        type = "toggle",
        set = "SetMindControl",
        get = "GetMindControl",
      },
      vehicle = {
        name = L["Vehicle Support"],
        desc = L["When on a vehicle, map the first 6 buttons of this bar to the vehicle actions. The vehicle-exit button is mapped to the 7th button. Pitch controls are not supported."],
        order = 3,
        type = "toggle",
        get = "GetVehicle",
        set = "SetVehicle",
      },
      hideEmpty = {
        name = L["Hide Empty Buttons"],
        order = 4,
        type = "toggle",
        width = "full",
        get  = "GetHideEmpty",
        set  = "SetHideEmpty",
      },
      lockButtons = {
        name = L["Lock Buttons"],
        desc = L["Prevents picking up/dragging actions (use SHIFT to override this behavior)"],
        order = 5,
        width = "full",
        type = "toggle",
        get = "GetLockButtons",
        set = "SetLockButtons",
      },
      lockOnlyCombat = {
        name = L["Only in Combat"],
        desc = L["Only lock the buttons when in combat"],
        order = 6,
        width = "full",
        type = "toggle",
        disabled = "LockButtonsCombatDisabled",
        get = "GetLockButtonsCombat",
        set = "SetLockButtonsCombat",
      },
      actions = {
        name   = L["Edit Action IDs"],
        order  = 7,
        type   = "group",
        inline = true,
        args   = {
          method = {
            name   = L["Assign"],
            order  = 1,
            type   = "select",
            width  = "full",
            values = { [0] = L["Choose Method..."],
                       [1] = L["Individually"],
                       [2] = L["All at Once"], },
            get    = "GetActionEditMethod",
            set    = "SetActionEditMethod",
          },
          rowSelect = {
            name   = L["Row"],
            desc   = L["Rows are numbered top to bottom"],
            order  = 2,
            type   = "select",
            width  = "half",
            hidden = "IsButtonSelectHidden",
            values = "GetRowList",
            get    = "GetSelectedRow",
            set    = "SetSelectedRow",
          },
          colSelect = {
            name   = L["Col"],
            desc   = L["Columns are numbered left to right"],
            order  = 3,
            type   = "select",
            width  = "half",
            hidden = "IsButtonSelectHidden",
            values = "GetColumnList",
            get    = "GetSelectedColumn",
            set    = "SetSelectedColumn",
          },
          pageSelect = {
            name   = L["Page"],
            order  = 4,
            type   = "select",
            width  = "half",
            hidden = "IsPageSelectHidden",
            values = "GetPageList",
            get    = "GetSelectedPage",
            set    = "SetSelectedPage",
          },
          single = {
            name   = L["Action ID"],
            usage  = L["Specify ID 1-120"],
            order  = 5,
            type   = "input",
            width  = "half",
            hidden = "IsButtonSelectHidden",
            get    = "GetActionID",
            set    = "SetActionID",
            validate = "ValidateActionID",
          },
          multi = {
            name   = L["ID List"],
            usage  = L["Specify a comma-separated list of IDs for each button in the bar (in order). Separate multiple pages with semicolons (;)"],
            order  = 6,
            type   = "input",
            multiline = true,
            width  = "double",
            hidden = "IsMultiIDHidden",
            get    = "GetMultiID",
            set    = "SetMultiID",
            validate = "ValidateMultiID",
          },
        },
      },
    }
  }

  Editor.buttonHandlers[ActionHandler.buttonClass:GetButtonTypeID()] = ActionHandler

  local meta = { __index = ActionHandler }

  function ActionHandler:New( bar )
    return setmetatable(
      {
        bar = bar,
        config = bar:GetConfig(),
      }, 
      meta)
  end

  function ActionHandler:Refresh()
    self.buttonClass:SetupBar(self.bar)
  end

  function ActionHandler:UpdateButtonLock()
    self.buttonClass:SetButtonLock(self.bar, self.config.lockButtons, self.config.lockButtonsCombat)
  end

  function ActionHandler:GetLastButton()
    return self.bar:GetButton(self.bar:GetNumButtons())
  end

    -- options handlers
  function ActionHandler:GetOptions()
    return {
      type = "group",
      name = L["Action Buttons"],
      handler = self,
      order = 2,
      args = self.options
    }
  end

  function ActionHandler:SetHideEmpty(info, value)
    if value ~= self.config.hideEmpty then
      self.config.hideEmpty = value
      for _, b in self.bar:IterateButtons() do
        b:ShowGrid(not value)
      end
    end
  end

  function ActionHandler:GetHideEmpty()
    return self.config.hideEmpty
  end

  function ActionHandler:GetLockButtons()
    return self.config.lockButtons
  end

  function ActionHandler:SetLockButtons(info, value)
    self.config.lockButtons = value
    self:UpdateButtonLock()
  end

  function ActionHandler:GetLockButtonsCombat()
    return self.config.lockButtonsCombat
  end

  function ActionHandler:SetLockButtonsCombat(info, value)
    self.config.lockButtonsCombat = value
    self:UpdateButtonLock()
  end

  function ActionHandler:LockButtonsCombatDisabled()
    return not self.config.lockButtons
  end

  function ActionHandler:GetNumPages()
    return self.config.nPages
  end

  function ActionHandler:SetNumPages(info, value)
    self.config.nPages = value
    self:Refresh()
  end

  function ActionHandler:GetMindControl()
    return self.config.mindcontrol
  end

  function ActionHandler:SetMindControl(info, value)
    self.config.mindcontrol = value
    self:Refresh()
  end

  function ActionHandler:GetVehicle()
    return self.config.vehicle
  end

  function ActionHandler:SetVehicle(info, value)
    self.config.vehicle = value
    self:Refresh()
  end

  function ActionHandler:GetActionEditMethod()
    return self.editMethod or 0
  end

  function ActionHandler:SetActionEditMethod(info, value)
    self.editMethod = value
  end

  function ActionHandler:IsButtonSelectHidden()
    return self.editMethod ~= 1
  end

  function ActionHandler:GetRowList()
    local r,c = self.bar:GetButtonGrid()
    if self.rowList == nil or #self.rowList ~= r then
      local list = { }
      for i = 1, r do
        table.insert(list,i)
      end
      self.rowList = list
    end
    return self.rowList
  end

  function ActionHandler:GetSelectedRow()
    local r, c = self.bar:GetButtonGrid()
    local row = self.selectedRow or 1
    if row > r then
      row = 1
    end
    self.selectedRow = row
    return row
  end

  function ActionHandler:SetSelectedRow(info, value)
    self.selectedRow = value
  end

  function ActionHandler:GetColumnList()
    local r,c = self.bar:GetButtonGrid()
    if self.columnList == nil or #self.columnList ~= c then
      local list = { }
      for i = 1, c do
        table.insert(list,i)
      end
      self.columnList = list
    end
    return self.columnList
  end

  function ActionHandler:GetSelectedColumn()
    local r, c = self.bar:GetButtonGrid()
    local col = self.selectedColumn or 1
    if col > c then
      col = 1
    end
    self.selectedColumn = col
    return col
  end

  function ActionHandler:SetSelectedColumn(info, value)
    self.selectedColumn = value
  end

  function ActionHandler:IsPageSelectHidden()
    return self.editMethod ~= 1 or (self.config.nPages or 1) < 2
  end

  function ActionHandler:GetPageList()
    local n = self.config.nPages or 1
    if self.pageList == nil or #self.pageList ~= n then
      local p = { }
      for i = 1, n do
        table.insert(p,i)
      end
      self.pageList = p
    end
    return self.pageList
  end

  function ActionHandler:GetSelectedPage()
    local p = self.selectedPage or 1
    if p > (self.config.nPages or 1) then
      p = 1
    end
    self.selectedPage = p
    return p
  end

  function ActionHandler:SetSelectedPage(info, value)
    self.selectedPage = value
  end

  function ActionHandler:GetActionID()
    local row = self.selectedRow or 1
    local col = self.selectedColumn or 1
    local r, c = self.bar:GetButtonGrid()
    local n = (row-1) * c + col
    local btn = self.bar:GetButton(n)
    if btn then
      return tostring(btn:GetActionID(self.selectedPage or 1))
    end
  end

  function ActionHandler:SetActionID(info, value)
    local row = self.selectedRow or 1
    local col = self.selectedColumn or 1
    local r, c = self.bar:GetButtonGrid()
    local n = (row-1) * c + col
    local btn = self.bar:GetButton(n)
    if btn then
      btn:SetActionID(tonumber(value), self.selectedPage or 1)
    end
  end

  function ActionHandler:ValidateActionID(info, value)
    value = tonumber(value)
    if value == nil or value < 1 or value > 120 then
      return L["Specify ID 1-120"]
    end
    return true
  end

  function ActionHandler:IsMultiIDHidden()
    return self.editMethod ~= 2
  end

  function ActionHandler:GetMultiID()
    local p = { }
    for i = 1, self.config.nPages or 1 do
      local b = { }
      for _, btn in self.bar:IterateButtons() do
        table.insert(b, btn:GetActionID(i))
      end
      table.insert(p, table.concat(b,","))
    end
    return table.concat(p,";\n")
  end


  local function ParseMultiID(nBtns, nPages, s)
    if s:match("[^%d%s,;]") then
      return nil
    end
    local p = { }
    for list in s:gmatch("[^;]+") do
      local pattern = ("^%s?$"):format(("%s*(%d+)%s*,"):rep(nBtns))
      local ids = { list:match(pattern) }
      if #ids ~= nBtns then
        return nil
      end
      table.insert(p,ids)
    end
    if #p ~= nPages then
      return nil
    end
    return p
  end

  function ActionHandler:SetMultiID(info, value)
    local p = ParseMultiID(self.bar:GetNumButtons(), self.config.nPages or 1, value)
    for page, b in ipairs(p) do
      for button, id in ipairs(b) do
        self.bar:GetButton(button):SetActionID(id, page)
      end
    end
  end

  function ActionHandler:ValidateMultiID(info, value)
    local bad = L["Invalid action ID list string"]
    if value == nil or ParseMultiID(self.bar:GetNumButtons(), self.config.nPages or 1, value) == nil then
      return bad
    end
    return true
  end
end


----------------------------------
---- PetAction button handler ----
----------------------------------

do
  local PetHandler = { 
    buttonClass = ReAction.Button.PetAction,
  }

  Editor.buttonHandlers[PetHandler.buttonClass:GetButtonTypeID()] = PetHandler

  local meta = { __index = PetHandler }

  function PetHandler:New(bar)
    return setmetatable(
      {
        bar = bar,
        config = bar.config
      }, meta)
  end

  function PetHandler:GetLockButtons()
    return self.config.lockButtons
  end

  function PetHandler:SetLockButtons(info, value)
    self.config.lockButtons = value
    self.buttonClass:UpdateButtonLock(self.bar)
  end

  function PetHandler:GetLockButtonsCombat()
    return self.config.lockButtonsCombat
  end

  function PetHandler:SetLockButtonsCombat(info, value)
    self.config.lockButtonsCombat = value
    self.buttonClass:UpdateButtonLock(self.bar)
  end

  function PetHandler:LockButtonsCombatDisabled()
    return not self.config.lockButtons
  end

  function PetHandler:GetOptions()
    return {
      type = "group",
      name = L["Pet Buttons"],
      handler = self,
      order = 2,
      args = {
        lockButtons = {
          name = L["Lock Buttons"],
          desc = L["Prevents picking up/dragging actions (use SHIFT to override this behavior)"],
          order = 2,
          type = "toggle",
          get = "GetLockButtons",
          set = "SetLockButtons",
        },
        lockOnlyCombat = {
          name = L["Only in Combat"],
          desc = L["Only lock the buttons when in combat"],
          order = 3,
          type = "toggle",
          disabled = "LockButtonsCombatDisabled",
          get = "GetLockButtonsCombat",
          set = "SetLockButtonsCombat",
        },
      }
    }
  end
end


-------------------------------------
---- Vehicle Exit button handler ----
-------------------------------------

do
  local VExitHandler = { 
    buttonClass = ReAction.Button.VehicleExit,
  }

  Editor.buttonHandlers[VExitHandler.buttonClass:GetButtonTypeID()] = VExitHandler

  local meta = { __index = VExitHandler }

  function VExitHandler:New(bar)
    return setmetatable(
      {
        bar = bar,
      }, meta)
  end

  function VExitHandler:GetConfig()
    return self.bar:GetConfig()
  end

  function VExitHandler:GetPassengerOnly()
    return not self:GetConfig().withControls
  end

  function VExitHandler:SetPassengerOnly(info, value)
    self:GetConfig().withControls = not value
    self.buttonClass:UpdateRegistration(self.bar)
  end


  function VExitHandler:GetOptions()
    return {
      type = "group",
      name = L["Exit Vehicle"],
      handler = self,
      args = {
        passengerOnly = {
          name = L["Show only when passenger"],
          desc = L["Only show the button when riding as a passenger in a vehicle (no vehicle controls)"],
          order = 2,
          width = "double",
          type = "toggle",
          get = "GetPassengerOnly",
          set = "SetPassengerOnly",
        },
      }
    }
  end
end


------------------------------
--- Dynamic State options ----
------------------------------
do
  local ApplyStates   = ReAction.Bar.ApplyStates
  local CleanupStates = ReAction.Bar.CleanupStates
  local SetProperty   = ReAction.Bar.SetStateProperty
  local GetProperty   = ReAction.Bar.GetStateProperty

  -- pre-sorted by the order they should appear in
  local rules = {
    --  rule       fields
    { "stance",  { {battle = L["Battle Stance"]}, {defensive = L["Defensive Stance"]}, {berserker = L["Berserker Stance"]} } },
    { "form",    { {caster = L["Caster Form"]}, {bear = L["Bear Form"]}, {cat = L["Cat Form"]}, {tree = L["Tree of Life"]}, {moonkin = L["Moonkin Form"]} } },
    { "stealth", { {stealth = L["Stealth"]}, {nostealth = L["No Stealth"]}, {shadowdance = L["Shadow Dance"]} } },
    { "shadow",  { {shadowform = L["Shadowform"]}, {noshadowform = L["No Shadowform"]} } },
    { "demon",   { {demon = L["Demon Form"]}, {nodemon = L["No Demon Form"]} } },
    { "pet",     { {pet = L["With Pet"]}, {nopet = L["Without Pet"]} } },
    { "target",  { {harm = L["Hostile Target"]}, {help = L["Friendly Target"]}, {notarget = L["No Target"]} } },
    { "focus",   { {focusharm = L["Hostile Focus"]}, {focushelp = L["Friendly Focus"]}, {nofocus = L["No Focus"]} } },
    { "possess", { {possess = L["Mind Control"]} } },
    { "vehicle", { {vehicle = L["In a Vehicle"]} } },
    { "group",   { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } },
    { "combat",  { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } },
  }

  local ruleSelect = { }
  local ruleMap    = { }
  local optionMap  = setmetatable({},{__mode="k"})


  -- unpack rules table into ruleSelect and ruleMap
  for _, c in ipairs(rules) do
    local rule, fields = unpack(c)
    for _, field in ipairs(fields) do
      local key, label = next(field)
      table.insert(ruleSelect, label)
      table.insert(ruleMap, key)
    end
  end

  local stateOptions = {
    ordering = {
      name = L["Info"],
      order = 1,
      type = "group",
      args = {
        rename = {
          name = L["Name"],
          order = 1,
          type = "input",
          get  = "GetName",
          set  = "SetStateName",
          pattern = "^%w*$",
          usage = L["State names must be alphanumeric without spaces"],
        },
        delete = {
          name = L["Delete this State"],
          order = 2,
          type = "execute",
          func = "DeleteState",
          confirm = true,
        },
        ordering = {
          name = L["Evaluation Order"],
          desc = L["State transitions are evaluated in the order listed: Move a state up or down to change the order"],
          order = 3,
          type = "group",
          inline = true,
          args = {
            up = {
              name  = L["Up"],
              order = 1,
              type  = "execute",
              width = "half",
              func  = "MoveStateUp",
            },
            down = {
              name  = L["Down"],
              order = 2,
              type  = "execute",
              width = "half",
              func  = "MoveStateDown",
            }
          }
        }
      }
    },
    properties = {
      name = L["Properties"],
      order = 2,
      type = "group",
      args = { 
        desc = {
          name = L["Set the properties for the bar when in this state"],
          order = 1,
          type = "description"
        },
        page = {
          name     = L["Show Page #"],
          order    = 11,
          type     = "select",
          width    = "half",
          disabled = "IsPageDisabled",
          hidden   = "IsPageHidden",
          values   = "GetPageValues",
          set      = "SetProp",
          get      = "GetPage",
        },
        hide = {
          name = L["Hide Bar"],
          order = 90,
          width = "full",
          type = "toggle",
          set  = "SetProp",
          get  = "GetProp",
        },
        --[[ BROKEN
        keybindState = {
          name  = L["Override Keybinds"],
          desc  = L["Set this state to maintain its own set of keybinds which override the defaults when active"],
          order = 91,
          type  = "toggle",
          set   = "SetProp",
          get   = "GetProp",
        }, ]]

        anchorEnable = {
          name  = L["Reposition"],
          order = 111,
          type  = "toggle",
          set   = "SetProp",
          get   = "GetProp",
        },
        anchorGroup = {
          name  = L["Position"],
          order = 112,
          type  = "group",
          inline = true,
          disabled = "GetAnchorDisabled",
          args = {
            anchorFrame = {
              name   = L["Anchor Frame"],
              order  = 1,
              type   = "select",
              values = "GetAnchorFrames",
              set    = "SetAnchorFrame",
              get    = "GetAnchorFrame",
            },
            anchorPoint = {
              name  = L["Point"],
              order = 2,
              type  = "select",
              values = pointTable,
              set   = "SetAnchorPointProp",
              get   = "GetAnchorPointProp",
            },
            anchorRelPoint = {
              name  = L["Relative Point"],
              order = 3,
              type  = "select",
              values = pointTable,
              set   = "SetAnchorPointProp",
              get   = "GetAnchorPointProp",
            },
            anchorX = {
              name  = L["X Offset"],
              order = 4,
              type  = "range",
              min   = -100,
              max   = 100,
              step  = 1,
              set   = "SetProp",
              get   = "GetProp",
            },
            anchorY = {
              name  = L["Y Offset"],
              order = 5,
              type  = "range",
              min   = -100,
              max   = 100,
              step  = 1,
              set   = "SetProp",
              get   = "GetProp",
            },
          },
        },

        enableScale = {
          name  = L["Set New Scale"],
          order = 121,
          type  = "toggle",
          set   = "SetProp",
          get   = "GetProp",
        },
        scaleGroup = {
          name  = L["Scale"],
          order = 122,
          type  = "group",
          inline = true,
          disabled = "GetScaleDisabled",
          args = {
            scale = {
              name  = L["Scale"],
              order = 1,
              type  = "range",
              min   = 0.25,
              max   = 2.5,
              step  = 0.05,
              isPercent = true,
              set   = "SetProp",
              get   = "GetScale",
            },
          },
        },

        enableAlpha = {
          name  = L["Set Transparency"],
          order = 131,
          type  = "toggle",
          set   = "SetProp",
          get   = "GetProp",
        },
        alphaGroup = {
          name  = L["Transparency"],
          order = 132,
          type  = "group",
          inline = true,
          disabled = "GetAlphaDisabled",
          args = {
            alpha = {
              name  = L["Transparency"],
              order = 1,
              type  = "range",
              min   = 0,
              max   = 1,
              step  = 0.01,
              bigStep = 0.05,
              isPercent = true,
              set   = "SetProp",
              get   = "GetAlpha",
            },
          },
        },
      },
      plugins = { }
    },
    rules = {
      name   = L["Rule"],
      order  = 3,
      type   = "group",
      args   = {
        mode = {
          name   = L["Select this state"],
          order  = 2,
          type   = "select",
          style  = "dropdown",
          values = { 
            default = L["by default"], 
            any = L["when ANY of these"], 
            all = L["when ALL of these"], 
            custom = L["via custom rule"],
            keybind = L["via keybinding"],
          },
          set    = "SetType",
          get    = "GetType",
        },
        clear = {
          name     = L["Clear All"],
          order    = 3,
          type     = "execute",
          hidden   = "GetClearAllDisabled",
          disabled = "GetClearAllDisabled",
          func     = "ClearAllConditions",
        },
        inputs = {
          name     = L["Conditions"],
          order    = 4,
          type     = "multiselect",
          hidden   = "GetConditionsDisabled",
          disabled = "GetConditionsDisabled",
          values   = ruleSelect,
          set      = "SetCondition",
          get      = "GetCondition",
        },
        custom = {
          name = L["Custom Rule"],
          order = 5,
          type = "input",
          multiline = true,
          hidden = "GetCustomDisabled",
          disabled = "GetCustomDisabled",
          desc = L["Syntax like macro rules: see preset rules for examples"],
          set  = "SetCustomRule",
          get  = "GetCustomRule",
          validate = "ValidateCustomRule",
        },
        keybind = {
          name = L["Keybinding"],
          order = 6,
          inline = true,
          hidden = "GetKeybindDisabled",
          disabled = "GetKeybindDisabled",
          type = "group",
          args = {
            desc = {
              name = L["Invoking a state keybind toggles an override of all other transition rules."],
              order = 1,
              type = "description",
            },
            keybind = {
              name = L["State Hotkey"],
              desc = L["Define an override toggle keybind"],
              order = 2,
              type = "keybinding",
              set  = "SetKeybind",
              get  = "GetKeybind",
            },
          },
        },
      },
    },
  }

  local StateHandler = { }
  local meta         = { __index = StateHandler }

  function StateHandler:New( bar, opts )
    local self = setmetatable(
      { 
        bar = bar 
      }, 
      meta )

    function self:GetName()
      return opts.name
    end

    function self:SetName(name)
      opts.name = name
    end

    function self:GetOrder()
      return opts.order
    end

    -- get reference to states table: even if the bar
    -- name changes the states table ref won't
    self.states = tbuild(bar:GetConfig(), "states")
    self.state  = tbuild(self.states, opts.name)

    opts.order = self:GetRuleField("order")
    if opts.order == nil then
      -- add after the highest
      opts.order = 100
      for _, state in pairs(self.states) do
        local x = tonumber(tfetch(state, "rule", "order"))
        if x and x >= opts.order then
          opts.order = x + 1
        end
      end
      self:SetRuleField("order",opts.order)
    end

    return self
  end

  -- helper methods

  function StateHandler:SetRuleField( key, value, ... )
    tbuild(self.state, "rule", ...)[key] = value
  end

  function StateHandler:GetRuleField( ... )
    return tfetch(self.state, "rule", ...)
  end

  function StateHandler:FixAll( setkey )
    -- if multiple selections in the same group are chosen when 'all' is selected,
    -- keep only one of them. If changing the mode, the first in the fields list will 
    -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set,
    -- it will be retained.
    local notified = false
    if self:GetRuleField("type") == "all" then
      for _, c in ipairs(rules) do
        local rule, fields = unpack(c)
        local once = false
        if setkey then
          for idx, field in ipairs(fields) do
            if next(field) == setkey then
              once = true
            end
          end
        end
        for idx, field in ipairs(fields) do
          local key = next(field)
          if self:GetRuleField("values",key) then
            if once and key ~= setkey then
              self:SetRuleField(key,false,"values")
              if not setkey and not notified then
                ReAction:UserError(L["Warning: one or more incompatible rules were turned off"])
                notified = true
              end
            end
            once = true
          end
        end
      end
    end
  end

  function StateHandler:GetNeighbors()
    local before, after
    for k, v in pairs(self.states) do
      local o = tonumber(tfetch(v, "rule", "order"))
      if o and k ~= self:GetName() then
        local obefore = tfetch(self.states,before,"rule","order")
        local oafter  = tfetch(self.states,after,"rule","order")
        if o < self:GetOrder() and (not obefore or obefore < o) then
          before = k
        end
        if o > self:GetOrder() and (not oafter or oafter > o) then
          after = k
        end
      end
    end
    return before, after
  end

  function StateHandler:SwapOrder( a, b )
    -- do options table
    local args = optionMap[self.bar].args
    args[a].order, args[b].order = args[b].order, args[a].order
    -- do profile
    a = tbuild(self.states, a, "rule")
    b = tbuild(self.states, b, "rule")
    a.order, b.order = b.order, a.order
  end

  -- handler methods 

  function StateHandler:GetProp( info )
    -- gets property of the same name as the options arg
    return GetProperty(self.bar, self:GetName(), info[#info])
  end

  function StateHandler:SetProp( info, value )
    -- sets property of the same name as the options arg
    SetProperty(self.bar, self:GetName(), info[#info], value)
  end

  function StateHandler:DeleteState()
    if self.states[self:GetName()] then
      self.states[self:GetName()] = nil
      ApplyStates(self.bar)
    end
    optionMap[self.bar].args[self:GetName()] = nil
  end

  function StateHandler:SetStateName(info, value)
    -- check for existing state name
    if self.states[value] then
      ReAction:UserError(format(L["State named '%s' already exists"],value))
      return
    end
    local args = optionMap[self.bar].args
    local name = self:GetName()
    self.states[value], args[value], self.states[name], args[name] = self.states[name], args[name], nil, nil
    self:SetName(value)
    ApplyStates(self.bar)
    ReAction:ShowEditor(self.bar, moduleID, value)
    end

  function StateHandler:MoveStateUp()
    local before, after = self:GetNeighbors()
    if before then
      self:SwapOrder(before, self:GetName())
      ApplyStates(self.bar)
    end
  end

  function StateHandler:MoveStateDown()
    local before, after = self:GetNeighbors()
    if after then
      self:SwapOrder(self:GetName(), after)
      ApplyStates(self.bar)
    end
  end

  function StateHandler:GetAnchorDisabled()
    return not GetProperty(self.bar, self:GetName(), "anchorEnable")
  end

  function StateHandler:IsPageDisabled()
    local n = self.bar:GetConfig().nPages or 1
    return not (n > 1)
  end

  function StateHandler:IsPageHidden()
    return not self.bar:GetConfig().nPages
  end

  function StateHandler:GetPageValues()
    if not self._pagevalues then
      self._pagevalues = { }
    end
    local n = self.bar:GetConfig().nPages
      -- cache the results
    if self._npages ~= n then
      self._npages = n
      wipe(self._pagevalues)
      for i = 1, n do
        self._pagevalues["page"..i] = i
      end
    end
    return self._pagevalues
  end

  function StateHandler:GetPage(info)
    return self:GetProp(info) or 1
  end

  function StateHandler:GetAnchorFrames(info)
    self._anchorframes = self._anchorframes or { }
    table.wipe(self._anchorframes)

    table.insert(self._anchorframes, "UIParent")
    for name, bar in ReAction:IterateBars() do
      table.insert(self._anchorframes, bar:GetFrame():GetName())
    end
    return self._anchorframes
  end

  function StateHandler:GetAnchorFrame(info)
    local value = self:GetProp(info)
    for k,v in pairs(self._anchorframes) do
      if v == value then
        return k
      end
    end
  end

  function StateHandler:SetAnchorFrame(info, value)
    local f = _G[self._anchorframes[value]]
    if f then
      self.bar:SetFrameRef("anchor-"..self:GetName(), f)
      self:SetProp(info, f:GetName())
    end
  end

  function StateHandler:SetAnchorPointProp(info, value)
    self:SetProp(info, value ~= "NONE" and value or nil)
  end

  function StateHandler:GetAnchorPointProp(info)
    return self:GetProp(info) or "NONE"
  end

  function StateHandler:GetScale(info)
    return self:GetProp(info) or 1.0
  end

  function StateHandler:GetScaleDisabled()
    return not GetProperty(self.bar, self:GetName(), "enableScale")
  end

  function StateHandler:GetAlpha(info)
    return self:GetProp(info) or 1.0
  end

  function StateHandler:GetAlphaDisabled()
    return not GetProperty(self.bar, self:GetName(), "enableAlpha")
  end

  function StateHandler:SetType(info, value)
    self:SetRuleField("type", value)
    self:FixAll()
    ApplyStates(self.bar)
  end

  function StateHandler:GetType()
    return self:GetRuleField("type")
  end

  function StateHandler:GetClearAllDisabled()
    local t = self:GetRuleField("type")
    return not( t == "any" or t == "all" or t == "custom")
  end

  function StateHandler:ClearAllConditions()
    local t = self:GetRuleField("type")
    if t == "custom" then
      self:SetRuleField("custom","")
    elseif t == "any" or t == "all" then
      self:SetRuleField("values", {})
    end
    ApplyStates(self.bar)
  end

  function StateHandler:GetConditionsDisabled()
    local t = self:GetRuleField("type")
    return not( t == "any" or t == "all")
  end

  function StateHandler:SetCondition(info, key, value)
    self:SetRuleField(ruleMap[key], value or nil, "values")
    if value then
      self:FixAll(ruleMap[key])
    end
    ApplyStates(self.bar)
  end

  function StateHandler:GetCondition(info, key)
    return self:GetRuleField("values", ruleMap[key]) or false
  end

  function StateHandler:GetCustomDisabled()
    return self:GetRuleField("type") ~= "custom"
  end

  function StateHandler:SetCustomRule(info, value)
    self:SetRuleField("custom",value)
    ApplyStates(self.bar)
  end

  function StateHandler:GetCustomRule()
    return self:GetRuleField("custom") or ""
  end

  function StateHandler:ValidateCustomRule(info, value)
    local s = value:gsub("%s","") -- remove all spaces
    -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
    repeat
      if s == "" then
        return true
      end
      local c, r = s:match("(%b[])(.*)")
      if c == nil and s and #s > 0 then
        return format(L["Invalid custom rule '%s': each clause must appear within [brackets]"],value or "")
      end
      s = r
    until c == nil
    return true
  end

  function StateHandler:GetKeybindDisabled()
    return self:GetRuleField("type") ~= "keybind"
  end

  function StateHandler:GetKeybind()
    return self:GetRuleField("keybind")
  end

  function StateHandler:SetKeybind(info, value)
    if value and #value == 0 then
      value = nil
    end
    self:SetRuleField("keybind",value)
    ApplyStates(self.bar)
  end

  local function CreateStateOptions(bar, name)
    local opts = { 
      type = "group",
      name = name,
      childGroups = "tab",
      args = stateOptions
    }

    opts.handler = StateHandler:New(bar,opts)

    return opts
  end

  function Editor:CreateStateOptions(bar)
    local private = { }
    local states = tbuild(bar:GetConfig(), "states")
    local options = {
      name = L["Dynamic State"],
      type = "group",
      order = -1,
      childGroups = "tree",
      disabled = InCombatLockdown,
      args = {
        __desc__ = {
          name = L["States are evaluated in the order they are listed"],
          order = 1,
          type = "description",
        },
        __new__ = {
          name = L["New State..."],
          order = 2,
          type = "group",
          args = {
            name = {
              name = L["State Name"],
              desc = L["Set a name for the new state"],
              order = 1,
              type = "input",
              get = function() return private.newstatename or "" end,
              set = function(info,value) private.newstatename = value end,
              pattern = "^%w*$",
              usage = L["State names must be alphanumeric without spaces"],
            },
            create = {
              name = L["Create State"],
              order = 2,
              type = "execute",
              func = function ()
                  local name = private.newstatename
                  if states[name] then
                    ReAction:UserError(format(L["State named '%s' already exists"],name))
                  else
                    -- TODO: select default state options and pass as final argument
                    states[name] = { }
                    optionMap[bar].args[name] = CreateStateOptions(bar,name)
                    ReAction:ShowEditor(bar, "stateOpts", name)
                    private.newstatename = ""
                  end
                end,
              disabled = function()
                  local name = private.newstatename or ""
                  return #name == 0 or name:find("%W")
                end,
            }
          }
        }
      }
    }
    for name, config in pairs(states) do
      options.args[name] = CreateStateOptions(bar,name)
    end
    optionMap[bar] = options
    return options
  end
end


---- Export to ReAction ----
function ReAction:ShowEditor(bar, ...)
  if InCombatLockdown() then
    self:UserError(L["ReAction config mode disabled during combat."])
  else
    self.editor = self.editor or Editor:New()
    self.editor:Open(bar, ...)
    self:SetConfigMode(true)
  end
end

function ReAction:CloseEditor()
  if self.editor then
    self.editor:Close()
  end
end

function ReAction:RefreshEditor()
  if self.editor then
    self.editor:RefreshBarOptions()
  end
end