// A list editor. It allows reading editing and saving lists as *.txt files. // It uses wire_expression2_browser for it's file browser. // The files have an easy structure for easy editing. Rows are separated by '\n' and columns by '|'. // Made by Grocel. local PANEL = {} AccessorFunc( PANEL, "m_strRootPath", "RootPath" ) // path of the root Root AccessorFunc( PANEL, "m_strList", "List" ) // List files AccessorFunc( PANEL, "m_strFile", "File" ) // sounds listed in list files AccessorFunc( PANEL, "m_bUnsaved", "Unsaved" ) // edited list file Saved? local max_char_count = 200 //File length limit local max_item_count = 512 //Item count limit local invalid_filename_chars = { ["*"] = "", ["?"] = "", [">"] = "", ["<"] = "", ["|"] = "", ["\\"] = "", ['"'] = "", [" "] = "_", } local invalid_chars = { ["*"] = "", ["?"] = "", [">"] = "", ["<"] = "", ["\\"] = "", ['"'] = "", } local counttab={} for i=1, max_item_count do counttab[i] = true end local function ConnectPathes(path1, path2) local path = "" if (isstring(path1) and path1 ~= "") then path = path1 if (isstring(path2) and path2 ~= "") then path = path1.."/"..path2 end else if (isstring(path2) and path2 ~= "") then path = path2 end end return path end //Parse the lines from a given file object local function ReadLine(filedata) if (!filedata) then return end local fileline = "" local comment = false local count = 0 for i=1, 32 do // skip 32 lines at maximum local line = "" local fileend = false for i=1, max_char_count+56 do // maximum chars per line local byte = filedata:ReadByte() fileend = !byte if (fileend) then break end // file end local char = string.char(byte) if (invalid_chars[char]) then // replace invalid chars char = invalid_chars[char] end if (char == "\n") then break end // line end line = line .. char end line = string.Trim(line) if (!fileend and line == "") then continue end fileline = line break end local linetable = string.Explode("|", fileline) or {} if (#linetable == 0) then return end for k, v in ipairs(linetable) do // cleanup local line = linetable[k] if (k == 1) then line = string.Trim(line, "/") end line = string.Trim(line) linetable[k] = line end if (#linetable[1] == 0) then return end return linetable end local function fileName(filepath) return string.match(filepath, "[/\\]?([^/\\]*)$") end local function SaveTo(self, func, ...) if (!IsValid(self)) then return end local args = {...} local path = self.FileBrowser:GetFileName() or self.m_strList or "" Derma_StringRequestNoBlur( "Save to New File", "", string.sub(fileName(path), 0, -5), // remove .txt at the end function( strTextOut ) if (!IsValid(self)) then return end strTextOut = string.gsub(strTextOut, ".", invalid_filename_chars) if (strTextOut == "") then return end local filepath = string.GetPathFromFilename(path) if (!filepath or filepath == "") then filepath = self.m_strRootPath.."/" end local saved = self:SaveList(filepath..strTextOut..".txt") if (saved and func) then func(self, unpack(args)) end end ) return true end //Ask for override: Opens a confirmation if the file name is different box. local function AsForOverride(self, func, filename, ...) if (!IsValid(self)) then return end if (!func) then return end if (filename == self.m_strList) then func(self, filename, ...) return end if (!file.Exists(filename, "DATA")) then func(self, filename, ...) return end local args = {...} Derma_Query( "Overwrite this file?", "Save To", "Overwrite", function() if (!IsValid(self)) then return end func(self, filename, unpack(args)) end, "Cancel" ) end //Ask for save: Opens a confirmation box. local function AsForSave(self, func, ...) if (!IsValid(self)) then return end if (!func) then return end if (!self.m_bUnsaved) then func(self, ...) return end local args = {...} Derma_Query( "Would you like to save the changes?", "Unsaved List!", "Yes", // Save and resume. function() if (!IsValid(self)) then return end if (!self.m_strList or self.m_strList == "") then SaveTo(self, func, unpack(args)) return end local saved = self:SaveList(self.m_strList) if (saved) then func(self, unpack(args)) end end, "No", // Don't save and resume. function() if (!IsValid(self)) then return end func(self, unpack(args)) end, "Cancel" // Do nothing. ) end function PANEL:Init() self.TimedpairsName = "wire_listeditor_files_" .. tostring({}) self:SetDrawBackground(false) self:SetUnsaved(false) self.TabfileCount = 0 self.Tabfile = {} self.ListsPanel = vgui.Create("DPanel") self.ListsPanel:SetDrawBackground(false) self.FilesPanel = vgui.Create("DPanel") self.FilesPanel:SetDrawBackground(false) self.FileBrowser = self.ListsPanel:Add("wire_expression2_browser") self.FileBrowser:Dock(FILL) self.FileBrowser.OnFileOpen = function(panel, listfile) self:OpenList(listfile) end self.FileBrowser:RemoveRightClick("Open in New Tab") // we don't need tabs. self.FileBrowser:AddRightClick(self.FileBrowser.filemenu,4,"Save To..", function() self:SaveList(self.FileBrowser:GetFileName()) end) self.FileBrowser.Update:Remove() // it's replaced self.Files = self.FilesPanel:Add("DListView") self.Files:SetMultiSelect(false) self.Files:Dock(FILL) self.Files:AddColumn("Engine") local Column = self.Files:AddColumn("Setting's") Column:SetFixedWidth(170) Column:SetWide(170) self.Files.OnRowSelected = function(parent, id, line) local name = line.m_strFilename local data = line.m_tabData self.m_strFile = name self.m_strSelectedList = self.m_strList self:DoClick(name, data, parent, line) end self.Files.DoDoubleClick = function(parent, id, line) local name = line.m_strFilename local data = line.m_tabData self.m_strFile = name self.m_strSelectedList = self.m_strList self:DoDoubleClick(name, data, parent, line) end self.Files.OnRowRightClick = function(parent, id, line) local name = line.m_strFilename local data = line.m_tabData self.m_strFile = name self.m_strSelectedList = self.m_strList self:DoRightClick(name, data, parent, line) end self.ListTopPanel = self.FilesPanel:Add("DPanel") self.ListTopPanel:SetDrawBackground(false) self.ListTopPanel:Dock(TOP) self.ListTopPanel:SetTall(20) self.ListTopPanel:DockMargin(0, 0, 0, 3) self.SaveIcon = self.ListTopPanel:Add("DImageButton") self.SaveIcon:SetImage("icon16/table_save.png") self.SaveIcon:SetWide(20) self.SaveIcon:Dock(LEFT) self.SaveIcon:SetToolTip("Save list") self.SaveIcon:SetStretchToFit(false) self.SaveIcon:DockMargin(0, 0, 0, 0) self.SaveIcon.DoClick = function() if (!self.m_strList or self.m_strList == "") then SaveTo(self) return end self:SaveList(self.m_strList) end self.SaveToIcon = self.ListTopPanel:Add("DImageButton") self.SaveToIcon:SetImage("icon16/disk.png") self.SaveToIcon:SetWide(20) self.SaveToIcon:Dock(LEFT) self.SaveToIcon:SetToolTip("Save To..") self.SaveToIcon:SetStretchToFit(false) self.SaveToIcon:DockMargin(0, 0, 0, 0) self.SaveToIcon.DoClick = function() SaveTo(self) end self.NewIcon = self.ListTopPanel:Add("DImageButton") self.NewIcon:SetImage("icon16/table_add.png") self.NewIcon:SetWide(20) self.NewIcon:Dock(LEFT) self.NewIcon:SetToolTip("New list") self.NewIcon:SetStretchToFit(false) self.NewIcon:DockMargin(10, 0, 0, 0) self.NewIcon.DoClick = function() self:ClearList() end self.RefreshIcon = self.ListTopPanel:Add("DImageButton") self.RefreshIcon:SetImage("icon16/arrow_refresh.png") self.RefreshIcon:SetWide(20) self.RefreshIcon:Dock(LEFT) self.RefreshIcon:SetToolTip("Refresh and Reload") self.RefreshIcon:SetStretchToFit(false) self.RefreshIcon:DockMargin(0, 0, 0, 0) self.RefreshIcon.DoClick = function() self:Refresh() end self.ListNameLabel = self.ListTopPanel:Add("DLabel") self.ListNameLabel:SetText("") self.ListNameLabel:SetWide(20) self.ListNameLabel:Dock(FILL) self.ListNameLabel:DockMargin(12, 0, 0, 0) self.ListNameLabel:SetDark(true) self.SplitPanel = self:Add( "DHorizontalDivider" ) self.SplitPanel:Dock( FILL ) self.SplitPanel:SetLeft(self.ListsPanel) self.SplitPanel:SetRight(self.FilesPanel) self.SplitPanel:SetLeftWidth(200) self.SplitPanel:SetLeftMin(150) self.SplitPanel:SetRightMin(300) self.SplitPanel:SetDividerWidth(3) self:SetRootPath("wirelists") end function PANEL:PerformLayout() local minw = self:GetWide() - self.SplitPanel:GetRightMin() - self.SplitPanel:GetDividerWidth() local oldminw = self.SplitPanel:GetLeftWidth(minw) if (oldminw > minw) then self.SplitPanel:SetLeftWidth(minw) end //Fixes scrollbar glitches on resize if (IsValid(self.FileBrowser.Folders)) then self.FileBrowser.Folders:OnMouseWheeled(0) end self.Files:OnMouseWheeled(0) end function PANEL:UpdateListNameLabel() if (!IsValid(self.ListNameLabel)) then return end self.ListNameLabel:SetText((self.m_bUnsaved and "*" or "")..(self.m_strList or "")) end function PANEL:ClearList() AsForSave(self, function(self) self:SetList(nil) self:SetUnsaved(false) self.TabfileCount = 0 WireLib.TimedpairsStop(self.TimedpairsName) self.Files:Clear(true) self.Tabfile = {} end) end function PANEL:Setup() if (!self.m_strRootPath) then return false end self.m_strSelectedList = nil self.m_strFile = nil self:ClearList() self.FileBrowser:Setup(self.m_strRootPath) return true end function PANEL:Refresh() self.FileBrowser:Refresh() self:OpenList(self.m_strList) self:InvalidateLayout() end function PANEL:Think() if (self.SplitPanel:GetDragging()) then self:InvalidateLayout() end if ( !self.bSetup ) then self.bSetup = self:Setup() end end function PANEL:AddItem(...) local itemtable = {...} local item = itemtable[1] if (!isstring(item) or item == "") then return end if (self.TabfileCount > max_item_count) then return end if (#item > max_char_count) then return end if (self.Tabfile[item]) then return end local itemargs = {} for k, v in ipairs(itemtable) do if (k == 1) then continue end itemargs[k-1] = v end self.Tabfile[item] = itemargs local line = self.Files:AddLine(...) line.m_strFilename = item line.m_tabData = itemargs //if (self.m_strFile == item) then if (self.m_strSelectedList == self.m_strList and self.m_strFile == item) then self.Files:SelectItem(line) end self.TabfileCount = self.TabfileCount + 1 self:SetUnsaved(true) return line end function PANEL:ItemInList(item) if (!item) then return false end if (self.Tabfile[item]) then return true end return false end function PANEL:RemoveItem(item) if (!item) then return end if (!self.Tabfile[item]) then return end if (!self.Files.Lines) then return end for k, v in ipairs(self.Files.Lines) do if (v.m_strFilename == item) then self.Files:RemoveLine(v:GetID()) self.Tabfile[item] = nil self:SetUnsaved(true) self.TabfileCount = self.TabfileCount - 1 break end end end function PANEL:OpenList(strfile) if (!strfile) then return end if (strfile == "") then return end AsForSave(self, function(self, strfile) local filedata = file.Open(strfile, "rb", "DATA") if (!filedata) then return end WireLib.TimedpairsStop(self.TimedpairsName) self.Files:Clear(true) self.Tabfile = {} self.TabfileCount = 0 WireLib.Timedpairs(self.TimedpairsName, counttab, 20, function(index, _, self, filedata) if (!IsValid(self)) then return false end if (!IsValid(self.Files)) then return false end local linetable = ReadLine(filedata) if (!linetable) then return false end // do not add to empty lines self:AddItem(unpack(linetable)) self:SetUnsaved(false) end, function(index, _, self, filedata) filedata:Close() self:SetUnsaved(false) end, self, filedata) self:SetUnsaved(false) self:SetList(strfile) end, strfile) end function PANEL:SaveList(strfile) if (!self.Tabfile) then return end if (!strfile) then return end if (strfile == "") then return end AsForOverride(self, function(self, strfile) local filedata = file.Open(strfile, "w", "DATA") if (!filedata) then Derma_Query( "File could not be saved!", "Error!", "OK" ) return end for key, itemtable in SortedPairs(self.Tabfile) do local item = key for k, supitem in ipairs(itemtable) do item = item.." | "..supitem end filedata:Write(item.."\n") end filedata:Close() self:SetUnsaved(false) self:SetList(strfile) self:Refresh() end, strfile) end function PANEL:SetRootPath(path) self.m_strRootPath = path self.bSetup = self:Setup() end function PANEL:SetUnsaved(bool) self.m_bUnsaved = bool self:UpdateListNameLabel() end function PANEL:SetList(listfile) self.m_strList = listfile self:UpdateListNameLabel() end function PANEL:DoClick(name, data, parent, line) -- Override end function PANEL:DoDoubleClick(name, data, parent, line) -- Override end function PANEL:DoRightClick(name, data, parent, line) -- Override end vgui.Register("acf_list2editor", PANEL, "DPanel")