556 lines
13 KiB
Lua
556 lines
13 KiB
Lua
// 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")
|