From 9905a6fb55fc2b45709d27f797702281e19af11a Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Tue, 3 Feb 2026 18:25:47 -0700 Subject: [PATCH 1/4] update deploy actions --- .github/workflows/release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef70f9f..a4b8453 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,4 +60,12 @@ jobs: pandoc: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CF_API_KEY: ${{ secrets.CF_API_KEY }} \ No newline at end of file + CF_API_KEY: ${{ secrets.CF_API_KEY }} + + - name: Dump .release contents (always) + if: always() + run: | + echo "=== .release listing ===" + ls -la .release || true + echo "=== tree ===" + find .release -maxdepth 6 -print || true From 7071f283439be2e0507897ebde56c6d274d38132 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Tue, 3 Feb 2026 18:33:44 -0700 Subject: [PATCH 2/4] update deploy actions --- .github/workflows/release.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4b8453..7acbea6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,19 +53,3 @@ jobs: files: | dist/DevForge-${{ steps.ver.outputs.tag }}.zip generate_release_notes: true - - - name: Package and publish - uses: BigWigsMods/packager@v2 - with: - pandoc: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CF_API_KEY: ${{ secrets.CF_API_KEY }} - - - name: Dump .release contents (always) - if: always() - run: | - echo "=== .release listing ===" - ls -la .release || true - echo "=== tree ===" - find .release -maxdepth 6 -print || true From c78114d669e9c8da147e1dd226210186f1261b26 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Sun, 8 Feb 2026 12:14:10 -0700 Subject: [PATCH 3/4] Bugfixes and WA import enhancements --- DevForge/Core/Init.lua | 27 +- DevForge/DevForge.toc | 2 +- .../Modules/ErrorHandler/ErrorHandler.lua | 63 ++- DevForge/Modules/ErrorHandler/ErrorList.lua | 1 + .../Modules/ErrorHandler/ErrorMonitor.lua | 22 +- DevForge/Modules/EventMonitor/EventIndex.lua | 78 ++- .../EventMonitor/EventMonitorFilter.lua | 89 ++-- .../Modules/EventMonitor/EventMonitorLog.lua | 109 +++- DevForge/Modules/MacroEditor/MacroList.lua | 1 + DevForge/Modules/Performance/PerfData.lua | 37 +- DevForge/Modules/Performance/PerfTable.lua | 7 +- .../Modules/SnippetEditor/AddonScaffold.lua | 11 +- .../Modules/SnippetEditor/FrameBuilder.lua | 11 +- .../Modules/SnippetEditor/SnippetEditor.lua | 128 +++-- DevForge/Modules/TableViewer/TableViewer.lua | 1 + .../Modules/TextureBrowser/TextureBrowser.lua | 3 + DevForge/Modules/WAImporter/WACodeGen.lua | 492 ++++++++++++++---- DevForge/Modules/WAImporter/WADecode.lua | 9 + DevForge/Modules/WAImporter/WAImporter.lua | 214 +++++++- DevForge/UI/ActivityBar.lua | 1 + DevForge/UI/BottomPanel.lua | 8 + DevForge/UI/MainWindow.lua | 23 +- DevForge/UI/Sidebar.lua | 1 + DevForge/UI/TabBar.lua | 1 + DevForge/UI/Widgets/Button.lua | 1 + DevForge/UI/Widgets/CopyDialog.lua | 7 + DevForge/UI/Widgets/DropDown.lua | 8 + DevForge/UI/Widgets/SearchBox.lua | 1 + 28 files changed, 1077 insertions(+), 279 deletions(-) diff --git a/DevForge/Core/Init.lua b/DevForge/Core/Init.lua index 0eaa72c..7d58338 100644 --- a/DevForge/Core/Init.lua +++ b/DevForge/Core/Init.lua @@ -9,21 +9,44 @@ DF.frame = CreateFrame("Frame") DF.frame:RegisterEvent("ADDON_LOADED") DF.frame:RegisterEvent("PLAYER_LOGOUT") +-- After /reload, named frames survive but all Lua scripts are wiped. +-- Hide the stale window immediately so the user can't interact with a +-- scriptless zombie. We record whether it was showing so we can reopen +-- after the fresh UI is built. +local staleWasShown = false +do + local stale = _G["DevForgeMainWindow"] + if stale then + staleWasShown = stale:IsShown() + stale:Hide() + stale:EnableMouse(false) + end +end + DF.frame:SetScript("OnEvent", function(_, event, ...) if event == "ADDON_LOADED" then local name = ... if name == ADDON_NAME then DF.frame:UnregisterEvent("ADDON_LOADED") if DF.Schema then - DF.Schema:Init() + local ok, err = pcall(DF.Schema.Init, DF.Schema) + if not ok then print("|cFFFF4444DevForge: Schema:Init() error:|r " .. tostring(err)) end end -- Install error handler hooks immediately so no errors are missed if DF.ErrorHandler then - DF.ErrorHandler:Init() + local ok, err = pcall(DF.ErrorHandler.Init, DF.ErrorHandler) + if not ok then print("|cFFFF4444DevForge: ErrorHandler:Init() error:|r " .. tostring(err)) end end if DF.EventBus then DF.EventBus:Fire("DF_ADDON_LOADED") end + + -- If the window was open before /reload, reopen it seamlessly + if staleWasShown then + C_Timer.After(0, function() + DF:Toggle() + end) + end end elseif event == "PLAYER_LOGOUT" then if DF.EventBus then diff --git a/DevForge/DevForge.toc b/DevForge/DevForge.toc index b590459..ef6a33f 100644 --- a/DevForge/DevForge.toc +++ b/DevForge/DevForge.toc @@ -3,7 +3,7 @@ ## Notes: In-game dev toolkit: inspector, editor, console, events, errors, APIs, textures, performance ## Author: hatdragon ## Contributors: -## Version: r1.0.1 +## Version: r1.0.9 ## SavedVariables: DevForgeDB ## IconTexture: Interface\Icons\INV_Gizmo_02 diff --git a/DevForge/Modules/ErrorHandler/ErrorHandler.lua b/DevForge/Modules/ErrorHandler/ErrorHandler.lua index c4c03af..05e8e66 100644 --- a/DevForge/Modules/ErrorHandler/ErrorHandler.lua +++ b/DevForge/Modules/ErrorHandler/ErrorHandler.lua @@ -109,11 +109,25 @@ local function OnBugGrabbed(_, bugObj) ) end +-- Correct stack data extraction (mirrors BugGrabber's GetErrorData approach) +local function GetErrorData() + local currentStackHeight = GetCallstackHeight and GetCallstackHeight() + local errorCallStackHeight = GetErrorCallstackHeight and GetErrorCallstackHeight() + if currentStackHeight and errorCallStackHeight then + local debugStackLevel = currentStackHeight - (errorCallStackHeight - 1) + return debugstack(debugStackLevel), debuglocals(debugStackLevel) + end + return debugstack(3), debuglocals(3) +end + -- Self-hook error handler (used when BugGrabber is absent) +local inHandler = false local function OnLuaError(message) - local stack = debugstack(3) - local locals = debuglocals(3) + if inHandler then return end + inHandler = true + local stack, locals = GetErrorData() ProcessError(message, stack, locals, "error") + inHandler = false end function Handler:Init() @@ -147,27 +161,50 @@ function Handler:Init() BugGrabber.RegisterAddonActionCallback(OnBugGrabbed) end else - -- Self-hook using Blizzard 12.x API + -- Self-hook: try each approach individually with pcall so one + -- failure doesn't block the rest. Blizzard_ScriptErrors may + -- assert when addons interact with the error-handler chain. + local hooked = false if _G.AddLuaErrorHandler then - AddLuaErrorHandler(OnLuaError) - elseif _G.seterrorhandler then - -- Fallback: wrap existing error handler + local ok = pcall(AddLuaErrorHandler, OnLuaError) + hooked = ok + end + if not hooked and _G.seterrorhandler then local oldHandler = geterrorhandler() - seterrorhandler(function(msg) + local ok = pcall(seterrorhandler, function(msg) OnLuaError(msg) if oldHandler then return oldHandler(msg) end end) + hooked = ok end end - -- Register for LUA_WARNING events - local warningFrame = CreateFrame("Frame") - warningFrame:RegisterEvent("LUA_WARNING") - warningFrame:SetScript("OnEvent", function(_, _, warnType, warnMessage) - ProcessError(warnMessage, nil, nil, "warning") - end) + -- Register for additional error-producing events + if not _G.BugGrabber then + local eventFrame = CreateFrame("Frame") + eventFrame:RegisterEvent("LUA_WARNING") + eventFrame:RegisterEvent("ADDON_ACTION_BLOCKED") + eventFrame:RegisterEvent("ADDON_ACTION_FORBIDDEN") + + local badAddons = {} + eventFrame:SetScript("OnEvent", function(_, event, ...) + if event == "LUA_WARNING" then + local arg1, arg2 = ... + local warningText = arg2 or arg1 + ProcessError(warningText, nil, nil, "warning") + elseif event == "ADDON_ACTION_BLOCKED" or event == "ADDON_ACTION_FORBIDDEN" then + local addonName, addonFunc = ... + local name = addonName or "" + if not badAddons[name] then + badAddons[name] = true + local msg = ("[%s] AddOn '%s' tried to call the protected function '%s'."):format(event, name, addonFunc or "") + ProcessError(msg, nil, nil, "error") + end + end + end) + end end function Handler:GetErrors() diff --git a/DevForge/Modules/ErrorHandler/ErrorList.lua b/DevForge/Modules/ErrorHandler/ErrorList.lua index 40f410e..f9fb270 100644 --- a/DevForge/Modules/ErrorHandler/ErrorList.lua +++ b/DevForge/Modules/ErrorHandler/ErrorList.lua @@ -25,6 +25,7 @@ function ErrList:Create(parent) end local row = CreateFrame("Button", nil, self.pane:GetContent()) + row:RegisterForClicks("LeftButtonUp") row:SetHeight(ROW_HEIGHT) -- Selection highlight diff --git a/DevForge/Modules/ErrorHandler/ErrorMonitor.lua b/DevForge/Modules/ErrorHandler/ErrorMonitor.lua index f2a6bf3..a359fa3 100644 --- a/DevForge/Modules/ErrorHandler/ErrorMonitor.lua +++ b/DevForge/Modules/ErrorHandler/ErrorMonitor.lua @@ -41,9 +41,6 @@ DF.ModuleSystem:Register("ErrorHandler", function(sidebarParent, editorParent) local pauseBtn = DF.Widgets:CreateButton(toolbar, "Pause", 60) pauseBtn:SetPoint("LEFT", copyBtn, "RIGHT", 4, 0) - local toConsoleBtn = DF.Widgets:CreateButton(toolbar, "To Console", 80) - toConsoleBtn:SetPoint("LEFT", pauseBtn, "RIGHT", 8, 0) - -- Count label local countLabel = toolbar:CreateFontString(nil, "OVERLAY") countLabel:SetFontObject(DF.Theme:UIFont()) @@ -80,6 +77,7 @@ DF.ModuleSystem:Register("ErrorHandler", function(sidebarParent, editorParent) errorList:Refresh() errorDetail:Clear() UpdateCount() + DF.EventBus:Fire("DF_ERRORS_CLEARED") end) -- Pause/Resume button @@ -93,24 +91,6 @@ DF.ModuleSystem:Register("ErrorHandler", function(sidebarParent, editorParent) end end) - -- Copy to Console button - toConsoleBtn:SetScript("OnClick", function() - local text = errorDetail:GetText() - if text and text ~= "" then - -- Put error context into the REPL input - if DF.bottomPanel then - local input = DF.bottomPanel:GetInputLine() - if input then - -- Extract just the error message for the input - local errMsg = text:match("%[ERROR%] (.-)\n") or text:match("%[WARNING%] (.-)\n") or text:sub(1, 200) - input:SetText("-- Error: " .. errMsg) - input:Focus() - end - DF.bottomPanel:SelectTab("output") - end - end - end) - -- Live update callback via EventBus (set up by BottomPanel) DF.EventBus:On("DF_ERROR_RECEIVED", function(err, isDuplicate) errorList:Refresh() diff --git a/DevForge/Modules/EventMonitor/EventIndex.lua b/DevForge/Modules/EventMonitor/EventIndex.lua index 891c7f2..74ee576 100644 --- a/DevForge/Modules/EventMonitor/EventIndex.lua +++ b/DevForge/Modules/EventMonitor/EventIndex.lua @@ -424,9 +424,24 @@ local CATEGORIES = { }, } +-- Fast lookup set of all hardcoded event names +local knownEvents = {} +for _, cat in ipairs(CATEGORIES) do + for _, entry in ipairs(cat.events) do + knownEvents[entry.event] = true + end +end + +-- Discovered events (captured at runtime, not in hardcoded index) +local discovered = {} -- event name -> true + -- Flat index for searching local flatIndex = nil +local function InvalidateFlatIndex() + flatIndex = nil +end + local function BuildFlatIndex() if flatIndex then return end flatIndex = {} @@ -439,6 +454,13 @@ local function BuildFlatIndex() } end end + for event in pairs(discovered) do + flatIndex[#flatIndex + 1] = { + event = event, + desc = "Discovered at runtime", + category = "Discovered", + } + end table.sort(flatIndex, function(a, b) return a.event < b.event end) end @@ -476,17 +498,63 @@ function Index:Search(query) return results end --- Lookup a single event +-- Lookup a single event (fast path via hash set) function Index:Lookup(eventName) - BuildFlatIndex() - for _, entry in ipairs(flatIndex) do - if entry.event == eventName then - return entry + if knownEvents[eventName] or discovered[eventName] then + BuildFlatIndex() + for _, entry in ipairs(flatIndex) do + if entry.event == eventName then + return entry + end end end return nil end +-- Check if an event is known (hardcoded or discovered) +function Index:IsKnown(eventName) + return knownEvents[eventName] or discovered[eventName] or false +end + +-- Register a runtime-discovered event +function Index:RegisterDiscovered(eventName) + if knownEvents[eventName] or discovered[eventName] then return end + discovered[eventName] = true + InvalidateFlatIndex() +end + +-- Persistence +function Index:LoadDiscovered() + if DevForgeDB and DevForgeDB.discoveredEvents then + for _, event in ipairs(DevForgeDB.discoveredEvents) do + if not knownEvents[event] then + discovered[event] = true + end + end + InvalidateFlatIndex() + end +end + +function Index:SaveDiscovered() + if not DevForgeDB then return end + local list = {} + for event in pairs(discovered) do + list[#list + 1] = event + end + table.sort(list) + DevForgeDB.discoveredEvents = list +end + +-- Get discovered events as a sorted list of { event, desc } +function Index:GetDiscovered() + local list = {} + for event in pairs(discovered) do + list[#list + 1] = { event = event, desc = "Discovered at runtime" } + end + table.sort(list, function(a, b) return a.event < b.event end) + return list +end + -- Build tree nodes for the TreeView widget function Index:BuildTreeNodes(filterQuery) local nodes = {} diff --git a/DevForge/Modules/EventMonitor/EventMonitorFilter.lua b/DevForge/Modules/EventMonitor/EventMonitorFilter.lua index f8698ff..c9b2f1a 100644 --- a/DevForge/Modules/EventMonitor/EventMonitorFilter.lua +++ b/DevForge/Modules/EventMonitor/EventMonitorFilter.lua @@ -46,12 +46,30 @@ function Filter:Create(parent) searchBox.frame:SetPoint("LEFT", allOffBtn, "RIGHT", 6, 0) searchBox.frame:SetPoint("RIGHT", -2, 0) + --------------------------------------------------------------------------- + -- Info banner + --------------------------------------------------------------------------- + local banner = CreateFrame("Frame", nil, frame, "BackdropTemplate") + banner:SetHeight(20) + banner:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -2) + banner:SetPoint("TOPRIGHT", toolbar, "BOTTOMRIGHT", 0, -2) + local bannerBg = banner:CreateTexture(nil, "BACKGROUND") + bannerBg:SetAllPoints() + bannerBg:SetColorTexture(0.35, 0.25, 0.05, 0.6) + local bannerText = banner:CreateFontString(nil, "OVERLAY") + bannerText:SetFontObject(DF.Theme:UIFont()) + bannerText:SetPoint("LEFT", 6, 0) + bannerText:SetPoint("RIGHT", -6, 0) + bannerText:SetJustifyH("LEFT") + bannerText:SetTextColor(0.9, 0.75, 0.3, 1) + bannerText:SetText("This list may be incomplete. New events are discovered automatically while capturing.") + --------------------------------------------------------------------------- -- Scroll area --------------------------------------------------------------------------- local container = CreateFrame("Frame", nil, frame, "BackdropTemplate") DF.Theme:ApplyDarkPanel(container, true) - container:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -2) + container:SetPoint("TOPLEFT", banner, "BOTTOMLEFT", 0, -2) container:SetPoint("BOTTOMRIGHT", 0, 0) local scrollFrame = CreateFrame("ScrollFrame", nil, container) @@ -119,14 +137,6 @@ function Filter:Create(parent) local q = self.searchQuery:lower() local categories = DF.EventIndex:GetCategories() - -- Build set of indexed event names so we can find uncategorized ones - local indexedEvents = {} - for _, cat in ipairs(categories) do - for _, entry in ipairs(cat.events) do - indexedEvents[entry.event] = true - end - end - for _, cat in ipairs(categories) do -- Collect matching events local matchingEvents = {} @@ -156,48 +166,33 @@ function Filter:Create(parent) end end - -- Gather uncategorized events from log + blacklist - local uncatSeen = {} - local uncatEvents = {} - for _, entry in ipairs(DF.EventMonitorLog:GetEntries()) do - if not indexedEvents[entry.event] and not uncatSeen[entry.event] then - uncatSeen[entry.event] = true - uncatEvents[#uncatEvents + 1] = { event = entry.event, desc = "Captured event" } - end - end - for event in pairs(DF.EventMonitorLog:GetBlacklistTable()) do - if not indexedEvents[event] and not uncatSeen[event] then - uncatSeen[event] = true - uncatEvents[#uncatEvents + 1] = { event = event, desc = "Blacklisted event" } - end - end - - if #uncatEvents > 0 then - table.sort(uncatEvents, function(a, b) return a.event < b.event end) - local otherCat = { name = "Other / Captured", events = uncatEvents } + -- Discovered events (runtime-captured, persisted across sessions) + local discoveredEvents = DF.EventIndex:GetDiscovered() + if #discoveredEvents > 0 then + local discoveredCat = { name = "Discovered", events = discoveredEvents } -- Apply search filter - local matchingOther = {} - for _, entry in ipairs(uncatEvents) do + local matchingDiscovered = {} + for _, entry in ipairs(discoveredEvents) do if q == "" or entry.event:lower():find(q, 1, true) then - matchingOther[#matchingOther + 1] = entry + matchingDiscovered[#matchingDiscovered + 1] = entry end end - if #matchingOther > 0 then + if #matchingDiscovered > 0 then self.flatList[#self.flatList + 1] = { type = ROW_CATEGORY, - cat = otherCat, - matchCount = #matchingOther, + cat = discoveredCat, + matchCount = #matchingDiscovered, } - if self.expanded[otherCat.name] then - for _, entry in ipairs(matchingOther) do + if self.expanded[discoveredCat.name] then + for _, entry in ipairs(matchingDiscovered) do self.flatList[#self.flatList + 1] = { type = ROW_EVENT, event = entry.event, desc = entry.desc, - cat = otherCat, + cat = discoveredCat, } end end @@ -307,6 +302,7 @@ function Filter:Create(parent) local function SetupCheckboxClick(row) if row.cbBtn then return end local cbBtn = CreateFrame("Button", nil, row) + cbBtn:RegisterForClicks("LeftButtonUp") cbBtn:SetSize(16, 16) cbBtn:SetPoint("CENTER", row.cbBg, "CENTER", 0, 0) cbBtn:SetScript("OnClick", function() @@ -470,10 +466,11 @@ function Filter:Create(parent) -- Button handlers --------------------------------------------------------------------------- allOnBtn:SetScript("OnClick", function() - local categories = DF.EventIndex:GetCategories() - for _, cat in ipairs(categories) do - for _, entry in ipairs(cat.events) do - DF.EventMonitorLog:SetBlacklisted(entry.event, false) + for _, data in ipairs(panel.flatList) do + if data.type == ROW_CATEGORY then + for _, entry in ipairs(data.cat.events) do + DF.EventMonitorLog:SetBlacklisted(entry.event, false) + end end end panel:Refresh() @@ -481,10 +478,11 @@ function Filter:Create(parent) end) allOffBtn:SetScript("OnClick", function() - local categories = DF.EventIndex:GetCategories() - for _, cat in ipairs(categories) do - for _, entry in ipairs(cat.events) do - DF.EventMonitorLog:SetBlacklisted(entry.event, true) + for _, data in ipairs(panel.flatList) do + if data.type == ROW_CATEGORY then + for _, entry in ipairs(data.cat.events) do + DF.EventMonitorLog:SetBlacklisted(entry.event, true) + end end end panel:Refresh() @@ -499,6 +497,7 @@ function Filter:Create(parent) for _, cat in ipairs(categories) do panel.expanded[cat.name] = true end + panel.expanded["Discovered"] = true end panel:Refresh() end) diff --git a/DevForge/Modules/EventMonitor/EventMonitorLog.lua b/DevForge/Modules/EventMonitor/EventMonitorLog.lua index ba76a49..d8c0d4c 100644 --- a/DevForge/Modules/EventMonitor/EventMonitorLog.lua +++ b/DevForge/Modules/EventMonitor/EventMonitorLog.lua @@ -5,6 +5,7 @@ DF.EventMonitorLog = {} local Log = DF.EventMonitorLog local MAX_ENTRIES = 2000 +local DRAIN_PER_FRAME = 25 -- max queued events to process per frame local entries = {} local paused = false local filters = {} -- event name -> true (whitelist). empty = capture all @@ -12,6 +13,10 @@ local blacklist = {} -- event name -> true (never capture) local entryId = 0 local onNewEntry = nil -- callback(entry) +-- Frame-batching: queue raw event data, process expensive serialization across frames +local pendingQueue = {} -- { { event, timestamp, numArgs, arg1, arg2, ... }, ... } +local drainFrame = nil -- OnUpdate frame for processing queue + -- High-frequency events blacklisted by default (same approach as Blizzard_EventTrace) local DEFAULT_BLACKLIST = { "CURSOR_CHANGED", "MODIFIER_STATE_CHANGED", @@ -27,29 +32,23 @@ function Log:Init() blacklist[event] = true end self:LoadBlacklist() + if DF.EventIndex then + DF.EventIndex:LoadDiscovered() + end end function Log:SetOnNewEntry(callback) onNewEntry = callback end --- Add an event to the log. Returns true if the event was actually captured. -function Log:Push(event, timestamp, ...) - if paused then return false end - - -- Blacklist check - if blacklist[event] then return false end - - -- Whitelist check (empty = capture all) - if next(filters) and not filters[event] then return false end - - entryId = entryId + 1 +-- Process a single queued event into a full entry (expensive: PrettyPrint, SecretGuard) +local function ProcessQueuedEvent(queued) + local event = queued.event + local numArgs = queued.numArgs local args = {} - local numArgs = math.min(select("#", ...), 64) -- cap for safety for i = 1, numArgs do - local val = select(i, ...) - -- Secret value check + local val = queued[i] if DF.SecretGuard:IsSecret(val) then args[i] = { display = DF.Colors.secret .. "[secret]|r", raw = nil } else @@ -58,9 +57,9 @@ function Log:Push(event, timestamp, ...) end local entry = { - id = entryId, + id = queued.id, event = event, - time = timestamp or GetTime(), + time = queued.time, args = args, numArgs = numArgs, } @@ -75,6 +74,75 @@ function Log:Push(event, timestamp, ...) if onNewEntry then onNewEntry(entry) end +end + +-- OnUpdate handler: drain pending queue in batches +local function DrainQueue() + if #pendingQueue == 0 then + if drainFrame then drainFrame:Hide() end + return + end + + local count = math.min(#pendingQueue, DRAIN_PER_FRAME) + for i = 1, count do + ProcessQueuedEvent(pendingQueue[i]) + end + + -- Shift remaining items (remove processed from front) + if count == #pendingQueue then + wipe(pendingQueue) + if drainFrame then drainFrame:Hide() end + else + local remaining = #pendingQueue - count + for i = 1, remaining do + pendingQueue[i] = pendingQueue[i + count] + end + for i = remaining + 1, remaining + count do + pendingQueue[i] = nil + end + end +end + +local function EnsureDrainFrame() + if not drainFrame then + drainFrame = CreateFrame("Frame") + drainFrame:SetScript("OnUpdate", DrainQueue) + end + drainFrame:Show() +end + +-- Add an event to the log. Returns true if the event was queued for capture. +function Log:Push(event, timestamp, ...) + -- Auto-discover unknown events (before filtering, so blacklisted ones get discovered too) + if DF.EventIndex and not DF.EventIndex:IsKnown(event) then + DF.EventIndex:RegisterDiscovered(event) + end + + if paused then return false end + + -- Blacklist check + if blacklist[event] then return false end + + -- Whitelist check (empty = capture all) + if next(filters) and not filters[event] then return false end + + -- Lightweight capture: assign ID and timestamp now, defer expensive serialization + entryId = entryId + 1 + + local numArgs = math.min(select("#", ...), 64) + local queued = { + id = entryId, + event = event, + time = timestamp or GetTime(), + numArgs = numArgs, + } + -- Store raw arg values by index (cheap table insert) + for i = 1, numArgs do + queued[i] = select(i, ...) + end + + pendingQueue[#pendingQueue + 1] = queued + EnsureDrainFrame() return true end @@ -84,11 +152,13 @@ function Log:GetEntries() end function Log:GetCount() - return #entries + return #entries + #pendingQueue end function Log:Clear() wipe(entries) + wipe(pendingQueue) + if drainFrame then drainFrame:Hide() end entryId = 0 end @@ -192,7 +262,10 @@ function Log:FormatEntry(entry) return timeStr .. " " .. eventStr .. argStr end --- Save blacklist on logout +-- Save blacklist + discovered events on logout DF.EventBus:On("DF_PLAYER_LOGOUT", function() DF.EventMonitorLog:SaveBlacklist() + if DF.EventIndex then + DF.EventIndex:SaveDiscovered() + end end) diff --git a/DevForge/Modules/MacroEditor/MacroList.lua b/DevForge/Modules/MacroEditor/MacroList.lua index 2816f5e..0bea4d6 100644 --- a/DevForge/Modules/MacroEditor/MacroList.lua +++ b/DevForge/Modules/MacroEditor/MacroList.lua @@ -25,6 +25,7 @@ function MacList:Create(parent) end local row = CreateFrame("Button", nil, self.pane:GetContent()) + row:RegisterForClicks("LeftButtonUp") row:SetHeight(ROW_HEIGHT) -- Selection highlight diff --git a/DevForge/Modules/Performance/PerfData.lua b/DevForge/Modules/Performance/PerfData.lua index 342463c..1358780 100644 --- a/DevForge/Modules/Performance/PerfData.lua +++ b/DevForge/Modules/Performance/PerfData.lua @@ -4,8 +4,9 @@ DF.PerfData = {} local PerfData = DF.PerfData -local snapshots = {} -- { name -> snapshot } -local sortedList = {} -- sorted array of snapshots +local snapshots = {} -- { name -> snapshot } +local virtualSnapshots = {} -- { key -> snapshot } for debug/run-project entries +local sortedList = {} -- sorted array of snapshots local ticker = nil local onUpdate = nil local pollingInterval = 2 @@ -67,13 +68,29 @@ local function DoUpdate() end end - -- Rebuild sorted list + -- Poll virtual entries (debug/run-project) + for _, snap in pairs(virtualSnapshots) do + if snap.pollFn then + local ok, data = pcall(snap.pollFn) + if ok and data and data.cpu then + local prevCpu = snap.cpu or 0 + snap.cpu = data.cpu + snap.cpuDelta = data.cpu - prevCpu + snap.cpuPerSec = (pollingInterval > 0) and (snap.cpuDelta / pollingInterval) or 0 + end + end + end + + -- Rebuild sorted list (real + virtual) wipe(sortedList) for _, snap in pairs(snapshots) do if snap.loaded then sortedList[#sortedList + 1] = snap end end + for _, snap in pairs(virtualSnapshots) do + sortedList[#sortedList + 1] = snap + end if onUpdate then onUpdate() end end @@ -153,3 +170,17 @@ end function PerfData:SetOnUpdate(cb) onUpdate = cb end + +-- Virtual entries for debug / Run Project tracking +function PerfData:RegisterVirtual(key, name, pollFn) + local snap = NewSnapshot(name) + snap.loaded = true + snap.virtual = true + snap.pollFn = pollFn + virtualSnapshots[key] = snap + return snap +end + +function PerfData:UnregisterVirtual(key) + virtualSnapshots[key] = nil +end diff --git a/DevForge/Modules/Performance/PerfTable.lua b/DevForge/Modules/Performance/PerfTable.lua index da90e00..a2ab952 100644 --- a/DevForge/Modules/Performance/PerfTable.lua +++ b/DevForge/Modules/Performance/PerfTable.lua @@ -45,6 +45,7 @@ function PerfTable:Create(parent) local headerBtns = {} for i, col in ipairs(COLUMNS) do local btn = CreateFrame("Button", nil, header) + btn:RegisterForClicks("LeftButtonUp") btn:SetHeight(tbl.headerHeight) local text = btn:CreateFontString(nil, "OVERLAY") @@ -278,7 +279,11 @@ function PerfTable:Create(parent) local text = "" if col.key == "name" then text = val or "" - ft:SetTextColor(0.83, 0.83, 0.83, 1) + if snap.virtual then + ft:SetTextColor(0.9, 0.75, 0.3, 1) + else + ft:SetTextColor(0.83, 0.83, 0.83, 1) + end elseif col.key == "memory" or col.key == "memoryPeak" then text = format("%.0f", val or 0) ft:SetTextColor(unpack(memColor)) diff --git a/DevForge/Modules/SnippetEditor/AddonScaffold.lua b/DevForge/Modules/SnippetEditor/AddonScaffold.lua index b8aa883..3bd9f45 100644 --- a/DevForge/Modules/SnippetEditor/AddonScaffold.lua +++ b/DevForge/Modules/SnippetEditor/AddonScaffold.lua @@ -239,6 +239,13 @@ end local function GetDialog() if dialog then return dialog end + -- Clean up stale named frame from previous /reload + local stale = _G["DevForgeAddonScaffold"] + if stale then + stale:Hide(); stale:EnableMouse(false) + for _, c in pairs({stale:GetChildren()}) do c:Hide(); c:EnableMouse(false) end + end + local frame = CreateFrame("Frame", "DevForgeAddonScaffold", UIParent, "BackdropTemplate") frame:SetFrameStrata("FULLSCREEN_DIALOG") frame:SetSize(480, 480) @@ -248,7 +255,9 @@ local function GetDialog() frame:EnableMouse(true) frame:Hide() DF.Theme:ApplyDialogChrome(frame) - tinsert(UISpecialFrames, "DevForgeAddonScaffold") + if not tContains(UISpecialFrames, "DevForgeAddonScaffold") then + tinsert(UISpecialFrames, "DevForgeAddonScaffold") + end -- Title bar local titleBar = CreateFrame("Frame", nil, frame) diff --git a/DevForge/Modules/SnippetEditor/FrameBuilder.lua b/DevForge/Modules/SnippetEditor/FrameBuilder.lua index cd4aec1..5be0002 100644 --- a/DevForge/Modules/SnippetEditor/FrameBuilder.lua +++ b/DevForge/Modules/SnippetEditor/FrameBuilder.lua @@ -243,6 +243,13 @@ end local function GetDialog() if dialog then return dialog end + -- Clean up stale named frame from previous /reload + local stale = _G["DevForgeFrameBuilder"] + if stale then + stale:Hide(); stale:EnableMouse(false) + for _, c in pairs({stale:GetChildren()}) do c:Hide(); c:EnableMouse(false) end + end + local frame = CreateFrame("Frame", "DevForgeFrameBuilder", UIParent, "BackdropTemplate") frame:SetFrameStrata("FULLSCREEN_DIALOG") frame:SetSize(480, 540) @@ -252,7 +259,9 @@ local function GetDialog() frame:EnableMouse(true) frame:Hide() DF.Theme:ApplyDialogChrome(frame) - tinsert(UISpecialFrames, "DevForgeFrameBuilder") + if not tContains(UISpecialFrames, "DevForgeFrameBuilder") then + tinsert(UISpecialFrames, "DevForgeFrameBuilder") + end -- Title bar local titleBar = CreateFrame("Frame", nil, frame) diff --git a/DevForge/Modules/SnippetEditor/SnippetEditor.lua b/DevForge/Modules/SnippetEditor/SnippetEditor.lua index 4fcba1a..6c7dc2c 100644 --- a/DevForge/Modules/SnippetEditor/SnippetEditor.lua +++ b/DevForge/Modules/SnippetEditor/SnippetEditor.lua @@ -27,6 +27,7 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local function CreateToggleButton(parent, text, width) local btn = CreateFrame("Button", nil, parent, "BackdropTemplate") + btn:RegisterForClicks("LeftButtonUp") btn:SetSize(width, 20) btn:SetBackdrop({ bgFile = "Interface\\ChatFrame\\ChatFrameBackground", @@ -692,17 +693,42 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local addonName = project.name local filesRun, filesErrored = 0, 0 + -- Memory baseline for Perf tracking + collectgarbage("collect") + local memBefore = collectgarbage("count") + local cpuAccum = 0 -- microseconds accumulated by wrapped frame scripts + local trackedFrames = {} + + -- Unregister previous virtual entry if re-running + local debugKey = "debug:" .. project.id + if ranProjects[project.id] then + DF.PerfData:UnregisterVirtual(debugKey) + end + -- Track frames that register for lifecycle events during execution - -- so we can simulate them after all files are loaded + -- so we can simulate them after all files are loaded. + -- IMPORTANT: The CreateFrame wrapper stays active during event simulation + -- so that frames created by event handlers (e.g. ns.Init() called from PEW) + -- also get their lifecycle events tracked and fired in subsequent passes. local addonLoadedFrames = {} + local playerLoginFrames = {} local enteringWorldFrames = {} local realCreateFrame = CreateFrame + local frameSeen = {} -- prevent duplicate tracking CreateFrame = function(frameType, ...) local frame = realCreateFrame(frameType, ...) + trackedFrames[#trackedFrames + 1] = frame local realRegisterEvent = frame.RegisterEvent frame.RegisterEvent = function(self, event, ...) + local key = tostring(self) .. event + if frameSeen[key] then + return realRegisterEvent(self, event, ...) + end + frameSeen[key] = true if event == "ADDON_LOADED" then addonLoadedFrames[#addonLoadedFrames + 1] = self + elseif event == "PLAYER_LOGIN" then + playerLoginFrames[#playerLoginFrames + 1] = self elseif event == "PLAYER_ENTERING_WORLD" then enteringWorldFrames[#enteringWorldFrames + 1] = self end @@ -739,17 +765,21 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) end end - -- Restore CreateFrame before firing events - CreateFrame = realCreateFrame + -- Simulate lifecycle events with cascading: handlers may create new frames + -- that register for later events. We loop until no new frames appear. + -- WoW order: ADDON_LOADED → PLAYER_LOGIN → PLAYER_ENTERING_WORLD + local processedAL, processedLogin, processedPEW = 0, 0, 0 + local maxPasses = 5 + + -- Helper: fire an event on unprocessed frames, capture print output + local function FireEvent(eventName, frames, processed, ...) + if processed >= #frames then return processed end - -- Simulate ADDON_LOADED for frames that registered during execution - if #addonLoadedFrames > 0 then DF.EventBus:Fire("DF_OUTPUT_LINE", { - text = "-- Firing ADDON_LOADED", + text = "-- Firing " .. eventName, color = DF.Colors.comment, }) - -- Capture print output during event handlers local prints = {} local origPrint = print print = function(...) @@ -760,10 +790,14 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) prints[#prints + 1] = table.concat(parts, " ") end - for _, frame in ipairs(addonLoadedFrames) do + local eventArgs = { ... } + local nArgs = select("#", ...) + while processed < #frames do + processed = processed + 1 + local frame = frames[processed] local handler = frame:GetScript("OnEvent") if handler then - local ok, err = pcall(handler, frame, "ADDON_LOADED", addonName) + local ok, err = pcall(handler, frame, eventName, unpack(eventArgs, 1, nArgs)) if not ok then prints[#prints + 1] = DF.Colors.error .. tostring(err) .. "|r" filesErrored = filesErrored + 1 @@ -776,43 +810,60 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) for _, line in ipairs(prints) do DF.EventBus:Fire("DF_OUTPUT_LINE", { text = DF.Colors.text .. line .. "|r" }) end + + return processed end - -- Simulate PLAYER_ENTERING_WORLD for frames that registered during execution - if #enteringWorldFrames > 0 then - DF.EventBus:Fire("DF_OUTPUT_LINE", { - text = "-- Firing PLAYER_ENTERING_WORLD", - color = DF.Colors.comment, - }) + for pass = 1, maxPasses do + local alBefore = #addonLoadedFrames + local loginBefore = #playerLoginFrames + local pewBefore = #enteringWorldFrames - local prints = {} - local origPrint = print - print = function(...) - local parts = {} - for i = 1, select("#", ...) do - parts[i] = tostring(select(i, ...)) - end - prints[#prints + 1] = table.concat(parts, " ") - end + processedAL = FireEvent("ADDON_LOADED", addonLoadedFrames, processedAL, addonName) + processedLogin = FireEvent("PLAYER_LOGIN", playerLoginFrames, processedLogin) + processedPEW = FireEvent("PLAYER_ENTERING_WORLD", enteringWorldFrames, processedPEW, true, false) - for _, frame in ipairs(enteringWorldFrames) do - local handler = frame:GetScript("OnEvent") - if handler then - local ok, err = pcall(handler, frame, "PLAYER_ENTERING_WORLD", true, false) - if not ok then - prints[#prints + 1] = DF.Colors.error .. tostring(err) .. "|r" - filesErrored = filesErrored + 1 - end - end - end + -- If no new frames were added during this pass, we're done + local hadNew = (#addonLoadedFrames > alBefore) + or (#playerLoginFrames > loginBefore) + or (#enteringWorldFrames > pewBefore) + if not hadNew then break end + end - print = origPrint + -- Restore CreateFrame AFTER all simulation is complete + CreateFrame = realCreateFrame - for _, line in ipairs(prints) do - DF.EventBus:Fire("DF_OUTPUT_LINE", { text = DF.Colors.text .. line .. "|r" }) + -- Measure memory allocation and register Perf virtual entry + local memAfter = collectgarbage("count") + local memUsed = math.max(0, memAfter - memBefore) + + -- Wrap OnUpdate/OnEvent on tracked frames for live CPU measurement + for _, frame in ipairs(trackedFrames) do + local origOnUpdate = frame:GetScript("OnUpdate") + if origOnUpdate then + frame:SetScript("OnUpdate", function(self, elapsed) + local t0 = debugprofilestop() + origOnUpdate(self, elapsed) + cpuAccum = cpuAccum + (debugprofilestop() - t0) + end) + end + local origOnEvent = frame:GetScript("OnEvent") + if origOnEvent then + frame:SetScript("OnEvent", function(self, event, ...) + local t0 = debugprofilestop() + origOnEvent(self, event, ...) + cpuAccum = cpuAccum + (debugprofilestop() - t0) + end) end end + -- Register virtual entry in PerfData + local snap = DF.PerfData:RegisterVirtual(debugKey, "DEBUG: " .. project.name, function() + return { cpu = cpuAccum / 1000 } -- convert μs to ms + end) + snap.memory = memUsed + snap.memoryPeak = memUsed + -- Summary local summary = filesRun .. " file(s) executed" if filesErrored > 0 then @@ -842,8 +893,7 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) end end) - runBtn:SetScript("OnClick", function(_, _, down) - if down then return end -- ignore mouse-down, fire only on mouse-up + runBtn:SetScript("OnClick", function() if not currentSnippetId then return end SaveCurrent() if codeEditor.PushUndo then codeEditor:PushUndo() end diff --git a/DevForge/Modules/TableViewer/TableViewer.lua b/DevForge/Modules/TableViewer/TableViewer.lua index 03003e4..bf76b2c 100644 --- a/DevForge/Modules/TableViewer/TableViewer.lua +++ b/DevForge/Modules/TableViewer/TableViewer.lua @@ -129,6 +129,7 @@ DF.ModuleSystem:Register("TableViewer", function(sidebarParent, editorParent) -- Status label (clickable — click to copy raw text) local statusBtn = CreateFrame("Button", nil, toolbar) + statusBtn:RegisterForClicks("LeftButtonUp") statusBtn:SetPoint("LEFT", copyBtn, "RIGHT", 8, 0) statusBtn:SetPoint("RIGHT", -4, 0) statusBtn:SetHeight(DF.Layout.buttonHeight) diff --git a/DevForge/Modules/TextureBrowser/TextureBrowser.lua b/DevForge/Modules/TextureBrowser/TextureBrowser.lua index 46c2675..af2426c 100644 --- a/DevForge/Modules/TextureBrowser/TextureBrowser.lua +++ b/DevForge/Modules/TextureBrowser/TextureBrowser.lua @@ -54,6 +54,7 @@ DF.ModuleSystem:Register("TextureBrowser", function(sidebarParent, editorParent) end) local previewBlocker = CreateFrame("Button", nil, UIParent) + previewBlocker:RegisterForClicks("LeftButtonUp") previewBlocker:SetAllPoints(UIParent) previewBlocker:SetFrameStrata("FULLSCREEN") previewBlocker:Hide() @@ -170,6 +171,7 @@ DF.ModuleSystem:Register("TextureBrowser", function(sidebarParent, editorParent) for i, def in ipairs(TAB_DEFS) do local btn = CreateFrame("Button", nil, tabBtnRow, "BackdropTemplate") + btn:RegisterForClicks("LeftButtonUp") btn:SetHeight(tabBtnHeight) btn:SetBackdrop({ bgFile = "Interface\\ChatFrame\\ChatFrameBackground", @@ -417,6 +419,7 @@ DF.ModuleSystem:Register("TextureBrowser", function(sidebarParent, editorParent) -- Favorite star local star = CreateFrame("Button", nil, item.frame) + star:RegisterForClicks("LeftButtonUp") star:SetSize(14, 14) star:SetPoint("TOPRIGHT", item.frame, "TOPRIGHT", -1, -1) star:SetFrameLevel(item.frame:GetFrameLevel() + 2) diff --git a/DevForge/Modules/WAImporter/WACodeGen.lua b/DevForge/Modules/WAImporter/WACodeGen.lua index bd3f0ee..4280710 100644 --- a/DevForge/Modules/WAImporter/WACodeGen.lua +++ b/DevForge/Modules/WAImporter/WACodeGen.lua @@ -8,21 +8,6 @@ local WACodeGen = DF.WACodeGen -- Helpers --------------------------------------------------------------------------- --- Check if an aura should start shown (custom/always-on triggers) --- vs hidden (event-driven triggers like aura2/status) -local function ShouldStartShown(aura) - if not aura.triggers or #aura.triggers == 0 then - return false - end - for _, trig in ipairs(aura.triggers) do - if trig.type == "aura2" or trig.type == "status" or trig.type == "event" then - return false - end - end - -- All triggers are custom or unknown — likely an always-on display - return true -end - local function SanitizeName(name) if not name or name == "" then return "MyAura" end return name:gsub("[^%w_]", ""):gsub("^%d", "_") @@ -43,18 +28,61 @@ local function Indent(lines, prefix) return out end --- Check if an aura has any custom triggers -local function HasCustomTriggers(aura) +-- Check if an aura has event-driven triggers that handle visibility +local function HasEventTriggers(aura) for _, trig in ipairs(aura.triggers or {}) do - if trig.type == "custom" and trig.custom then return true end + if trig.type == "aura2" or trig.type == "status" or trig.type == "event" then + return true + end end return false end --- Check if an aura has event-driven triggers that handle visibility -local function HasEventTriggers(aura) +-- Parse a WA custom trigger events string ("EVENT1 EVENT2:unit") into structured list +local function ParseCustomEvents(eventsStr) + if not eventsStr or eventsStr == "" then return {} end + local events = {} + for token in eventsStr:gmatch("%S+") do + -- Strip trailing commas/semicolons (WA often comma-separates events) + token = token:gsub("[,;]+$", "") + if token ~= "" then + local evt, unit = token:match("^(.+):(.+)$") + if evt then + events[#events + 1] = { event = evt, unit = unit } + else + events[#events + 1] = { event = token } + end + end + end + return events +end + +-- Strip leading comment lines and whitespace from WA custom code, +-- returning (leadingComments, strippedCode) +local function StripLeadingComments(code) + local lines = {} + local leading = {} + local pastComments = false + for line in code:gmatch("[^\r\n]+") do + if not pastComments then + local trimmed = line:match("^%s*(.-)%s*$") + if trimmed == "" or trimmed:match("^%-%-") then + leading[#leading + 1] = line + else + pastComments = true + lines[#lines + 1] = line + end + else + lines[#lines + 1] = line + end + end + return leading, table.concat(lines, "\n") +end + +-- Has custom triggers that should be polled via OnUpdate (excludes stateupdate/event-driven) +local function HasPollableCustomTriggers(aura) for _, trig in ipairs(aura.triggers or {}) do - if trig.type == "aura2" or trig.type == "status" or trig.type == "event" then + if trig.type == "custom" and trig.custom and trig.custom_type ~= "stateupdate" and trig.custom_type ~= "event" then return true end end @@ -163,11 +191,13 @@ local function EmitAuraEnv(lines, aura, frameName) if frameName then lines[#lines + 1] = " aura_env.region = " .. frameName end + lines[#lines + 1] = " aura_env.state = {}" + lines[#lines + 1] = " aura_env.states = {}" lines[#lines + 1] = "" end --- Emit WA API stubs if the aura's custom code references WeakAuras APIs. --- Uses real WA table if WeakAuras is installed, stubs otherwise. +-- Emit per-aura WA setup: register this aura's region in the global WA regions table. +-- Global WA stubs are set up once in GenerateInit; this just adds aura-specific bindings. local function EmitWAStubs(lines, aura) -- Collect all custom code strings to scan local codeStrings = {} @@ -186,22 +216,10 @@ local function EmitWAStubs(lines, aura) local needsWA = allCode:find("WeakAuras") if not needsWA then return end - lines[#lines + 1] = " -- WA API compatibility shim (uses real WA if installed, stubs if not)" - lines[#lines + 1] = " local WeakAuras = WeakAuras or {}" - - if allCode:find("WeakAuras%.IsOptionsOpen") or allCode:find("WeakAuras%.IsOptionsOpen%(") then - lines[#lines + 1] = " if not WeakAuras.IsOptionsOpen then WeakAuras.IsOptionsOpen = function() return false end end" - end - if allCode:find("WeakAuras%.GetRegion") then - lines[#lines + 1] = " if not WeakAuras.GetRegion then WeakAuras.GetRegion = function() return aura_env.region end end" - end - if allCode:find("WeakAuras%.regions") then - lines[#lines + 1] = " if not WeakAuras.regions then WeakAuras.regions = setmetatable({}, { __index = function() return { region = aura_env.region } end }) end" - end - if allCode:find("WeakAuras%.GetData") then - lines[#lines + 1] = " if not WeakAuras.GetData then WeakAuras.GetData = function() return {} end end" - end - + lines[#lines + 1] = " -- Register this aura in WA regions table (global stubs set up at top of Init)" + lines[#lines + 1] = " if WeakAuras and WeakAuras.regions then" + lines[#lines + 1] = " WeakAuras.regions[" .. Quoted(aura.id or "Unnamed") .. "] = { region = aura_env.region }" + lines[#lines + 1] = " end" lines[#lines + 1] = "" end @@ -223,6 +241,108 @@ local function EmitCustomCode(lines, code, banner, asLive) lines[#lines + 1] = " -- ============ end WA " .. banner .. " ============" end +-- Wire onShow/onHide code as frame callbacks instead of commenting them out. +local function EmitFrameCallbacks(lines, aura, frameName) + if aura.onShowCode then + lines[#lines + 1] = "" + lines[#lines + 1] = " -- ============ WA onShow callback ============" + lines[#lines + 1] = " " .. frameName .. ':SetScript("OnShow", function(self)' + for line in aura.onShowCode:gmatch("[^\r\n]+") do + lines[#lines + 1] = " " .. line + end + lines[#lines + 1] = " end)" + end + if aura.onHideCode then + lines[#lines + 1] = "" + lines[#lines + 1] = " -- ============ WA onHide callback ============" + lines[#lines + 1] = " " .. frameName .. ':SetScript("OnHide", function(self)' + for line in aura.onHideCode:gmatch("[^\r\n]+") do + lines[#lines + 1] = " " .. line + end + lines[#lines + 1] = " end)" + end +end + +-- Fire stateupdate triggers once after setup to establish initial state. +local function EmitInitialStateUpdateEval(lines, aura, frameName) + for i, trig in ipairs(aura.triggers or {}) do + if trig.type == "custom" and trig.custom_type == "stateupdate" then + lines[#lines + 1] = "" + lines[#lines + 1] = " -- Initial stateupdate evaluation (trigger " .. i .. ")" + lines[#lines + 1] = " do" + lines[#lines + 1] = " local changed = stateUpdate_" .. i .. " and stateUpdate_" .. i .. "(allstates_" .. i .. ', "STATUS")' + lines[#lines + 1] = " if changed then" + lines[#lines + 1] = " local anyShow = (next(allstates_" .. i .. ") == nil) -- empty allstates = trigger active" + lines[#lines + 1] = " for _, st in pairs(allstates_" .. i .. ") do" + lines[#lines + 1] = " if st.show then anyShow = true; break end" + lines[#lines + 1] = " end" + lines[#lines + 1] = " triggerStates[" .. i .. "] = anyShow" + lines[#lines + 1] = " EvalTriggers()" + lines[#lines + 1] = " end" + lines[#lines + 1] = " end" + end + end +end + +-- Emit per-frame unified trigger state tracking. +-- Creates a triggerStates table and EvalTriggers() function that combines +-- all trigger states according to the aura's disjunctive mode (AND/OR). +local function EmitTriggerStateTracking(lines, aura, frameName) + if not aura.triggers or #aura.triggers == 0 then return end + + local N = #aura.triggers + local useAny = (aura.disjunctive == "any") + + lines[#lines + 1] = "" + lines[#lines + 1] = " -- Unified trigger state tracking (disjunctive=" .. tostring(aura.disjunctive) .. ")" + lines[#lines + 1] = " local triggerStates = {}" + for i, trig in ipairs(aura.triggers) do + if trig.type == "custom" or trig.type == "aura2" or trig.type == "status" or trig.type == "event" then + lines[#lines + 1] = " triggerStates[" .. i .. "] = false" + else + -- Unknown trigger type: default to active so it doesn't block visibility + lines[#lines + 1] = " triggerStates[" .. i .. "] = true -- " .. tostring(trig.type) .. ": default active" + end + end + lines[#lines + 1] = "" + if useAny then + lines[#lines + 1] = " local function EvalTriggers()" + lines[#lines + 1] = " for i = 1, " .. N .. " do" + lines[#lines + 1] = " if triggerStates[i] then " .. frameName .. ":Show(); return end" + lines[#lines + 1] = " end" + lines[#lines + 1] = " " .. frameName .. ":Hide()" + lines[#lines + 1] = " end" + else + lines[#lines + 1] = " local function EvalTriggers()" + lines[#lines + 1] = " for i = 1, " .. N .. " do" + lines[#lines + 1] = " if not triggerStates[i] then " .. frameName .. ":Hide(); return end" + lines[#lines + 1] = " end" + lines[#lines + 1] = " " .. frameName .. ":Show()" + lines[#lines + 1] = " end" + end +end + +-- Check if any aura in the analysis references WeakAuras APIs +local function AnalysisNeedsWA(analysis) + for _, aura in ipairs(analysis.auras or {}) do + local codeStrings = {} + local function collect(s) if s then codeStrings[#codeStrings + 1] = s end end + collect(aura.initCode) + collect(aura.onShowCode) + collect(aura.onHideCode) + collect(aura.customText) + for _, trig in ipairs(aura.triggers or {}) do + collect(trig.custom) + collect(trig.customName) + end + if #codeStrings > 0 then + local allCode = table.concat(codeStrings, "\n") + if allCode:find("WeakAuras") then return true end + end + end + return false +end + -- WA bundled texture substitutions local WA_TEXTURE_SUBS = { Circle_Smooth2 = "Interface\\COMMON\\Indicator-Gray", @@ -376,16 +496,73 @@ local function GenPowerTrigger(trig, frameName, index) return lines end -local function GenCustomTrigger(trig, index) +local function GenStateUpdateTrigger(trig, frameName, index) + local lines = {} + local function add(s) lines[#lines + 1] = s end + + -- Register events from the trigger's events field (pcall for custom WA events) + local events = ParseCustomEvents(trig.events) + for _, ev in ipairs(events) do + if ev.unit then + add('pcall(' .. frameName .. '.RegisterUnitEvent, ' .. frameName .. ', "' .. ev.event .. '", "' .. ev.unit .. '")') + else + add('pcall(' .. frameName .. '.RegisterEvent, ' .. frameName .. ', "' .. ev.event .. '")') + end + end + + -- State table for this trigger + add("local allstates_" .. index .. " = {}") + add("") + + -- The stateupdate function: signature is function(allstates, event, ...) + add("-- WA stateupdate trigger " .. index) + if trig.custom then + local code = trig.custom:match("^%s*(.-)%s*$") + local leadingLines, stripped = StripLeadingComments(code) + if stripped:match("^function%s*%(") then + for _, cline in ipairs(leadingLines) do + add(cline) + end + add("local stateUpdate_" .. index .. " = " .. stripped) + else + add("local function stateUpdate_" .. index .. "(allstates, event, ...)") + for line in code:gmatch("[^\r\n]+") do + add(" " .. line) + end + add("end") + end + else + add("-- (no custom code found for stateupdate trigger)") + end + + return lines +end + +local function GenCustomTrigger(trig, frameName, index) local lines = {} local function add(s) lines[#lines + 1] = s end + -- Register events if present (event-driven custom triggers; pcall for custom WA events) + if trig.events and trig.events ~= "" and frameName then + local events = ParseCustomEvents(trig.events) + for _, ev in ipairs(events) do + if ev.unit then + add('pcall(' .. frameName .. '.RegisterUnitEvent, ' .. frameName .. ', "' .. ev.event .. '", "' .. ev.unit .. '")') + else + add('pcall(' .. frameName .. '.RegisterEvent, ' .. frameName .. ', "' .. ev.event .. '")') + end + end + end + add("-- WA custom trigger " .. index) if trig.custom then local code = trig.custom:match("^%s*(.-)%s*$") - -- WA custom triggers are anonymous function bodies; assign to a local - if code:match("^function%s*%(") then - add("local customTrigger_" .. index .. " = " .. code) + local leadingLines, stripped = StripLeadingComments(code) + if stripped:match("^function%s*%(") then + for _, cline in ipairs(leadingLines) do + add(cline) + end + add("local customTrigger_" .. index .. " = " .. stripped) else add("local function customTrigger_" .. index .. "()") for line in code:gmatch("[^\r\n]+") do @@ -434,7 +611,10 @@ local function GenTriggerCode(trig, frameName, index) return lines end elseif trig.type == "custom" then - return GenCustomTrigger(trig, index) + if trig.custom_type == "stateupdate" then + return GenStateUpdateTrigger(trig, frameName, index) + end + return GenCustomTrigger(trig, frameName, index) elseif trig.type == "event" then return GenEventTrigger(trig, frameName, index) end @@ -461,25 +641,26 @@ local function GenOnEventHandler(aura, frameName, varPrefix) hasTriggers = true break end + if trig.type == "custom" and (trig.custom_type == "stateupdate" or trig.custom_type == "event") then + hasTriggers = true + break + end end if not hasTriggers then return lines end add(frameName .. ':SetScript("OnEvent", function(self, event, ...)') - -- Aura triggers + -- Standard triggers (aura2 / status / event) for i, trig in ipairs(aura.triggers) do if trig.type == "aura2" then add(' if event == "UNIT_AURA" then') add(" local show = CheckAura_" .. i .. "()") - if aura.regionType == "icon" or aura.regionType == "texture" then - add(" if show then self:Show() else self:Hide() end") - elseif aura.regionType == "aurabar" then + if aura.regionType == "aurabar" then add(" self.active = show") - add(" if show then self:Show() else self:Hide() end") - else - add(" if show then self:Show() else self:Hide() end") end + add(" triggerStates[" .. i .. "] = show") + add(" EvalTriggers()") add(" end") elseif trig.type == "status" then if trig.event == "Cooldown Progress (Spell)" or @@ -487,15 +668,10 @@ local function GenOnEventHandler(aura, frameName, varPrefix) add(' if event == "SPELL_UPDATE_COOLDOWN" then') add(" local onCD, start, dur = CheckCooldown_" .. i .. "()") if aura.regionType == "icon" then - add(" if onCD and self.cooldown then") - add(" self.cooldown:SetCooldown(start, dur)") - add(" self:Show()") - add(" else") - add(" self:Hide()") - add(" end") - else - add(" if onCD then self:Show() else self:Hide() end") + add(" if onCD and self.cooldown then self.cooldown:SetCooldown(start, dur) end") end + add(" triggerStates[" .. i .. "] = onCD and true or false") + add(" EvalTriggers()") add(" end") elseif trig.event == "Health" then add(' if event == "UNIT_HEALTH" then') @@ -520,8 +696,46 @@ local function GenOnEventHandler(aura, frameName, varPrefix) end end + -- Stateupdate triggers: called on any event, manage allstates table + for i, trig in ipairs(aura.triggers) do + if trig.type == "custom" and trig.custom_type == "stateupdate" then + add("") + add(" -- Stateupdate trigger " .. i) + add(" do") + add(" local changed = stateUpdate_" .. i .. " and stateUpdate_" .. i .. "(allstates_" .. i .. ", event, ...)") + add(" if changed then") + add(" local anyShow = (next(allstates_" .. i .. ") == nil) -- empty allstates = trigger active") + add(" for _, st in pairs(allstates_" .. i .. ") do") + add(" if st.show then anyShow = true; break end") + add(" end") + add(" triggerStates[" .. i .. "] = anyShow") + add(" EvalTriggers()") + add(" end") + add(" end") + end + end + + -- Event-driven custom triggers: called on their registered events + for i, trig in ipairs(aura.triggers) do + if trig.type == "custom" and trig.custom_type == "event" then + add("") + add(" -- Event-driven custom trigger " .. i) + add(" do") + add(" local result = customTrigger_" .. i .. " and customTrigger_" .. i .. "(event, ...)") + add(" triggerStates[" .. i .. "] = result and true or false") + add(" EvalTriggers()") + add(" end") + end + end + add("end)") + -- Register frame for WA ScanEvents custom event dispatch + add("") + add("if WeakAuras and WeakAuras._scanEventFrames then") + add(" WeakAuras._scanEventFrames[#WeakAuras._scanEventFrames + 1] = " .. frameName) + add("end") + return lines end @@ -535,7 +749,10 @@ end local function EmitCustomTriggerOnUpdate(lines, aura, frameName, displayFnName, additionalLines) local customIdxs = {} for i, trig in ipairs(aura.triggers or {}) do - if trig.type == "custom" and trig.custom then + -- Only poll non-stateupdate, non-event-driven custom triggers via OnUpdate + if trig.type == "custom" and trig.custom + and trig.custom_type ~= "stateupdate" + and trig.custom_type ~= "event" then customIdxs[#customIdxs + 1] = i end end @@ -546,8 +763,6 @@ local function EmitCustomTriggerOnUpdate(lines, aura, frameName, displayFnName, if not hasCustomTriggers and not hasDynText and not mouseFollow then return end - local useAny = (aura.disjunctive == "any") - lines[#lines + 1] = "" -- Simple cursor-only OnUpdate (no throttle needed) @@ -592,22 +807,13 @@ local function EmitCustomTriggerOnUpdate(lines, aura, frameName, displayFnName, lines[#lines + 1] = " local trigResult_" .. idx .. " = customTrigger_" .. idx .. " and customTrigger_" .. idx .. "()" end - -- Compute show from individual results - local parts = {} - for _, idx in ipairs(customIdxs) do - parts[#parts + 1] = "trigResult_" .. idx - end - + -- Update unified trigger states from polled results lines[#lines + 1] = "" - if useAny then - lines[#lines + 1] = " -- Show if any trigger active (disjunctive=\"any\")" - lines[#lines + 1] = " local show = " .. table.concat(parts, " or ") - else - lines[#lines + 1] = " -- Show if all triggers active (disjunctive=\"all\")" - lines[#lines + 1] = " local show = " .. table.concat(parts, " and ") + for _, idx in ipairs(customIdxs) do + lines[#lines + 1] = " triggerStates[" .. idx .. "] = trigResult_" .. idx .. " and true or false" end - lines[#lines + 1] = " if not show then self:Hide() return end" - lines[#lines + 1] = " self:Show()" + lines[#lines + 1] = " EvalTriggers()" + lines[#lines + 1] = " if not self:IsShown() then return end" end if hasDynText then @@ -741,6 +947,9 @@ local function GenIconAura(aura, index, parentFrame) add("") end + -- Unified trigger state tracking + EmitTriggerStateTracking(lines, aura, frameName) + -- OnEvent handler local handlerLines = GenOnEventHandler(aura, frameName, var) for _, hl in ipairs(handlerLines) do @@ -749,24 +958,26 @@ local function GenIconAura(aura, index, parentFrame) -- WA custom code EmitCustomCode(lines, aura.initCode, "init custom code", true) - EmitCustomCode(lines, aura.onShowCode, "onShow custom code", false) - EmitCustomCode(lines, aura.onHideCode, "onHide custom code", false) + EmitFrameCallbacks(lines, aura, frameName) -- OnUpdate for custom trigger polling / cursor following - if (HasCustomTriggers(aura) and not HasEventTriggers(aura)) or aura.anchorFrameType == "MOUSE" then + if HasPollableCustomTriggers(aura) or aura.anchorFrameType == "MOUSE" then EmitCustomTriggerOnUpdate(lines, aura, frameName, nil) end + -- Initial stateupdate evaluation + EmitInitialStateUpdateEval(lines, aura, frameName) + if not aura.triggers or #aura.triggers == 0 then add("") add(" -- TODO: No triggers defined; frame created but no event handling") end add("") - if ShouldStartShown(aura) then - add(" " .. frameName .. ":Show()") + if aura.triggers and #aura.triggers > 0 then + add(" EvalTriggers() -- Set initial visibility from trigger defaults") else - add(" " .. frameName .. ":Hide() -- Hidden until triggered") + add(" " .. frameName .. ":Show() -- No triggers, always visible") end add("end") @@ -819,8 +1030,11 @@ local function GenAuraBarAura(aura, index, parentFrame) add("") end - if HasCustomTriggers(aura) and not HasEventTriggers(aura) then - -- Custom-trigger-only bar: poll triggers via OnUpdate + -- Unified trigger state tracking + EmitTriggerStateTracking(lines, aura, frameName) + + if HasPollableCustomTriggers(aura) then + -- Custom trigger polling via OnUpdate (handles show/hide via EvalTriggers) EmitCustomTriggerOnUpdate(lines, aura, frameName, nil) else -- OnUpdate for smooth countdown (event-driven bars) @@ -848,8 +1062,10 @@ local function GenAuraBarAura(aura, index, parentFrame) -- WA custom code EmitCustomCode(lines, aura.initCode, "init custom code", true) - EmitCustomCode(lines, aura.onShowCode, "onShow custom code", false) - EmitCustomCode(lines, aura.onHideCode, "onHide custom code", false) + EmitFrameCallbacks(lines, aura, frameName) + + -- Initial stateupdate evaluation + EmitInitialStateUpdateEval(lines, aura, frameName) if not aura.triggers or #aura.triggers == 0 then add("") @@ -857,10 +1073,10 @@ local function GenAuraBarAura(aura, index, parentFrame) end add("") - if ShouldStartShown(aura) then - add(" " .. frameName .. ":Show()") + if aura.triggers and #aura.triggers > 0 then + add(" EvalTriggers() -- Set initial visibility from trigger defaults") else - add(" " .. frameName .. ":Hide()") + add(" " .. frameName .. ":Show() -- No triggers, always visible") end add("end") @@ -968,6 +1184,9 @@ local function GenTextAura(aura, index, parentFrame) add("") end + -- Unified trigger state tracking + EmitTriggerStateTracking(lines, aura, frameName) + -- OnEvent handler (for event-driven triggers) local handlerLines = GenOnEventHandler(aura, frameName, var) for _, hl in ipairs(handlerLines) do @@ -976,15 +1195,17 @@ local function GenTextAura(aura, index, parentFrame) -- WA custom code EmitCustomCode(lines, aura.initCode, "init custom code", true) - EmitCustomCode(lines, aura.onShowCode, "onShow custom code", false) - EmitCustomCode(lines, aura.onHideCode, "onHide custom code", false) + EmitFrameCallbacks(lines, aura, frameName) -- OnUpdate: custom trigger polling + dynamic text refresh + conditions + cursor following - if HasCustomTriggers(aura) or hasDynamicText or aura.anchorFrameType == "MOUSE" then + if HasPollableCustomTriggers(aura) or hasDynamicText or aura.anchorFrameType == "MOUSE" then local condLines = BuildConditionLines(aura) EmitCustomTriggerOnUpdate(lines, aura, frameName, displayFnName, condLines) end + -- Initial stateupdate evaluation + EmitInitialStateUpdateEval(lines, aura, frameName) + if not aura.triggers or #aura.triggers == 0 then if not hasDynamicText then add("") @@ -993,10 +1214,10 @@ local function GenTextAura(aura, index, parentFrame) end add("") - if ShouldStartShown(aura) then - add(" " .. frameName .. ":Show()") + if aura.triggers and #aura.triggers > 0 then + add(" EvalTriggers() -- Set initial visibility from trigger defaults") else - add(" " .. frameName .. ":Hide()") + add(" " .. frameName .. ":Show() -- No triggers, always visible") end add("end") @@ -1055,6 +1276,9 @@ local function GenTextureAura(aura, index, parentFrame) add("") end + -- Unified trigger state tracking + EmitTriggerStateTracking(lines, aura, frameName) + -- OnEvent handler local handlerLines = GenOnEventHandler(aura, frameName, var) for _, hl in ipairs(handlerLines) do @@ -1063,24 +1287,26 @@ local function GenTextureAura(aura, index, parentFrame) -- WA custom code EmitCustomCode(lines, aura.initCode, "init custom code", true) - EmitCustomCode(lines, aura.onShowCode, "onShow custom code", false) - EmitCustomCode(lines, aura.onHideCode, "onHide custom code", false) + EmitFrameCallbacks(lines, aura, frameName) -- OnUpdate for custom trigger polling / cursor following - if (HasCustomTriggers(aura) and not HasEventTriggers(aura)) or aura.anchorFrameType == "MOUSE" then + if HasPollableCustomTriggers(aura) or aura.anchorFrameType == "MOUSE" then EmitCustomTriggerOnUpdate(lines, aura, frameName, nil) end + -- Initial stateupdate evaluation + EmitInitialStateUpdateEval(lines, aura, frameName) + if not aura.triggers or #aura.triggers == 0 then add("") add(" -- TODO: No triggers defined; frame created but no event handling") end add("") - if ShouldStartShown(aura) then - add(" " .. frameName .. ":Show()") + if aura.triggers and #aura.triggers > 0 then + add(" EvalTriggers() -- Set initial visibility from trigger defaults") else - add(" " .. frameName .. ":Hide()") + add(" " .. frameName .. ":Show() -- No triggers, always visible") end add("end") @@ -1271,6 +1497,70 @@ local function GenerateInit(analysis, projectName) add(" if not db.enabled then return end") add("") + -- Global WeakAuras API stubs (set up once, before any aura code runs) + if AnalysisNeedsWA(analysis) then + -- Emit aura data registry so GetData can return subRegions, config, etc. + add(" -- Aura data registry (for WeakAuras.GetData compatibility)") + add(" local _auraData = {") + for _, aura in ipairs(analysis.auras) do + add(" [" .. Quoted(aura.id) .. "] = {") + add(" id = " .. Quoted(aura.id) .. ",") + add(" regionType = " .. Quoted(aura.regionType or "unknown") .. ",") + if aura.subRegions then + add(" subRegions = " .. EmitTableLiteral(aura.subRegions, " ") .. ",") + end + if aura.config then + add(" config = " .. EmitTableLiteral(aura.config, " ") .. ",") + end + add(" },") + end + add(" }") + add("") + + add(" -- WeakAuras API compatibility (stubs when WeakAuras addon is not installed)") + add(" if not WeakAuras then") + add(" WeakAuras = setmetatable({") + add(' IsOptionsOpen = function() return false end,') + add(' GetData = function(id) return _auraData[id] or {} end,') + add(' GetRegion = function(id)') + add(' local r = WeakAuras.regions and WeakAuras.regions[id]') + add(' return r and r.region') + add(' end,') + add(' ScanEvents = function(event, ...)') + add(' for _, frame in ipairs(WeakAuras._scanEventFrames) do') + add(' local handler = frame:GetScript("OnEvent")') + add(' if handler then handler(frame, event, ...) end') + add(' end') + add(' end,') + add(' WatchGCD = function() end,') + add(' WatchSpellCooldown = function() end,') + add(' WatchItemCooldown = function() end,') + add(' WatchRuneDuration = function() end,') + add(' StopMotion = function() end,') + add(' prettyPrint = function(...) print(...) end,') + add(' IsRetail = function() return WOW_PROJECT_ID == WOW_PROJECT_MAINLINE end,') + add(' IsClassicEra = function() return false end,') + add(' IsCataClassic = function() return false end,') + add(' me = UnitGUID("player"),') + add(' myGUID = UnitGUID("player"),') + add(" regions = {},") + add(" currentStates = {},") + add(" _scanEventFrames = {},") + add(" }, {") + add(" __index = function(t, k)") + add(" -- Auto-stub unknown methods as no-ops to prevent errors") + add(" local v = rawget(t, k)") + add(" if v == nil then") + add(" v = function() end") + add(" rawset(t, k, v)") + add(" end") + add(" return v") + add(" end,") + add(" })") + add(" end") + add("") + end + if analysis.isGroup then -- Container frame for groups add(" -- Container frame for group: " .. (analysis.groupId or "WAGroup")) diff --git a/DevForge/Modules/WAImporter/WADecode.lua b/DevForge/Modules/WAImporter/WADecode.lua index bb2cdae..47c24d5 100644 --- a/DevForge/Modules/WAImporter/WADecode.lua +++ b/DevForge/Modules/WAImporter/WADecode.lua @@ -104,7 +104,15 @@ local function ParseTriggers(triggerList) spellName = t.realSpellName or t.spellName, custom = t.custom, customName = t.customName, + custom_type = t.custom_type, -- "stateupdate", "event", "status" + events = t.events, -- space-separated event list (e.g. "UNIT_AURA:player PLAYER_ENTERING_WORLD") + check = t.check, -- stateupdate check function } + -- Extract untrigger custom code + local ut = entry.untrigger + if ut and ut.custom then + info.customUntrigger = ut.custom + end triggers[#triggers + 1] = info end end @@ -353,6 +361,7 @@ local function AnalyzeAura(d) alpha = d.alpha, desaturate = d.desaturate, anchorFrameType = d.anchorFrameType, + subRegions = d.subRegions, } end diff --git a/DevForge/Modules/WAImporter/WAImporter.lua b/DevForge/Modules/WAImporter/WAImporter.lua index 9a34099..0ab1292 100644 --- a/DevForge/Modules/WAImporter/WAImporter.lua +++ b/DevForge/Modules/WAImporter/WAImporter.lua @@ -81,6 +81,13 @@ end local function GetDialog() if dialog then return dialog end + -- Clean up stale named frame from previous /reload + local stale = _G["DevForgeWAImporter"] + if stale then + stale:Hide(); stale:EnableMouse(false) + for _, c in pairs({stale:GetChildren()}) do c:Hide(); c:EnableMouse(false) end + end + local frame = CreateFrame("Frame", "DevForgeWAImporter", UIParent, "BackdropTemplate") frame:SetFrameStrata("FULLSCREEN_DIALOG") frame:SetSize(480, 520) @@ -90,7 +97,9 @@ local function GetDialog() frame:EnableMouse(true) frame:Hide() DF.Theme:ApplyDialogChrome(frame) - tinsert(UISpecialFrames, "DevForgeWAImporter") + if not tContains(UISpecialFrames, "DevForgeWAImporter") then + tinsert(UISpecialFrames, "DevForgeWAImporter") + end -- Title bar local titleBar = CreateFrame("Frame", nil, frame) @@ -166,6 +175,29 @@ local function GetDialog() decodeBtn:SetPoint("TOPLEFT", LEFT, yOff - 16) yOff = yOff - 16 - DF.Layout.buttonHeight - 6 + -- Warning banner (shown after successful decode) + local warningBanner = CreateFrame("Frame", nil, frame) + warningBanner:SetPoint("TOPLEFT", LEFT, -42) + warningBanner:SetPoint("RIGHT", frame, "RIGHT", RIGHT, 0) + warningBanner:Hide() + + local warningBg = warningBanner:CreateTexture(nil, "BACKGROUND") + warningBg:SetAllPoints() + warningBg:SetColorTexture(0.35, 0.25, 0.05, 0.5) + + local warningMsg = warningBanner:CreateFontString(nil, "OVERLAY") + warningMsg:SetFontObject(DF.Theme:UIFont()) + warningMsg:SetPoint("TOPLEFT", 8, -6) + warningMsg:SetPoint("RIGHT", -8, 0) + warningMsg:SetWordWrap(true) + warningMsg:SetText("Imported code will execute in your WoW client. Only import from trusted sources. We do not speak for the safety or completeness of the generated code. Please review it before using.") + warningMsg:SetTextColor(0.9, 0.75, 0.3, 1) + warningMsg:SetJustifyH("LEFT") + warningBanner:SetHeight(warningMsg:GetStringHeight() + 12) + + -- Default info panel anchor (below import area; re-anchored after decode) + local infoPanelDefaultY = yOff + -- Info panel (shown after decode) local infoPanel = CreateFrame("Frame", nil, frame) infoPanel:SetPoint("TOPLEFT", LEFT, yOff) @@ -179,15 +211,31 @@ local function GetDialog() infoName:SetTextColor(0.83, 0.83, 0.83, 1) infoName:SetText("") - local infoDetails = infoPanel:CreateFontString(nil, "OVERLAY") + -- Scrollable details area + local infoScroll = CreateFrame("ScrollFrame", nil, infoPanel) + infoScroll:SetPoint("TOPLEFT", infoName, "BOTTOMLEFT", 0, -2) + infoScroll:SetPoint("BOTTOMRIGHT", 0, 0) + infoScroll:EnableMouseWheel(true) + infoScroll:SetScript("OnMouseWheel", function(self, delta) + local current = self:GetVerticalScroll() + local child = self:GetScrollChild() + local maxScroll = math.max(0, (child and child:GetHeight() or 0) - self:GetHeight()) + self:SetVerticalScroll(math.max(0, math.min(current - delta * 20, maxScroll))) + end) + + local infoScrollChild = CreateFrame("Frame", nil, infoScroll) + local scrollContentWidth = 440 + infoScrollChild:SetWidth(scrollContentWidth) + infoScroll:SetScrollChild(infoScrollChild) + + local infoDetails = infoScrollChild:CreateFontString(nil, "OVERLAY") infoDetails:SetFontObject(DF.Theme:UIFont()) - infoDetails:SetPoint("TOPLEFT", infoName, "BOTTOMLEFT", 0, -2) - infoDetails:SetPoint("RIGHT", infoPanel, "RIGHT", -4, 0) + infoDetails:SetPoint("TOPLEFT", 0, 0) + infoDetails:SetWidth(scrollContentWidth - 4) infoDetails:SetTextColor(0.5, 0.5, 0.5, 1) infoDetails:SetText("") infoDetails:SetWordWrap(true) infoDetails:SetJustifyH("LEFT") - -- yOff adjusted dynamically when info panel shows -- Project name input (hidden until decode) local projectNameInput = CreateTextInput(frame, "Project:", "", 200, function(val) @@ -240,41 +288,117 @@ local function GetDialog() return clean end + local regionTypeNames = { + text = "Text", icon = "Icon", aurabar = "Aura Bar", + texture = "Texture", progresstexture = "Progress Texture", + model = "Model", group = "Group", dynamicgroup = "Dynamic Group", + stopmotion = "Stop Motion", + } + + local function DescribeTrigger(trig) + if trig.type == "aura2" then + local name = trig.auranames and trig.auranames[1] + local id = trig.auraspellids and trig.auraspellids[1] + if name then return "watches buff/debuff \"" .. name .. "\"" end + if id then return "watches buff/debuff (spell " .. tostring(id) .. ")" end + return "watches buffs/debuffs" + elseif trig.type == "status" then + local evt = trig.event or "" + if evt:find("Health") then return "tracks health" + elseif evt:find("Power") then return "tracks power/resource" + elseif evt:find("Cast") then return "tracks spell casts" + elseif evt:find("Cooldown") then return "tracks cooldowns" + elseif evt:find("Totem") then return "tracks totems" + elseif evt:find("Stance") or evt:find("Form") then return "tracks stance/form" + elseif evt:find("Range") then return "tracks range check" + elseif evt:find("Talent") then return "checks talents" + elseif evt:find("Combat") then return "tracks combat state" + elseif evt:find("Threat") then return "tracks threat" + elseif evt:find("Unit") then return "tracks unit info" + else return "tracks " .. evt end + elseif trig.type == "event" then + return "fires on game event" + elseif trig.type == "custom" then + if trig.custom_type == "stateupdate" then return "custom state manager" + elseif trig.custom_type == "event" then return "custom event handler" + else return "custom polled check" end + end + return trig.type or "unknown" + end + local function BuildInfoText(analysis) - local parts = {} + local lines = {} + local function add(s) lines[#lines + 1] = s end + -- Header + local name = analysis.groupId or "Unnamed" if analysis.isGroup then - parts[#parts + 1] = "Group: " .. (analysis.groupId or "Unknown") - parts[#parts + 1] = "Children: " .. #analysis.auras + add(name .. " (" .. #analysis.auras .. " auras)") else local a = analysis.auras[1] - if a then - parts[#parts + 1] = "Type: " .. (a.regionType or "unknown") - end + local rt = a and (regionTypeNames[a.regionType] or a.regionType) or "unknown" + add(name .. " (" .. rt .. ")") end + add("") + -- Per-aura summaries for i, aura in ipairs(analysis.auras) do - local trigDesc = {} + local rt = regionTypeNames[aura.regionType] or aura.regionType or "?" + local label = aura.id or ("Aura " .. i) + + -- Build a one-line description of what this aura does + local what = {} for _, trig in ipairs(aura.triggers or {}) do - local desc = trig.type or "unknown" - if trig.type == "aura2" then - if trig.auranames and #trig.auranames > 0 then - desc = "aura: " .. trig.auranames[1] - elseif trig.auraspellids and #trig.auraspellids > 0 then - desc = "aura ID: " .. tostring(trig.auraspellids[1]) - end - elseif trig.type == "status" then - desc = trig.event or "status" - end - trigDesc[#trigDesc + 1] = desc + what[#what + 1] = DescribeTrigger(trig) + end + + local desc = #what > 0 and table.concat(what, ", ") or "no triggers" + add("|cff88bbee" .. label .. "|r " .. rt .. " - " .. desc) + + -- Notable features + local features = {} + if aura.customText then features[#features + 1] = "dynamic text" end + if aura.initCode then features[#features + 1] = "init code" end + if aura.onShowCode then features[#features + 1] = "on-show action" end + if aura.onHideCode then features[#features + 1] = "on-hide action" end + if aura.conditions and #aura.conditions > 0 then + features[#features + 1] = #aura.conditions .. " condition(s)" + end + if #features > 0 then + add(" " .. table.concat(features, ", ")) + end + end + + -- Footer: config / options / load info + local footer = {} + local totalOpts = 0 + local hasConfig = false + for _, aura in ipairs(analysis.auras) do + if aura.authorOptions then totalOpts = totalOpts + #aura.authorOptions end + if aura.config then hasConfig = true end + end + if totalOpts > 0 then footer[#footer + 1] = totalOpts .. " user option(s)" end + if hasConfig and totalOpts == 0 then footer[#footer + 1] = "has config values" end + + -- Load conditions + local firstAura = analysis.auras[1] + if firstAura then + if firstAura.loadClass and #firstAura.loadClass > 0 then + footer[#footer + 1] = "class: " .. table.concat(firstAura.loadClass, "/") end - if #trigDesc > 0 then - local label = aura.id or ("Aura " .. i) - parts[#parts + 1] = label .. " triggers: " .. table.concat(trigDesc, ", ") + if firstAura.loadSpec and #firstAura.loadSpec > 0 then + local specs = {} + for _, s in ipairs(firstAura.loadSpec) do specs[#specs + 1] = tostring(s) end + footer[#footer + 1] = "spec: " .. table.concat(specs, "/") end end - return table.concat(parts, "\n") + if #footer > 0 then + add("") + add(table.concat(footer, " | ")) + end + + return table.concat(lines, "\n") end local function OnDecode() @@ -285,10 +409,16 @@ local function GetDialog() generatedFiles = nil errorText:SetText("") infoPanel:Hide() + warningBanner:Hide() projectNameInput.frame:Hide() fileSelectorRow:Hide() codePreview.frame:Hide() + -- Restore import area visibility (in case a previous decode hid it) + importLabel:Show() + importBox.frame:Show() + decodeBtn:Show() + local str = state.importString if not str or str:match("^%s*$") then errorText:SetText("Paste a WeakAuras export string above.") @@ -315,10 +445,27 @@ local function GetDialog() state.analysis = analysis + -- Hide import area after successful decode + importLabel:Hide() + importBox.frame:Hide() + decodeBtn:Hide() + errorText:SetText("") + + -- Show warning banner + warningBanner:Show() + + -- Re-anchor info panel below warning (reclaims import area space) + infoPanel:ClearAllPoints() + infoPanel:SetPoint("TOPLEFT", warningBanner, "BOTTOMLEFT", 0, -4) + infoPanel:SetPoint("RIGHT", frame, "RIGHT", RIGHT, 0) + infoPanel:SetHeight(120) + -- Populate info panel local auraName = analysis.groupId or (analysis.auras[1] and analysis.auras[1].id) or "WAImport" infoName:SetText(auraName) infoDetails:SetText(BuildInfoText(analysis)) + -- Update scroll child height to fit content + infoScrollChild:SetHeight(math.max(infoDetails:GetStringHeight() + 4, 1)) infoPanel:Show() -- Set project name @@ -394,8 +541,21 @@ local function GetDialog() state.currentFile = nil generatedFiles = nil + -- Restore import area + importLabel:Show() importBox:SetText("") + importBox.frame:Show() + decodeBtn:Show() + errorText:SetText("") + warningBanner:Hide() + + -- Reset info panel to default position + infoPanel:ClearAllPoints() + infoPanel:SetPoint("TOPLEFT", LEFT, infoPanelDefaultY) + infoPanel:SetPoint("RIGHT", frame, "RIGHT", RIGHT, 0) + infoPanel:SetHeight(50) + infoName:SetText("") infoDetails:SetText("") infoPanel:Hide() diff --git a/DevForge/UI/ActivityBar.lua b/DevForge/UI/ActivityBar.lua index c413ab7..041f742 100644 --- a/DevForge/UI/ActivityBar.lua +++ b/DevForge/UI/ActivityBar.lua @@ -62,6 +62,7 @@ function ActivityBar:Create(parent) lastGroup = def.group local btn = CreateFrame("Button", nil, frame) + btn:RegisterForClicks("LeftButtonUp") btn:SetSize(L.activityBarWidth, L.activityBtnHeight) btn:SetPoint("TOPLEFT", 0, yOffset) diff --git a/DevForge/UI/BottomPanel.lua b/DevForge/UI/BottomPanel.lua index 56dbd94..d957e70 100644 --- a/DevForge/UI/BottomPanel.lua +++ b/DevForge/UI/BottomPanel.lua @@ -38,6 +38,7 @@ function BottomPanel:Create(parent) -- Collapse toggle (left side of tab bar) local collapseBtn = CreateFrame("Button", nil, tabBar) + collapseBtn:RegisterForClicks("LeftButtonUp") collapseBtn:SetSize(20, L.bottomTabHeight) collapseBtn:SetPoint("LEFT", 2, 0) @@ -84,6 +85,7 @@ function BottomPanel:Create(parent) for i, def in ipairs(TAB_DEFS) do local btn = CreateFrame("Button", nil, tabBar, "BackdropTemplate") + btn:RegisterForClicks("LeftButtonUp") btn:SetSize(tabBtnWidth, L.bottomTabHeight) btn:SetPoint("LEFT", tabBar, "LEFT", tabXOffset + (i - 1) * (tabBtnWidth + 2), 0) btn:SetBackdrop({ @@ -232,6 +234,12 @@ function BottomPanel:Create(parent) eventsOutput:Clear() end) + DF.EventBus:On("DF_ERRORS_CLEARED", function() + errorCount = 0 + panel:SetBadge("errors", 0) + errorsOutput:Clear() + end) + --------------------------------------------------------------------------- -- Tab selection --------------------------------------------------------------------------- diff --git a/DevForge/UI/MainWindow.lua b/DevForge/UI/MainWindow.lua index c32b646..e6f5e6a 100644 --- a/DevForge/UI/MainWindow.lua +++ b/DevForge/UI/MainWindow.lua @@ -10,6 +10,19 @@ function MainWin:Create() local L = DF.Layout + -- After /reload the named frame survives with stale children from the + -- previous session. Disable the old frame so it cannot intercept mouse + -- events over the freshly-created widgets. + local stale = _G["DevForgeMainWindow"] + if stale then + stale:Hide() + stale:EnableMouse(false) + for _, child in pairs({stale:GetChildren()}) do + child:Hide() + child:EnableMouse(false) + end + end + local frame = CreateFrame("Frame", "DevForgeMainWindow", UIParent, "BackdropTemplate") frame:SetFrameStrata("HIGH") frame:SetClampedToScreen(true) @@ -75,6 +88,7 @@ function MainWin:Create() taintText:SetTextColor(0.6, 0.4, 0.2, 0.6) local infoBtn = CreateFrame("Button", nil, titleBar) + infoBtn:RegisterForClicks("LeftButtonUp") infoBtn:SetSize(32, 32) infoBtn:SetPoint("LEFT", taintText, "RIGHT", 3, 0) local infoBtnIcon = infoBtn:CreateTexture(nil, "OVERLAY") @@ -345,7 +359,14 @@ function MainWin:Create() --------------------------------------------------------------------------- -- Hide on Escape --------------------------------------------------------------------------- - table.insert(UISpecialFrames, "DevForgeMainWindow") + -- Avoid duplicate entries after /reload + local found = false + for _, name in ipairs(UISpecialFrames) do + if name == "DevForgeMainWindow" then found = true; break end + end + if not found then + table.insert(UISpecialFrames, "DevForgeMainWindow") + end -- Deactivate module on hide, reactivate on show frame:SetScript("OnHide", function() diff --git a/DevForge/UI/Sidebar.lua b/DevForge/UI/Sidebar.lua index 09938eb..dd20fc5 100644 --- a/DevForge/UI/Sidebar.lua +++ b/DevForge/UI/Sidebar.lua @@ -42,6 +42,7 @@ function Sidebar:Create(parent) -- Collapse toggle button local collapseBtn = CreateFrame("Button", nil, header) + collapseBtn:RegisterForClicks("LeftButtonUp") collapseBtn:SetSize(16, 16) collapseBtn:SetPoint("RIGHT", -4, 0) diff --git a/DevForge/UI/TabBar.lua b/DevForge/UI/TabBar.lua index 8ef7d2e..64cbe26 100644 --- a/DevForge/UI/TabBar.lua +++ b/DevForge/UI/TabBar.lua @@ -38,6 +38,7 @@ function TabBar:Create(parent) function bar:CreateTab(moduleName, label, xPos) local tab = CreateFrame("Button", nil, self.frame, "BackdropTemplate") + tab:RegisterForClicks("LeftButtonUp") tab:SetSize(DF.Layout.tabWidth, DF.Layout.tabHeight) tab:SetBackdrop({ bgFile = "Interface\\ChatFrame\\ChatFrameBackground", diff --git a/DevForge/UI/Widgets/Button.lua b/DevForge/UI/Widgets/Button.lua index 18a5850..304d023 100644 --- a/DevForge/UI/Widgets/Button.lua +++ b/DevForge/UI/Widgets/Button.lua @@ -7,6 +7,7 @@ function DF.Widgets:CreateButton(parent, text, width, height) height = height or DF.Layout.buttonHeight local btn = CreateFrame("Button", nil, parent, "BackdropTemplate") + btn:RegisterForClicks("LeftButtonUp") btn:SetSize(width, height) btn:SetBackdrop({ bgFile = "Interface\\ChatFrame\\ChatFrameBackground", diff --git a/DevForge/UI/Widgets/CopyDialog.lua b/DevForge/UI/Widgets/CopyDialog.lua index 1264cda..188b9e7 100644 --- a/DevForge/UI/Widgets/CopyDialog.lua +++ b/DevForge/UI/Widgets/CopyDialog.lua @@ -9,6 +9,13 @@ local dialog = nil local function GetDialog() if dialog then return dialog end + -- Clean up stale named frame from previous /reload + local stale = _G["DevForgeCopyDialog"] + if stale then + stale:Hide(); stale:EnableMouse(false) + for _, c in pairs({stale:GetChildren()}) do c:Hide(); c:EnableMouse(false) end + end + local frame = CreateFrame("Frame", "DevForgeCopyDialog", UIParent, "BackdropTemplate") frame:SetFrameStrata("FULLSCREEN_DIALOG") frame:SetFrameLevel(999) diff --git a/DevForge/UI/Widgets/DropDown.lua b/DevForge/UI/Widgets/DropDown.lua index 0e0b6f3..e0df096 100644 --- a/DevForge/UI/Widgets/DropDown.lua +++ b/DevForge/UI/Widgets/DropDown.lua @@ -37,6 +37,7 @@ function DF.Widgets:CreateDropDown() -- Close on outside click local blocker = CreateFrame("Button", nil, UIParent) + blocker:RegisterForClicks("LeftButtonUp") blocker:SetAllPoints(UIParent) blocker:SetFrameStrata("FULLSCREEN") blocker:Hide() @@ -51,6 +52,7 @@ function DF.Widgets:CreateDropDown() end local row = CreateFrame("Button", nil, frame) + row:RegisterForClicks("LeftButtonUp") row:SetHeight(ROW_HEIGHT) local hl = row:CreateTexture(nil, "HIGHLIGHT") @@ -159,6 +161,12 @@ function DF.Widgets:CreateDropDown() frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cursorX / scale, cursorY / scale) end + -- Raise above the anchor's parent so the menu isn't occluded by sibling widgets + if anchor then + local level = anchor:GetFrameLevel() + 10 + frame:SetFrameLevel(level) + end + blocker:Show() blocker:SetFrameLevel(frame:GetFrameLevel() - 1) frame:Show() diff --git a/DevForge/UI/Widgets/SearchBox.lua b/DevForge/UI/Widgets/SearchBox.lua index c06c862..6bf953e 100644 --- a/DevForge/UI/Widgets/SearchBox.lua +++ b/DevForge/UI/Widgets/SearchBox.lua @@ -28,6 +28,7 @@ function DF.Widgets:CreateSearchBox(parent, placeholder, height) -- Clear button local clearBtn = CreateFrame("Button", nil, frame) + clearBtn:RegisterForClicks("LeftButtonUp") clearBtn:SetSize(14, 14) clearBtn:SetPoint("RIGHT", -4, 0) clearBtn:Hide() From 16e92a45c633aa467d48f6f9b02c8e7a8d065d11 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Sun, 8 Feb 2026 12:23:42 -0700 Subject: [PATCH 4/4] resync --- DevForge/DevForge.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DevForge/DevForge.toc b/DevForge/DevForge.toc index ef6a33f..c03dcac 100644 --- a/DevForge/DevForge.toc +++ b/DevForge/DevForge.toc @@ -3,7 +3,7 @@ ## Notes: In-game dev toolkit: inspector, editor, console, events, errors, APIs, textures, performance ## Author: hatdragon ## Contributors: -## Version: r1.0.9 +## Version: r1.0.10 ## SavedVariables: DevForgeDB ## IconTexture: Interface\Icons\INV_Gizmo_02