local CONTROL_MARGIN_RIGHT = 5 local LINE_MARGIN = 5 local LINE_HEIGHT = 16 local g_gridListContents = {} -- info about binded gridlists local g_openedWindows = {} -- {window1table = true, window2table = true, ...} local g_protectedElements = {} local GRIDLIST_UPDATE_CHUNK_SIZE = 10 local classInfo = { wnd = {className = 'Window', padding = {25, 10, 10, 10}, isContainer = true}, tbp = {className = 'TabPanel'}, tab = {className = 'Tab', padding = 10, isContainer = true}, lbl = {className = 'Label', height = 20}, btn = {className = 'Button', height = 20, padding = {0, 4}}, chk = {className = 'CheckBox', height = 20, padding = {0, 6}}, rad = {className = 'RadioButton', height = 20, padding = {0, 10}}, txt = {className = 'Edit', width=100, height = 24}, lst = {className = 'GridList', width = 250, height = 400}, img = {className = 'StaticImage'} } function createWindow(wnd, rebuild) if wnd.element then if rebuild then destroyElement(wnd.element) else guiSetVisible(wnd.element, true) guiBringToFront(wnd.element) g_openedWindows[wnd] = true if wnd.oncreate then wnd.oncreate() end return end end _planWindow(wnd) _buildWindow(wnd) end local function onProtectedClick(btn,state,x,y) if isFunctionOnCD(g_protectedElements[source]) then return end g_protectedElements[source](btn,state,x,y) end function _planWindow(wnd, baseWnd, parentWnd, x, y, maxHeightInLine) -- simulate building a window to get the proper height local wndClass = wnd[1] if not maxHeightInLine then maxHeightInLine = LINE_HEIGHT end local text, padding, parentPadding if wndClass ~= 'br' then padding = classInfo[wndClass].padding if type(padding) == 'number' then padding = table.rep(padding, 4) classInfo[wndClass].padding = padding elseif type(padding) == 'table' then if #padding == 1 then padding = table.rep(padding[1], 4) classInfo[wndClass].padding = padding elseif #padding == 2 then padding = table.flatten(table.rep(padding, 2)) classInfo[wndClass].padding = padding elseif #padding == 3 then table.insert(padding, padding[2]) classInfo[wndClass].padding = padding end elseif not padding then padding = table.rep(0, 4) classInfo[wndClass].padding = padding end text = wnd.text or wnd.id or '' if not wnd.width then wnd.width = (classInfo[wndClass].width or (8*text:len()) + (not classInfo[wndClass].isContainer and (padding[2] + padding[4]) or 0)) end if not wnd.height and not classInfo[wndClass].isContainer then wnd.height = (classInfo[wndClass].height or LINE_HEIGHT) + padding[1] + padding[3] end end parentPadding = parentWnd and classInfo[parentWnd[1]].padding if wndClass == 'br' or (not classInfo[wndClass].isContainer and x + wnd.width > parentWnd.width - parentPadding[2]) then -- line wrap x = parentPadding[4] y = y + maxHeightInLine + LINE_MARGIN maxHeightInLine = LINE_HEIGHT if wndClass == 'br' then return nil, x, y, maxHeightInLine end end if not wnd.x then wnd.x = x end if not wnd.y then wnd.y = y end wnd.parent = parentWnd if wnd.controls then local childX, childY = padding[4], padding[1] local childMaxHeightInLine = LINE_HEIGHT for id, controlwnd in pairs(wnd.controls) do _, childX, childY, childMaxHeightInLine = _planWindow(controlwnd, baseWnd or wnd, wnd, childX, childY, childMaxHeightInLine) end if classInfo[wndClass].isContainer then wnd.height = childY + childMaxHeightInLine + padding[3] end end if wnd.tabs then local maxTabHeight = 0 for id, tab in pairs(wnd.tabs) do tab[1] = 'tab' tab.width = wnd.width _planWindow(tab, baseWnd, wnd) if tab.height > maxTabHeight then maxTabHeight = tab.height end end wnd.height = maxTabHeight end if classInfo[wndClass].isContainer then return elem else if wnd.height > maxHeightInLine then maxHeightInLine = wnd.height end return elem, x + wnd.width + CONTROL_MARGIN_RIGHT, y, maxHeightInLine end end function _buildWindow(wnd, baseWnd, parentWnd) local wndClass = wnd[1] if wndClass == 'br' then return end local relX, relY, relWidth, relHeight if parentWnd then if wnd.x and wnd.y then relX = wnd.x/parentWnd.width relY = wnd.y/parentWnd.height end relWidth = wnd.width / parentWnd.width relHeight = wnd.height / parentWnd.height end local elem if wndClass == 'wnd' then local screenWidth, screenHeight = guiGetScreenSize() if not wnd.x then wnd.x = screenWidth/2 - wnd.width/2 else local i, f = math.modf(wnd.x) if f ~= 0 then wnd.x = screenWidth * wnd.x end if wnd.x < 0 then wnd.x = screenWidth - math.abs(wnd.x) - wnd.width end end if not wnd.y then wnd.y = screenHeight/2 - wnd.height/2 else local i, f = math.modf(wnd.y) if f ~= 0 then wnd.y = screenHeight * wnd.y end if wnd.y < 0 then wnd.y = screenHeight - math.abs(wnd.y) - wnd.height end end elem = guiCreateWindow(wnd.x, wnd.y, wnd.width, wnd.height, wnd.text, false) guiWindowSetSizable(elem, false) guiBringToFront(elem) g_openedWindows[wnd] = true elseif wndClass == 'chk' then elem = guiCreateCheckBox(relX, relY, relWidth, relHeight, wnd.text or wnd.id or '', false, true, parentWnd.element) elseif wndClass == 'tbp' then elem = guiCreateTabPanel(relX, relY, relWidth, relHeight, true, parentWnd.element) elseif wndClass == 'tab' then elem = guiCreateTab(text, parentWnd.element) elseif wndClass == 'lst' then elem = guiCreateGridList(relX, relY, relWidth, relHeight, true, parentWnd.element) if wnd.columns then guiGridListSetSortingEnabled(elem, false) for i, column in ipairs(wnd.columns) do guiGridListAddColumn(elem, column.text or column.attr or '', column.width or 0.9) end end elseif wndClass == 'img' then elem = guiCreateStaticImage(relX, relY, relWidth, relHeight, wnd.src or '', true, parentWnd.element) elseif wndClass == 'txt' then elem = guiCreateEdit(relX, relY, relWidth, relHeight, wnd.text or '', true, parentWnd.element) if wnd.onchanged then addEventHandler('onClientGUIChanged', elem, wnd.onchanged) end else elem = _G['guiCreate' .. classInfo[wndClass].className](relX, relY, relWidth, relHeight, wnd.text or wnd.id or '', true, parentWnd.element) if wnd.align and wndClass == 'lbl' then guiLabelSetHorizontalAlign(elem, wnd.align, true) end end wnd.element = elem if wnd.controls then for id, controlwnd in pairs(wnd.controls) do _buildWindow(controlwnd, baseWnd or wnd, wnd) end end if wnd.tabs then for id, tab in pairs(wnd.tabs) do _buildWindow(tab, baseWnd, wnd) end end if wnd.rows then if wnd.rows.xml then -- get rows from xml bindGridListToTable(wnd, not gridListHasCache(wnd) and xmlToTable(wnd.rows.xml, wnd.rows.attrs) or false, wnd.expandlastlevel or wnd.expandlastlevel == nil) else -- rows hardcoded in window definition bindGridListToTable(wnd, not gridListHasCache(wnd) and wnd.rows or false, false) end end local clickhandler = nil if wnd.onclick then if wndClass == 'img' then clickhandler = function(btn, state, x, y) local imgX, imgY = getControlScreenPos(wnd) wnd.onclick((x - imgX)/wnd.width, (y - imgY)/wnd.height, btn) end else clickhandler = function() wnd.onclick() end end elseif wnd.window then clickhandler = function() toggleWindow(wnd.window) end elseif wnd.inputbox then clickhandler = function() wndInput = { 'wnd', width = 170, height = 60, controls = { {'txt', id='input', text='', width=60}, {'btn', id='ok', onclick=function() wnd.inputbox.callback(getControlText(wndInput, 'input')) closeWindow(wndInput) end}, {'btn', id='cancel', closeswindow=true} } } for propname, propval in pairs(wnd.inputbox) do wndInput[propname] = propval end createWindow(wndInput) guiBringToFront(getControl(wndInput, 'input')) end elseif wnd.closeswindow then clickhandler = function() closeWindow(baseWnd) end end if clickhandler then if wnd.ClickSpamProtected then g_protectedElements[elem] = clickhandler addEventHandler('onClientGUIClick', elem, onProtectedClick, false) else addEventHandler('onClientGUIClick', elem, clickhandler, false) end end if wnd.ondoubleclick then local doubleclickhandler if wndClass == 'img' then doubleclickhandler = function(btn, state, x, y) local imgX, imgY = getControlScreenPos(wnd) wnd.ondoubleclick((x - imgX)/wnd.width, (y - imgY)/wnd.height) end else doubleclickhandler = wnd.ondoubleclick end if wnd.DoubleClickSpamProtected then g_protectedElements[elem] = doubleclickhandler addEventHandler('onClientGUIDoubleClick', elem, onProtectedClick, false) else addEventHandler('onClientGUIDoubleClick', elem, doubleclickhandler, false) end end if wnd.oncreate then wnd.oncreate() end end function isWindowOpen(wnd) return wnd.element and guiGetVisible(wnd.element) end function getControlScreenPos(wnd) local x, y = 0, 0 local curX, curY local curWnd = wnd while curWnd do curX, curY = guiGetPosition(curWnd.element, false) x = x + curX y = y + curY curWnd = curWnd.parent end return x, y end function closeWindow(...) -- closeWindow(window1, window2, ...) local args = { ... } for i=1,#args do g_openedWindows[args[i]] = nil if not args[i].element then return end if args[i].onclose then args[i].onclose() end guiSetVisible(args[i].element, false) end if not isAnyWindowOpen() then showCursor(false) end end function toggleWindow(...) local args = { ... } for i=1,#args do if isWindowOpen(args[i]) then closeWindow(args[i]) else createWindow(args[i]) end end end function isAnyWindowOpen() for wnd,_ in pairs(g_openedWindows) do if isWindowOpen(wnd) then return true end end return false end function hideAllWindows() for wnd,_ in pairs(g_openedWindows) do guiSetVisible(wnd.element, false) end end function showAllWindows() for wnd,_ in pairs(g_openedWindows) do guiSetVisible(wnd.element, true) end end function showControls(wnd, ...) for i,ctrlName in ipairs({ ... }) do guiSetVisible(getControl(wnd, ctrlName), true) end end function hideControls(wnd, ...) for i,ctrlName in ipairs({ ... }) do guiSetVisible(getControl(wnd, ctrlName), false) end end function getControlData(...) local lookIn local currentData local args = { ... } if type(args[1]) == 'string' then currentData = _G[args[1]] else currentData = args[1] end for i=2,#args do if args[i] == '..' then currentData = currentData.parent else lookIn = currentData.controls or currentData.tabs if not lookIn then return false end currentData = false for j,control in pairs(lookIn) do if control.id == args[i] then currentData = control break end end if not currentData then return false end end end return currentData end function getControl(...) if type(({...})[1]) == 'userdata' then -- if a control element was passed return ({...})[1] end local data = getControlData(...) if not data then return false end return data.element end function getControlText(...) return guiGetText(getControl(...)) end function getControlNumber(...) return tonumber(guiGetText(getControl(...))) end function getControlNumbers(...) -- getControlNumbers(..., controlnames) -- ... = path to parent window -- controlnames: array of control names -- returns a list value1, value2, ... with the numbers entered in the specified controls local args = {...} local controlnames = table.remove(args) local result = {} for i,name in ipairs(controlnames) do result[i] = getControlNumber(unpack(args), name) end return unpack(result) end function setControlText(...) local args = {...} local text = table.remove(args) local element = getControl(unpack(args)) if (element and isElement(element) and text) then guiSetText(element, text) end end function setControlNumber(...) local args = {...} local num = table.remove(args) guiSetText(getControl(unpack(args)), tostring(num)) end function setControlNumbers(...) -- setControlNumbers(..., controlvalues) -- ... = path to parent window -- controlvalues: a table {controlname = value, ...} with the numbers to set the control texts to local args = {...} local controlvalues = table.remove(args) for name,value in pairs(controlvalues) do setControlNumber(unpack(args), name, value) end end function getControlBaseWindow(...) local control = getControlData(...) while control.parent do control = control.parent end return control end function appendControl(...) -- appendControl(..., newChildControlData) local args = {...} local newChild = table.remove(args) local parent = getControlData(unpack(args)) if not parent.controls then parent.controls = {} end table.insert(parent.controls, newChild) local wnd = getControlBaseWindow(parent) local visible = isWindowOpen(wnd) createWindow(wnd, true) if not visible then guiSetVisible(wnd.element, false) end return newChild.element end function getSelectedGridListData(...) -- Returns the data associated with the first item of the selected row in a grid list local list = getControl(...) local selID = guiGridListGetSelectedItem(list) if not selID then return false end return guiGridListGetItemData(list, selID, 1), selID end function getSelectedGridListItem(...) -- getSelectedGridListItem(...[, column]) -- Returns the text of the specified item in the selected row of a grid list. -- The column parameter may be omitted and in that case defaults to 1. local list local column local args = {...} if type(arg[arg.n]) == 'number' then column = table.remove(args) else column = 1 end list = getControl(unpack(args)) local selID = guiGridListGetSelectedItem(list) if not selID then return false end return guiGridListGetItemText(list, selID, column), selID end function getSelectedGridListLeaf(...) local listControl = getControlData(...) local selData = getSelectedGridListData(listControl.element) if not selData then return false end local listdata = g_gridListContents[listControl] return followTreePath(listdata.data, listdata.currentPath, table.map(string.split(selData, '/'), tonumber)) end function bindGridListToTable(...) -- bindGridListToTable(..., t, expandLastLevel) -- ... = control path to gridlist -- t = table to set. If false, use the cached table -- expandLastLevel = if a group occurs in the list with only leaves as direct children, -- show those leaves under the group name in the list -- Makes a table created by xmlToTable() browsable in a gridlist. local args = {...} local expandLastLevel = table.remove(args) local t = table.remove(args) if t and not treeHasMetaInfo(t) then addTreeMetaInfo(t) end local gridListControlData = getControlData(unpack(args)) local gridlist = gridListControlData.element local columns = table.merge({}, gridListControlData.columns, 'attr') -- currentPath: list of indices to follow through the groups table -- to get to the current group. f.e. {1,3} = third child group of first main group local listdata = g_gridListContents[gridListControlData] if not listdata then listdata = {currentPath={}, data=t, columns=columns} if gridListControlData.pathlbl then listdata.pathlbl = getControl(gridListControlData, '..', gridListControlData.pathlbl) end g_gridListContents[gridListControlData] = listdata elseif t then listdata.data = t listdata.currentPath = {} end if not t then t = g_gridListContents[gridListControlData].data end _updateBindedGridList(gridlist, listdata, expandLastLevel) if not listdata.clickHandlersSet then -- set item click handler if gridListControlData.onitemclick then if gridListControlData.ClickSpamProtected then addEventHandler('onClientGUIClick', gridlist, function() if isFunctionOnCD(gridListControlData.onitemclick) then return end local leaf = getSelectedGridListLeaf(gridListControlData) if leaf then gridListControlData.onitemclick(leaf) end end, false ) else addEventHandler('onClientGUIClick', gridlist, function() local leaf = getSelectedGridListLeaf(gridListControlData) if leaf then gridListControlData.onitemclick(leaf) end end, false ) end end -- set double click handler addEventHandler('onClientGUIDoubleClick', gridlist, function() local listData = g_gridListContents[gridListControlData] local previousGroup = followTreePath(listData.data, listData.currentPath) local selData, selRow = getSelectedGridListData(gridlist) if not selData then return end if tonumber(selData) == 0 then -- Go to parent group table.remove(listData.currentPath) else -- Go into child group or do item double click callback local clickedNode = followTreePath(listData.data, listData.currentPath, table.map(string.split(selData, '/'), tonumber)) if clickedNode[1] == 'group' then table.insert(listData.currentPath, tonumber(selData)) else if gridListControlData.onitemdoubleclick then if gridListControlData.DoubleClickSpamProtected then if isFunctionOnCD(gridListControlData.onitemdoubleclick) then return end gridListControlData.onitemdoubleclick(clickedNode, selRow) else gridListControlData.onitemdoubleclick(clickedNode, selRow) end end return end end applyToLeaves(previousGroup, function(item) item.row = nil end) _updateBindedGridList(gridlist, listData, expandLastLevel) end, false ) listdata.clickHandlersSet = true end local modifiableCols = table.findall(gridListControlData.columns, 'enablemodify', true) if #modifiableCols then local mt = { __index = function(leaf, k) return leaf.shadow[k] end, __newindex = function(leaf, k, v) if k ~= 'row' and leaf.row then leaf.shadow[k] = v guiGridListSetItemText(gridlist, leaf.row, table.find(columns, k), type(v) == 'boolean' and (v and 'X' or '') or tostring(v), false, false) guiSetAlpha(gridlist, guiGetAlpha(gridlist)) -- force repaint else rawset(leaf, k, v) end end } local attr applyToLeaves(t, function(leaf) -- move modifiable attributes into shadow so that __newindex triggers if not leaf.shadow then leaf.shadow = {} for i,index in ipairs(modifiableCols) do attr = gridListControlData.columns[index].attr leaf.shadow[attr] = leaf[attr] leaf[attr] = nil end end setmetatable(leaf, mt) end ) end end function gridListHasCache(...) local listControl = getControlData(...) return g_gridListContents[listControl] and true or false end function getGridListCache(...) local listControl = getControlData(...) return g_gridListContents[listControl] and g_gridListContents[listControl].data end function resetGridListPath(...) g_gridListContents[getControlData(...)].currentPath = {} end function _updateBindedGridList(gridlist, listdata, expandLastLevel, isContinuation) -- Updates the contents of a binded gridlist, following the current path -- If expandLastLevel is true, when entering a view of groups with leaves right under -- them, the leaves will be displayed in the list under the group headers -- (instead of having to double click the group to view the leaves) -- isContinuation is used for adding list items in small groups at frame updates -- to improve responsiveness. Do not specify this parameter when calling this -- function. -- find current group local group = followTreePath(listdata.data, listdata.currentPath) local toDisplay = group.children or group if not isContinuation then -- clear eventual previous event if _updateGridListOnFrame then _removeGridListFrameUpdate(listdata) end guiGridListClear(gridlist) -- set update event if necessary if not expandLastLevel and toDisplay[1][1] ~= 'group' then _updateGridListOnFrame = function() _updateBindedGridList(gridlist, listdata, false, true) end addEventHandler('onClientRender', root, _updateGridListOnFrame) end -- update path label if necessary if listdata.pathlbl then guiSetText(listdata.pathlbl, treePathToString(listdata.data, listdata.currentPath)) end -- add row to go back to parent group if necessary if #(listdata.currentPath) > 0 then guiGridListAddRow(gridlist) guiGridListSetItemText(gridlist, 0, 1, '..', false, false) guiGridListSetItemData(gridlist, 0, 1, '0') end end -- display the group contents local rowID, leafRowID if expandLastLevel or toDisplay[1][1] == 'group' then for i,item in ipairs(toDisplay) do if type(item) == 'table' then rowID = guiGridListAddRow(gridlist) if item[1] == 'group' then -- group if expandLastLevel and (listdata.data.maxSubDepth == 2 or item.depth > 1) and item.maxSubDepth == item.depth + 1 then guiGridListSetItemText(gridlist, rowID, 1, item.name, false, false) for j,leaf in ipairs(item.children) do leafRowID = guiGridListAddRow(gridlist) _putLeafInGridListRow(gridlist, leafRowID, leaf, listdata.columns) guiGridListSetItemData(gridlist, leafRowID, 1, i .. '/' .. j) leaf.row = leafRowID end else guiGridListSetItemText(gridlist, rowID, 1, '+ ' .. item.name, false, false) end else -- leaf _putLeafInGridListRow(gridlist, rowID, item, listdata.columns) end guiGridListSetItemData(gridlist, rowID, 1, tostring(i)) item.row = rowID end end else local startIndex = listdata.nextChunkStartIndex or 1 local endIndex = math.min(startIndex + GRIDLIST_UPDATE_CHUNK_SIZE - 1, #toDisplay) for i=startIndex,endIndex do rowID = guiGridListAddRow(gridlist) _putLeafInGridListRow(gridlist, rowID, toDisplay[i], listdata.columns) guiGridListSetItemData(gridlist, rowID, 1, tostring(i)) toDisplay[i].row = rowID end if endIndex == #toDisplay then _removeGridListFrameUpdate(listdata) else listdata.nextChunkStartIndex = endIndex + 1 end end end function _putLeafInGridListRow(gridlist, row, leaf, columnAttrs) for k,attr in ipairs(columnAttrs) do guiGridListSetItemText(gridlist, row, k, type(leaf[attr]) == 'boolean' and (leaf[attr] and 'X' or '') or (' ' .. tostring(leaf[attr])), false, false) end end function _removeGridListFrameUpdate(listdata) removeEventHandler('onClientRender', root, _updateGridListOnFrame) listdata.nextChunkStartIndex = nil _updateGridListOnFrame = nil end