You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

446 lines
16 KiB

--[[
This menu system is intended to be used in place of the default DropDownMenu.
Instead of building menus on the fly, menus are pre-made tables with functions to fetch
run-time values.
To use:
1) Once per session, register a menu table with rematch:RegisterMenu("name",table)
2) If a petID or team key being acted on, rematch:SetMenuSubject(petID or whatever)
3) rematch:ShowMenu("name"[,"cursor"]) or ("name",anchorPoint,relativeTo,relativePoint,xoff,yoff)
Valid menu tags: title, hidden, indent, disable, subMenu, check, radio, value, icon, text,
stay, iconCoords, func, spacer, highlight, noPostFunc, tooltipTitle, tooltipBody,
disableReason
]]
local _,L = ...
local rematch = Rematch
local settings
local menus = {} -- table of registered menus, indexed by name
local menuPostFuncs = {} -- table of functions to run after the click of a registered menu item
local framePool = {} -- pool for menu frames in list form in order of level
local buttonPool = {} -- pool for menu buttons in list form
local menuLevel = nil -- level of menu we're on (index to framePool)
local subject = nil -- the object (usually petID or team key) a menu is acting on
local menuParent = nil -- the button/object that spawned the menu (or that menu is attached to)
-- a menu needs registered to add it to the menu table
-- menuName is an arbitrary name but should be unique to the table
function rematch:RegisterMenu(menuName,menuTable,func)
menus[menuName] = menuTable
menuPostFuncs[menuName] = func
end
-- returns the table created for menuName
function rematch:GetMenu(menuName) return menus[menuName] end
-- for functions that deal with a specific item (petID or team key), SetMenuSubject before ShowMenu
function rematch:SetMenuSubject(arg) subject=arg end
function rematch:GetMenuSubject() return subject end
function rematch:GetMenuParent() return menuParent end
-- returns true if a specific menu is open (ie "PetFilter") or any menu if none specified
function rematch:IsMenuOpen(menu)
if not framePool[1] then return end -- no menus were ever opened!
if not menu and framePool[1]:IsVisible() then return true end -- no menu specified, something is open
for _,frame in ipairs(framePool) do
if frame.menu==menu and frame:IsVisible() then
return true
end
end
end
-- if a value is a function, returns the result of the function; otherwise the value itself
local function getvalue(entry,variable)
if type(variable)=="function" then return variable(entry,subject) else return variable end
end
-- returns a menu frame for the menu level, creating one if needed
function rematch:GetMenuFrame(level,parent)
local frame,name
if not framePool[level] then -- create frame if it doesn't already exist
frame = CreateFrame("Frame",level==1 and "RematchMenu" or nil,nil,"RematchMenuFrameTemplate")
frame:SetID(level)
framePool[level] = frame
frame.Buttons = {}
frame:SetScript("OnHide",rematch.MenuFrameOnHide)
if level==1 then -- first level gets an OnUpdate and keyboard handler to capture ESC
frame:SetScript("OnUpdate",rematch.MenuFrameOnUpdate)
frame:EnableKeyboard(true)
frame:SetScript("OnKeyDown",function(self,key)
if key==GetBindingKey("TOGGLEGAMEMENU") then
self:Hide() -- hide menu but don't pass ESC along
self:SetPropagateKeyboardInput(false)
else -- ESC not hit, send it along
self:SetPropagateKeyboardInput(true)
end
end)
frame.elapsed = 0
end
end
frame = framePool[level]
frame:SetParent(parent)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
return frame
end
-- the checkbox/radio is one texture: Interface\Common\UI-DropDownRadioChecks
-- with checkbox textures in top half and radio textures in bottom half
-- and "on" texture on left and "off" texture on right
function rematch:MenuButtonSetChecked(button,isChecked,isRadio)
local offset = (isRadio and 0.5 or 0) + (isChecked and 0.25 or 0)
button.Check:SetTexCoord(offset,offset+0.25,0.5,0.75)
end
function rematch:MenuButtonSetEnabled(button,enabled)
if enabled then
button.Text:SetTextColor(1,1,1)
button.Icon:SetDesaturated(false)
button.Check:SetDesaturated(false)
button:Enable()
else
button.Text:SetTextColor(0.5,0.5,0.5)
button.Icon:SetDesaturated(true)
button.Check:SetDesaturated(true)
button:Disable()
end
end
-- if anchorPoint is "cursor", menu is anchored to the cursor
-- if keepWidgets is true, widgets (pet card, dialogs, etc) aren't hidden
function rematch:ShowMenu(name,anchorPoint,relativeTo,relativePoint,anchorXoff,anchorYoff,keepWidgets)
if not keepWidgets then
rematch:HideWidgets("HideMenu")
end
-- when called by a subMenu, anchorPoint and relativeTo are both frames, and relativePoint is level
local level = type(relativePoint)=="number" and relativePoint or 1
if level==1 then
if type(relativeTo)=="table" then
menuParent = relativeTo
else
menuParent = GetMouseFocus()
end
end
-- hide any menus at this level or deeper
for i=level,#framePool do
framePool[i]:Hide()
end
local frame = rematch:GetMenuFrame(level or 1,relativeTo or UIParent)
frame.Title:Hide()
frame.Shadow:SetPoint("TOPLEFT",3,-3)
-- special case for a notable npcs menu when npcs are not yet cached; make an attempt and carry on
if name=="TargetMenu" and not rematch:GetMenu("TargetMenu") then
rematch:CacheNpcIDs()
rematch.notablesCached = true -- may not be really cached but need menu built NOW
rematch:CreateTargetMenus()
end
local numItems = #menus[name]
local maxWidth = 32
local hasSubMenu -- becomes true if any item in this menu has a sub-menu
local yoff = -6
local index = 1
-- go through each entry and fill its button
for i,entry in ipairs(menus[name]) do
-- get a button (make one if needed)
local button = frame.Buttons[i]
if not button then
frame.Buttons[i] = CreateFrame("Button",nil,frame,"RematchMenuButtonTemplate")
button = frame.Buttons[i]
button:SetID(i)
button:SetScript("OnEnter",rematch.MenuButtonOnEnter)
button:SetScript("OnLeave",rematch.MenuButtonOnLeave)
button:SetScript("OnMouseDown",rematch.MenuButtonOnMouseDown)
button:SetScript("OnMouseUp",rematch.MenuButtonOnMouseUp)
button:SetScript("OnClick",rematch.MenuButtonOnClick)
end
-- wipe everything the button had before
button.Icon:Hide()
button.Check:Hide()
button.Arrow:Hide()
button.Text:SetText("")
local width = 0
local padding = getvalue(entry,entry.indent) or 0 -- amount of space to pad to left
local padright = 0 -- amount of space to pad to the right
local height = 20
button.entry = entry
if getvalue(entry,entry.hidden) then
button:Hide() -- menu item is hidden
else -- for non-hidden items
button:SetPoint("TOPLEFT",8,yoff)
if i==1 and entry.title then -- handle title
frame.Title:Show()
frame.Title.Text:SetText(getvalue(entry,entry.text))
local titleWidth = frame.Title.Text:GetStringWidth()+16
width = min(180,titleWidth) -- make titles 180 width at most unless further menu items widen menu
button:Hide() -- the actual button is hidden
frame.Shadow:SetPoint("TOPLEFT",3,-24)
else -- non-title button
-- spacer is a half-height gap with no visible button
if entry.spacer then
button:Hide()
height = 6
else
button:Show()
end
-- check or radio button
if getvalue(entry,entry.check) or getvalue(entry,entry.radio) then
button.Check:SetPoint("LEFT",padding-2,entry.radio and -1 or 0)
padding = padding + 19
button.isChecked = getvalue(entry,entry.value)
rematch:MenuButtonSetChecked(button,button.isChecked,entry.radio)
button.Check:Show()
end
-- icon
local icon = getvalue(entry,entry.icon)
if icon then
button.Icon:SetPoint("LEFT",padding,0)
padding = padding + 19
button.Icon:SetTexture(icon)
if entry.iconCoords then
button.Icon:SetTexCoord(unpack(entry.iconCoords))
else
button.Icon:SetTexCoord(0.085,0.915,0.085,0.915)
end
button.Icon:Show()
end
-- button text
button.Text:SetText(getvalue(entry,entry.text) or "")
button.Text:SetPoint("LEFT",padding,0)
width = button.Text:GetStringWidth()+2 -- little extra for "mouse down" shift
-- submenu arrow
if getvalue(entry,entry.subMenu) then
hasSubMenu = true
button.Arrow:Show()
button.Arrow:SetTexCoord(0,1,1,1,0,0,1,0) -- rotate arrow
end
-- disabled state
rematch:MenuButtonSetEnabled(button,not getvalue(entry,entry.disabled))
-- text highlight (enabled button will be SetTextColor(1,1,1) already)
if getvalue(entry,entry.highlight) then
button.Text:SetTextColor(1,0.82,0)
end
-- delete button will go here, add space to show it
if getvalue(entry,entry.deleteButton) then
padright = 18
end
-- space for edit button (all menus with an edit button must have a delete button too)
if getvalue(entry,entry.editButton) then
padright = 34
end
end
yoff = yoff - height
end
button.padding = padding
maxWidth = max(maxWidth,width+padding+padright+(hasSubMenu and 8 or 0))
end
-- hide buttons not used
for i=#menus[name]+1,#frame.Buttons do
frame.Buttons[i]:Hide()
end
-- make all buttons the same width as the maxWidth
for _,button in pairs(frame.Buttons) do
button:SetWidth(maxWidth)
end
frame:SetSize(maxWidth+16,abs(yoff)+6)
-- now position the frame
local uiScale = UIParent:GetEffectiveScale()
frame:ClearAllPoints()
if type(anchorPoint)=="table" then -- if this subMenu is being anchored to a menu button
if rematch.reverseMenuAnchor or (anchorPoint:GetRight()+maxWidth)*anchorPoint:GetEffectiveScale() > UIParent:GetRight()*uiScale then
frame:SetPoint("TOPRIGHT",anchorPoint,"TOPLEFT",-2,5)
rematch.reverseMenuAnchor = true
else
frame:SetPoint("TOPLEFT",anchorPoint,"TOPRIGHT",2,5)
end
elseif anchorPoint=="cursor" or not anchorPoint then -- if "cursor" or no anchor, to cursor
local x,y = GetCursorPosition()
uiScale = frame:GetEffectiveScale()
frame:SetPoint("TOPLEFT",UIParent,"BOTTOMLEFT",x/uiScale-4,y/uiScale+4)
rematch.reverseMenuAnchor = nil
elseif anchorPoint then -- or a defined anchor
frame:SetPoint(anchorPoint,relativeTo,relativePoint,anchorXoff,anchorYoff)
rematch.reverseMenuAnchor = nil
end
frame.menu = name
frame:Show()
end
-- shows or hides a menu as a toggle
function rematch:ToggleMenu(name,...)
PlaySound(856)
if rematch:IsMenuOpen(name) then
rematch:HideMenu()
else
rematch:ShowMenu(name,...)
end
end
function rematch:HideMenu()
if framePool[1] then
framePool[1]:Hide()
end
end
-- when any menu frame hides, hide any menus deeper than the one being hidden
function rematch:MenuFrameOnHide()
for i=self:GetID()+1,#framePool do
framePool[i]:Hide()
end
end
function rematch:MenuFrameOnUpdate(elapsed)
self.elapsed = self.elapsed + elapsed
if self.elapsed>0 then
local overMenu
for _,frame in ipairs(framePool) do
if MouseIsOver(frame) then
overMenu = true
end
end
if overMenu then
self.timeAway = 0
elseif self.timeAway then
self.timeAway = self.timeAway + self.elapsed
if self.timeAway>2 then
rematch:HideMenu()
self.timeAway = nil -- timer doesn't start until mouse goes over menu
end
end
self.elapsed = 0
end
end
function rematch:MenuButtonOnEnter()
self.Highlight:Show()
rematch.MenuFrameOnHide(self:GetParent()) -- hide any menus deeper than the one belonging to this button
-- if we entered a button with a subMenu, show it
if self.entry.subMenu then
local level = self:GetParent():GetID()
rematch:ShowMenu(getvalue(self.entry,self.entry.subMenu),self,self,level+1)
end
-- show tooltip if entry has one and/or reason it's disabled if menu item disabled
local tooltipTitle = self.entry.tooltipTitle
local tooltipBody = getvalue(self.entry,self.entry.tooltipBody)
local disabled = getvalue(self.entry,self.entry.disabled)
local disabledReason = getvalue(self.entry,self.entry.disabledReason)
if self.entry.tooltipTitle or tooltipBody or (disabled and disabledReason) then
-- if menu button is disabled and a disabledReason is given, append it to the tooltipBody
if disabled and disabledReason then
if not tooltipTitle and not tooltipBody then -- if tooltip is disabled but has no body or title, make reason the title
tooltipTitle = format("%s%s",rematch.hexRed,disabledReason)
tooltipBody = nil
else -- otherwise append reason to the end of the body (if one exists)
tooltipBody = format("%s%s%s%s",tooltipBody or "",tooltipBody and "\n\n" or "",rematch.hexRed,disabledReason)
end
end
rematch.ShowTooltip(self,tooltipTitle or getvalue(self.entry,self.entry.text),tooltipBody,self.entry.text==L["Help"])
end
-- if delete or edit+delete buttons are in entry, show the SideButtons
if self.entry.editButton or self.entry.deleteButton then
local sideButtons = RematchMenuSideButtons
sideButtons:SetParent(self)
sideButtons:SetPoint("RIGHT")
sideButtons:SetWidth(self.entry.editButton and 32 or 16)
sideButtons.Edit:SetShown(self.entry.editButton and true)
sideButtons:Show()
end
end
function rematch:MenuButtonOnLeave()
self.Highlight:Hide()
rematch:HideTooltip()
if not MouseIsOver(RematchMenuSideButtons) then
RematchMenuSideButtons:Hide()
end
end
function rematch:MenuButtonOnMouseDown()
if self:IsEnabled() then
self.Text:SetPoint("LEFT",self.padding+2,-1)
end
end
function rematch:MenuButtonOnMouseUp()
self.Text:SetPoint("LEFT",self.padding,0)
end
function rematch:MenuButtonOnClick()
PlaySound(856)
rematch.timeUIChanged = GetTime() -- to prevent hiding menu from HideWidgets in various panels
local entry = self.entry
if type(entry.func)=="function" then
entry.func(entry,subject,self.isChecked) -- run the func defined in the entry, passing itself and the subject
end
if not getvalue(entry,entry.stay) and not getvalue(entry,entry.check) and not getvalue(entry,entry.radio) and not entry.subMenu then
rematch:HideMenu() -- if not staying, hide menu
else -- we're staying, refresh menus
rematch:RefreshMenus()
end
if not entry.noPostFunc and menuPostFuncs[self:GetParent().menu] then
menuPostFuncs[self:GetParent().menu](entry)
end
end
-- goes through all visible menus and updates checks/radios, disables and highlights
function rematch:RefreshMenus()
for _,frame in ipairs(framePool) do
if frame:IsVisible() then
for _,button in ipairs(frame.Buttons) do
if button:IsVisible() then
local entry = button.entry
if entry.check or entry.radio then
button.isChecked = getvalue(entry,entry.value)
rematch:MenuButtonSetChecked(button,button.isChecked,entry.radio)
end
rematch:MenuButtonSetEnabled(button,not getvalue(entry,entry.disabled))
if getvalue(entry,entry.highlight) then
button.Text:SetTextColor(1,0.82,0)
end
end
end
end
end
end
-- onenter of delete or edit sidebuttons
function rematch:MenuSideButtonOnEnter()
self:GetParent():GetParent().Highlight:Show()
end
-- onleave of delete or edit sidebuttons
function rematch:MenuSideButtonOnLeave()
local parent = self:GetParent()
if not MouseIsOver(parent) or not parent:GetParent():IsVisible() then
parent:Hide()
parent:GetParent().Highlight:Hide()
end
end
-- onclick of delete or edit sidebuttons
function rematch:MenuSideButtonOnClick()
local entry = self:GetParent():GetParent().entry
if entry[self.func] then
entry[self.func](entry)
end
end