--[[ 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