diff --git a/Libs/ForAllIndentsAndPurposes.lua b/Libs/ForAllIndentsAndPurposes.lua new file mode 100644 index 0000000..e9d17ba --- /dev/null +++ b/Libs/ForAllIndentsAndPurposes.lua @@ -0,0 +1,1315 @@ +-- For All Indents And Purposes +local revision = 19 +-- Maintainer: kristofer.karlsson@gmail.com + +-- For All Indents And Purposes - +-- a indentation + syntax highlighting library +-- All valid lua code should be processed correctly. + +-- Usage (for developers) +-------- +-- Variant 1: - non embedded +-- 1) Add ForAllIndentsAndPurposes to your dependencies (or optional dependencies) + +-- Variant 2: - embedded +-- 1.a) Copy indent.lua to your addon directory +-- 1.b) Put indent.lua first in your list of files in the TOC + +-- For both variants: +-- 2) hook the editboxes that you want to have indentation like this: +-- IndentationLib.enable(editbox [, colorTable [, tabWidth] ]) +-- if you don't select a color table, it will use the default. +-- Read through this code for further usage help. +-- (The documentation IS the code) + +-- luacheck: globals IndentationLib + +if not IndentationLib then + IndentationLib = {} +end + +if not IndentationLib.revision or revision > IndentationLib.revision then + local lib = IndentationLib + lib.revision = revision + + local stringlen = string.len + local stringformat = string.format + local stringfind = string.find + local stringsub = string.sub + local stringbyte = string.byte + local stringchar = string.char + local stringrep = string.rep + local stringgsub = string.gsub + + local defaultTabWidth = 2 + local defaultColorTable + + local workingTable = {} + local workingTable2 = {} + local function tableclear(t) + for k in next,t do + t[k] = nil + end + end + + local function stringinsert(s, pos, insertStr) + return stringsub(s, 1, pos) .. insertStr .. stringsub(s, pos + 1) + end + lib.stringinsert = stringinsert + + local function stringdelete(s, pos1, pos2) + return stringsub(s, 1, pos1 - 1) .. stringsub(s, pos2 + 1) + end + lib.stringdelete = stringdelete + + -- token types + local tokens = {} + lib.tokens = tokens + tokens.TOKEN_UNKNOWN = 0 + tokens.TOKEN_NUMBER = 1 + tokens.TOKEN_LINEBREAK = 2 + tokens.TOKEN_WHITESPACE = 3 + tokens.TOKEN_IDENTIFIER = 4 + tokens.TOKEN_ASSIGNMENT = 5 + tokens.TOKEN_EQUALITY = 6 + tokens.TOKEN_MINUS = 7 + tokens.TOKEN_COMMENT_SHORT = 8 + tokens.TOKEN_COMMENT_LONG = 9 + tokens.TOKEN_STRING = 10 + tokens.TOKEN_LEFTBRACKET = 11 + tokens.TOKEN_PERIOD = 12 + tokens.TOKEN_DOUBLEPERIOD = 13 + tokens.TOKEN_TRIPLEPERIOD = 14 + tokens.TOKEN_LTE = 15 + tokens.TOKEN_LT = 16 + tokens.TOKEN_GTE = 17 + tokens.TOKEN_GT = 18 + tokens.TOKEN_NOTEQUAL = 19 + tokens.TOKEN_COMMA = 20 + tokens.TOKEN_SEMICOLON = 21 + tokens.TOKEN_COLON = 22 + tokens.TOKEN_LEFTPAREN = 23 + tokens.TOKEN_RIGHTPAREN = 24 + tokens.TOKEN_PLUS = 25 + tokens.TOKEN_SLASH = 27 + tokens.TOKEN_LEFTWING = 28 + tokens.TOKEN_RIGHTWING = 29 + tokens.TOKEN_CIRCUMFLEX = 30 + tokens.TOKEN_ASTERISK = 31 + tokens.TOKEN_RIGHTBRACKET = 32 + tokens.TOKEN_KEYWORD = 33 + tokens.TOKEN_SPECIAL = 34 + tokens.TOKEN_VERTICAL = 35 + tokens.TOKEN_TILDE = 36 + -- WoW specific tokens + tokens.TOKEN_COLORCODE_START = 37 + tokens.TOKEN_COLORCODE_STOP = 38 + -- new as of lua 5.1 + tokens.TOKEN_HASH = 39 + tokens.TOKEN_PERCENT = 40 + + + -- ascii codes + local bytes = {} + lib.bytes = bytes + bytes.BYTE_LINEBREAK_UNIX = stringbyte("\n") + bytes.BYTE_LINEBREAK_MAC = stringbyte("\r") + bytes.BYTE_SINGLE_QUOTE = stringbyte("'") + bytes.BYTE_DOUBLE_QUOTE = stringbyte('"') + bytes.BYTE_0 = stringbyte("0") + bytes.BYTE_9 = stringbyte("9") + bytes.BYTE_PERIOD = stringbyte(".") + bytes.BYTE_SPACE = stringbyte(" ") + bytes.BYTE_TAB = stringbyte("\t") + bytes.BYTE_E = stringbyte("E") + bytes.BYTE_e = stringbyte("e") + bytes.BYTE_MINUS = stringbyte("-") + bytes.BYTE_EQUALS = stringbyte("=") + bytes.BYTE_LEFTBRACKET = stringbyte("[") + bytes.BYTE_RIGHTBRACKET = stringbyte("]") + bytes.BYTE_BACKSLASH = stringbyte("\\") + bytes.BYTE_COMMA = stringbyte(",") + bytes.BYTE_SEMICOLON = stringbyte(";") + bytes.BYTE_COLON = stringbyte(":") + bytes.BYTE_LEFTPAREN = stringbyte("(") + bytes.BYTE_RIGHTPAREN = stringbyte(")") + bytes.BYTE_TILDE = stringbyte("~") + bytes.BYTE_PLUS = stringbyte("+") + bytes.BYTE_SLASH = stringbyte("/") + bytes.BYTE_LEFTWING = stringbyte("{") + bytes.BYTE_RIGHTWING = stringbyte("}") + bytes.BYTE_CIRCUMFLEX = stringbyte("^") + bytes.BYTE_ASTERISK = stringbyte("*") + bytes.BYTE_LESSTHAN = stringbyte("<") + bytes.BYTE_GREATERTHAN = stringbyte(">") + -- WoW specific chars + bytes.BYTE_VERTICAL = stringbyte("|") + bytes.BYTE_r = stringbyte("r") + bytes.BYTE_c = stringbyte("c") + -- new as of lua 5.1 + bytes.BYTE_HASH = stringbyte("#") + bytes.BYTE_PERCENT = stringbyte("%") + + + local linebreakCharacters = {} + lib.linebreakCharacters = linebreakCharacters + linebreakCharacters[bytes.BYTE_LINEBREAK_UNIX] = 1 + linebreakCharacters[bytes.BYTE_LINEBREAK_MAC] = 1 + + local whitespaceCharacters = {} + lib.whitespaceCharacters = whitespaceCharacters + whitespaceCharacters[bytes.BYTE_SPACE] = 1 + whitespaceCharacters[bytes.BYTE_TAB] = 1 + + local specialCharacters = {} + lib.specialCharacters = specialCharacters + specialCharacters[bytes.BYTE_PERIOD] = -1 + specialCharacters[bytes.BYTE_LESSTHAN] = -1 + specialCharacters[bytes.BYTE_GREATERTHAN] = -1 + specialCharacters[bytes.BYTE_LEFTBRACKET] = -1 + specialCharacters[bytes.BYTE_EQUALS] = -1 + specialCharacters[bytes.BYTE_MINUS] = -1 + specialCharacters[bytes.BYTE_SINGLE_QUOTE] = -1 + specialCharacters[bytes.BYTE_DOUBLE_QUOTE] = -1 + specialCharacters[bytes.BYTE_TILDE] = -1 + specialCharacters[bytes.BYTE_RIGHTBRACKET] = tokens.TOKEN_RIGHTBRACKET + specialCharacters[bytes.BYTE_COMMA] = tokens.TOKEN_COMMA + specialCharacters[bytes.BYTE_COLON] = tokens.TOKEN_COLON + specialCharacters[bytes.BYTE_SEMICOLON] = tokens.TOKEN_SEMICOLON + specialCharacters[bytes.BYTE_LEFTPAREN] = tokens.TOKEN_LEFTPAREN + specialCharacters[bytes.BYTE_RIGHTPAREN] = tokens.TOKEN_RIGHTPAREN + specialCharacters[bytes.BYTE_PLUS] = tokens.TOKEN_PLUS + specialCharacters[bytes.BYTE_SLASH] = tokens.TOKEN_SLASH + specialCharacters[bytes.BYTE_LEFTWING] = tokens.TOKEN_LEFTWING + specialCharacters[bytes.BYTE_RIGHTWING] = tokens.TOKEN_RIGHTWING + specialCharacters[bytes.BYTE_CIRCUMFLEX] = tokens.TOKEN_CIRCUMFLEX + specialCharacters[bytes.BYTE_ASTERISK] = tokens.TOKEN_ASTERISK + -- WoW specific + specialCharacters[bytes.BYTE_VERTICAL] = -1 + -- new as of lua 5.1 + specialCharacters[bytes.BYTE_HASH] = tokens.TOKEN_HASH + specialCharacters[bytes.BYTE_PERCENT] = tokens.TOKEN_PERCENT + + local function nextNumberExponentPartInt(text, pos) + while true do + local byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_NUMBER, pos + end + + if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then + pos = pos + 1 + else + return tokens.TOKEN_NUMBER, pos + end + end + end + + local function nextNumberExponentPart(text, pos) + local byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_NUMBER, pos + end + + if byte == bytes.BYTE_MINUS then + -- handle this case: a = 1.2e-- some comment + -- i decide to let 1.2e be parsed as a a number + byte = stringbyte(text, pos + 1) + if byte == bytes.BYTE_MINUS then + return tokens.TOKEN_NUMBER, pos + end + return nextNumberExponentPartInt(text, pos + 1) + end + + return nextNumberExponentPartInt(text, pos) + end + + local function nextNumberFractionPart(text, pos) + while true do + local byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_NUMBER, pos + end + + if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then + pos = pos + 1 + elseif byte == bytes.BYTE_E or byte == bytes.BYTE_e then + return nextNumberExponentPart(text, pos + 1) + else + return tokens.TOKEN_NUMBER, pos + end + end + end + + local function nextNumberIntPart(text, pos) + while true do + local byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_NUMBER, pos + end + + if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then + pos = pos + 1 + elseif byte == bytes.BYTE_PERIOD then + return nextNumberFractionPart(text, pos + 1) + elseif byte == bytes.BYTE_E or byte == bytes.BYTE_e then + return nextNumberExponentPart(text, pos + 1) + else + return tokens.TOKEN_NUMBER, pos + end + end + end + + local function nextIdentifier(text, pos) + while true do + local byte = stringbyte(text, pos) + + if not byte or + linebreakCharacters[byte] or + whitespaceCharacters[byte] or + specialCharacters[byte] then + return tokens.TOKEN_IDENTIFIER, pos + end + pos = pos + 1 + end + end + + -- returns false or: true, nextPos, equalsCount + local function isBracketStringNext(text, pos) + local byte = stringbyte(text, pos) + if byte == bytes.BYTE_LEFTBRACKET then + local pos2 = pos + 1 + byte = stringbyte(text, pos2) + while byte == bytes.BYTE_EQUALS do + pos2 = pos2 + 1 + byte = stringbyte(text, pos2) + end + if byte == bytes.BYTE_LEFTBRACKET then + return true, pos2 + 1, (pos2 - 1) - pos + else + return false + end + else + return false + end + end + + -- Already parsed the [==[ part when get here + local function nextBracketString(text, pos, equalsCount) + local state = 0 + while true do + local byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_STRING, pos + end + + if byte == bytes.BYTE_RIGHTBRACKET then + if state == 0 then + state = 1 + elseif state == equalsCount + 1 then + return tokens.TOKEN_STRING, pos + 1 + else + state = 0 + end + elseif byte == bytes.BYTE_EQUALS then + if state > 0 then + state = state + 1 + end + else + state = 0 + end + pos = pos + 1 + end + end + + local function nextComment(text, pos) + -- When we get here we have already parsed the "--" + -- Check for long comment + local isBracketString, nextPos, equalsCount = isBracketStringNext(text, pos) + if isBracketString then + local tokenType, nextPos2 = nextBracketString(text, nextPos, equalsCount) + return tokens.TOKEN_COMMENT_LONG, nextPos2 + end + + local byte = stringbyte(text, pos) + + -- Short comment, find the first linebreak + while true do + byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_COMMENT_SHORT, pos + end + if linebreakCharacters[byte] then + return tokens.TOKEN_COMMENT_SHORT, pos + end + pos = pos + 1 + end + end + + local function nextString(text, pos, character) + local even = true + while true do + local byte = stringbyte(text, pos) + if not byte then + return tokens.TOKEN_STRING, pos + end + + if byte == character then + if even then + return tokens.TOKEN_STRING, pos + 1 + end + end + if byte == bytes.BYTE_BACKSLASH then + even = not even + else + even = true + end + + pos = pos + 1 + end + end + + -- INPUT + -- 1: text: text to search in + -- 2: tokenPos: where to start searching + -- OUTPUT + -- 1: token type + -- 2: position after the last character of the token + local function nextToken(text, pos) + local byte = stringbyte(text, pos) + if not byte then + return nil + end + + if linebreakCharacters[byte] then + return tokens.TOKEN_LINEBREAK, pos + 1 + end + + if whitespaceCharacters[byte] then + while true do + pos = pos + 1 + byte = stringbyte(text, pos) + if not byte or not whitespaceCharacters[byte] then + return tokens.TOKEN_WHITESPACE, pos + end + end + end + + local token = specialCharacters[byte] + if token then + if token ~= -1 then + return token, pos + 1 + end + + -- WoW specific (for color codes) + if byte == bytes.BYTE_VERTICAL then + byte = stringbyte(text, pos + 1) + if byte == bytes.BYTE_VERTICAL then + return tokens.TOKEN_VERTICAL, pos + 2 + end + if byte == bytes.BYTE_c then + return tokens.TOKEN_COLORCODE_START, pos + 10 + end + if byte == bytes.BYTE_r then + return tokens.TOKEN_COLORCODE_STOP, pos + 2 + end + return tokens.TOKEN_UNKNOWN, pos + 1 + end + + if byte == bytes.BYTE_MINUS then + byte = stringbyte(text, pos + 1) + if byte == bytes.BYTE_MINUS then + return nextComment(text, pos + 2) + end + return tokens.TOKEN_MINUS, pos + 1 + end + + if byte == bytes.BYTE_SINGLE_QUOTE then + return nextString(text, pos + 1, bytes.BYTE_SINGLE_QUOTE) + end + + if byte == bytes.BYTE_DOUBLE_QUOTE then + return nextString(text, pos + 1, bytes.BYTE_DOUBLE_QUOTE) + end + + if byte == bytes.BYTE_LEFTBRACKET then + local isBracketString, nextPos, equalsCount = isBracketStringNext(text, pos) + if isBracketString then + return nextBracketString(text, nextPos, equalsCount) + else + return tokens.TOKEN_LEFTBRACKET, pos + 1 + end + end + + if byte == bytes.BYTE_EQUALS then + byte = stringbyte(text, pos + 1) + if not byte then + return tokens.TOKEN_ASSIGNMENT, pos + 1 + end + if byte == bytes.BYTE_EQUALS then + return tokens.TOKEN_EQUALITY, pos + 2 + end + return tokens.TOKEN_ASSIGNMENT, pos + 1 + end + + if byte == bytes.BYTE_PERIOD then + byte = stringbyte(text, pos + 1) + if not byte then + return tokens.TOKEN_PERIOD, pos + 1 + end + if byte == bytes.BYTE_PERIOD then + byte = stringbyte(text, pos + 2) + if byte == bytes.BYTE_PERIOD then + return tokens.TOKEN_TRIPLEPERIOD, pos + 3 + end + return tokens.TOKEN_DOUBLEPERIOD, pos + 2 + elseif byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then + return nextNumberFractionPart(text, pos + 2) + end + return tokens.TOKEN_PERIOD, pos + 1 + end + + if byte == bytes.BYTE_LESSTHAN then + byte = stringbyte(text, pos + 1) + if byte == bytes.BYTE_EQUALS then + return tokens.TOKEN_LTE, pos + 2 + end + return tokens.TOKEN_LT, pos + 1 + end + + if byte == bytes.BYTE_GREATERTHAN then + byte = stringbyte(text, pos + 1) + if byte == bytes.BYTE_EQUALS then + return tokens.TOKEN_GTE, pos + 2 + end + return tokens.TOKEN_GT, pos + 1 + end + + if byte == bytes.BYTE_TILDE then + byte = stringbyte(text, pos + 1) + if byte == bytes.BYTE_EQUALS then + return tokens.TOKEN_NOTEQUAL, pos + 2 + end + return tokens.TOKEN_TILDE, pos + 1 + end + + return tokens.TOKEN_UNKNOWN, pos + 1 + elseif byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then + return nextNumberIntPart(text, pos + 1) + else + return nextIdentifier(text, pos + 1) + end + end + + -- Cool stuff begins here! (indentation and highlighting) + + local noIndentEffect = {0, 0} + local indentLeft = {-1, 0} + local indentRight = {0, 1} + local indentBoth = {-1, 1} + + local keywords = {} + lib.keywords = keywords + keywords["and"] = noIndentEffect + keywords["break"] = noIndentEffect + keywords["false"] = noIndentEffect + keywords["for"] = noIndentEffect + keywords["if"] = noIndentEffect + keywords["in"] = noIndentEffect + keywords["local"] = noIndentEffect + keywords["nil"] = noIndentEffect + keywords["not"] = noIndentEffect + keywords["or"] = noIndentEffect + keywords["return"] = noIndentEffect + keywords["true"] = noIndentEffect + keywords["while"] = noIndentEffect + + keywords["until"] = indentLeft + keywords["elseif"] = indentLeft + keywords["end"] = indentLeft + + keywords["do"] = indentRight + keywords["then"] = indentRight + keywords["repeat"] = indentRight + keywords["function"] = indentRight + + keywords["else"] = indentBoth + + local tokenIndentation = {} + lib.tokenIndentation = tokenIndentation + tokenIndentation[tokens.TOKEN_LEFTPAREN] = indentRight + tokenIndentation[tokens.TOKEN_LEFTBRACKET] = indentRight + tokenIndentation[tokens.TOKEN_LEFTWING] = indentRight + + tokenIndentation[tokens.TOKEN_RIGHTPAREN] = indentLeft + tokenIndentation[tokens.TOKEN_RIGHTBRACKET] = indentLeft + tokenIndentation[tokens.TOKEN_RIGHTWING] = indentLeft + + local function fillWithTabs(n) + return stringrep("\t", n) + end + + local function fillWithSpaces(a, b) + return stringrep(" ", a*b) + end + + function lib.colorCodeCode(code, colorTable, caretPosition) + local stopColor = colorTable and colorTable[0] + if not stopColor then + return code, caretPosition + end + + local stopColorLen = stringlen(stopColor) + + tableclear(workingTable) + local tsize = 0 + local totalLen = 0 + + local numLines = 0 + local newCaretPosition + local prevTokenWasColored = false + local prevTokenWidth = 0 + + local pos = 1 + local level = 0 + + while true do + if caretPosition and not newCaretPosition and pos >= caretPosition then + if pos == caretPosition then + newCaretPosition = totalLen + else + newCaretPosition = totalLen + local diff = pos - caretPosition + if diff > prevTokenWidth then + diff = prevTokenWidth + end + if prevTokenWasColored then + diff = diff + stopColorLen + end + newCaretPosition = newCaretPosition - diff + end + end + + prevTokenWasColored = false + prevTokenWidth = 0 + + local tokenType, nextPos = nextToken(code, pos) + + if not tokenType then + break + end + + if tokenType == tokens.TOKEN_COLORCODE_START or tokenType == tokens.TOKEN_COLORCODE_STOP or tokenType == tokens.TOKEN_UNKNOWN then + -- ignore color codes + elseif tokenType == tokens.TOKEN_LINEBREAK or tokenType == tokens.TOKEN_WHITESPACE then + if tokenType == tokens.TOKEN_LINEBREAK then + numLines = numLines + 1 + end + local str = stringsub(code, pos, nextPos - 1) + prevTokenWidth = nextPos - pos + + tsize = tsize + 1 + workingTable[tsize] = str + totalLen = totalLen + stringlen(str) + else + local str = stringsub(code, pos, nextPos - 1) + + prevTokenWidth = nextPos - pos + + -- Add coloring + if keywords[str] then + tokenType = tokens.TOKEN_KEYWORD + end + + local color + if stopColor then + color = colorTable[str] + if not color then + color = colorTable[tokenType] + if not color then + if tokenType == tokens.TOKEN_IDENTIFIER then + color = colorTable[tokens.TOKEN_IDENTIFIER] + else + color = colorTable[tokens.TOKEN_SPECIAL] + end + end + end + end + + if color then + tsize = tsize + 1 + workingTable[tsize] = color + tsize = tsize + 1 + workingTable[tsize] = str + tsize = tsize + 1 + workingTable[tsize] = stopColor + + totalLen = totalLen + stringlen(color) + (nextPos - pos) + stopColorLen + prevTokenWasColored = true + else + tsize = tsize + 1 + workingTable[tsize] = str + + totalLen = totalLen + stringlen(str) + end + end + + pos = nextPos + end + return table.concat(workingTable), newCaretPosition, numLines + end + + function lib.indentCode(code, tabWidth, colorTable, caretPosition) + local fillFunction + if tabWidth == nil then + tabWidth = defaultTabWidth + end + if tabWidth then + fillFunction = fillWithSpaces + else + fillFunction = fillWithTabs + end + + tableclear(workingTable) + local tsize = 0 + local totalLen = 0 + + tableclear(workingTable2) + local tsize2 = 0 + local totalLen2 = 0 + + local stopColor = colorTable and colorTable[0] + local stopColorLen = not stopColor or stringlen(stopColor) + + local newCaretPosition + local newCaretPositionFinalized = false + local prevTokenWasColored = false + local prevTokenWidth = 0 + + local pos = 1 + local level = 0 + + local hitNonWhitespace = false + local hitIndentRight = false + local preIndent = 0 + local postIndent = 0 + while true do + if caretPosition and not newCaretPosition and pos >= caretPosition then + if pos == caretPosition then + newCaretPosition = totalLen + totalLen2 + else + newCaretPosition = totalLen + totalLen2 + local diff = pos - caretPosition + if diff > prevTokenWidth then + diff = prevTokenWidth + end + if prevTokenWasColored then + diff = diff + stopColorLen + end + newCaretPosition = newCaretPosition - diff + end + end + + prevTokenWasColored = false + prevTokenWidth = 0 + + local tokenType, nextPos = nextToken(code, pos) + + if not tokenType or tokenType == tokens.TOKEN_LINEBREAK then + level = level + preIndent + if level < 0 then level = 0 end + + local s = fillFunction(level, tabWidth) + + tsize = tsize + 1 + workingTable[tsize] = s + totalLen = totalLen + stringlen(s) + + if newCaretPosition and not newCaretPositionFinalized then + newCaretPosition = newCaretPosition + stringlen(s) + newCaretPositionFinalized = true + end + + for k, v in next,workingTable2 do + tsize = tsize + 1 + workingTable[tsize] = v + totalLen = totalLen + stringlen(v) + end + + if not tokenType then + break + end + + tsize = tsize + 1 + workingTable[tsize] = stringsub(code, pos, nextPos - 1) + totalLen = totalLen + nextPos - pos + + level = level + postIndent + if level < 0 then level = 0 end + + tableclear(workingTable2) + tsize2 = 0 + totalLen2 = 0 + + hitNonWhitespace = false + hitIndentRight = false + preIndent = 0 + postIndent = 0 + elseif tokenType == tokens.TOKEN_WHITESPACE then + if hitNonWhitespace then + prevTokenWidth = nextPos - pos + tsize2 = tsize2 + 1 + local s = stringsub(code, pos, nextPos - 1) + workingTable2[tsize2] = s + totalLen2 = totalLen2 + stringlen(s) + end + elseif tokenType == tokens.TOKEN_COLORCODE_START or tokenType == tokens.TOKEN_COLORCODE_STOP or tokenType == tokens.TOKEN_UNKNOWN then + -- skip these, though they shouldn't be encountered here anyway + else + hitNonWhitespace = true + local str = stringsub(code, pos, nextPos - 1) + prevTokenWidth = nextPos - pos + + -- See if this is an indent-modifier + local indentTable + if tokenType == tokens.TOKEN_IDENTIFIER then + indentTable = keywords[str] + else + indentTable = lib.tokenIndentation[tokenType] + end + + if indentTable then + if hitIndentRight then + postIndent = postIndent + indentTable[1] + indentTable[2] + else + local pre = indentTable[1] + local post = indentTable[2] + if post > 0 then + hitIndentRight = true + end + preIndent = preIndent + pre + postIndent = postIndent + post + end + end + + -- Add coloring + if keywords[str] then + tokenType = tokens.TOKEN_KEYWORD + end + + local color + if stopColor then + color = colorTable[str] + if not color then + color = colorTable[tokenType] + if not color then + if tokenType == tokens.TOKEN_IDENTIFIER then + color = colorTable[tokens.TOKEN_IDENTIFIER] + else + color = colorTable[tokens.TOKEN_SPECIAL] + end + end + end + end + + if color then + tsize2 = tsize2 + 1 + workingTable2[tsize2] = color + totalLen2 = totalLen2 + stringlen(color) + + tsize2 = tsize2 + 1 + workingTable2[tsize2] = str + totalLen2 = totalLen2 + nextPos - pos + + tsize2 = tsize2 + 1 + workingTable2[tsize2] = stopColor + totalLen2 = totalLen2 + stopColorLen + + prevTokenWasColored = true + else + tsize2 = tsize2 + 1 + workingTable2[tsize2] = str + totalLen2 = totalLen2 + nextPos - pos + + end + end + pos = nextPos + end + return table.concat(workingTable), newCaretPosition + end + + -- WoW specific code: + local GetTime = GetTime + + local editboxSetText + local editboxGetText + + -- Caret code (thanks Tem!) + local function critical_enter(editbox) + local script = editbox:GetScript("OnTextSet") + if script then + editbox:SetScript("OnTextSet", nil) + end + return script + end + + local function critical_leave(editbox, script) + if script then + editbox:SetScript("OnTextSet", script) + end + end + + local function setCaretPos_main(editbox, pos) + local text = editboxGetText(editbox) + + if stringlen(text) > 0 then + editboxSetText(editbox, stringinsert(text, pos, "a")) + editbox:HighlightText(pos, pos + 1) + editbox:Insert("\0") + end + end + + local function getCaretPos(editbox) + local script = critical_enter(editbox) + + local text = editboxGetText(editbox) + editbox:Insert("\1") + local pos = stringfind(editboxGetText(editbox), "\1", 1, 1) + editboxSetText(editbox, text) + + if pos then + setCaretPos_main(editbox, pos - 1) + end + critical_leave(editbox, script) + + return (pos or 0) - 1 + end + + local function setCaretPos(editbox, pos) + local script, script2 = critical_enter(editbox) + setCaretPos_main(editbox, pos) + critical_leave(editbox, script, script2) + end + -- end of caret code + + function lib.stripWowColors(code) + + -- HACK! + -- This is a fix for a bug, where an unfinished string causes a lot of newlines to be created. + -- The reason for the bug, is that a |r\n\n gets converted to \n\n|r after the next indent-run + -- The fix is to remove those last two linebreaks when stripping + code = stringgsub(code, "|r\n\n$", "|r") + + tableclear(workingTable) + local tsize = 0 + + local pos = 1 + + local prevVertical = false + local even = true + local selectionStart = 1 + + while true do + local byte = stringbyte(code, pos) + if not byte then + break + end + if byte == bytes.BYTE_VERTICAL then + even = not even + prevVertical = true + else + if prevVertical and not even then + if byte == bytes.BYTE_c then + + if pos - 2 >= selectionStart then + tsize = tsize + 1 + workingTable[tsize] = stringsub(code, selectionStart, pos - 2) + end + + pos = pos + 8 + selectionStart = pos + 1 + elseif byte == bytes.BYTE_r then + + if pos - 2 >= selectionStart then + tsize = tsize + 1 + workingTable[tsize] = stringsub(code, selectionStart, pos - 2) + end + selectionStart = pos + 1 + end + end + prevVertical = false + even = true + end + pos = pos + 1 + end + if pos >= selectionStart then + tsize = tsize + 1 + workingTable[tsize] = stringsub(code, selectionStart, pos - 1) + end + return table.concat(workingTable) + end + + function lib.decode(code) + if code then + code = lib.stripWowColors(code) + code = stringgsub(code, "||", "|") + end + return code or "" + end + + function lib.encode(code) + if code then + code = stringgsub(code, "|", "||") + end + return code or "" + end + + function lib.stripWowColorsWithPos(code, pos) + code = stringinsert(code, pos, "\2") + code = lib.stripWowColors(code) + pos = stringfind(code, "\2", 1, 1) + code = stringdelete(code, pos, pos) + return code, pos + end + + -- returns the padded code, and true if modified, false if unmodified + local linebreak = stringbyte("\n") + function lib.padWithLinebreaks(code) + local len = stringlen(code) + if stringbyte(code, len) == linebreak then + if stringbyte(code, len - 1) == linebreak then + return code, false + end + return code .. "\n", true + end + return code .. "\n\n", true + + end + + -- Data tables + -- No weak table magic, since editboxes can never be removed in WoW + local enabled = {} + local dirty = {} + + local editboxIndentCache = {} + local decodeCache = {} + local editboxStringCache = {} + local editboxNumLinesCache = {} + + function lib.colorCodeEditbox(editbox) + dirty[editbox] = nil + + local colorTable = editbox.faiap_colorTable or defaultColorTable + local tabWidth = editbox.faiap_tabWidth + + local orgCode = editboxGetText(editbox) + local prevCode = editboxStringCache[editbox] + if prevCode == orgCode then + return + end + + local pos = getCaretPos(editbox) + + local code + code, pos = lib.stripWowColorsWithPos(orgCode, pos) + + colorTable[0] = "|r" + + local newCode, newPos, numLines = lib.colorCodeCode(code, colorTable, pos) + newCode = lib.padWithLinebreaks(newCode) + + editboxStringCache[editbox] = newCode + if orgCode ~= newCode then + local script, script2 = critical_enter(editbox) + decodeCache[editbox] = nil + local stringlenNewCode = stringlen(newCode) + + editboxSetText(editbox, newCode) + if newPos then + if newPos < 0 then newPos = 0 end + if newPos > stringlenNewCode then newPos = stringlenNewCode end + + setCaretPos(editbox, newPos) + end + critical_leave(editbox, script, script2) + end + + if editboxNumLinesCache[editbox] ~= numLines then + lib.indentEditbox(editbox) + end + editboxNumLinesCache[editbox] = numLines + end + + function lib.indentEditbox(editbox) + dirty[editbox] = nil + + local colorTable = editbox.faiap_colorTable or defaultColorTable + local tabWidth = editbox.faiap_tabWidth + + local orgCode = editboxGetText(editbox) + local prevCode = editboxIndentCache[editbox] + if prevCode == orgCode then + return + end + + local pos = getCaretPos(editbox) + + local code + code, pos = lib.stripWowColorsWithPos(orgCode, pos) + + colorTable[0] = "|r" + local newCode, newPos = lib.indentCode(code, tabWidth, colorTable, pos) + newCode = lib.padWithLinebreaks(newCode) + editboxIndentCache[editbox] = newCode + if code ~= newCode then + local script, script2 = critical_enter(editbox) + decodeCache[editbox] = nil + + local stringlenNewCode = stringlen(newCode) + + editboxSetText(editbox, newCode) + + if newPos then + if newPos < 0 then newPos = 0 end + if newPos > stringlenNewCode then newPos = stringlenNewCode end + + setCaretPos(editbox, newPos) + end + critical_leave(editbox, script, script2) + end + end + + local function hookHandler(editbox, handler, newFun) + local oldFun = editbox:GetScript(handler) + if oldFun == newFun then + -- already hooked, ignore it + return + end + editbox["faiap_old_" .. handler] = oldFun + editbox:SetScript(handler, newFun) + end + + local function textChangedHook(editbox, ...) + local oldFun = editbox["faiap_old_OnTextChanged"] + if oldFun then + oldFun(editbox, ...) + end + if enabled[editbox] then + dirty[editbox] = GetTime() + end + end + + local function tabPressedHook(editbox, ...) + local oldFun = editbox["faiap_old_OnTabPressed"] + if oldFun then + oldFun(editbox, ...) + end + if enabled[editbox] then + return lib.indentEditbox(editbox) + end + end + + local function onUpdateHook(editbox, ...) + local oldFun = editbox["faiap_old_OnUpdate"] + if oldFun then + oldFun(editbox, ...) + end + if enabled[editbox] then + local now = GetTime() + local lastUpdate = dirty[editbox] or now + if now - lastUpdate > 0.2 then + decodeCache[editbox] = nil + return lib.colorCodeEditbox(editbox) + end + end + end + + local function newGetText(editbox) + local decoded = decodeCache[editbox] + if not decoded then + decoded = lib.decode(editboxGetText(editbox)) + decodeCache[editbox] = decoded + end + return decoded or "" + end + + local function newSetText(editbox, text) + decodeCache[editbox] = nil + if text then + local encoded = lib.encode(text) + + return editboxSetText(editbox, encoded) + end + end + + function lib.enable(editbox, colorTable, tabWidth) + if not editboxSetText then + editboxSetText = editbox.SetText + editboxGetText = editbox.GetText + end + + local modified + if editbox.faiap_colorTable ~= colorTable then + editbox.faiap_colorTable = colorTable + modified = true + end + if editbox.faiap_tabWidth ~= tabWidth then + editbox.faiap_tabWidth = tabWidth + modified = true + end + + if enabled[editbox] then + if modified then + lib.indentEditbox(editbox) + end + return + end + + -- Editbox is possibly hooked, but disabled + enabled[editbox] = true + + editbox.oldMaxBytes = editbox:GetMaxBytes() + editbox.oldMaxLetters = editbox:GetMaxLetters() + editbox:SetMaxBytes(0) + editbox:SetMaxLetters(0) + + editbox.GetText = newGetText + editbox.SetText = newSetText + + hookHandler(editbox, "OnTextChanged", textChangedHook) + hookHandler(editbox, "OnTabPressed", tabPressedHook) + hookHandler(editbox, "OnUpdate", onUpdateHook) + + lib.indentEditbox(editbox) + end + + -- Deprecated function + lib.addSmartCode = lib.enable + + function lib.disable(editbox) + if not enabled[editbox] then + return + end + enabled[editbox] = nil + + -- revert settings for max bytes / letters + editbox:SetMaxBytes(editbox.oldMaxBytes) + editbox:SetMaxLetters(editbox.oldMaxLetters) + + -- try a real unhooking, if possible + if editbox:GetScript("OnTextChanged") == textChangedHook then + editbox:SetScript("OnTextChanged", editbox.faiap_old_OnTextChanged) + editbox.faiap_old_OnTextChanged = nil + end + + if editbox:GetScript("OnTabPressed") == tabPressedHook then + editbox:SetScript("OnTabPressed", editbox.faiap_old_OnTabPressed) + editbox.faiap_old_OnTabPressed = nil + end + + if editbox:GetScript("OnUpdate") == onUpdateHook then + editbox:SetScript("OnUpdate", editbox.faiap_old_OnUpdate) + editbox.faiap_old_OnUpdate = nil + end + + editbox.GetText = nil + editbox.SetText = nil + + -- change the text back to unformatted + editbox:SetText(newGetText(editbox)) + + -- clear caches + editboxIndentCache[editbox] = nil + decodeCache[editbox] = nil + editboxStringCache[editbox] = nil + editboxNumLinesCache[editbox] = nil + end + + defaultColorTable = {} + lib.defaultColorTable = defaultColorTable + defaultColorTable[tokens.TOKEN_SPECIAL] = "|c00ff99ff" + defaultColorTable[tokens.TOKEN_KEYWORD] = "|c006666ff" + defaultColorTable[tokens.TOKEN_COMMENT_SHORT] = "|c00999999" + defaultColorTable[tokens.TOKEN_COMMENT_LONG] = "|c00999999" + + local stringColor = "|c00ffff77" + defaultColorTable[tokens.TOKEN_STRING] = stringColor + defaultColorTable[".."] = stringColor + + local tableColor = "|c00ff9900" + defaultColorTable["..."] = tableColor + defaultColorTable["{"] = tableColor + defaultColorTable["}"] = tableColor + defaultColorTable["["] = tableColor + defaultColorTable["]"] = tableColor + + local arithmeticColor = "|c0033ff55" + defaultColorTable[tokens.TOKEN_NUMBER] = arithmeticColor + defaultColorTable["+"] = arithmeticColor + defaultColorTable["-"] = arithmeticColor + defaultColorTable["/"] = arithmeticColor + defaultColorTable["*"] = arithmeticColor + + local logicColor1 = "|c0055ff88" + defaultColorTable["=="] = logicColor1 + defaultColorTable["<"] = logicColor1 + defaultColorTable["<="] = logicColor1 + defaultColorTable[">"] = logicColor1 + defaultColorTable[">="] = logicColor1 + defaultColorTable["~="] = logicColor1 + + local logicColor2 = "|c0088ffbb" + defaultColorTable["and"] = logicColor2 + defaultColorTable["or"] = logicColor2 + defaultColorTable["not"] = logicColor2 + + defaultColorTable[0] = "|r" + +end + +-- just for testing +--[[ +function testTokenizer() + local str = "" + for line in io.lines("indent.lua") do + str = str .. line .. "\n" + end + + local pos = 1 + + while true do + local tokenType, nextPos = nextToken(str, pos) + + if not tokenType then + break + end + + if true or tokenType ~= tokens.TOKEN_WHITESPACE and tokenType ~= tokens.TOKEN_LINEBREAK then + print(stringformat("Found token %d (%d-%d): (%s)", tokenType, pos, nextPos - 1, stringsub(str, pos, nextPos - 1))) + end + + if tokenType == tokens.TOKEN_UNKNOWN then + print("unknown token!") + break + end + + pos = nextPos + end +end + + +function testIndenter(i) + local lib = IndentationLib + local str = "" + for line in io.lines("test.lua") do + str = str .. line .. "\n" + end + + local colorTable = lib.defaultColorTable + print(lib.indentCode(str, 4, colorTable, i)) +end + + +testIndenter() + +--]] diff --git a/MaxDps.toc b/MaxDps.toc index 0985270..fe88d0f 100644 --- a/MaxDps.toc +++ b/MaxDps.toc @@ -1,8 +1,8 @@ ## Title: MaxDps ## Notes: Rotation helper framework. -## Version: 7.1.4.3 +## Version: 7.2.5 ## Author: Kaminaris -## Interface: 70100 +## Interface: 70200 ## SavedVariables: MaxDpsOptions ## OptionalDependencies: Bartender4, ElvUI, ButtonForge, SVUI_ActionBars @@ -17,8 +17,10 @@ Libs\AceDB-3.0\AceDB-3.0.xml Libs\AceGUI-3.0\AceGUI-3.0.xml Libs\AceGUI-3.0-SharedMediaWidgets\widget.xml Libs\AceConfig-3.0\AceConfig-3.0.xml +Libs\ForAllIndentsAndPurposes.lua core.lua buttons.lua helper.lua -timetodie.lua \ No newline at end of file +timetodie.lua +custom.lua \ No newline at end of file diff --git a/core.lua b/core.lua index 85639f2..992649d 100644 --- a/core.lua +++ b/core.lua @@ -171,6 +171,11 @@ function MaxDps:OnInitialize() LibStub('AceConfig-3.0'):RegisterOptionsTable('MaxDps', options, {'/maxdps'}); self.db = LibStub('AceDB-3.0'):New('MaxDpsOptions', defaultOptions); self.optionsFrame = LibStub('AceConfigDialog-3.0'):AddToBlizOptions('MaxDps', 'MaxDps'); + self:RegisterChatCommand('maxdps', 'ShowCustomWindow'); + + if not self.db.global.customRotations then + self.db.global.customRotations = {}; + end end MaxDps.DefaultPrint = MaxDps.Print; @@ -191,6 +196,7 @@ function MaxDps:EnableRotation() self:Print(self.Colors.Info .. 'Fetching'); self.Fetch(); + MaxDps:CheckTalents(); if self.ModuleOnEnable then self.ModuleOnEnable(); end @@ -259,7 +265,8 @@ function MaxDps:UNIT_ENTERED_VEHICLE(event, unit) end function MaxDps:UNIT_EXITED_VEHICLE(event, unit) - if unit == 'player' and self.ModuleLoaded then + if unit == 'player' then + self:InitRotations(); self:EnableRotation(); end end @@ -281,8 +288,7 @@ end function MaxDps:PLAYER_REGEN_DISABLED() if self.db.global.onCombatEnter and not self.rotationEnabled then self:Print(self.Colors.Success .. 'Auto enable on combat!'); - self:LoadModule(); - self:CheckSpecialization(); + self:InitRotations(); self:EnableRotation(); end end @@ -310,7 +316,8 @@ function MaxDps:InvokeNextSpell() -- invoke spell check local oldSkill = self.Spell; - self.Spell = self:NextSpell(); + local timeShift, currentSpell, gcd = MaxDps:EndCast(); + self.Spell = self:NextSpell(timeShift, currentSpell, gcd, self.PlayerTalents); if (oldSkill ~= self.Spell or oldSkill == nil) and self.Spell ~= nil then self:GlowNextSpellId(self.Spell); @@ -320,40 +327,48 @@ function MaxDps:InvokeNextSpell() end end -function MaxDps:LoadModule() - if self.ModuleLoaded then - return; - end +function MaxDps:InitRotations() + self:Print(self.Colors.Info .. 'Initializing rotations'); - self:Print(self.Colors.Info .. 'Loading class module'); local _, _, classId = UnitClass('player'); - if self.Classes[classId] == nil then - self:Print(_tdError, 'Invalid player class, please contact author of addon.'); + local spec = GetSpecialization(); + self.ClassId = classId; + self.Spec = spec; + + self:LoadCustomRotations(); + if self.CustomRotations[classId] and self.CustomRotations[classId][spec] then + self.CurrentRotation = self.CustomRotations[classId][spec]; + self.NextSpell = self.CurrentRotation.fn; + self:Print(self.Colors.Success .. 'Loaded Custom Rotation: ' .. self.CurrentRotation.name); + else + self:LoadModule(); + end +end + +function MaxDps:LoadModule() + if self.Classes[self.ClassId] == nil then + self:Print(self.Colors.Error .. 'Invalid player class, please contact author of addon.'); return; end - local module = 'MaxDps_' .. self.Classes[classId]; + local module = 'MaxDps_' .. self.Classes[self.ClassId]; + local _, _, _, loadable, reason = GetAddOnInfo(module); - if not IsAddOnLoaded(module) then - LoadAddOn(module); + if IsAddOnLoaded(module) then + self:Print(self.Colors.Info .. self.Description); + self:EnableRotationModule(self.Spec); + self:Print(self.Colors.Info .. 'Finished Loading class module'); + return; end - if not IsAddOnLoaded(module) then - self:Print(self.Colors.Error .. 'Could not find class module.'); + if reason == 'MISSING' or reason == 'DISABLED' then + self:Print(self.Colors.Error .. 'Could not find class module ' .. module .. ' or it was disabled.'); return; end - local mode = GetSpecialization(); + LoadAddOn(module); - self:EnableRotationModule(mode); + self:EnableRotationModule(self.Spec); self:Print(self.Colors.Info .. self.Description); - self:Print(self.Colors.Info .. 'Finished Loading class module'); - self.ModuleLoaded = true; -end - -function MaxDps:CheckSpecialization() - local mode = GetSpecialization(); - - self:EnableRotationModule(mode); end \ No newline at end of file diff --git a/custom.lua b/custom.lua new file mode 100644 index 0000000..0408224 --- /dev/null +++ b/custom.lua @@ -0,0 +1,412 @@ +local SharedMedia = LibStub('LibSharedMedia-3.0'); + +MaxDps.Specs = { + [1] = { + [1] = 'Arms', + [2] = 'Fury', + [3] = 'Protection', + }, + [2] = { + [1] = 'Holy', + [2] = 'Protection', + [3] = 'Retribution', + }, + [3] = { + [1] = 'BeastMastery', + [2] = 'Marksmanship', + [3] = 'Survival', + }, + [4] = { + [1] = 'Assassination', + [2] = 'Outlaw', + [3] = 'Subtlety', + }, + [5] = { + [1] = 'Discipline', + [2] = 'Holy', + [3] = 'Shadow', + }, + [6] = { + [1] = 'Blood', + [2] = 'Frost', + [3] = 'Unholy', + }, + [7] = { + [1] = 'Elemental', + [2] = 'Enhancement', + [3] = 'Restoration', + }, + [8] = { + [1] = 'Arcane', + [2] = 'Fire', + [3] = 'Frost', + }, + [9] = { + [1] = 'Affliction', + [2] = 'Demonology', + [3] = 'Destruction', + }, + [10] = { + [1] = 'Brewmaster', + [2] = 'Mistweaver', + [3] = 'Windwalker', + }, + [11] = { + [1] = 'Balance', + [2] = 'Feral', + [3] = 'Guardian', + [4] = 'Restoration', + }, + [12] = { + [1] = 'Havoc', + [2] = 'Vengeance', + }, +} + +MaxDps.CustomRotations = {}; + +StaticPopupDialogs['REMOVE_MAXDPS_ROTATION'] = { + text = 'Are you sure?', + button1 = 'Yes', + button2 = 'No', + OnAccept = function() + MaxDps:RemoveCustomRotation(); + end, + OnCancel = function (_,reason) + end, + whileDead = true, + hideOnEscape = true, +} + +AceGUI:RegisterLayout('2Columns3', function(content, children) + if children[1] then + children[1]:SetWidth(200) + children[1].frame:SetPoint('TOPLEFT', content, 'TOPLEFT', 0, 0) + children[1].frame:SetPoint('BOTTOMLEFT', content, 'BOTTOMLEFT', 0, 0) + children[1].frame:Show(); + + if children[1].DoLayout then + children[1]:DoLayout() + end + end + + if children[2] then + children[2].frame:SetPoint('TOPLEFT', children[1].frame, 'TOPRIGHT', 0, 0) + children[2].frame:SetPoint('RIGHT', content, 'RIGHT', 0, 0) + children[2]:SetHeight(100) + children[2].frame:Show(); + + if children[2].DoLayout then + children[2]:DoLayout() + end + end + + if children[3] then + children[3].frame:SetPoint('TOPLEFT', children[2].frame, 'BOTTOMLEFT', 0, 0) + children[3].frame:SetPoint('BOTTOMRIGHT', content, 'BOTTOMRIGHT', 0, 0) + children[3].frame:Show(); + + if children[3].DoLayout then + children[3]:DoLayout() + end + end + + if(content.obj.LayoutFinished) then + content.obj:LayoutFinished(content.obj, nil, nil); + end +end) + +function MaxDps:ShowCustomWindow() + if not self.CustomWindow then + self.CustomWindow = AceGUI:Create('Window'); + self.CustomWindow:SetTitle('MaxDps Custom Rotations'); + self.CustomWindow.frame:SetFrameStrata('DIALOG'); + self.CustomWindow:SetLayout('2Columns3'); + self.CustomWindow:SetWidth(700); + self.CustomWindow:SetHeight(550); + self.CustomWindow:EnableResize(true); + self.CustomWindow:SetCallback('OnClose', function(widget) + MaxDps:LoadCustomRotations(); + end) + + local scrollLeft = AceGUI:Create('ScrollFrame'); + scrollLeft:SetLayout('Flow'); + self.CustomWindow.scrollLeft = scrollLeft; + self.CustomWindow:AddChild(scrollLeft); + + local scrollRight = AceGUI:Create('ScrollFrame'); + scrollRight:SetLayout('Flow'); + self.CustomWindow:AddChild(scrollRight); + +-- Rotation Name + local rotationName = AceGUI:Create('EditBox'); + rotationName:SetLabel('Rotation Name'); + rotationName:SetCallback('OnTextChanged', function(self, event, text) + if not MaxDps.CurrentEditRotation then return end; + MaxDps.CurrentEditRotation.name = text; + MaxDps:UpdateCustomRotationButtons(); + end); + scrollRight:AddChild(rotationName); + self.CustomWindow.rotationName = rotationName; + +-- Rotation Class + local rotationClass = AceGUI:Create('Dropdown'); + rotationClass:SetLabel('Class'); + rotationClass:SetList(MaxDps.Classes); + rotationClass:SetCallback('OnValueChanged', function(self, event, key) + if not MaxDps.CurrentEditRotation then return end; + MaxDps.CurrentEditRotation.class = key; + local specs = MaxDps.Specs[key]; + if specs then + MaxDps.CustomWindow.rotationSpec:SetList(specs); + end + end); + scrollRight:AddChild(rotationClass); + self.CustomWindow.rotationClass = rotationClass; + +-- Rotation Spec + local rotationSpec = AceGUI:Create('Dropdown'); + rotationSpec:SetLabel('Spec'); + rotationSpec:SetCallback('OnValueChanged', function(self, event, key) + if not MaxDps.CurrentEditRotation then return end; + MaxDps.CurrentEditRotation.spec = key; + end); + scrollRight:AddChild(rotationSpec); + self.CustomWindow.rotationSpec = rotationSpec; + +-- Rotation Enabled + local rotationEnabled = AceGUI:Create('CheckBox'); + rotationEnabled:SetLabel('Enabled'); + rotationEnabled:SetCallback('OnValueChanged', function(self, event, val) + if not MaxDps.CurrentEditRotation then return end; + MaxDps.CurrentEditRotation.enabled = val; + end); + scrollRight:AddChild(rotationEnabled); + self.CustomWindow.rotationEnabled = rotationEnabled; + +-- Rotation Delete + local rotationDelete = AceGUI:Create('Button'); + rotationDelete:SetText('Remove'); + rotationDelete:SetCallback('OnClick', function() + if not MaxDps.CurrentEditRotation then return end; + StaticPopup_Show('REMOVE_MAXDPS_ROTATION'); + end); + scrollRight:AddChild(rotationDelete); + +-- Editor + local editor = AceGUI:Create('MultiLineEditBox'); + editor:SetLabel('Custom Rotation'); + editor.button:Hide(); + local fontPath = SharedMedia:Fetch('font', 'Fira Mono Medium'); + if(fontPath) then + editor.editBox:SetFont(fontPath, 12); + end + editor:SetCallback('OnTextChanged', function(self, event, value) + if not MaxDps.CurrentEditRotation then return end; + value = IndentationLib.decode(value); + if MaxDps.CurrentEditRotation then + MaxDps.CurrentEditRotation.fn = value; + end + end); + self.CustomWindow:AddChild(editor); + self.CustomWindow.editor = editor; + + IndentationLib.enable(editor.editBox, nil, 4); + + self:UpdateCustomRotationButtons(); + self:EnableDisableCustomFields(true, true); + end + self:DisableRotation(); + self.CustomWindow:Show(); +end + +function MaxDps:UpdateCustomRotationButtons() + self.CustomWindow.scrollLeft:ReleaseChildren(); + + local btn = AceGUI:Create('Button'); + + btn:SetFullWidth(true); + btn:SetText('Add Rotation'); + btn:SetHeight(40); + btn.text:SetTextColor(1, 0, 0); + btn:SetCallback('OnClick', function() + MaxDps:AddCustomRotation(); + end); + + self.CustomWindow.scrollLeft:AddChild(btn); + + for k, rotation in pairs(self.db.global.customRotations) do + local btn = AceGUI:Create('Button'); + + btn:SetFullWidth(true); + btn:SetText(rotation.name); + btn:SetHeight(40); + btn:SetCallback('OnClick', function(self, event) + for k, btn in pairs(MaxDps.CustomWindow.scrollLeft.children) do + if k > 1 then + btn.text:SetTextColor(1, 1, 1); + end + end + self.text:SetTextColor(0, 1, 0); + MaxDps:EditRotation(rotation); + end); + if self.CurrentEditRotation == rotation then + btn.text:SetTextColor(0, 1, 0); + else + btn.text:SetTextColor(1, 1, 1); + end + self.CustomWindow.scrollLeft:AddChild(btn); + end + self.CustomWindow.scrollLeft:DoLayout(); +end + +function MaxDps:AddCustomRotation() + local customRotation = { + name = 'New Rotation', + enabled = false, + class = nil, + spec = nil, + fn = "function(_, timeShift, currentSpell, gcd, talents)\n \nend", + }; + + tinsert(self.db.global.customRotations, customRotation); + self:UpdateCustomRotationButtons(); + MaxDps:EditRotation(customRotation); +end + +function MaxDps:RemoveCustomRotation() + for k, rotation in pairs(self.db.global.customRotations) do + if rotation == MaxDps.CurrentEditRotation then + self.db.global.customRotations[k] = nil; + end + end + + self.CurrentEditRotation = nil; + self:UpdateCustomRotationButtons(); + self:EnableDisableCustomFields(true, true); +end + +function MaxDps:EditRotation(rotation) + self.CurrentEditRotation = rotation; + + self.CustomWindow.rotationName:SetText(rotation.name); + self.CustomWindow.rotationEnabled:SetValue(rotation.enabled); + self.CustomWindow.rotationClass:SetValue(rotation.class); + local specs = MaxDps.Specs[rotation.class]; + if specs then + self.CustomWindow.rotationSpec:SetList(specs); + else + self.CustomWindow.rotationSpec:SetList({}); + end + self.CustomWindow.rotationSpec:SetValue(rotation.spec); + self.CustomWindow.editor:SetText(IndentationLib.encode(rotation.fn)); + self:EnableDisableCustomFields(false); +end + +function MaxDps:EnableDisableCustomFields(flag, clear) + clear = clear or false; + self.CustomWindow.rotationName:SetDisabled(flag); + self.CustomWindow.rotationEnabled:SetDisabled(flag); + self.CustomWindow.rotationClass:SetDisabled(flag); + self.CustomWindow.rotationSpec:SetDisabled(flag); + self.CustomWindow.editor:SetDisabled(flag); + if clear then + self.CustomWindow.rotationName:SetText(''); + self.CustomWindow.rotationEnabled:SetValue(false); + self.CustomWindow.rotationClass:SetValue(nil); + self.CustomWindow.rotationSpec:SetValue(nil); + self.CustomWindow.editor:SetText(''); + end +end + +function MaxDps:LoadCustomRotations() + for k,v in pairs(self.CustomRotations) do + self.CustomRotations[k] = nil; + end + + for k, rotation in pairs(self.db.global.customRotations) do + if rotation.enabled then + local fn = MaxDps.LoadFunction(rotation.fn); + if not self.CustomRotations[rotation.class] then + self.CustomRotations[rotation.class] = {} + end + + self.CustomRotations[rotation.class][rotation.spec] = { + name = rotation.name, + fn = fn + } + end + end + self:Print(self.Colors.Info .. 'Custom Rotations Loaded!'); +end + +--[[ + Borrowed from WeakAuras + + This is free software: you can redistribute it and/or modify it under the terms of + the GNU General Public License version 2 as published by the Free Software + Foundation. + + For more information see WeakAuras License +]] +local blockedFunctions = { + getfenv = true, + setfenv = true, + loadstring = true, + pcall = true, + SendMail = true, + SetTradeMoney = true, + AddTradeMoney = true, + PickupTradeMoney = true, + PickupPlayerMoney = true, + TradeFrame = true, + MailFrame = true, + EnumerateFrames = true, + RunScript = true, + AcceptTrade = true, + SetSendMailMoney = true, + EditMacro = true, + SlashCmdList = true, + DevTools_DumpCommand = true, + hash_SlashCmdList = true, + CreateMacro = true, + SetBindingMacro = true, +} + +local function forbidden() + print('|cffffff00A MaxDps just tried to use a forbidden function but has been blocked from doing so.|r'); +end + +local env_getglobal; +local exec_env = setmetatable({}, { __index = +function(t, k) + if k == '_G' then + return t; + elseif k == 'getglobal' then + return env_getglobal; + elseif blockedFunctions[k] then + return forbidden; + else + return _G[k]; + end +end +}); + +local function_cache = {}; +function MaxDps.LoadFunction(string) + if function_cache[string] then + return function_cache[string]; + else + local loadedFunction, errorString = loadstring('return ' .. string); + if errorString then + print(errorString); + else + setfenv(loadedFunction, exec_env); + local success, func = pcall(assert(loadedFunction)); + if success then + function_cache[string] = func; + return func; + end + end + end +end \ No newline at end of file