local Libra = LibStub("Libra") local Type, Version = "Dropdown", 13 if Libra:GetModuleVersion(Type) >= Version then return end Libra.modules[Type] = Libra.modules[Type] or {} local Dropdown = Libra.modules[Type] Dropdown.Prototype = Dropdown.Prototype or CreateFrame("Frame") Dropdown.MenuPrototype = Dropdown.MenuPrototype or setmetatable({}, {__index = Dropdown.Prototype}) Dropdown.FramePrototype = Dropdown.FramePrototype or setmetatable({}, {__index = Dropdown.Prototype}) Dropdown.objects = Dropdown.objects or {} Dropdown.listData = Dropdown.listData or {} Dropdown.hookedLists = Dropdown.hookedLists or {} Dropdown.hookedButtons = Dropdown.hookedButtons or {} Dropdown.secureButtons = Dropdown.secureButtons or {} Dropdown.secureBin = Dropdown.secureBin or {} local menuMT = {__index = Dropdown.MenuPrototype} local frameMT = {__index = Dropdown.FramePrototype} local Prototype = Dropdown.Prototype local MenuPrototype = Dropdown.MenuPrototype local FramePrototype = Dropdown.FramePrototype local objects = Dropdown.objects local listData = Dropdown.listData local function setHeight() end local function constructor(self, type, parent, name) local dropdown if type == "Menu" then -- adding a SetHeight dummy lets us use a simple table instead of a frame, no side effects noticed so far dropdown = setmetatable({}, menuMT) dropdown:SetDisplayMode("MENU") dropdown.SetHeight = setHeight dropdown.xOffset = 0 dropdown.yOffset = 0 end if type == "Frame" then name = name or Libra:GetWidgetName(self.name) dropdown = setmetatable(CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate"), frameMT) dropdown:SetWidth(115) dropdown.label = dropdown:CreateFontString(name.."Label", "BACKGROUND", "GameFontNormalSmall") dropdown.label:SetPoint("BOTTOMLEFT", dropdown, "TOPLEFT", 16, 3) end objects[dropdown] = true return dropdown end local methods = { Refresh = UIDropDownMenu_Refresh, } for k, v in pairs(methods) do Prototype[k] = v end function Prototype:AddButton(info, level) info.owner = self if info.icon and not info.iconOnly then -- hack to properly increase button width for icon when .iconOnly is not set info.padding = (info.padding or 0) + 10 end self.displayMode = self._displayMode self.selectedName = self._selectedName self.selectedValue = self._selectedValue self.selectedID = self._selectedID UIDropDownMenu_AddButton(info, level) self.displayMode = nil self.selectedName = nil self.selectedValue = nil self.selectedID = nil end function Prototype:ToggleMenu(value, anchorName, xOffset, yOffset, menuList, level, ...) ToggleDropDownMenu(level, value, self, anchorName, xOffset, yOffset, menuList, ...) end function Prototype:RebuildMenu(level) level = level or 1 if self:IsMenuShown(level) then -- hiding a menu will also hide all deeper level menus, so we'll check which ones are open and restore them afterwards local maxLevel for i = level, UIDROPDOWNMENU_MENU_LEVEL do if _G["DropDownList"..i]:IsShown() then maxLevel = i else break end end self:HideMenu(level) for i = level, maxLevel do local listData = listData[i] -- set .rebuild to indicate that we don't want to reset the scroll offset on the next ToggleDropDownMenu self.rebuild = true self:ToggleMenu(listData.value, listData.anchorName, listData.xOffset, listData.yOffset, listData.menuList, i, listData.button, listData.autoHideDelay) end end end function Prototype:HideMenu(level) if UIDropDownMenu_GetCurrentDropDown() == self then HideDropDownMenu(level) end end function Prototype:CloseMenus(level) if UIDropDownMenu_GetCurrentDropDown() == self then CloseDropDownMenus(level) end end function Prototype:IsMenuShown(level) level = level or 1 local listFrame = _G["DropDownList"..level] return UIDropDownMenu_GetCurrentDropDown() == self and listFrame and listFrame:IsShown() end function Prototype:SetSelectedName(name, useValue) self._selectedName = name self._selectedValue = nil self._selectedID = nil self.selectedName = name self:Refresh(useValue) self.selectedName = nil end function Prototype:SetSelectedValue(value, useValue) self._selectedValue = value self._selectedName = nil self._selectedID = nil self.selectedValue = value self:Refresh(useValue) self.selectedValue = nil end function Prototype:SetSelectedID(id, useValue) self._selectedID = id self._selectedName = nil self._selectedValue = nil self.selectedID = id self:Refresh(useValue) self.selectedID = nil end function Prototype:GetSelectedName() return self._selectedName end function Prototype:GetSelectedValue() return self._selectedValue end function Prototype:GetSelectedID() if self._selectedID then return self._selectedID else -- If no explicit selectedID then try to send the id of a selected value or name for i=1, UIDROPDOWNMENU_MAXBUTTONS do local button = _G["DropDownList"..UIDROPDOWNMENU_MENU_LEVEL.."Button"..i] -- See if checked or not if self:GetSelectedName() then if button:GetText() == self:GetSelectedName() then return i end elseif self:GetSelectedValue() then if button.value == self:GetSelectedValue() then return i end end end end end function Prototype:SetDisplayMode(mode) self._displayMode = mode end local menuMethods = { Toggle = Prototype.ToggleMenu, Rebuild = Prototype.RebuildMenu, Hide = Prototype.HideMenu, Close = Prototype.CloseMenus, IsShown = Prototype.IsMenuShown, } for k, v in pairs(menuMethods) do MenuPrototype[k] = v end local frameMethods = { Enable = UIDropDownMenu_EnableDropDown, Disable = UIDropDownMenu_DisableDropDown, IsEnabled = UIDropDownMenu_IsEnabled, JustifyText = UIDropDownMenu_JustifyText, SetButtonWidth = UIDropDownMenu_SetButtonWidth, SetText = UIDropDownMenu_SetText, GetText = UIDropDownMenu_GetText, } for k, v in pairs(frameMethods) do FramePrototype[k] = v end local setWidth = Prototype.SetWidth function FramePrototype:SetWidth(width, padding) _G[self:GetName().."Middle"]:SetWidth(width) local defaultPadding = 25 if padding then setWidth(self, width + padding) _G[self:GetName().."Text"]:SetWidth(width) else setWidth(self, width + defaultPadding + defaultPadding) _G[self:GetName().."Text"]:SetWidth(width - defaultPadding) end self.noResize = 1 end function FramePrototype:SetLabel(text) self.label:SetText(text) end function FramePrototype:SetEnabled(enable) if enable then self:Enable() else self:Disable() end end local numShownButtons local function update(level) local scroll = listData[level].scroll for i = 1, UIDROPDOWNMENU_MAXBUTTONS do local button = _G["DropDownList"..level.."Button"..i] local _, _, _, x, y = button:GetPoint() local y = -((button:GetID() - 1 - scroll) * UIDROPDOWNMENU_BUTTON_HEIGHT) - UIDROPDOWNMENU_BORDER_HEIGHT button:SetPoint("TOPLEFT", x, y) button:SetShown(i > scroll and i <= (numShownButtons + scroll)) end Dropdown.scrollButtons[level].up:SetShown(scroll > 0) Dropdown.scrollButtons[level].down:SetShown(scroll < _G["DropDownList"..level].numButtons - numShownButtons) end local function scroll(self, delta) local level = self:GetID() local listData = listData[level] delta = (type(delta) == "number" and delta or self.delta) if IsShiftKeyDown() then delta = delta * (numShownButtons - 1) end listData.scroll = listData.scroll - (type(delta) == "number" and delta or self.delta) listData.scroll = min(listData.scroll, (self.numButtons or self:GetParent().numButtons) - numShownButtons) listData.scroll = max(listData.scroll, 0) update(level) end local scrollScripts = { OnEnter = function(self) UIDropDownMenu_StopCounting(self:GetParent()) end, OnLeave = function(self) UIDropDownMenu_StartCounting(self:GetParent()) end, OnMouseDown = function(self) self.texture:SetPoint("CENTER", 1, -1) end, OnMouseUp = function(self) self.texture:SetPoint("CENTER") end, OnHide = function(self) self.texture:SetPoint("CENTER") -- explicitly hide so that they are hidden for unmanaged dropdowns self:Hide() end, } local function createScrollButton(listFrame) local level = listFrame:GetID() local button = CreateFrame("Button", nil, listFrame) button:SetSize(16, 16) button:SetScript("OnClick", scroll) for script, handler in pairs(scrollScripts) do button:SetScript(script, handler) end button:SetID(level) button.texture = button:CreateTexture() button.texture:SetSize(16, 16) button.texture:SetPoint("CENTER") button.texture:SetTexture([[Interface\Calendar\MoreArrow]]) return button end local function createScrollButtons(listFrame) local scrollUp = listFrame.scrollUp or createScrollButton(listFrame) scrollUp:SetPoint("TOP") scrollUp.delta = 1 scrollUp.texture:SetTexCoord(0, 1, 1, 0) listFrame.scrollUp = scrollUp local scrollDown = listFrame.scrollDown or createScrollButton(listFrame) scrollDown:SetPoint("BOTTOM") scrollDown.delta = -1 listFrame.scrollDown = scrollDown end Dropdown.scrollButtons = Dropdown.scrollButtons or setmetatable({}, { __index = function(self, level) local listFrame = _G["DropDownList"..level] createScrollButtons(listFrame) self[level] = { up = listFrame.scrollUp, down = listFrame.scrollDown, } return self[level] end, }) function Dropdown:ToggleDropDownMenuHook(level, value, dropdownFrame, anchorName, xOffset, yOffset, menuList, button, autoHideDelay) level = level or 1 if level ~= 1 then dropdownFrame = dropdownFrame or UIDROPDOWNMENU_OPEN_MENU end local listFrameName = "DropDownList"..level local listFrame = _G[listFrameName] if not objects[dropdownFrame] then return end if dropdownFrame and dropdownFrame._displayMode == "MENU" then _G[listFrameName.."Backdrop"]:Hide() _G[listFrameName.."MenuBackdrop"]:Show() end -- store all parameters per level so we can use them to rebuild the menu listData[level] = listData[level] or {} local listData = listData[level] listData.value = value listData.anchorName = anchorName listData.xOffset = xOffset listData.yOffset = yOffset listData.menuList = menuList listData.button = button listData.autoHideDelay = autoHideDelay numShownButtons = dropdownFrame.numShownButtons or floor((UIParent:GetHeight() - UIDROPDOWNMENU_BORDER_HEIGHT * 2) / UIDROPDOWNMENU_BUTTON_HEIGHT) local scrollable = numShownButtons < listFrame.numButtons if scrollable then -- make scrollable listData.scroll = listData.scroll or 0 if not dropdownFrame.rebuild then listData.scroll = 0 end listFrame:SetScript("OnMouseWheel", scroll) listFrame:SetHeight((numShownButtons * UIDROPDOWNMENU_BUTTON_HEIGHT) + (UIDROPDOWNMENU_BORDER_HEIGHT * 2)) if listFrame:GetTop() > GetScreenHeight() then local point, anchorFrame, relativePoint, x, y = listFrame:GetPoint() local offTop = (GetScreenHeight() - listFrame:GetTop())-- / listFrame:GetScale() listFrame:SetPoint(point, anchorFrame, relativePoint, x, y + offTop) end update(level) else if listFrame:GetTop() > GetScreenHeight() then local point, anchorFrame, relativePoint, x, y = listFrame:GetPoint() local offTop = (GetScreenHeight() - listFrame:GetTop())-- / listFrame:GetScale() listFrame:SetPoint(point, anchorFrame, relativePoint, x, y + offTop) end listFrame:SetScript("OnMouseWheel", nil) self.scrollButtons[level].up:Hide() self.scrollButtons[level].down:Hide() end dropdownFrame.rebuild = nil end if not Dropdown.hookToggleDropDownMenu then hooksecurefunc("ToggleDropDownMenu", function(...) Dropdown:ToggleDropDownMenuHook(...) end) Dropdown.hookToggleDropDownMenu = true end function Dropdown:AddButtonHook(info, level) if not objects[UIDropDownMenu_GetCurrentDropDown()] then return end local listFrameName = "DropDownList"..(level or 1) local listFrame = _G[listFrameName] local button = _G[listFrameName.."Button"..(listFrame.numButtons)] button.onEnter = info.onEnter button.onLeave = info.onLeave button.tooltipLines = info.tooltipLines if info.attributes and not InCombatLockdown() then local secureButton = self.secureBin[1] tremove(Dropdown.secureBin, 1) -- since this is a separate button, we need to set the disabled state on it too secureButton:SetEnabled(not info.disabled) secureButton:SetParent(button) secureButton:SetAllPoints() secureButton:Show() -- clear existing attributes for attribute in pairs(secureButton.attributes) do secureButton:SetAttribute(attribute, nil) secureButton.attributes[attribute] = nil end for attribute, value in pairs(info.attributes) do secureButton:SetAttribute(attribute, value) secureButton.attributes[attribute] = true end tinsert(Dropdown.secureButtons, secureButton) end end if not Dropdown.hookAddButton then hooksecurefunc("UIDropDownMenu_AddButton", function(...) Dropdown:AddButtonHook(...) end) Dropdown.hookAddButton = true end local function onEnter(self) if self.onEnter then self:onEnter() elseif self.tooltipLines and self.tooltipTitle then GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:AddLine(self.tooltipTitle, HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b) if self.tooltipText then for line in self.tooltipText:gmatch("[^\n]+") do GameTooltip:AddLine(line) end end GameTooltip:Show() end end local function onLeave(self) if self.onLeave then self:onLeave() end end local function invisibleButtonOnEnter(self) local parent = self:GetParent() if parent.onEnter or parent.tooltipWhileDisabled then onEnter(parent) end end local function invisibleButtonOnLeave(self) local parent = self:GetParent() if parent.onLeave or parent.tooltipWhileDisabled then onLeave(parent) end end function Dropdown:HideListHook(self) if not InCombatLockdown() then for i = #Dropdown.secureButtons, 1, -1 do -- hide secure buttons attached to this list frame local button = Dropdown.secureButtons[i] if button:GetParent():GetParent() == self then Dropdown:DismissSecureButton(button) tremove(Dropdown.secureButtons, i) end end end if objects[UIDropDownMenu_GetCurrentDropDown()] then self:SetScript("OnMouseWheel", nil) end end local function listOnHide(self) Dropdown:HideListHook(self) end function Dropdown:CreateFramesHook(numLevels, numButtons) for level = 1, numLevels do if not self.hookedLists[level] then _G["DropDownList"..level]:HookScript("OnHide", listOnHide) self.hookedLists[level] = true end self.hookedButtons[level] = self.hookedButtons[level] or {} for i = 1, numButtons do if not self.hookedButtons[level][i] then local button = _G["DropDownList"..level.."Button"..i] button:HookScript("OnEnter", onEnter) button:HookScript("OnLeave", onLeave) button.invisibleButton:HookScript("OnEnter", invisibleButtonOnEnter) button.invisibleButton:HookScript("OnLeave", invisibleButtonOnLeave) self.hookedButtons[level][i] = true end end end end if not Dropdown.hookCreateFrames then Dropdown:CreateFramesHook(UIDROPDOWNMENU_MAXLEVELS, UIDROPDOWNMENU_MAXBUTTONS) hooksecurefunc("UIDropDownMenu_CreateFrames", function(...) Dropdown:CreateFramesHook(...) end) Dropdown.hookCreateFrames = true end -- script handlers to mimic regular dropdown button behaviour local scripts = { PreClick = function(self) local parent = self:GetParent() parent:GetScript("OnClick")(parent) end, OnMouseDown = function(self) self:GetParent():SetButtonState("PUSHED") end, OnMouseUp = function(self) self:GetParent():SetButtonState("NORMAL") end, OnEnter = function(self) local parent = self:GetParent() parent:GetScript("OnEnter")(parent) end, OnLeave = function(self) local parent = self:GetParent() parent:GetScript("OnLeave")(parent) end, } setmetatable(Dropdown.secureBin, { __index = function(self, index) local button = CreateFrame("Button", nil, nil, "SecureActionButtonTemplate") for script, handler in pairs(scripts) do button:SetScript(script, handler) end button.attributes = {} return button end, }) function Dropdown:DismissSecureButton(button) button:Hide() button:ClearAllPoints() button:SetParent(nil) tinsert(Dropdown.secureBin, button) end Dropdown.frame = Dropdown.frame or CreateFrame("Frame") Dropdown.frame:RegisterEvent("PLAYER_REGEN_DISABLED") Dropdown.frame:SetScript("OnEvent", function(self) for i, button in ipairs(Dropdown.secureButtons) do Dropdown:DismissSecureButton(button) end wipe(Dropdown.secureButtons) end) Libra:RegisterModule(Type, Version, constructor)