-- Path of Building -- -- Class: List Control -- Basic list control. -- -- This is an abstract base class; derived classes can supply these properties and methods to configure the list control: -- .label [Adds a label above the top left corner] -- .dragTargetList [List of controls that can receive drag events from this list control] -- .showRowSeparators [Shows separators between rows] -- :GetRowValue(column, index, value) [Required; called to retrieve the text for the given column of the given list value] -- :AddValueTooltip(index, value) [Called to add the tooltip for the given list value] -- :GetDragValue(index, value) [Called to retrieve the drag type and object for the given list value] -- :CanReceiveDrag(type, value) [Called on drag target to determine if it can receive this value] -- :ReceiveDrag(type, value, source) [Called on the drag target when a drag completes] -- :OnDragSend(index, value, target) [Called after a drag event] -- :OnOrderChange() [Called after list order is changed through dragging] -- :OnSelect(index, value) [Called when a list value is selected] -- :OnSelClick(index, value, doubleClick) [Called when a list value is clicked] -- :OnSelCopy(index, value) [Called when Ctrl+C is pressed while a list value is selected] -- :OnSelDelete(index, value) [Called when backspace or delete is pressed while a list value is selected] -- :OnSelKeyDown(index, value) [Called when any other key is pressed while a list value is selected] -- local ipairs = ipairs local t_insert = table.insert local t_remove = table.remove local m_min = math.min local m_max = math.max local m_floor = math.floor local ListClass = newClass("ListControl", "Control", "ControlHost", function(self, anchor, x, y, width, height, rowHeight, scrollH, isMutable, list) self.Control(anchor, x, y, width, height) self.ControlHost() self.rowHeight = rowHeight self.scrollH = scrollH self.isMutable = isMutable self.list = list or { } self.colList = { { } } self.tooltip = new("Tooltip") self.font = "VAR" self.controls.scrollBarH = new("ScrollBarControl", {"BOTTOM",self,"BOTTOM"}, -8, -1, 0, 16, rowHeight * 2, "HORIZONTAL") { shown = function() return self.scrollH end, width = function() local width, height = self:GetSize() return width - 18 end } self.controls.scrollBarV = new("ScrollBarControl", {"RIGHT",self,"RIGHT"}, -1, 0, 16, 0, rowHeight * 2) { y = function() return (self.scrollH and -8 or 0) end, height = function() local width, height = self:GetSize() return height - 2 - (self.scrollH and 16 or 0) end } end) function ListClass:SelectIndex(index) self.selValue = self.list[index] if self.selValue then self.selIndex = index local width, height = self:GetSize() self.controls.scrollBarV:SetContentDimension(#self.list * self.rowHeight, height - 4) self.controls.scrollBarV:ScrollIntoView((index - 2) * self.rowHeight, self.rowHeight * 3) if self.OnSelect then self:OnSelect(self.selIndex, self.selValue) end end end function ListClass:GetColumnProperty(column, property) if type(column[property]) == "function" then return column[property](self, column) else return column[property] end end function ListClass:IsMouseOver() if not self:IsShown() then return end return self:IsMouseInBounds() or self:GetMouseOverControl() end function ListClass:GetRowRegion() local width, height = self:GetSize() return { x = 2, y = self.colLabels and 20 or 2, width = width - 20, height = height - 4 - (self.scrollH and 16 or 0) - (self.colLabels and 18 or 0), } end function ListClass:Draw(viewPort) local x, y = self:GetPos() local width, height = self:GetSize() local rowHeight = self.rowHeight local list = self.list local colOffset = 0 for index, column in ipairs(self.colList) do column._offset = colOffset column._width = self:GetColumnProperty(column, "width") or (index == #self.colList and width - 20 - colOffset) or 0 colOffset = colOffset + column._width end local scrollBarV = self.controls.scrollBarV local rowRegion = self:GetRowRegion() scrollBarV:SetContentDimension(#list * rowHeight, rowRegion.height) local scrollOffsetV = scrollBarV.offset local scrollBarH = self.controls.scrollBarH local lastCol = self.colList[#self.colList] scrollBarH:SetContentDimension(lastCol._offset + lastCol._width, rowRegion.width) local scrollOffsetH = scrollBarH.offset local cursorX, cursorY = GetCursorPos() if self.selValue and self.selDragging and not self.selDragActive and (cursorX-self.selCX)*(cursorX-self.selCX)+(cursorY-self.selCY)*(cursorY-self.selCY) > 100 then self.selDragActive = true if self.dragTargetList then self.dragType, self.dragValue = self:GetDragValue(self.selIndex, self.selValue) for _, target in ipairs(self.dragTargetList) do if not target.CanReceiveDrag or target:CanReceiveDrag(self.dragType, self.dragValue) then target.otherDragSource = self end end end end self.selDragIndex = nil if (self.selDragActive or self.otherDragSource) and self.isMutable then if cursorX >= x + 2 and cursorY >= y + 2 and cursorX < x + width - 20 and cursorY < y + height - 2 then local index = math.floor((cursorY - y - 2 + scrollOffsetV) / rowHeight + 0.5) + 1 if not self.selIndex or index < self.selIndex or index > self.selIndex + 1 then self.selDragIndex = m_min(index, #list + 1) end end end if self.selDragActive and self.dragTargetList then self.dragTarget = nil for _, target in ipairs(self.dragTargetList) do if not self.dragTarget and target.otherDragSource == self and target:IsMouseOver() then self.dragTarget = target target.otherDragTargeting = true else target.otherDragTargeting = false end end end local label = self:GetProperty("label") if label then DrawString(x, y - 20, "LEFT", 16, self.font, label) end if self.otherDragSource and not self.CanDragToValue then SetDrawColor(0.2, 0.6, 0.2) elseif self.hasFocus then SetDrawColor(1, 1, 1) else SetDrawColor(0.5, 0.5, 0.5) end DrawImage(nil, x, y, width, height) if self.otherDragSource and not self.CanDragToValue then SetDrawColor(0, 0.05, 0) else SetDrawColor(0, 0, 0) end DrawImage(nil, x + 1, y + 1, width - 2, height - 2) self:DrawControls(viewPort) SetViewport(x + 2, y + 2, width - 20, height - 4 - (self.scrollH and 16 or 0)) local textOffsetY = self.showRowSeparators and 2 or 0 local textHeight = rowHeight - textOffsetY * 2 local ttIndex, ttValue, ttX, ttY, ttWidth local minIndex = m_floor(scrollOffsetV / rowHeight + 1) local maxIndex = m_min(m_floor((scrollOffsetV + height) / rowHeight + 1), #list) for colIndex, column in ipairs(self.colList) do local colFont = self:GetColumnProperty(column, "font") or "VAR" local elipWidth = DrawStringWidth(textHeight, colFont, "...") local colOffset = column._offset - scrollOffsetH local colWidth = column._width for index = minIndex, maxIndex do local lineY = rowHeight * (index - 1) - scrollOffsetV + (self.colLabels and 18 or 0) local value = list[index] local text = self:GetRowValue(colIndex, index, value) local textWidth = DrawStringWidth(textHeight, colFont, text) if textWidth > colWidth - 2 then local clipIndex = DrawStringCursorIndex(textHeight, colFont, text, colWidth - elipWidth - 2, 0) text = text:sub(1, clipIndex - 1) .. "..." textWidth = DrawStringWidth(textHeight, colFont, text) end if not scrollBarV.dragging and (not self.selDragActive or (self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource))) then local cursorX, cursorY = GetCursorPos() local relX = cursorX - (x + 2) local relY = cursorY - (y + 2) if relX >= colOffset and relX < width - 20 and relY >= 0 and relY >= lineY and relY < height - 2 - (self.scrollH and 18 or 0) and relY < lineY + rowHeight then ttIndex = index ttValue = value ttX = x + 2 + colOffset ttY = lineY + y + 2 ttWidth = m_max(textWidth + 8, relX - colOffset) end end if self.showRowSeparators then if self.hasFocus and value == self.selValue then SetDrawColor(1, 1, 1) elseif value == ttValue then SetDrawColor(0.8, 0.8, 0.8) else SetDrawColor(0.5, 0.5, 0.5) end DrawImage(nil, colOffset, lineY, colWidth, rowHeight) if (value == self.selValue or value == ttValue) then SetDrawColor(0.33, 0.33, 0.33) elseif self.otherDragSource and self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource) then SetDrawColor(0, 0.2, 0) elseif index % 2 == 0 then SetDrawColor(0.05, 0.05, 0.05) else SetDrawColor(0, 0, 0) end DrawImage(nil, colOffset, lineY + 1, colWidth, rowHeight - 2) elseif value == self.selValue or value == ttValue then if self.hasFocus and value == self.selValue then SetDrawColor(1, 1, 1) elseif value == ttValue then SetDrawColor(0.8, 0.8, 0.8) else SetDrawColor(0.5, 0.5, 0.5) end DrawImage(nil, colOffset, lineY, colWidth, rowHeight) if self.otherDragSource and self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource) then SetDrawColor(0, 0.2, 0) else SetDrawColor(0.15, 0.15, 0.15) end DrawImage(nil, colOffset, lineY + 1, colWidth, rowHeight - 2) end SetDrawColor(1, 1, 1) DrawString(colOffset, lineY + textOffsetY, "LEFT", textHeight, colFont, text) end if self.colLabels then local cursorX, cursorY = GetCursorPos() local relX = cursorX - (x + 2) local relY = cursorY - (y + 2) SetDrawColor(0.5, 0.5, 0.5) DrawImage(nil, colOffset, 1, colWidth, 18) SetDrawColor(0.15, 0.15, 0.15) DrawImage(nil, colOffset + 1, 2, colWidth - 2, 16) local label = self:GetColumnProperty(column, "label") if label and #label > 0 then SetDrawColor(1, 1, 1) DrawString(colOffset + colWidth/2, 4, "CENTER_X", 12, "VAR", label) end end end if #self.list == 0 and self.defaultText then SetDrawColor(1, 1, 1) DrawString(2, 2, "LEFT", 14, self.font, self.defaultText) end if self.selDragIndex then local lineY = rowHeight * (self.selDragIndex - 1) - scrollOffsetV SetDrawColor(1, 1, 1) DrawImage(nil, 0, lineY - 1, width - 20, 3) SetDrawColor(0, 0, 0) DrawImage(nil, 0, lineY, width - 20, 1) end SetViewport() if self.selDragActive and self.dragTargetList and (not self.isMutable or not self:IsMouseOver()) then main.showDragText = self:GetRowValue(1, self.selIndex, self.selValue) end self.hoverIndex = ttIndex self.hoverValue = ttValue if ttIndex and self.AddValueTooltip then SetDrawLayer(nil, 100) self:AddValueTooltip(self.tooltip, ttIndex, ttValue) self.tooltip:Draw(ttX, ttY, ttWidth, rowHeight, viewPort) SetDrawLayer(nil, 0) end end function ListClass:OnKeyDown(key, doubleClick) if not self:IsShown() or not self:IsEnabled() then return end local mOverControl = self:GetMouseOverControl() if mOverControl and mOverControl.OnKeyDown then return mOverControl:OnKeyDown(key) end if not self:IsMouseOver() and key:match("BUTTON") then return end if key == "LEFTBUTTON" then self.selValue = nil self.selIndex = nil local x, y = self:GetPos() local cursorX, cursorY = GetCursorPos() local rowRegion = self:GetRowRegion() if cursorX >= x + rowRegion.x and cursorY >= y + rowRegion.y and cursorX < x + rowRegion.x + rowRegion.width and cursorY < y + rowRegion.y + rowRegion.height then local index = math.floor((cursorY - y - rowRegion.y + self.controls.scrollBarV.offset) / self.rowHeight) + 1 self.selValue = self.list[index] if self.selValue then self.selIndex = index if (self.isMutable or self.dragTargetList) and self:IsShown() then self.selCX = cursorX self.selCY = cursorY self.selDragging = true self.selDragActive = false end if self.OnSelect then self:OnSelect(self.selIndex, self.selValue) end if self.OnSelClick then self:OnSelClick(self.selIndex, self.selValue, doubleClick) end end end elseif #self.list > 0 and not self.selDragActive then if key == "UP" then self:SelectIndex(((self.selIndex or 1) - 2) % #self.list + 1) elseif key == "DOWN" then self:SelectIndex((self.selIndex or #self.list) % #self.list + 1) elseif key == "HOME" then self:SelectIndex(1) elseif key == "END" then self:SelectIndex(#self.list) elseif self.selValue then if key == "c" and IsKeyDown("CTRL") then if self.OnSelCopy then self:OnSelCopy(self.selIndex, self.selValue) end elseif key == "x" and IsKeyDown("CTRL") then if self.OnSelCut then self:OnSelCut(self.selIndex, self.selValue) end elseif key == "BACK" or key == "DELETE" then if self.OnSelDelete then self:OnSelDelete(self.selIndex, self.selValue) end elseif self.OnSelKeyDown then self:OnSelKeyDown(self.selIndex, self.selValue, key) end end if self.OnAnyKeyDown then self:OnAnyKeyDown(key) end end return self end function ListClass:OnKeyUp(key) if not self:IsShown() or not self:IsEnabled() then return end if key == "WHEELDOWN" then if self.scrollH and IsKeyDown("SHIFT") then self.controls.scrollBarH:Scroll(1) else self.controls.scrollBarV:Scroll(1) end elseif key == "WHEELUP" then if self.scrollH and IsKeyDown("SHIFT") then self.controls.scrollBarH:Scroll(-1) else self.controls.scrollBarV:Scroll(-1) end elseif self.selValue then if key == "LEFTBUTTON" then self.selDragging = false if self.selDragActive then self.selDragActive = false if self.selDragIndex and self.selDragIndex ~= self.selIndex then t_remove(self.list, self.selIndex) if self.selDragIndex > self.selIndex then self.selDragIndex = self.selDragIndex - 1 end t_insert(self.list, self.selDragIndex, self.selValue) if self.OnOrderChange then self:OnOrderChange() end self.selValue = nil elseif self.dragTarget then self.dragTarget:ReceiveDrag(self.dragType, self.dragValue, self) if self.OnDragSend then self:OnDragSend(self.selIndex, self.selValue, self.dragTarget) end self.selValue = nil end if self.dragTargetList then for _, target in ipairs(self.dragTargetList) do target.otherDragSource = nil target.otherDragTargeting = false end end self.dragType = nil self.dragValue = nil self.dragTarget = nil end end end return self end