From b99b11bce3cd1c30fed0f0b006d4d45b5c7d7554 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Mon, 30 Mar 2026 12:29:59 -0600 Subject: [PATCH 1/4] Add Classic Era and progressive Classic support Single codebase now supports Retail (120000/120001), Mists Classic (50503), Cata Classic (40402), and Classic Era (11508) via multi-TOC and a compat shim layer. - Add Core/Compat.lua: flavor detection flags (IsRetail/IsClassic/IsClassicEra), C_AddOns polyfill for legacy addon API, HelpTip stub for Classic clients - Add DevForge_Vanilla.toc for Classic Era (excludes WAImporter files) - Update main TOC with Mists/Cata interface versions - ActivityBar: add fileID fallbacks for all module icons (atlas names are Retail-only; Classic clients fall back to classic-range icon textures) - SnippetEditor: hide WA Import button on non-Retail, move Duplicate into the New dropdown to reduce toolbar crowding - AddonScaffold: generate flavor-appropriate interface version in scaffolds Co-Authored-By: Claude Opus 4.6 (1M context) --- DevForge/Core/Compat.lua | 35 ++ DevForge/Core/Constants.lua | 6 +- DevForge/DevForge.toc | 25 +- DevForge/DevForge_Vanilla.toc | 131 +++++ .../Modules/SnippetEditor/AddonScaffold.lua | 14 +- .../Modules/SnippetEditor/SnippetEditor.lua | 471 ++++++++++++++---- DevForge/UI/ActivityBar.lua | 72 +-- 7 files changed, 628 insertions(+), 126 deletions(-) create mode 100644 DevForge/Core/Compat.lua create mode 100644 DevForge/DevForge_Vanilla.toc diff --git a/DevForge/Core/Compat.lua b/DevForge/Core/Compat.lua new file mode 100644 index 0000000..c6508a9 --- /dev/null +++ b/DevForge/Core/Compat.lua @@ -0,0 +1,35 @@ +local _, DF = ... + +-- ============================================================================ +-- Flavor Detection +-- ============================================================================ +DF.IsRetail = (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE) +DF.IsClassicEra = (WOW_PROJECT_ID == WOW_PROJECT_CLASSIC) +DF.IsClassic = not DF.IsRetail + +-- ============================================================================ +-- C_AddOns polyfill (Retail 11.0+ namespace; Classic has legacy globals) +-- ============================================================================ +if not C_AddOns then + C_AddOns = { + GetNumAddOns = GetNumAddOns, + GetAddOnInfo = GetAddOnInfo, + IsAddOnLoaded = IsAddOnLoaded, + LoadAddOn = LoadAddOn, + GetAddOnMetadata = GetAddOnMetadata, + } +end + +-- ============================================================================ +-- HelpTip stub (Retail-only tutorial tooltip API; used in MainWindow.lua) +-- ============================================================================ +if not HelpTip then + HelpTip = { + ButtonStyle = { GotIt = 1 }, + Point = { BottomEdgeCenter = 1 }, + Alignment = { Center = 1 }, + Show = function() end, + Hide = function() end, + IsShowing = function() return false end, + } +end diff --git a/DevForge/Core/Constants.lua b/DevForge/Core/Constants.lua index f2ee1c2..a05be3e 100644 --- a/DevForge/Core/Constants.lua +++ b/DevForge/Core/Constants.lua @@ -90,8 +90,12 @@ DF.Layout = { bottomCollapseH = 20, } +DF.IsRetail = DF.IsRetail ~= nil and DF.IsRetail or (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE) +DF.IsClassicEra = DF.IsClassicEra ~= nil and DF.IsClassicEra or (WOW_PROJECT_ID == WOW_PROJECT_CLASSIC) +DF.IsClassic = DF.IsClassic ~= nil and DF.IsClassic or not DF.IsRetail + DF.ADDON_NAME = "DevForge" -DF.ADDON_VERSION = "1.0.0" +DF.ADDON_VERSION = C_AddOns.GetAddOnMetadata("DevForge", "Version") or "unknown" DF.MAX_HISTORY = 200 DF.PRETTY_DEPTH = 3 DF.DEBOUNCE_MS = 200 diff --git a/DevForge/DevForge.toc b/DevForge/DevForge.toc index 9fa08b4..4fa3105 100644 --- a/DevForge/DevForge.toc +++ b/DevForge/DevForge.toc @@ -1,10 +1,14 @@ -## Interface: 120000 +## Interface: 120000, 120001, 50503, 40402 ## Title: DevForge -## Notes: In-game Lua IDE - Console, Inspector, API Browser, Editor, Events, Errors, Performance, Macros, Textures -## Author: DevForge -## Version: 1.0.0 +## Notes: In-game dev toolkit: inspector, editor, console, events, errors, APIs, textures, sounds, performance +## Author: hatdragon +## Contributors: +## Version: r1.0.13 + ## SavedVariables: DevForgeDB ## IconTexture: Interface\Icons\INV_Gizmo_02 +## X-BugGrabber-Display: DevForge + ## Category: Development Tools ## Category-deDE: Entwicklungstools ## Category-esES: Herramientas de Desarrollo @@ -18,12 +22,18 @@ ## Category-zhTW: 開發工具 ## X-Category: Development Tools +## X-Curse-Project-ID: 1453381 +## X-Wago-ID: BKpgXv6E +## X-Website: https://hatdragon.github.io/DevForge/ +## X-Source: https://github.com/hatdragon/DevForge + # Libraries Libs\LibStub\LibStub.lua Libs\LibDeflate\LibDeflate.lua Libs\LibSerialize\LibSerialize.lua # Core +Core\Compat.lua Core\Constants.lua Core\Init.lua Core\SecretGuard.lua @@ -125,3 +135,10 @@ Modules\TextureBrowser\TextureIconData.lua Modules\TextureBrowser\TextureRuntime.lua Modules\TextureBrowser\TextureIndex.lua Modules\TextureBrowser\TextureBrowser.lua + +# Sound Browser Module +Modules\SoundBrowser\SoundKitData.lua +Modules\SoundBrowser\SoundFileData.lua +Modules\SoundBrowser\SoundRuntime.lua +Modules\SoundBrowser\SoundIndex.lua +Modules\SoundBrowser\SoundBrowser.lua diff --git a/DevForge/DevForge_Vanilla.toc b/DevForge/DevForge_Vanilla.toc new file mode 100644 index 0000000..030eb5d --- /dev/null +++ b/DevForge/DevForge_Vanilla.toc @@ -0,0 +1,131 @@ +## Interface: 11508 +## Title: DevForge +## Notes: In-game dev toolkit: inspector, editor, console, events, errors, APIs, textures, sounds, performance +## Author: hatdragon +## Contributors: +## Version: r1.0.13 + +## SavedVariables: DevForgeDB +## IconTexture: Interface\Icons\INV_Gizmo_02 +## X-BugGrabber-Display: DevForge + +## Category: Development Tools +## X-Category: Development Tools + +## X-Curse-Project-ID: 1453381 +## X-Wago-ID: BKpgXv6E +## X-Website: https://hatdragon.github.io/DevForge/ +## X-Source: https://github.com/hatdragon/DevForge + +# Libraries +Libs\LibStub\LibStub.lua +Libs\LibDeflate\LibDeflate.lua +Libs\LibSerialize\LibSerialize.lua + +# Core +Core\Compat.lua +Core\Constants.lua +Core\Init.lua +Core\SecretGuard.lua +Core\Util.lua +Core\EventBus.lua +Core\ModuleSystem.lua + +# SavedVariables +SavedVariables\Schema.lua + +# UI Framework +UI\Theme.lua +UI\Widgets\Button.lua +UI\Widgets\ScrollPane.lua +UI\Widgets\SearchBox.lua +UI\Widgets\CodeEditBox.lua +UI\Widgets\TreeView.lua +UI\Widgets\PropertyGrid.lua +UI\Widgets\SplitPane.lua +UI\Widgets\DropDown.lua +UI\Widgets\CopyDialog.lua +UI\ActivityBar.lua +UI\Sidebar.lua +UI\BottomPanel.lua +UI\MainWindow.lua + +# Cross-module Integration +Core\IntegrationBus.lua + +# Error Handler Module +Modules\ErrorHandler\ErrorHandler.lua +Modules\ErrorHandler\ErrorList.lua +Modules\ErrorHandler\ErrorDetail.lua +Modules\ErrorHandler\ErrorMonitor.lua + +# Console Module +Modules\Console\ConsoleHistory.lua +Modules\Console\ConsoleExec.lua +Modules\Console\ConsoleOutput.lua +Modules\Console\ConsoleInput.lua +Modules\Console\Console.lua + +# Inspector Module +Modules\Inspector\InspectorHighlight.lua +Modules\Inspector\InspectorPicker.lua +Modules\Inspector\InspectorTree.lua +Modules\Inspector\InspectorProps.lua +Modules\Inspector\InspectorGrid.lua +Modules\Inspector\Inspector.lua + +# API Browser Module +Modules\APIBrowser\APIBrowserData.lua +Modules\APIBrowser\APIBrowserSearch.lua +Modules\APIBrowser\APIBrowserList.lua +Modules\APIBrowser\APIBrowserDetail.lua +Modules\APIBrowser\APIBrowser.lua + +# Table Viewer Module +Modules\TableViewer\TableDump.lua +Modules\TableViewer\TableViewer.lua + +# CVar Viewer Module +Modules\CVarViewer\CVarData.lua +Modules\CVarViewer\CVarViewer.lua + +# Snippet Editor Module +Modules\SnippetEditor\SnippetStore.lua +Modules\SnippetEditor\SnippetList.lua +Modules\SnippetEditor\TemplateData.lua +Modules\SnippetEditor\TemplateBrowser.lua +Modules\SnippetEditor\FrameBuilder.lua +Modules\SnippetEditor\AddonScaffold.lua +Modules\SnippetEditor\SnippetEditor.lua + +# WA Importer Module (Retail only, omitted on Classic Era) + +# Event Monitor Module +Modules\EventMonitor\EventMonitorLog.lua +Modules\EventMonitor\EventIndex.lua +Modules\EventMonitor\EventMonitorFilter.lua +Modules\EventMonitor\EventMonitor.lua + +# Performance Module +Modules\Performance\PerfData.lua +Modules\Performance\PerfTable.lua +Modules\Performance\PerfMonitor.lua + +# Macro Editor Module +Modules\MacroEditor\MacroStore.lua +Modules\MacroEditor\MacroList.lua +Modules\MacroEditor\MacroEditor.lua + +# Texture Browser Module +Modules\TextureBrowser\TextureAtlasData.lua +Modules\TextureBrowser\TextureIconData.lua +Modules\TextureBrowser\TextureRuntime.lua +Modules\TextureBrowser\TextureIndex.lua +Modules\TextureBrowser\TextureBrowser.lua + +# Sound Browser Module +Modules\SoundBrowser\SoundKitData.lua +Modules\SoundBrowser\SoundFileData.lua +Modules\SoundBrowser\SoundRuntime.lua +Modules\SoundBrowser\SoundIndex.lua +Modules\SoundBrowser\SoundBrowser.lua diff --git a/DevForge/Modules/SnippetEditor/AddonScaffold.lua b/DevForge/Modules/SnippetEditor/AddonScaffold.lua index 3caf396..0e4da09 100644 --- a/DevForge/Modules/SnippetEditor/AddonScaffold.lua +++ b/DevForge/Modules/SnippetEditor/AddonScaffold.lua @@ -110,7 +110,8 @@ local function GenerateTOC(state) local function add(s) lines[#lines + 1] = s end local name = state.addonName ~= "" and state.addonName or "MyAddon" - add("## Interface: 120000") + local interfaceVersion = DF.IsRetail and "120000, 120001" or DF.IsClassicEra and "11508" or "50503" + add("## Interface: " .. interfaceVersion) add("## Title: " .. name) add("## Notes: " .. (state.description ~= "" and state.description or name)) add("## Author: " .. (state.author ~= "" and state.author or "Unknown")) @@ -239,6 +240,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 +256,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/SnippetEditor.lua b/DevForge/Modules/SnippetEditor/SnippetEditor.lua index da23f1e..46dd37f 100644 --- a/DevForge/Modules/SnippetEditor/SnippetEditor.lua +++ b/DevForge/Modules/SnippetEditor/SnippetEditor.lua @@ -9,6 +9,7 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local editor = {} local currentSnippetId = nil + local ranProjects = {} -- [projectId] = true for projects that have been run --------------------------------------------------------------------------- -- Sidebar: toggle bar + snippet list + template browser @@ -26,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", @@ -75,7 +77,79 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) snippetsContainer:SetPoint("BOTTOMRIGHT", 0, 0) local snippetList = DF.SnippetList:Create(snippetsContainer) - snippetList.frame:SetAllPoints(snippetsContainer) + snippetList.frame:SetPoint("TOPLEFT", snippetsContainer, "TOPLEFT", 0, 0) + snippetList.frame:SetPoint("BOTTOMRIGHT", snippetsContainer, "BOTTOMRIGHT", 0, 0) + + -- Running-projects panel (bottom of sidebar, hidden until first run) + local runPanel = CreateFrame("Frame", nil, snippetsContainer, "BackdropTemplate") + runPanel:SetPoint("BOTTOMLEFT", 0, 0) + runPanel:SetPoint("BOTTOMRIGHT", 0, 0) + runPanel:Hide() + local runPanelBg = runPanel:CreateTexture(nil, "BACKGROUND") + runPanelBg:SetAllPoints() + runPanelBg:SetColorTexture(0.15, 0.15, 0.18, 1) + + local runPanelSep = runPanel:CreateTexture(nil, "OVERLAY") + runPanelSep:SetHeight(1) + runPanelSep:SetPoint("TOPLEFT", 0, 0) + runPanelSep:SetPoint("TOPRIGHT", 0, 0) + runPanelSep:SetColorTexture(0.3, 0.3, 0.3, 0.5) + + local runPanelHeader = runPanel:CreateFontString(nil, "OVERLAY") + runPanelHeader:SetFontObject(DF.Theme:UIFont()) + runPanelHeader:SetPoint("TOPLEFT", 6, -5) + runPanelHeader:SetTextColor(0.9, 0.75, 0.3, 1) + runPanelHeader:SetText("Running") + + local runPanelRows = {} + + local runPanelReload = DF.Widgets:CreateButton(runPanel, "Reload UI", 65) + runPanelReload:SetPoint("BOTTOMLEFT", 6, 6) + runPanelReload:SetScript("OnClick", function() ReloadUI() end) + + local function RefreshRunPanel() + -- Collect running project names + local entries = {} + for projectId in pairs(ranProjects) do + local proj = DF.SnippetStore:Get(projectId) + if proj then + entries[#entries + 1] = proj.name or "Untitled" + end + end + table.sort(entries) + + if #entries == 0 then + runPanel:Hide() + snippetList.frame:SetPoint("BOTTOMRIGHT", snippetsContainer, "BOTTOMRIGHT", 0, 0) + return + end + + -- Create/update row labels + for i, name in ipairs(entries) do + if not runPanelRows[i] then + local row = runPanel:CreateFontString(nil, "OVERLAY") + row:SetFontObject(DF.Theme:UIFont()) + row:SetJustifyH("LEFT") + row:SetTextColor(0.7, 0.7, 0.7, 1) + runPanelRows[i] = row + end + runPanelRows[i]:SetText(" " .. name) + runPanelRows[i]:ClearAllPoints() + runPanelRows[i]:SetPoint("TOPLEFT", 6, -5 - (i * 16)) + runPanelRows[i]:SetPoint("RIGHT", -6, 0) + runPanelRows[i]:Show() + end + -- Hide extra rows + for i = #entries + 1, #runPanelRows do + runPanelRows[i]:Hide() + end + + -- header + rows + button + padding + local panelHeight = 10 + (#entries + 1) * 16 + 28 + runPanel:SetHeight(panelHeight) + runPanel:Show() + snippetList.frame:SetPoint("BOTTOMRIGHT", snippetsContainer, "BOTTOMRIGHT", 0, panelHeight) + end -- Templates container local templatesContainer = CreateFrame("Frame", nil, sidebarFrame) @@ -123,24 +197,24 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local newBtn = DF.Widgets:CreateButton(toolbar, "+ New", 60) newBtn:SetPoint("LEFT", 2, 0) - local dupBtn = DF.Widgets:CreateButton(toolbar, "Duplicate", 75) - dupBtn:SetPoint("LEFT", newBtn, "RIGHT", 4, 0) - local delBtn = DF.Widgets:CreateButton(toolbar, "Delete", 60) - delBtn:SetPoint("LEFT", dupBtn, "RIGHT", 4, 0) + delBtn:SetPoint("LEFT", newBtn, "RIGHT", 4, 0) -- Frame Builder and Scaffold buttons between Delete and Run - local frameBuilderBtn = DF.Widgets:CreateButton(toolbar, "Frame Builder", 95) + local frameBuilderBtn = DF.Widgets:CreateButton(toolbar, "Frames", 60) frameBuilderBtn:SetPoint("LEFT", delBtn, "RIGHT", 4, 0) - local scaffoldBtn = DF.Widgets:CreateButton(toolbar, "Scaffold", 70) + local scaffoldBtn = DF.Widgets:CreateButton(toolbar, "Scaffold", 65) scaffoldBtn:SetPoint("LEFT", frameBuilderBtn, "RIGHT", 4, 0) - local waImportBtn = DF.Widgets:CreateButton(toolbar, "WA Import", 80) - waImportBtn:SetPoint("LEFT", scaffoldBtn, "RIGHT", 4, 0) + local waImportBtn + if DF.IsRetail then + waImportBtn = DF.Widgets:CreateButton(toolbar, "WA Import", 80) + waImportBtn:SetPoint("LEFT", scaffoldBtn, "RIGHT", 4, 0) + end local runProjectBtn = DF.Widgets:CreateButton(toolbar, "Run Project", 80) - runProjectBtn:SetPoint("LEFT", waImportBtn, "RIGHT", 4, 0) + runProjectBtn:SetPoint("LEFT", waImportBtn or scaffoldBtn, "RIGHT", 4, 0) local saveBtn = DF.Widgets:CreateButton(toolbar, "Save", 55) saveBtn:SetPoint("RIGHT", -2, 0) @@ -158,12 +232,43 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) emptyState:SetText("Create a snippet to get started.") emptyState:SetTextColor(0.5, 0.5, 0.5, 1) + -- Run-project warning banner + local runBanner = CreateFrame("Frame", nil, editorFrame, "BackdropTemplate") + runBanner:SetHeight(20) + runBanner:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -2) + runBanner:SetPoint("TOPRIGHT", toolbar, "BOTTOMRIGHT", 0, -2) + runBanner:Hide() + local runBannerBg = runBanner:CreateTexture(nil, "BACKGROUND") + runBannerBg:SetAllPoints() + runBannerBg:SetColorTexture(0.35, 0.25, 0.05, 0.6) + local runBannerReload = DF.Widgets:CreateButton(runBanner, "Reload UI", 65) + runBannerReload:SetPoint("RIGHT", -4, 0) + runBannerReload:SetScript("OnClick", function() ReloadUI() end) + + local runBannerText = runBanner:CreateFontString(nil, "OVERLAY") + runBannerText:SetFontObject(DF.Theme:UIFont()) + runBannerText:SetPoint("LEFT", 6, 0) + runBannerText:SetPoint("RIGHT", runBannerReload, "LEFT", -6, 0) + runBannerText:SetJustifyH("LEFT") + runBannerText:SetTextColor(0.9, 0.75, 0.3, 1) + runBannerText:SetText("Re-running won't unload previous frames or state.") + -- Editor content (hidden when no snippet selected) local editorContent = CreateFrame("Frame", nil, editorFrame) - editorContent:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -2) editorContent:SetPoint("BOTTOMRIGHT", 0, 0) editorContent:Hide() + local function UpdateEditorContentAnchor() + editorContent:ClearAllPoints() + if runBanner:IsShown() then + editorContent:SetPoint("TOPLEFT", runBanner, "BOTTOMLEFT", 0, -2) + else + editorContent:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -2) + end + editorContent:SetPoint("BOTTOMRIGHT", 0, 0) + end + UpdateEditorContentAnchor() + -- Name input row local nameRow = CreateFrame("Frame", nil, editorContent) nameRow:SetHeight(24) @@ -209,6 +314,8 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local function LoadSnippet(id) if not id then + runBanner:Hide() + UpdateEditorContentAnchor() editorContent:Hide() emptyState:Show() currentSnippetId = nil @@ -217,12 +324,23 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local snippet = DF.SnippetStore:Get(id) if not snippet then + runBanner:Hide() + UpdateEditorContentAnchor() editorContent:Hide() emptyState:Show() currentSnippetId = nil return end + -- Show banner if this snippet belongs to any previously-run project + local snippetProjectId = snippet.isProject and snippet.id or snippet.parentId + if snippetProjectId and ranProjects[snippetProjectId] then + runBanner:Show() + else + runBanner:Hide() + end + UpdateEditorContentAnchor() + emptyState:Hide() editorContent:Show() @@ -285,20 +403,130 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) --------------------------------------------------------------------------- -- Toolbar button handlers --------------------------------------------------------------------------- + local function CreateNew(parentId) + SaveCurrent() + local snippet = DF.SnippetStore:Create("Untitled", parentId) + snippetList:Refresh() + LoadSnippet(snippet.id) + nameInput:SetFocus() + nameInput:HighlightText() + end + snippetList:SetOnSelect(function(id) SaveCurrent() LoadSnippet(id) end) + local contextMenu = DF.Widgets:CreateDropDown() + + snippetList:SetOnRightClick(function(snippetId, isProjectRow) + local snippet = DF.SnippetStore:Get(snippetId) + if not snippet then return end + + local items = {} + if snippet.isProject then + items[#items + 1] = { + text = "New file in " .. (snippet.name or "project"), + func = function() + snippetList:SetExpanded(snippetId, true) + CreateNew(snippetId) + end, + } + local childCount = #DF.SnippetStore:GetChildren(snippetId) + local label = "Delete project" + if childCount > 0 then + label = label .. " (" .. childCount .. " files)" + end + items[#items + 1] = { + text = label, + func = function() + DF.SnippetStore:Delete(snippetId) + if currentSnippetId then + local cur = DF.SnippetStore:Get(currentSnippetId) + if not cur then + currentSnippetId = nil + SelectNext() + end + end + snippetList:Refresh() + end, + } + else + items[#items + 1] = { + text = "Delete snippet", + func = function() + if currentSnippetId == snippetId then + currentSnippetId = nil + end + DF.SnippetStore:Delete(snippetId) + snippetList:Refresh() + if not currentSnippetId then + SelectNext() + end + end, + } + end + + contextMenu:Show(nil, items) + end) + local newMenu = DF.Widgets:CreateDropDown() - local function CreateNew(parentId) + -- Lightweight project-name prompt (avoids StaticPopup quirks) + local projectPrompt = CreateFrame("Frame", nil, UIParent, "BackdropTemplate") + projectPrompt:SetFrameStrata("FULLSCREEN_DIALOG") + projectPrompt:SetSize(280, 80) + projectPrompt:SetPoint("CENTER") + projectPrompt:SetClampedToScreen(true) + projectPrompt:EnableMouse(true) + projectPrompt:Hide() + DF.Theme:ApplyDialogChrome(projectPrompt) + + local ppLabel = projectPrompt:CreateFontString(nil, "OVERLAY") + ppLabel:SetFontObject(DF.Theme:UIFont()) + ppLabel:SetPoint("TOPLEFT", 10, -10) + ppLabel:SetText("Project name:") + ppLabel:SetTextColor(0.65, 0.65, 0.65, 1) + + local ppInput = CreateFrame("EditBox", nil, projectPrompt, "BackdropTemplate") + ppInput:SetPoint("TOPLEFT", 10, -28) + ppInput:SetPoint("RIGHT", -10, 0) + ppInput:SetHeight(22) + ppInput:SetAutoFocus(false) + ppInput:SetFontObject(DF.Theme:UIFont()) + ppInput:SetTextColor(0.83, 0.83, 0.83, 1) + ppInput:SetMaxLetters(100) + DF.Theme:ApplyInputStyle(ppInput) + + local ppCreate = DF.Widgets:CreateButton(projectPrompt, "Create", 60) + ppCreate:SetPoint("BOTTOMRIGHT", -10, 8) + + local ppCancel = DF.Widgets:CreateButton(projectPrompt, "Cancel", 60) + ppCancel:SetPoint("RIGHT", ppCreate, "LEFT", -4, 0) + + local function FinishProjectPrompt() + local name = ppInput:GetText() + if not name or name == "" then name = "Untitled Project" end + projectPrompt:Hide() SaveCurrent() - local snippet = DF.SnippetStore:Create("Untitled", parentId) + local project = DF.SnippetStore:CreateProject(name) snippetList:Refresh() - LoadSnippet(snippet.id) - nameInput:SetFocus() - nameInput:HighlightText() + snippetList:SetExpanded(project.id, true) + end + + ppCreate:SetScript("OnClick", FinishProjectPrompt) + ppCancel:SetScript("OnClick", function() projectPrompt:Hide() end) + ppInput:SetScript("OnEnterPressed", FinishProjectPrompt) + ppInput:SetScript("OnEscapePressed", function() projectPrompt:Hide() end) + + projectPrompt:SetScript("OnShow", function() + ppInput:SetText("Untitled Project") + ppInput:HighlightText() + ppInput:SetFocus() + end) + + local function CreateNewProject() + projectPrompt:Show() end newBtn:SetScript("OnClick", function(self) @@ -320,25 +548,26 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) end end - if not projectId then - CreateNew(nil) - return + local items = {} + if projectId then + items[#items + 1] = { text = "New in " .. projectName, func = function() CreateNew(projectId) end } + items[#items + 1] = { text = "New standalone snippet", func = function() CreateNew(nil) end } + else + items[#items + 1] = { text = "New snippet", func = function() CreateNew(nil) end } end - - newMenu:Show(self, { - { text = "New in " .. projectName, func = function() CreateNew(projectId) end }, - { text = "New standalone snippet", func = function() CreateNew(nil) end }, - }) - end) - - dupBtn:SetScript("OnClick", function() - if not currentSnippetId then return end - SaveCurrent() - local clone = DF.SnippetStore:Duplicate(currentSnippetId) - if clone then - snippetList:Refresh() - LoadSnippet(clone.id) + items[#items + 1] = { text = "New empty project", func = CreateNewProject } + if currentSnippetId then + items[#items + 1] = { text = "Duplicate current", func = function() + SaveCurrent() + local clone = DF.SnippetStore:Duplicate(currentSnippetId) + if clone then + snippetList:Refresh() + LoadSnippet(clone.id) + end + end } end + + newMenu:Show(self, items) end) delBtn:SetScript("OnClick", function() @@ -374,22 +603,24 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) end) end) - waImportBtn:SetScript("OnClick", function() - DF.WAImporter:Show(function(files, projectName) - SaveCurrent() - local project = DF.SnippetStore:CreateProject(projectName) - for _, file in ipairs(files) do - local s = DF.SnippetStore:Create(file.name, project.id) - DF.SnippetStore:Save(s.id, s.name, file.code) - end - SetSidebarTab("snippets") - snippetList:Refresh() - local children = DF.SnippetStore:GetChildren(project.id) - if #children > 0 then - LoadSnippet(children[1].id) - end + if waImportBtn then + waImportBtn:SetScript("OnClick", function() + DF.WAImporter:Show(function(files, projectName) + SaveCurrent() + local project = DF.SnippetStore:CreateProject(projectName) + for _, file in ipairs(files) do + local s = DF.SnippetStore:Create(file.name, project.id) + DF.SnippetStore:Save(s.id, s.name, file.code) + end + SetSidebarTab("snippets") + snippetList:Refresh() + local children = DF.SnippetStore:GetChildren(project.id) + if #children > 0 then + LoadSnippet(children[1].id) + end + end) end) - end) + end -- Parse a .toc file's content and return ordered list of .lua filenames local function ParseTocLoadOrder(tocCode) @@ -410,6 +641,13 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) if not DF.ConsoleExec then return end SaveCurrent() + -- Ensure bottom panel is visible and on the Output tab + local bp = DF.bottomPanel + if bp then + if bp.collapsed then bp:Expand() end + bp:SelectTab("output") + end + -- Determine project context local snippet = DF.SnippetStore:Get(currentSnippetId) if not snippet or not snippet.parentId then return end @@ -457,17 +695,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 @@ -504,17 +767,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(...) @@ -525,10 +792,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 @@ -541,43 +812,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 @@ -588,6 +876,11 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) color = DF.Colors.func, }) DF.EventBus:Fire("DF_OUTPUT_LINE", { text = "" }) + + ranProjects[project.id] = true + runBanner:Show() + UpdateEditorContentAnchor() + RefreshRunPanel() end) saveBtn:SetScript("OnClick", function() @@ -610,10 +903,10 @@ DF.ModuleSystem:Register("SnippetEditor", function(sidebarParent, editorParent) local code = codeEditor:GetText() if not code or code == "" then return end - if not DF.ConsoleExec then return end - - -- Execute and send output to bottom panel - DF.EventBus:Fire("DF_EXECUTE_CODE", { code = code, source = "SnippetEditor" }) + DF.EventBus:Fire("DF_EXECUTE_CODE", { + code = code, + source = "snippet", + }) end) --------------------------------------------------------------------------- diff --git a/DevForge/UI/ActivityBar.lua b/DevForge/UI/ActivityBar.lua index c413ab7..edc61e0 100644 --- a/DevForge/UI/ActivityBar.lua +++ b/DevForge/UI/ActivityBar.lua @@ -10,20 +10,21 @@ local ActivityBar = DF.UI.ActivityBar -- fileID entries use desaturation + vertex color dimming local MODULE_ICONS = { -- Code: writing & running - { name = "Console", atlas = "Crosshair_repairnpc_32", group = "code" }, - { name = "SnippetEditor", atlas = "Crosshair_Repair_32", group = "code" }, - { name = "MacroEditor", fileID = 136377, group = "code" }, + { name = "Console", atlas = "Crosshair_repairnpc_32", fileID = 134327, group = "code" }, -- INV_Scroll_02 (scroll/REPL) + { name = "SnippetEditor", atlas = "Crosshair_Repair_32", fileID = 133741, group = "code" }, -- INV_Misc_Book_09 (book/code storage) + { name = "MacroEditor", fileID = 136377, group = "code" }, -- Ability_Marksmanship -- Inspect: looking at things - { name = "Inspector", atlas = "Crosshair_Inspect_32", group = "inspect" }, - { name = "TableViewer", atlasOn = "common-icon-visual", atlasOff = "common-icon-visual-disabled", group = "inspect" }, - { name = "TextureBrowser", atlas = "Crosshair_Transmogrify_32", group = "inspect" }, + { name = "Inspector", atlas = "Crosshair_Inspect_32", fileID = 134442, group = "inspect" }, -- INV_Misc_Spyglass_03 (magnifier) + { name = "TableViewer", atlasOn = "common-icon-visual", atlasOff = "common-icon-visual-disabled", fileID = 134332, group = "inspect" }, -- INV_Scroll_07 (data listing) + { name = "TextureBrowser", atlas = "Crosshair_Transmogrify_32", fileID = 132319, group = "inspect" }, -- Ability_Spy (eye/viewing) + { name = "SoundBrowser", atlas = "common-icon-sound-pressed", fileID = 133706, group = "inspect" }, -- INV_Misc_Bell_01 (bell/sound) -- Reference: browsing data - { name = "APIBrowser", atlas = "crosshair_speak_32", group = "reference" }, - { name = "CVarViewer", atlas = "Adventure-Mission-Silver-Dragon", group = "reference" }, - { name = "EventMonitor", atlas = "Crosshair_mail_32", group = "reference" }, + { name = "APIBrowser", atlas = "crosshair_speak_32", fileID = 133739, group = "reference" },-- INV_Misc_Book_07 (blue book/docs) + { name = "CVarViewer", atlas = "Adventure-Mission-Silver-Dragon", fileID = 136243, group = "reference" }, -- Trade_Engineering (gear/settings) + { name = "EventMonitor", atlas = "Crosshair_mail_32", fileID = 136048, group = "reference" },-- Spell_Nature_Lightning (live events) -- Diagnostics - { name = "ErrorHandler", atlas = "crosshair_crosshairs_32", group = "diag" }, - { name = "Performance", atlas = "crosshair_track_32", group = "diag" }, + { name = "ErrorHandler", atlas = "crosshair_crosshairs_32", fileID = 136168, group = "diag" }, -- Spell_Fire_SealOfFire (warning) + { name = "Performance", atlas = "crosshair_track_32", fileID = 134377, group = "diag" }, -- INV_Misc_PocketWatch_02 (stopwatch) } function ActivityBar:Create(parent) @@ -62,6 +63,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) @@ -76,23 +78,30 @@ function ActivityBar:Create(parent) local icon = btn:CreateTexture(nil, "ARTWORK") icon:SetSize(L.activityIconSize, L.activityIconSize) icon:SetPoint("CENTER", 1, 0) -- offset 1px right to account for accent bar - if def.fileID then + local iconSet = false + if not def.fileID or def.atlas then + -- Try atlas first (preferred on Retail) + if def.atlas then + local atlasOk = pcall(function() icon:SetAtlas(def.atlas) end) + if atlasOk and icon:GetAtlas() then + iconSet = true + end + elseif def.atlasOff then + local atlasOk = pcall(function() icon:SetAtlas(def.atlasOff) end) + if atlasOk and icon:GetAtlas() then + iconSet = true + end + end + end + if not iconSet and def.fileID then icon:SetTexture(def.fileID) + iconSet = true + end + if iconSet then icon:SetDesaturated(true) icon:SetVertexColor(0.6, 0.6, 0.6) - elseif def.atlas then - local atlasOk = pcall(function() icon:SetAtlas(def.atlas) end) - if atlasOk then - icon:SetDesaturated(true) - icon:SetVertexColor(0.6, 0.6, 0.6) - else - icon:SetColorTexture(0.3, 0.3, 0.3, 0.8) - end else - local atlasOk = pcall(function() icon:SetAtlas(def.atlasOff) end) - if not atlasOk then - icon:SetColorTexture(0.3, 0.3, 0.3, 0.8) - end + icon:SetColorTexture(0.3, 0.3, 0.3, 0.8) end -- Hover highlight @@ -132,11 +141,14 @@ function ActivityBar:Create(parent) DF.ModuleSystem:Activate(def.name) end) + -- Track whether atlas was actually used (may fail on Classic) + local usedAtlas = iconSet and not def.fileID or (iconSet and icon:GetAtlas() and icon:GetAtlas() ~= "") local entry = { name = def.name, btn = btn, accent = accent, icon = icon, + usedAtlas = usedAtlas, fileID = def.fileID, atlas = def.atlas, atlasOff = def.atlasOff, @@ -156,19 +168,19 @@ function ActivityBar:Create(parent) for _, entry in ipairs(bar.ordered) do if entry.name == moduleName then entry.accent:Show() - if entry.fileID or entry.atlas then + if entry.usedAtlas and entry.atlasOn then + pcall(function() entry.icon:SetAtlas(entry.atlasOn) end) + else entry.icon:SetDesaturated(false) entry.icon:SetVertexColor(1, 1, 1) - else - pcall(function() entry.icon:SetAtlas(entry.atlasOn) end) end else entry.accent:Hide() - if entry.fileID or entry.atlas then + if entry.usedAtlas and entry.atlasOff then + pcall(function() entry.icon:SetAtlas(entry.atlasOff) end) + else entry.icon:SetDesaturated(true) entry.icon:SetVertexColor(0.6, 0.6, 0.6) - else - pcall(function() entry.icon:SetAtlas(entry.atlasOff) end) end end end From 368ef58d3c46e6f832c41c10726eb9d5d64b7939 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Mon, 30 Mar 2026 12:32:36 -0600 Subject: [PATCH 2/4] Update README: Classic support is now live across all flavors Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 107099a..bd9e7d1 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,14 @@ DevForge is designed to be **self-contained**, **dependency-light**, and adaptab ## Supported Clients -| Client | Status | -|------|------| -| Retail (Mainline) | ✅ Full support | -| Classic (current line) | 🧪 In progress (targeting parity) | -| Classic Era | 🧭 Planned | - -DevForge uses client detection and feature gating to ensure safe behavior across versions. +| Client | Interface | Status | +|--------|-----------|--------| +| Retail (Mainline) | 120000 / 120001 | ✅ Full support | +| Classic — Mists of Pandaria | 50503 | ✅ Supported | +| Classic — Cataclysm | 40402 | ✅ Supported | +| Classic Era (Vanilla) | 11508 | ✅ Supported | + +A single codebase serves all clients. `Core/Compat.lua` provides polyfills for Retail-only APIs (`C_AddOns`, `HelpTip`) and flavor detection flags. The main TOC covers Retail and progressive Classic; `DevForge_Vanilla.toc` covers Classic Era. All modules load on every flavor except **WA Importer**, which is Retail-only. --- From 5ca3195ecfce807a2580fddf726b7bd47d328bd6 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Mon, 30 Mar 2026 12:33:30 -0600 Subject: [PATCH 3/4] Update docs: Classic support is now live across all flavors Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/index.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index 77e6751..fa7610a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,11 +87,14 @@ Each feature is documented individually with screenshots and usage notes. ## Client Support -- **Retail:** Fully supported -- **Classic (current line):** Actively targeting parity -- **Classic Era:** Planned - -Features automatically adapt based on client capabilities and available APIs. +| Client | Status | +|--------|--------| +| Retail (Mainline) | ✅ Full support | +| Classic — Mists of Pandaria | ✅ Supported | +| Classic — Cataclysm | ✅ Supported | +| Classic Era (Vanilla) | ✅ Supported | + +All modules work across every flavor except **WA Importer**, which is Retail-only. Features automatically adapt based on client capabilities and available APIs. --- From 454c70b3e5d2f7dc6e7bbec904d123f1052962f1 Mon Sep 17 00:00:00 2001 From: Hatdragon Date: Tue, 21 Apr 2026 16:32:16 -0600 Subject: [PATCH 4/4] update for 12.0.5 --- DevForge/DevForge.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DevForge/DevForge.toc b/DevForge/DevForge.toc index 4fa3105..2a00436 100644 --- a/DevForge/DevForge.toc +++ b/DevForge/DevForge.toc @@ -1,4 +1,4 @@ -## Interface: 120000, 120001, 50503, 40402 +## Interface: 120005, 50503, 40402 ## Title: DevForge ## Notes: In-game dev toolkit: inspector, editor, console, events, errors, APIs, textures, sounds, performance ## Author: hatdragon