-- Path of Building -- -- Class: Edit Control -- Basic edit control. -- local m_max = math.max local m_min = math.min local m_floor = math.floor local function lastLine(str) local lastLineIndex = 1 while true do local nextLine = str:find("\n", lastLineIndex, true) if nextLine then lastLineIndex = nextLine + 1 else break end end return str:sub(lastLineIndex, -1) end local function newlineCount(str) local count = 0 local lastLineIndex = 1 while true do local nextLine = str:find("\n", lastLineIndex, true) if nextLine then count = count + 1 lastLineIndex = nextLine + 1 else return count end end end local EditClass = newClass("EditControl", "ControlHost", "Control", "UndoHandler", "TooltipHost", function(self, anchor, x, y, width, height, init, prompt, filter, limit, changeFunc, lineHeight) self.ControlHost() self.Control(anchor, x, y, width, height) self.UndoHandler() self.TooltipHost() self:SetText(init or "") self.prompt = prompt self.filter = filter or "^%w%p " self.filterPattern = "["..self.filter.."]" self.limit = limit self.changeFunc = changeFunc self.lineHeight = lineHeight self.font = "VAR" self.textCol = "^7" self.inactiveCol = "^8" self.disableCol = "^9" self.selCol = "^0" self.selBGCol = "^xBBBBBB" self.blinkStart = GetTime() if self.filter == "%D" or self.filter == "^%-%d" then -- Add +/- buttons for integer number edits self.isNumeric = true local function buttonSize() local width, height = self:GetSize() return height - 4 end self.controls.buttonDown = new("ButtonControl", {"RIGHT",self,"RIGHT"}, -2, 0, buttonSize, buttonSize, "-", function() self:OnKeyUp("DOWN") end) self.controls.buttonUp = new("ButtonControl", {"RIGHT",self.controls.buttonDown,"LEFT"}, -1, 0, buttonSize, buttonSize, "+", function() self:OnKeyUp("UP") end) end self.controls.scrollBarH = new("ScrollBarControl", {"BOTTOMLEFT",self,"BOTTOMLEFT"}, 1, -1, 0, 14, 60, "HORIZONTAL", true) self.controls.scrollBarH.width = function() local width, height = self:GetSize() return width - (self.controls.scrollBarV.enabled and 16 or 2) end self.controls.scrollBarV = new("ScrollBarControl", {"TOPRIGHT",self,"TOPRIGHT"}, -1, 1, 14, 0, (lineHeight or 0) * 3, "VERTICAL", true) self.controls.scrollBarV.height = function() local width, height = self:GetSize() return height - (self.controls.scrollBarH.enabled and 16 or 2) end if not lineHeight then self.controls.scrollBarH.shown = false self.controls.scrollBarV.shown = false end end) function EditClass:SetText(text, notify) self.buf = tostring(text) self.caret = #self.buf + 1 self.sel = nil if notify and self.changeFunc then self.changeFunc(self.buf) end self:ResetUndo() end function EditClass:IsMouseOver() if not self:IsShown() then return false end return self:IsMouseInBounds() or self:GetMouseOverControl() end function EditClass:SelectAll() self.caret = #self.buf + 1 self.sel = 1 self:ScrollCaretIntoView() end function EditClass:ReplaceSel(text) text = text:gsub("\r","") if text:match(self.filterPattern) then return end local left = m_min(self.caret, self.sel) local right = m_max(self.caret, self.sel) local newBuf = self.buf:sub(1, left - 1) .. text .. self.buf:sub(right) if self.limit and #newBuf > self.limit then return end self.buf = newBuf self.caret = left + #text self.sel = nil self:ScrollCaretIntoView() self.blinkStart = GetTime() if self.changeFunc then self.changeFunc(self.buf) end self:AddUndoState() end function EditClass:Insert(text) text = text:gsub("\r","") if text:match(self.filterPattern) then return end local newBuf = self.buf:sub(1, self.caret - 1) .. text .. self.buf:sub(self.caret) if self.limit and #newBuf > self.limit then return end self.buf = newBuf self.caret = self.caret + #text self.sel = nil self:ScrollCaretIntoView() self.blinkStart = GetTime() if self.changeFunc then self.changeFunc(self.buf) end self:AddUndoState() end function EditClass:UpdateScrollBars() local width, height = self:GetSize() local textHeight = self.lineHeight or (height - 4) if self.lineHeight then self.controls.scrollBarH:SetContentDimension(DrawStringWidth(textHeight, self.font, self.buf) + 2, width - 18) self.controls.scrollBarV:SetContentDimension(newlineCount(self.buf.."\n") * textHeight, height - (self.controls.scrollBarH.enabled and 18 or 4)) else self.controls.scrollBarH:SetContentDimension(DrawStringWidth(textHeight, self.font, self.buf) + 2, width - 4 - (self.prompt and DrawStringWidth(textHeight, self.font, self.prompt) + textHeight/2 or 0)) end end function EditClass:ScrollCaretIntoView() local width, height = self:GetSize() local textHeight = self.lineHeight or (height - 4) local pre = self.buf:sub(1, self.caret - 1) local caretX = DrawStringWidth(textHeight, self.font, lastLine(pre)) self:UpdateScrollBars() self.controls.scrollBarH:ScrollIntoView(caretX - textHeight, textHeight * 2) if self.lineHeight then local caretY = newlineCount(pre) * textHeight self.controls.scrollBarV:ScrollIntoView(caretY, textHeight) end end function EditClass:MoveCaretVertically(offset) local pre = self.buf:sub(1, self.caret - 1) local caretX = DrawStringWidth(self.lineHeight, self.font, lastLine(pre)) local caretY = newlineCount(pre) * self.lineHeight self.caret = DrawStringCursorIndex(self.lineHeight, self.font, self.buf, caretX + 1, caretY + self.lineHeight/2 + offset) self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() self.blinkStart = GetTime() end function EditClass:Draw(viewPort) local x, y = self:GetPos() local width, height = self:GetSize() local enabled = self:IsEnabled() local mOver = self:IsMouseOver() if not enabled then SetDrawColor(0.33, 0.33, 0.33) elseif mOver then SetDrawColor(1, 1, 1) else SetDrawColor(0.5, 0.5, 0.5) end DrawImage(nil, x, y, width, height) if not enabled then SetDrawColor(0, 0, 0) elseif self.hasFocus or mOver then if self.lineHeight then SetDrawColor(0.1, 0.1, 0.1) else SetDrawColor(0.15, 0.15, 0.15) end else SetDrawColor(0, 0, 0) end DrawImage(nil, x + 1, y + 1, width - 2, height - 2) local textX = x + 2 local textY = y + 2 local textHeight = self.lineHeight or (height - 4) if self.prompt then if not enabled then DrawString(textX, textY, "LEFT", textHeight, self.font, self.disableCol..self.prompt) else DrawString(textX, textY, "LEFT", textHeight, self.font, self.textCol..self.prompt..":") end textX = textX + DrawStringWidth(textHeight, self.font, self.prompt) + textHeight/2 end if not enabled then return end if mOver then SetDrawLayer(nil, 100) self:DrawTooltip(x, y, width, height, viewPort) SetDrawLayer(nil, 0) end self:UpdateScrollBars() local marginL = textX - x - 2 local marginR = self.controls.scrollBarV:IsShown() and 14 or 0 local marginB = self.controls.scrollBarH:IsShown() and 14 or 0 SetViewport(textX, textY, width - 4 - marginL - marginR, height - 4 - marginB) if not self.hasFocus then SetDrawColor(self.inactiveCol) DrawString(-self.controls.scrollBarH.offset, -self.controls.scrollBarV.offset, "LEFT", textHeight, self.font, self.buf) SetViewport() self:DrawControls(viewPort) return end if not IsKeyDown("LEFTBUTTON") then self.drag = false end if self.drag then local cursorX, cursorY = GetCursorPos() self.caret = DrawStringCursorIndex(textHeight, self.font, self.buf, cursorX - textX + self.controls.scrollBarH.offset, cursorY - textY + self.controls.scrollBarV.offset) self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() end textX = -self.controls.scrollBarH.offset textY = -self.controls.scrollBarV.offset if self.lineHeight then local left = m_min(self.caret, self.sel or self.caret) local right = m_max(self.caret, self.sel or self.caret) local caretX SetDrawColor(self.textCol) for s, line, e in (self.buf.."\n"):gmatch("()([^\n]*)\n()") do textX = -self.controls.scrollBarH.offset if left >= e or right <= s then DrawString(textX, textY, "LEFT", textHeight, self.font, line) end if left < e then if left > s then local pre = line:sub(1, left - s) DrawString(textX, textY, "LEFT", textHeight, self.font, pre) textX = textX + DrawStringWidth(textHeight, self.font, pre) end if left >= s and left == self.caret then caretX, caretY = textX, textY end end if left ~= right and left < e and right > s then local sel = self.selCol .. StripEscapes(line:sub(m_max(1, left - s + 1), m_min(#line, right - s))) if right >= e then sel = sel .. " " end local selWidth = DrawStringWidth(textHeight, self.font, sel) SetDrawColor(self.selBGCol) DrawImage(nil, textX, textY, selWidth, textHeight) DrawString(textX, textY, "LEFT", textHeight, self.font, sel) SetDrawColor(self.textCol) textX = textX + selWidth end if right >= s and right < e and right == self.caret then caretX, caretY = textX, textY end if right > s then if right < e then local post = line:sub(right - s + 1) DrawString(textX, textY, "LEFT", textHeight, self.font, post) textX = textX + DrawStringWidth(textHeight, self.font, post) end end textY = textY + textHeight end if caretX then if (GetTime() - self.blinkStart) % 1000 < 500 then SetDrawColor(self.textCol) DrawImage(nil, caretX, caretY, 1, textHeight) end end elseif self.sel and self.sel ~= self.caret then local left = m_min(self.caret, self.sel) local right = m_max(self.caret, self.sel) local pre = self.textCol .. self.buf:sub(1, left - 1) local sel = self.selCol .. StripEscapes(self.buf:sub(left, right - 1)) local post = self.textCol .. self.buf:sub(right) DrawString(textX, textY, "LEFT", textHeight, self.font, pre) textX = textX + DrawStringWidth(textHeight, self.font, pre) local selWidth = DrawStringWidth(textHeight, self.font, sel) SetDrawColor(self.selBGCol) DrawImage(nil, textX, textY, selWidth, textHeight) DrawString(textX, textY, "LEFT", textHeight, self.font, sel) DrawString(textX + selWidth, textY, "LEFT", textHeight, self.font, post) if (GetTime() - self.blinkStart) % 1000 < 500 then local caretX = (self.caret > self.sel) and textX + selWidth or textX SetDrawColor(self.textCol) DrawImage(nil, caretX, textY, 1, textHeight) end else local pre = self.textCol .. self.buf:sub(1, self.caret - 1) local post = self.buf:sub(self.caret) DrawString(textX, textY, "LEFT", textHeight, self.font, pre) textX = textX + DrawStringWidth(textHeight, self.font, pre) DrawString(textX, textY, "LEFT", textHeight, self.font, post) if (GetTime() - self.blinkStart) % 1000 < 500 then SetDrawColor(self.textCol) DrawImage(nil, textX, textY, 1, textHeight) end end SetViewport() self:DrawControls(viewPort) end function EditClass:OnFocusGained() self.blinkStart = GetTime() if not self.drag and not self.selControl and not self.lineHeight then self:SelectAll() end end function EditClass:OnKeyDown(key, doubleClick) if not self:IsShown() or not self:IsEnabled() then return end local mOverControl = self:GetMouseOverControl() if mOverControl and mOverControl.OnKeyDown then self.selControl = mOverControl return mOverControl:OnKeyDown(key) and self else self.selControl = nil end local shift = IsKeyDown("SHIFT") local ctrl = IsKeyDown("CTRL") if key == "LEFTBUTTON" then if not self.Object:IsMouseOver() then return end if doubleClick then if self.lineHeight then if self.buf:sub(self.caret - 1, self.caret):match("^%C\n$") then self.caret = self.caret - 1 end while self.buf:sub(self.caret - 1, self.caret):match("[^\n][ \t]") do self.caret = self.caret - 1 end local caretChar = self.buf:sub(self.caret, self.caret) if caretChar:match("%w") then self.sel = self.caret while self.buf:sub(self.sel - 1, self.sel - 1):match("%w") do self.sel = self.sel - 1 end while self.buf:sub(self.caret, self.caret):match("%w") do self.caret = self.caret + 1 end elseif caretChar:match("%S") then self.sel = self.caret while self.buf:sub(self.sel - 1, self.sel - 1) == caretChar do self.sel = self.sel - 1 end while self.buf:sub(self.caret, self.caret) == caretChar do self.caret = self.caret + 1 end end else self.sel = 1 self.caret = #self.buf + 1 end self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() else self.drag = true local x, y = self:GetPos() local width, height = self:GetSize() local textX = x + 2 local textY = y + 2 local textHeight = self.lineHeight or (height - 4) if self.prompt then textX = textX + DrawStringWidth(textHeight, self.font, self.prompt) + textHeight/2 end local cursorX, cursorY = GetCursorPos() self.caret = DrawStringCursorIndex(textHeight, self.font, self.buf, cursorX - textX + self.controls.scrollBarH.offset, cursorY - textY + self.controls.scrollBarV.offset) self.sel = self.caret self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() self.blinkStart = GetTime() end elseif key == "ESCAPE" then return elseif key == "RETURN" then if self.lineHeight then self:Insert("\n") else if self.enterFunc then self.enterFunc(self.buf) end return end elseif key == "a" and ctrl then self:SelectAll() elseif (key == "c" or key == "x") and ctrl then if self.sel and self.sel ~= self.caret then local left = m_min(self.caret, self.sel) local right = m_max(self.caret, self.sel) Copy(self.buf:sub(left, right - 1)) if key == "x" then self:ReplaceSel("") end end elseif key == "v" and ctrl then local text = Paste() if text then if self.pasteFilter then text = self.pasteFilter(text) end text = text:gsub("[\128-\255]","?") if self.sel and self.sel ~= self.caret then self:ReplaceSel(text) else self:Insert(text) end end elseif key == "z" and ctrl then self:Undo() elseif key == "y" and ctrl then self:Redo() elseif key == "LEFT" then self.sel = shift and (self.sel or self.caret) or nil if self.caret > 1 then self.caret = self.caret - 1 self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() self.blinkStart = GetTime() end elseif key == "RIGHT" then self.sel = shift and (self.sel or self.caret) or nil if self.caret <= #self.buf then self.caret = self.caret + 1 self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() self.blinkStart = GetTime() end elseif key == "UP" and self.lineHeight then self.sel = shift and (self.sel or self.caret) or nil self:MoveCaretVertically(-self.lineHeight) elseif key == "DOWN" and self.lineHeight then self.sel = shift and (self.sel or self.caret) or nil self:MoveCaretVertically(self.lineHeight) elseif key == "HOME" then self.sel = shift and (self.sel or self.caret) or nil if self.lineHeight and not ctrl then self.caret = self.caret - #lastLine(self.buf:sub(1, self.caret - 1)) else self.caret = 1 end self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() self.blinkStart = GetTime() elseif key == "END" then self.sel = shift and (self.sel or self.caret) or nil if self.lineHeight and not ctrl then self.caret = self.caret + #self.buf:sub(self.caret, -1):match("[^\n]*") else self.caret = #self.buf + 1 end self.lastUndoState.caret = self.caret self:ScrollCaretIntoView() self.blinkStart = GetTime() elseif key == "PAGEUP" and self.lineHeight then self.sel = shift and (self.sel or self.caret) or nil local width, height = self:GetSize() self:MoveCaretVertically(-height + 18) elseif key == "PAGEDOWN" and self.lineHeight then self.sel = shift and (self.sel or self.caret) or nil local width, height = self:GetSize() self:MoveCaretVertically(height - 18) elseif key == "BACK" then if self.sel and self.sel ~= self.caret then self:ReplaceSel("") elseif self.caret > 1 then local len = 1 if IsKeyDown("CTRL") then while self.caret - len > 1 and self.buf:sub(self.caret - len, self.caret - len):match("%s") and not self.buf:sub(self.caret - len - 1, self.caret - len - 1):match("\n") do len = len + 1 end if self.buf:sub(self.caret - len, self.caret - len):match("%w") then while self.caret - len > 1 and self.buf:sub(self.caret - len - 1, self.caret - len - 1):match("%w") do len = len + 1 end end end self.buf = self.buf:sub(1, self.caret - 1 - len) .. self.buf:sub(self.caret) self.caret = self.caret - len self.sel = nil self:ScrollCaretIntoView() self.blinkStart = GetTime() if self.changeFunc then self.changeFunc(self.buf) end self:AddUndoState() end elseif key == "DELETE" then if self.sel and self.sel ~= self.caret then self:ReplaceSel("") elseif self.caret <= #self.buf then local len = 1 if IsKeyDown("CTRL") then while self.caret + len <= #self.buf and self.buf:sub(self.caret + len - 1, self.caret + len - 1):match("%s") and not self.buf:sub(self.caret + len, self.caret + len):match("\n") do len = len + 1 end if self.buf:sub(self.caret + len - 1, self.caret + len - 1):match("%w") then while self.caret + len <= #self.buf and self.buf:sub(self.caret + len, self.caret + len):match("%w") do len = len + 1 end end end self.buf = self.buf:sub(1, self.caret - 1) .. self.buf:sub(self.caret + len) self.sel = nil self.blinkStart = GetTime() if self.changeFunc then self.changeFunc(self.buf) end self:AddUndoState() end elseif key == "TAB" then return self.Object:TabAdvance(shift and -1 or 1) end return self end function EditClass:OnKeyUp(key) if not self:IsShown() or not self:IsEnabled() then return end if self.selControl then local newSel = self.selControl:OnKeyUp(key) if newSel then return self else self.selControl = nil end end if key == "LEFTBUTTON" then if self.drag then self.drag = false end elseif self.isNumeric then local cur = tonumber(self.buf) if key == "WHEELUP" or key == "UP" then if cur then self:SetText(tostring(cur + (self.numberInc or 1)), true) else self:SetText("1", true) end elseif key == "WHEELDOWN" or key == "DOWN" then if cur and (self.filter ~= "%D" or cur > 0 )then self:SetText(tostring(cur - (self.numberInc or 1)), true) else self:SetText("0", true) end end elseif key == "WHEELUP" then if self.controls.scrollBarV.enabled then self.controls.scrollBarV:Scroll(-1) else self.controls.scrollBarH:Scroll(-1) end elseif key == "WHEELDOWN" then if self.controls.scrollBarV.enabled then self.controls.scrollBarV:Scroll(1) else self.controls.scrollBarH:Scroll(1) end end return self.hasFocus and self end function EditClass:OnChar(key) if not self:IsShown() or not self:IsEnabled() then return end if key ~= '\b' then if self.sel and self.sel ~= self.caret then self:ReplaceSel(key) else self:Insert(key) end end return self end function EditClass:CreateUndoState() local state = { buf = self.buf, caret = self.caret, } self.lastUndoState = state return state end function EditClass:RestoreUndoState(state) self.buf = state.buf self.caret = state.caret self.sel = nil self:ScrollCaretIntoView() if self.changeFunc then self.changeFunc(self.buf) end self.lastUndoState = state end