local mouseUpRefCounts = {} local currentOnMenuItemAddedCallback = nil local currentOnMenuHiddenCallback = nil local selectedIndex = nil local lastCommandWasFromMenu = true local DEFAULT_TEXT_COLOR = ZO_ColorDef:New(GetInterfaceColor(INTERFACE_COLOR_TYPE_TEXT_COLORS, INTERFACE_TEXT_COLOR_NORMAL)) local DEFAULT_TEXT_HIGHLIGHT = ZO_ColorDef:New(GetInterfaceColor(INTERFACE_COLOR_TYPE_TEXT_COLORS, INTERFACE_TEXT_COLOR_CONTEXT_HIGHLIGHT)) MENU_ADD_OPTION_LABEL = 1 MENU_ADD_OPTION_CHECKBOX = 2 MENU_TYPE_DEFAULT = 1 MENU_TYPE_COMBO_BOX = 2 MENU_TYPE_TEXT_ENTRY_DROP_DOWN = 3 MENU_TYPE_MULTISELECT_COMBO_BOX = 4 local menuInfo = { [MENU_TYPE_DEFAULT] = { backdropEdge = "EsoUI/Art/Tooltips/UI-Border.dds", backdropCenter = "EsoUI/Art/Tooltips/UI-TooltipCenter.dds", backdropInsets = {16,16,-16,-16}, backdropEdgeWidth = 128, backdropEdgeHeight = 16, }, [MENU_TYPE_COMBO_BOX] = { backdropEdge = "EsoUI/Art/Miscellaneous/dropDown_edge.dds", backdropCenter = "EsoUI/Art/Miscellaneous/dropDown_center.dds", backdropInsets = {16, 16, -16, -16}, backdropEdgeWidth = 128, backdropEdgeHeight = 16, }, [MENU_TYPE_TEXT_ENTRY_DROP_DOWN] = { backdropEdge = "EsoUI/Art/Miscellaneous/textEntry_dropDown_edge.dds", backdropCenter = "EsoUI/Art/Miscellaneous/textEntry_dropDown_center.dds", backdropInsets = {8, 8, -8, -8}, backdropEdgeWidth = 64, backdropEdgeHeight = 8, hideMunge = true, }, [MENU_TYPE_MULTISELECT_COMBO_BOX] = { backdropEdge = "EsoUI/Art/Miscellaneous/dropDown_edge.dds", backdropCenter = "EsoUI/Art/Miscellaneous/dropDown_center.dds", backdropInsets = {16, 16, -16, -16}, backdropEdgeWidth = 128, backdropEdgeHeight = 16, }, } local function AnchorMenuToMouse(menuToAnchor) local x, y = GetUIMousePosition() local width, height = GuiRoot:GetDimensions() menuToAnchor:ClearAnchors() local right = true if x + ZO_Menu.width > width then right = false end local bottom = true if y + ZO_Menu.height > height then bottom = false end if right then if bottom then menuToAnchor:SetAnchor(TOPLEFT, nil, TOPLEFT, x, y) else menuToAnchor:SetAnchor(BOTTOMLEFT, nil, TOPLEFT, x, y) end else if bottom then menuToAnchor:SetAnchor(TOPRIGHT, nil, TOPLEFT, x, y) else menuToAnchor:SetAnchor(BOTTOMRIGHT, nil, TOPLEFT, x, y) end end end local function UpdateMenuDimensions(menuEntry) if ZO_Menu.currentIndex > 0 then local textWidth, textHeight = menuEntry.item.nameLabel:GetTextDimensions() local checkboxWidth, checkboxHeight = 0, 0 if menuEntry.checkbox then checkboxWidth, checkboxHeight = menuEntry.checkbox:GetDesiredWidth(), menuEntry.checkbox:GetDesiredHeight() end local entryWidth = textWidth + checkboxWidth + ZO_Menu.menuPad * 2 local entryHeight = zo_max(textHeight, checkboxHeight) if entryWidth > ZO_Menu.width then ZO_Menu.width = entryWidth end ZO_Menu.height = ZO_Menu.height + entryHeight + menuEntry.itemYPad -- More adjustments will come later...this just needs to set the height -- HACK: Because anchor processing doesn't happen right away, and because GetDimensions -- does NOT return desired dimensions...this will actually need to remember the height -- that the label was set to. And to remember it, we need to find the menu item in the -- appropriate menu... menuEntry.item.storedHeight = entryHeight end end function ClearMenu() local owner = ZO_Menu.owner ZO_Menu:SetHidden(true) SetAddMenuItemCallback(nil) -- just in case this wasn't cleared SetMenuHiddenCallback(nil) ZO_MenuHighlight:SetHidden(true) ZO_Menu_SetSelectedIndex(nil) if ZO_Menu.itemPool then ZO_Menu.itemPool:ReleaseAllObjects() end if ZO_Menu.checkBoxPool then ZO_Menu.checkBoxPool:ReleaseAllObjects() end ZO_Menu.highlightPool:ReleaseAllObjects() ZO_Menu.nextAnchor = ZO_Menu ZO_Menu:SetDimensions(0, 0) ZO_Menu.currentIndex = 1 ZO_Menu.width = 0 ZO_Menu.height = 0 ZO_Menu.items = {} ZO_Menu.spacing = 0 ZO_Menu.menuPad = 8 ZO_Menu.owner = nil if type(owner) == "userdata" then owner:SetHandler("OnEffectivelyHidden", nil, "ZO_Menu") end end function IsMenuVisible() return mouseUpRefCounts[ZO_Menu] ~= nil end function SetMenuMinimumWidth(minWidth) ZO_Menu.width = minWidth end function SetMenuSpacing(spacing) ZO_Menu.spacing = spacing end function SetMenuPad(menuPad) ZO_Menu.menuPad = menuPad end local function SetMenuOwner(owner) if type(owner) == "userdata" then owner:SetHandler("OnEffectivelyHidden", ClearMenu, "ZO_Menu") end ZO_Menu.owner = owner end function GetMenuOwner(menu) local menu = menu or ZO_Menu return ZO_Menu.owner end function MenuOwnerClosed(potentialOwner) if IsMenuVisible() and (GetMenuOwner() == potentialOwner) then ClearMenu() end end -- NOTE: If owner is a valid control, the menu system will use this to install an OnEffectivelyHidden handler -- so that if the control is hidden for any reason, the menu will properly close. The handler is currently -- overwritten/removed, so make sure that you account for that if your controls need to have their own -- effectively hidden handlers. function ShowMenu(owner, initialRefCount, menuType) if next(ZO_Menu.items) == nil then return false end menuType = menuType or MENU_TYPE_DEFAULT ZO_Menu:SetDimensions((ZO_Menu.menuPad * 2) + ZO_Menu.width, (ZO_Menu.menuPad * 2) + ZO_Menu.height + ZO_Menu.spacing * (#ZO_Menu.items - 1)) -- Force the control that contains this label to the same size as the label -- so that mouse over and item click behavior works on each line without -- needing to mouse over the actual text of the label. -- Keep the height the same as it was... for k, v in pairs(ZO_Menu.items) do -- HACK: See note below about storing the height... v.item:SetDimensions(ZO_Menu.width - ZO_Menu.menuPad, v.item.storedHeight) end if ZO_Menu.menuType ~= menuType then local menuInfo = menuInfo[menuType] ZO_MenuBG:SetCenterTexture(menuInfo.backdropCenter) ZO_MenuBG:SetEdgeTexture(menuInfo.backdropEdge, menuInfo.backdropEdgeWidth, menuInfo.backdropEdgeHeight) ZO_MenuBG:SetInsets(unpack(menuInfo.backdropInsets)) ZO_MenuBGMungeOverlay:SetHidden(menuInfo.hideMunge) end ZO_Menu.menuType = menuType ZO_Menu:SetHidden(false) ZO_Menus:BringWindowToTop() ZO_Menu.underlayControl:SetHidden(not ZO_Menu.useUnderlay) AnchorMenuToMouse(ZO_Menu) -- Combobox will set this to two so that the first mouse up doesn't close the menu -- Otherwise, the first mouse up will close the menu. mouseUpRefCounts[ZO_Menu] = initialRefCount or 2 -- The menu has been shown, get rid of the add item callback; it will be set again for other menus. SetAddMenuItemCallback(nil) SetMenuOwner(owner) return true end function AnchorMenu(control, offsetY) -- By default, the menu is shown where the mouse cursor is... -- but if valid anchor data is passed in, that's used instead. ZO_Menu:ClearAnchors() ZO_Menu:SetAnchor(TOPLEFT, control, BOTTOMLEFT, 0, offsetY) -- The menu must properly contain its contents, but if "control" is -- larger than the contents' widths, use "control" to size the menu. -- NOTE: Not using padding on purpose since I don't want to redo all the options dropdowns. If something else breaks this, fix those by increasing their min width if control:GetWidth() >= ZO_Menu.width then ZO_Menu:SetAnchor(TOPRIGHT, control, BOTTOMRIGHT, 0, offsetY) end end function SetAddMenuItemCallback(itemAddedCallback) if itemAddedCallback and type(itemAddedCallback) == "function" then currentOnMenuItemAddedCallback = itemAddedCallback else currentOnMenuItemAddedCallback = nil end end function SetMenuHiddenCallback(menuHiddenCallback) if menuHiddenCallback and type(menuHiddenCallback) == "function" then currentOnMenuHiddenCallback = menuHiddenCallback else currentOnMenuHiddenCallback = nil end end function GetMenuPadding() return ZO_Menu.menuPad end --onSelect is a required field function AddMenuItem(labelText, onSelect, itemType, labelFont, normalColor, highlightColor, itemYPad, horizontalAlignment, isHighlighted, onEnter, onExit, enabled) if not internalassert(onSelect, "AddMenuItem requires a valid onSelect function") then -- ESO-804925 return end local isEnabled = enabled if isEnabled == nil then -- Evaluate nil to be equivalent to true for backwards compatibility. isEnabled = true end local menuItemControl = ZO_Menu.itemPool:AcquireObject() menuItemControl.OnSelect = onSelect menuItemControl.menuIndex = ZO_Menu.currentIndex menuItemControl.enabled = isEnabled menuItemControl.onEnter = onEnter menuItemControl.onExit = onExit local checkboxItemControl if itemType == MENU_ADD_OPTION_CHECKBOX then checkboxItemControl = ZO_Menu.checkBoxPool:AcquireObject() ZO_CheckButton_SetToggleFunction(checkboxItemControl, onSelect) checkboxItemControl.menuIndex = ZO_Menu.currentIndex end local addedItemIndex = ZO_Menu.currentIndex ZO_Menu.currentIndex = ZO_Menu.currentIndex + 1 itemYPad = itemYPad or 0 table.insert(ZO_Menu.items, { item = menuItemControl, checkbox = checkboxItemControl, itemYPad = itemYPad }) local nameLabel = menuItemControl.nameLabel menuItemControl:ClearAnchors() menuItemControl:SetHidden(false) -- We need to reset these anchors first so the label can get it's proper size for the parent to reference nameLabel:ClearAnchors() nameLabel:SetAnchor(TOPLEFT) if checkboxItemControl then checkboxItemControl:ClearAnchors() checkboxItemControl:SetHidden(false) end if ZO_Menu.nextAnchor == ZO_Menu then if checkboxItemControl then checkboxItemControl:SetAnchor(TOPLEFT, ZO_Menu.nextAnchor, TOPLEFT, ZO_Menu.menuPad, ZO_Menu.menuPad + itemYPad) menuItemControl:SetAnchor(TOPLEFT, checkboxItemControl, TOPRIGHT, 0, 0) else menuItemControl:SetAnchor(TOPLEFT, ZO_Menu.nextAnchor, TOPLEFT, ZO_Menu.menuPad, ZO_Menu.menuPad + itemYPad) end else if checkboxItemControl then checkboxItemControl:SetAnchor(TOPLEFT, ZO_Menu.nextAnchor, BOTTOMLEFT, 0, ZO_Menu.spacing + itemYPad) menuItemControl:SetAnchor(TOPLEFT, checkboxItemControl, TOPRIGHT, 0, 0) else menuItemControl:SetAnchor(TOPLEFT, ZO_Menu.nextAnchor, BOTTOMLEFT, 0, ZO_Menu.spacing + itemYPad) end end ZO_Menu.nextAnchor = checkboxItemControl or menuItemControl if labelFont == nil then if not IsInGamepadPreferredMode() then labelFont = "ZoFontGame" else labelFont = "ZoFontGamepad22" end end nameLabel.normalColor = normalColor or DEFAULT_TEXT_COLOR nameLabel.highlightColor = highlightColor or DEFAULT_TEXT_HIGHLIGHT -- NOTE: Must set text AFTER the current index has been incremented. nameLabel:SetFont(labelFont) nameLabel:SetText(labelText) UpdateMenuDimensions(ZO_Menu.items[#ZO_Menu.items]) -- Now that we know how the label wil size, finish the anchors if we need the label to be right aligned -- Right align won't work if the label has no width horizontalAlignment = horizontalAlignment or TEXT_ALIGN_LEFT nameLabel:SetHorizontalAlignment(horizontalAlignment) if horizontalAlignment == TEXT_ALIGN_RIGHT then nameLabel:SetAnchor(TOPRIGHT) end nameLabel:SetColor(nameLabel.normalColor:UnpackRGBA()) if isHighlighted then menuItemControl.isHighlighted = isHighlighted ZO_Menu_AcquireAndApplyHighlight(menuItemControl) end if currentOnMenuItemAddedCallback then currentOnMenuItemAddedCallback() end return addedItemIndex end function UpdateMenuItemState(item, state) local menuEntry = ZO_Menu.items[item] if menuEntry and menuEntry.checkbox then ZO_CheckButton_SetCheckState(menuEntry.checkbox, state) end end function ZO_Menu_SelectItem(control) ZO_MenuHighlight:ClearAnchors() ZO_MenuHighlight:SetAnchor(TOPLEFT, control, TOPLEFT, -2, -2) ZO_MenuHighlight:SetAnchor(BOTTOMRIGHT, control, BOTTOMRIGHT, 2, 2) ZO_MenuHighlight:SetHidden(false) if not control.isHighlighted then control.nameLabel:SetColor(control.nameLabel.highlightColor:UnpackRGBA()) end end function ZO_Menu_UnselectItem(control) ZO_MenuHighlight:SetHidden(true) if not control.isHighlighted then control.nameLabel:SetColor(control.nameLabel.normalColor:UnpackRGBA()) end end function ZO_Menu_SetSelectedIndex(index) if not ZO_Menu.items then return end if index then index = zo_max(zo_min(index, #ZO_Menu.items), 1) end if selectedIndex ~= index then if selectedIndex then local entry = ZO_Menu.items[selectedIndex] if entry then local control = entry.item if control then ZO_Menu_UnselectItem(control) end end end selectedIndex = index if selectedIndex then local entry = ZO_Menu.items[selectedIndex] if entry then local control = entry.item if control then ZO_Menu_SelectItem(control) end end end end end function ZO_Menu_GetNumMenuItems() return #ZO_Menu.items end function ZO_Menu_GetSelectedIndex() return selectedIndex end function ZO_Menu_GetSelectedText() local control = ZO_Menu.items[selectedIndex].item if control then return control.nameLabel:GetText() end end function ZO_Menu_EnterItem(control) ZO_Menu_SetSelectedIndex(control.menuIndex) if type(control.onEnter) == "function" then control.onEnter(control) end end function ZO_Menu_ExitItem(control) if selectedIndex == control.menuIndex then ZO_Menu_SetSelectedIndex(nil) if type(control.onExit) == "function" then control.onExit(control) end end end function ZO_Menu_ClickItem(control, button) if button == MOUSE_BUTTON_INDEX_LEFT then ZO_Menu_SetLastCommandWasFromMenu(true) local menuEntry = ZO_Menu.items[control.menuIndex] if menuEntry and menuEntry.checkbox then -- Treat this like the checkbox was clicked. ZO_CheckButton_OnClicked(menuEntry.checkbox, button) else -- Treat it like the label was clicked if control.OnSelect then control.OnSelect() else -- ESO-804925 internalassert(false, string.format("Attempting to click menu entry [%s] but it has no OnSelect behavior defined.", control.nameLabel:GetText())) end if control.enabled ~= false then if ZO_Menu.menuType == MENU_TYPE_MULTISELECT_COMBO_BOX then control.isHighlighted = not control.isHighlighted if control.isHighlighted then ZO_Menu_AcquireAndApplyHighlight(control) else ZO_Menu_ReleaseHighlight(control) end else ClearMenu() end end end end end function ZO_Menu_OnHide(control) mouseUpRefCounts[ZO_Menu] = nil ZO_Menu.useUnderlay = false if currentOnMenuHiddenCallback then currentOnMenuHiddenCallback() end end function ZO_Menu_AcquireAndApplyHighlight(control) local highlight, key = ZO_Menu.highlightPool:AcquireObject() highlight.key = key control.highlight = highlight highlight:SetAnchor(TOPLEFT, control, TOPLEFT, -2, -2) highlight:SetAnchor(BOTTOMRIGHT, control, BOTTOMRIGHT, 2, 2) highlight:SetHidden(false) control.nameLabel:SetColor(control.nameLabel.highlightColor:UnpackRGBA()) end function ZO_Menu_ReleaseHighlight(control) ZO_Menu.highlightPool:ReleaseObject(control.highlight.key) control.highlight = nil end local function OnGlobalMouseUp() local refCount = mouseUpRefCounts[ZO_Menu] if refCount ~= nil then local moc = WINDOW_MANAGER:GetMouseOverControl() if moc:GetOwningWindow() ~= ZO_Menus or moc == ZO_Menu.underlayControl then refCount = refCount - 1 mouseUpRefCounts[ZO_Menu] = refCount if refCount <= 0 then ClearMenu() end end end end local function ResetFunction(control) control:SetHidden(true) control:ClearAnchors() control.OnSelect = nil control.menuIndex = nil control.isHighlighted = nil end local function ResetCheckbox(checkbox) ResetFunction(checkbox) ZO_CheckButton_SetToggleFunction(checkbox, nil) ZO_CheckButton_SetUnchecked(checkbox) end local function EntryFactory(pool) local control = ZO_ObjectPool_CreateControl("ZO_MenuItem", pool, ZO_Menu) control.nameLabel = control:GetNamedChild("Name") return control end local function CheckBoxFactory(pool) return ZO_ObjectPool_CreateControl("ZO_MenuItemCheckButton", pool, ZO_Menu) end local function HighlightFactory(pool) return ZO_ObjectPool_CreateControl("ZO_MenuItemHighlight", pool, ZO_Menu) end local function HighlightResetFunction(control) control:SetHidden(true) control:ClearAnchors() control.key = nil end function ZO_Menu_Initialize() --Pre-allocate these so the control closures (for OnMouseUp) are created as secure code and add-ons can use them. ZO_Menu.itemPool = ZO_ObjectPool:New(EntryFactory, ResetFunction) for i = 1, 30 do ZO_Menu.itemPool:AcquireObject() end ZO_Menu.itemPool:ReleaseAllObjects() ZO_Menu.checkBoxPool = ZO_ObjectPool:New(CheckBoxFactory, ResetCheckbox) for i = 1, 30 do ZO_Menu.checkBoxPool:AcquireObject() end ZO_Menu.checkBoxPool:ReleaseAllObjects() ZO_Menu.highlightPool = ZO_ObjectPool:New(HighlightFactory, HighlightResetFunction) ZO_Menu.underlayControl = ZO_Menu:GetNamedChild("Underlay") ClearMenu() EVENT_MANAGER:RegisterForEvent("ZO_Menu_OnGlobalMouseUp", EVENT_GLOBAL_MOUSE_UP, OnGlobalMouseUp) end function ZO_Menu_WasLastCommandFromMenu() return lastCommandWasFromMenu end function ZO_Menu_SetLastCommandWasFromMenu(menuCommand) lastCommandWasFromMenu = menuCommand end function ZO_Menu_SetUseUnderlay(useUnderlay) ZO_Menu.useUnderlay = useUnderlay end