--[[-------------------------------------------------------------------------- Copyright (c) 2007-2017, James Whitehead II All rights reserved. WowLua is an interactive interpreter for World of Warcraft --------------------------------------------------------------------------]]-- local addon = ... local version = GetAddOnMetadata("WowLua", "Version") or "SVN" WowLua = { VERSION = "WowLua v" .. version .. " Interactive Interpreter", queue = {}, queuePos = 0, } local L = WowLuaLocals WowLua_DB = { pages = { [1] = {name = format(L.NEW_PAGE_TITLE, 1), content = "", untitled = true} }, currentPage = 1, untitled = 2, fontSize = 14, } local DB = {} local eframe = CreateFrame("Frame") eframe:RegisterEvent("ADDON_LOADED") eframe:SetScript("OnEvent", function(self, event, ...) if event == "ADDON_LOADED" then local arg1 = ... if arg1 == addon then if WowLua_DB.fontSize then local file, height, flags = WowLuaMonoFont:GetFont() WowLuaMonoFont:SetFont(file, WowLua_DB.fontSize, flags) end end end end) function WowLua:CreateNewPage() local name = format(L.NEW_PAGE_TITLE, WowLua_DB.untitled) WowLua_DB.untitled = WowLua_DB.untitled + 1 local entry = { name = name, content = "", untitled = true } table.insert(WowLua_DB.pages, entry) WowLua_DB.currentPage = #WowLua_DB.pages return entry, #WowLua_DB.pages end function WowLua:GetNumPages() return #WowLua_DB.pages end function WowLua:SavePage(num, content) local entry = WowLua_DB.pages[num] entry.content = content end function WowLua:RenamePage(num, name) local entry = WowLua_DB.pages[num] entry.name = name entry.untitled = nil end function WowLua:DeletePage(num) table.remove(WowLua_DB.pages, num) end function WowLua:LockPage(num, locked) local entry = WowLua_DB.pages[num] entry.locked = locked end function WowLua:IsPageLocked(num) local entry = WowLua_DB.pages[num] return entry.locked end function WowLua:GetCurrentPage() local page = WowLua_DB.currentPage return page, WowLua_DB.pages[page] end function WowLua:SelectPage(id) if type(id) == "number" then WowLua_DB.currentPage = id return WowLua_DB.pages[id], id elseif type(id) == "string" then for idx,entry in ipairs(WowLua_DB.pages) do if entry.name == id then WowLua_DB.currentPage = idx return entry, idx end end end end local function wowpad_print(...) local out = "" for i=1,select("#", ...) do -- Comma seperate values if i > 1 then out = out .. ", " end out = out .. tostring(select(i, ...)) end WowLuaFrameOutput:AddMessage("|cff999999" .. out .. "|r") end if not print then print = wowpad_print end local function processSpecialCommands(txt) if txt == L.RELOAD_COMMAND then ReloadUI() return true elseif txt == L.RESET_COMMAND then WowLuaFrame:ClearAllPoints() WowLuaFrame:SetPoint("CENTER") WowLuaFrame:SetWidth(640) WowLuaFrame:SetHeight(512) WowLuaFrameResizeBar:ClearAllPoints() WowLuaFrameResizeBar:SetPoint("TOPLEFT", WowLuaFrame, "BOTTOMLEFT", 14, 100) WowLuaFrameResizeBar:SetPoint("TOPRIGHT", WowLuaFrame, "BOTTOMRIGHT", 0, 100) return true end end function WowLua:ProcessLine(text) WowLuaFrameCommandEditBox:SetText("") if processSpecialCommands(text) then return end -- escape any color codes: local output = text:gsub("\124", "\124\124") WowLuaFrameOutput:AddMessage(WowLuaFrameCommandPrompt:GetText() .. output) WowLuaFrameCommandEditBox:AddHistoryLine(output) -- If they're using "= value" syntax, just print it text = text:gsub("^%s*=%s*(.+)", "print(%1)") -- Store this command into self.cmd in case we have multiple lines if self.cmd then self.cmd = self.cmd .. "\n" .. text self.orig = self.orig .. "\n" .. text else self.cmd = text self.orig = text end -- Trim the command before we run it self.cmd = string.trim(self.cmd) -- Process the current command local func,err = loadstring(self.cmd) -- Fail to compile? Give it a return -- Check to see if this just needs a return in front of it if not func then local newfunc,newerr = loadstring("print(" .. self.cmd .. ")") if newfunc then func,err = newfunc,newerr end end if not func then -- Check to see if this is just an unfinished block if err:sub(-7, -1) == "''" then -- Change the prompt WowLuaFrameCommandPrompt:SetText(">> ") return end WowLuaFrameOutput:AddMessage("|cffff0000" .. err .. "|r") self.cmd = nil WowLuaFrameCommandPrompt:SetText("> ") else -- Make print a global function local old_print = print print = wowpad_print -- Call the function local succ,err = pcall(func) -- Restore the value of print print = old_print if not succ then WowLuaFrameOutput:AddMessage("|cffff0000" .. err .. "|r") end self.cmd = nil WowLuaFrameCommandPrompt:SetText("> ") end end function WowLua:RunScript(text) -- escape any color codes: local output = text:gsub("\124", "\124\124") if text == L.RELOAD_COMMAND then ReloadUI() end -- If they're using "= value" syntax, just print it text = text:gsub("^%s*=%s*(.+)", "print(%1)") -- Trim the command before we run it text = string.trim(text) -- Process the current command local func,err = loadstring(text, "WowLua") if not func then WowLuaFrameOutput:AddMessage("|cffff0000" .. err .. "|r") return false, err else -- Make print a global function local old_print = print print = wowpad_print -- Call the function local succ,err = pcall(func) -- Restore the value of print print = old_print if not succ then WowLuaFrameOutput:AddMessage("|cffff0000" .. err .. "|r") return false, err end end return true end function WowLua:Initialize(frame) WowLua:OnSizeChanged(frame) table.insert(UISpecialFrames, "WowLuaFrame") PlaySound(SOUNDKIT.IG_MAINMENU_OPEN); self:UpdateButtons() end function WowLua:Button_OnEnter(frame) GameTooltip:SetOwner(frame, "ANCHOR_BOTTOM"); local operation = frame:GetName():match("WowLuaButton_(.+)"):gsub("_", " ") local tooltip = L.TOOLTIPS[operation] GameTooltip:SetText(tooltip and tooltip.name or operation) if tooltip then GameTooltip:AddLine(tooltip.text, 1, 1, 1) end GameTooltip:Show(); end function WowLua:Button_OnLeave(frame) GameTooltip:Hide() end function WowLua:Button_OnClick(button) local operation = button:GetName():match("WowLuaButton_(.+)") if operation == "New" then WowLua:Button_New(button) elseif operation == "Open" then WowLua:Button_Open(button) elseif operation == "Save" then WowLua:Button_Save(button) elseif operation == "Undo" then WowLua:Button_Undo(button) elseif operation == "Redo" then WowLua:Button_Redo(button) elseif operation == "Delete" then WowLua:Button_Delete(button) elseif operation == "Lock" then WowLua:Button_Lock(button) elseif operation == "Unlock" then WowLua:Button_Unlock(button) elseif operation == "Previous" then WowLua:Button_Previous(button) elseif operation == "Next" then WowLua:Button_Next(button) elseif operation == "Run" then WowLua:Button_Run(button) elseif operation == "Config" then WowLua:Button_Config(button) elseif operation == "Close" then WowLua:Button_Close(button) end end function WowLua:DebugQueue() print("Current queue position: " .. self.queuePos) for k,v in pairs(self.queue) do print(k, v:sub(1, 20)) end end function WowLua:FlushQueue() table.wipe(self.queue) self.queuePos = 0 end function WowLua:Queue(text) if #self.queue == 0 then local page, entry = WowLua:GetCurrentPage() self.queue[1] = entry.content self.queuePos = 1 end if text ~= self.queue[self.queuePos] then self.queuePos = self.queuePos+1 self.queue[self.queuePos] = text for i=self.queuePos+1,#self.queue do self.queue[i]=nil end end end function WowLua:GetUndoPage() -- Before we do any "Undo", queue the current text WowLua:Queue(WowLuaFrameEditBox:GetText()) local item = self.queue[self.queuePos-1] if item then self.queuePos = self.queuePos-1 return item end return self.queue[self.queuePos] end function WowLua:GetRedoPage() local item = self.queue[self.queuePos+1] if item then self.queuePos = self.queuePos+1 return item end return self.queue[self.queuePos] end function WowLua:Button_New(button) if self:IsModified() then -- Display the unsaved changes dialog local dialog = StaticPopup_Show("WOWLUA_UNSAVED") dialog.data = "Button_New" return end -- Create a new page and display it local entry, num = WowLua:CreateNewPage() WowLuaFrameEditBox:SetText(entry.content) WowLua:UpdateButtons() WowLua:SetTitle(false) WowLua:FlushQueue() end function WowLua:Button_Open(button) ToggleDropDownMenu(1, nil, WowLuaOpenDropDown, button:GetName(), 0, 0) end function WowLua:OpenDropDownOnLoad(frame) UIDropDownMenu_Initialize(frame, self.OpenDropDownInitialize) end local function dropDownFunc(button, page) WowLua:GoToPage(page) end function WowLua.OpenDropDownInitialize() UIDropDownMenu_AddButton{ text = L.OPEN_MENU_TITLE, isTitle = 1 } for page, entry in ipairs(WowLua_DB.pages) do UIDropDownMenu_AddButton{ text = entry.name, func = dropDownFunc, arg1 = page } end end StaticPopupDialogs["WOWLUA_SAVE_AS"] = { text = L.SAVE_AS_TEXT, button1 = OKAY and OKAY or "Okay", button2 = CANCEL and CANCEL or "Cancel", OnAccept = function(self) local name = self:GetName().."EditBox" local button = _G[name] local text = button:GetText() WowLua:RenamePage(WowLua.save_as, text) WowLua:SetTitle() end, timeout = 0, whileDead = 1, exclusive = 1, showAlert = 1, hideOnEscape = 1, hasEditBox = 1, maxLetters = 32, OnShow = function(self) getglobal(self:GetName().."Button1"):Disable(); local editBox = _G[self:GetName().."EditBox"] editBox:SetFocus() editBox:SetText(WowLua.save_as_name) editBox:HighlightText() end, OnHide = function(self) local activeWindow = ChatEdit_GetActiveWindow() if activeWindow then activeWindow:SetText("") end end, EditBoxOnEnterPressed = function(self) if _G[self:GetParent():GetName().."Button1"]:IsEnabled() == 1 then local name = self:GetParent():GetName().."EditBox" local button = _G[name] local text = button:GetText() WowLua:RenamePage(WowLua.save_as, text) WowLua:SetTitle() self:GetParent():Hide(); end end, EditBoxOnTextChanged = function (self) local editBox = _G[self:GetParent():GetName().."EditBox"]; local txt = editBox:GetText() if #txt > 0 then _G[self:GetParent():GetName().."Button1"]:Enable(); else _G[self:GetParent():GetName().."Button1"]:Disable(); end end, EditBoxOnEscapePressed = function(self) self:GetParent():Hide(); ClearCursor(); end } function WowLua:Button_Save(button) if button and IsShiftKeyDown() then -- Show the static popup for renaming local page, entry = self:GetCurrentPage() WowLua.save_as = page WowLua.save_as_name = entry.name StaticPopup_Show("WOWLUA_SAVE_AS", entry.name) return else local text = WowLuaFrameEditBox:GetText() local page = self:GetCurrentPage() self:SavePage(page, text) self:UpdateButtons() self:SetTitle(false) WowLua:Queue(text) end end function WowLua:Button_Undo(button) local page, entry = self:GetCurrentPage() local undo = WowLua:GetUndoPage() WowLuaFrameEditBox:SetText(undo or entry.content) end function WowLua:Button_Redo(button) local page, entry = self:GetCurrentPage() local redo = WowLua:GetRedoPage() WowLuaFrameEditBox:SetText(redo) end function WowLua:Button_Delete(button) if self:IsModified() then -- Display the unsaved changes dialog local dialog = StaticPopup_Show("WOWLUA_UNSAVED") dialog.data = "Button_Delete" return end local page, entry = self:GetCurrentPage() if self:GetNumPages() == 1 then self:Button_New() self:Button_Previous() end self:DeletePage(page) if page > 1 then page = page - 1 end local entry = self:SelectPage(page) WowLuaFrameEditBox:SetText(entry.content) self:UpdateButtons() self:SetTitle(false) end function WowLua:Button_Lock(button) local id = self:GetCurrentPage() self:LockPage(id, true) self:UpdateButtons() end function WowLua:Button_Unlock(button) local id = self:GetCurrentPage() self:LockPage(id, false) self:UpdateButtons() end StaticPopupDialogs["WOWLUA_UNSAVED"] = { text = L.UNSAVED_TEXT, button1 = OKAY and OKAY or "Okay", button2 = CANCEL and CANCEL or "Cancel", OnAccept = function(self) local page,entry = WowLua:GetCurrentPage() WowLuaFrameEditBox:SetText(entry.content) local action = self:GetParent().data if type(action) == "string" then WowLua[action](WowLua) else WowLua:GoToPage(self:GetParent().data) end end, timeout = 0, whileDead = 1, exclusive = 1, showAlert = 1, hideOnEscape = 1, EditBoxOnEscapePressed = function(self) self:GetParent():Hide(); ClearCursor(); end } function WowLua:Button_Previous() self:GoToPage(self:GetCurrentPage() - 1) end function WowLua:Button_Next() self:GoToPage(self:GetCurrentPage() + 1) end function WowLua:GoToPage(page) if self:IsModified() then -- Display the unsaved changes dialog local dialog = StaticPopup_Show("WOWLUA_UNSAVED") dialog.data = page return end local entry = self:SelectPage(page) if not entry then return end WowLuaFrameEditBox:SetText(entry.content) self:UpdateButtons() self:SetTitle(false) WowLua:FlushQueue() end function WowLua:UpdateButtons() local current = self:GetCurrentPage() local max = self:GetNumPages() if current == 1 then WowLuaButton_Previous:Disable() else WowLuaButton_Previous:Enable() end if current == max then WowLuaButton_Next:Disable() else WowLuaButton_Next:Enable() end self.indent.indentEditbox(WowLuaFrameEditBox) if self:IsPageLocked(current) then WowLuaButton_Unlock:Show() WowLuaButton_Lock:Hide() WowLuaButton_Delete:Disable() WowLuaFrameEditBox:SetScript("OnTextChanged", self.lockedTextChanged) else WowLuaButton_Unlock:Hide() WowLuaButton_Lock:Show() WowLuaButton_Delete:Enable() WowLuaFrameEditBox:SetScript("OnTextChanged", self.unlockedTextChanged) end end function WowLua.lockedTextChanged(box) if WowLua.reverting then WowLua.reverting = false else WowLua.reverting = true local entry = select(2, WowLua:GetCurrentPage()) local pos = WowLua.lastCursorPos box:SetText(entry.content) WowLua.indent.indentEditbox(WowLuaFrameEditBox) if pos then box:SetCursorPosition(pos) end end end function WowLua:Button_Run() local text = WowLuaFrameEditBox:GetText() -- Run the script, if there is an error then highlight it if text then -- Add the current state of the page to the queue WowLua:Queue(text) local succ,err = WowLua:RunScript(text) if not succ then local chunkName,lineNum = err:match("(%b[]):(%d+):") lineNum = tonumber(lineNum) WowLua:UpdateLineNums(lineNum) -- Highlight the text in the editor by finding the char of the line number we're on text = WowLua.indent.coloredGetText(WowLuaFrameEditBox) local curLine,start = 1,1 while curLine < lineNum do local s,e = text:find("\n", start) start = e + 1 curLine = curLine + 1 end local nextLine = select(2, text:find("\n", start)) WowLuaFrameEditBox:SetFocus() WowLuaFrameEditBox:SetCursorPosition(start - 1) end end end function WowLua:Button_Config() InterfaceOptionsFrame_OpenToCategory("WowLua") end function WowLua:Button_Close() if self:IsModified() then -- Display the unsaved changes dialog local dialog = StaticPopup_Show("WOWLUA_UNSAVED") dialog.data = "Button_Close" return end HideUIPanel(WowLuaFrame) end function WowLua:IsModified() local page,entry = self:GetCurrentPage() local orig = entry.content local current = WowLuaFrameEditBox:GetText(true) return orig ~= current end function WowLua:IsUntitled() local page, entry = self:GetCurrentPage() return entry.untitled end function WowLua:SetTitle(modified) local page,entry = self:GetCurrentPage() WowLuaFrameTitle:SetFormattedText("%s%s - WowLua Editor", entry.name, self:IsModified() and "*" or "") end function WowLua:OnSizeChanged(frame) -- The first graphic is offset 13 pixels to the right local width = frame:GetWidth() - 13 local bg2w,bg3w,bg4w = 0,0,0 -- Resize bg2 up to 256 width local bg2w = width - 256 if bg2w > 256 then bg3w = bg2w - 256 bg2w = 256 end if bg3w > 256 then bg4w = bg3w - 256 bg3w = 256 end local bg2 = WowLuaFrameBG2 local bg3 = WowLuaFrameBG3 local bg4 = WowLuaFrameBG4 if bg2w > 0 then bg2:SetWidth(bg2w) bg2:SetTexCoord(0, (bg2w / 256), 0, 1) bg2:Show() else bg2:Hide() end if bg3w and bg3w > 0 then bg3:SetWidth(bg3w) bg3:SetTexCoord(0, (bg3w / 256), 0, 1) bg3:Show() else bg3:Hide() end if bg4w and bg4w > 0 then bg4:SetWidth(bg4w) bg4:SetTexCoord(0, (bg4w / 256), 0, 1) bg4:Show() else bg4:Hide() end if WowLuaFrameResizeBar and false then local parent = WowLuaFrameResizeBar:GetParent() local cursorY = select(2, GetCursorPosition()) local newPoint = select(5, WowLuaFrameResizeBar:GetPoint()) local maxPoint = parent:GetHeight() - 175; if newPoint < 100 then newPoint = 100 elseif newPoint > maxPoint then newPoint = maxPoint end WowLuaFrameResizeBar:ClearAllPoints() WowLuaFrameResizeBar:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 14, newPoint) WowLuaFrameResizeBar:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", 0, newPoint) end end function WowLua:ResizeBar_OnMouseDown(frame, button) frame.cursorStart = select(2, GetCursorPosition()) frame.anchorStart = select(5, frame:GetPoint()) frame:SetScript("OnUpdate", function(...) WowLua:ResizeBar_OnUpdate(...) end) end function WowLua:ResizeBar_OnMouseUp(frame, button) frame:SetScript("OnUpdate", nil) end function WowLua:ResizeBar_OnUpdate(frame, elapsed) local parent = frame:GetParent() local cursorY = select(2, GetCursorPosition()) local newPoint = frame.anchorStart - (frame.cursorStart - cursorY)/frame:GetEffectiveScale() local maxPoint = parent:GetHeight() - 175; if newPoint < 100 then newPoint = 100 elseif newPoint > maxPoint then newPoint = maxPoint end frame:ClearAllPoints() frame:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 14, newPoint) frame:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", 0, newPoint) end function WowLua:OnVerticalScroll(scrollFrame) local offset = scrollFrame:GetVerticalScroll(); local scrollbar = getglobal(scrollFrame:GetName().."ScrollBar"); scrollbar:SetValue(offset); local min, max = scrollbar:GetMinMaxValues(); local display = false; if ( offset == 0 ) then getglobal(scrollbar:GetName().."ScrollUpButton"):Disable(); else getglobal(scrollbar:GetName().."ScrollUpButton"):Enable(); display = true; end if ((scrollbar:GetValue() - max) == 0) then getglobal(scrollbar:GetName().."ScrollDownButton"):Disable(); else getglobal(scrollbar:GetName().."ScrollDownButton"):Enable(); display = true; end if ( display ) then scrollbar:Show(); else scrollbar:Hide(); end end function WowLua:UpdateLineNums(highlightNum) -- highlightNum is the line number indicated by the error message if highlightNum then WowLua.highlightNum = highlightNum else highlightNum = WowLua.highlightNum end -- Since we know this is FAIAP enabled, we need to pass true in order -- to get the raw values local editbox = WowLuaFrameEditBox local linebox = WowLuaFrameLineNumEditBox local linetest = WowLuaFrameEditBoxLineTest local linescroll = WowLuaFrameLineNumScrollFrame local width = editbox:GetWidth() local text = editbox:GetText(true) local linetext = "" local count = 1 for line in text:gmatch("([^\n]*\n?)") do if #line > 0 then if count == highlightNum then linetext = linetext .. "|cFFFF1111" .. count .. "|r" .. "\n" else linetext = linetext .. count .. "\n" end count = count + 1 -- Check to see if the line of text spans more than one actual line linetest:SetText(line:gsub("|", "||")) local testwidth = linetest:GetWidth() if testwidth >= width then linetext = linetext .. string.rep("\n", testwidth / width) end end end if text:sub(-1, -1) == "\n" then linetext = linetext .. count .. "\n" count = count + 1 end -- Make the line number frame wider as necessary local offset = tostring(count):len() * 10 linescroll:ClearAllPoints() linescroll:SetPoint("TOPLEFT", WowLuaFrame, "TOPLEFT", 18, -74) linescroll:SetPoint("BOTTOMRIGHT", WowLuaFrameResizeBar, "TOPLEFT", 15 + offset, -4) linebox:SetText(linetext) linetest:SetText(text) end local function canScroll(scroll, direction) if direction == "up" then return not scroll:AtTop() end if direction == "down" then return not scroll:AtBottom() end return true; end function WowLua:UpdateScrollingMessageFrame(frame) local name = frame:GetName(); local display = false; if ( canScroll(frame, "up") ) then getglobal(name.."UpButton"):Enable(); display = true; else getglobal(name.."UpButton"):Disable(); end if ( canScroll(frame, "down") ) then getglobal(name.."DownButton"):Enable(); display = true; else getglobal(name.."DownButton"):Disable(); end if ( display ) then getglobal(name.."UpButton"):Show(); getglobal(name.."DownButton"):Show(); else getglobal(name.."UpButton"):Hide(); getglobal(name.."DownButton"):Hide(); end end local scrollMethods = { ["line"] = { ["up"] = "ScrollUp", ["down"] = "ScrollDown" }, ["page"] = { ["up"] = "PageUp", ["down"] = "PageDown" }, ["end"] = { ["up"] = "ScrollToTop", ["down"] = "ScrollToBottom" }, }; function WowLua:ScrollingMessageFrameScroll(scroll, direction, type) -- Make sure we can scroll first if ( not canScroll(scroll, direction) ) then return; end local method = scrollMethods[type][direction]; scroll[method](scroll); end function WowLua:OnTextChanged(frame) frame.highlightNum = nil end function WowLua:OnCursorChanged(frame) WowLua.dirty = true end BINDING_HEADER_WOWLUA = "WowLua Editor/Interpreter" BINDING_NAME_TOGGLE_WOWLUA = "Show/Hide window" BINDING_NAME_RUN_WOWLUA = "Run current page" BINDING_NAME_SAVE_WOWLUA = "Save current page" SLASH_WOWLUA1 = "/lua" SLASH_WOWLUA2 = "/wowlua" local first = true SlashCmdList["WOWLUA"] = function(txt) local page, entry = WowLua:GetCurrentPage() if first then WowLuaFrameEditBox:SetText(entry.content) WowLuaFrameEditBox:SetWidth(WowLuaFrameEditScrollFrame:GetWidth()) WowLua:SetTitle(false) first = false end WowLuaFrame:Show() if processSpecialCommands(txt) then return end if txt:match("%S") then WowLua:ProcessLine(txt) end WowLuaFrameCommandEditBox:SetFocus() end local function printf(fmt, ...) print(fmt:format(...)) end SLASH_WOWLUARUN1 = "/luarun" SLASH_WOWLUARUN2 = "/wowluarun" SlashCmdList["WOWLUARUN"] = function(txt, editbox) local entry, idx = WowLua:SelectPage(txt) if not entry then printf("|cFF33FF99WowLua|r: Unable to find a page named '%s'", txt) return else printf("|cFF33FF99WowLua|r: Running page '%s'", txt) local func, err = loadstring(entry.content, "WowLua") if not func then printf("|cFF33FF99WowLua|r: Error compiling page '%s': %s", txt, err) else -- Call the function local succ, err = pcall(func) if not succ then printf("|cFF33FF99WowLua|r: Error while running page '%s': %s", txt, err) end end end end