diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2f711bc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + luacheck: + name: Luacheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: nebularg/actions-luacheck@v1 + with: + files: '*.lua' + args: '--no-color -q' + + toc-validation: + name: TOC validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify TOC file references exist + run: | + EXIT_CODE=0 + while IFS= read -r line; do + line="$(echo "$line" | sed 's/\r$//')" + [[ -z "$line" ]] && continue + [[ "$line" =~ ^## ]] && continue + filepath="$(echo "$line" | tr '\\' '/')" + if [[ ! -f "$filepath" ]]; then + echo "FAIL: $filepath listed in .toc but does not exist" + EXIT_CODE=1 + fi + done < EasyFind.toc + exit $EXIT_CODE + + commit-hygiene: + name: Commit hygiene + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Validate commit messages + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + EXIT_CODE=0 + + while IFS= read -r sha; do + MSG="$(git log --format='%B' -n 1 "$sha")" + SUBJECT="$(echo "$MSG" | head -n 1)" + + if [[ -z "$SUBJECT" ]]; then + echo "FAIL [$sha]: Empty subject line" + EXIT_CODE=1 + continue + fi + + if [[ ${#SUBJECT} -gt 72 ]]; then + echo "FAIL [$sha]: Subject exceeds 72 chars (${#SUBJECT})" + EXIT_CODE=1 + fi + + done < <(git rev-list "$BASE".."$HEAD") + + exit $EXIT_CODE + + file-hygiene: + name: File hygiene + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for prohibited files + run: | + EXIT_CODE=0 + + # Block .env files + ENV_FILES=$(find . -name '.env' -o -name '.env.*' | grep -v '.env.example' | grep -v '.git/' || true) + if [[ -n "$ENV_FILES" ]]; then + echo "FAIL: .env file(s) in repository: $ENV_FILES" + EXIT_CODE=1 + fi + + # Block credential files + SECRET_FILES=$(find . -not -path './.git/*' -iregex '.*\.\(key\|pem\|p12\|pfx\)$' || true) + if [[ -n "$SECRET_FILES" ]]; then + echo "FAIL: Credential file(s) found: $SECRET_FILES" + EXIT_CODE=1 + fi + + # Check for hardcoded secrets in Lua files + SECRET_PATTERNS=$(grep -rlE '(api[_-]?key|secret|token|password)\s*[:=]\s*["'"'"'][A-Za-z0-9+/=_-]{20,}' *.lua Locales/*.lua 2>/dev/null || true) + if [[ -n "$SECRET_PATTERNS" ]]; then + echo "FAIL: Possible hardcoded secret in: $SECRET_PATTERNS" + EXIT_CODE=1 + fi + + exit $EXIT_CODE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cae8ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Release artifacts +*.zip + +# Editor files +*.swp +*.swo +*~ + +# OS files +Thumbs.db +.DS_Store diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..8bdfb33 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,82 @@ +std = "lua51" +max_line_length = false + +-- Suppress warnings that are standard WoW addon patterns, not real issues +ignore = { + "431", -- shadowing upvalue (self in nested callbacks is standard WoW pattern) + "432", -- shadowing upvalue argument +} + +-- Globals this addon sets +globals = { + "EasyFind", + "EasyFindDB", + "EasyFind_OnAddonCompartmentClick", + "SlashCmdList", + "StaticPopupDialogs", + "SLASH_EASYFIND1", + "BINDING_NAME_EASYFIND_TOGGLE", + "BINDING_NAME_EASYFIND_FOCUS", + "BINDING_NAME_EASYFIND_TOGGLE_FOCUS", + "BINDING_NAME_EASYFIND_CLEAR", +} + +-- WoW API globals (read-only) +read_globals = { + -- WoW Lua extensions + "debugstack", "strsplit", "strtrim", "wipe", "hooksecurefunc", + "format", "time", "date", + + -- Core API + "CreateFrame", "CreateFont", "CreateVector2D", + "GetTime", "GetLocale", "GetCVar", "GetCursorPosition", + "GetAddOnMetadata", "GetAtlasInfo", "GetMinimapShape", + "GetBindingKey", "GetCurrentBindingSet", "SaveBindings", "SetBinding", + "GetCategoryInfo", "GetPlayerFacing", + "SetPortraitTexture", "ToggleWorldMap", "ToggleDropDownMenu", + "InterfaceOptions_AddCategory", "InCombatLockdown", + "IsShiftKeyDown", "IsMouseButtonDown", "IsAltKeyDown", "IsControlKeyDown", + "IsInGroup", "IsInInstance", "IsIndoors", "UnitIsGroupLeader", + "UnitFactionGroup", "UnitPosition", + "StaticPopup_Show", "GameTooltip_Hide", "PlaySound", "ReloadUI", + "GetSpellInfo", "GetItemInfo", "UseToyByItemID", + "PanelTemplates_GetSelectedTab", + + -- Frames and UI objects + "UIParent", "GameTooltip", "WorldMapFrame", "Minimap", "MinimapCluster", + "CharacterFrame", "PaperDollFrame", "AchievementFrame", + "CharacterStatsPane", "PaperDollTitlesPane", "PaperDollEquipmentManagerPane", + "CurrencyFrame", + "SpellBookFrame", "PlayerSpellsFrame", "CollectionsJournal", + "EncounterJournal", "PVEFrame", "ReputationFrame", "TokenFrame", + "GroupFinderFrame", "LFGListFrame", "HelpFrame", "ClassTalentFrame", + "GuildMicroButton", "StoreMicroButton", "PlayerFrame", "StoreFrame", + "LFDParentFrame", "RaidFinderFrame", + "LFGListPVEStub", "LFGListPVPStub", + "HonorFrame", "ConquestFrame", "TrainingGroundsFrame", + "PVPQueueFrame", + "AchievementFrameCategories_ExpandToCategory", + "AchievementFrameCategories_UpdateDataProvider", + "Menu", "ScrollBoxConstants", + + -- C_* namespaces + "C_AddOns", "C_AchievementInfo", "C_AreaPoiInfo", "C_CurrencyInfo", + "C_EncounterJournal", "C_GossipInfo", "C_MajorFactions", "C_Map", + "C_Minimap", "C_MountJournal", "C_Navigation", "C_PetJournal", + "C_Reputation", "C_SuperTrack", "C_TaxiMap", "C_Texture", "C_Timer", + "C_ToyBox", "C_VignetteInfo", + + -- Constants, Enums, Mixins + "Enum", "Settings", "BackdropTemplateMixin", + "SOUNDKIT", "UIDROPDOWNMENU_OPEN_MENU", "UISpecialFrames", + "LE_PET_JOURNAL_FILTER_COLLECTED", "LE_PET_JOURNAL_FILTER_NOT_COLLECTED", + + -- Font objects + "Game15Font_Shadow", "GameFontNormal", "GameFontNormalSmall", + "GameFontHighlight", "GameFontHighlightSmall", "GameFontDisable", + "GameFontDisableSmall", "GameFontNormalLarge", +} + +-- WoW callbacks have fixed signatures; unused args are normal +unused_args = false +self = false diff --git a/Core.lua b/Core.lua index f767069..8cd5562 100644 --- a/Core.lua +++ b/Core.lua @@ -426,29 +426,29 @@ function EasyFind:TestIndicatorTexture(texturePath) insets = { left = 11, right = 12, top = 12, bottom = 11 } }) testFrame:SetBackdropColor(0, 0, 0, 0.9) - + if not testFrame.texture then testFrame.texture = testFrame:CreateTexture(nil, "ARTWORK") testFrame.texture:SetSize(200, 200) testFrame.texture:SetPoint("CENTER") end - + if not testFrame.title then testFrame.title = testFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") testFrame.title:SetPoint("TOP", 0, -15) end - + if not testFrame.closeBtn then testFrame.closeBtn = CreateFrame("Button", nil, testFrame, "UIPanelCloseButton") testFrame.closeBtn:SetPoint("TOPRIGHT", -5, -5) end - + -- Try to load the texture testFrame.texture:SetTexture(texturePath) testFrame.texture:SetVertexColor(ns.YELLOW_HIGHLIGHT[1], ns.YELLOW_HIGHLIGHT[2], ns.YELLOW_HIGHLIGHT[3], 1) testFrame.title:SetText("Testing: " .. texturePath) testFrame:Show() - + EasyFind:Print("Testing texture: " .. texturePath) EasyFind:Print("Close the preview window to dismiss.") end diff --git a/Database.lua b/Database.lua index c7e33bd..1656843 100644 --- a/Database.lua +++ b/Database.lua @@ -1621,7 +1621,7 @@ function Database:ScoreInitials(text, query) return bonus end end - + -- Strategy 2: Prefix-of-words - each query segment matches the start of a word -- "raba" → "ra(ndom) ba(ttleground)" - greedily consume query chars across words local qi = 1 -- position in query @@ -1648,7 +1648,7 @@ function Database:ScoreInitials(text, query) -- Score based on how many words were matched (more = better abbreviation) return 110 + mmin(wordsMatched * 3, 20) end - + return 0 end @@ -1712,7 +1712,7 @@ function Database:DamerauLevenshtein(s1, s2, len1, len2) local prev2, prev, curr = dlPrev2, dlPrev, dlCurr for j = 0, len2 do prev[j] = j end - + for i = 1, len1 do curr[0] = i local minInRow = i diff --git a/Highlight.lua b/Highlight.lua index fd6795a..8f96e81 100644 --- a/Highlight.lua +++ b/Highlight.lua @@ -57,28 +57,28 @@ function Highlight:CreateHighlightFrame() highlightFrame:SetFrameStrata("TOOLTIP") highlightFrame:SetFrameLevel(500) highlightFrame:Hide() - + local borderSize = 4 - + -- Highlight border is ALWAYS yellow local top = highlightFrame:CreateTexture(nil, "OVERLAY") top:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) highlightFrame.top = top - + local bottom = highlightFrame:CreateTexture(nil, "OVERLAY") bottom:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) highlightFrame.bottom = bottom - + local left = highlightFrame:CreateTexture(nil, "OVERLAY") left:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) highlightFrame.left = left - + local right = highlightFrame:CreateTexture(nil, "OVERLAY") right:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) highlightFrame.right = right - + highlightFrame.borderSize = borderSize - + local animGroup = highlightFrame:CreateAnimationGroup() animGroup:SetLooping("BOUNCE") local alpha = animGroup:CreateAnimation("Alpha") @@ -124,7 +124,7 @@ function Highlight:CreateInstructionFrame() instructionFrame:SetFrameStrata("TOOLTIP") instructionFrame:SetFrameLevel(502) instructionFrame:Hide() - + instructionFrame:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border", @@ -132,7 +132,7 @@ function Highlight:CreateInstructionFrame() insets = { left = 5, right = 5, top = 5, bottom = 5 } }) instructionFrame:SetBackdropColor(0, 0, 0, 0.95) -- Very dark background - + local text = instructionFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") text:SetPoint("TOPLEFT", 15, -15) text:SetPoint("TOPRIGHT", -15, -15) @@ -141,7 +141,7 @@ function Highlight:CreateInstructionFrame() text:SetWordWrap(true) -- Enable word wrap text:SetNonSpaceWrap(true) instructionFrame.text = text - + -- Dismiss button local dismissBtn = CreateFrame("Button", nil, instructionFrame, "UIPanelButtonTemplate") dismissBtn:SetSize(80, 22) @@ -151,7 +151,7 @@ function Highlight:CreateInstructionFrame() Highlight:Cancel() end) instructionFrame.dismissBtn = dismissBtn - + local closeBtn = CreateFrame("Button", nil, instructionFrame, "UIPanelCloseButton") closeBtn:SetPoint("TOPRIGHT", 5, 5) closeBtn:SetSize(20, 20) @@ -165,7 +165,7 @@ function Highlight:CreateContextTooltip() contextTooltip:SetFrameStrata("TOOLTIP") contextTooltip:SetFrameLevel(503) contextTooltip:Hide() - + contextTooltip:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark", edgeFile = TOOLTIP_BORDER, @@ -174,7 +174,7 @@ function Highlight:CreateContextTooltip() }) contextTooltip:SetBackdropColor(0, 0, 0, 0.9) contextTooltip:SetBackdropBorderColor(GOLD_COLOR[1], GOLD_COLOR[2], GOLD_COLOR[3], 1) - + local text = contextTooltip:CreateFontString(nil, "OVERLAY", "GameFontNormal") text:SetPoint("CENTER", 0, 0) text:SetTextColor(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3]) @@ -207,7 +207,7 @@ end function Highlight:StartGuide(guideData) self:Cancel() - + if not guideData or not guideData.steps or #guideData.steps == 0 then if guideData and guideData.steps and guideData.steps[1] and guideData.steps[1].customText then self:ShowInstruction(guideData.steps[1].customText) @@ -215,10 +215,10 @@ function Highlight:StartGuide(guideData) end return end - + currentGuide = guideData currentStepIndex = 1 - + -- Use a ticker to continuously check step conditions stepTicker = C_Timer.NewTicker(0.1, function() local ok, err = pcall(self.UpdateGuide, self) @@ -232,18 +232,18 @@ end -- Start the guide at a specific step (used by DirectOpen to skip to final highlight) function Highlight:StartGuideAtStep(guideData, stepIndex) self:Cancel() - + if not guideData or not guideData.steps or #guideData.steps == 0 then return end - + if stepIndex > #guideData.steps then stepIndex = #guideData.steps end - + currentGuide = guideData currentStepIndex = stepIndex - + stepTicker = C_Timer.NewTicker(0.1, function() local ok, err = pcall(self.UpdateGuide, self) if not ok then @@ -258,22 +258,22 @@ function Highlight:UpdateGuide() self:Cancel() return end - + local step = currentGuide.steps[currentStepIndex] if not step then self:Cancel() return end - + -- Custom text only if step.customText then self:ShowInstruction(step.customText) C_Timer.After(5, function() self:Cancel() end) return end - + local isLastStep = (currentStepIndex == #currentGuide.steps) - + -- Step 1 type: Highlight a button directly (like micro menu buttons) if step.buttonFrame then local targetFrame = _G[step.buttonFrame] @@ -300,7 +300,7 @@ function Highlight:UpdateGuide() end return end - + -- Portrait menu step 1: highlight player portrait and tell user to right-click if step.portraitMenu then -- Check if the portrait menu is already open @@ -310,7 +310,7 @@ function Highlight:UpdateGuide() self:AdvanceStep() return end - + -- Highlight the player frame portrait and show contextual tooltip local portrait = PlayerFrame if portrait and portrait:IsShown() then @@ -319,7 +319,7 @@ function Highlight:UpdateGuide() end return end - + -- Portrait menu step 2: find and highlight the correct menu option if step.portraitMenuOption then -- Check if portrait menu is still open @@ -330,7 +330,7 @@ function Highlight:UpdateGuide() self:HideHighlight() return end - + -- Find the menu option button and highlight it local optionBtn = self:FindPortraitMenuOption(step.portraitMenuOption) if optionBtn then @@ -349,18 +349,18 @@ function Highlight:UpdateGuide() end return end - + -- Step 2+ type: Wait for frame, then highlight tab or region if step.waitForFrame then local frame = self:GetFrameByPath(step.waitForFrame) - + if not frame or not frame:IsShown() then -- Frame closed, go back to step 1 currentStepIndex = 1 self:HideHighlight() return end - + -- Frame is open if step.tabIndex then -- Check if already on correct tab @@ -369,7 +369,7 @@ function Highlight:UpdateGuide() self:AdvanceStep() return end - + -- Need to click tab local tabBtn = self:GetTabButton(step.waitForFrame, step.tabIndex) if tabBtn then @@ -388,7 +388,7 @@ function Highlight:UpdateGuide() end return end - + -- Side tab (like Dungeon Finder / Raid Finder / Premade Groups in Group Finder) if step.sideTabIndex then -- First check: are we still on the correct main tab? @@ -401,7 +401,7 @@ function Highlight:UpdateGuide() break end end - + if requiredTabIndex then local currentTab = self:GetCurrentTabIndex(step.waitForFrame) if currentTab and currentTab ~= requiredTabIndex then @@ -416,14 +416,14 @@ function Highlight:UpdateGuide() end end end - + -- Check if already on correct side tab if self:IsSideTabSelected(step.waitForFrame, step.sideTabIndex) then -- Correct tab, advance to next step self:AdvanceStep() return end - + -- Need to click side tab local sideBtn = self:GetSideTabButton(step.waitForFrame, step.sideTabIndex) if sideBtn and sideBtn:IsShown() then @@ -448,7 +448,7 @@ function Highlight:UpdateGuide() end return end - + -- PVP Side tab (Quick Match / Rated / Premade Groups / Training Grounds in PvP tab) if step.pvpSideTabIndex then -- First check: are we still on the correct main tab (PvP tab = tab 2)? @@ -461,7 +461,7 @@ function Highlight:UpdateGuide() break end end - + if requiredTabIndex then local currentTab = self:GetCurrentTabIndex(step.waitForFrame) if currentTab and currentTab ~= requiredTabIndex then @@ -477,14 +477,14 @@ function Highlight:UpdateGuide() end end end - + -- Check if already on correct PvP side tab if self:IsPvPSideTabSelected(step.waitForFrame, step.pvpSideTabIndex) then -- Correct tab, advance to next step self:AdvanceStep() return end - + -- Need to click PvP side tab local pvpBtn = self:GetPvPSideTabButton(step.waitForFrame, step.pvpSideTabIndex) if pvpBtn and pvpBtn:IsShown() then @@ -509,7 +509,7 @@ function Highlight:UpdateGuide() end return end - + -- Statistics category navigation (tree-based category selection) if step.statisticsCategory then -- First check: are we still on the Statistics tab? @@ -524,7 +524,7 @@ function Highlight:UpdateGuide() self:HideHighlight() return end - + -- Prerequisite check: verify ALL earlier statisticsCategory steps are still -- expanded (children visible). A parent doesn't need to stay "selected" once -- we've drilled into a child - it just needs to still be expanded. @@ -538,7 +538,7 @@ function Highlight:UpdateGuide() end end end - + -- Check if already on correct statistics category if self:IsCategorySelectedByData(step.statisticsCategory) then if isLastStep then @@ -574,7 +574,7 @@ function Highlight:UpdateGuide() end return end - + -- Achievement category navigation (tree-based category selection in Achievements/Guild tabs) if step.achievementCategory then -- First check: are we still on the correct tab? @@ -586,7 +586,7 @@ function Highlight:UpdateGuide() break end end - + if requiredTabIndex then local currentTab = PanelTemplates_GetSelectedTab and PanelTemplates_GetSelectedTab(AchievementFrame) if currentTab and currentTab ~= requiredTabIndex then @@ -601,7 +601,7 @@ function Highlight:UpdateGuide() return end end - + -- Prerequisite check: verify ALL earlier achievementCategory steps are still -- expanded (children visible). A parent doesn't need to stay "selected" once -- we've drilled into a child - it just needs to still be expanded. @@ -615,7 +615,7 @@ function Highlight:UpdateGuide() end end end - + -- Check if already on correct category if self:IsCategorySelectedByData(step.achievementCategory) then if isLastStep then @@ -651,7 +651,7 @@ function Highlight:UpdateGuide() end return end - + -- Character Frame sidebar buttons (Character Stats, Titles, Equipment Manager) if step.sidebarButtonFrame or step.sidebarIndex then -- Check we're on the correct CharacterFrame tab first @@ -663,7 +663,7 @@ function Highlight:UpdateGuide() break end end - + if requiredTabIndex then local currentTab = self:GetCurrentTabIndex(step.waitForFrame or "CharacterFrame") if currentTab and currentTab ~= requiredTabIndex then @@ -677,7 +677,7 @@ function Highlight:UpdateGuide() end end end - + -- Check if the sidebar tab is already selected if self:IsSidebarTabSelected(step.sidebarIndex) then if isLastStep then @@ -687,7 +687,7 @@ function Highlight:UpdateGuide() end return end - + -- Highlight the sidebar tab button local sidebarBtn = self:GetSidebarTabButton(step.sidebarIndex) if sidebarBtn then @@ -707,7 +707,7 @@ function Highlight:UpdateGuide() end return end - + -- Currency header expansion (expand a header section in the Currency tab) if step.currencyHeader then -- Check we're on the correct CharacterFrame tab (Currency = tab 3) @@ -719,7 +719,7 @@ function Highlight:UpdateGuide() break end end - + if requiredTabIndex then local currentTab = self:GetCurrentTabIndex(step.waitForFrame or "CharacterFrame") if currentTab and currentTab ~= requiredTabIndex then @@ -733,10 +733,10 @@ function Highlight:UpdateGuide() end end end - + -- Check if header is already expanded, collapsed, or not in list (parent collapsed) local headerState = self:IsCurrencyHeaderExpanded(step.currencyHeader) - + if headerState == true then -- Header is expanded - advance to next step if isLastStep then @@ -746,7 +746,7 @@ function Highlight:UpdateGuide() end return end - + if headerState == nil then -- Header not found - parent must be collapsed. -- First, try to go back to previous currencyHeader step @@ -795,7 +795,7 @@ function Highlight:UpdateGuide() return end end - + -- Currency row highlight (scroll to and highlight a specific currency by ID) if step.currencyID then -- Check we're on the correct CharacterFrame tab (Currency = tab 3) @@ -807,7 +807,7 @@ function Highlight:UpdateGuide() break end end - + if requiredTabIndex then local currentTab = self:GetCurrentTabIndex(step.waitForFrame or "CharacterFrame") if currentTab and currentTab ~= requiredTabIndex then @@ -821,7 +821,7 @@ function Highlight:UpdateGuide() end end end - + -- Check if ALL parent headers are still expanded; if any collapsed or missing, go back for i = currentStepIndex - 1, 1, -1 do local prevStep = currentGuide.steps[i] @@ -836,7 +836,7 @@ function Highlight:UpdateGuide() -- Don't break - check ALL parent headers in the chain end end - + -- Scroll to the currency and highlight its row self:ScrollToCurrencyRow(step.currencyID) local currencyBtn = self:GetCurrencyRowButton(step.currencyID) @@ -1056,12 +1056,12 @@ function Highlight:UpdateGuide() self:ShowInstruction(step.text) return end - + -- Highlight a region (like PvP talents area) - these are final destinations if step.regionFrames or step.regionFrame then local framePaths = step.regionFrames or { step.regionFrame } local region = nil - + -- Try each possible frame path for _, path in ipairs(framePaths) do local testFrame = self:GetFrameByPath(path) @@ -1070,12 +1070,12 @@ function Highlight:UpdateGuide() break end end - + -- If path-based lookup failed, try text-based search as fallback if not region and step.searchButtonText then region = self:FindRatedPvPButton(step.searchButtonText) end - + if region then -- Found the region, highlight it (no text needed) self:HighlightFrame(region) @@ -1090,7 +1090,7 @@ function Highlight:UpdateGuide() end return end - + -- Search for a button by text (for PvP rated queue buttons like Solo Shuffle, 2v2, 3v3) if step.searchButtonText then local pvpBtn = self:FindRatedPvPButton(step.searchButtonText) @@ -1111,7 +1111,7 @@ end function Highlight:AdvanceStep() self:HideHighlight() currentStepIndex = currentStepIndex + 1 - + if currentStepIndex > #currentGuide.steps then self:Cancel() end @@ -1119,12 +1119,12 @@ end function Highlight:GetFrameByPath(path) if not path then return nil end - + -- Special dynamic lookup for unnamed frames if path == "FIND_PVP_TALENTS" then return self:FindPvPTalentsTray() end - + return Utils.GetFrameByPath(path) end @@ -1134,14 +1134,14 @@ function Highlight:FindPvPTalentsTray() "ClassTalentFrame.TalentsTab.PvPTalentSlotTray", "ClassTalentFrame.PvPTalentSlotTray", } - + for _, path in ipairs(paths) do local frame = Utils.GetFrameByPath(path) if frame and frame:IsShown() then return frame end end - + return nil end @@ -1169,7 +1169,7 @@ function Highlight:IsTabSelected(frameName, tabIndex) end return false end - + -- CollectionsJournal if frameName == "CollectionsJournal" then local frame = CollectionsJournal @@ -1178,7 +1178,7 @@ function Highlight:IsTabSelected(frameName, tabIndex) end return false end - + -- CharacterFrame (Character Info tabs: Character, Reputation, Currency) if frameName == "CharacterFrame" then local frame = CharacterFrame @@ -1194,13 +1194,13 @@ function Highlight:IsTabSelected(frameName, tabIndex) -- Reputation tab if ReputationFrame and ReputationFrame:IsShown() then return true end elseif tabIndex == 3 then - -- Currency tab + -- Currency tab if TokenFrame and TokenFrame:IsShown() then return true end if CurrencyFrame and CurrencyFrame:IsShown() then return true end end return false end - + -- PVEFrame (Group Finder tabs at bottom) if frameName == "PVEFrame" then local frame = PVEFrame @@ -1209,7 +1209,7 @@ function Highlight:IsTabSelected(frameName, tabIndex) end return false end - + -- AchievementFrame if frameName == "AchievementFrame" then local frame = AchievementFrame @@ -1218,7 +1218,7 @@ function Highlight:IsTabSelected(frameName, tabIndex) end return false end - + -- EncounterJournal (Adventure Guide) if frameName == "EncounterJournal" then local frame = EncounterJournal @@ -1226,11 +1226,11 @@ function Highlight:IsTabSelected(frameName, tabIndex) -- Modern Adventure Guide tabs (7 total in modern WoW): -- Tab 1 = Journeys, Tab 2 = Traveler's Log, Tab 3 = Suggested Content -- Tab 4 = Dungeons, Tab 5 = Raids, Tab 6 = Item Sets (Loot), Tab 7 = Tutorials - + -- Check by specific frame visibility local tabContentChecks = { -- Tab 1: Journeys - function() + function() if frame.JourneysFrame and frame.JourneysFrame:IsShown() then return true end return false end, @@ -1255,7 +1255,7 @@ function Highlight:IsTabSelected(frameName, tabIndex) end return false end, - -- Tab 5: Raids + -- Tab 5: Raids function() if frame.instanceSelect and frame.instanceSelect:IsShown() then -- Check if we're filtering to raids @@ -1277,12 +1277,12 @@ function Highlight:IsTabSelected(frameName, tabIndex) return false end, } - + local checkFn = tabContentChecks[tabIndex] if checkFn and checkFn() then return true end - + -- Try PanelTemplates as fallback if PanelTemplates_GetSelectedTab then local selectedTab = PanelTemplates_GetSelectedTab(frame) @@ -1291,7 +1291,7 @@ function Highlight:IsTabSelected(frameName, tabIndex) end return false end - + return false end @@ -1332,7 +1332,7 @@ function Highlight:IsSideTabSelected(frameName, sideTabIndex) end return false end - + return false end @@ -1354,7 +1354,7 @@ function Highlight:IsPvPSideTabSelected(frameName, sideTabIndex) tab3Active = true end end - + if sideTabIndex == 1 then return tab1Active elseif sideTabIndex == 2 then @@ -1373,7 +1373,7 @@ function Highlight:IsPvPSideTabSelected(frameName, sideTabIndex) end return false end - + -- Fallback: check button selected state for any tab local pvpButtons = self:GetPvPSideTabButtons() if pvpButtons and pvpButtons[sideTabIndex] then @@ -1385,19 +1385,19 @@ function Highlight:IsPvPSideTabSelected(frameName, sideTabIndex) if sideTab.Selected and sideTab.Selected:IsShown() then return true end if sideTab.isSelected then return true end end - + return false end - + return false end function Highlight:GetPvPSideTabButtons() -- Find the PvP side tab buttons local buttons = {} - + if not PVPQueueFrame then return buttons end - + -- Primary: CategoryButton1-4 (confirmed via DevFrame) -- CategoryButton1 = Quick Match, 2 = Rated, 3 = Premade Groups, 4 = Training Grounds for i = 1, 4 do @@ -1406,7 +1406,7 @@ function Highlight:GetPvPSideTabButtons() buttons[i] = btn end end - + -- Fallback: try legacy named buttons if CategoryButtons not found if not buttons[1] then local legacyNames = {"HonorButton", "ConquestButton", "LFGListButton", "TrainingGroundsButton"} @@ -1416,7 +1416,7 @@ function Highlight:GetPvPSideTabButtons() end end end - + return buttons end @@ -1427,7 +1427,7 @@ function Highlight:GetPvPSideTabButton(frameName, sideTabIndex) if btn and btn:IsShown() then return btn end - + -- Fallback: Try to find generic category buttons local fallbackNames = { "PVPQueueFrameCategoryButton1", @@ -1437,7 +1437,7 @@ function Highlight:GetPvPSideTabButton(frameName, sideTabIndex) } return _G[fallbackNames[sideTabIndex]] end - + return nil end @@ -1676,7 +1676,7 @@ function Highlight:IsSidebarTabSelected(sidebarIndex) if not CharacterFrame or not CharacterFrame:IsShown() then return false end - + -- Check if the correct sidebar pane is shown if sidebarIndex == 1 then -- Character Stats - check if CharacterStatsPane or the default paperdoll stats view is shown @@ -1689,7 +1689,7 @@ function Highlight:IsSidebarTabSelected(sidebarIndex) -- Equipment Manager pane if PaperDollEquipmentManagerPane and PaperDollEquipmentManagerPane:IsShown() then return true end end - + -- Also check the sidebar tab's visual selected state local sidebarTab = _G["PaperDollSidebarTab" .. sidebarIndex] if sidebarTab then @@ -1699,7 +1699,7 @@ function Highlight:IsSidebarTabSelected(sidebarIndex) if sidebarTab.GetChecked and sidebarTab:GetChecked() then return true end if sidebarTab.IsChecked and sidebarTab:IsChecked() then return true end end - + return false end @@ -1707,13 +1707,13 @@ function Highlight:GetSidebarTabButton(sidebarIndex) if not CharacterFrame or not CharacterFrame:IsShown() then return nil end - + -- Try PaperDollSidebarTab buttons directly (confirmed via Frame Inspector) local sidebarTab = _G["PaperDollSidebarTab" .. sidebarIndex] if sidebarTab and sidebarTab:IsShown() then return sidebarTab end - + -- Try PaperDollSidebarTabs container children local sidebarTabs = _G["PaperDollSidebarTabs"] if not sidebarTabs and PaperDollFrame then @@ -1725,7 +1725,7 @@ function Highlight:GetSidebarTabButton(sidebarIndex) return child end end - + return nil end @@ -1734,15 +1734,15 @@ function Highlight:FindRatedPvPButton(buttonText) if not PVEFrame or not PVEFrame:IsShown() then return nil end - + local searchText = slower(buttonText) - + -- Determine the active sub-panel and search only within it. -- This prevents finding buttons on inactive sub-tabs (e.g. finding -- "Random Battlegrounds" on Quick Match when looking for "Arena Skirmishes" -- on Premade Groups). local searchRoots = {} - + -- PvP sub-panels if HonorFrame and HonorFrame:IsShown() then searchRoots[#searchRoots + 1] = HonorFrame @@ -1756,7 +1756,7 @@ function Highlight:FindRatedPvPButton(buttonText) if TrainingGroundsFrame and TrainingGroundsFrame:IsShown() then searchRoots[#searchRoots + 1] = TrainingGroundsFrame end - + -- PvE sub-panels if LFDParentFrame and LFDParentFrame:IsShown() then searchRoots[#searchRoots + 1] = LFDParentFrame @@ -1767,18 +1767,18 @@ function Highlight:FindRatedPvPButton(buttonText) if LFGListPVEStub and LFGListPVEStub:IsShown() then searchRoots[#searchRoots + 1] = LFGListPVEStub end - + -- LFGListFrame is shared between PvE and PvP premade groups if LFGListFrame and LFGListFrame:IsShown() then searchRoots[#searchRoots + 1] = LFGListFrame end - + -- Search each active sub-panel for _, root in ipairs(searchRoots) do local result = SearchFrameTreeFuzzy(root, searchText) if result then return result end end - + -- Fallback: search entire PVEFrame (for edge cases like Mythic+ or unknown layouts) return SearchFrameTreeFuzzy(PVEFrame, searchText) end @@ -1793,7 +1793,7 @@ function Highlight:GetTabButton(frameName, tabIndex) -- Fallback to global tab buttons local tabBtn = _G["PlayerSpellsFrameTab" .. tabIndex] if tabBtn then return tabBtn end - + -- Try the tab system directly if frame then -- Modern tab system might use TabSystem @@ -1809,27 +1809,27 @@ function Highlight:GetTabButton(frameName, tabIndex) end end end - + -- CollectionsJournal tabs if frameName == "CollectionsJournal" then return _G["CollectionsJournalTab" .. tabIndex] end - + -- CharacterFrame tabs (Character, Reputation, Currency) if frameName == "CharacterFrame" then return _G["CharacterFrameTab" .. tabIndex] end - + -- PVEFrame tabs (at bottom: Dungeons & Raids, Player vs. Player, Mythic+) if frameName == "PVEFrame" then return _G["PVEFrameTab" .. tabIndex] end - + -- AchievementFrame tabs (Achievements, Guild, Statistics) if frameName == "AchievementFrame" then return _G["AchievementFrameTab" .. tabIndex] end - + -- EncounterJournal tabs (Adventure Guide) if frameName == "EncounterJournal" then local frame = EncounterJournal @@ -1837,7 +1837,7 @@ function Highlight:GetTabButton(frameName, tabIndex) -- Modern Adventure Guide tabs (7 total in modern WoW): -- Tab 1 = Journeys, Tab 2 = Traveler's Log, Tab 3 = Suggested Content -- Tab 4 = Dungeons, Tab 5 = Raids, Tab 6 = Item Sets (Loot), Tab 7 = Tutorials - + -- Tab keywords to search for in button names local tabKeywords = { {"Journey", "Journeys"}, -- Tab 1 @@ -1848,7 +1848,7 @@ function Highlight:GetTabButton(frameName, tabIndex) {"Loot", "ItemSet", "Set"}, -- Tab 6 {"Tutorial", "HelpFrame"}, -- Tab 7 } - + -- Helper function to search a container for tab buttons local function findTabInContainer(container, keywords) if not container then return nil end @@ -1872,7 +1872,7 @@ function Highlight:GetTabButton(frameName, tabIndex) end return nil end - + local keywords = tabKeywords[tabIndex] if keywords then -- Try direct frame properties first @@ -1889,29 +1889,29 @@ function Highlight:GetTabButton(frameName, tabIndex) local btn = frame[btnName] if btn then return btn end end - + -- Search in TopNavBar (where modern tabs often live) local btn = findTabInContainer(frame.TopNavBar, keywords) if btn then return btn end - + -- Search in TabBar btn = findTabInContainer(frame.TabBar, keywords) if btn then return btn end - + -- Search in TabSystem btn = findTabInContainer(frame.TabSystem, keywords) if btn then return btn end - + -- Search frame's direct children btn = findTabInContainer(frame, keywords) if btn then return btn end end - + -- Try TabSystem.tabs array by index if frame.TabSystem and frame.TabSystem.tabs then return frame.TabSystem.tabs[tabIndex] end - + -- Try iterating children of various containers by index local containers = {frame.TopNavBar, frame.TabBar, frame.TabSystem, frame} for _, container in ipairs(containers) do @@ -1929,16 +1929,16 @@ function Highlight:GetTabButton(frameName, tabIndex) end end end - + -- Last resort: try global names local tab = _G["EncounterJournalTab" .. tabIndex] if tab then return tab end - + local bottomTab = _G["EncounterJournalBottomTab" .. tabIndex] if bottomTab then return bottomTab end end end - + return nil end @@ -1954,16 +1954,16 @@ function Highlight:GetSideTabButton(frameName, sideTabIndex) if btn and btn:IsShown() then return btn end - + -- Fallback: look for buttons by name local buttonNames = { "GroupFinderFrameGroupButton1", - "GroupFinderFrameGroupButton2", + "GroupFinderFrameGroupButton2", "GroupFinderFrameGroupButton3", } return _G[buttonNames[sideTabIndex]] end - + return nil end @@ -1972,13 +1972,13 @@ function Highlight:HighlightFrame(frame, instructionText) self:HideHighlight() return end - + local bs = highlightFrame.borderSize local pad = 4 - + highlightFrame:ClearAllPoints() highlightFrame:SetAllPoints(frame) - + -- Top and bottom own the corners (full width including padding) highlightFrame.top:ClearAllPoints() highlightFrame.top:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", -pad, 0) @@ -2000,7 +2000,7 @@ function Highlight:HighlightFrame(frame, instructionText) highlightFrame.right:SetPoint("TOPRIGHT", highlightFrame.top, "BOTTOMRIGHT", 0, 0) highlightFrame.right:SetPoint("BOTTOMRIGHT", highlightFrame.bottom, "TOPRIGHT", 0, 0) highlightFrame.right:SetWidth(bs) - + if not highlightFrame:IsShown() then highlightShownAt = GetTime() end @@ -2008,14 +2008,14 @@ function Highlight:HighlightFrame(frame, instructionText) if highlightFrame.animGroup and not highlightFrame.animGroup:IsPlaying() then highlightFrame.animGroup:Play() end - + indicatorFrame:ClearAllPoints() indicatorFrame:SetPoint("BOTTOM", frame, "TOP", 0, 10) indicatorFrame:Show() if indicatorFrame.animGroup and not indicatorFrame.animGroup:IsPlaying() then indicatorFrame.animGroup:Play() end - + if instructionText then self:ShowInstruction(instructionText) elseif instructionFrame then @@ -2025,16 +2025,16 @@ end function Highlight:ShowInstruction(text) instructionFrame.text:SetText(text) - + -- Calculate proper width and height with word wrap local maxWidth = 450 instructionFrame:SetWidth(maxWidth) - + -- Get actual text height after word wrap local textHeight = instructionFrame.text:GetStringHeight() local frameHeight = mmax(90, textHeight + 60) -- 60px for padding and button instructionFrame:SetHeight(frameHeight) - + instructionFrame:ClearAllPoints() instructionFrame:SetPoint("TOP", UIParent, "TOP", 0, -100) instructionFrame:Show() @@ -2074,7 +2074,7 @@ function Highlight:IsPortraitMenuOpen() return true end end - + -- Method 2: Check modern Menu system (11.0+) -- The Menu API creates frames managed by MenuManager if Menu and Menu.GetManager then @@ -2086,12 +2086,12 @@ function Highlight:IsPortraitMenuOpen() end end end - + -- Method 3: Check if UIDROPDOWNMENU is open if UIDROPDOWNMENU_OPEN_MENU and UIDROPDOWNMENU_OPEN_MENU ~= "" then return true end - + -- Method 4: Check for any visible context menu frame by common naming patterns local menuNames = {"PlayerFrameDropDown", "DropDownList1", "UnitPopupWindow"} for _, name in ipairs(menuNames) do @@ -2103,7 +2103,7 @@ function Highlight:IsPortraitMenuOpen() end end end - + return false end @@ -2116,7 +2116,7 @@ function Highlight:GetPortraitMenuFrame() return dropdown end end - + -- Check modern Menu system if Menu and Menu.GetManager then local ok, manager = pcall(Menu.GetManager) @@ -2127,7 +2127,7 @@ function Highlight:GetPortraitMenuFrame() end end end - + -- Fallback: check common frame names local menuNames = {"UnitPopupWindow"} for _, name in ipairs(menuNames) do @@ -2139,13 +2139,13 @@ function Highlight:GetPortraitMenuFrame() end end end - + return nil end function Highlight:FindPortraitMenuOption(optionName) local optionNameLower = slower(optionName) - + local function getFrameText(frame) if not frame then return nil end if frame.GetText then @@ -2164,10 +2164,10 @@ function Highlight:FindPortraitMenuOption(optionName) end return nil end - + local function searchFrame(frame, depth) if not frame or depth > 8 then return nil end - + local childOk, nChildren = pcall(function() return select("#", frame:GetChildren()) end) if not childOk or not nChildren then return nil end for i = 1, nChildren do @@ -2179,14 +2179,14 @@ function Highlight:FindPortraitMenuOption(optionName) -- Found matching option - return it if clickable return child end - + local result = searchFrame(child, depth + 1) if result then return result end end end return nil end - + -- Search DropDownList frames (legacy) for i = 1, 5 do local dropdown = _G["DropDownList" .. i] @@ -2195,7 +2195,7 @@ function Highlight:FindPortraitMenuOption(optionName) if result then return result end end end - + -- Search modern Menu system (11.0+) if Menu and Menu.GetManager then local ok, manager = pcall(Menu.GetManager) @@ -2207,7 +2207,7 @@ function Highlight:FindPortraitMenuOption(optionName) end end end - + -- Fallback: search common menu frame names local menuNames = {"UnitPopupWindow"} for _, name in ipairs(menuNames) do @@ -2220,7 +2220,7 @@ function Highlight:FindPortraitMenuOption(optionName) end end end - + return nil end @@ -2230,10 +2230,10 @@ end -- Returns: true (expanded), false (collapsed), nil (not in list - parent collapsed) function Highlight:IsCurrencyHeaderExpanded(headerName) if not C_CurrencyInfo or not C_CurrencyInfo.GetCurrencyListSize then return nil end - + local headerNameLower = slower(headerName) local size = C_CurrencyInfo.GetCurrencyListSize() - + for i = 1, size do local info = C_CurrencyInfo.GetCurrencyListInfo(i) if info and info.isHeader and info.name and slower(info.name) == headerNameLower then @@ -2449,12 +2449,12 @@ end function Highlight:Cancel() self:HideHighlight() self:HideContextTooltip() - + if stepTicker then stepTicker:Cancel() stepTicker = nil end - + currentGuide = nil currentStepIndex = nil end diff --git a/MapSearch.lua b/MapSearch.lua index 6fa8b4f..01a90cc 100644 --- a/MapSearch.lua +++ b/MapSearch.lua @@ -1168,17 +1168,17 @@ local CATEGORIES = { travel = { keywords = {"travel", "transport", "transportation", "getting around"} }, instance = { keywords = {"instance", "instances", "group content"} }, service = { keywords = {"service", "services", "npc"} }, - + flightmaster = { keywords = {"flight", "fly", "flight master", "flight point", "fp", "taxi"}, parent = "travel" }, zeppelin = { keywords = {"zeppelin", "zep", "airship", "blimp"}, parent = "travel" }, boat = { keywords = {"boat", "ship", "ferry"}, parent = "travel" }, portal = { keywords = {"portal", "portals", "teleport", "mage"}, parent = "travel" }, tram = { keywords = {"tram", "deeprun"}, parent = "travel" }, - + dungeon = { keywords = {"dungeon", "dungeons", "5 man", "5man", "mythic", "heroic"}, parent = "instance" }, raid = { keywords = {"raid", "raids", "raiding"}, parent = "instance" }, delve = { keywords = {"delve", "delves"}, parent = "instance" }, - + bank = { keywords = {"bank", "vault", "storage", "guild bank", "personal bank"}, parent = "service" }, auctionhouse = { keywords = {"auction", "ah", "auction house"}, parent = "service" }, innkeeper = { keywords = {"inn", "innkeeper", "rest", "hearthstone"}, parent = "service" }, @@ -1191,7 +1191,7 @@ local CATEGORIES = { repairvendor = { keywords = {"repair", "repairs", "anvil"}, parent = "service" }, barber = { keywords = {"barber", "barbershop", "appearance", "haircut"}, parent = "service" }, transmogrifier = { keywords = {"transmog", "transmogrifier", "appearance"}, parent = "service" }, - + prof_blacksmithing = { keywords = {"blacksmithing", "bs"}, parent = "service" }, prof_jewelcrafting = { keywords = {"jewelcrafting", "jc"}, parent = "service" }, prof_leatherworking = { keywords = {"leatherworking", "lw"}, parent = "service" }, @@ -1409,14 +1409,14 @@ function MapSearch:CreateSearchFrame() searchFrame:SetMovable(true) searchFrame:EnableMouse(true) searchFrame:SetToplevel(true) - + local yOff = EasyFind.db.mapSearchYOffset or 0 if EasyFind.db.mapSearchPosition then searchFrame:SetPoint("TOPLEFT", WorldMapFrame.ScrollContainer, "BOTTOMLEFT", EasyFind.db.mapSearchPosition, yOff) else searchFrame:SetPoint("TOPLEFT", WorldMapFrame.ScrollContainer, "BOTTOMLEFT", 0, yOff) end - + local WHITE8x8 = "Interface\\BUTTONS\\WHITE8x8" ns.CreateSearchBorder(searchFrame) if (EasyFind.db.resultsTheme or "Classic") == "Retail" then @@ -1481,7 +1481,7 @@ function MapSearch:CreateSearchFrame() self.isDragging = false end end) - + local contentSz = ns.SEARCHBAR_HEIGHT * ns.SEARCHBAR_FILL local iconSz = contentSz * ns.SEARCHBAR_ICON_SCALE local searchIcon = searchFrame:CreateTexture(nil, "ARTWORK") @@ -1505,7 +1505,7 @@ function MapSearch:CreateSearchFrame() placeholder:SetTextColor(0.5, 0.5, 0.5, 1.0) placeholder:SetText("Search within this zone") editBox.placeholder = placeholder - + editBox:SetScript("OnEditFocusGained", function(self) if self.blockFocus then self:ClearFocus() @@ -1529,13 +1529,13 @@ function MapSearch:CreateSearchFrame() MapSearch:ShowPinnedItems() end end) - + editBox:SetScript("OnEditFocusLost", function(self) if self:GetText() == "" then self.placeholder:Show() end end) - + editBox:SetScript("OnTextChanged", function(self) if self:GetText() ~= "" then self.placeholder:Hide() @@ -1544,7 +1544,7 @@ function MapSearch:CreateSearchFrame() activeSearchFrame = searchFrame MapSearch:OnSearchTextChanged(self:GetText()) end) - + editBox:SetScript("OnEnterPressed", function(self) MapSearch:ActivateSelected() end) @@ -1669,7 +1669,7 @@ function MapSearch:CreateSearchFrame() editBox:SetFocus() end end) - + -- Invisible button over the icon to capture hover for tooltip local searchIconHitbox = CreateFrame("Button", nil, searchFrame) searchIconHitbox:SetSize(22, 22) @@ -1718,14 +1718,14 @@ function MapSearch:CreateSearchFrame() globalSearchFrame:SetMovable(true) globalSearchFrame:EnableMouse(true) globalSearchFrame:SetToplevel(true) - + local gYOff = EasyFind.db.mapSearchYOffset or 0 if EasyFind.db.globalSearchPosition then globalSearchFrame:SetPoint("TOPRIGHT", WorldMapFrame.ScrollContainer, "BOTTOMRIGHT", EasyFind.db.globalSearchPosition, gYOff) else globalSearchFrame:SetPoint("TOPRIGHT", WorldMapFrame.ScrollContainer, "BOTTOMRIGHT", 0, gYOff) end - + local WHITE8x8 = "Interface\\BUTTONS\\WHITE8x8" ns.CreateSearchBorder(globalSearchFrame) if (EasyFind.db.resultsTheme or "Classic") == "Retail" then @@ -1789,7 +1789,7 @@ function MapSearch:CreateSearchFrame() self.isDragging = false end end) - + local globalSearchIcon = globalSearchFrame:CreateTexture(nil, "ARTWORK") globalSearchIcon:SetSize(iconSz, iconSz) globalSearchIcon:SetPoint("LEFT", 10, 0) @@ -1811,7 +1811,7 @@ function MapSearch:CreateSearchFrame() globalPlaceholder:SetTextColor(0.5, 0.5, 0.5, 1.0) globalPlaceholder:SetText("Search for zones & instances") globalEditBox.placeholder = globalPlaceholder - + globalEditBox:SetScript("OnEditFocusGained", function(self) if self.blockFocus then self:ClearFocus() @@ -1835,13 +1835,13 @@ function MapSearch:CreateSearchFrame() MapSearch:ShowPinnedItems() end end) - + globalEditBox:SetScript("OnEditFocusLost", function(self) if self:GetText() == "" then self.placeholder:Show() end end) - + globalEditBox:SetScript("OnTextChanged", function(self) if self:GetText() ~= "" then self.placeholder:Hide() @@ -1850,7 +1850,7 @@ function MapSearch:CreateSearchFrame() activeSearchFrame = globalSearchFrame MapSearch:OnSearchTextChanged(self:GetText()) end) - + globalEditBox:SetScript("OnEnterPressed", function(self) MapSearch:ActivateSelected() end) @@ -1955,7 +1955,7 @@ function MapSearch:CreateSearchFrame() globalEditBox:HookScript("OnTextChanged", function(self) globalClearBtn:SetShown(self:GetText() ~= "") end) - + -- Shift-click editbox starts parent drag when not focused globalEditBox:HookScript("OnMouseDown", function(self) if IsShiftKeyDown() and not self:HasFocus() then @@ -1974,7 +1974,7 @@ function MapSearch:CreateSearchFrame() globalEditBox:SetFocus() end end) - + -- Invisible button over the icon to capture hover for tooltip local globalIconHitbox = CreateFrame("Button", nil, globalSearchFrame) globalIconHitbox:SetSize(22, 22) @@ -2610,7 +2610,7 @@ function MapSearch:CreateHighlightFrame() highlightFrame:SetFrameStrata("TOOLTIP") highlightFrame:SetFrameLevel(2000) highlightFrame:Hide() - + local top = highlightFrame:CreateTexture(nil, "OVERLAY") top:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) highlightFrame.top = top @@ -2632,7 +2632,7 @@ function MapSearch:CreateHighlightFrame() indicatorFrame:SetSize(ns.ICON_SIZE, ns.ICON_SIZE) indicatorFrame:SetPoint("BOTTOM", highlightFrame, "TOP", 0, 2) ns.CreateIndicatorTextures(indicatorFrame) - + local animGroup = highlightFrame:CreateAnimationGroup() animGroup:SetLooping("BOUNCE") local alpha = animGroup:CreateAnimation("Alpha") @@ -2652,14 +2652,14 @@ function MapSearch:CreateHighlightFrame() indAlpha:SetToAlpha(0.4) indAlpha:SetDuration(ANIM_DURATION) indicatorFrame.animGroup = indAnimGroup - + -- Create static location pin - shows the icon for locations from database waypointPin = CreateFrame("Frame", "EasyFindLocationPin", WorldMapFrame.ScrollContainer.Child) waypointPin:SetSize(64, 64) -- Large icon for visibility waypointPin:SetFrameStrata("HIGH") waypointPin:SetFrameLevel(2000) waypointPin:Hide() - + -- Enable mouse for hover tooltip + click-to-navigate (local search) -- or hover-to-dismiss (global search) waypointPin:EnableMouse(true) @@ -2697,11 +2697,11 @@ function MapSearch:CreateHighlightFrame() MapSearch:ClearAll() end end) - + local wpIcon = waypointPin:CreateTexture(nil, "ARTWORK") wpIcon:SetAllPoints() waypointPin.icon = wpIcon - + -- Add a pulsing glow effect around the icon (ALWAYS YELLOW - this is a pin, not an arrow) local glow = waypointPin:CreateTexture(nil, "BACKGROUND") glow:SetSize(100, 100) @@ -2710,7 +2710,7 @@ function MapSearch:CreateHighlightFrame() glow:SetVertexColor(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 0.8) glow:SetBlendMode("ADD") waypointPin.glow = glow - + -- Animation for the location pin glow local pinAnimGroup = waypointPin:CreateAnimationGroup() pinAnimGroup:SetLooping("BOUNCE") @@ -2729,10 +2729,10 @@ function MapSearch:CreateZoneHighlightFrame() zoneHighlightFrame:SetFrameLevel(400) zoneHighlightFrame:SetAllPoints(WorldMapFrame.ScrollContainer.Child) zoneHighlightFrame:Hide() - + -- Store references to zone highlight textures zoneHighlightFrame.highlights = {} - + -- Create a pool of highlight textures we can reuse (ALWAYS YELLOW) for i = 1, 10 do local highlight = zoneHighlightFrame:CreateTexture("EasyFindZoneHighlight"..i, "OVERLAY") @@ -2741,7 +2741,7 @@ function MapSearch:CreateZoneHighlightFrame() highlight:Hide() zoneHighlightFrame.highlights[i] = highlight end - + -- Animation for pulsing effect local animGroup = zoneHighlightFrame:CreateAnimationGroup() animGroup:SetLooping("BOUNCE") @@ -2750,7 +2750,7 @@ function MapSearch:CreateZoneHighlightFrame() alpha:SetToAlpha(0.5) alpha:SetDuration(ANIM_DURATION) zoneHighlightFrame.animGroup = animGroup - + -- Create indicator for zone highlighting local zoneInd = CreateFrame("Frame", "EasyFindZoneIndicator", WorldMapFrame.ScrollContainer.Child) zoneInd:SetSize(ns.ICON_SIZE, ns.ICON_SIZE) @@ -2778,10 +2778,10 @@ end function MapSearch:GetDirectChildZones(mapID) mapID = mapID or WorldMapFrame:GetMapID() if not mapID then return {} end - + local zones = {} local seen = {} - + -- Get all direct children (not recursive) local children = GetMapChildrenInfo(mapID, nil, false) -- false = not recursive if children then @@ -2801,7 +2801,7 @@ function MapSearch:GetDirectChildZones(mapID) end end end - + return zones end @@ -2810,7 +2810,7 @@ function MapSearch:GetMapHierarchy(mapID) local hierarchy = {} local currentID = mapID local maxDepth = 10 -- Safety limit - + while currentID and maxDepth > 0 do local mapInfo = GetMapInfo(currentID) if mapInfo then @@ -2825,7 +2825,7 @@ function MapSearch:GetMapHierarchy(mapID) end maxDepth = maxDepth - 1 end - + return hierarchy end @@ -2934,10 +2934,10 @@ local ZONE_ABBREVIATIONS = { function MapSearch:SearchZones(query) if not query or query == "" then return {} end - + query = slower(query) local zones - + if isGlobalSearch then if cachedWorldZones then zones = cachedWorldZones @@ -2971,25 +2971,25 @@ function MapSearch:SearchZones(query) -- Local: only direct children of current map zones = self:GetDirectChildZones() end - + local matches = {} local abbrevTarget = ZONE_ABBREVIATIONS[query] -- check once outside loop - + for _, zone in ipairs(zones) do local nameLower = slower(zone.name) local score = ns.Database:ScoreName(nameLower, query, #query) - + -- Check abbreviation match (e.g. "sw" → "stormwind city") if abbrevTarget and nameLower == abbrevTarget then score = mmax(score, 200) -- Treat as exact match end - + if score >= 50 then zone.score = score tinsert(matches, zone) end end - + -- Sort by score, then by name tsort(matches, function(a, b) if a.score ~= b.score then @@ -2997,7 +2997,7 @@ function MapSearch:SearchZones(query) end return a.name < b.name end) - + return matches end @@ -3015,7 +3015,7 @@ function MapSearch:GroupZonesByParent(zones) end return tostring(zone.parentMapID or 0) end - + local function getPathDisplay(zone) if zone.path and #zone.path > 0 then local parts = {} @@ -3026,12 +3026,12 @@ function MapSearch:GroupZonesByParent(zones) end return zone.parentName or "" end - + -- First pass: count how many zones share each full parent path local pathCounts = {} local pathDisplay = {} local pathParentMapID = {} - + for _, zone in ipairs(zones) do local pathKey = getPathKey(zone) pathCounts[pathKey] = (pathCounts[pathKey] or 0) + 1 @@ -3045,19 +3045,19 @@ function MapSearch:GroupZonesByParent(zones) end end end - + -- Second pass: build result list local result = {} local processedPaths = {} - + for _, zone in ipairs(zones) do local pathKey = getPathKey(zone) - + if processedPaths[pathKey] then -- Already processed this path group, skip else processedPaths[pathKey] = true - + -- Collect all zones with this same parent path local groupZones = {} for _, z in ipairs(zones) do @@ -3065,15 +3065,15 @@ function MapSearch:GroupZonesByParent(zones) tinsert(groupZones, z) end end - + -- Sort zones within the group alphabetically tsort(groupZones, function(a, b) return a.name < b.name end) - + -- Only create a grouped header if there are 2+ zones with the same parent local isGrouped = #groupZones >= 2 - + tinsert(result, { parentMapID = pathParentMapID[pathKey], parentPath = pathDisplay[pathKey], @@ -3082,12 +3082,12 @@ function MapSearch:GroupZonesByParent(zones) }) end end - + -- Sort groups by their parent path alphabetically so related items appear together tsort(result, function(a, b) return (a.parentPath or "") < (b.parentPath or "") end) - + return result end @@ -3243,12 +3243,12 @@ end -- Highlight a zone on the continent map using the actual zone shape texture function MapSearch:HighlightZone(mapID) DebugPrint("[EasyFind] HighlightZone called for mapID:", mapID) - - if not zoneHighlightFrame then + + if not zoneHighlightFrame then DebugPrint("[EasyFind] HighlightZone: no zoneHighlightFrame!") - return + return end - + -- Save pending zone navigation before clearing (we might be highlighting an intermediate zone) local savedPending = self.pendingZoneHighlight DebugPrint("[EasyFind] HighlightZone: saved pending:", savedPending) @@ -3259,17 +3259,17 @@ function MapSearch:HighlightZone(mapID) -- Restore pending zone navigation self.pendingZoneHighlight = savedPending DebugPrint("[EasyFind] HighlightZone: restored pending:", self.pendingZoneHighlight) - + local canvas = WorldMapFrame.ScrollContainer.Child - if not canvas then + if not canvas then DebugPrint("[EasyFind] HighlightZone: no canvas!") - return + return end - + local mapInfo = GetMapInfo(mapID) - if not mapInfo then + if not mapInfo then DebugPrint("[EasyFind] HighlightZone: no mapInfo for", mapID) - return + return end DebugPrint("[EasyFind] HighlightZone: zone name:", mapInfo.name, "mapType:", mapInfo.mapType) @@ -3558,13 +3558,13 @@ function MapSearch:HighlightZone(mapID) end end end - + DebugPrint("[EasyFind] HighlightZone: About to show frame") zoneHighlightFrame:Show() DebugPrint("[EasyFind] HighlightZone: zoneHighlightFrame:IsShown() =", zoneHighlightFrame:IsShown()) zoneHighlightFrame.animGroup:Play() DebugPrint("[EasyFind] HighlightZone: highlight and frame shown") - + -- Position indicator with smart bounds checking if zoneHighlightFrame.indicator then local zoneInd = zoneHighlightFrame.indicator @@ -3626,27 +3626,27 @@ function MapSearch:HighlightZone(mapID) else DebugPrint("[EasyFind] HighlightZone: no indicator frame!") end - + DebugPrint("[EasyFind] HighlightZone: COMPLETE for zone:", mapInfo.name) - + return true end function MapSearch:ClearZoneHighlight() if not zoneHighlightFrame then return end - + for _, highlight in ipairs(zoneHighlightFrame.highlights) do highlight:SetTexture(nil) highlight:SetTexCoord(0, 1, 0, 1) highlight:Hide() end - + if zoneHighlightFrame.border then for _, border in pairs(zoneHighlightFrame.border) do border:Hide() end end - + if zoneHighlightFrame.centerGlow then zoneHighlightFrame.centerGlow:Hide() if zoneHighlightFrame.centerGlowAnim then @@ -3668,9 +3668,9 @@ function MapSearch:ClearZoneHighlight() if zoneHighlightFrame.animGroup then zoneHighlightFrame.animGroup:Stop() end - + zoneHighlightFrame:Hide() - + -- Also clear breadcrumb highlight if self.breadcrumbHighlight then if self.breadcrumbHighlight.indicatorFrame then @@ -3684,7 +3684,7 @@ function MapSearch:ClearZoneHighlight() end self.breadcrumbHighlight:Hide() -- OnHide unlocks the button highlight automatically end - + -- Clear pending zone navigation (but NOT pendingWaypoint - that's the final -- destination waypoint and must survive through the zone navigation chain) self.pendingZoneHighlight = nil @@ -3693,35 +3693,35 @@ end -- Highlight a zone with step-by-step navigation guidance (teaching mode) function MapSearch:HighlightZoneOnMap(targetMapID, zoneName) DebugPrint("[EasyFind] HighlightZoneOnMap called for targetMapID:", targetMapID) - + local targetInfo = GetMapInfo(targetMapID) - if not targetInfo then + if not targetInfo then DebugPrint("[EasyFind] ERROR: No targetInfo for mapID", targetMapID) - return + return end - + DebugPrint("[EasyFind] Target zone:", targetInfo.name) - + local targetParentMapID = ZONE_PARENT_OVERRIDES[targetMapID] or targetInfo.parentMapID if not targetParentMapID then DebugPrint("[EasyFind] No parent, going directly to zone") WorldMapFrame:SetMapID(targetMapID) return end - + if ZONE_PARENT_OVERRIDES[targetMapID] then DebugPrint("[EasyFind] Using parent override for", targetMapID, "→", targetParentMapID) end - + local targetParentInfo = GetMapInfo(targetParentMapID) DebugPrint("[EasyFind] Target parent:", targetParentInfo and targetParentInfo.name or "nil", "ID:", targetParentMapID) - + local currentMapID = WorldMapFrame:GetMapID() - if not currentMapID then + if not currentMapID then DebugPrint("[EasyFind] ERROR: No currentMapID") - return + return end - + if currentMapID == targetMapID then DebugPrint("[EasyFind] Already viewing target zone, nothing to do") return @@ -3848,7 +3848,7 @@ function MapSearch:HighlightZoneOnMap(targetMapID, zoneName) -- Build paths from root (World) to each map local targetParentPath = self:GetMapPath(targetParentMapID) local currentPath = self:GetMapPath(currentMapID) - + DebugPrint("[EasyFind] Target parent path:") for i, p in ipairs(targetParentPath) do DebugPrint(" ", i, p.name, "ID:", p.mapID) @@ -3857,7 +3857,7 @@ function MapSearch:HighlightZoneOnMap(targetMapID, zoneName) for i, p in ipairs(currentPath) do DebugPrint(" ", i, p.name, "ID:", p.mapID) end - + -- Find the DEEPEST common ancestor (DCA) local dcaIndex = 0 local dcaMapID = nil @@ -3869,10 +3869,10 @@ function MapSearch:HighlightZoneOnMap(targetMapID, zoneName) break end end - + local dcaInfo = dcaMapID and GetMapInfo(dcaMapID) DebugPrint("[EasyFind] DCA:", dcaInfo and dcaInfo.name or "nil", "ID:", dcaMapID, "Index:", dcaIndex) - + if not dcaMapID then DebugPrint("[EasyFind] ERROR: No common ancestor, falling back to direct nav") WorldMapFrame:SetMapID(targetParentMapID) @@ -3881,7 +3881,7 @@ function MapSearch:HighlightZoneOnMap(targetMapID, zoneName) end) return end - + -- CASE 2: Current map IS the deepest common ancestor if currentMapID == dcaMapID then DebugPrint("[EasyFind] CASE 2: We're at DCA, need to go DOWN toward target") @@ -3903,7 +3903,7 @@ function MapSearch:HighlightZoneOnMap(targetMapID, zoneName) end return end - + -- CASE 2b: Current zone-level map geographically contains the target even -- though it's not in the API parent chain (e.g. Azuremyst Isle contains -- Exodar, but Exodar's API parent is Kalimdor). Try HighlightZone directly @@ -3933,7 +3933,7 @@ function MapSearch:GetMapPath(mapID) local path = {} local currentID = mapID local maxDepth = 15 - + while currentID and maxDepth > 0 do local info = GetMapInfo(currentID) if info then @@ -3944,14 +3944,14 @@ function MapSearch:GetMapPath(mapID) end maxDepth = maxDepth - 1 end - + return path end -- Highlight a breadcrumb button to guide user to zoom out toward the target function MapSearch:HighlightBreadcrumbForNavigation(dcaMapID, finalTargetMapID, targetParentPath, dcaIndex) DebugPrint("[EasyFind] HighlightBreadcrumbForNavigation: DCA=", dcaMapID, "finalTarget=", finalTargetMapID) - + self:ClearZoneHighlight() local navBar = WorldMapFrame.NavBar @@ -4020,7 +4020,7 @@ end -- Find a breadcrumb button for a given map ID function MapSearch:FindBreadcrumbButton(navBar, mapID) DebugPrint("[EasyFind] FindBreadcrumbButton looking for mapID:", mapID) - + -- The NavBar in WoW uses a different structure - buttons are direct children -- Let's iterate through children to find the right button for i = 1, select("#", navBar:GetChildren()) do @@ -4035,7 +4035,7 @@ function MapSearch:FindBreadcrumbButton(navBar, mapID) return child end end - + -- Check the navigation list - the button might be the entry itself if navBar.navList then DebugPrint("[EasyFind] navList has", #navBar.navList, "entries") @@ -4058,7 +4058,7 @@ function MapSearch:FindBreadcrumbButton(navBar, mapID) else DebugPrint("[EasyFind] navBar.navList is nil!") end - + -- Check home button (usually World/Cosmic) if navBar.home and navBar.home:IsShown() then -- Cosmic map is always the home button; API name "Cosmic" differs @@ -4084,7 +4084,7 @@ function MapSearch:FindBreadcrumbButton(navBar, mapID) else DebugPrint("[EasyFind] No home button or not shown") end - + -- Last resort: look for WorldMapNavBarButton frames local buttonName = "WorldMapNavBarButton" for i = 1, 10 do @@ -4276,10 +4276,10 @@ end function MapSearch:IsOnContinentMap() local mapID = WorldMapFrame:GetMapID() if not mapID then return false end - + local mapInfo = GetMapInfo(mapID) if not mapInfo then return false end - + -- Continent type is 2, World is 1 return mapInfo.mapType == Enum.UIMapType.Continent or mapInfo.mapType == Enum.UIMapType.World end @@ -4556,7 +4556,7 @@ function MapSearch:GetCategoryMatch(query) local matchedCategory = nil local matchScore = 0 local isExactCategoryMatch = false - + for catName, catData in pairs(CATEGORIES) do for _, keyword in ipairs(catData.keywords) do local kw = slower(keyword) @@ -4571,28 +4571,28 @@ function MapSearch:GetCategoryMatch(query) end end end - + return matchedCategory, matchScore, isExactCategoryMatch end function MapSearch:GetRelatedCategories(category) local related = {category} local catData = CATEGORIES[category] - + -- Only add parent, NOT siblings -- Siblings are only included when searching for the parent category itself if catData and catData.parent then tinsert(related, catData.parent) -- Do NOT add sibling categories - that causes "pvp" to show "auction house" end - + -- Add children of this category (if searching for a parent like "service" or "travel") for catName, data in pairs(CATEGORIES) do if data.parent == category then tinsert(related, catName) end end - + return related end @@ -4992,9 +4992,9 @@ end function MapSearch:GetStaticLocations() local mapID = WorldMapFrame:GetMapID() if not mapID then return {} end - + local results = {} - + -- Get built-in static locations local locations = STATIC_LOCATIONS[mapID] if locations then @@ -5010,7 +5010,7 @@ function MapSearch:GetStaticLocations() }) end end - + -- Also check EasyFindDevDB for dev/testing (raw POIs from recorder) -- Skip dev POIs whose names already exist in built-in static locations if EasyFindDevDB and EasyFindDevDB.rawPOIs then @@ -5034,7 +5034,7 @@ function MapSearch:GetStaticLocations() end end end - + return results end @@ -5042,10 +5042,10 @@ function MapSearch:ScanMapPOIs() local pois = {} local mapID = WorldMapFrame:GetMapID() if not mapID then return pois end - + local canvas = WorldMapFrame.ScrollContainer and WorldMapFrame.ScrollContainer.Child if not canvas then return pois end - + -- First: Use WoW's API to get Area POIs directly (boats, zeppelins, portals, etc) -- Only include POIs we can categorize as useful (travel, services) -- Skip generic area POIs like landmarks, zone markers, events @@ -5057,7 +5057,7 @@ function MapSearch:ScanMapPOIs() local category = nil -- Start with nil, only add if we categorize it local poiName = slower(poiInfo.name or "") local desc = slower(poiInfo.description or "") - + -- Only categorize POIs we actually want to show if sfind(poiName, "zeppelin") or sfind(poiName, "airship") or sfind(desc, "zeppelin") then category = "zeppelin" @@ -5089,7 +5089,7 @@ function MapSearch:ScanMapPOIs() elseif sfind(poiName, "chromie") then category = "chromie" end - + -- Only add POIs we've explicitly categorized (skips generic landmarks, zone markers, events, etc.) if category then tinsert(pois, { @@ -5106,7 +5106,7 @@ function MapSearch:ScanMapPOIs() end end end - + -- Delve entrances via dedicated API (not returned by GetAreaPOIForMap) if GetDelvesForMap then local delveIDs = GetDelvesForMap(mapID) @@ -5139,18 +5139,18 @@ function MapSearch:ScanMapPOIs() end end end - + return pois end function MapSearch:GetPinInfo(pin) if not pin or not pin:IsShown() then return nil end - + local name = nil local icon = nil local pinType = "unknown" local category = nil - + -- Flight masters - handled by ScanFlightMasters() with proper zone filtering if pin.taxiNodeData then return nil @@ -5170,12 +5170,12 @@ function MapSearch:GetPinInfo(pin) y = pin.normalizedY, } end - + -- Area POIs (boats, zeppelins, portals, etc) - but NOT quests if pin.areaPoiInfo then name = pin.areaPoiInfo.name or pin.areaPoiInfo.description pinType = "areapoi" - + local poiName = slower(name or "") local poiDesc = slower(pin.areaPoiInfo.description or "") if sfind(poiName, "zeppelin") or sfind(poiName, "airship") then @@ -5208,7 +5208,7 @@ function MapSearch:GetPinInfo(pin) return nil end end - + -- Vignettes (rares, treasures) if pin.vignetteInfo then name = pin.vignetteInfo.name @@ -5218,22 +5218,22 @@ function MapSearch:GetPinInfo(pin) category = "treasure" end end - + -- SKIP quests entirely - don't include them if pin.questID then return nil end - + -- SKIP world quests too if pin.worldQuest then return nil end - + -- Dungeon/Raid instances - handled by ScanDungeonEntrances() with proper zone filtering if pin.journalInstanceID then return nil end - + -- Scan pin regions for atlas-based icons (shows the real map pin icon) -- Also used to identify unknown pin types by their atlas name local skipAtlas = { ["Waypoint-MapPin-Tracked"] = true, ["Waypoint-MapPin-Untracked"] = true, ["UI-QuestPoi-OuterGlow"] = true } @@ -5279,7 +5279,7 @@ function MapSearch:GetPinInfo(pin) end end end - + if not name or name == "" then return nil end @@ -5345,11 +5345,11 @@ function MapSearch:OnSearchTextChanged(text) end return end - + -- Clear any previous zone highlights and POI highlights self:ClearZoneHighlight() self:ClearHighlight() - + -- Search for zones (works for both local and global mode) local zoneMatches = {} if self:IsOnContinentMap() or isGlobalSearch then @@ -5527,7 +5527,7 @@ function MapSearch:OnSearchTextChanged(text) end end end - + local results = self:SearchPOIs(allPOIs, text) -- Apply global search filters (zones / dungeons / raids / delves) @@ -5613,10 +5613,10 @@ function MapSearch:SearchPOIs(pois, query) local results = reuseSearchResults local seen = reuseSearchSeen local duplicates = reuseSearchDuplicates - + local matchedCategory, catScore, isExactCategoryMatch = self:GetCategoryMatch(query) local relatedCategories = matchedCategory and self:GetRelatedCategories(matchedCategory) or {} - + -- First pass: name matches for _, poi in ipairs(pois) do local nameLower = slower(poi.name) @@ -5646,14 +5646,14 @@ function MapSearch:SearchPOIs(pois, query) score = score + 200 end end - + if score >= 50 then -- Track all instances in duplicates table if not duplicates[key] then duplicates[key] = {} end tinsert(duplicates[key], poi) - + -- Only add to results once (first instance) if not seen[key] then seen[key] = true @@ -5663,7 +5663,7 @@ function MapSearch:SearchPOIs(pois, query) end end end - + -- Second pass: ALWAYS include category matches when user typed a category keyword -- (typing "dungeon" shows ALL dungeons, not just name matches) if matchedCategory then @@ -5678,7 +5678,7 @@ function MapSearch:SearchPOIs(pois, query) if poi.category == matchedCategory then score = 150 end - + -- Related category match (e.g., search "travel" shows all travel types) if score == 0 then for _, relCat in ipairs(relatedCategories) do @@ -5688,14 +5688,14 @@ function MapSearch:SearchPOIs(pois, query) end end end - + if score > 0 then -- Track all instances in duplicates table if not duplicates[key] then duplicates[key] = {} end tinsert(duplicates[key], poi) - + if not seen[key] then seen[key] = true poi.score = score @@ -5706,14 +5706,14 @@ function MapSearch:SearchPOIs(pois, query) end end end - + -- Attach duplicates info to results for _, result in ipairs(results) do if result.duplicateKey and duplicates[result.duplicateKey] then result.allInstances = duplicates[result.duplicateKey] end end - + -- Sort results by score tsort(results, function(a, b) -- Zone results come before POI results at equal scores @@ -5735,7 +5735,7 @@ function MapSearch:ShowResults(results) self:HideResults() return end - + local count = mmin(#results, MAX_RESULTS_POOL) -- When maximized, bars are at top so results always drop downward local resultsAbove = EasyFind.db.mapResultsAbove and not mapIsMaximized @@ -6208,7 +6208,7 @@ function MapSearch:SelectResult(data) self:NavigateToEntrance(data.name, data.x, data.y, data.icon, data.category, data.entranceMapID) return end - + -- Check if this POI has multiple instances (duplicates) if data.allInstances and #data.allInstances > 1 then -- Show ALL instances on the map @@ -6247,17 +6247,17 @@ function MapSearch:ShowMultipleWaypoints(instances) local canvas = WorldMapFrame.ScrollContainer.Child if not canvas then return end - + local canvasWidth, canvasHeight = canvas:GetSize() local userScale = EasyFind.db.iconScale or 0.8 local ms = ns.MULTI_SCALE -- slightly smaller for clusters - + local iconSize = ns.UIToCanvas(ns.PIN_SIZE * ms) * userScale local glowSize = ns.UIToCanvas(ns.PIN_GLOW_SIZE * ms) * userScale local highlightSize = ns.UIToCanvas(ns.HIGHLIGHT_SIZE * ms) * userScale local indicatorSize = ns.UIToCanvas(ns.ICON_SIZE * ms) * userScale local indicatorGlowSize = ns.UIToCanvas(ns.ICON_GLOW_SIZE* ms) * userScale - + -- Create additional waypoint pins if needed if not self.extraPins then self.extraPins = {} @@ -6268,7 +6268,7 @@ function MapSearch:ShowMultipleWaypoints(instances) if not self.extraIndicators then self.extraIndicators = {} end - + -- Show each instance with pin, highlight box, and indicator for i, instance in ipairs(instances) do if instance.x and instance.y then @@ -6285,18 +6285,18 @@ function MapSearch:ShowMultipleWaypoints(instances) local extraPin = CreateFrame("Frame", "EasyFindExtraPin"..(i-1), canvas) extraPin:SetFrameStrata("HIGH") extraPin:SetFrameLevel(1999) - + local icon = extraPin:CreateTexture(nil, "ARTWORK") icon:SetAllPoints() extraPin.icon = icon - + local glow = extraPin:CreateTexture(nil, "BACKGROUND") glow:SetPoint("CENTER") glow:SetTexture(STAR_GLOW_TEXTURE) glow:SetVertexColor(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 0.8) -- Pin glow always yellow glow:SetBlendMode("ADD") extraPin.glow = glow - + local animGroup = extraPin:CreateAnimationGroup() animGroup:SetLooping("BOUNCE") local pulse = animGroup:CreateAnimation("Alpha") @@ -6335,13 +6335,13 @@ function MapSearch:ShowMultipleWaypoints(instances) self.extraPins[i-1] = extraPin end pin = self.extraPins[i-1] - + -- Create or reuse extra highlight boxes if not self.extraHighlights[i-1] then local extraHighlight = CreateFrame("Frame", "EasyFindExtraHighlight"..(i-1), canvas) extraHighlight:SetFrameStrata("HIGH") extraHighlight:SetFrameLevel(1998) - + local top = extraHighlight:CreateTexture(nil, "OVERLAY") top:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) extraHighlight.top = top @@ -6357,7 +6357,7 @@ function MapSearch:ShowMultipleWaypoints(instances) local right = extraHighlight:CreateTexture(nil, "OVERLAY") right:SetColorTexture(YELLOW_HIGHLIGHT[1], YELLOW_HIGHLIGHT[2], YELLOW_HIGHLIGHT[3], 1) extraHighlight.right = right - + local animGroup = extraHighlight:CreateAnimationGroup() animGroup:SetLooping("BOUNCE") local alpha = animGroup:CreateAnimation("Alpha") @@ -6365,11 +6365,11 @@ function MapSearch:ShowMultipleWaypoints(instances) alpha:SetToAlpha(0.4) alpha:SetDuration(ANIM_DURATION) extraHighlight.animGroup = animGroup - + self.extraHighlights[i-1] = extraHighlight end highlight = self.extraHighlights[i-1] - + -- Create or reuse extra indicators if not self.extraIndicators[i-1] then local extraInd = CreateFrame("Frame", "EasyFindExtraIndicator"..(i-1), canvas) @@ -6392,7 +6392,7 @@ function MapSearch:ShowMultipleWaypoints(instances) end ind = self.extraIndicators[i-1] end - + -- Position and show the pin pin:SetSize(iconSize, iconSize) pin:ClearAllPoints() @@ -6406,11 +6406,11 @@ function MapSearch:ShowMultipleWaypoints(instances) iconTexture = instance.icon end SetIconTexture(pin.icon, iconTexture) - + if pin.glow then pin.glow:SetSize(glowSize, glowSize) end - + pin:Show() -- Position and show the highlight box @@ -6459,9 +6459,9 @@ function MapSearch:ShowWaypointAt(x, y, icon, category) local canvas = WorldMapFrame.ScrollContainer.Child if not canvas then return end - + local canvasWidth, canvasHeight = canvas:GetSize() - + -- Convert UI-unit sizes to canvas units so they appear the same screen size local userScale = EasyFind.db.iconScale or 0.8 local iconSize = ns.UIToCanvas(ns.PIN_SIZE) * userScale @@ -6469,11 +6469,11 @@ function MapSearch:ShowWaypointAt(x, y, icon, category) local highlightSize = ns.UIToCanvas(ns.HIGHLIGHT_SIZE) * userScale local indicatorSize = ns.UIToCanvas(ns.ICON_SIZE) * userScale local indicatorGlowSize = ns.UIToCanvas(ns.ICON_GLOW_SIZE) * userScale - + -- Resize the pin and glow waypointPin:SetSize(iconSize, iconSize) waypointPin.glow:SetSize(glowSize, glowSize) - + -- Use category icon if no specific icon provided local iconTexture = GetCategoryIcon(category or "unknown") if icon then @@ -6515,27 +6515,27 @@ end function MapSearch:HighlightPin(pin) waypointPin:Hide() - + if not pin or not pin:IsShown() then self:ClearHighlight() return end - + currentHighlightedPin = pin - + -- Convert UI-unit sizes to canvas units local userScale = EasyFind.db.iconScale or 0.8 - + local width, height = pin:GetSize() local minPinSize = ns.UIToCanvas(36) * userScale width = mmax(width or 24, minPinSize) height = mmax(height or 24, minPinSize) - + local indicatorSize = ns.UIToCanvas(ns.ICON_SIZE) * userScale local indicatorGlowSize = ns.UIToCanvas(ns.ICON_GLOW_SIZE) * userScale indicatorFrame:SetSize(indicatorSize, indicatorSize) indicatorFrame.glow:SetSize(indicatorGlowSize, indicatorGlowSize) - + highlightFrame:SetSize(width, height) highlightFrame:ClearAllPoints() highlightFrame:SetPoint("CENTER", pin, "CENTER", 0, 0) @@ -6599,7 +6599,7 @@ function MapSearch:ClearHighlight() if arr.animGroup then arr.animGroup:Stop() end end end - + currentHighlightedPin = nil end @@ -6953,18 +6953,18 @@ end -- Called when the user changes the icon scale setting. function MapSearch:UpdateIconScales() -- Updates all visible pins, highlights, and arrows in real-time. - + local canvas = WorldMapFrame.ScrollContainer.Child if not canvas then return end - + local userScale = EasyFind.db.iconScale or 0.8 - + local iconSize = ns.UIToCanvas(ns.PIN_SIZE) * userScale local glowSize = ns.UIToCanvas(ns.PIN_GLOW_SIZE) * userScale local highlightSize = ns.UIToCanvas(ns.HIGHLIGHT_SIZE) * userScale local indicatorSize = ns.UIToCanvas(ns.ICON_SIZE) * userScale local indicatorGlowSize = ns.UIToCanvas(ns.ICON_GLOW_SIZE) * userScale - + -- Helper: resize an indicator frame + its textures local function resizeIndicator(frame, aSize, gSize) if not frame then return end @@ -6972,7 +6972,7 @@ function MapSearch:UpdateIconScales() if frame.indicator then frame.indicator:SetSize(aSize, aSize) end if frame.glow then frame.glow:SetSize(gSize, gSize) end end - + -- Update main waypoint pin if waypointPin then waypointPin:SetSize(iconSize, iconSize) @@ -6980,7 +6980,7 @@ function MapSearch:UpdateIconScales() waypointPin.glow:SetSize(glowSize, glowSize) end end - + -- Update main highlight frame if highlightFrame and highlightFrame:IsShown() then highlightFrame:SetSize(highlightSize, highlightSize) @@ -6995,7 +6995,7 @@ function MapSearch:UpdateIconScales() if zoneHighlightFrame and zoneHighlightFrame.indicator then resizeIndicator(zoneHighlightFrame.indicator, zoneIndSize, zoneIndGlowSize) end - + -- Update extra pins for duplicates local ms = ns.MULTI_SCALE local multiIconSize = ns.UIToCanvas(ns.PIN_SIZE * ms) * userScale @@ -7003,7 +7003,7 @@ function MapSearch:UpdateIconScales() local multiHighlightSize = ns.UIToCanvas(ns.HIGHLIGHT_SIZE * ms) * userScale local multiIndSize = ns.UIToCanvas(ns.ICON_SIZE * ms) * userScale local multiIndGlowSize = ns.UIToCanvas(ns.ICON_GLOW_SIZE* ms) * userScale - + if self.extraPins then for _, pin in ipairs(self.extraPins) do if pin:IsShown() then @@ -7014,7 +7014,7 @@ function MapSearch:UpdateIconScales() end end end - + if self.extraHighlights then for _, hl in ipairs(self.extraHighlights) do if hl:IsShown() then @@ -7022,7 +7022,7 @@ function MapSearch:UpdateIconScales() end end end - + if self.extraIndicators then for _, arr in ipairs(self.extraIndicators) do resizeIndicator(arr, multiIndSize, multiIndGlowSize) diff --git a/UI.lua b/UI.lua index 6d4fb30..286b7d1 100644 --- a/UI.lua +++ b/UI.lua @@ -343,7 +343,7 @@ function UI:Initialize() searchFrame.hoverZone:Show() end end - + inCombat = InCombatLockdown() if inCombat then searchFrame:Hide() @@ -406,7 +406,7 @@ function UI:CreateSearchFrame() searchFrame:SetMovable(true) searchFrame:EnableMouse(true) searchFrame:SetClampedToScreen(true) - + -- Apply saved position or default if EasyFind.db.uiSearchPosition then local pos = EasyFind.db.uiSearchPosition @@ -537,7 +537,7 @@ function UI:CreateSearchFrame() EasyFind.db.uiSearchPosition = {point, relPoint, x, y} end end) - + local placeholder = editBox:CreateFontString(nil, "ARTWORK", ns.SEARCHBAR_FONT) placeholder:SetPoint("LEFT", 2, 0) placeholder:SetPoint("RIGHT", editBox, "RIGHT", -2, 0) @@ -546,7 +546,7 @@ function UI:CreateSearchFrame() placeholder:SetTextColor(0.5, 0.5, 0.5, 1.0) placeholder:SetText("Search your UI here") editBox.placeholder = placeholder - + editBox:SetScript("OnEditFocusGained", function(self) if self.blockFocus then self:ClearFocus() @@ -562,7 +562,7 @@ function UI:CreateSearchFrame() UI:ShowPinnedItems() end end) - + editBox:SetScript("OnEditFocusLost", function(self) -- Skip cleanup when SelectResult is actively clearing text/focus if selectingResult then return end @@ -585,28 +585,28 @@ function UI:CreateSearchFrame() end) end end) - + editBox:SetScript("OnTextChanged", function(self) if self:GetText() ~= "" then self.placeholder:Hide() end UI:OnSearchTextChanged(self:GetText()) end) - + editBox:SetScript("OnEnterPressed", function(self) UI:ActivateSelected() end) - + editBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() -- Text and results stay visible; user can click back in to resume end) - + -- Clear-text X button (grey circle X, matching retail quest log style) -- Only visible when there is text in the editbox. local clearTextBtn = Utils.CreateClearButton(searchFrame, "EasyFindClearTextButton") clearTextBtn:SetFrameLevel(searchFrame:GetFrameLevel() + 10) - + clearTextBtn:SetScript("OnClick", function() editBox:SetText("") editBox:ClearFocus() @@ -674,12 +674,12 @@ function UI:CreateSearchFrame() editBox:SetFocus() end end) - + -- Show/hide the clear-text X based on whether there's text editBox:HookScript("OnTextChanged", function(self) clearTextBtn:SetShown(self:GetText() ~= "") end) - + -- Key repeat with progressive acceleration for held arrow/tab keys. -- Starts at REPEAT_INITIAL delay, accelerates toward REPEAT_FAST over REPEAT_ACCEL seconds. local REPEAT_INITIAL = 0.30 @@ -944,11 +944,11 @@ function UI:CreateSearchFrame() local point, _, relPoint, x, y = self:GetPoint() EasyFind.db.uiSearchPosition = {point, relPoint, x, y} end) - + -- Apply saved scale self:UpdateScale() self:UpdateOpacity() - + -- Movement fade: reduce opacity while player is moving (like the world map) local MOVE_FADE_FACTOR = 0.4 local moveFading = false -- true when alpha is reduced due to movement @@ -968,11 +968,11 @@ function UI:CreateSearchFrame() hoverZone:SetPoint("CENTER", searchFrame, "CENTER", 0, 0) hoverZone:Hide() searchFrame.hoverZone = hoverZone - + -- Track whether the mouse is over the zone or the bar local smartShowVisible = false local smartShowTimer = nil - + local function SmartShowFadeIn() if smartShowTimer then smartShowTimer:Cancel(); smartShowTimer = nil end if EasyFind.db.visible == false then return end @@ -982,7 +982,7 @@ function UI:CreateSearchFrame() searchFrame:Show() end end - + local function SmartShowFadeOut() if EasyFind.db.visible == false then return end -- Don't hide if the editbox has focus or contains text @@ -1005,7 +1005,7 @@ function UI:CreateSearchFrame() end) end) end - + hoverZone:SetScript("OnEnter", SmartShowFadeIn) hoverZone:SetScript("OnLeave", SmartShowFadeOut) searchFrame:HookScript("OnEnter", function() @@ -1014,7 +1014,7 @@ function UI:CreateSearchFrame() searchFrame:HookScript("OnLeave", function() if EasyFind.db.smartShow then SmartShowFadeOut() end end) - + searchFrame.smartShowFadeIn = SmartShowFadeIn searchFrame.smartShowFadeOut = SmartShowFadeOut searchFrame.smartShowVisible = function() return smartShowVisible end @@ -1313,14 +1313,14 @@ function UI:CreateResultsFrame() resultsFrame:SetPoint("TOP", searchFrame, "BOTTOM", 0, 2) resultsFrame:SetFrameStrata("MEDIUM") resultsFrame:SetFrameLevel(searchFrame:GetFrameLevel() + 1) - + resultsFrame:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 20, insets = { left = 5, right = 5, top = 5, bottom = 5 } }) - + resultsFrame:Hide() local resizeTimer @@ -1623,7 +1623,7 @@ function UI:CreateResultButton(index) tabText:SetJustifyH("LEFT") tabText:SetTextColor(0.60, 0.58, 0.55, 1.0) -- muted gray (normal state) resultRow.tabText = tabText - + -- Hover handlers: brighten tab bg, text near-white, icon bright yellow headerTab:SetScript("OnEnter", function(self) local parent = self:GetParent() @@ -1666,7 +1666,7 @@ function UI:CreateResultButton(index) unearnedTooltip:Hide() end end) - + -- Tab selection highlight (keyboard nav, child of headerTab) local tabSelTex = headerTab:CreateTexture(nil, "BACKGROUND") tabSelTex:SetAllPoints() @@ -1709,7 +1709,7 @@ function UI:CreateResultButton(index) branch:Hide() resultRow.treeBranch[d] = branch end - + local icon = resultRow:CreateTexture(nil, "ARTWORK") icon:SetSize(16, 16) icon:SetPoint("LEFT", 0, 0) @@ -2181,15 +2181,15 @@ function UI:ShowHierarchicalResults(hierarchical, preserveScroll) return end if not resultsFrame then return end - + -- Cache the FULL (unfiltered) list so collapse toggles can re-render cachedHierarchical = hierarchical - + local theme = GetActiveTheme() local rowH = theme.rowHeight local indPx = theme.indentPx local padT = theme.resultsPadTop - + -- Apply theme backdrop to results frame resultsFrame:SetBackdrop(theme.resultsBackdrop) if theme.resultsBackdropColor then @@ -2200,7 +2200,7 @@ function UI:ShowHierarchicalResults(hierarchical, preserveScroll) end local customW = EasyFind.db.uiResultsWidth resultsFrame:SetWidth((customW and customW > 1) and customW or theme.resultsWidth) - + -- Apply background atlas if specified (e.g. quest log background) if not resultsFrame.bgAtlasTex then local tex = resultsFrame:CreateTexture(nil, "BACKGROUND", nil, -1) @@ -2217,7 +2217,7 @@ function UI:ShowHierarchicalResults(hierarchical, preserveScroll) resultsFrame.bgAtlasTex:Hide() resultsFrame:SetClipsChildren(false) end - + -- ---------------------------------------------------------------- -- Build the visible list by filtering out children of collapsed nodes -- ---------------------------------------------------------------- @@ -2438,7 +2438,7 @@ function UI:ShowHierarchicalResults(hierarchical, preserveScroll) end end end - + -- Header styling resultRow._isMatch = entry.isMatch and entry.isPathNode if entry.isPinHeader then @@ -3036,7 +3036,7 @@ function UI:ShowHierarchicalResults(hierarchical, preserveScroll) end end end - + -- Show/hide pin separator between pinned items and search results if resultsFrame.pinSeparator then if hasResultsAfterPins then @@ -3743,11 +3743,11 @@ end function UI:ClickCharacterSidebar(sidebarIndex) -- The sidebar buttons are PaperDollSidebarTab1/2/3 inside PaperDollSidebarTabs -- (confirmed via Frame Inspector) - + if not CharacterFrame or not CharacterFrame:IsShown() then return false end - + -- Switch to the Character tab (tab 1) first if PanelTemplates_GetSelectedTab and PanelTemplates_GetSelectedTab(CharacterFrame) ~= 1 then ClickButton(_G["CharacterFrameTab1"]) @@ -3778,13 +3778,13 @@ function UI:ClickCharacterSidebar(sidebarIndex) return ClickButton(select(sidebarIndex, sidebarTabs:GetChildren())) end end - + -- Method 3: Try the ToggleSidebarTab function if available if PaperDollFrame and PaperDollFrame.ToggleSidebarTab then PaperDollFrame:ToggleSidebarTab(sidebarIndex) return true end - + return false end @@ -3793,7 +3793,7 @@ function UI:ClickAchievementCategory(categoryName) if not AchievementFrame or not AchievementFrame:IsShown() then return false end - + local categoryNameLower = slower(categoryName) -- Primary: use the data provider to find the category and select it via Blizzard API @@ -3846,9 +3846,9 @@ function UI:ClickAchievementCategory(categoryName) end end end - + end - + return false end @@ -3908,7 +3908,7 @@ function UI:Hide() searchFrame.editBox:ClearFocus() searchFrame.editBox.placeholder:Show() EasyFind.db.visible = false - + searchFrame.hoverZone:SetShown(EasyFind.db.smartShow) end @@ -3959,7 +3959,7 @@ end -- Helper function to open the player portrait right-click menu function UI:OpenPortraitMenu() if not PlayerFrame then return end - + -- Method 1: Modern WoW - PlayerFrame has a dropdown system via PlayerFrameDropDown local dropDown = _G["PlayerFrameDropDown"] if dropDown then @@ -3968,19 +3968,19 @@ function UI:OpenPortraitMenu() return end end - + -- Method 2: Try Click() which goes through the WoW frame pipeline if PlayerFrame.Click then pcall(PlayerFrame.Click, PlayerFrame, "RightButton") return end - + -- Method 3: Try UnitPopup API if UnitPopup_ShowMenu then UnitPopup_ShowMenu(PlayerFrame, "SELF", "player") return end - + -- Method 4: Modern Menu system if PlayerFrame.unit and Menu and Menu.ModifyMenu then -- Try to invoke the right-click behavior via secure handler @@ -3993,12 +3993,12 @@ end -- Helper function to click a portrait menu option by name function UI:ClickPortraitMenuOption(optionName) local optionNameLower = slower(optionName) - + -- Search through open dropdown frames for the matching button -- Modern WoW uses the Menu system local function searchFrame(frame, depth) if not frame or depth > 5 then return false end - + for i = 1, select("#", frame:GetChildren()) do local child = select(i, frame:GetChildren()) if child and child:IsShown() then @@ -4014,17 +4014,17 @@ function UI:ClickPortraitMenuOption(optionName) end end end - + if text and sfind(slower(text), optionNameLower) then if ClickButton(child) then return true end end - + if searchFrame(child, depth + 1) then return true end end end return false end - + -- Search common dropdown/menu frames for i = 1, 5 do local dropdown = _G["DropDownList" .. i] @@ -4032,7 +4032,7 @@ function UI:ClickPortraitMenuOption(optionName) if searchFrame(dropdown, 0) then return true end end end - + -- Also check UIParent children for modern menu frames for i = 1, select("#", UIParent:GetChildren()) do local child = select(i, UIParent:GetChildren()) @@ -4043,7 +4043,7 @@ function UI:ClickPortraitMenuOption(optionName) end end end - + return false end @@ -4490,15 +4490,15 @@ end -- Flash a label on the search frame (used for Currency hint) function UI:FlashLabel(labelText) if not searchFrame or not searchFrame.label then return end - + local label = searchFrame.label local originalText = label:GetText() local originalR, originalG, originalB = label:GetTextColor() - + -- Set to the hint text label:SetText(labelText) label:SetTextColor(GOLD_COLOR[1], GOLD_COLOR[2], GOLD_COLOR[3]) - + -- Create flash animation local flashCount = 0 local ticker