Files
VimFinder/finder.lua
T
2026-10-24 00:00:00 +00:00

1163 lines
51 KiB
Lua

-- Search the player's known and currently exposed game content.
-- Nothing in this module scans the full spell database or chooses an item.
-- -- VimFinder is a standalone addon: it needs only the client. Another addon
-- -- (VimWow) may replace the hooks below to fit the finder into its own
-- -- keys, bag and window focus.
-- VimFinder is a standalone addon: it needs only the client and the shared
-- options window (VimWowSettings), where its tab sets its key. Another addon
-- (VimWow) may replace the hooks below to fit the finder into its bag and
-- window focus.
-- VimWowFinder = {}
-- local Finder = VimWowFinder
-- Finder.owner = CreateFrame("Frame", "VimWowFinderOwner", UIParent)
-- Finder.capture = CreateFrame("Frame", "VimWowFinderCapture", UIParent)
-- Finder.toggle = CreateFrame("Button", "VimWowFinderToggle", UIParent)
-- Finder.closeButton = CreateFrame("Button", "VimWowFinderClose", UIParent)
-- Finder.up = CreateFrame("Button", "VimWowFinderUp", UIParent)
-- Finder.down = CreateFrame("Button", "VimWowFinderDown", UIParent)
VimFinder = {}
local Finder = VimFinder
Finder.owner = CreateFrame("Frame", "VimFinderOwner", UIParent)
Finder.capture = CreateFrame("Frame", "VimFinderCapture", UIParent)
Finder.toggle = CreateFrame("Button", "VimFinderToggle", UIParent)
Finder.closeButton = CreateFrame("Button", "VimFinderClose", UIParent)
Finder.up = CreateFrame("Button", "VimFinderUp", UIParent)
Finder.down = CreateFrame("Button", "VimFinderDown", UIParent)
Finder.results = {}
Finder.rows = {}
-- -- The key binding window lists the finder under its own header.
-- BINDING_HEADER_VIMFINDER = "VimFinder"
-- BINDING_NAME_VIMFINDER = "Open the finder"
-- -- Alt+Space opens the finder until the player binds the finder a key of
-- -- their own in the key binding window.
-- The finder's key is set on its tab in VimWow Settings, which names it
-- by this; the game's key binding window does not list it.
-- BINDING_NAME_VIMFINDER = "Open the finder"
-- The finder's key is set on its tab in VimWow Settings. Without
-- ClassicAPI's override bindings that key is a game key binding to the
-- Bindings.xml command, which the key binding window lists too.
BINDING_HEADER_VIMFINDER = "VimFinder"
BINDING_NAME_VIMFINDER = "Open the finder"
local DEFAULT_KEYS = {"ALT-SPACE"}
-- Hooks. Alone, the finder takes its keys unless a key is being set in
-- VimWow Settings, opens the bags and claims no window focus.
function Finder:IsActive()
-- return true
return not VimWowBindingsCapturing
end
-- The keys the finder binds itself while closed, as first, second: those
-- set on its tab in VimWow Settings, else Alt+Space.
function Finder:OpenKeys()
-- if type(GetBindingKey) == "function" and GetBindingKey("VIMFINDER") then
-- return nil, nil
-- end
-- return DEFAULT_KEYS[1], DEFAULT_KEYS[2]
if VimWowSettings and VimWowSettings.BindingKeys then
return VimWowSettings:BindingKeys("VimFinder", "VIMFINDER", DEFAULT_KEYS)
end
return DEFAULT_KEYS[1], DEFAULT_KEYS[2]
end
-- A choice is about to open a window (now, or once the server answers
-- within the given seconds).
function Finder:ExpectWindow(seconds)
end
-- Opens the bags, before the finder closes. Opening, never toggling, keeps
-- an open bag open.
function Finder:ShowBags()
if type(OpenBackpack) == "function" then OpenBackpack() end
if type(OpenBag) == "function" and type(GetContainerNumSlots) == "function" then
for bag = 1, 4 do
if (GetContainerNumSlots(bag) or 0) > 0 then OpenBag(bag) end
end
end
end
-- After the finder closed on a bag choice: the item's bag and slot, or
-- nothing for Disenchant, whose item the player picks.
function Finder:SelectBagSlot(bag, slot)
end
local function visible(name)
local frame = getglobal(name)
return frame and frame:IsVisible()
end
local function inCombat()
return type(UnitAffectingCombat) == "function"
and UnitAffectingCombat("player")
end
local function add(list, kind, name, detail, index, book)
if name and name ~= "" then
local item = {kind = kind, name = name, detail = detail or kind,
index = index, book = book}
table.insert(list, item)
return item
end
end
-- local professions = {"Alchemy", "Blacksmithing", "Cooking", "Enchanting",
-- "Engineering", "First Aid", "Leatherworking", "Tailoring"}
-- Fishing casts toward the player's facing and needs no target.
local professions = {"Alchemy", "Blacksmithing", "Cooking", "Enchanting",
"Engineering", "First Aid", "Fishing", "Leatherworking", "Tailoring"}
-- These spellbook entries are combat controls or require a chosen target.
-- They add noise to a finder intended for inspection and safe utility casts.
local hiddenSpells = {attack = true, shoot = true, attach = true,
["auto attack"] = true, ["auto shot"] = true}
-- -- The spell API does not identify target-free casts. Keep this list narrow:
-- -- these summon a companion beside the player, with no unit or ground target.
-- local companionSummons = {["summon imp"] = true,
-- ["summon voidwalker"] = true, ["summon succubus"] = true,
-- ["summon incubus"] = true, ["summon felhunter"] = true,
-- ["summon felguard"] = true}
-- The spell API does not identify target-free casts. Keep this list narrow:
-- these summon a companion or place an object beside the player, with no
-- unit or ground target.
local companionSummons = {["summon imp"] = true,
["summon voidwalker"] = true, ["summon succubus"] = true,
["summon incubus"] = true, ["summon felhunter"] = true,
["summon felguard"] = true, ["basic campfire"] = true}
local function isProfession(name)
for _, profession in ipairs(professions) do
if name == profession or name == getglobal(string.upper(
string.gsub(profession, " ", "_"))) then return true end
end
end
local function canActivate(item)
-- if item.kind == "tracking" or item.kind == "trade"
-- or item.kind == "craft" then return true end
-- if item.kind == "tracking" or item.kind == "trade"
-- or item.kind == "craft" or item.kind == "bag" then return true end
-- if item.kind == "tracking" or item.kind == "trade"
-- or item.kind == "craft" or item.kind == "bag"
-- or item.kind == "window" then return true end
if item.kind == "tracking" or item.kind == "trade"
or item.kind == "craft" or item.kind == "bag"
or item.kind == "window" or item.kind == "recipe" then return true end
if item.kind ~= "spell" then return false end
if hiddenSpells[string.lower(item.name)] then return false end
if item.name == "Disenchant" or isProfession(item.name) then return true end
if type(IsPassiveSpell) == "function"
and IsPassiveSpell(item.index, "spell") then return false end
if type(IsHarmfulSpell) == "function"
and IsHarmfulSpell(item.index, "spell") then return false end
if companionSummons[string.lower(item.name)] then return true end
local spellID
if type(GetSpellInfo) == "function" then
local _, _, _, _, _, _, _, _, _, id = GetSpellInfo(item.index, "spell")
spellID = id
end
if C_Spell and type(C_Spell.IsSelfBuff) == "function" and spellID
and C_Spell.IsSelfBuff(spellID) then return true end
if type(IsHelpfulSpell) == "function"
and type(IsHarmfulSpell) == "function"
and IsHelpfulSpell(item.index, "spell") then
return "self"
end
return false
end
-- Every known recipe of the character's professions, their windows open
-- or not (the user, 2026-10-18: derived from the professions the
-- character has, never listed by hand). With ClassicAPI: a profession is a
-- spellbook spell whose skill line (C_SpellBook.GetSpellSkillLine) the
-- character has (GetSkillLineRank) and that has craftable recipes
-- (C_TradeSkillUI.GetTradeSkillListRecipes, the line's full list from the
-- client's data); the recipes known are IsPlayerSpell's. A scan runs at
-- each opening, so a recipe learned since shows at once.
local recipeLines = {} -- skillID -> {{spellID, createdItem}, ...}, read once
local skillOfSpell = {} -- spellID -> {skillID, line name} or false, read once
local function recipeAPI()
return C_SpellBook and type(C_SpellBook.GetSpellSkillLine) == "function"
and type(C_SpellBook.GetSkillLineRank) == "function"
and C_TradeSkillUI and type(C_TradeSkillUI.GetTradeSkillListRecipes) == "function"
and type(GetSpellInfo) == "function" and type(IsPlayerSpell) == "function"
end
local function lineRecipes(skillID)
if recipeLines[skillID] == nil then
local recipes = {}
for _, row in ipairs(C_TradeSkillUI.GetTradeSkillListRecipes(skillID, "") or {}) do
local spellID = row.spellID or row[1]
-- if spellID then
-- A recipe has skill-up levels; a class line's item spells
-- (Demonology's Create Soulstone) have none, so that line is no
-- profession and Enter never casts one of its spells to open
-- it (FD11, 2026-10-24). VimWow's native recipe icons take the
-- same rows.
local grey = row.trivialLevel or row[3] or 0
local green = row.greenLevel or row[4] or 0
if spellID and (grey > 0 or green > 0) then
table.insert(recipes, {spellID = spellID, createdItem = row.createdItem or row[5]})
end
end
recipeLines[skillID] = recipes
end
return recipeLines[skillID]
end
-- The professions with recipes among the spellbook's spells (list's
-- "spell" entries): {{name, skillID, slot}, ...}, the spell named as its
-- line (Cooking, not Basic Campfire) opening the window.
local function professionLines(list)
local lines, byID = {}, {}
if not recipeAPI() then return lines end
for _, item in ipairs(list) do
if item.kind == "spell" then
local _, _, _, _, _, _, _, _, _, spellID = GetSpellInfo(item.index, "spell")
if spellID and skillOfSpell[spellID] == nil then
local lineName, skillID = C_SpellBook.GetSpellSkillLine(spellID)
skillOfSpell[spellID] = skillID and {skillID, lineName} or false
end
local skill = spellID and skillOfSpell[spellID]
if skill and C_SpellBook.GetSkillLineRank(skill[1])
and table.getn(lineRecipes(skill[1])) > 0 then
local line = byID[skill[1]]
if not line then
line = {name = skill[2] or item.name, skillID = skill[1], slot = item.index}
byID[skill[1]] = line
table.insert(lines, line)
end
if item.name == skill[2] then line.slot = item.index end
end
end
end
return lines
end
Finder.ProfessionLines = professionLines
function Finder:Scan()
local list = {}
if type(GetNumSpellTabs) == "function" and type(GetSpellTabInfo) == "function"
and type(GetSpellName) == "function" then
for tab = 1, GetNumSpellTabs() or 0 do
local _, _, offset, count = GetSpellTabInfo(tab)
for slot = (offset or 0) + 1, (offset or 0) + (count or 0) do
local name, rank = GetSpellName(slot, "spell")
local detail = "Known spell"
if rank and rank ~= "" then detail = detail .. " · " .. rank end
if name and not hiddenSpells[string.lower(name)] then
add(list, "spell", name, detail, slot, "spell")
end
end
end
end
if type(HasPetSpells) == "function" and type(GetSpellName) == "function" then
local count = HasPetSpells()
if type(count) == "number" then
local book = BOOKTYPE_PET or "pet"
for slot = 1, count do
local name, rank = GetSpellName(slot, book)
local detail = "Known pet spell"
if rank and rank ~= "" then detail = detail .. " · " .. rank end
add(list, "petspell", name, detail, slot, book)
end
end
end
if type(GetNumTrackingTypes) == "function"
and type(GetTrackingInfo) == "function" then
for i = 1, GetNumTrackingTypes() or 0 do
local name, _, active, _, spellID = GetTrackingInfo(i)
local tracker = add(list, "tracking", name,
active and "Tracking is on · Enter to turn off"
or "Tracking is off · Enter to turn on", i, spellID)
if tracker then
tracker.active = active
for _, spell in ipairs(list) do
if spell.kind == "spell" and spell.name == name then
tracker.spellSlot = spell.index
break
end
end
end
end
end
if visible("TradeSkillFrame") and type(GetNumTradeSkills) == "function"
and type(GetTradeSkillInfo) == "function" then
for i = 1, GetNumTradeSkills() or 0 do
local name, kind = GetTradeSkillInfo(i)
if kind ~= "header" then
add(list, "trade", name, "Profession recipe · Enter to select", i)
end
end
end
if visible("CraftFrame") and type(GetNumCrafts) == "function"
and type(GetCraftInfo) == "function" then
for i = 1, GetNumCrafts() or 0 do
local name, _, kind = GetCraftInfo(i)
if kind ~= "header" then
add(list, "craft", name, "Craft recipe · Enter to select", i)
end
end
end
-- the known recipes of every profession (above), but those of the
-- window open, listed already
local listed = {}
for _, item in ipairs(list) do
if item.kind == "trade" or item.kind == "craft" then listed[item.name] = true end
end
for _, line in ipairs(professionLines(list)) do
for _, recipe in ipairs(lineRecipes(line.skillID)) do
if IsPlayerSpell(recipe.spellID) then
local name = GetSpellInfo(recipe.spellID)
if name and not listed[name] then
listed[name] = true
local item = add(list, "recipe", name, line.name .. " recipe · Enter to open",
recipe.spellID, line.slot)
if item then item.createdItem = recipe.createdItem end
end
end
end
end
if type(GetContainerNumSlots) == "function"
and type(GetContainerItemLink) == "function" then
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) or 0 do
local link = GetContainerItemLink(bag, slot)
-- local name = link and string.match(link, "%[([^%]]+)%]")
-- Lua 5.0 has no string.match (ClassicAPI adds it).
local _, _, name = string.find(link or "", "%[([^%]]+)%]")
-- if name then add(list, "bag", name,
-- "In bag " .. bag .. ", slot " .. slot, slot, bag) end
if name then add(list, "bag", name, "In bag " .. bag
.. ", slot " .. slot .. " · Enter to show", slot, bag) end
end
end
end
-- Every window the client's key bindings open, keyed or not: the
-- toggles under the Interface header of its key binding list, by the
-- client's own names.
if type(GetNumBindings) == "function" and type(GetBinding) == "function"
and type(RunBinding) == "function" then
local header
for i = 1, GetNumBindings() or 0 do
local command = GetBinding(i)
if command and string.sub(command, 1, 6) == "HEADER" then
header = command
elseif command and header == "HEADER_INTERFACE"
and (string.find(command, "^TOGGLE") or string.find(command, "^OPEN")) then
add(list, "window", getglobal("BINDING_NAME_" .. command) or command,
"Window · Enter to open or close", i, command)
end
end
end
self.catalog = list
self:Search()
end
-- A contiguous match ranks ahead of initials/subsequence matches.
local function score(name, query)
if query == "" then return 0 end
local lower = string.lower(name)
local start = string.find(lower, query, 1, true)
if start then return 100 - start end
local from, gaps = 1, 0
for i = 1, string.len(query) do
local found = string.find(lower, string.sub(query, i, i), from, true)
if not found then return nil end
gaps = gaps + found - from
from = found + 1
end
return 20 - gaps
end
-- The bag search highlights slots with the same fuzzy match.
Finder.Score = score
-- Use a content identity rather than a spell slot or bag position, which can
-- change between sessions. Kind keeps similarly named actions separate.
local function usageKey(item)
return item.kind .. ":" .. item.name
end
-- local function useCount(item)
-- local counts = type(VimWowDB) == "table" and VimWowDB.finderUsage
local function useCount(item)
local counts = type(VimFinderDB) == "table" and VimFinderDB.usage
local count = type(counts) == "table" and counts[usageKey(item)]
return type(count) == "number" and count > 0 and count or 0
end
-- function Finder:RecordUse(item)
-- if type(VimWowDB) ~= "table" then return end
-- if type(VimWowDB.finderUsage) ~= "table" then
-- VimWowDB.finderUsage = {}
-- end
-- local key = usageKey(item)
-- VimWowDB.finderUsage[key] = useCount(item) + 1
-- end
-- How often each choice was used, saved per account in VimFinderDB.
function Finder:RecordUse(item)
if type(VimFinderDB) ~= "table" then VimFinderDB = {} end
if type(VimFinderDB.usage) ~= "table" then
VimFinderDB.usage = {}
end
local key = usageKey(item)
VimFinderDB.usage[key] = useCount(item) + 1
end
function Finder:Search()
local query = string.lower(self.edit:GetText() or "")
local matches = {}
for _, item in ipairs(self.catalog or {}) do
local rank = score(item.name, query)
if rank then
item.usage = useCount(item)
-- Familiar choices can break close search matches, but cannot
-- overtake a much better text match.
item.score = rank + (query ~= "" and math.min(item.usage * 2, 10) or 0)
table.insert(matches, item)
end
end
table.sort(matches, function(a, b)
if query == "" and a.usage ~= b.usage then
return a.usage > b.usage
end
if a.score ~= b.score then return a.score > b.score end
if a.usage ~= b.usage then return a.usage > b.usage end
if a.name ~= b.name then return a.name < b.name end
if a.kind ~= b.kind then
if a.kind == "tracking" then return true end
if b.kind == "tracking" then return false end
end
return a.kind < b.kind
end)
self.results = matches
self.selected = 1
self:Draw()
end
function Finder:Tooltip(item)
if not GameTooltip then return end
GameTooltip:SetOwner(self.frame, "ANCHOR_NONE")
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("BOTTOMLEFT", self.frame, "BOTTOMRIGHT", 12, 0)
local shown, supported
if (item.kind == "spell" or item.kind == "petspell")
and GameTooltip.SetSpell then
supported = true
shown = GameTooltip:SetSpell(item.index, item.book)
elseif item.kind == "tracking" and item.spellSlot
and GameTooltip.SetSpell then
supported = true
shown = GameTooltip:SetSpell(item.spellSlot, "spell")
elseif item.kind == "tracking" and item.book
and GameTooltip.SetSpellByID then
supported = true
shown = GameTooltip:SetSpellByID(item.book)
elseif item.kind == "trade" and GameTooltip.SetTradeSkillItem then
supported = true
shown = GameTooltip:SetTradeSkillItem(item.index)
elseif item.kind == "craft" and GameTooltip.SetCraftSpell then
supported = true
shown = GameTooltip:SetCraftSpell(item.index)
elseif item.kind == "bag" and GameTooltip.SetBagItem then
supported = true
shown = GameTooltip:SetBagItem(item.book, item.index)
-- a recipe: the item it makes, else the recipe's spell
elseif item.kind == "recipe" and item.createdItem and item.createdItem > 0
and GameTooltip.SetHyperlink then
supported = true
shown = GameTooltip:SetHyperlink("item:" .. item.createdItem .. ":0:0:0")
elseif item.kind == "recipe" and GameTooltip.SetSpellByID then
supported = true
shown = GameTooltip:SetSpellByID(item.index)
end
if not supported or shown == false then
if GameTooltip.SetText then GameTooltip:SetText(item.name) end
if GameTooltip.AddLine then GameTooltip:AddLine(item.detail) end
end
GameTooltip:Show()
end
-- The window's layout: the dialog look of the Game Menu and popups.
local WIDTH, HEIGHT = 520, 300
local ROW_LEFT, ROW_TOP, ROW_HEIGHT = 26, 60, 24
local ROW_WIDTH = WIDTH - 2 * ROW_LEFT
local WINDOW_ICON = "Interface\\Icons\\INV_Misc_Book_09"
local UNKNOWN_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
-- Each result's icon, as the client's own windows show it.
local function iconFor(item)
local texture
if (item.kind == "spell" or item.kind == "petspell")
and type(GetSpellTexture) == "function" then
texture = GetSpellTexture(item.index, item.book)
elseif item.kind == "tracking" and type(GetTrackingInfo) == "function" then
local _
_, texture = GetTrackingInfo(item.index)
elseif item.kind == "trade" and type(GetTradeSkillIcon) == "function" then
texture = GetTradeSkillIcon(item.index)
elseif item.kind == "craft" and type(GetCraftIcon) == "function" then
texture = GetCraftIcon(item.index)
elseif item.kind == "bag" and type(GetContainerItemInfo) == "function" then
texture = GetContainerItemInfo(item.book, item.index)
elseif item.kind == "window" then
texture = WINDOW_ICON
-- elseif item.kind == "recipe" and type(GetSpellInfo) == "function" then
-- local _
-- _, _, texture = GetSpellInfo(item.index)
-- A recipe shows the item it makes, as the profession window does: many
-- crafting spells have only the client's placeholder face as their own
-- icon. The spell's icon is kept for a recipe that makes no item
-- (enchanting) or whose item the client has not cached yet.
elseif item.kind == "recipe" then
local _
if item.createdItem and item.createdItem > 0 and type(GetItemInfo) == "function" then
_, _, _, _, _, _, _, _, texture = GetItemInfo(item.createdItem)
end
if not texture and type(GetSpellInfo) == "function" then
_, _, texture = GetSpellInfo(item.index)
end
end
return texture or UNKNOWN_ICON
end
-- function Finder:Draw()
-- local count = table.getn(self.results)
-- if self.selected > count then self.selected = count end
-- if self.selected < 1 then self.selected = 1 end
-- local first = math.max(1, self.selected - 5)
-- self.selectionBackground:Hide()
-- for i, row in ipairs(self.rows) do
-- local item = self.results[first + i - 1]
-- if item then
-- row:SetText((first + i - 1 == self.selected and "> " or " ")
-- .. item.name .. " — " .. item.detail)
-- if canActivate(item) then row:SetTextColor(1, 1, 1)
-- else row:SetTextColor(0.52, 0.52, 0.52) end
-- if first + i - 1 == self.selected then
-- self.selectionBackground:ClearAllPoints()
-- self.selectionBackground:SetPoint("TOPLEFT", self.frame,
-- "TOPLEFT", 13, -40 - i * 23 + 2)
-- self.selectionBackground:Show()
-- end
-- row:Show()
-- else row:Hide() end
-- end
-- Each row: the result's icon, its name in white (grey when Enter only
-- shows it), and what it is on the right; the gold quest log bar marks the
-- selected one.
function Finder:Draw()
local count = table.getn(self.results)
if self.selected > count then self.selected = count end
if self.selected < 1 then self.selected = 1 end
local first = math.max(1, self.selected - 5)
self.selectionBackground:Hide()
for i, row in ipairs(self.rows) do
local item = self.results[first + i - 1]
local detail, icon = self.details[i], self.icons[i]
if item then
row:SetText(item.name)
detail:SetText(item.detail)
icon:SetTexture(iconFor(item))
if canActivate(item) then
row:SetTextColor(1, 1, 1)
detail:SetTextColor(0.65, 0.65, 0.65)
else
-- row:SetTextColor(0.52, 0.52, 0.52)
-- detail:SetTextColor(0.52, 0.52, 0.52)
-- A lighter grey, readable under the gold selection bar too.
row:SetTextColor(0.7, 0.7, 0.7)
detail:SetTextColor(0.7, 0.7, 0.7)
end
if first + i - 1 == self.selected then
self.selectionBackground:ClearAllPoints()
self.selectionBackground:SetPoint("TOPLEFT", self.frame,
"TOPLEFT", ROW_LEFT - 4, -ROW_TOP - (i - 1) * ROW_HEIGHT)
self.selectionBackground:Show()
end
row:Show(); detail:Show(); icon:Show()
else row:Hide(); detail:Hide(); icon:Hide() end
end
-- self.count:SetText(count .. " results")
self.count:SetText(count .. (count == 1 and " result" or " results"))
local selected = self.results[self.selected]
if selected then self:Tooltip(selected)
elseif GameTooltip and GameTooltip.IsOwned
and GameTooltip:IsOwned(self.frame) then GameTooltip:Hide() end
end
function Finder:Move(delta)
if not self.frame:IsVisible() then return end
self.selected = math.max(1, math.min(table.getn(self.results),
(self.selected or 1) + delta))
self:Draw()
end
local function scrollRecipe(kind, index, total)
local frameName = kind == "trade" and "TradeSkillListScrollFrame"
or "CraftListScrollFrame"
local displayed = kind == "trade" and
(TRADE_SKILLS_DISPLAYED or 19) or (CRAFTS_DISPLAYED or 8)
local height = kind == "trade" and
(TRADE_SKILL_HEIGHT or 16) or (CRAFT_SKILL_HEIGHT or 16)
local scroll = getglobal(frameName)
if not scroll or type(FauxScrollFrame_GetOffset) ~= "function"
or type(FauxScrollFrame_SetOffset) ~= "function" then return end
local offset = FauxScrollFrame_GetOffset(scroll) or 0
if index <= offset then offset = index - 1
elseif index > offset + displayed then offset = index - displayed end
offset = math.max(0, math.min(total - displayed, offset))
FauxScrollFrame_SetOffset(scroll, offset)
local bar = getglobal(frameName .. "ScrollBar")
if bar then bar:SetValue(offset * height) end
end
-- A recipe chosen from a closed (or another) profession window: its
-- window opens, then the recipe is selected in it, as Enter on an open
-- window's recipe does. True once it is selected.
function Finder:SelectPendingRecipe()
local pending = self.pendingRecipe
if not pending then return false end
if visible("TradeSkillFrame") and type(GetNumTradeSkills) == "function"
and type(TradeSkillFrame_SetSelection) == "function" then
for i = 1, GetNumTradeSkills() or 0 do
local name, kind = GetTradeSkillInfo(i)
if kind ~= "header" and name == pending.name then
scrollRecipe("trade", i, GetNumTradeSkills())
TradeSkillFrame_SetSelection(i)
if type(TradeSkillFrame_Update) == "function" then TradeSkillFrame_Update() end
self.pendingRecipe = nil
return true
end
end
end
if visible("CraftFrame") and type(GetNumCrafts) == "function"
and type(CraftFrame_SetSelection) == "function" then
for i = 1, GetNumCrafts() or 0 do
local name, _, kind = GetCraftInfo(i)
if kind ~= "header" and name == pending.name then
scrollRecipe("craft", i, GetNumCrafts())
CraftFrame_SetSelection(i)
if type(CraftFrame_Update) == "function" then CraftFrame_Update() end
self.pendingRecipe = nil
return true
end
end
end
return false
end
-- Waits for the window a recipe opened (its server answer), a few seconds.
local recipeWaiter = CreateFrame("Frame", "VimFinderRecipeWaiter", UIParent)
recipeWaiter:Hide()
recipeWaiter:SetScript("OnUpdate", function()
local pending = Finder.pendingRecipe
if not pending or GetTime() > pending.untilTime then
Finder.pendingRecipe = nil
recipeWaiter:Hide()
elseif Finder:SelectPendingRecipe() then
recipeWaiter:Hide()
end
end)
function Finder:Activate()
local item = self.results[self.selected]
-- if not item or inCombat() then return end
-- -- Windows open in combat, as their keys do.
-- if not item or (inCombat() and item.kind ~= "window") then return end
-- Windows open and spells (pet spells, tracking) cast in combat, as
-- their keys do (the user, 2026-10-15: Rockbiter Weapon); bag items,
-- trade skills and crafts still wait.
local castable = item and (item.kind == "spell" or item.kind == "petspell" or item.kind == "tracking")
if not item or (inCombat() and item.kind ~= "window" and not castable) then return end
local action = canActivate(item)
if not action then return end
if item.kind == "window" then
self:RecordUse(item)
self:Close()
-- The window it opens takes focus; one it closes claims nothing.
-- if VimWowPanelFocus then VimWowPanelFocus:ClaimNext(0.5) end
self:ExpectWindow(0.5)
RunBinding(item.book)
elseif item.kind == "tracking" and item.active
and type(CancelTrackingBuff) == "function" then
CancelTrackingBuff()
self:RecordUse(item)
self:Scan()
elseif item.kind == "tracking" and type(SetTracking) == "function" then
SetTracking(item.index)
self:RecordUse(item)
self:Scan()
elseif item.kind == "trade" and visible("TradeSkillFrame")
and type(TradeSkillFrame_SetSelection) == "function" then
scrollRecipe("trade", item.index, GetNumTradeSkills())
TradeSkillFrame_SetSelection(item.index)
if type(TradeSkillFrame_Update) == "function" then TradeSkillFrame_Update() end
self:RecordUse(item)
self:Close()
elseif item.kind == "craft" and visible("CraftFrame")
and type(CraftFrame_SetSelection) == "function" then
scrollRecipe("craft", item.index, GetNumCrafts())
CraftFrame_SetSelection(item.index)
if type(CraftFrame_Update) == "function" then CraftFrame_Update() end
self:RecordUse(item)
self:Close()
elseif item.kind == "recipe" then
-- its profession's window (cast as Enter on the profession does),
-- then the recipe selected there; at once when it is open already
self.pendingRecipe = {name = item.name, untilTime = GetTime() + 3}
self:RecordUse(item)
self:Close()
if not self:SelectPendingRecipe() and type(CastSpell) == "function" then
self:ExpectWindow(3)
CastSpell(item.book, "spell")
recipeWaiter:Show()
end
-- elseif item.kind == "bag" then
-- -- Opens the combined bag with that item's slot selected.
-- if not VimWowCombinedBags then return end
-- VimWowCombinedBags:Show()
-- self:RecordUse(item)
-- self:Close()
-- if VimWowPanelFocus and VimWowPanelFocus.Select
-- and VimWowPanelFocus:Select("bags") and VimWowNavigator then
-- VimWowNavigator:Refresh()
-- VimWowNavigator:SelectBagSlot(item.book, item.index)
-- end
elseif item.kind == "bag" then
-- Opens the bags; a host (VimWow) selects that item's slot.
self:ShowBags()
self:RecordUse(item)
self:Close()
self:SelectBagSlot(item.book, item.index)
elseif item.kind == "spell" and item.name == "Disenchant" then
-- The player must click an item; never auto-select or destroy one.
if type(CastSpell) ~= "function" then return end
CastSpell(item.index, "spell")
self:RecordUse(item)
-- VimWowCombinedBags:Show()
-- self:Close()
-- if VimWowPanelFocus and VimWowPanelFocus.Select
-- and VimWowPanelFocus:Select("bags")
-- and VimWowNavigator then VimWowNavigator:Refresh() end
self:ShowBags()
self:Close()
self:SelectBagSlot()
elseif item.kind == "spell" and action == "self" then
if type(CastSpellByName) == "function" then
-- A spell that opens a window (Smelting, Poisons) gives it focus
-- once the server answers.
-- if VimWowPanelFocus then VimWowPanelFocus:ClaimNext(3) end
self:ExpectWindow(3)
CastSpellByName(item.name, true)
self:RecordUse(item)
self:Close()
end
elseif item.kind == "spell" and type(CastSpell) == "function" then
-- A profession (Tailoring, Enchanting) gives its window focus once
-- the server answers.
-- if VimWowPanelFocus then VimWowPanelFocus:ClaimNext(3) end
self:ExpectWindow(3)
CastSpell(item.index, "spell")
self:RecordUse(item)
self:Close()
end
end
function Finder:KeyMove(key, delta)
local now = GetTime()
if self.lastKey == key and now - (self.lastKeyAt or 0) < 0.02 then return end
self.lastKey, self.lastKeyAt = key, now
self:Move(delta)
end
function Finder:Close()
self.frame:Hide()
self.edit:ClearFocus()
self.capture:EnableKeyboard(false)
self.ctrlLeft, self.ctrlRight = nil, nil
self:RefreshBindings()
if GameTooltip and GameTooltip.IsOwned
and GameTooltip:IsOwned(self.frame) then GameTooltip:Hide() end
end
function Finder:Toggle()
if self.frame:IsVisible() then return end
-- if not VimWow_IsEnabled() then return end
if not self:IsActive() then return end
local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
if focus and focus ~= self.edit then return end
self.frame:Show()
self:RefreshBindings()
self.edit:SetText("")
self:Scan()
self.edit:SetFocus()
self.ctrlLeft = IsLeftControlKeyDown and IsLeftControlKeyDown() or nil
self.ctrlRight = IsRightControlKeyDown and IsRightControlKeyDown() or nil
if self.ctrlLeft or self.ctrlRight then
self.edit:ClearFocus()
self.capture:EnableKeyboard(true)
end
end
-- Finder.frame = CreateFrame("Frame", "VimWowFinderFrame", UIParent)
Finder.frame = CreateFrame("Frame", "VimFinderFrame", UIParent)
-- Finder.frame:SetWidth(600)
-- Finder.frame:SetHeight(260)
Finder.frame:SetWidth(WIDTH)
Finder.frame:SetHeight(HEIGHT)
Finder.frame:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 95)
-- Finder.frame:SetFrameStrata("DIALOG")
-- Over every window and unit frame (some unit frame bars draw above DIALOG),
-- under tooltips.
Finder.frame:SetFrameStrata("FULLSCREEN_DIALOG")
-- Finder.frame:SetBackdrop({bgFile = "Interface/Tooltips/UI-Tooltip-Background",
-- edgeFile = "Interface/Tooltips/UI-Tooltip-Border", edgeSize = 12,
-- insets = {left = 3, right = 3, top = 3, bottom = 3}})
-- The dialog look of the Game Menu and the popups: dark marble, silver
-- border, and the title on a header plate.
Finder.frame:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = {left = 11, right = 12, top = 12, bottom = 11}})
-- The marble lets about 40% of the world through; a black layer under it
-- (one layer below the finder, still over every window and unit frame)
-- leaves about 15%, enough to notice a change behind the finder. As the
-- finder's child it shows, hides and fades with it.
Finder.shade = CreateFrame("Frame", "VimFinderShade", Finder.frame)
Finder.shade:SetFrameStrata("FULLSCREEN")
Finder.shade:SetPoint("TOPLEFT", Finder.frame, "TOPLEFT", 11, -12)
Finder.shade:SetPoint("BOTTOMRIGHT", Finder.frame, "BOTTOMRIGHT", -12, 11)
Finder.shadeTexture = Finder.shade:CreateTexture(nil, "BACKGROUND")
Finder.shadeTexture:SetTexture("Interface/Buttons/WHITE8X8")
Finder.shadeTexture:SetVertexColor(0, 0, 0, 0.63)
Finder.shadeTexture:SetAllPoints(Finder.shade)
Finder.header = Finder.frame:CreateTexture(nil, "ARTWORK")
Finder.header:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
Finder.header:SetWidth(256)
Finder.header:SetHeight(64)
Finder.header:SetPoint("TOP", Finder.frame, "TOP", 0, 12)
Finder.title = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
Finder.title:SetPoint("TOP", Finder.header, "TOP", 0, -14)
Finder.title:SetText("Finder")
-- Finder.selectionBackground = Finder.frame:CreateTexture(nil, "ARTWORK")
-- Finder.selectionBackground:SetTexture("Interface/Buttons/WHITE8X8")
-- Finder.selectionBackground:SetVertexColor(0.03, 0.03, 0.03, 0.55)
-- Finder.selectionBackground:SetWidth(574)
-- Finder.selectionBackground:SetHeight(22)
-- The quest log's selection bar, in gold.
Finder.selectionBackground = Finder.frame:CreateTexture(nil, "ARTWORK")
Finder.selectionBackground:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
Finder.selectionBackground:SetBlendMode("ADD")
Finder.selectionBackground:SetVertexColor(1, 0.82, 0, 1)
Finder.selectionBackground:SetWidth(ROW_WIDTH + 8)
Finder.selectionBackground:SetHeight(ROW_HEIGHT)
Finder.selectionBackground:Hide()
Finder.capture:SetFrameStrata("TOOLTIP")
Finder.capture:EnableKeyboard(false)
-- Finder.edit = CreateFrame("EditBox", "VimWowFinderInput", Finder.frame,
-- "InputBoxTemplate")
Finder.edit = CreateFrame("EditBox", "VimFinderInput", Finder.frame,
"InputBoxTemplate")
Finder.edit:SetAutoFocus(false)
-- Finder.edit:SetWidth(550)
-- Finder.edit:SetHeight(28)
-- Finder.edit:SetPoint("TOP", Finder.frame, "TOP", 0, -15)
-- The template's left cap reaches 5 pixels left of the box.
Finder.edit:SetWidth(ROW_WIDTH - 13)
Finder.edit:SetHeight(20)
Finder.edit:SetPoint("TOPLEFT", Finder.frame, "TOPLEFT", ROW_LEFT + 9, -30)
-- Finder.count = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
-- Finder.count:SetPoint("BOTTOMRIGHT", Finder.frame, "BOTTOMRIGHT", -15, 10)
Finder.count = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
Finder.count:SetPoint("BOTTOMRIGHT", Finder.frame, "BOTTOMRIGHT", -ROW_LEFT, 20)
-- The keys, in green, as the client's own key labels.
Finder.hints = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
Finder.hints:SetPoint("BOTTOMLEFT", Finder.frame, "BOTTOMLEFT", ROW_LEFT, 20)
Finder.hints:SetTextColor(0.65, 0.65, 0.65)
Finder.hints:SetText("|cff00ff00Enter|r Use |cff00ff00Tab|r / "
.. "|cff00ff00Ctrl+J/K|r Move |cff00ff00Esc|r Close")
Finder.details = {}
Finder.icons = {}
-- for i = 1, 8 do
-- local row = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
-- row:SetPoint("TOPLEFT", Finder.frame, "TOPLEFT", 18, -40 - i * 23)
-- row:SetWidth(560)
-- Finder.rows[i] = row
-- end
for i = 1, 8 do
local top = -ROW_TOP - (i - 1) * ROW_HEIGHT
local icon = Finder.frame:CreateTexture(nil, "OVERLAY")
icon:SetWidth(20)
icon:SetHeight(20)
icon:SetPoint("TOPLEFT", Finder.frame, "TOPLEFT", ROW_LEFT, top - 2)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
Finder.icons[i] = icon
local row = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
row:SetPoint("TOPLEFT", Finder.frame, "TOPLEFT", ROW_LEFT + 28, top - 6)
row:SetWidth(ROW_WIDTH / 2 - 28)
row:SetHeight(14)
row:SetJustifyH("LEFT")
Finder.rows[i] = row
local detail = Finder.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
detail:SetPoint("TOPRIGHT", Finder.frame, "TOPRIGHT", -ROW_LEFT - 2, top - 7)
detail:SetWidth(ROW_WIDTH / 2)
detail:SetHeight(12)
detail:SetJustifyH("RIGHT")
Finder.details[i] = detail
end
Finder.edit:SetScript("OnTextChanged", function() Finder:Search() end)
Finder.edit:SetScript("OnEscapePressed", function() Finder:Close() end)
Finder.edit:SetScript("OnEnterPressed", function() Finder:Activate() end)
Finder.edit:SetScript("OnTabPressed", function()
Finder:Move(IsShiftKeyDown and IsShiftKeyDown() and -1 or 1)
end)
-- The 1.12 EditBox consumes arrows before OnKeyDown in the stock client.
-- Keep this route for clients that forward them; Tab is the native fallback.
Finder.edit:SetScript("OnKeyDown", function(_, key)
-- The 1.12 client passes the key in arg1 (ClassicAPI also passes it).
key = key or arg1
if key == "UP" then Finder:Move(-1)
elseif key == "DOWN" then Finder:Move(1)
elseif key == "J" and IsControlKeyDown and IsControlKeyDown() then
Finder:Move(1)
elseif key == "K" and IsControlKeyDown and IsControlKeyDown() then
Finder:Move(-1)
end
end)
Finder.capture:SetScript("OnKeyDown", function(_, key)
key = key or arg1
if not Finder.frame:IsVisible() or not (Finder.ctrlLeft or Finder.ctrlRight)
then return end
if key == "J" or key == "DOWN" then Finder:KeyMove("next", 1)
elseif key == "K" or key == "UP" then Finder:KeyMove("previous", -1)
elseif key == "ESCAPE" then Finder:Close()
elseif key == "ENTER" then Finder:Activate() end
end)
Finder.toggle:SetScript("OnClick", function() Finder:Toggle() end)
Finder.closeButton:SetScript("OnClick", function() Finder:Close() end)
Finder.up:SetScript("OnClick", function() Finder:KeyMove("previous", -1) end)
Finder.down:SetScript("OnClick", function() Finder:KeyMove("next", 1) end)
Finder.frame:Hide()
-- Whether the finder may be open and take its keys: its host allows it and
-- no other text box has the keyboard.
function Finder:Allowed()
if not self:IsActive() then return false end
local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
return not (focus and focus ~= self.edit)
end
-- function Finder:RefreshBindings(active)
-- if active == nil then
-- active = VimWow_IsEnabled() and not VimWowBindingsCapturing
-- and type(SetOverrideBindingClick) == "function"
-- and type(ClearOverrideBindings) == "function"
-- local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
-- if focus and focus ~= self.edit then active = false end
-- end
-- local binding = active and VimWow_BindingSignature("VIMWOWFINDER",
-- {"ALT-SPACE"}) or ""
-- Override bindings come from ClassicAPI; without them the finder still
-- opens from its key binding and keeps Esc, Enter, Tab and Ctrl+J/K.
-- Without ClassicAPI's override bindings the keys set on the finder's tab
-- are game key bindings to its Bindings.xml command: a key is taken only
-- while no other binding has it, and a key the tab no longer names is given
-- back. VimFinderDB.gameKeys keeps the keys bound this way. Nothing is set
-- before the client's own bindings have loaded (PLAYER_ENTERING_WORLD).
function Finder:ApplyGameBindings(first, second)
if type(SetBinding) ~= "function" or type(GetBindingAction) ~= "function" then
return
end
if type(VimFinderDB) ~= "table" then VimFinderDB = {} end
if type(VimFinderDB.gameKeys) ~= "table" then VimFinderDB.gameKeys = {} end
local held = VimFinderDB.gameKeys
local wanted = {}
if first then wanted[first] = true end
if second then wanted[second] = true end
local changed = false
for key in pairs(held) do
if not wanted[key] then
if GetBindingAction(key) == "VIMFINDER" then
SetBinding(key)
changed = true
end
held[key] = nil
end
end
for key in pairs(wanted) do
local action = GetBindingAction(key)
if action == nil or action == "" then
SetBinding(key, "VIMFINDER")
changed = true
held[key] = true
elseif action == "VIMFINDER" then
held[key] = true
end
end
if changed and type(SaveBindings) == "function" then
SaveBindings(type(GetCurrentBindingSet) == "function"
and GetCurrentBindingSet() or 1)
end
end
function Finder:RefreshBindings(active)
if type(SetOverrideBindingClick) ~= "function" then
if not self.bindingsReadyAt or GetTime() < self.bindingsReadyAt then return end
local first, second = self:OpenKeys()
local wanted = tostring(first) .. "," .. tostring(second)
if wanted ~= self.gameBinding then
self.gameBinding = wanted
self:ApplyGameBindings(first, second)
end
return
end
-- With ClassicAPI the key is an override binding. The game key bindings
-- the finder set in a session without it are given back once: kept,
-- the old key still opened the finder after a key change on the Finder
-- tab. Bindings the player set in the key binding window stay.
if not self.gameKeysReleased and self.bindingsReadyAt and GetTime() >= self.bindingsReadyAt then
self.gameKeysReleased = true
self:ApplyGameBindings(nil, nil)
end
if active == nil then active = self:Allowed() end
active = active and type(SetOverrideBindingClick) == "function"
and type(ClearOverrideBindings) == "function"
local first, second
if active then first, second = self:OpenKeys() end
local binding = active and (tostring(first) .. "," .. tostring(second)) or ""
if active and self.frame:IsVisible() then binding = binding .. ":navigation" end
if binding == self.binding then return end
if type(ClearOverrideBindings) == "function" then
ClearOverrideBindings(self.owner)
end
if active then
-- VimWow_Override(self.owner, "VIMWOWFINDER",
-- {"ALT-SPACE"}, self.toggle, true)
local toggle = self.toggle:GetName()
if first then SetOverrideBindingClick(self.owner, true, first, toggle) end
if second then SetOverrideBindingClick(self.owner, true, second, toggle) end
if self.frame:IsVisible() then
SetOverrideBindingClick(self.owner, true, "ESCAPE",
self.closeButton:GetName())
SetOverrideBindingClick(self.owner, true, "CTRL-K", self.up:GetName())
SetOverrideBindingClick(self.owner, true, "CTRL-J", self.down:GetName())
SetOverrideBindingClick(self.owner, true, "CTRL-UP", self.up:GetName())
SetOverrideBindingClick(self.owner, true, "CTRL-DOWN", self.down:GetName())
end
end
self.binding = binding
end
Finder.owner:SetScript("OnUpdate", function()
-- Ctrl was released last frame (MODIFIER_STATE_CHANGED): typing again.
if Finder.refocus then
Finder.refocus = nil
if Finder.frame:IsVisible() and not (Finder.ctrlLeft or Finder.ctrlRight) then
Finder.capture:EnableKeyboard(false)
Finder.edit:SetFocus()
end
end
-- Without ClassicAPI (which brings IsLeftControlKeyDown and the
-- MODIFIER_STATE_CHANGED event) Ctrl is read each frame while the
-- finder is open, so Ctrl+J/K still move.
if type(IsLeftControlKeyDown) ~= "function" and Finder.frame:IsVisible()
and type(IsControlKeyDown) == "function" then
local held = IsControlKeyDown() and true or false
if held ~= ((Finder.ctrlLeft or Finder.ctrlRight) and true or false) then
Finder.ctrlLeft, Finder.ctrlRight = held or nil, nil
if held then
Finder.edit:ClearFocus()
Finder.capture:EnableKeyboard(true)
else
Finder.capture:EnableKeyboard(false)
Finder.edit:SetFocus()
end
end
end
local now = GetTime()
if now < (Finder.nextUpdate or 0) then return end
Finder.nextUpdate = now + 0.5
-- local active = VimWow_IsEnabled() and not VimWowBindingsCapturing
-- and type(SetOverrideBindingClick) == "function"
-- and type(ClearOverrideBindings) == "function"
-- local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
-- if focus and focus ~= Finder.edit then active = false end
local active = Finder:Allowed()
if not active and Finder.frame:IsVisible() then Finder:Close() end
Finder:RefreshBindings(active)
if Finder.frame:IsVisible() then
-- Finder.frame:SetAlpha(inCombat() and 0.22 or 0.86)
-- Opaque out of combat: the dialog background's own see-through
-- shows enough of the world to notice a change there.
-- Finder.frame:SetAlpha(inCombat() and 0.22 or 1)
-- 60% in combat: 22% was too faint to read (the user, 2026-10-15).
Finder.frame:SetAlpha(inCombat() and 0.6 or 1)
end
end)
-- for _, event in ipairs({"SPELLS_CHANGED", "TRADE_SKILL_UPDATE",
-- "CRAFT_UPDATE", "BAG_UPDATE", "UNIT_PET", "PET_BAR_UPDATE",
-- "MODIFIER_STATE_CHANGED"}) do
for _, event in ipairs({"SPELLS_CHANGED", "TRADE_SKILL_UPDATE",
"CRAFT_UPDATE", "BAG_UPDATE", "UNIT_PET", "PET_BAR_UPDATE",
"MODIFIER_STATE_CHANGED", "PLAYER_ENTERING_WORLD"}) do
Finder.owner:RegisterEvent(event)
end
Finder.owner:SetScript("OnEvent", function()
-- The client's key bindings have loaded: game bindings may be set a
-- second later (RefreshBindings, without ClassicAPI).
if event == "PLAYER_ENTERING_WORLD" then
Finder.bindingsReadyAt = Finder.bindingsReadyAt or GetTime() + 1
return
end
if not Finder.frame:IsVisible() then return end
if event == "MODIFIER_STATE_CHANGED" then
local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
if focus and focus ~= Finder.edit then Finder:Close(); return end
if arg1 == "LCTRL" then Finder.ctrlLeft = arg2 == 1
elseif arg1 == "RCTRL" then Finder.ctrlRight = arg2 == 1
else return end
-- A focused 1.12 EditBox consumes Ctrl+J/K before bindings dispatch.
-- Release focus while Ctrl is held, then resume typing on release.
if Finder.ctrlLeft or Finder.ctrlRight then
Finder.edit:ClearFocus()
Finder.capture:EnableKeyboard(true)
else
-- Finder.capture:EnableKeyboard(false)
-- Finder.edit:SetFocus()
-- The search box takes the keyboard back on the next frame
-- (OnUpdate). This event fires before the client handles the
-- Ctrl release; a search box focused now takes that release,
-- and the client then keeps Ctrl held (IsControlKeyDown) after
-- a Ctrl key opened the finder, until Ctrl is pressed again.
Finder.refocus = true
end
-- else Finder:Scan() end
else
-- A rescan keeps the selected result, so Enter still uses the one
-- shown after a bag, spell or pet update.
local current = Finder.results and Finder.results[Finder.selected]
local key = current and usageKey(current)
Finder:Scan()
for i, item in ipairs(key and Finder.results or {}) do
if usageKey(item) == key then
Finder.selected = i
Finder:Draw()
break
end
end
end
end)
-- The finder's tab in VimWow Settings: its key, kept in VimFinderDB.
Finder.settings = {
id = "VimFinder", name = "Finder", order = 150,
db = function()
if type(VimFinderDB) ~= "table" then VimFinderDB = {} end
return VimFinderDB
end,
bindings = {{action = "VIMFINDER", defaults = DEFAULT_KEYS}},
-- A new key is bound at once.
onChange = function() Finder.binding = nil end,
}
if VimWowSettings and VimWowSettings.Register then
VimWowSettings:Register(Finder.settings)
end