From 0e019881a9fb2029f5556d8e2ed18f909d82ae02 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:38:04 -0700 Subject: [PATCH 01/31] Place shared constants in shared file Needed to define some enumerations, I figured I would do a bit more while I'm at it --- lua/advdupe2/cl_file.lua | 44 ++++++++++++++----------- lua/autorun/advdupe2_sh_init.lua | 14 ++++++++ lua/autorun/client/advdupe2_cl_init.lua | 15 ++++----- lua/autorun/server/advdupe2_sv_init.lua | 7 +--- 4 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 lua/autorun/advdupe2_sh_init.lua diff --git a/lua/advdupe2/cl_file.lua b/lua/advdupe2/cl_file.lua index 58dc4bcd..7e030e72 100644 --- a/lua/advdupe2/cl_file.lua +++ b/lua/advdupe2/cl_file.lua @@ -1,6 +1,6 @@ local invalidCharacters = { "\"", ":"} function AdvDupe2.SanitizeFilename(filename) - for i=1, #invalidCharacters do + for i = 1, #invalidCharacters do filename = string.gsub(filename, invalidCharacters[i], "_") end filename = string.gsub(filename, "%s+", " ") @@ -16,7 +16,7 @@ function AdvDupe2.ReceiveFile(data, autoSave) end local path if autoSave then - if(LocalPlayer():GetInfo("advdupe2_auto_save_overwrite")~="0")then + if LocalPlayer():GetInfo("advdupe2_auto_save_overwrite") ~= "0" then path = AdvDupe2.GetFilename(AdvDupe2.AutoSavePath, true) else path = AdvDupe2.GetFilename(AdvDupe2.AutoSavePath) @@ -35,15 +35,15 @@ function AdvDupe2.ReceiveFile(data, autoSave) dupefile:Close() local errored = false - if(LocalPlayer():GetInfo("advdupe2_debug_openfile")=="1")then - if(not file.Exists(path, "DATA"))then AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) return end + if LocalPlayer():GetInfo("advdupe2_debug_openfile") == "1" then + if not file.Exists(path, "DATA") then AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) return end local readFile = file.Open(path, "rb", "DATA") if not readFile then AdvDupe2.Notify("File could not be read", NOTIFY_ERROR) return end local readData = readFile:Read(readFile:Size()) readFile:Close() - local success,dupe,info,moreinfo = AdvDupe2.Decode(readData) - if(success)then + local success, dupe = AdvDupe2.Decode(readData) + if success then AdvDupe2.Notify("DEBUG CHECK: File successfully opens. No EOF errors.") else AdvDupe2.Notify("DEBUG CHECK: " .. dupe, NOTIFY_ERROR) @@ -53,15 +53,15 @@ function AdvDupe2.ReceiveFile(data, autoSave) local filename = string.StripExtension(string.GetFileFromFilename( path )) if autoSave then - if(IsValid(AdvDupe2.FileBrowser.AutoSaveNode))then + if IsValid(AdvDupe2.FileBrowser.AutoSaveNode) then local add = true - for i=1, #AdvDupe2.FileBrowser.AutoSaveNode.Files do - if(filename==AdvDupe2.FileBrowser.AutoSaveNode.Files[i].Label:GetText())then - add=false + for i = 1, #AdvDupe2.FileBrowser.AutoSaveNode.Files do + if filename == AdvDupe2.FileBrowser.AutoSaveNode.Files[i].Label:GetText() then + add = false break end end - if(add)then + if add then AdvDupe2.FileBrowser.AutoSaveNode:AddFile(filename) AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.AutoSaveNode) end @@ -70,7 +70,8 @@ function AdvDupe2.ReceiveFile(data, autoSave) AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode:AddFile(filename) AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode) end - if(!errored)then + + if not errored then AdvDupe2.Notify("File successfully saved!",NOTIFY_GENERIC, 5) end end @@ -94,17 +95,20 @@ function AdvDupe2.SendFile(name, data) net.SendToServer() end +local ADVDUPE2_AREA_ADVDUPE2 = AdvDupe2.AREA_ADVDUPE2 +local ADVDUPE2_AREA_PUBLIC = AdvDupe2.AREA_PUBLIC + function AdvDupe2.UploadFile(ReadPath, ReadArea) if AdvDupe2.Uploading then AdvDupe2.Notify("Already opening file, please wait.", NOTIFY_ERROR) return end - if(ReadArea==0)then - ReadPath = AdvDupe2.DataFolder.."/"..ReadPath..".txt" - elseif(ReadArea==1)then - ReadPath = AdvDupe2.DataFolder.."/-Public-/"..ReadPath..".txt" + if ReadArea == ADVDUPE2_AREA_ADVDUPE2 then + ReadPath = AdvDupe2.DataFolder .. "/" .. ReadPath .. ".txt" + elseif ReadArea == ADVDUPE2_AREA_PUBLIC then + ReadPath = AdvDupe2.DataFolder .. "/-Public-/" .. ReadPath .. ".txt" else - ReadPath = "adv_duplicator/"..ReadPath..".txt" + ReadPath = "adv_duplicator/" .. ReadPath .. ".txt" end - if(not file.Exists(ReadPath, "DATA"))then AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) return end + if not file.Exists(ReadPath, "DATA") then AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) return end local read = file.Read(ReadPath) if not read then AdvDupe2.Notify("File could not be read", NOTIFY_ERROR) return end @@ -113,11 +117,11 @@ function AdvDupe2.UploadFile(ReadPath, ReadArea) name = string.sub(name, 1, #name-4) local success, dupe, info, moreinfo = AdvDupe2.Decode(read) - if(success)then + if success then AdvDupe2.SendFile(name, read) AdvDupe2.LoadGhosts(dupe, info, moreinfo, name) else - AdvDupe2.Notify("File could not be decoded. ("..dupe..") Upload Canceled.", NOTIFY_ERROR) + AdvDupe2.Notify("File could not be decoded. (" .. dupe .. ") Upload Canceled.", NOTIFY_ERROR) end end diff --git a/lua/autorun/advdupe2_sh_init.lua b/lua/autorun/advdupe2_sh_init.lua new file mode 100644 index 00000000..66677124 --- /dev/null +++ b/lua/autorun/advdupe2_sh_init.lua @@ -0,0 +1,14 @@ +AdvDupe2 = AdvDupe2 or {} + +AdvDupe2.Version = "1.1.0" +AdvDupe2.Revision = 51 + +AdvDupe2.DataFolder = "advdupe2" --name of the folder in data where dupes will be saved + +-- enums +AdvDupe2.AREA_ADVDUPE2 = 0 +AdvDupe2.AREA_PUBLIC = 1 +AdvDupe2.AREA_ADVDUPE1 = 2 + +AdvDupe2.NODETYPE_FOLDER = 1 +AdvDupe2.NODETYPE_FILE = 2 \ No newline at end of file diff --git a/lua/autorun/client/advdupe2_cl_init.lua b/lua/autorun/client/advdupe2_cl_init.lua index dd3a30a3..74bd817a 100644 --- a/lua/autorun/client/advdupe2_cl_init.lua +++ b/lua/autorun/client/advdupe2_cl_init.lua @@ -1,13 +1,10 @@ -AdvDupe2 = { - Version = "1.1.0", - Revision = 51, - InfoText = {}, - DataFolder = "advdupe2", - FileRenameTryLimit = 256, - ProgressBar = {} -} +AdvDupe2 = AdvDupe2 or {} -if(!file.Exists(AdvDupe2.DataFolder, "DATA"))then +AdvDupe2.InfoText = {} +AdvDupe2.FileRenameTryLimit = 256 +AdvDupe2.ProgressBar = {} + +if not file.Exists(AdvDupe2.DataFolder, "DATA") then file.CreateDir(AdvDupe2.DataFolder) end diff --git a/lua/autorun/server/advdupe2_sv_init.lua b/lua/autorun/server/advdupe2_sv_init.lua index fdc98937..625450e6 100644 --- a/lua/autorun/server/advdupe2_sv_init.lua +++ b/lua/autorun/server/advdupe2_sv_init.lua @@ -1,9 +1,4 @@ -AdvDupe2 = { - Version = "1.1.0", - Revision = 51 -} - -AdvDupe2.DataFolder = "advdupe2" --name of the folder in data where dupes will be saved +AdvDupe2 = AdvDupe2 or {} function AdvDupe2.Notify(ply,msg,typ, showsvr, dur) net.Start("AdvDupe2Notify") From c44e7880875f0f74f89edcae7efa6bdf6a09cc20 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:38:43 -0700 Subject: [PATCH 02/31] Couple of formatting things that annoyed me --- lua/autorun/client/advdupe2_cl_init.lua | 6 ++---- lua/autorun/server/advdupe2_sv_init.lua | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lua/autorun/client/advdupe2_cl_init.lua b/lua/autorun/client/advdupe2_cl_init.lua index 74bd817a..fb306a38 100644 --- a/lua/autorun/client/advdupe2_cl_init.lua +++ b/lua/autorun/client/advdupe2_cl_init.lua @@ -16,11 +16,9 @@ include( "advdupe2/cl_ghost.lua" ) function AdvDupe2.Notify(msg,typ,dur) surface.PlaySound(typ == 1 and "buttons/button10.wav" or "ambient/water/drip1.wav") GAMEMODE:AddNotify(msg, typ or NOTIFY_GENERIC, dur or 5) - //if not game.SinglePlayer() then - print("[AdvDupe2Notify]\t"..msg) - //end + print("[AdvDupe2Notify]\t" .. msg) end net.Receive("AdvDupe2Notify", function() AdvDupe2.Notify(net.ReadString(), net.ReadUInt(8), net.ReadFloat()) -end) +end) \ No newline at end of file diff --git a/lua/autorun/server/advdupe2_sv_init.lua b/lua/autorun/server/advdupe2_sv_init.lua index 625450e6..c5781d85 100644 --- a/lua/autorun/server/advdupe2_sv_init.lua +++ b/lua/autorun/server/advdupe2_sv_init.lua @@ -1,14 +1,14 @@ AdvDupe2 = AdvDupe2 or {} -function AdvDupe2.Notify(ply,msg,typ, showsvr, dur) +function AdvDupe2.Notify(ply, msg, typ, showsvr, dur) net.Start("AdvDupe2Notify") net.WriteString(msg) net.WriteUInt(typ or 0, 8) net.WriteFloat(dur or 5) net.Send(ply) - if(showsvr==true)then - print("[AdvDupe2Notify]\t"..ply:Nick()..": "..msg) + if showsvr == true then + print("[AdvDupe2Notify]\t" .. ply:Nick() .. ": " .. msg) end end From 986e9348a2af4f2195ee59bfd1caa42bca7652a5 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:39:20 -0700 Subject: [PATCH 03/31] Temporary hard-refresh-advdupe2 button will remove when PR is complete --- lua/weapons/gmod_tool/stools/advdupe2.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lua/weapons/gmod_tool/stools/advdupe2.lua b/lua/weapons/gmod_tool/stools/advdupe2.lua index 02c70b98..b5c3388f 100644 --- a/lua/weapons/gmod_tool/stools/advdupe2.lua +++ b/lua/weapons/gmod_tool/stools/advdupe2.lua @@ -1064,9 +1064,16 @@ if(CLIENT) then CreateClientConVar("advdupe2_paste_protectoveride", 1, false, true) CreateClientConVar("advdupe2_debug_openfile", 1, false, true) - local function BuildCPanel(CPanel) + local BuildCPanel + function BuildCPanel(CPanel) CPanel:ClearControls() + local refresh = vgui.Create("DButton") + refresh:SetText("Hard-refresh") + refresh:Dock(TOP) + refresh.DoClick = function() CPanel:Clear() BuildCPanel(CPanel) end + CPanel:AddItem(refresh) + local FileBrowser = vgui.Create("advdupe2_browser") CPanel:AddItem(FileBrowser) FileBrowser:SetSize(CPanel:GetWide(), 405) @@ -1078,7 +1085,7 @@ if(CLIENT) then Check:SetDark(true) Check:SetConVar( "advdupe2_original_origin" ) Check:SetValue( 0 ) - Check:SetToolTip("Paste at the position originally copied") + Check:SetTooltip("Paste at the position originally copied") CPanel:AddItem(Check) Check = vgui.Create("DCheckBoxLabel") From acd4f1c9b29c1b0104144dd98c2d7f0f891610b6 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 00:40:31 -0700 Subject: [PATCH 04/31] Initial file browser V2 work There are still a lot of fragments of V1 left behind and most things don't work yet, but the basics work and the proof-of-concept at how fast this can be is there Some things here refer to "immediate" - the original idea was a lot more immediate-mode style, but this is more retained in Lua-land than anything at this point --- lua/advdupe2/file_browser.lua | 1533 ++++++++++++++++++++++----------- 1 file changed, 1037 insertions(+), 496 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index c1381950..031daa32 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -3,15 +3,21 @@ Desc: Displays and interfaces with duplication files. - Author: TB + Authors: March (v2.0), TB (v1.0) - Version: 1.0 + Version: 2.0 ]] +-- Enums +local ADVDUPE2_AREA_ADVDUPE2 = AdvDupe2.AREA_ADVDUPE2 +local ADVDUPE2_AREA_PUBLIC = AdvDupe2.AREA_PUBLIC +local ADVDUPE2_AREA_ADVDUPE1 = AdvDupe2.AREA_ADVDUPE1 +local ADVDUPE2_NODETYPE_FOLDER = AdvDupe2.NODETYPE_FOLDER +local ADVDUPE2_NODETYPE_FILE = AdvDupe2.NODETYPE_FILE + local History = {} local Narrow = {} -local switch = true local count = 0 local function AddHistory(txt) @@ -102,14 +108,565 @@ function BROWSERPNL:Init() end function BROWSERPNL:OnVScroll(iOffset) - self.pnlCanvas:SetPos(0, iOffset) + -- self.pnlCanvas:SetPos(0, iOffset) end derma.DefineControl("advdupe2_browser_panel", "AD2 File Browser", BROWSERPNL, "Panel") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +local NODE_MT = {} +local NODE = setmetatable({}, NODE_MT) + +function NODE:Init(Type, Browser) + self.Type = Type + self.Browser = Browser + self.Files = {} + self.Folders = {} + self.Sorted = {} + self.Expanded = false + self.Selected = false + + self:MarkSortDirty() +end + +function NODE_MT:__call(Type, Browser) + if Type == nil then return error("Cannot create typeless node") end + if not IsValid(Browser) then return error ("Cannot create a headless node (we need a browser)") end + + local Node = setmetatable({}, {__index = NODE}) + Node:Init(Type, Browser) + + return Node +end + +function NODE:IsRoot() return (self.Root or error("No root?")) == self end +function NODE:IsFolder() return self.Type == ADVDUPE2_NODETYPE_FOLDER end +function NODE:IsFile() return self.Type == ADVDUPE2_NODETYPE_FILE end + +function NODE:AddFolder(Text) + local Node = NODE(ADVDUPE2_NODETYPE_FOLDER, self.Browser) + Node.Text = Text + Node.ParentNode = self + Node.Root = self.Root + if self.Expanded then self:MarkSortDirty() end + self.Folders[#self.Folders + 1] = Node + + return Node +end + +function NODE:AddFile(Text) + local Node = NODE(ADVDUPE2_NODETYPE_FILE, self.Browser) + Node.Text = Text + Node.ParentNode = self + Node.Root = self.Root + if self.Expanded then self:MarkSortDirty() end + self.Files[#self.Files + 1] = Node + + return Node +end + +function NODE:Count() return #self.Files + #self.Folders end + +function NODE:MarkSortDirty() + self.SortDirty = true + self.Browser.SortDirty = true +end + +function NODE:Clear() + table.Empty(self.Files) + table.Empty(self.Folders) + table.Empty(self.Sorted) + self:MarkSortDirty() +end + +function NODE:RemoveNode(Node) + if Node:IsFolder() then + table.RemoveByValue(self.Folders, Node) + else + table.RemoveByValue(self.Files, Node) + end + + self:MarkSortDirty() +end + +function NODE:Remove() + local ParentNode = self.ParentNode + + if ParentNode then + ParentNode:RemoveNode(self) + else + self:MarkSortDirty() + end +end + +function NODE:SetExpanded(Expanded) + if Expanded == self.Expanded then return end + + self.Expanded = Expanded + self:MarkSortDirty() +end + +function NODE:Expand() self:SetExpanded(true) end +function NODE:Collapse() self:SetExpanded(false) end +function NODE:ToggleExpanded() self:SetExpanded(not self.Expanded) end + +local function SetupDataFile(Node, Path, Name) + Node.Text = Name + Node.Path = Path +end + +local function SetupDataSubfolder(Node, Path, Name) + Node.Text = Name + Node.Path = Path +end + +-- Expects a directory path ending in a forward slash. +local LoadDataFolderInternal +function LoadDataFolderInternal(Node, Path) + local Files, Directories = file.Find(Path .. "*", "DATA", "nameasc") + if not Files or not Directories then return end + + for _, File in ipairs(Files) do + local FilePath = Path .. File + local FileNode = Node:AddFile(FilePath) + SetupDataFile(FileNode, FilePath, File) + end + + for _, Directory in ipairs(Directories) do + local DirectoryPath = Path .. Directory + local DirectoryNode = Node:AddFolder(DirectoryPath) + SetupDataSubfolder(DirectoryNode, DirectoryPath, Directory) + DirectoryNode.FirstObserved = function(DirNode) -- note FirstObserved will be destroyed after first call + LoadDataFolderInternal(DirNode, DirectoryPath .. "/") + end + end +end + + +function NODE:LoadDataFolder(Path) + self:Clear() + LoadDataFolderInternal(self, Path) +end + +-- Defines GetNumericalFilename +-- May need optimization and refactoring later - especially for non-ASCII strings... +-- This handles things very similarly to how Windows does in terms of sorting, but also adds sorting by month +-- May also be a good idea in the future to add a setting for the above functionality. +local GetNumericalFilename +do + local isDigit = { + ['0'] = 0, + ['1'] = 1, + ['2'] = 2, + ['3'] = 3, + ['4'] = 4, + ['5'] = 5, + ['6'] = 6, + ['7'] = 7, + ['8'] = 8, + ['9'] = 9 + } + + -- faster than string.byte calls + local char2byte = {} + for i = 1, 255 do char2byte[string.char(i)] = string.byte(string.lower(string.char(i))) end + char2byte['_'] = 2000 + + local buildMonth = {} + for k, v in ipairs{"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"} do + local tbl = buildMonth + for i = 1, #v do + local c = v[i] + if i == #v then + tbl[c] = k + else + if not tbl[c] then + tbl[c] = {} + end + + tbl = tbl[c] + end + end + end + + local numericalStore = {} + function GetNumericalFilename(name) + if numericalStore[name] then return numericalStore[name] end + + local ret = {} + local digit = nil + local monthTester = buildMonth + local monthStoreJustInCase = {} + + for i = 1, #name do + local c = name[i] + local cIsDigit = isDigit[c] + if cIsDigit then + if digit == nil then + digit = 0 + end + digit = (digit * 10) + cIsDigit + else + if monthTester[c] then + monthTester = monthTester[c] + monthStoreJustInCase[#monthStoreJustInCase + 1] = char2byte[c] + if type(monthTester) == "number" then + local nextC = name[i + 1] + local nextIfine = nextC == ' ' or nextC == '_' or nextC == '-' + if i == #name or nextIfine then + ret[#ret + 1] = monthTester + monthStoreJustInCase = {} + monthTester = buildMonth + if nextIfine then + i = i + 1 + end + else + for i = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i] + end + monthStoreJustInCase = {} + monthTester = buildMonth + end + end + elseif digit ~= nil then + ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) + digit = nil + else + if monthTester ~= buildMonth then + for i = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i] + end + monthStoreJustInCase = {} + monthTester = buildMonth + end + ret[#ret + 1] = char2byte[c] + end + end + end + + if digit ~= nil then + ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) + end + if monthTester ~= buildMonth then + for i = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i] + end + end + + numericalStore[name] = ret -- store so this doesnt have to be calculated multiple times for no reason + return ret + end +end + +function NODE.SortFunction(A, B) + local IsFileA, IsFileB = A:IsFile(), B:IsFile() + + if not IsFileA and IsFileB then return true end + if IsFileA and not IsFileB then return false end + + local NameA, NameB = GetNumericalFilename(string.StripExtension(A.Text)), GetNumericalFilename(string.StripExtension(B.Text)) + + for I = 1, math.max(#NameA, #NameB) do + local AC, BC = NameA[I], NameB[I] + + if AC == nil then return true end + if BC == nil then return false end + + if AC ~= BC then + return AC < BC + end + end +end + +function NODE:PerformResort() + if not self.SortDirty then return end + + local Sorted = self.Sorted + local Files = self.Files + local Folders = self.Folders + table.Empty(Sorted) + + for I = 1, #Files do Sorted[#Sorted + 1] = Files[I] end + for I = 1, #Folders do Sorted[#Sorted + 1] = Folders[I] end + + -- For each node, check FirstObserved and call if it exists + for _, Node in ipairs(Sorted) do + if Node.FirstObserved then + Node:FirstObserved() + Node.FirstObserved = nil + end + end + + -- Perform actual resort + table.sort(Sorted, self.SortFunction) +end + +-- Returns an enumerator +function NODE:GetSortedChildNodes() + self:PerformResort() + return ipairs(self.Sorted) +end + +function NODE.InjectIntoBrowser(Browser) + for FuncName, Func in pairs(NODE) do + Browser[FuncName] = Func + end + + NODE.Init(Browser, ADVDUPE2_NODETYPE_FOLDER, Browser) +end + + + + +-- This interface describes the logic behind a root folder (like AdvDupe1 or AdvDupe2). + +local IRootFolder_MT = {} +local IRootFolder = setmetatable({}, IRootFolder_MT) +AdvDupe2.IRootFolder = IRootFolder -- If other addons want to post-verify their IRootFolder implementations like we do + +-- todo; debug.getinfo and determine argument counts to further sanity check? +IRootFolder.Init = function(Impl, Browser, Node) end +IRootFolder.GetFolderName = function(Impl) end +-- These define node operations +-- These are RAW operations, as in the underlying Browser might do some prompts first +-- But for example, calling IRootFolder:UserDelete() is expected to actually delete the node +-- (and the browser will create the prompt) +IRootFolder.UserUpload = function(Impl, Browser, Node) end +IRootFolder.UserPreview = function(Impl, Browser, Node) end +IRootFolder.UserSave = function(Impl, Browser, Node, Filename, Description) end +IRootFolder.UserRename = function(Impl, Browser, Node, RenameTo) end +IRootFolder.UserMenu = function(Impl, Browser, Node, Menu) end +IRootFolder.UserDelete = function(Impl, Browser, Node) end + +-- Ensures the implementor implemented the interface correctly +-- if they didn't throw non-halting errors since it might be an optional method +function IRootFolder_MT:__call(RootFolderType) + for FuncName, _ in pairs(IRootFolder) do + if not RootFolderType[FuncName] then + ErrorNoHaltWithStack("AdvDupe2: IRootFolder implementation failed to implement " .. FuncName .. ", this may not work as intended...") + end + end + + return RootFolderType +end + + + + + + + + + + + + +-- This turns a data-folder path name into something AdvDupe2.UploadFile can tolerate +local function GetNodeDataPath(Node) + local Path = Node.Path + local FirstSlash = string.find(Path, "/") + local RemovedFirstDirPath = string.sub(Path, FirstSlash + 1) + return string.StripExtension(RemovedFirstDirPath) +end + +-- wraps File.Exists and throws a notification +local function FileExists(Path) + if not Path then + AdvDupe2.Notify("Expected path, got nil!") + return false + end + + if not file.Exists(Path, "DATA") then + AdvDupe2.Notify("File '" .. Path .. "' does not exist.") + return false + end + + return true +end + +local function OpenPreview(Node, Area) + local Path = GetNodeDataPath(Node) + local ReadPath + + if Area == ADVDUPE2_AREA_ADVDUPE2 then + ReadPath = AdvDupe2.DataFolder .. "/" .. Path .. ".txt" + elseif Area == ADVDUPE2_AREA_PUBLIC then + ReadPath = AdvDupe2.DataFolder .. "/-Public-/" .. ReadPath .. ".txt" + else + ReadPath = "adv_duplicator/" .. Path .. ".txt" + end + + if not FileExists(ReadPath) then return end + + local Read = file.Read(ReadPath) + local Name = string.StripExtension(string.GetFileFromFilename(Path)) + + local Success, Dupe, Info, MoreInfo = AdvDupe2.Decode(Read) + + if Success then + AdvDupe2.LoadGhosts(Dupe, Info, MoreInfo, Name, true) + end +end + + + +local AdvDupe1Folder, AdvDupe2Folder + +do + AdvDupe1Folder = {} + function AdvDupe1Folder:GetFolderName() return "Advanced Duplicator 1" end + function AdvDupe1Folder:Init(Browser, Node) + Node:LoadDataFolder("adv_duplicator/") + + if Node:Count() < 0 then + Node:Remove() + end + end + + function AdvDupe1Folder:UserUpload(Browser, Node) + AdvDupe2.UploadFile(GetNodeDataPath(Node), ADVDUPE2_AREA_ADVDUPE1) + end + + function AdvDupe1Folder:UserPreview(Browser, Node) + OpenPreview(Node, ADVDUPE2_AREA_ADVDUPE1) + end + + + function AdvDupe1Folder:UserSave(Browser, Node, Filename, Description) + + end + + function AdvDupe1Folder:UserRename(Browser, Node, RenameTo) + + end + + function AdvDupe1Folder:UserMenu(Browser, Node, Menu) + + end + + function AdvDupe1Folder:UserDelete(Browser, Node) + + end + + IRootFolder(AdvDupe1Folder) -- validation +end + +do + AdvDupe2Folder = {} + function AdvDupe2Folder:GetFolderName() return "Advanced Duplicator 2" end + function AdvDupe2Folder:Init(Browser, Node) + Node:LoadDataFolder("advdupe2/") + end + + function AdvDupe2Folder:UserUpload(Browser, Node) + AdvDupe2.UploadFile(GetNodeDataPath(Node), ADVDUPE2_AREA_ADVDUPE2) + end + + function AdvDupe2Folder:UserPreview(Browser, Node) + OpenPreview(Node, ADVDUPE2_AREA_ADVDUPE2) + end + + function AdvDupe2Folder:UserSave(Browser, Node, Filename, Description) + + end + + function AdvDupe2Folder:UserRename(Browser, Node, RenameTo) + + end + + function AdvDupe2Folder:UserMenu(Browser, Node, Menu) + if Node:IsFile() then + Menu:AddOption("Open", function() self:UserUpload(Browser, Node) end, "icon16/page_go.png") + Menu:AddOption("Preview", function() self:UserPreview(Browser, Node) end, "icon16/information.png") + Menu:AddSpacer() + Menu:AddOption("Rename...", nil, "icon16/textfield_rename.png") + Menu:AddOption("Move...", nil, "icon16/arrow_right.png") + Menu:AddOption("Delete", nil, "icon16/bin_closed.png") + else + Menu:AddOption("Save", nil, "icon16/disk.png") + Menu:AddOption("New Folder", nil, "icon16/folder_add.png") + Menu:AddSpacer() + Menu:AddOption("Search", nil, "icon16/magnifier.png") + end + end + + function AdvDupe2Folder:UserDelete(Browser, Node) + + end + + IRootFolder(AdvDupe2Folder) -- validation +end + + + + + + + + + + + + + + + + local BROWSER = {} AccessorFunc(BROWSER, "m_pSelectedItem", "SelectedItem") -Derma_Hook(BROWSER, "Paint", "Paint", "Panel") local origSetTall local function SetTall(self, val) @@ -124,66 +681,27 @@ function BROWSER:Init() self.VBar = vgui.Create("DVScrollBar", self:GetParent()) self.VBar:Dock(RIGHT) - self.Nodes = 0 - self.ChildrenExpanded = {} - self.ChildList = self - self.m_bExpanded = true - self.Folders = {} - self.Files = {} - self.LastClick = CurTime() -end - -local function GetNodePath(node) - local path = node.Label:GetText() - local area = 0 - local name = "" - node = node.ParentNode - if (not node.ParentNode) then - if (path == "Public") then - area = 1 - elseif (path == "Advanced Duplicator 1") then - area = 2 - end - return "", area - end - while (true) do - - name = node.Label:GetText() - if (name == "Advanced Duplicator 2") then - break - elseif (name == "Public") then - area = 1 - break - elseif (name == "Advanced Duplicator 1") then - area = 2 - break - end - path = name .. "/" .. path - node = node.ParentNode - end + -- Implement NODE + NODE.InjectIntoBrowser(self) + self.SortDirty = true + self.ExpandedNodeArray = {} - return path, area + self.LastClick = CurTime() end -function BROWSER:DoNodeLeftClick(node) - if (self.m_pSelectedItem == node and CurTime() - self.LastClick <= 0.25) then -- Check for double click - if (node.Derma.ClassName == "advdupe2_browser_folder") then - if (node.Expander) then - node:SetExpanded() -- It's a folder, expand/collapse it - end - elseif (node.Derma.ClassName == "advdupe2_browser_file") then - if (node.Control.Search) then - AdvDupe2.UploadFile(GetNodePath(node.Ref)) - else - AdvDupe2.UploadFile(GetNodePath(node)) - end +function BROWSER:DoNodeLeftClick(Node) + if self.m_pSelectedItem == Node and CurTime() - self.LastClick <= 0.25 then -- Check for double click + if Node:IsFolder() then + Node:ToggleExpanded() else - AdvDupe2.UploadFile(GetNodePath(node.Ref)) + local RootImpl = Node.Root.RootImpl + RootImpl:UserUpload(self, Node) end else - self:SetSelected(node) -- A node was clicked, select it + self:SetSelected(Node) -- A node was clicked, select it end + self.LastClick = CurTime() end @@ -269,30 +787,6 @@ function AdvDupe2.GetFilename(path, overwrite) return path .. ".txt" end -local function GetFullPath(node) - local path, area = GetNodePath(node) - if (area == 0) then - path = AdvDupe2.DataFolder .. "/" .. path .. "/" - elseif (area == 1) then - - else - path = "adv_duplicator/" .. path .. "/" - end - return path -end - -local function GetNodeRoot(node) - local Root - while (true) do - if (not node.ParentNode.ParentNode) then - Root = node - break - end - node = node.ParentNode - end - return Root -end - local function RenameFileCl(node, name) local path, area = GetNodePath(node) local File, FilePath, tempFilePath = "", "", "" @@ -397,317 +891,46 @@ end local function Search(node, name) local pnFileBr = AdvDupe2.FileBrowser pnFileBr.Search = vgui.Create("advdupe2_browser_panel", pnFileBr) - pnFileBr.Search:SetPos(pnFileBr.Browser:GetPos()) - pnFileBr.Search:SetSize(pnFileBr.Browser:GetSize()) - pnFileBr.Search.pnlCanvas.Search = true - pnFileBr.Browser:SetVisible(false) - local Files = SearchNodes(node, name) - tableSortNodes(Files) - for k, v in pairs(Files) do - pnFileBr.Search.pnlCanvas:AddFile(v.Label:GetText()).Ref = v - end -end - -function BROWSER:DoNodeRightClick(node) - self:SetSelected(node) - - local parent = self:GetParent():GetParent() - parent.FileName:KillFocus() - parent.Desc:KillFocus() - local Menu = DermaMenu() - local root = GetNodeRoot(node).Label:GetText() - if (node.Derma.ClassName == "advdupe2_browser_file") then - if (node.Control.Search) then - Menu:AddOption("Open", function() - AdvDupe2.UploadFile(GetNodePath(node.Ref)) - end) - Menu:AddOption("Preview", function() - local ReadPath, ReadArea = GetNodePath(node.Ref) - if (ReadArea == 0) then - ReadPath = AdvDupe2.DataFolder .. "/" .. ReadPath .. ".txt" - elseif (ReadArea == 1) then - ReadPath = AdvDupe2.DataFolder .. "/-Public-/" .. ReadPath .. ".txt" - else - ReadPath = "adv_duplicator/" .. ReadPath .. ".txt" - end - if (not file.Exists(ReadPath, "DATA")) then - AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) - return - end - - local read = file.Read(ReadPath) - local name = string.Explode("/", ReadPath) - name = name[#name] - name = string.sub(name, 1, #name - 4) - local success, dupe, info, moreinfo = AdvDupe2.Decode(read) - if (success) then - AdvDupe2.LoadGhosts(dupe, info, moreinfo, name, true) - end - end) - else - Menu:AddOption("Open", function() - AdvDupe2.UploadFile(GetNodePath(node)) - end) - Menu:AddOption("Preview", function() - local ReadPath, ReadArea = GetNodePath(node) - if (ReadArea == 0) then - ReadPath = AdvDupe2.DataFolder .. "/" .. ReadPath .. ".txt" - elseif (ReadArea == 1) then - ReadPath = AdvDupe2.DataFolder .. "/-Public-/" .. ReadPath .. ".txt" - else - ReadPath = "adv_duplicator/" .. ReadPath .. ".txt" - end - if (not file.Exists(ReadPath, "DATA")) then - AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) - return - end - - local read = file.Read(ReadPath) - local name = string.Explode("/", ReadPath) - name = name[#name] - name = string.sub(name, 1, #name - 4) - local success, dupe, info, moreinfo = AdvDupe2.Decode(read) - if (success) then - AdvDupe2.LoadGhosts(dupe, info, moreinfo, name, true) - end - end) - Menu:AddSpacer() - Menu:AddOption("Rename", function() - if (parent.Expanding) then return end - parent.Submit:SetMaterial("icon16/page_edit.png") - parent.Submit:SetTooltip("Rename File") - parent.Desc:SetVisible(false) - parent.Info:SetVisible(false) - parent.FileName.FirstChar = true - parent.FileName.PrevText = parent.FileName:GetValue() - parent.FileName:SetVisible(true) - parent.FileName:SetText(node.Label:GetText()) - parent.FileName:SelectAllOnFocus(true) - parent.FileName:OnMousePressed() - parent.FileName:RequestFocus() - parent.Expanding = true - AdvDupe2.FileBrowser:Slide(true) - parent.Submit.DoClick = function() - local name = parent.FileName:GetValue() - if (name == "") then - AdvDupe2.Notify("Name field is blank.", NOTIFY_ERROR) - parent.FileName:SelectAllOnFocus(true) - parent.FileName:OnGetFocus() - parent.FileName:RequestFocus() - return - end - AddHistory(name) - RenameFileCl(node, name) - AdvDupe2.FileBrowser:Slide(false) - end - parent.FileName.OnEnter = parent.Submit.DoClick - end) - Menu:AddOption("Move File", function() - parent.Submit:SetMaterial("icon16/page_paste.png") - parent.Submit:SetTooltip("Move File") - parent.FileName:SetVisible(false) - parent.Desc:SetVisible(false) - parent.Info:SetText( - "Select the folder you want to move \nthe File to.") - parent.Info:SizeToContents() - parent.Info:SetVisible(true) - AdvDupe2.FileBrowser:Slide(true) - node.Control.ActionNode = node - parent.Submit.DoClick = function() - MoveFileClient(node.Control.m_pSelectedItem) - end - end) - Menu:AddOption("Delete", function() - parent.Submit:SetMaterial("icon16/bin_empty.png") - parent.Submit:SetTooltip("Delete File") - parent.FileName:SetVisible(false) - parent.Desc:SetVisible(false) - if (#node.Label:GetText() > 22) then - parent.Info:SetText( - 'Are you sure that you want to delete \nthe FILE, "' .. - node.Label:GetText() .. '" \nfrom your CLIENT?') - else - parent.Info:SetText( - 'Are you sure that you want to delete \nthe FILE, "' .. - node.Label:GetText() .. '" from your CLIENT?') - end - parent.Info:SizeToContents() - parent.Info:SetVisible(true) - AdvDupe2.FileBrowser:Slide(true) - parent.Submit.DoClick = function() - local path, area = GetNodePath(node) - if (area == 1) then - path = "-Public-/" .. path - end - if (area == 2) then - path = "adv_duplicator/" .. path .. ".txt" - else - path = AdvDupe2.DataFolder .. "/" .. path .. ".txt" - end - node.Control:RemoveNode(node) - file.Delete(path) - AdvDupe2.FileBrowser:Slide(false) - end - end) - end - else - if (root ~= "-Advanced Duplicator 1-") then - Menu:AddOption("Save", function() - if (parent.Expanding) then return end - parent.Submit:SetMaterial("icon16/page_save.png") - parent.Submit:SetTooltip("Save Duplication") - if (parent.FileName:GetValue() == "Folder_Name...") then - parent.FileName:SetText("File_Name...") - end - parent.Desc:SetVisible(true) - parent.Info:SetVisible(false) - parent.FileName.FirstChar = true - parent.FileName.PrevText = parent.FileName:GetValue() - parent.FileName:SetVisible(true) - parent.FileName:SelectAllOnFocus(true) - parent.FileName:OnMousePressed() - parent.FileName:RequestFocus() - node.Control.ActionNode = node - parent.Expanding = true - AdvDupe2.FileBrowser:Slide(true) - parent.Submit.DoClick = function() - local name = parent.FileName:GetValue() - if (name == "" or name == "File_Name...") then - AdvDupe2.Notify("Name field is blank.", NOTIFY_ERROR) - parent.FileName:SelectAllOnFocus(true) - parent.FileName:OnGetFocus() - parent.FileName:RequestFocus() - return - end - local desc = parent.Desc:GetValue() - if (desc == "Description...") then - desc = "" - end - AdvDupe2.SavePath = GetFullPath(node) .. name - AddHistory(name) - if (game.SinglePlayer()) then - RunConsoleCommand("AdvDupe2_SaveFile", name, desc, GetNodePath(node)) - else - RunConsoleCommand("AdvDupe2_SaveFile", name) - end - AdvDupe2.FileBrowser:Slide(false) - end - parent.FileName.OnEnter = - function() - parent.FileName:KillFocus() - parent.Desc:SelectAllOnFocus(true) - parent.Desc.OnMousePressed() - parent.Desc:RequestFocus() - end - parent.Desc.OnEnter = parent.Submit.DoClick - end) - end - Menu:AddOption("New Folder", function() - if (parent.Expanding) then return end - parent.Submit:SetMaterial("icon16/folder_add.png") - parent.Submit:SetTooltip("Add new folder") - if (parent.FileName:GetValue() == "File_Name...") then - parent.FileName:SetText("Folder_Name...") - end - parent.Desc:SetVisible(false) - parent.Info:SetVisible(false) - parent.FileName.FirstChar = true - parent.FileName.PrevText = parent.FileName:GetValue() - parent.FileName:SetVisible(true) - parent.FileName:SelectAllOnFocus(true) - parent.FileName:OnMousePressed() - parent.FileName:RequestFocus() - parent.Expanding = true - AdvDupe2.FileBrowser:Slide(true) - parent.Submit.DoClick = function() AddNewFolder(node) end - parent.FileName.OnEnter = parent.Submit.DoClick - end) - Menu:AddOption("Search", function() - parent.Submit:SetMaterial("icon16/find.png") - parent.Submit:SetTooltip("Search Files") - if (parent.FileName:GetValue() == "Folder_Name...") then - parent.FileName:SetText("File_Name...") - end - parent.Desc:SetVisible(false) - parent.Info:SetVisible(false) - parent.FileName.FirstChar = true - parent.FileName.PrevText = parent.FileName:GetValue() - parent.FileName:SetVisible(true) - parent.FileName:SelectAllOnFocus(true) - parent.FileName:OnMousePressed() - parent.FileName:RequestFocus() - parent.Expanding = true - AdvDupe2.FileBrowser:Slide(true) - parent.Submit.DoClick = function() - Search(node, string.lower(parent.FileName:GetValue())) - AddHistory(parent.FileName:GetValue()) - parent.FileName:SetVisible(false) - parent.Submit:SetMaterial("icon16/arrow_undo.png") - parent.Submit:SetTooltip("Return to Browser") - parent.Info:SetVisible(true) - parent.Info:SetText(#parent.Search.pnlCanvas.Files .. - ' files found searching for, "' .. - parent.FileName:GetValue() .. '"') - parent.Info:SizeToContents() - parent.Submit.DoClick = function() - parent.Search:Remove() - parent.Search = nil - parent.Browser:SetVisible(true) - AdvDupe2.FileBrowser:Slide(false) - parent.Cancel:SetVisible(true) - end - parent.Cancel:SetVisible(false) - end - parent.FileName.OnEnter = parent.Submit.DoClick - end) - if (node.Label:GetText()[1] ~= "-") then - Menu:AddOption("Delete", function() - parent.Submit:SetMaterial("icon16/bin_empty.png") - parent.Submit:SetTooltip("Delete Folder") - parent.FileName:SetVisible(false) - parent.Desc:SetVisible(false) - if (#node.Label:GetText() > 22) then - parent.Info:SetText( - 'Are you sure that you want to delete \nthe FOLDER, "' .. - node.Label:GetText() .. '" \nfrom your CLIENT?') - else - parent.Info:SetText( - 'Are you sure that you want to delete \nthe FOLDER, "' .. - node.Label:GetText() .. '" from your CLIENT?') - end - parent.Info:SizeToContents() - parent.Info:SetVisible(true) - AdvDupe2.FileBrowser:Slide(true) - parent.Submit.DoClick = function() - local path, area = GetNodePath(node) - if (area == 1) then - path = "-Public-/" .. path - end - if (area == 2) then - path = "adv_duplicator/" .. path .. "/" - else - path = AdvDupe2.DataFolder .. "/" .. path .. "/" - end - node.Control:RemoveNode(node) - DeleteFilesInFolders(path) - AdvDupe2.FileBrowser:Slide(false) - end - end) - end + pnFileBr.Search:SetPos(pnFileBr.Browser:GetPos()) + pnFileBr.Search:SetSize(pnFileBr.Browser:GetSize()) + pnFileBr.Search.pnlCanvas.Search = true + pnFileBr.Browser:SetVisible(false) + local Files = SearchNodes(node, name) + tableSortNodes(Files) + for k, v in pairs(Files) do + pnFileBr.Search.pnlCanvas:AddFile(v.Label:GetText()).Ref = v end - if (not node.Control.Search) then - Menu:AddSpacer() - Menu:AddOption("Collapse Folder", function() - if (node.ParentNode.ParentNode) then - node.ParentNode:SetExpanded(false) +end + +function BROWSER:DoNodeRightClick(Node) + self:SetSelected(node) + + local BrowserPanel = self:GetParent():GetParent() + BrowserPanel.FileName:KillFocus() + BrowserPanel.Desc:KillFocus() + + local Menu = DermaMenu() + local RootImpl = Node.Root.RootImpl + + function Menu:AddOption(Text, Func, Icon) + local Option = DMenu.AddOption(self, Text, Func) + if Icon then + local IconPanel = Option:Add("DImage") + IconPanel:SetImage(Icon) + IconPanel:SetKeepAspect(true) + IconPanel:SetSize(16, 16) + local OldLayout = Option.PerformLayout + function Option:PerformLayout(W, H) + OldLayout(self, W, H) + IconPanel:SetPos(6, (H / 2) - 8) end - end) - Menu:AddOption("Collapse Root", function() CollapseParentsComplete(node) end) - if (parent.Expanded) then - Menu:AddOption("Cancel Action", function() parent.Cancel:DoClick() end) end end + RootImpl:UserMenu(self, Node, Menu) + + Menu:SetAlpha(0) + Menu:AlphaTo(255, 0.1, 0) Menu:Open() end @@ -717,91 +940,10 @@ local function CollapseParents(node, val) CollapseParents(node.ParentNode, val) end -function BROWSER:RemoveNode(node) - local parent = node.ParentNode - parent.Nodes = parent.Nodes - 1 - if (node.IsFolder) then - if (node.m_bExpanded) then - CollapseParents(parent, node.ChildList:GetTall() + 20) - for i = 1, #parent.ChildrenExpanded do - if (node == parent.ChildrenExpanded[i]) then - table.remove(parent.ChildrenExpanded, i) - break - end - end - elseif (parent.m_bExpanded) then - CollapseParents(parent, 20) - end - for i = 1, #parent.Folders do - if (node == parent.Folders[i]) then - table.remove(parent.Folders, i) - end - end - node.ChildList:Remove() - node:Remove() - else - for i = 1, #parent.Files do - if (node == parent.Files[i]) then - table.remove(parent.Files, i) - end - end - CollapseParents(parent, 20) - node:Remove() - if (#parent.Files == 0 and #parent.Folders == 0) then - parent.Expander:Remove() - parent.Expander = nil - parent.m_bExpanded = false - end - end - if (self.VBar.Scroll > self.VBar.CanvasSize) then - self.VBar:SetScroll(self.VBar.Scroll) - end - if (self.m_pSelectedItem) then - self.m_pSelectedItem = nil - end -end - function BROWSER:OnMouseWheeled(dlta) return self.VBar:OnMouseWheeled(dlta) end -function BROWSER:AddFolder(text) - local node = vgui.Create("advdupe2_browser_folder", self) - node.Control = self - - node.Offset = 0 - node.ChildrenExpanded = {} - node.Icon:SetPos(18, 1) - node.Label:SetPos(44, 0) - node.Label:SetText(text) - node.Label:SizeToContents() - node.ParentNode = self - node.IsFolder = true - self.Nodes = self.Nodes + 1 - node.Folders = {} - node.Files = {} - table.insert(self.Folders, node) - self:SetTall(self:GetTall() + 20) - - return node -end - -function BROWSER:AddFile(text) - local node = vgui.Create("advdupe2_browser_file", self) - node.Control = self - node.Offset = 0 - node.Icon:SetPos(18, 1) - node.Label:SetPos(44, 0) - node.Label:SetText(text) - node.Label:SizeToContents() - node.ParentNode = self - self.Nodes = self.Nodes + 1 - table.insert(self.Files, node) - self:SetTall(self:GetTall() + 20) - - return node -end - function BROWSER:Sort(node) tableSortNodes(node.Folders) tableSortNodes(node.Files) @@ -819,11 +961,13 @@ function BROWSER:Sort(node) end function BROWSER:SetSelected(node) - if (IsValid(self.m_pSelectedItem)) then - self.m_pSelectedItem:SetSelected(false) + if self.m_pSelectedItem then + self.m_pSelectedItem.Selected = false end self.m_pSelectedItem = node - if (node) then node:SetSelected(true) end + if node then + node.Selected = true + end end local function ExpandParents(node, val) @@ -877,8 +1021,358 @@ function BROWSER:DeleteNode() self:RemoveNode(self.ActionNode) end +local DoRecursiveVistesting function DoRecursiveVistesting(Parent, ExpandedNodeArray, Depth) + Depth = Depth or 0 + for _, Child in Parent:GetSortedChildNodes() do + Child.Depth = Depth + ExpandedNodeArray[#ExpandedNodeArray + 1] = Child + if Child.Expanded then + DoRecursiveVistesting(Child, ExpandedNodeArray, Depth + 1) + end + end +end + +-- Just in case this needs to be changed later +-- can't remember if this should be curtime or not... +local MaxTimeToDoubleClick = 0.1 +local NodeTall = 24 +local NodePadding = 0 +local TallOfOneNode = NodeTall + NodePadding +local NodeDepthWidth = 12 +local NodeFont = "DermaDefault" + +local ICON_FOLDER_EMPTY = Material("icon16/folder.png", "smooth") +local ICON_FOLDER_CONTAINS = Material("icon16/folder_page.png", "smooth") +local ICON_FILE = Material("icon16/page.png", "smooth") + +-- This function collapses the current node state into a single sequential array +function BROWSER:SortRecheck() + if not self.SortDirty then return end + + table.Empty(self.ExpandedNodeArray) + DoRecursiveVistesting(self, self.ExpandedNodeArray) + + -- This is how tall we are + local Tall = #self.ExpandedNodeArray * TallOfOneNode + self:SetTall(Tall) + + self.SortDirty = false +end + +-- Gets or creates the immediate state table. +function BROWSER:GetImmediateState() + local ImmediateState = self.ImmediateState + if not ImmediateState then + ImmediateState = {} + self.ImmediateState = ImmediateState + ImmediateState.Mouse = {} + + -- functions + + function ImmediateState:IsMouseInRect(X, Y, W, H) + local MX, MY = self.MouseX or 0, self.MouseY or 0 + return (MX >= X and MX <= (X + W)) and (MY >= Y and MY <= (Y + H)) + end + end + + return ImmediateState +end + +local function GetNodeBounds(ScrollOffset, AbsIndex, Width, Depth) + return + Depth * NodeDepthWidth, + (TallOfOneNode * (AbsIndex - 1)) - ScrollOffset, + Width - (Depth * NodeDepthWidth), + NodeTall +end + +local ExpanderSize = 16 +local IconSize = 16 +local LeftmostToExpanderPadding = 4 +local ExpanderToIconPadding = 4 +local IconToTextPadding = 6 + +local ExpanderXOffset = LeftmostToExpanderPadding +local IconXOffset = ExpanderXOffset + ExpanderSize + ExpanderToIconPadding +local TextXOffset = IconXOffset + IconSize + IconToTextPadding + +local function GetExpanderBounds(X, Y, W, H, Padding, Depth) + Padding = Padding or 0 + local Size = ExpanderSize + Padding + + return (Depth * NodeDepthWidth) + (X + ExpanderXOffset) - (Padding / 2), ((Y + (H / 2)) - (Size / 2)) + 1, Size, Size +end + +local function GetIconBounds(X, Y, W, H, Padding, Depth) + local Size = IconSize + (Padding or 0) + + return (Depth * NodeDepthWidth) + X + IconXOffset, (Y + (H / 2)) - (Size / 2), Size, Size +end + +local function GetTextPosition(X, Y, W, H, Depth) + return (Depth * NodeDepthWidth) + X + TextXOffset, Y + (H / 2) +end + +-- This function flushes in the immediate-mode state from C-funcs into Lua-land +-- and performs calculations that may be needed later on in a cached state +-- The immediate state object is unique to the browser +function BROWSER:FlushImmediateState() + local ImmediateState = self:GetImmediateState() + + local Scroll = IsValid(self.VBar) and (self.VBar:GetScroll()) or 0 + + local MouseX, MouseY = self:CursorPos() + + ImmediateState.Mouse.Cursor = "arrow" + ImmediateState.LastScroll = ImmediateState.Scroll or Scroll + ImmediateState.Scroll = Scroll + ImmediateState.DeltaScroll = Scroll - ImmediateState.LastScroll + + ImmediateState.Width = self:GetWide() + ImmediateState.Height = self:GetTall() + + for I = MOUSE_LEFT, MOUSE_LAST do + local Mouse = ImmediateState.Mouse[I] + if not Mouse then + Mouse = {} + ImmediateState.Mouse[I] = Mouse + end + + Mouse.LastDown = Mouse.Down or false + Mouse.Down = input.IsMouseDown(I) + Mouse.Clicked = Mouse.Down and not Mouse.LastDown + Mouse.Released = not Mouse.Down and Mouse.LastDown + + end + + ImmediateState.Mouse.Down = false + ImmediateState.Mouse.Double = false + ImmediateState.Mouse.Clicked = false + ImmediateState.Mouse.Released = false + -- Reverse priority. Left should have the highest precedence + for I = MOUSE_LAST, MOUSE_LEFT, -1 do + local Mouse = ImmediateState.Mouse[I] + + if Mouse.Down then ImmediateState.Mouse.Down = true end + if Mouse.Double then ImmediateState.Mouse.Double = true end + if Mouse.Clicked then ImmediateState.Mouse.Clicked = I end + if Mouse.Released then ImmediateState.Mouse.Released = I end + end + + -- Mouse positions + ImmediateState.LastMouseX = ImmediateState.MouseX or MouseX + ImmediateState.MouseX = MouseX + ImmediateState.DeltaX = MouseX - ImmediateState.LastMouseX + + ImmediateState.LastMouseY = ImmediateState.MouseY or MouseY + ImmediateState.MouseY = MouseY + ImmediateState.DeltaY = MouseY - ImmediateState.LastMouseY + + ImmediateState.ReleasedNode = nil + ImmediateState.PanelHovered = self:IsHovered() + + -- The starting and end indices into self.ExpandedNodeArray + ImmediateState.StartIndex = math.max(math.floor( Scroll / TallOfOneNode) + 1, 1) + ImmediateState.EndIndex = math.min(math.floor((Scroll + self:GetParent():GetTall()) / TallOfOneNode) + 1, #self.ExpandedNodeArray) + + -- Vis testing parameters + ImmediateState.TotalVisibleNodes = (ImmediateState.EndIndex - ImmediateState.StartIndex) + -- We test against this array subspan for mouse events + local BreakInputTesting = false + for AbsIndex = ImmediateState.StartIndex, ImmediateState.EndIndex do + local Node = self.ExpandedNodeArray[AbsIndex] + + if not BreakInputTesting then + local X, Y, W, H = GetNodeBounds(ImmediateState.Scroll, AbsIndex, ImmediateState.Width, Node.Depth) + local MouseInRect = ImmediateState:IsMouseInRect(X, Y, W, H) and ImmediateState.PanelHovered + if MouseInRect then + ImmediateState.Hovered = Node + ImmediateState.IsExpanderHovered = Node:IsFolder() and ImmediateState:IsMouseInRect(GetExpanderBounds(X, Y, W, H, nil, Node.Depth)) + + if ImmediateState.Mouse.Clicked then + ImmediateState.Depressed = Node + ImmediateState.IsExpanderDepressed = ImmediateState.IsExpanderHovered + end + + if ImmediateState.Mouse.Released then + ImmediateState.ReleasedNode = ImmediateState.Depressed + ImmediateState.IsExpanderReleased = ImmediateState.IsExpanderHovered + end + + BreakInputTesting = true + else + ImmediateState.Hovered = false + ImmediateState.IsExpanderHovered = false + end + end + end + + if ImmediateState.Mouse.Released or not ImmediateState.Mouse.Down then + ImmediateState.Depressed = nil + ImmediateState.IsExpanderDepressed = nil + end + + if ImmediateState.IsExpanderHovered then + ImmediateState.Mouse.Cursor = "hand" + end +end + +-- This function considers the current immediate state and triggers events/sets cursor +function BROWSER:ConsiderCurrentState() + local ImmediateState = self.ImmediateState + + -- Clicked for node logic, released for expander logic + local Clicked = ImmediateState.Mouse.Clicked + local Released = ImmediateState.Mouse.Released + + if Clicked then + local Node = ImmediateState.Depressed + local ExpanderDepressed = ImmediateState.IsExpanderDepressed + + if Node and not ExpanderDepressed then + if Clicked == MOUSE_LEFT then + self:DoNodeLeftClick(Node) + elseif Clicked == MOUSE_RIGHT then + self:DoNodeRightClick(Node) + end + end + end + + if Released then + local Node = ImmediateState.ReleasedNode + local ExpanderReleased = ImmediateState.IsExpanderReleased + + if Node and ExpanderReleased then + Node:ToggleExpanded() + end + end + + self:SetCursor(ImmediateState.Mouse.Cursor) +end + +-- This function paints the current immediate state to the DPanel. +function BROWSER:PaintCurrentState() + local Skin = self:GetSkin() + local SkinTex = Skin.tex + + local ImmediateState = self.ImmediateState + local ScrollOffset = ImmediateState.Scroll + local Width = ImmediateState.Width + + for AbsIndex = ImmediateState.StartIndex, ImmediateState.EndIndex do + local Node = self.ExpandedNodeArray[AbsIndex] + + local IsHovered = ImmediateState.Hovered == Node + local IsDepressed = ImmediateState.Depressed == Node + local IsExpanderHovered = IsHovered and ImmediateState.IsExpanderHovered + local IsExpanderDepressed = IsDepressed and ImmediateState.IsExpanderDepressed + + local NX, NY, NW, NH = GetNodeBounds(ScrollOffset, AbsIndex, Width, Node.Depth) + local IX, IY, IW, IH = GetIconBounds(NX, NY, NW, NH, nil, Node.Depth) + local EX, EY, EW, EH = GetExpanderBounds(NX, NY, NW, NH, IsExpanderDepressed and -2 or IsExpanderHovered and 2 or 0, Node.Depth) + local TextX, TextY = GetTextPosition(NX, NY, NW, NH, Node.Depth) + + -- Paint background + if IsDepressed then + SkinTex.Panels.Dark(NX, NY, NW, NH, color_white) + elseif IsHovered then + SkinTex.Panels.Bright(NX, NY, NW, NH, color_white) + else + SkinTex.Panels.Normal(NX, NY, NW, NH, color_white) + end + + -- Paint expander + if Node:IsFolder() and Node:Count() > 0 then + if not Node.Expanded then + SkinTex.TreePlus(EX, EY, EW, EH) + else + SkinTex.TreeMinus(EX, EY, EW, EH) + end + end + + -- Paint icon + local Icon + if Node:IsFolder() then + Icon = Node:Count() > 0 and ICON_FOLDER_CONTAINS or ICON_FOLDER_EMPTY + else + Icon = ICON_FILE + end + + surface.SetMaterial(Icon) + surface.DrawTexturedRect(IX, IY, IW, IH) + + -- Paint text + draw.SimpleText(Node.Text or "", NodeFont, TextX, TextY, Skin.colTextEntryText or color_black, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) + end +end + +function BROWSER:Think() + -- Perform a sort recheck ... + self:SortRecheck() + -- ... then flush the current state ... + self:FlushImmediateState() + -- ... then consider the current state. + self:ConsiderCurrentState() + -- FlushImmediateState fetches information from the VGUI panel and tests against that information. + -- ConsiderCurrentState, given the work that FlushImmediateState did, will trigger events and set some + -- VGUI panel data (cursor for example). +end + +function BROWSER:Paint(w, h) + DPanel.Paint(self, w, h) + -- Renders the immediate state to the screen + self:PaintCurrentState() +end + +function BROWSER:AddRootFolder(RootFolderType) + RootFolderType = IRootFolder(RootFolderType or error("RootFolderType must contain a IRootFolder implementation")) -- This checks if the type implemented the interface + local RealNode = self:AddFolder(RootFolderType:GetFolderName()) + + RealNode.Root = RealNode + RealNode.RootImpl = RootFolderType + + RootFolderType:Init(self, RealNode) + + return RealNode +end + derma.DefineControl("advdupe2_browser_tree", "AD2 File Browser", BROWSER, "Panel") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + local FOLDER = {} AccessorFunc(FOLDER, "m_bBackground", "PaintBackground", FORCE_BOOL) @@ -1049,15 +1543,42 @@ function FOLDER:SetSelected(bool) end end -function FOLDER:OnMousePressed(code) - if (code == 107) then - self.Control:DoNodeLeftClick(self) - elseif (code == 108) then - self.Control:DoNodeRightClick(self) - end -end +derma.DefineControl("advdupe2_browser_folder", "AD2 Browser Folder node", {}, "Panel") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -derma.DefineControl("advdupe2_browser_folder", "AD2 Browser Folder node", FOLDER, "Panel") local FILE = {} @@ -1103,6 +1624,39 @@ end derma.DefineControl("advdupe2_browser_file", "AD2 Browser File node", FILE, "Panel") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + local PANEL = {} AccessorFunc(PANEL, "m_bBackground", "PaintBackground", FORCE_BOOL) AccessorFunc(PANEL, "m_bgColor", "BackgroundColor") @@ -1159,7 +1713,6 @@ local function PanelSetSize(self, x, y) end local function UpdateClientFiles() - local pnlCanvas = AdvDupe2.FileBrowser.Browser.pnlCanvas for i = 1, 2 do @@ -1168,27 +1721,15 @@ local function UpdateClientFiles() end end - local advdupe2 = pnlCanvas:AddFolder("Advanced Duplicator 2") - local advdupe1 = pnlCanvas:AddFolder("Advanced Duplicator 1") - - advdupe1:LoadDataFolder("adv_duplicator/") - advdupe2:LoadDataFolder("advdupe2/") + pnlCanvas.Expanded = true - if (pnlCanvas.Folders[2]) then - if (#pnlCanvas.Folders[2].Folders == 0 and #pnlCanvas.Folders[2].Files == 0) then - pnlCanvas:RemoveNode(pnlCanvas.Folders[2]) - end - - pnlCanvas.Folders[1]:SetParent(nil) - pnlCanvas.Folders[1]:SetParent(pnlCanvas.ChildList) - pnlCanvas.Folders[1].ChildList:SetParent(nil) - pnlCanvas.Folders[1].ChildList:SetParent(pnlCanvas.ChildList) - end + pnlCanvas:AddRootFolder(AdvDupe1Folder) + pnlCanvas:AddRootFolder(AdvDupe2Folder) + hook.Run("AdvDupe2_PostMenuFolders", pnlCanvas) end function PANEL:Init() - AdvDupe2.FileBrowser = self self.Expanded = false self.Expanding = false From e144c74ea3c321198518a54c5522225020c4769a Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:30:28 -0700 Subject: [PATCH 05/31] Typo --- lua/advdupe2/file_browser.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 031daa32..b1010fcc 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -903,7 +903,7 @@ local function Search(node, name) end function BROWSER:DoNodeRightClick(Node) - self:SetSelected(node) + self:SetSelected(Node) local BrowserPanel = self:GetParent():GetParent() BrowserPanel.FileName:KillFocus() From 7ab20de52be60e57a063321c8cfc040f5e390b2c Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:25:43 -0700 Subject: [PATCH 06/31] Move GetFilename to its own file Not sure if this is even used serverside and needs to be shared... --- lua/advdupe2/file_browser.lua | 11 ----------- lua/advdupe2/sh_file.lua | 12 ++++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 lua/advdupe2/sh_file.lua diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index b1010fcc..25b0ed81 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -774,17 +774,6 @@ local function CollapseParentsComplete(node) CollapseParentsComplete(node.ParentNode) end -function AdvDupe2.GetFilename(path, overwrite) - if not overwrite and file.Exists(path .. ".txt", "DATA") then - for i = 1, AdvDupe2.FileRenameTryLimit do - local p = string.format("%s_%03d.txt", path, i) - if not file.Exists(p, "DATA") then - return p - end - end - return false - end - return path .. ".txt" end local function RenameFileCl(node, name) diff --git a/lua/advdupe2/sh_file.lua b/lua/advdupe2/sh_file.lua new file mode 100644 index 00000000..65eda5e3 --- /dev/null +++ b/lua/advdupe2/sh_file.lua @@ -0,0 +1,12 @@ +function AdvDupe2.GetFilename(path, overwrite) + if not overwrite and file.Exists(path .. ".txt", "DATA") then + for i = 1, AdvDupe2.FileRenameTryLimit do + local p = string.format("%s_%03d.txt", path, i) + if not file.Exists(p, "DATA") then + return p + end + end + return false + end + return path .. ".txt" +end From a3b14535c41ac96fbde568b1edbe24832dcb065a Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:26:23 -0700 Subject: [PATCH 07/31] Move parameters to convars --- lua/advdupe2/file_browser.lua | 63 +++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 25b0ed81..7401779e 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -17,6 +17,56 @@ local ADVDUPE2_NODETYPE_FILE = AdvDupe2.NODETYPE_FILE local History = {} local Narrow = {} +local Narrow = {} + +-- Just in case this needs to be changed later + +local MaxTimeToDoubleClick, NodeTall, NodePadding, TallOfOneNode, NodeDepthWidth, NodeFont + +local ICON_FOLDER_EMPTY +local ICON_FOLDER_CONTAINS +local ICON_FILE + +local FlushConvars +local UserInterfaceTimeFunc = RealTime + +-- Convars and flushing convars into local registers. +-- FlushConvars gets called in BROWSER:Think() before anything else +do + local MaxTimeToDoubleClick_cv = CreateClientConVar("advdupe2_menu_maxtimetodoubleclick", "0.25", true, false, + "Max time delta between clicks to count as a double click, in seconds.", 0, 1000000) + local NodeTall_cv = CreateClientConVar("advdupe2_menu_nodetall", "24", true, false, + "How tall a single file/directory node is in the file browser, in pixels.", 0, 1000000) + local NodePadding_cv = CreateClientConVar("advdupe2_menu_nodepadding", "0", true, false, + "The height padding inbetween two nodes, in pixels.", 0, 1000000) + + -- The total height of one node, including padding. Use this everywhere + local TallOfOneNode_cv = function() return NodeTall + NodePadding end + -- The width + local NodeDepthWidth_cv = CreateClientConVar("advdupe2_menu_nodedepthwidth", "12", true, false, + "The width of a single node layet, in pixels. For example a file in Advanced Duplicator 2/Folder has a depth of 2, so the pixel width, given the default value, will be (12 * 2) == 24.", 0, 1000000) + local NodeFont_cv = CreateClientConVar("advdupe2_menu_nodefont", "DermaDefault", true, false, + "The surface.CreateFont-registered font the file browser uses.") + + local NodeIconFolderEmpty_cv = CreateClientConVar("advdupe2_menu_nodeicon_folderempty", "icon16/folder.png", true, false, + "The materials/ localized path for an empty folder.") + local NodeIconFolderContains_cv = CreateClientConVar("advdupe2_menu_nodeicon_folder", "icon16/folder_page.png", true, false, + "The materials/ localized path for a folder with contents.") + local NodeIconFile_cv = CreateClientConVar("advdupe2_menu_nodeicon_file", "icon16/page.png", true, false, + "The materials/ localized path for a file.") + ICON_FOLDER_EMPTY = Material(NodeIconFolderEmpty_cv:GetString(), "smooth") + ICON_FOLDER_CONTAINS = Material(NodeIconFolderContains_cv:GetString(), "smooth") + ICON_FILE = Material(NodeIconFile_cv:GetString(), "smooth") + + function FlushConvars() + MaxTimeToDoubleClick = MaxTimeToDoubleClick_cv:GetFloat() + NodeTall = NodeTall_cv:GetFloat() + NodePadding = NodePadding_cv:GetFloat() + TallOfOneNode = TallOfOneNode_cv() + NodeDepthWidth = NodeDepthWidth_cv:GetFloat() + NodeFont = NodeFont_cv:GetString() + end +end local count = 0 @@ -1021,19 +1071,6 @@ local DoRecursiveVistesting function DoRecursiveVistesting(Parent, ExpandedNodeA end end --- Just in case this needs to be changed later --- can't remember if this should be curtime or not... -local MaxTimeToDoubleClick = 0.1 -local NodeTall = 24 -local NodePadding = 0 -local TallOfOneNode = NodeTall + NodePadding -local NodeDepthWidth = 12 -local NodeFont = "DermaDefault" - -local ICON_FOLDER_EMPTY = Material("icon16/folder.png", "smooth") -local ICON_FOLDER_CONTAINS = Material("icon16/folder_page.png", "smooth") -local ICON_FILE = Material("icon16/page.png", "smooth") - -- This function collapses the current node state into a single sequential array function BROWSER:SortRecheck() if not self.SortDirty then return end From 23c3d41083ecfc083a716c2ab80b3e36550e25ed Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:27:10 -0700 Subject: [PATCH 08/31] Switch CurTime to RealTime --- lua/advdupe2/file_browser.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 7401779e..f3b1b88a 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -737,11 +737,11 @@ function BROWSER:Init() self.SortDirty = true self.ExpandedNodeArray = {} - self.LastClick = CurTime() + self.LastClick = UserInterfaceTimeFunc() end function BROWSER:DoNodeLeftClick(Node) - if self.m_pSelectedItem == Node and CurTime() - self.LastClick <= 0.25 then -- Check for double click + if self.m_pSelectedItem == Node and UserInterfaceTimeFunc() - self.LastClick <= MaxTimeToDoubleClick then -- Check for double click if Node:IsFolder() then Node:ToggleExpanded() else @@ -824,6 +824,7 @@ local function CollapseParentsComplete(node) CollapseParentsComplete(node.ParentNode) end + self.LastClick = UserInterfaceTimeFunc() end local function RenameFileCl(node, name) From c0c18ced7173e57c231fa5ac08fa0d809fc921f9 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:27:22 -0700 Subject: [PATCH 09/31] Various other WIP changes --- lua/advdupe2/file_browser.lua | 742 +++++++++++++++++----------------- 1 file changed, 382 insertions(+), 360 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index f3b1b88a..5424bfcd 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -16,7 +16,6 @@ local ADVDUPE2_NODETYPE_FOLDER = AdvDupe2.NODETYPE_FOLDER local ADVDUPE2_NODETYPE_FILE = AdvDupe2.NODETYPE_FILE local History = {} -local Narrow = {} local Narrow = {} -- Just in case this needs to be changed later @@ -219,350 +218,361 @@ derma.DefineControl("advdupe2_browser_panel", "AD2 File Browser", BROWSERPNL, "P local NODE_MT = {} local NODE = setmetatable({}, NODE_MT) +do + function NODE:Init(Type, Browser) + self.Type = Type + self.Browser = Browser + self.Files = {} + self.Folders = {} + self.Sorted = {} + self.Expanded = false + self.Selected = false -function NODE:Init(Type, Browser) - self.Type = Type - self.Browser = Browser - self.Files = {} - self.Folders = {} - self.Sorted = {} - self.Expanded = false - self.Selected = false - - self:MarkSortDirty() -end - -function NODE_MT:__call(Type, Browser) - if Type == nil then return error("Cannot create typeless node") end - if not IsValid(Browser) then return error ("Cannot create a headless node (we need a browser)") end - - local Node = setmetatable({}, {__index = NODE}) - Node:Init(Type, Browser) + self:MarkSortDirty() + end - return Node -end + function NODE_MT:__call(Type, Browser) + if Type == nil then return error("Cannot create typeless node") end + if not IsValid(Browser) then return error ("Cannot create a headless node (we need a browser)") end -function NODE:IsRoot() return (self.Root or error("No root?")) == self end -function NODE:IsFolder() return self.Type == ADVDUPE2_NODETYPE_FOLDER end -function NODE:IsFile() return self.Type == ADVDUPE2_NODETYPE_FILE end + local Node = setmetatable({}, {__index = NODE}) + Node:Init(Type, Browser) -function NODE:AddFolder(Text) - local Node = NODE(ADVDUPE2_NODETYPE_FOLDER, self.Browser) - Node.Text = Text - Node.ParentNode = self - Node.Root = self.Root - if self.Expanded then self:MarkSortDirty() end - self.Folders[#self.Folders + 1] = Node + return Node + end - return Node -end + function NODE:IsRoot() return (self.Root or error("No root?")) == self end + function NODE:IsFolder() return self.Type == ADVDUPE2_NODETYPE_FOLDER end + function NODE:IsFile() return self.Type == ADVDUPE2_NODETYPE_FILE end -function NODE:AddFile(Text) - local Node = NODE(ADVDUPE2_NODETYPE_FILE, self.Browser) - Node.Text = Text - Node.ParentNode = self - Node.Root = self.Root - if self.Expanded then self:MarkSortDirty() end - self.Files[#self.Files + 1] = Node + function NODE:AddFolder(Text) + local Node = NODE(ADVDUPE2_NODETYPE_FOLDER, self.Browser) + Node.Text = Text + Node.ParentNode = self + Node.Root = self.Root + if self.Expanded then self:MarkSortDirty() end + self.Folders[#self.Folders + 1] = Node - return Node -end + return Node + end -function NODE:Count() return #self.Files + #self.Folders end + function NODE:AddFile(Text) + local Node = NODE(ADVDUPE2_NODETYPE_FILE, self.Browser) + Node.Text = Text + Node.ParentNode = self + Node.Root = self.Root + if self.Expanded then self:MarkSortDirty() end + self.Files[#self.Files + 1] = Node -function NODE:MarkSortDirty() - self.SortDirty = true - self.Browser.SortDirty = true -end + return Node + end -function NODE:Clear() - table.Empty(self.Files) - table.Empty(self.Folders) - table.Empty(self.Sorted) - self:MarkSortDirty() -end + function NODE:Count() return #self.Files + #self.Folders end -function NODE:RemoveNode(Node) - if Node:IsFolder() then - table.RemoveByValue(self.Folders, Node) - else - table.RemoveByValue(self.Files, Node) + function NODE:MarkSortDirty() + self.SortDirty = true + self.Browser.SortDirty = true end - self:MarkSortDirty() -end + function NODE:Clear() + table.Empty(self.Files) + table.Empty(self.Folders) + table.Empty(self.Sorted) + self:MarkSortDirty() + end -function NODE:Remove() - local ParentNode = self.ParentNode + function NODE:RemoveNode(Node) + if Node:IsFolder() then + table.RemoveByValue(self.Folders, Node) + else + table.RemoveByValue(self.Files, Node) + end - if ParentNode then - ParentNode:RemoveNode(self) - else self:MarkSortDirty() end -end -function NODE:SetExpanded(Expanded) - if Expanded == self.Expanded then return end + function NODE:Remove() + local ParentNode = self.ParentNode - self.Expanded = Expanded - self:MarkSortDirty() -end - -function NODE:Expand() self:SetExpanded(true) end -function NODE:Collapse() self:SetExpanded(false) end -function NODE:ToggleExpanded() self:SetExpanded(not self.Expanded) end + if ParentNode then + ParentNode:RemoveNode(self) + else + self:MarkSortDirty() + end + end -local function SetupDataFile(Node, Path, Name) - Node.Text = Name - Node.Path = Path -end + function NODE:SetExpanded(Expanded) + if Expanded == self.Expanded then return end -local function SetupDataSubfolder(Node, Path, Name) - Node.Text = Name - Node.Path = Path -end + self.Expanded = Expanded + self:MarkSortDirty() + end --- Expects a directory path ending in a forward slash. -local LoadDataFolderInternal -function LoadDataFolderInternal(Node, Path) - local Files, Directories = file.Find(Path .. "*", "DATA", "nameasc") - if not Files or not Directories then return end + function NODE:Expand() self:SetExpanded(true) end + function NODE:Collapse() self:SetExpanded(false) end + function NODE:ToggleExpanded() self:SetExpanded(not self.Expanded) end - for _, File in ipairs(Files) do - local FilePath = Path .. File - local FileNode = Node:AddFile(FilePath) - SetupDataFile(FileNode, FilePath, File) + local function SetupDataFile(Node, Path, Name) + Node.Text = Name + Node.Path = Path end - for _, Directory in ipairs(Directories) do - local DirectoryPath = Path .. Directory - local DirectoryNode = Node:AddFolder(DirectoryPath) - SetupDataSubfolder(DirectoryNode, DirectoryPath, Directory) - DirectoryNode.FirstObserved = function(DirNode) -- note FirstObserved will be destroyed after first call - LoadDataFolderInternal(DirNode, DirectoryPath .. "/") - end + local function SetupDataSubfolder(Node, Path, Name) + Node.Text = Name + Node.Path = Path end -end + -- Expects a directory path ending in a forward slash. + local LoadDataFolderInternal + function LoadDataFolderInternal(Node, Path) + local Files, Directories = file.Find(Path .. "*", "DATA", "nameasc") + if not Files or not Directories then return end -function NODE:LoadDataFolder(Path) - self:Clear() - LoadDataFolderInternal(self, Path) -end - --- Defines GetNumericalFilename --- May need optimization and refactoring later - especially for non-ASCII strings... --- This handles things very similarly to how Windows does in terms of sorting, but also adds sorting by month --- May also be a good idea in the future to add a setting for the above functionality. -local GetNumericalFilename -do - local isDigit = { - ['0'] = 0, - ['1'] = 1, - ['2'] = 2, - ['3'] = 3, - ['4'] = 4, - ['5'] = 5, - ['6'] = 6, - ['7'] = 7, - ['8'] = 8, - ['9'] = 9 - } - - -- faster than string.byte calls - local char2byte = {} - for i = 1, 255 do char2byte[string.char(i)] = string.byte(string.lower(string.char(i))) end - char2byte['_'] = 2000 - - local buildMonth = {} - for k, v in ipairs{"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"} do - local tbl = buildMonth - for i = 1, #v do - local c = v[i] - if i == #v then - tbl[c] = k - else - if not tbl[c] then - tbl[c] = {} - end + for _, File in ipairs(Files) do + local FilePath = Path .. File + local FileNode = Node:AddFile(FilePath) + SetupDataFile(FileNode, FilePath, File) + end - tbl = tbl[c] + for _, Directory in ipairs(Directories) do + local DirectoryPath = Path .. Directory + local DirectoryNode = Node:AddFolder(DirectoryPath) + SetupDataSubfolder(DirectoryNode, DirectoryPath, Directory) + DirectoryNode.FirstObserved = function(DirNode) -- note FirstObserved will be destroyed after first call + LoadDataFolderInternal(DirNode, DirectoryPath .. "/") end end end - local numericalStore = {} - function GetNumericalFilename(name) - if numericalStore[name] then return numericalStore[name] end - local ret = {} - local digit = nil - local monthTester = buildMonth - local monthStoreJustInCase = {} + function NODE:LoadDataFolder(Path) + self:Clear() + LoadDataFolderInternal(self, Path) + end + + -- Defines GetNumericalFilename + -- May need optimization and refactoring later - especially for non-ASCII strings... + -- This handles things very similarly to how Windows does in terms of sorting, but also adds sorting by month + -- May also be a good idea in the future to add a setting for the above functionality. + local GetNumericalFilename + do + local isDigit = { + ['0'] = 0, + ['1'] = 1, + ['2'] = 2, + ['3'] = 3, + ['4'] = 4, + ['5'] = 5, + ['6'] = 6, + ['7'] = 7, + ['8'] = 8, + ['9'] = 9 + } + + -- faster than string.byte calls + local char2byte = {} + for i = 1, 255 do char2byte[string.char(i)] = string.byte(string.lower(string.char(i))) end + char2byte['_'] = 2000 + + local buildMonth = {} + for k, v in ipairs{"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"} do + local tbl = buildMonth + for i = 1, #v do + local c = v[i] + if i == #v then + tbl[c] = k + else + if not tbl[c] then + tbl[c] = {} + end - for i = 1, #name do - local c = name[i] - local cIsDigit = isDigit[c] - if cIsDigit then - if digit == nil then - digit = 0 + tbl = tbl[c] end - digit = (digit * 10) + cIsDigit - else - if monthTester[c] then - monthTester = monthTester[c] - monthStoreJustInCase[#monthStoreJustInCase + 1] = char2byte[c] - if type(monthTester) == "number" then - local nextC = name[i + 1] - local nextIfine = nextC == ' ' or nextC == '_' or nextC == '-' - if i == #name or nextIfine then - ret[#ret + 1] = monthTester - monthStoreJustInCase = {} - monthTester = buildMonth - if nextIfine then - i = i + 1 + end + end + + local numericalStore = {} + function GetNumericalFilename(name) + if numericalStore[name] then return numericalStore[name] end + + local ret = {} + local digit = nil + local monthTester = buildMonth + local monthStoreJustInCase = {} + + for i = 1, #name do + local c = name[i] + local cIsDigit = isDigit[c] + if cIsDigit then + if digit == nil then + digit = 0 + end + digit = (digit * 10) + cIsDigit + else + if monthTester[c] then + monthTester = monthTester[c] + monthStoreJustInCase[#monthStoreJustInCase + 1] = char2byte[c] + if type(monthTester) == "number" then + local nextC = name[i + 1] + local nextIfine = nextC == ' ' or nextC == '_' or nextC == '-' + if i == #name or nextIfine then + ret[#ret + 1] = monthTester + monthStoreJustInCase = {} + monthTester = buildMonth + if nextIfine then + i = i + 1 + end + else + for i = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i] + end + monthStoreJustInCase = {} + monthTester = buildMonth end - else + end + elseif digit ~= nil then + ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) + digit = nil + else + if monthTester ~= buildMonth then for i = 1, #monthStoreJustInCase do ret[#ret + 1] = monthStoreJustInCase[i] end monthStoreJustInCase = {} monthTester = buildMonth end + ret[#ret + 1] = char2byte[c] end - elseif digit ~= nil then - ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) - digit = nil - else - if monthTester ~= buildMonth then - for i = 1, #monthStoreJustInCase do - ret[#ret + 1] = monthStoreJustInCase[i] - end - monthStoreJustInCase = {} - monthTester = buildMonth - end - ret[#ret + 1] = char2byte[c] end end - end - if digit ~= nil then - ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) - end - if monthTester ~= buildMonth then - for i = 1, #monthStoreJustInCase do - ret[#ret + 1] = monthStoreJustInCase[i] + if digit ~= nil then + ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) + end + if monthTester ~= buildMonth then + for i = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i] + end end - end - numericalStore[name] = ret -- store so this doesnt have to be calculated multiple times for no reason - return ret + numericalStore[name] = ret -- store so this doesnt have to be calculated multiple times for no reason + return ret + end end -end -function NODE.SortFunction(A, B) - local IsFileA, IsFileB = A:IsFile(), B:IsFile() + function NODE.SortFunction(A, B) + local IsFileA, IsFileB = A:IsFile(), B:IsFile() - if not IsFileA and IsFileB then return true end - if IsFileA and not IsFileB then return false end + if not IsFileA and IsFileB then return true end + if IsFileA and not IsFileB then return false end - local NameA, NameB = GetNumericalFilename(string.StripExtension(A.Text)), GetNumericalFilename(string.StripExtension(B.Text)) + local NameA, NameB = GetNumericalFilename(string.StripExtension(A.Text)), GetNumericalFilename(string.StripExtension(B.Text)) - for I = 1, math.max(#NameA, #NameB) do - local AC, BC = NameA[I], NameB[I] + for I = 1, math.max(#NameA, #NameB) do + local AC, BC = NameA[I], NameB[I] - if AC == nil then return true end - if BC == nil then return false end + if AC == nil then return true end + if BC == nil then return false end - if AC ~= BC then - return AC < BC + if AC ~= BC then + return AC < BC + end end end -end -function NODE:PerformResort() - if not self.SortDirty then return end + function NODE:PerformResort() + if not self.SortDirty then return end - local Sorted = self.Sorted - local Files = self.Files - local Folders = self.Folders - table.Empty(Sorted) + local Sorted = self.Sorted + local Files = self.Files + local Folders = self.Folders + table.Empty(Sorted) - for I = 1, #Files do Sorted[#Sorted + 1] = Files[I] end - for I = 1, #Folders do Sorted[#Sorted + 1] = Folders[I] end + for I = 1, #Files do Sorted[#Sorted + 1] = Files[I] end + for I = 1, #Folders do Sorted[#Sorted + 1] = Folders[I] end - -- For each node, check FirstObserved and call if it exists - for _, Node in ipairs(Sorted) do - if Node.FirstObserved then - Node:FirstObserved() - Node.FirstObserved = nil + -- For each node, check FirstObserved and call if it exists + for _, Node in ipairs(Sorted) do + if Node.FirstObserved then + Node:FirstObserved() + Node.FirstObserved = nil + end end + + -- Perform actual resort + table.sort(Sorted, self.SortFunction) end - -- Perform actual resort - table.sort(Sorted, self.SortFunction) -end + -- Returns an enumerator + function NODE:GetSortedChildNodes() + self:PerformResort() + return ipairs(self.Sorted) + end --- Returns an enumerator -function NODE:GetSortedChildNodes() - self:PerformResort() - return ipairs(self.Sorted) -end + function NODE.InjectIntoBrowser(Browser) + for FuncName, Func in pairs(NODE) do + Browser[FuncName] = Func + end -function NODE.InjectIntoBrowser(Browser) - for FuncName, Func in pairs(NODE) do - Browser[FuncName] = Func + NODE.Init(Browser, ADVDUPE2_NODETYPE_FOLDER, Browser) end - - NODE.Init(Browser, ADVDUPE2_NODETYPE_FOLDER, Browser) end - - - -- This interface describes the logic behind a root folder (like AdvDupe1 or AdvDupe2). local IRootFolder_MT = {} local IRootFolder = setmetatable({}, IRootFolder_MT) -AdvDupe2.IRootFolder = IRootFolder -- If other addons want to post-verify their IRootFolder implementations like we do - --- todo; debug.getinfo and determine argument counts to further sanity check? -IRootFolder.Init = function(Impl, Browser, Node) end -IRootFolder.GetFolderName = function(Impl) end --- These define node operations --- These are RAW operations, as in the underlying Browser might do some prompts first --- But for example, calling IRootFolder:UserDelete() is expected to actually delete the node --- (and the browser will create the prompt) -IRootFolder.UserUpload = function(Impl, Browser, Node) end -IRootFolder.UserPreview = function(Impl, Browser, Node) end -IRootFolder.UserSave = function(Impl, Browser, Node, Filename, Description) end -IRootFolder.UserRename = function(Impl, Browser, Node, RenameTo) end -IRootFolder.UserMenu = function(Impl, Browser, Node, Menu) end -IRootFolder.UserDelete = function(Impl, Browser, Node) end - --- Ensures the implementor implemented the interface correctly --- if they didn't throw non-halting errors since it might be an optional method -function IRootFolder_MT:__call(RootFolderType) - for FuncName, _ in pairs(IRootFolder) do - if not RootFolderType[FuncName] then - ErrorNoHaltWithStack("AdvDupe2: IRootFolder implementation failed to implement " .. FuncName .. ", this may not work as intended...") + +do + AdvDupe2.IRootFolder = IRootFolder -- If other addons want to post-verify their IRootFolder implementations like we do + + -- todo; debug.getinfo and determine argument counts to further sanity check? + IRootFolder.Init = function(Impl, Browser, Node) end + IRootFolder.GetFolderName = function(Impl) end + -- These define node operations + -- These are RAW operations, as in the underlying Browser might do some prompts first + -- But for example, calling IRootFolder:UserDelete() is expected to actually delete the node + -- (and the browser will create the prompt) + IRootFolder.UserUpload = function(Impl, Browser, Node) end + IRootFolder.UserPreview = function(Impl, Browser, Node) end + IRootFolder.UserSave = function(Impl, Browser, Node, Filename, Description) end + IRootFolder.UserRename = function(Impl, Browser, Node, RenameTo) end + IRootFolder.UserMenu = function(Impl, Browser, Node, Menu) end + IRootFolder.UserDelete = function(Impl, Browser, Node) end + + -- Ensures the implementor implemented the interface correctly + -- if they didn't throw non-halting errors since it might be an optional method + function IRootFolder_MT:__call(RootFolderType) + for FuncName, _ in pairs(IRootFolder) do + if not RootFolderType[FuncName] then + ErrorNoHaltWithStack("AdvDupe2: IRootFolder implementation failed to implement " .. FuncName .. ", this may not work as intended...") + end end - end - return RootFolderType + return RootFolderType + end end +-- This is a user prompt class, see Browser's UserPrompt stack methods +local USERPROMPT_MT = {} +local USERPROMPT = setmetatable({}, USERPROMPT_MT) +do + function USERPROMPT:Init(Browser) + self.Browser = Browser + self.Blocking = false + end + function USERPROMPT_MT:__call(Browser) + if not IsValid(Browser) then return error ("Cannot create a headless node (we need a browser)") end + local Node = setmetatable({}, {__index = USERPROMPT}) + Node:Init(Browser) - - - - - + return Node + end +end -- This turns a data-folder path name into something AdvDupe2.UploadFile can tolerate local function GetNodeDataPath(Node) @@ -612,7 +622,7 @@ local function OpenPreview(Node, Area) end - +-- These are the builtin IRootFolder implementations. local AdvDupe1Folder, AdvDupe2Folder do @@ -679,17 +689,17 @@ do function AdvDupe2Folder:UserMenu(Browser, Node, Menu) if Node:IsFile() then - Menu:AddOption("Open", function() self:UserUpload(Browser, Node) end, "icon16/page_go.png") + Menu:AddOption("Open", function() self:UserUpload(Browser, Node) end, "icon16/page_go.png") Menu:AddOption("Preview", function() self:UserPreview(Browser, Node) end, "icon16/information.png") Menu:AddSpacer() - Menu:AddOption("Rename...", nil, "icon16/textfield_rename.png") - Menu:AddOption("Move...", nil, "icon16/arrow_right.png") - Menu:AddOption("Delete", nil, "icon16/bin_closed.png") + Menu:AddOption("Rename...", function() Browser:StartRename(Node) end, "icon16/textfield_rename.png") + Menu:AddOption("Move...", function() Browser:StartMove(Node) end, "icon16/arrow_right.png") + Menu:AddOption("Delete", function() Browser:StartDelete(Node) end, "icon16/bin_closed.png") else - Menu:AddOption("Save", nil, "icon16/disk.png") - Menu:AddOption("New Folder", nil, "icon16/folder_add.png") + Menu:AddOption("Save", function() Browser:StartSave(Node) end, "icon16/disk.png") + Menu:AddOption("New Folder", function() Browser:StartFolder(Node) end, "icon16/folder_add.png") Menu:AddSpacer() - Menu:AddOption("Search", nil, "icon16/magnifier.png") + Menu:AddOption("Search", function() Browser:StartSearch(Node) end, "icon16/magnifier.png") end end @@ -700,20 +710,7 @@ do IRootFolder(AdvDupe2Folder) -- validation end - - - - - - - - - - - - - - +-- This is the base browser panel. Most VGUI interactions happen here local BROWSER = {} AccessorFunc(BROWSER, "m_pSelectedItem", "SelectedItem") @@ -752,78 +749,6 @@ function BROWSER:DoNodeLeftClick(Node) self:SetSelected(Node) -- A node was clicked, select it end - self.LastClick = CurTime() -end - -local function AddNewFolder(node) - local Controller = node.Control:GetParent():GetParent() - local name = Controller.FileName:GetValue() - local char = string.match(name, "[^%w_ ]") - if char then - AdvDupe2.Notify("Name contains invalid character ("..char..")!", NOTIFY_ERROR) - Controller.FileName:SelectAllOnFocus(true) - Controller.FileName:OnGetFocus() - Controller.FileName:RequestFocus() - return - end - if (name == "" or name == "Folder_Name...") then - AdvDupe2.Notify("Name is blank!", NOTIFY_ERROR) - Controller.FileName:SelectAllOnFocus(true) - Controller.FileName:OnGetFocus() - Controller.FileName:RequestFocus() - return - end - local path, area = GetNodePath(node) - if (area == 0) then - path = AdvDupe2.DataFolder .. "/" .. path .. "/" .. name - elseif (area == 1) then - path = AdvDupe2.DataFolder .. "/=Public=/" .. path .. "/" .. name - else - path = "adv_duplicator/" .. path .. "/" .. name - end - - if (file.IsDir(path, "DATA")) then - AdvDupe2.Notify("Folder name already exists.", NOTIFY_ERROR) - Controller.FileName:SelectAllOnFocus(true) - Controller.FileName:OnGetFocus() - Controller.FileName:RequestFocus() - return - end - file.CreateDir(path) - - local Folder = node:AddFolder(name) - node.Control:Sort(node) - - if (not node.m_bExpanded) then - node:SetExpanded() - end - - node.Control:SetSelected(Folder) - if (Controller.Expanded) then - AdvDupe2.FileBrowser:Slide(false) - end -end - -local function CollapseChildren(node) - node.m_bExpanded = false - if (node.Expander) then - node.Expander:SetExpanded(false) - node.ChildList:SetTall(0) - for i = 1, #node.ChildrenExpanded do - CollapseChildren(node.ChildrenExpanded[i]) - end - node.ChildrenExpanded = {} - end -end - -local function CollapseParentsComplete(node) - if (not node.ParentNode.ParentNode) then - node:SetExpanded(false) - return - end - CollapseParentsComplete(node.ParentNode) -end - self.LastClick = UserInterfaceTimeFunc() end @@ -1144,16 +1069,25 @@ end -- and performs calculations that may be needed later on in a cached state -- The immediate state object is unique to the browser function BROWSER:FlushImmediateState() + self:SetMouseInputEnabled(self:ThinkAboutUserPrompts()) + local ImmediateState = self:GetImmediateState() local Scroll = IsValid(self.VBar) and (self.VBar:GetScroll()) or 0 local MouseX, MouseY = self:CursorPos() + local CanInput = self:IsMouseInputEnabled() + local Now = UserInterfaceTimeFunc() + + ImmediateState.LastTime = ImmediateState.Time or Now + ImmediateState.Time = Now + ImmediateState.DeltaTime = ImmediateState.Time - ImmediateState.LastTime ImmediateState.Mouse.Cursor = "arrow" ImmediateState.LastScroll = ImmediateState.Scroll or Scroll ImmediateState.Scroll = Scroll ImmediateState.DeltaScroll = Scroll - ImmediateState.LastScroll + ImmediateState.CanInput = CanInput ImmediateState.Width = self:GetWide() ImmediateState.Height = self:GetTall() @@ -1166,7 +1100,7 @@ function BROWSER:FlushImmediateState() end Mouse.LastDown = Mouse.Down or false - Mouse.Down = input.IsMouseDown(I) + Mouse.Down = input.IsMouseDown(I) and CanInput Mouse.Clicked = Mouse.Down and not Mouse.LastDown Mouse.Released = not Mouse.Down and Mouse.LastDown @@ -1205,7 +1139,7 @@ function BROWSER:FlushImmediateState() -- Vis testing parameters ImmediateState.TotalVisibleNodes = (ImmediateState.EndIndex - ImmediateState.StartIndex) -- We test against this array subspan for mouse events - local BreakInputTesting = false + local BreakInputTesting = not CanInput -- if can't input, never even do input testing for AbsIndex = ImmediateState.StartIndex, ImmediateState.EndIndex do local Node = self.ExpandedNodeArray[AbsIndex] @@ -1244,6 +1178,86 @@ function BROWSER:FlushImmediateState() end end +function BROWSER:GetUserPromptStack() + local UserPrompts = self.UserPrompts + + if not UserPrompts then + UserPrompts = {} + self.UserPrompts = UserPrompts + end + + return UserPrompts +end + +-- Returns the index you should use for the stack. +function BROWSER:IncrementUserPromptStackPtr() + local StackPtr = self.UserPromptStackPtr + if not StackPtr then StackPtr = 0 self.UserPromptStackPtr = StackPtr end + + StackPtr = StackPtr + 1 + self.UserPromptStackPtr = StackPtr + + return StackPtr +end + +-- Returns the index to remove from the stack. +function BROWSER:DecrementUserPromptStackPtr() + local StackPtr = self.UserPromptStackPtr + if not StackPtr then StackPtr = 0 self.UserPromptStackPtr = StackPtr end + + StackPtr = StackPtr - 1 + if StackPtr < 0 then ErrorNoHaltWithStack("AdvDupe2: User prompt stack underflow???") StackPtr = 0 end + self.UserPromptStackPtr = StackPtr + + return StackPtr + 1 -- +1 because we want to remove what was previously at that stack pointer +end + +function BROWSER:GetUserPromptStackLength() + return self.UserPromptStackPtr or 0 +end + +function BROWSER:PushUserPrompt() + local UserPrompts = self:GetUserPromptStack() + local StackPointer = self:IncrementUserPromptStackPtr() + local Prompt = USERPROMPT() + UserPrompts[StackPointer] = Prompt + return Prompt +end + +function BROWSER:PopUserPrompt() + local UserPrompts = self:GetUserPromptStack() + local StackPointer = self:DecrementUserPromptStackPtr() + local Prompt = UserPrompts[StackPointer] + UserPrompts[StackPointer] = nil + return Prompt +end + +-- Sets input enabled on user prompt stack and determines if user input should be enabled/disabled on the main browser +-- Returns true if input is enabled +function BROWSER:ThinkAboutUserPrompts() + local UserPrompts = self:GetUserPromptStack() + local Blocking = false + + local LastBlocking = false + + for _, Prompt in ipairs(UserPrompts) do + Prompt:SetMouseInputEnabled(true) + if LastBlocking then + LastBlocking:SetMouseInputEnabled(false) + end + + Blocking = Blocking or Prompt.Blocking + + if Prompt.Blocking then + LastBlocking = Prompt + else + LastBlocking = false + end + end + + return not Blocking +end + -- This function considers the current immediate state and triggers events/sets cursor function BROWSER:ConsiderCurrentState() local ImmediateState = self.ImmediateState @@ -1278,7 +1292,7 @@ function BROWSER:ConsiderCurrentState() end -- This function paints the current immediate state to the DPanel. -function BROWSER:PaintCurrentState() +function BROWSER:PaintCurrentState(PanelWidth, PanelHeight) local Skin = self:GetSkin() local SkinTex = Skin.tex @@ -1331,9 +1345,17 @@ function BROWSER:PaintCurrentState() -- Paint text draw.SimpleText(Node.Text or "", NodeFont, TextX, TextY, Skin.colTextEntryText or color_black, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) end + + ImmediateState.BlockingAlpha = math.Clamp((ImmediateState.BlockingAlpha or 0) + (ImmediateState.DeltaTime * 2 * (ImmediateState.CanInput and -1 or 1)), 0, 1) + if ImmediateState.BlockingAlpha > 0 then + local Alpha = math.ease.InOutQuad(ImmediateState.BlockingAlpha) * 255 + surface.SetDrawColor(0, 0, 0, Alpha) + surface.DrawRect(0, 0, PanelWidth, PanelHeight) + end end function BROWSER:Think() + FlushConvars() -- Perform a sort recheck ... self:SortRecheck() -- ... then flush the current state ... @@ -1348,7 +1370,7 @@ end function BROWSER:Paint(w, h) DPanel.Paint(self, w, h) -- Renders the immediate state to the screen - self:PaintCurrentState() + self:PaintCurrentState(w, h) end function BROWSER:AddRootFolder(RootFolderType) From e17bacf58e1c9ce3609bcc431f29789543d966a8 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:28:18 -0700 Subject: [PATCH 10/31] Actually include sh_file.lua --- lua/autorun/client/advdupe2_cl_init.lua | 1 + lua/autorun/server/advdupe2_sv_init.lua | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lua/autorun/client/advdupe2_cl_init.lua b/lua/autorun/client/advdupe2_cl_init.lua index fb306a38..d30a873c 100644 --- a/lua/autorun/client/advdupe2_cl_init.lua +++ b/lua/autorun/client/advdupe2_cl_init.lua @@ -10,6 +10,7 @@ end include( "advdupe2/file_browser.lua" ) include( "advdupe2/sh_codec.lua" ) +include( "advdupe2/sh_file.lua" ) include( "advdupe2/cl_file.lua" ) include( "advdupe2/cl_ghost.lua" ) diff --git a/lua/autorun/server/advdupe2_sv_init.lua b/lua/autorun/server/advdupe2_sv_init.lua index c5781d85..aeeefb92 100644 --- a/lua/autorun/server/advdupe2_sv_init.lua +++ b/lua/autorun/server/advdupe2_sv_init.lua @@ -15,6 +15,7 @@ end AddCSLuaFile( "autorun/client/advdupe2_cl_init.lua" ) AddCSLuaFile( "advdupe2/file_browser.lua" ) AddCSLuaFile( "advdupe2/sh_codec.lua" ) +AddCSLuaFile( "advdupe2/sh_file.lua" ) AddCSLuaFile( "advdupe2/cl_file.lua" ) AddCSLuaFile( "advdupe2/cl_ghost.lua" ) @@ -119,5 +120,6 @@ end) include( "advdupe2/sv_clipboard.lua" ) include( "advdupe2/sh_codec.lua" ) include( "advdupe2/sv_misc.lua" ) +include( "advdupe2/sh_file.lua" ) include( "advdupe2/sv_file.lua" ) include( "advdupe2/sv_ghost.lua" ) From 3669c471b6824f0bbc83d6bbafc617dbf614f7a9 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:28:58 -0700 Subject: [PATCH 11/31] Further strip out unused V1 methods --- lua/advdupe2/file_browser.lua | 115 ---------------------------------- 1 file changed, 115 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 5424bfcd..5c063373 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -752,121 +752,6 @@ function BROWSER:DoNodeLeftClick(Node) self.LastClick = UserInterfaceTimeFunc() end -local function RenameFileCl(node, name) - local path, area = GetNodePath(node) - local File, FilePath, tempFilePath = "", "", "" - if (area == 0) then - tempFilePath = AdvDupe2.DataFolder .. "/" .. path - elseif (area == 1) then - tempFilePath = AdvDupe2.DataFolder .. "/=Public=/" .. path - elseif (area == 2) then - tempFilePath = "adv_duplicator/" .. path - end - - File = file.Read(tempFilePath .. ".txt") - FilePath = AdvDupe2.GetFilename( - string.sub(tempFilePath, 1, #tempFilePath - #node.Label:GetText()) .. name) - - if (not FilePath) then - AdvDupe2.Notify("Rename limit exceeded, could not rename.", NOTIFY_ERROR) - return - end - - FilePath = AdvDupe2.SanitizeFilename(FilePath) - file.Write(FilePath, File) - if (file.Exists(FilePath, "DATA")) then - file.Delete(tempFilePath .. ".txt") - local NewName = string.Explode("/", FilePath) - NewName = string.sub(NewName[#NewName], 1, -5) - node.Label:SetText(NewName) - node.Label:SizeToContents() - AdvDupe2.Notify("File renamed to " .. NewName) - else - AdvDupe2.Notify("File was not renamed.", NOTIFY_ERROR) - end - - node.Control:Sort(node.ParentNode) -end - -local function MoveFileClient(node) - if (not node) then - AdvDupe2.Notify("Select a folder to move the file to.", NOTIFY_ERROR) - return - end - if (node.Derma.ClassName == "advdupe2_browser_file") then - AdvDupe2.Notify("You muse select a folder as a destination.", NOTIFY_ERROR) - return - end - local base = AdvDupe2.DataFolder - local ParentNode - - local node2 = node.Control.ActionNode - local path, area = GetNodePath(node2) - local path2, area2 = GetNodePath(node) - - if (area ~= area2 or path == path2) then - AdvDupe2.Notify("Cannot move files between these directories.", NOTIFY_ERROR) - return - end - if (area == 2) then base = "adv_duplicator" end - - local savepath = AdvDupe2.GetFilename( - base .. "/" .. path2 .. "/" .. node2.Label:GetText()) - local OldFile = base .. "/" .. path .. ".txt" - - local ReFile = file.Read(OldFile) - file.Write(savepath, ReFile) - file.Delete(OldFile) - local name2 = string.Explode("/", savepath) - name2 = string.sub(name2[#name2], 1, -5) - node2.Control:RemoveNode(node2) - node2 = node:AddFile(name2) - node2.Control:Sort(node) - AdvDupe2.FileBrowser:Slide(false) - AdvDupe2.FileBrowser.Info:SetVisible(false) -end - -local function DeleteFilesInFolders(path) - local files, folders = file.Find(path .. "*", "DATA") - - for k, v in pairs(files) do file.Delete(path .. v) end - - for k, v in pairs(folders) do DeleteFilesInFolders(path .. v .. "/") end - - file.Delete(path) -end - -local function SearchNodes(node, name) - local tab = {} - for k, v in pairs(node.Files) do - if (string.find(string.lower(v.Label:GetText()), name)) then - table.insert(tab, v) - end - end - - for k, v in pairs(node.Folders) do - for i, j in pairs(SearchNodes(v, name)) do - table.insert(tab, j) - end - end - - return tab -end - -local function Search(node, name) - local pnFileBr = AdvDupe2.FileBrowser - pnFileBr.Search = vgui.Create("advdupe2_browser_panel", pnFileBr) - pnFileBr.Search:SetPos(pnFileBr.Browser:GetPos()) - pnFileBr.Search:SetSize(pnFileBr.Browser:GetSize()) - pnFileBr.Search.pnlCanvas.Search = true - pnFileBr.Browser:SetVisible(false) - local Files = SearchNodes(node, name) - tableSortNodes(Files) - for k, v in pairs(Files) do - pnFileBr.Search.pnlCanvas:AddFile(v.Label:GetText()).Ref = v - end -end - function BROWSER:DoNodeRightClick(Node) self:SetSelected(Node) From f93bd42d752dbbce00a8841788b4241e1ac14b53 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:30:09 -0700 Subject: [PATCH 12/31] Even more V1 method stripping --- lua/advdupe2/file_browser.lua | 57 ----------------------------------- 1 file changed, 57 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 5c063373..3d6907e3 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -784,12 +784,6 @@ function BROWSER:DoNodeRightClick(Node) Menu:Open() end -local function CollapseParents(node, val) - if (not node) then return end - node.ChildList:SetTall(node.ChildList:GetTall() - val) - CollapseParents(node.ParentNode, val) -end - function BROWSER:OnMouseWheeled(dlta) return self.VBar:OnMouseWheeled(dlta) end @@ -820,57 +814,6 @@ function BROWSER:SetSelected(node) end end -local function ExpandParents(node, val) - if (not node) then return end - node.ChildList:SetTall(node.ChildList:GetTall() + val) - ExpandParents(node.ParentNode, val) -end - -function BROWSER:Expand(node) - node.ChildList:SetTall(node.Nodes * 20) - table.insert(node.ParentNode.ChildrenExpanded, node) - ExpandParents(node.ParentNode, node.Nodes * 20) -end - -local function ExtendParents(node) - if (not node) then return end - node.ChildList:SetTall(node.ChildList:GetTall() + 20) - ExtendParents(node.ParentNode) -end - -function BROWSER:Extend(node) - node.ChildList:SetTall(node.ChildList:GetTall() + 20) - ExtendParents(node.ParentNode) -end - -function BROWSER:Collapse(node) - CollapseParents(node.ParentNode, node.ChildList:GetTall()) - - for i = 1, #node.ParentNode.ChildrenExpanded do - if (node.ParentNode.ChildrenExpanded[i] == node) then - table.remove(node.ParentNode.ChildrenExpanded, i) - break - end - end - CollapseChildren(node) -end - -function BROWSER:RenameNode(name) - self.ActionNode.Label:SetText(name) - self.ActionNode.Label:SizeToContents() - self:Sort(self.ActionNode.ParentNode) -end - -function BROWSER:MoveNode(name) - self:RemoveNode(self.ActionNode) - self.ActionNode2:AddFile(name) - self:Sort(self.ActionNode2) -end - -function BROWSER:DeleteNode() - self:RemoveNode(self.ActionNode) -end - local DoRecursiveVistesting function DoRecursiveVistesting(Parent, ExpandedNodeArray, Depth) Depth = Depth or 0 for _, Child in Parent:GetSortedChildNodes() do From d41d5e49a2049c717b459fb9223c15fc4b3998f7 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 13:49:19 -0700 Subject: [PATCH 13/31] Turn the rest of these into convars --- lua/advdupe2/file_browser.lua | 97 ++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 3d6907e3..acbee6ce 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -21,6 +21,9 @@ local Narrow = {} -- Just in case this needs to be changed later local MaxTimeToDoubleClick, NodeTall, NodePadding, TallOfOneNode, NodeDepthWidth, NodeFont +local ExpanderSize, IconSize, LeftmostToExpanderPadding, ExpanderToIconPadding, IconToTextPadding + +local ExpanderXOffset, IconXOffset, TextXOffset local ICON_FOLDER_EMPTY local ICON_FOLDER_CONTAINS @@ -47,12 +50,36 @@ do local NodeFont_cv = CreateClientConVar("advdupe2_menu_nodefont", "DermaDefault", true, false, "The surface.CreateFont-registered font the file browser uses.") + + local NodeIconFolderEmpty_cv = CreateClientConVar("advdupe2_menu_nodeicon_folderempty", "icon16/folder.png", true, false, "The materials/ localized path for an empty folder.") local NodeIconFolderContains_cv = CreateClientConVar("advdupe2_menu_nodeicon_folder", "icon16/folder_page.png", true, false, "The materials/ localized path for a folder with contents.") local NodeIconFile_cv = CreateClientConVar("advdupe2_menu_nodeicon_file", "icon16/page.png", true, false, "The materials/ localized path for a file.") + + local function CreateNodeTextRepresentation(Label, Offset) + return table.concat{ + Label, "\n\n", + " [+] [i] Advanced Duplicator 2", "\n", + string.rep(" ", Offset), "^\n", + string.rep(" ", Offset), "^--- You are here" + } + end + + local ExpanderSize_cv = CreateClientConVar("advdupe2_menu_nodeexpander_size", "16", true, false, + CreateNodeTextRepresentation("The size, in pixels, for a node expander button.", 4), 0, 1000000) + local IconSize_cv = CreateClientConVar("advdupe2_menu_nodeicon_size", "16", true, false, + CreateNodeTextRepresentation("The size, in pixels, for a folder or file icon.", 9), 0, 1000000) + local LeftmostToExpanderPadding_cv = CreateClientConVar("advdupe2_menu_nodepadding_toexpander", "4", true, false, + CreateNodeTextRepresentation("Distance, in pixels, between the leftmost side of the node and where the node expander is placed.", 1), 0, 1000000) + local ExpanderToIconPadding_cv = CreateClientConVar("advdupe2_menu_nodepadding_expandertoicon", "4", true, false, + CreateNodeTextRepresentation("Distance, in pixels, between the node expander and the node icon.", 7), 0, 1000000) + local IconToTextPadding_cv = CreateClientConVar("advdupe2_menu_nodepadding_icontotext", "6", true, false, + CreateNodeTextRepresentation("Distance, in pixels, between the node icon and the node text.", 12), 0, 1000000) + + ICON_FOLDER_EMPTY = Material(NodeIconFolderEmpty_cv:GetString(), "smooth") ICON_FOLDER_CONTAINS = Material(NodeIconFolderContains_cv:GetString(), "smooth") ICON_FILE = Material(NodeIconFile_cv:GetString(), "smooth") @@ -64,9 +91,44 @@ do TallOfOneNode = TallOfOneNode_cv() NodeDepthWidth = NodeDepthWidth_cv:GetFloat() NodeFont = NodeFont_cv:GetString() + + ExpanderSize = ExpanderSize_cv:GetFloat() + IconSize = IconSize_cv:GetFloat() + LeftmostToExpanderPadding = LeftmostToExpanderPadding_cv:GetFloat() + ExpanderToIconPadding = ExpanderToIconPadding_cv:GetFloat() + IconToTextPadding = IconToTextPadding_cv:GetFloat() + + ExpanderXOffset = LeftmostToExpanderPadding + IconXOffset = ExpanderXOffset + ExpanderSize + ExpanderToIconPadding + TextXOffset = IconXOffset + IconSize + IconToTextPadding end end +local function GetNodeBounds(ScrollOffset, AbsIndex, Width, Depth) + return + Depth * NodeDepthWidth, + (TallOfOneNode * (AbsIndex - 1)) - ScrollOffset, + Width - (Depth * NodeDepthWidth), + NodeTall +end + +local function GetExpanderBounds(X, Y, W, H, Padding, Depth) + Padding = Padding or 0 + local Size = ExpanderSize + Padding + + return (Depth * NodeDepthWidth) + (X + ExpanderXOffset) - (Padding / 2), ((Y + (H / 2)) - (Size / 2)) + 1, Size, Size +end + +local function GetIconBounds(X, Y, W, H, Padding, Depth) + local Size = IconSize + (Padding or 0) + + return (Depth * NodeDepthWidth) + X + IconXOffset, (Y + (H / 2)) - (Size / 2), Size, Size +end + +local function GetTextPosition(X, Y, W, H, Depth) + return (Depth * NodeDepthWidth) + X + TextXOffset, Y + (H / 2) +end + local count = 0 local function AddHistory(txt) @@ -858,41 +920,6 @@ function BROWSER:GetImmediateState() return ImmediateState end -local function GetNodeBounds(ScrollOffset, AbsIndex, Width, Depth) - return - Depth * NodeDepthWidth, - (TallOfOneNode * (AbsIndex - 1)) - ScrollOffset, - Width - (Depth * NodeDepthWidth), - NodeTall -end - -local ExpanderSize = 16 -local IconSize = 16 -local LeftmostToExpanderPadding = 4 -local ExpanderToIconPadding = 4 -local IconToTextPadding = 6 - -local ExpanderXOffset = LeftmostToExpanderPadding -local IconXOffset = ExpanderXOffset + ExpanderSize + ExpanderToIconPadding -local TextXOffset = IconXOffset + IconSize + IconToTextPadding - -local function GetExpanderBounds(X, Y, W, H, Padding, Depth) - Padding = Padding or 0 - local Size = ExpanderSize + Padding - - return (Depth * NodeDepthWidth) + (X + ExpanderXOffset) - (Padding / 2), ((Y + (H / 2)) - (Size / 2)) + 1, Size, Size -end - -local function GetIconBounds(X, Y, W, H, Padding, Depth) - local Size = IconSize + (Padding or 0) - - return (Depth * NodeDepthWidth) + X + IconXOffset, (Y + (H / 2)) - (Size / 2), Size, Size -end - -local function GetTextPosition(X, Y, W, H, Depth) - return (Depth * NodeDepthWidth) + X + TextXOffset, Y + (H / 2) -end - -- This function flushes in the immediate-mode state from C-funcs into Lua-land -- and performs calculations that may be needed later on in a cached state -- The immediate state object is unique to the browser From 9016aa31698c1131ade8413ad50a2b23e7de4b07 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 14:47:41 -0700 Subject: [PATCH 14/31] Slight rethinking --- lua/advdupe2/file_browser.lua | 646 +++++++--------------- lua/weapons/gmod_tool/stools/advdupe2.lua | 2 +- 2 files changed, 196 insertions(+), 452 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index acbee6ce..99f42754 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -12,8 +12,12 @@ local ADVDUPE2_AREA_ADVDUPE2 = AdvDupe2.AREA_ADVDUPE2 local ADVDUPE2_AREA_PUBLIC = AdvDupe2.AREA_PUBLIC local ADVDUPE2_AREA_ADVDUPE1 = AdvDupe2.AREA_ADVDUPE1 -local ADVDUPE2_NODETYPE_FOLDER = AdvDupe2.NODETYPE_FOLDER -local ADVDUPE2_NODETYPE_FILE = AdvDupe2.NODETYPE_FILE +local NODETYPE_FOLDER = AdvDupe2.NODETYPE_FOLDER +local NODETYPE_FILE = AdvDupe2.NODETYPE_FILE + +-- This lets us rip this stuff out if we need to. +local FileBrowserPrefix = "AdvDupe2" +local LowercaseFileBrowserPrefix = string.lower(FileBrowserPrefix) local History = {} local Narrow = {} @@ -35,48 +39,48 @@ local UserInterfaceTimeFunc = RealTime -- Convars and flushing convars into local registers. -- FlushConvars gets called in BROWSER:Think() before anything else do - local MaxTimeToDoubleClick_cv = CreateClientConVar("advdupe2_menu_maxtimetodoubleclick", "0.25", true, false, + local MaxTimeToDoubleClick_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_maxtimetodoubleclick", "0.25", true, false, "Max time delta between clicks to count as a double click, in seconds.", 0, 1000000) - local NodeTall_cv = CreateClientConVar("advdupe2_menu_nodetall", "24", true, false, + local NodeTall_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodetall", "24", true, false, "How tall a single file/directory node is in the file browser, in pixels.", 0, 1000000) - local NodePadding_cv = CreateClientConVar("advdupe2_menu_nodepadding", "0", true, false, + local NodePadding_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodepadding", "0", true, false, "The height padding inbetween two nodes, in pixels.", 0, 1000000) -- The total height of one node, including padding. Use this everywhere local TallOfOneNode_cv = function() return NodeTall + NodePadding end -- The width - local NodeDepthWidth_cv = CreateClientConVar("advdupe2_menu_nodedepthwidth", "12", true, false, - "The width of a single node layet, in pixels. For example a file in Advanced Duplicator 2/Folder has a depth of 2, so the pixel width, given the default value, will be (12 * 2) == 24.", 0, 1000000) - local NodeFont_cv = CreateClientConVar("advdupe2_menu_nodefont", "DermaDefault", true, false, + local NodeDepthWidth_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodedepthwidth", "12", true, false, + "The width of a single node layet, in pixels. For example a file in folder1/folder2 has a depth of 2, so the pixel width, given the default value, will be (12 * 2) == 24.", 0, 1000000) + local NodeFont_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodefont", "DermaDefault", true, false, "The surface.CreateFont-registered font the file browser uses.") - local NodeIconFolderEmpty_cv = CreateClientConVar("advdupe2_menu_nodeicon_folderempty", "icon16/folder.png", true, false, + local NodeIconFolderEmpty_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodeicon_folderempty", "icon16/folder.png", true, false, "The materials/ localized path for an empty folder.") - local NodeIconFolderContains_cv = CreateClientConVar("advdupe2_menu_nodeicon_folder", "icon16/folder_page.png", true, false, + local NodeIconFolderContains_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodeicon_folder", "icon16/folder_page.png", true, false, "The materials/ localized path for a folder with contents.") - local NodeIconFile_cv = CreateClientConVar("advdupe2_menu_nodeicon_file", "icon16/page.png", true, false, + local NodeIconFile_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodeicon_file", "icon16/page.png", true, false, "The materials/ localized path for a file.") local function CreateNodeTextRepresentation(Label, Offset) return table.concat{ Label, "\n\n", - " [+] [i] Advanced Duplicator 2", "\n", + " [+] [i] New folder/file", "\n", string.rep(" ", Offset), "^\n", string.rep(" ", Offset), "^--- You are here" } end - local ExpanderSize_cv = CreateClientConVar("advdupe2_menu_nodeexpander_size", "16", true, false, + local ExpanderSize_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodeexpander_size", "16", true, false, CreateNodeTextRepresentation("The size, in pixels, for a node expander button.", 4), 0, 1000000) - local IconSize_cv = CreateClientConVar("advdupe2_menu_nodeicon_size", "16", true, false, + local IconSize_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodeicon_size", "16", true, false, CreateNodeTextRepresentation("The size, in pixels, for a folder or file icon.", 9), 0, 1000000) - local LeftmostToExpanderPadding_cv = CreateClientConVar("advdupe2_menu_nodepadding_toexpander", "4", true, false, + local LeftmostToExpanderPadding_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodepadding_toexpander", "4", true, false, CreateNodeTextRepresentation("Distance, in pixels, between the leftmost side of the node and where the node expander is placed.", 1), 0, 1000000) - local ExpanderToIconPadding_cv = CreateClientConVar("advdupe2_menu_nodepadding_expandertoicon", "4", true, false, + local ExpanderToIconPadding_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodepadding_expandertoicon", "4", true, false, CreateNodeTextRepresentation("Distance, in pixels, between the node expander and the node icon.", 7), 0, 1000000) - local IconToTextPadding_cv = CreateClientConVar("advdupe2_menu_nodepadding_icontotext", "6", true, false, + local IconToTextPadding_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodepadding_icontotext", "6", true, false, CreateNodeTextRepresentation("Distance, in pixels, between the node icon and the node text.", 12), 0, 1000000) @@ -194,40 +198,6 @@ local function tableSortNodes(tbl) for k, v in ipairs(tbl) do tbl[k] = v[2] end end -local BROWSERPNL = {} -AccessorFunc(BROWSERPNL, "m_bBackground", "PaintBackground", FORCE_BOOL) -AccessorFunc(BROWSERPNL, "m_bgColor", "BackgroundColor") -Derma_Hook(BROWSERPNL, "Paint", "Paint", "Panel") -Derma_Hook(BROWSERPNL, "PerformLayout", "Layout", "Panel") - -local setbrowserpnlsize -local function SetBrowserPnlSize(self, x, y) - setbrowserpnlsize(self, x, y) - self.pnlCanvas:SetWide(x) - self.pnlCanvas.VBar:SetUp(y, self.pnlCanvas:GetTall()) -end - -function BROWSERPNL:Init() - setbrowserpnlsize = self.SetSize - self.SetSize = SetBrowserPnlSize - self.pnlCanvas = vgui.Create("advdupe2_browser_tree", self) - - self:SetPaintBackground(true) - self:SetPaintBackgroundEnabled(false) - self:SetPaintBorderEnabled(false) - self:SetBackgroundColor(self:GetSkin().text_bright) -end - -function BROWSERPNL:OnVScroll(iOffset) - -- self.pnlCanvas:SetPos(0, iOffset) -end - -derma.DefineControl("advdupe2_browser_panel", "AD2 File Browser", BROWSERPNL, "Panel") - - - - - @@ -304,11 +274,11 @@ do end function NODE:IsRoot() return (self.Root or error("No root?")) == self end - function NODE:IsFolder() return self.Type == ADVDUPE2_NODETYPE_FOLDER end - function NODE:IsFile() return self.Type == ADVDUPE2_NODETYPE_FILE end + function NODE:IsFolder() return self.Type == NODETYPE_FOLDER end + function NODE:IsFile() return self.Type == NODETYPE_FILE end function NODE:AddFolder(Text) - local Node = NODE(ADVDUPE2_NODETYPE_FOLDER, self.Browser) + local Node = NODE(NODETYPE_FOLDER, self.Browser) Node.Text = Text Node.ParentNode = self Node.Root = self.Root @@ -319,7 +289,7 @@ do end function NODE:AddFile(Text) - local Node = NODE(ADVDUPE2_NODETYPE_FILE, self.Browser) + local Node = NODE(NODETYPE_FILE, self.Browser) Node.Text = Text Node.ParentNode = self Node.Root = self.Root @@ -571,12 +541,12 @@ do return ipairs(self.Sorted) end - function NODE.InjectIntoBrowser(Browser) + function NODE.InjectIntoBrowser(TreeView, Browser) for FuncName, Func in pairs(NODE) do - Browser[FuncName] = Func + TreeView[FuncName] = Func end - NODE.Init(Browser, ADVDUPE2_NODETYPE_FOLDER, Browser) + NODE.Init(TreeView, NODETYPE_FOLDER, Browser) end end @@ -624,8 +594,12 @@ do function USERPROMPT:Init(Browser) self.Browser = Browser self.Blocking = false + + self.Panel = Browser:Add("DPanel") end + function USERPROMPT:GetPanel() return self.Panel end + function USERPROMPT_MT:__call(Browser) if not IsValid(Browser) then return error ("Cannot create a headless node (we need a browser)") end @@ -634,6 +608,32 @@ do return Node end + + function USERPROMPT:GetBlocking() return self.Blocking or false end + function USERPROMPT:SetBlocking(Blocking) self.Blocking = Blocking and true or false end + + function USERPROMPT:Destroy() + if IsValid(self.Panel) then + self.Panel:Remove() + end + end + + function USERPROMPT:SetDock(Dock) + self.Panel:Dock(Dock) + self.Panel:SetSize(self.Browser:GetTall() / 6) + end + + function USERPROMPT:ThinkAnimations() + + end + + -- Call this to close and pop later. + function USERPROMPT:Close() + self.Blocking = false + timer.Simple(0.5, function() + self.Browser:PopUserPromptByValue(self) + end) + end end -- This turns a data-folder path name into something AdvDupe2.UploadFile can tolerate @@ -774,8 +774,8 @@ end -- This is the base browser panel. Most VGUI interactions happen here -local BROWSER = {} -AccessorFunc(BROWSER, "m_pSelectedItem", "SelectedItem") +local BROWSERTREE = {} +AccessorFunc(BROWSERTREE, "m_pSelectedItem", "SelectedItem") local origSetTall local function SetTall(self, val) @@ -783,29 +783,29 @@ local function SetTall(self, val) self.VBar:SetUp(self:GetParent():GetTall(), self:GetTall()) end -function BROWSER:Init() +function BROWSERTREE:Init() self:SetTall(0) origSetTall = self.SetTall self.SetTall = SetTall - self.VBar = vgui.Create("DVScrollBar", self:GetParent()) + self.VBar = self:GetParent():Add "DVScrollBar" self.VBar:Dock(RIGHT) -- Implement NODE - NODE.InjectIntoBrowser(self) + NODE.InjectIntoBrowser(self, self:GetParent()) self.SortDirty = true self.ExpandedNodeArray = {} self.LastClick = UserInterfaceTimeFunc() end -function BROWSER:DoNodeLeftClick(Node) +function BROWSERTREE:DoNodeLeftClick(Node) if self.m_pSelectedItem == Node and UserInterfaceTimeFunc() - self.LastClick <= MaxTimeToDoubleClick then -- Check for double click if Node:IsFolder() then Node:ToggleExpanded() else local RootImpl = Node.Root.RootImpl - RootImpl:UserUpload(self, Node) + RootImpl:UserUpload(self.Browser, Node) end else self:SetSelected(Node) -- A node was clicked, select it @@ -814,7 +814,7 @@ function BROWSER:DoNodeLeftClick(Node) self.LastClick = UserInterfaceTimeFunc() end -function BROWSER:DoNodeRightClick(Node) +function BROWSERTREE:DoNodeRightClick(Node) self:SetSelected(Node) local BrowserPanel = self:GetParent():GetParent() @@ -839,34 +839,18 @@ function BROWSER:DoNodeRightClick(Node) end end - RootImpl:UserMenu(self, Node, Menu) + RootImpl:UserMenu(self.Browser, Node, Menu) Menu:SetAlpha(0) Menu:AlphaTo(255, 0.1, 0) Menu:Open() end -function BROWSER:OnMouseWheeled(dlta) +function BROWSERTREE:OnMouseWheeled(dlta) return self.VBar:OnMouseWheeled(dlta) end -function BROWSER:Sort(node) - tableSortNodes(node.Folders) - tableSortNodes(node.Files) - - for i = 1, #node.Folders do - node.Folders[i]:SetParent(nil) - node.Folders[i]:SetParent(node.ChildList) - node.Folders[i].ChildList:SetParent(nil) - node.Folders[i].ChildList:SetParent(node.ChildList) - end - for i = 1, #node.Files do - node.Files[i]:SetParent(nil) - node.Files[i]:SetParent(node.ChildList) - end -end - -function BROWSER:SetSelected(node) +function BROWSERTREE:SetSelected(node) if self.m_pSelectedItem then self.m_pSelectedItem.Selected = false end @@ -888,7 +872,7 @@ local DoRecursiveVistesting function DoRecursiveVistesting(Parent, ExpandedNodeA end -- This function collapses the current node state into a single sequential array -function BROWSER:SortRecheck() +function BROWSERTREE:SortRecheck() if not self.SortDirty then return end table.Empty(self.ExpandedNodeArray) @@ -902,7 +886,7 @@ function BROWSER:SortRecheck() end -- Gets or creates the immediate state table. -function BROWSER:GetImmediateState() +function BROWSERTREE:GetImmediateState() local ImmediateState = self.ImmediateState if not ImmediateState then ImmediateState = {} @@ -923,9 +907,7 @@ end -- This function flushes in the immediate-mode state from C-funcs into Lua-land -- and performs calculations that may be needed later on in a cached state -- The immediate state object is unique to the browser -function BROWSER:FlushImmediateState() - self:SetMouseInputEnabled(self:ThinkAboutUserPrompts()) - +function BROWSERTREE:FlushImmediateState() local ImmediateState = self:GetImmediateState() local Scroll = IsValid(self.VBar) and (self.VBar:GetScroll()) or 0 @@ -1033,88 +1015,8 @@ function BROWSER:FlushImmediateState() end end -function BROWSER:GetUserPromptStack() - local UserPrompts = self.UserPrompts - - if not UserPrompts then - UserPrompts = {} - self.UserPrompts = UserPrompts - end - - return UserPrompts -end - --- Returns the index you should use for the stack. -function BROWSER:IncrementUserPromptStackPtr() - local StackPtr = self.UserPromptStackPtr - if not StackPtr then StackPtr = 0 self.UserPromptStackPtr = StackPtr end - - StackPtr = StackPtr + 1 - self.UserPromptStackPtr = StackPtr - - return StackPtr -end - --- Returns the index to remove from the stack. -function BROWSER:DecrementUserPromptStackPtr() - local StackPtr = self.UserPromptStackPtr - if not StackPtr then StackPtr = 0 self.UserPromptStackPtr = StackPtr end - - StackPtr = StackPtr - 1 - if StackPtr < 0 then ErrorNoHaltWithStack("AdvDupe2: User prompt stack underflow???") StackPtr = 0 end - self.UserPromptStackPtr = StackPtr - - return StackPtr + 1 -- +1 because we want to remove what was previously at that stack pointer -end - -function BROWSER:GetUserPromptStackLength() - return self.UserPromptStackPtr or 0 -end - -function BROWSER:PushUserPrompt() - local UserPrompts = self:GetUserPromptStack() - local StackPointer = self:IncrementUserPromptStackPtr() - local Prompt = USERPROMPT() - UserPrompts[StackPointer] = Prompt - return Prompt -end - -function BROWSER:PopUserPrompt() - local UserPrompts = self:GetUserPromptStack() - local StackPointer = self:DecrementUserPromptStackPtr() - local Prompt = UserPrompts[StackPointer] - UserPrompts[StackPointer] = nil - return Prompt -end - --- Sets input enabled on user prompt stack and determines if user input should be enabled/disabled on the main browser --- Returns true if input is enabled -function BROWSER:ThinkAboutUserPrompts() - local UserPrompts = self:GetUserPromptStack() - local Blocking = false - - local LastBlocking = false - - for _, Prompt in ipairs(UserPrompts) do - Prompt:SetMouseInputEnabled(true) - if LastBlocking then - LastBlocking:SetMouseInputEnabled(false) - end - - Blocking = Blocking or Prompt.Blocking - - if Prompt.Blocking then - LastBlocking = Prompt - else - LastBlocking = false - end - end - - return not Blocking -end - -- This function considers the current immediate state and triggers events/sets cursor -function BROWSER:ConsiderCurrentState() +function BROWSERTREE:ConsiderCurrentState() local ImmediateState = self.ImmediateState -- Clicked for node logic, released for expander logic @@ -1147,7 +1049,7 @@ function BROWSER:ConsiderCurrentState() end -- This function paints the current immediate state to the DPanel. -function BROWSER:PaintCurrentState(PanelWidth, PanelHeight) +function BROWSERTREE:PaintCurrentState(PanelWidth, PanelHeight) local Skin = self:GetSkin() local SkinTex = Skin.tex @@ -1201,15 +1103,17 @@ function BROWSER:PaintCurrentState(PanelWidth, PanelHeight) draw.SimpleText(Node.Text or "", NodeFont, TextX, TextY, Skin.colTextEntryText or color_black, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) end - ImmediateState.BlockingAlpha = math.Clamp((ImmediateState.BlockingAlpha or 0) + (ImmediateState.DeltaTime * 2 * (ImmediateState.CanInput and -1 or 1)), 0, 1) + ImmediateState.BlockingAlpha = math.Clamp((ImmediateState.BlockingAlpha or 0) + (ImmediateState.DeltaTime * 4 * (ImmediateState.CanInput and -1 or 1)), 0, 1) if ImmediateState.BlockingAlpha > 0 then - local Alpha = math.ease.InOutQuad(ImmediateState.BlockingAlpha) * 255 + local Alpha = math.ease.InOutQuad(ImmediateState.BlockingAlpha) * 125 + local OldClipping = DisableClipping(true) surface.SetDrawColor(0, 0, 0, Alpha) - surface.DrawRect(0, 0, PanelWidth, PanelHeight) + surface.DrawRect(0, 0, self.Browser:GetSize()) + DisableClipping(OldClipping) end end -function BROWSER:Think() +function BROWSERTREE:Think() FlushConvars() -- Perform a sort recheck ... self:SortRecheck() @@ -1222,31 +1126,13 @@ function BROWSER:Think() -- VGUI panel data (cursor for example). end -function BROWSER:Paint(w, h) +function BROWSERTREE:Paint(w, h) DPanel.Paint(self, w, h) -- Renders the immediate state to the screen self:PaintCurrentState(w, h) end -function BROWSER:AddRootFolder(RootFolderType) - RootFolderType = IRootFolder(RootFolderType or error("RootFolderType must contain a IRootFolder implementation")) -- This checks if the type implemented the interface - local RealNode = self:AddFolder(RootFolderType:GetFolderName()) - - RealNode.Root = RealNode - RealNode.RootImpl = RootFolderType - - RootFolderType:Init(self, RealNode) - - return RealNode -end - -derma.DefineControl("advdupe2_browser_tree", "AD2 File Browser", BROWSER, "Panel") - - - - - - +derma.DefineControl(LowercaseFileBrowserPrefix .. "_browser_tree", FileBrowserPrefix .. " File Browser", BROWSERTREE, "Panel") @@ -1277,262 +1163,147 @@ derma.DefineControl("advdupe2_browser_tree", "AD2 File Browser", BROWSER, "Panel -local FOLDER = {} - -AccessorFunc(FOLDER, "m_bBackground", "PaintBackground", FORCE_BOOL) -AccessorFunc(FOLDER, "m_bgColor", "BackgroundColor") +local BROWSER = {} +AccessorFunc(BROWSER, "m_bBackground", "PaintBackground", FORCE_BOOL) +AccessorFunc(BROWSER, "m_bgColor", "BackgroundColor") +Derma_Hook(BROWSER, "Paint", "Paint", "Panel") +Derma_Hook(BROWSER, "PerformLayout", "Layout", "Panel") -Derma_Hook(FOLDER, "Paint", "Paint", "Panel") +local setbrowserpnlsize +local function SetBrowserPnlSize(self, x, y) + setbrowserpnlsize(self, x, y) + self.TreeView:SetWide(x) + self.TreeView.VBar:SetUp(y, self.TreeView:GetTall()) +end -function FOLDER:Init() - self:SetMouseInputEnabled(true) +function BROWSER:Init() + setbrowserpnlsize = self.SetSize + self.SetSize = SetBrowserPnlSize + self.TreeView = vgui.Create(LowercaseFileBrowserPrefix .. "_browser_tree", self) - self:SetTall(20) self:SetPaintBackground(true) self:SetPaintBackgroundEnabled(false) self:SetPaintBorderEnabled(false) - self:SetBackgroundColor(Color(0, 0, 0, 0)) - - self.Icon = vgui.Create("DImage", self) - self.Icon:SetImage("icon16/folder.png") - - self.Icon:SizeToContents() - - self.Label = vgui.Create("DLabel", self) - self.Label:SetDark(true) + self:SetBackgroundColor(self:GetSkin().text_bright) +end - self.m_bExpanded = false - self.Nodes = 0 - self.ChildrenExpanded = {} +-- Public facing API +function BROWSER:AddRootFolder(RootFolderType) + RootFolderType = IRootFolder(RootFolderType or error("RootFolderType must contain a IRootFolder implementation")) -- This checks if the type implemented the interface + local RealNode = self.TreeView:AddFolder(RootFolderType:GetFolderName()) - self:Dock(TOP) + RealNode.Root = RealNode + RealNode.RootImpl = RootFolderType - self.ChildList = vgui.Create("Panel", self:GetParent()) - self.ChildList:Dock(TOP) - self.ChildList:SetTall(0) -end + RootFolderType:Init(self, RealNode) -local function ExpandNode(self) - self:GetParent():SetExpanded() + return RealNode end -function FOLDER:AddFolder(text) - if (self.Nodes == 0) then - self.Expander = vgui.Create("DExpandButton", self) - self.Expander.DoClick = ExpandNode - self.Expander:SetPos(self.Offset, 2) - end - - local node = vgui.Create("advdupe2_browser_folder", self.ChildList) - node.Control = self.Control - - node.Offset = self.Offset + 20 +function BROWSER:StartSave(Node) + if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end - node.Icon:SetPos(18 + node.Offset, 1) - node.Label:SetPos(44 + node.Offset, 0) - node.Label:SetText(text) - node.Label:SizeToContents() - node.Label:SetDark(true) - node.ParentNode = self - node.IsFolder = true - node.Folders = {} - node.Files = {} + local Prompt = self:PushUserPrompt() + Prompt:SetBlocking(true) + Prompt:SetDock(BOTTOM) +end - self.Nodes = self.Nodes + 1 - self.Folders[#self.Folders + 1] = node +function BROWSER:GetUserPromptStack() + local UserPrompts = self.UserPrompts - if (self.m_bExpanded) then - self.Control:Extend(self) + if not UserPrompts then + UserPrompts = {} + self.UserPrompts = UserPrompts end - return node + return UserPrompts end -function FOLDER:Clear() - for _, node in ipairs(self.Folders) do - node:Remove() end - for _, node in ipairs(self.Files) do - node:Remove() end - self.Nodes = 0 -end +-- Returns the index you should use for the stack. +function BROWSER:IncrementUserPromptStackPtr() + local StackPtr = self.UserPromptStackPtr + if not StackPtr then StackPtr = 0 self.UserPromptStackPtr = StackPtr end -function FOLDER:AddFile(text) - if (self.Nodes == 0) then - self.Expander = vgui.Create("DExpandButton", self) - self.Expander.DoClick = ExpandNode - self.Expander:SetPos(self.Offset, 2) - end + StackPtr = StackPtr + 1 + self.UserPromptStackPtr = StackPtr - local node = vgui.Create("advdupe2_browser_file", self.ChildList) - node.Control = self.Control - node.Offset = self.Offset + 20 - node.Icon:SetPos(18 + node.Offset, 1) - node.Label:SetPos(44 + node.Offset, 0) - node.Label:SetText(text) - node.Label:SizeToContents() - node.Label:SetDark(true) - node.ParentNode = self + return StackPtr +end - self.Nodes = self.Nodes + 1 - table.insert(self.Files, node) +-- Returns the index to remove from the stack. +function BROWSER:DecrementUserPromptStackPtr() + local StackPtr = self.UserPromptStackPtr + if not StackPtr then StackPtr = 0 self.UserPromptStackPtr = StackPtr end - if (self.m_bExpanded) then - self.Control:Extend(self) - end + StackPtr = StackPtr - 1 + if StackPtr < 0 then ErrorNoHaltWithStack("AdvDupe2: User prompt stack underflow???") StackPtr = 0 end + self.UserPromptStackPtr = StackPtr - return node + return StackPtr + 1 -- +1 because we want to remove what was previously at that stack pointer end - -function FOLDER:LoadDataFolder(folderPath) - self:Clear() - self.LoadingPath = folderPath - self.LoadingFiles, self.LoadingDirectories = file.Find(folderPath .. "*", "DATA", "nameasc") - if self.LoadingFiles == nil then self.LoadingFiles = {} end - if self.LoadingDirectories == nil then self.LoadingDirectories = {} end - self.FileI, self.DirI = 1, 1 - self.LoadingFirst = true +function BROWSER:GetUserPromptStackLength() + return self.UserPromptStackPtr or 0 end -function FOLDER:Think() - if self.LoadingPath then - local path, files, dirs, fileI, dirI = self.LoadingPath, self.LoadingFiles, self.LoadingDirectories, self.FileI, self.DirI - if dirI > #dirs then - if fileI > #files then - self.LoadingPath = nil - return - else - local fileName = files[fileI] - local fileNode = self:AddFile(string.StripExtension(fileName)) - fileI = fileI + 1 - end - else - local dirName = dirs[dirI] - local dirNode = self:AddFolder(dirName) - dirNode:LoadDataFolder(path .. dirName .. "/") - dirI = dirI + 1 - end - - self.FileI = fileI - self.DirI = dirI - - if self.LoadingFirst then - if self.LoadingPath == "advdupe2/" then self:SetExpanded(true) end - self.LoadingFirst = false - end - end +function BROWSER:PushUserPrompt() + local UserPrompts = self:GetUserPromptStack() + local StackPointer = self:IncrementUserPromptStackPtr() + local Prompt = USERPROMPT(self) + UserPrompts[StackPointer] = Prompt + return Prompt end - -function FOLDER:SetExpanded(bool) - if (not self.Expander) then return end - if (bool == nil) then - self.m_bExpanded = not self.m_bExpanded - else - self.m_bExpanded = bool - end - self.Expander:SetExpanded(self.m_bExpanded) - if (self.m_bExpanded) then - self.Control:Expand(self) - else - self.Control:Collapse(self) - end +function BROWSER:PopUserPrompt() + local UserPrompts = self:GetUserPromptStack() + local StackPointer = self:DecrementUserPromptStackPtr() + local Prompt = UserPrompts[StackPointer] + UserPrompts[StackPointer] = nil + return Prompt end -function FOLDER:SetSelected(bool) - if (bool) then - self:SetBackgroundColor(self:GetSkin().bg_color_bright) - else - self:SetBackgroundColor(Color(0, 0, 0, 0)) - end +function BROWSER:PopUserPromptByValue(UserPrompt) + table.RemoveByValue(self:GetUserPromptStack(), UserPrompt) end -derma.DefineControl("advdupe2_browser_folder", "AD2 Browser Folder node", {}, "Panel") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -local FILE = {} - -AccessorFunc(FILE, "m_bBackground", "PaintBackground", FORCE_BOOL) -AccessorFunc(FILE, "m_bgColor", "BackgroundColor") -Derma_Hook(FILE, "Paint", "Paint", "Panel") - -function FILE:Init() - self:SetMouseInputEnabled(true) +function BROWSER:ClearAllUserPrompts() + table.Empty(self:GetUserPromptStack()) + self.UserPromptStackPtr = 0 +end +-- Sets input enabled on user prompt stack and determines if user input should be enabled/disabled on the main browser +-- Returns true if input is enabled - self:SetTall(20) - self:SetPaintBackground(true) - self:SetPaintBackgroundEnabled(false) - self:SetPaintBorderEnabled(false) - self:SetBackgroundColor(Color(0, 0, 0, 0)) +function BROWSER:ThinkAboutUserPrompts() + local UserPrompts = self:GetUserPromptStack() + local Blocking = false - self.Icon = vgui.Create("DImage", self) - self.Icon:SetImage("icon16/page.png") + local LastBlockingPanel = false - self.Icon:SizeToContents() + for K, Prompt in ipairs(UserPrompts) do + local Panel = Prompt:GetPanel() + Panel:SetMouseInputEnabled(true) - self.Label = vgui.Create("DLabel", self) - self.Label:SetDark(true) + if LastBlockingPanel then + LastBlockingPanel:SetMouseInputEnabled(false) + end - self:Dock(TOP) -end + Blocking = Blocking or Prompt.Blocking -function FILE:SetSelected(bool) - if (bool) then - self:SetBackgroundColor(self:GetSkin().bg_color_bright) - else - self:SetBackgroundColor(Color(0, 0, 0, 0)) + if Prompt.Blocking then + LastBlockingPanel = Panel + else + LastBlockingPanel = false + end end -end -function FILE:OnMousePressed(code) - if (code == 107) then - self.Control:DoNodeLeftClick(self) - elseif (code == 108) then - self.Control:DoNodeRightClick(self) - end + return not Blocking end -derma.DefineControl("advdupe2_browser_file", "AD2 Browser File node", FILE, "Panel") - - - - - +function BROWSER:Think() + self.TreeView:SetMouseInputEnabled(self:ThinkAboutUserPrompts()) +end +derma.DefineControl(FileBrowserPrefix .. "_browser_panel", "AD2 File Browser", BROWSER, "Panel") @@ -1616,21 +1387,14 @@ local function PanelSetSize(self, x, y) end -local function UpdateClientFiles() - local pnlCanvas = AdvDupe2.FileBrowser.Browser.pnlCanvas - - for i = 1, 2 do - if (pnlCanvas.Folders[1]) then - pnlCanvas:RemoveNode(pnlCanvas.Folders[1]) - end - end - - pnlCanvas.Expanded = true +local function UpdateClientFiles(Browser) + Browser.TreeView:Clear() + Browser.TreeView:Expand() - pnlCanvas:AddRootFolder(AdvDupe1Folder) - pnlCanvas:AddRootFolder(AdvDupe2Folder) + Browser:AddRootFolder(AdvDupe1Folder) + Browser:AddRootFolder(AdvDupe2Folder) - hook.Run("AdvDupe2_PostMenuFolders", pnlCanvas) + hook.Run(FileBrowserPrefix .. "_PostMenuFolders", Browser) end function PANEL:Init() @@ -1646,15 +1410,15 @@ function PANEL:Init() self:SetPaintBackgroundEnabled(false) self:SetBackgroundColor(self:GetSkin().bg_color_bright) - self.Browser = vgui.Create("advdupe2_browser_panel", self) - UpdateClientFiles() - self.Refresh = vgui.Create("DImageButton", self) + self.Browser = self:Add(LowercaseFileBrowserPrefix .. "_browser_panel") + UpdateClientFiles(self.Browser) + self.Refresh = self:Add "DImageButton" self.Refresh:SetMaterial("icon16/arrow_refresh.png") self.Refresh:SizeToContents() self.Refresh:SetTooltip("Refresh Files") - self.Refresh.DoClick = function(button) UpdateClientFiles() end + self.Refresh.DoClick = function(button) UpdateClientFiles(self.Browser) end - self.Help = vgui.Create("DImageButton", self) + self.Help = self:Add "DImageButton" self.Help:SetMaterial("icon16/help.png") self.Help:SizeToContents() self.Help:SetTooltip("Help Section") @@ -1673,7 +1437,7 @@ function PANEL:Init() Menu:Open() end - self.Submit = vgui.Create("DImageButton", self) + self.Submit = self:Add "DImageButton" self.Submit:SetMaterial("icon16/page_save.png") self.Submit:SizeToContents() self.Submit:SetTooltip("Confirm Action") @@ -1682,7 +1446,7 @@ function PANEL:Init() AdvDupe2.FileBrowser:Slide(false) end - self.Cancel = vgui.Create("DImageButton", self) + self.Cancel = self:Add "DImageButton" self.Cancel:SetMaterial("icon16/cross.png") self.Cancel:SizeToContents() self.Cancel:SetTooltip("Cancel Action") @@ -1691,7 +1455,7 @@ function PANEL:Init() AdvDupe2.FileBrowser:Slide(false) end - self.FileName = vgui.Create("DTextEntry", self) + self.FileName = self:Add "DTextEntry" self.FileName:SetAllowNonAsciiCharacters(true) self.FileName:SetText("File_Name...") self.FileName.Last = 0 @@ -1816,7 +1580,7 @@ function PANEL:Init() end end - self.Desc = vgui.Create("DTextEntry", self) + self.Desc = self:Add "DTextEntry" self.Desc.OnEnter = self.Submit.DoClick self.Desc:SetText("Description...") self.Desc.OnMousePressed = function() @@ -1826,7 +1590,7 @@ function PANEL:Init() end end - self.Info = vgui.Create("DLabel", self) + self.Info = self:Add "DLabel" self.Info:SetVisible(false) end @@ -1869,24 +1633,4 @@ function PANEL:GetNodePath(node) return GetNodePath(node) end -if (game.SinglePlayer()) then - net.Receive("AdvDupe2_AddFile", function() - local asvNode = AdvDupe2.FileBrowser.AutoSaveNode - local actNode = AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode - if (net.ReadBool()) then - if (IsValid(asvNode)) then - local name = net.ReadString() - for iD = 1, #asvNode.Files do - if (name == asvNode.Files[i]) then return end - end - asvNode:AddFile(name) - asvNode.Control:Sort(asvNode) - end - else - actNode:AddFile(net.ReadString()) - actNode.Control:Sort(actNode) - end - end) -end - -vgui.Register("advdupe2_browser", PANEL, "Panel") +vgui.Register(LowercaseFileBrowserPrefix .. "_browser", PANEL, "Panel") \ No newline at end of file diff --git a/lua/weapons/gmod_tool/stools/advdupe2.lua b/lua/weapons/gmod_tool/stools/advdupe2.lua index b5c3388f..34f2e326 100644 --- a/lua/weapons/gmod_tool/stools/advdupe2.lua +++ b/lua/weapons/gmod_tool/stools/advdupe2.lua @@ -1074,7 +1074,7 @@ if(CLIENT) then refresh.DoClick = function() CPanel:Clear() BuildCPanel(CPanel) end CPanel:AddItem(refresh) - local FileBrowser = vgui.Create("advdupe2_browser") + local FileBrowser = vgui.Create("advdupe2_browser", CPanel) CPanel:AddItem(FileBrowser) FileBrowser:SetSize(CPanel:GetWide(), 405) AdvDupe2.FileBrowser = FileBrowser From a9c91c20183af1694cad464af91cc045941367c2 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 18:15:34 -0700 Subject: [PATCH 15/31] Flesh out user prompts --- lua/advdupe2/file_browser.lua | 186 ++++++++++++++++++++++++++++--- lua/autorun/advdupe2_sh_init.lua | 5 +- 2 files changed, 173 insertions(+), 18 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 99f42754..6ea867b6 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -12,8 +12,9 @@ local ADVDUPE2_AREA_ADVDUPE2 = AdvDupe2.AREA_ADVDUPE2 local ADVDUPE2_AREA_PUBLIC = AdvDupe2.AREA_PUBLIC local ADVDUPE2_AREA_ADVDUPE1 = AdvDupe2.AREA_ADVDUPE1 -local NODETYPE_FOLDER = AdvDupe2.NODETYPE_FOLDER -local NODETYPE_FILE = AdvDupe2.NODETYPE_FILE +-- These are internal really, so i don't think these need to be exposed? +local NODETYPE_FOLDER = 1 +local NODETYPE_FILE = 2 -- This lets us rip this stuff out if we need to. local FileBrowserPrefix = "AdvDupe2" @@ -303,7 +304,7 @@ do function NODE:MarkSortDirty() self.SortDirty = true - self.Browser.SortDirty = true + self.Browser:MarkSortDirty() end function NODE:Clear() @@ -595,11 +596,18 @@ do self.Browser = Browser self.Blocking = false + self.StartOpen = UserInterfaceTimeFunc() + self.WillOpenAt = self.StartOpen + 0.2 + self.Panel = Browser:Add("DPanel") end function USERPROMPT:GetPanel() return self.Panel end + function USERPROMPT:Add(Type) + return self.Panel:Add(Type) + end + function USERPROMPT_MT:__call(Browser) if not IsValid(Browser) then return error ("Cannot create a headless node (we need a browser)") end @@ -619,20 +627,72 @@ do end function USERPROMPT:SetDock(Dock) - self.Panel:Dock(Dock) - self.Panel:SetSize(self.Browser:GetTall() / 6) + self.Dock = Dock end - function USERPROMPT:ThinkAnimations() + local function Normalize(X, Min, Max) + return math.Clamp((X - Min) / (Max - Min), 0, 1) + end + + function USERPROMPT:GetAnimationRatio(Now) + local Closing = self.Closing + + local OpenRatio = Normalize(Now, self.StartOpen, self.WillOpenAt) + local CloseRatio = Closing and (1 - Normalize(Now, self.StartClose, self.WillCloseAt)) or 1 + local OpenX, OpenY, OpenA = math.ease.OutQuad(OpenRatio), math.ease.OutBack(OpenRatio), math.ease.OutQuad(OpenRatio) + local CloseX, CloseY, CloseA = math.ease.OutQuad(CloseRatio), math.ease.OutBack(CloseRatio), math.ease.InQuart(CloseRatio) + + -- We return opening animation * closing animation to get smooth effects + -- This works because CloseRatio will be a downward value while OpenRatio will be an upward value + return OpenX * CloseX, OpenY * CloseY, OpenA * CloseA + end + + local DockPadding = 4 + + function USERPROMPT:DoThink() + local Now = UserInterfaceTimeFunc() + local Closing = self.Closing + + if Closing and Now >= self.WillCloseAt then + return false + end + + local RatioX, RatioY, RatioA = self:GetAnimationRatio(Now) + local Browser = self.Browser + local ParentWidth, ParentHeight = Browser:GetWide(), Browser:GetTall() + + local PosX, PosY + local SizeW, SizeH + local SizeC + + local ContentWide, ContentTall = self.Panel:ChildrenSize() + + local Dock = self.Dock + if not Dock then error("No dock??") end + + if Dock == BOTTOM then + SizeC = ContentTall + SizeW = (RatioX * ParentWidth) - (DockPadding * 2) + SizeH = SizeC * RatioY + PosX = ((ParentWidth / 2) - (SizeW / 2)) + PosY = ParentHeight - SizeH - DockPadding + end + + self.Panel:SetPos(PosX, PosY) + self.Panel:SetSize(SizeW, SizeH) + self.Panel:SetAlpha(RatioA * 255) + + return true end -- Call this to close and pop later. + -- This also forces Blocking back to false for main UI panel. function USERPROMPT:Close() - self.Blocking = false - timer.Simple(0.5, function() - self.Browser:PopUserPromptByValue(self) - end) + self.Blocking = false + self.Closing = true + self.StartClose = UserInterfaceTimeFunc() + self.WillCloseAt = self.StartClose + 0.3 end end @@ -1105,7 +1165,7 @@ function BROWSERTREE:PaintCurrentState(PanelWidth, PanelHeight) ImmediateState.BlockingAlpha = math.Clamp((ImmediateState.BlockingAlpha or 0) + (ImmediateState.DeltaTime * 4 * (ImmediateState.CanInput and -1 or 1)), 0, 1) if ImmediateState.BlockingAlpha > 0 then - local Alpha = math.ease.InOutQuad(ImmediateState.BlockingAlpha) * 125 + local Alpha = math.ease.InOutQuad(ImmediateState.BlockingAlpha) * 66 local OldClipping = DisableClipping(true) surface.SetDrawColor(0, 0, 0, Alpha) surface.DrawRect(0, 0, self.Browser:GetSize()) @@ -1188,6 +1248,11 @@ function BROWSER:Init() end -- Public facing API +function BROWSER:MarkSortDirty() + if not self.TreeView then return end + self.TreeView.SortDirty = true +end + function BROWSER:AddRootFolder(RootFolderType) RootFolderType = IRootFolder(RootFolderType or error("RootFolderType must contain a IRootFolder implementation")) -- This checks if the type implemented the interface local RealNode = self.TreeView:AddFolder(RootFolderType:GetFolderName()) @@ -1200,12 +1265,86 @@ function BROWSER:AddRootFolder(RootFolderType) return RealNode end +function BROWSER:GetRootImpl(Node) + return Node.Root.RootImpl or error "Cannot find IRootFolder implementation!" +end + function BROWSER:StartSave(Node) if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end local Prompt = self:PushUserPrompt() Prompt:SetBlocking(true) Prompt:SetDock(BOTTOM) + + local Name, Desc + + local function FinishSave() + local RootImpl = self:GetRootImpl(Node) + RootImpl:UserSave(self, Node, Name:GetText(), Desc:GetText()) + Prompt:Close() + end + + -- I tried SetTabPosition; it doesn't like to work, so we're doing it ourselves + + Name = Prompt:Add("DTextEntry") + Name:SetAllowNonAsciiCharacters(true) + Name:SetTabbingDisabled(false) + Name:Dock(TOP) + Name:DockMargin(4, 4, 4, 2) + Name:SetPlaceholderText("Dupe name") + + local Save = Prompt:Add("DImageButton") + Save:Dock(RIGHT) + Save:SetSize(24) + Save:SetStretchToFit(false) + Save:SetImage("icon16/disk.png") + + Desc = Prompt:Add("DTextEntry") + Desc:SetAllowNonAsciiCharacters(true) + Desc:SetTabbingDisabled(false) + Desc:Dock(FILL) + Desc:DockMargin(4, 4, 0, 4) + Desc:SelectAllOnFocus() + Desc:SetPlaceholderText("Dupe description (optional)") + + function Name:OnEnter() + self:KillFocus() + Desc:SelectAllOnFocus(true) + Desc:OnMousePressed() + Desc:RequestFocus() + end + function Name:OnKeyCode(KeyCode) + if KeyCode == KEY_TAB then + return timer.Simple(0, function() if IsValid(self) then self:OnEnter() end end) + end + + DTextEntry.OnKeyCode(self, KeyCode) + end + function Name:OnMousePressed() + self:OnGetFocus() + self:SelectAllOnFocus(true) + end + + function Desc:OnEnter() + self:KillFocus() + FinishSave() + end + function Desc:OnKeyCode(KeyCode) + if KeyCode == KEY_TAB then + return timer.Simple(0, function() if IsValid(self) then self:OnEnter() end end) + end + + DTextEntry.OnKeyCode(self, KeyCode) + end + function Desc:OnMousePressed() + self:OnGetFocus() + self:SelectAllOnFocus(true) + end + + function Save:DoClick() + FinishSave() + end + timer.Simple(0, function() if IsValid(Name) then Name:OnMousePressed() end end) end function BROWSER:GetUserPromptStack() @@ -1263,10 +1402,15 @@ function BROWSER:PopUserPrompt() end function BROWSER:PopUserPromptByValue(UserPrompt) - table.RemoveByValue(self:GetUserPromptStack(), UserPrompt) + if table.RemoveByValue(self:GetUserPromptStack(), UserPrompt) then + self:DecrementUserPromptStackPtr() + end end function BROWSER:ClearAllUserPrompts() + for _, Prompt in ipairs(self:GetUserPromptStack()) do + Prompt.Panel:Remove() + end table.Empty(self:GetUserPromptStack()) self.UserPromptStackPtr = 0 end @@ -1279,6 +1423,7 @@ function BROWSER:ThinkAboutUserPrompts() local LastBlockingPanel = false + local RemoveValues for K, Prompt in ipairs(UserPrompts) do local Panel = Prompt:GetPanel() Panel:SetMouseInputEnabled(true) @@ -1287,6 +1432,7 @@ function BROWSER:ThinkAboutUserPrompts() LastBlockingPanel:SetMouseInputEnabled(false) end + Panel:SetZPos(1000 + K) Blocking = Blocking or Prompt.Blocking if Prompt.Blocking then @@ -1294,16 +1440,28 @@ function BROWSER:ThinkAboutUserPrompts() else LastBlockingPanel = false end + + if not Prompt:DoThink() then + RemoveValues = RemoveValues or {} -- Only allocate this table if we need to remove prompts + RemoveValues[#RemoveValues + 1] = Prompt + end + end + + if RemoveValues then + for _, Prompt in ipairs(RemoveValues) do + self:PopUserPromptByValue(Prompt) + end end return not Blocking end function BROWSER:Think() - self.TreeView:SetMouseInputEnabled(self:ThinkAboutUserPrompts()) + local CanInput = self:ThinkAboutUserPrompts() + self.TreeView:SetMouseInputEnabled(CanInput) end -derma.DefineControl(FileBrowserPrefix .. "_browser_panel", "AD2 File Browser", BROWSER, "Panel") +derma.DefineControl(LowercaseFileBrowserPrefix .. "_browser_panel", "AD2 File Browser", BROWSER, "Panel") diff --git a/lua/autorun/advdupe2_sh_init.lua b/lua/autorun/advdupe2_sh_init.lua index 66677124..bc37f5df 100644 --- a/lua/autorun/advdupe2_sh_init.lua +++ b/lua/autorun/advdupe2_sh_init.lua @@ -8,7 +8,4 @@ AdvDupe2.DataFolder = "advdupe2" --name of the folder in data where dupes will b -- enums AdvDupe2.AREA_ADVDUPE2 = 0 AdvDupe2.AREA_PUBLIC = 1 -AdvDupe2.AREA_ADVDUPE1 = 2 - -AdvDupe2.NODETYPE_FOLDER = 1 -AdvDupe2.NODETYPE_FILE = 2 \ No newline at end of file +AdvDupe2.AREA_ADVDUPE1 = 2 \ No newline at end of file From e6233686d5e857ac12f04a9d1b460bdeae9a25eb Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:39:11 -0700 Subject: [PATCH 16/31] Add file saving back --- lua/advdupe2/cl_file.lua | 11 +- lua/advdupe2/file_browser.lua | 286 ++++++++++++++++++++++------------ 2 files changed, 197 insertions(+), 100 deletions(-) diff --git a/lua/advdupe2/cl_file.lua b/lua/advdupe2/cl_file.lua index 7e030e72..e31b927a 100644 --- a/lua/advdupe2/cl_file.lua +++ b/lua/advdupe2/cl_file.lua @@ -52,6 +52,9 @@ function AdvDupe2.ReceiveFile(data, autoSave) end local filename = string.StripExtension(string.GetFileFromFilename( path )) + + local Browser = AdvDupe2.FileBrowser.Browser + if autoSave then if IsValid(AdvDupe2.FileBrowser.AutoSaveNode) then local add = true @@ -62,13 +65,13 @@ function AdvDupe2.ReceiveFile(data, autoSave) end end if add then - AdvDupe2.FileBrowser.AutoSaveNode:AddFile(filename) - AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.AutoSaveNode) + error "Not implemented yet" + -- AutoSaveNode:AddFile(filename) + -- Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.AutoSaveNode) end end else - AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode:AddFile(filename) - AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode) + Browser:IncomingFile("advdupe2/" .. filename .. ".txt") end if not errored then diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 6ea867b6..cd5eb4f0 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -29,6 +29,7 @@ local MaxTimeToDoubleClick, NodeTall, NodePadding, TallOfOneNode, NodeDepthWidth local ExpanderSize, IconSize, LeftmostToExpanderPadding, ExpanderToIconPadding, IconToTextPadding local ExpanderXOffset, IconXOffset, TextXOffset +local TimeToOpenPrompts_cv, TimeToClosePrompts_cv local ICON_FOLDER_EMPTY local ICON_FOLDER_CONTAINS @@ -40,6 +41,12 @@ local UserInterfaceTimeFunc = RealTime -- Convars and flushing convars into local registers. -- FlushConvars gets called in BROWSER:Think() before anything else do + TimeToOpenPrompts_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_promptopentime", "0.2", true, false, + "The time it takes for a user-prompt to fully open, in seconds.", 0, 1000000) + TimeToClosePrompts_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_promptclose_time", "0.3", true, false, + "The time it takes for a user-prompt to fully close, in seconds.", 0, 1000000) + + local MaxTimeToDoubleClick_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_maxtimetodoubleclick", "0.25", true, false, "Max time delta between clicks to count as a double click, in seconds.", 0, 1000000) local NodeTall_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_nodetall", "24", true, false, @@ -136,72 +143,6 @@ end local count = 0 -local function AddHistory(txt) - txt = string.lower(txt) - local char1 = txt[1] - local char2 - for i = 1, #History do - char2 = History[i][1] - if (char1 == char2) then - if (History[i] == txt) then - return - end - elseif (char1 < char2) then - break - end - end - - table.insert(History, txt) - table.sort(History, function(a, b) return a < b end) -end - -local function NarrowHistory(txt, last) - txt = string.lower(txt) - local temp = {} - if (last <= #txt and last ~= 0 and #txt ~= 1) then - for i = 1, #Narrow do - if (Narrow[i][last + 1] == txt[last + 1]) then - table.insert(temp, Narrow[i]) - elseif (Narrow[i][last + 1] ~= '') then - break - end - end - else - local char1 = txt[1] - local char2 - for i = 1, #History do - char2 = History[i][1] - if (char1 == char2) then - if (#txt > 1) then - for k = 2, #txt do - if (txt[k] ~= History[i][k]) then - break - end - if (k == #txt) then - table.insert(temp, History[i]) - end - end - else - table.insert(temp, History[i]) - end - elseif (char1 < char2) then - break - end - end - end - - Narrow = temp -end - -local function tableSortNodes(tbl) - for k, v in ipairs(tbl) do tbl[k] = {string.lower(v.Label:GetText()), v} end - table.sort(tbl, function(a,b) return a[1]") + Text:Dock(FILL) + Text:SetContentAlignment(5) + Text:SetDark(true) + + local Icon = Notif:Add("DImageButton") + Icon:SetMouseInputEnabled(false) -- DImageButton provides SetStretchToFit while DImage doesn't - that's all we need here + Icon:SetSize(20, 20) + Icon:SetStretchToFit(false) + Icon:SetKeepAspect(true) + Icon:SetImage("icon16/" .. NotifIcons[Level] .. ".png") + + local OldLayout = Notif.Panel.PerformLayout + function Notif.Panel:PerformLayout(W, H) + OldLayout(self, W, H) + local CW = Text:GetContentSize() + Icon:SetPos((W / 2) - (CW / 2) - 12) + Text:SetTextInset(12, 0) + end + -- bit too hacky? + function Notif.Panel:ChildrenSize() + return 0, 24 + end + + timer.Simple(Time, function() if IsValid(Notif.Panel) then Notif:Close() end end) +end + function BROWSER:StartSave(Node) if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end local Prompt = self:PushUserPrompt() Prompt:SetBlocking(true) Prompt:SetDock(BOTTOM) + Prompt.Panel:DockPadding(4,4,4,4) local Name, Desc local function FinishSave() + -- Require filename + local FileName = Name:GetText() + if FileName == nil or FileName == "" then + self:Notify("You must specify a filename.", NOTIFY_ERROR, 2) + return + end local RootImpl = self:GetRootImpl(Node) - RootImpl:UserSave(self, Node, Name:GetText(), Desc:GetText()) + RootImpl:UserSave(self, Node, FileName, Desc:GetText()) Prompt:Close() end @@ -1290,20 +1323,32 @@ function BROWSER:StartSave(Node) Name:SetAllowNonAsciiCharacters(true) Name:SetTabbingDisabled(false) Name:Dock(TOP) - Name:DockMargin(4, 4, 4, 2) Name:SetPlaceholderText("Dupe name") - - local Save = Prompt:Add("DImageButton") + Name:SetZPos(1) + + local DescParent = Prompt:Add("Panel") + DescParent:Dock(TOP) + DescParent:DockMargin(0, 4, 0, 0) + DescParent:SetSize(20, 20) + DescParent:SetPaintBackgroundEnabled(false) + DescParent:SetZPos(10000) + + local Cancel = DescParent:Add("DImageButton") + Cancel:Dock(RIGHT) + Cancel:SetSize(20) + Cancel:SetStretchToFit(false) + Cancel:SetImage("icon16/cancel.png") + + local Save = DescParent:Add("DImageButton") Save:Dock(RIGHT) Save:SetSize(24) Save:SetStretchToFit(false) Save:SetImage("icon16/disk.png") - Desc = Prompt:Add("DTextEntry") + Desc = DescParent:Add("DTextEntry") Desc:SetAllowNonAsciiCharacters(true) Desc:SetTabbingDisabled(false) Desc:Dock(FILL) - Desc:DockMargin(4, 4, 0, 4) Desc:SelectAllOnFocus() Desc:SetPlaceholderText("Dupe description (optional)") @@ -1325,13 +1370,19 @@ function BROWSER:StartSave(Node) self:SelectAllOnFocus(true) end - function Desc:OnEnter() + function Desc:OnEnter(_, WasTab) self:KillFocus() - FinishSave() + if WasTab then -- Wrap back to Name. + Name:SelectAllOnFocus(true) + Name:OnMousePressed() + Name:RequestFocus() + else + FinishSave() + end end function Desc:OnKeyCode(KeyCode) if KeyCode == KEY_TAB then - return timer.Simple(0, function() if IsValid(self) then self:OnEnter() end end) + return timer.Simple(0, function() if IsValid(self) then self:OnEnter(nil, true) end end) end DTextEntry.OnKeyCode(self, KeyCode) @@ -1344,7 +1395,21 @@ function BROWSER:StartSave(Node) function Save:DoClick() FinishSave() end - timer.Simple(0, function() if IsValid(Name) then Name:OnMousePressed() end end) + function Cancel:DoClick() + Prompt:Close() + end + + -- This is a hack to make the name field discard the first released character for spawnmenu/contextmenu saving. + -- Since now the Name field automatically requests focus this is necessary to avoid an unnecessary character. + -- Hopefully it works (it seems to in testing) + local OnKeyCodeTyped = Name.OnKeyCodeTyped + function Name:OnKeyCodeTyped(KeyCode) + -- discard, reset back + self.OnKeyCodeTyped = OnKeyCodeTyped + end + + Name:RequestFocus() + Name:OnMousePressed() end function BROWSER:GetUserPromptStack() @@ -1414,23 +1479,27 @@ function BROWSER:ClearAllUserPrompts() table.Empty(self:GetUserPromptStack()) self.UserPromptStackPtr = 0 end + -- Sets input enabled on user prompt stack and determines if user input should be enabled/disabled on the main browser -- Returns true if input is enabled - +-- This stuff is kinda weird, but doesnt run that much and seems to be pretty OK. Real docking seems to cause +-- layout issues that I would rather not deal with, especially during animation. function BROWSER:ThinkAboutUserPrompts() - local UserPrompts = self:GetUserPromptStack() - local Blocking = false + local StackDocks = self.StackDocks or {} + StackDocks[TOP] = 0 + StackDocks[LEFT] = 0 + StackDocks[RIGHT] = 0 + StackDocks[BOTTOM] = 0 + + local UserPrompts = self:GetUserPromptStack() + local Blocking = false local LastBlockingPanel = false local RemoveValues for K, Prompt in ipairs(UserPrompts) do local Panel = Prompt:GetPanel() - Panel:SetMouseInputEnabled(true) - - if LastBlockingPanel then - LastBlockingPanel:SetMouseInputEnabled(false) - end + Panel:SetMouseInputEnabled(LastBlockingPanel and false or true) Panel:SetZPos(1000 + K) Blocking = Blocking or Prompt.Blocking @@ -1447,6 +1516,31 @@ function BROWSER:ThinkAboutUserPrompts() end end + local DockPadding = 4 + + for I = #UserPrompts, 1, -1 do + local Prompt = UserPrompts[I] + if Prompt and IsValid(Prompt.Panel) then + local Panel = Prompt.Panel + + local X, Y = Panel:GetPos() + local W, H = Panel:GetSize() + local Dock = Prompt.Dock + + if Dock ~= NODOCK and Dock ~= FILL then + if Dock == TOP or Dock == BOTTOM then + Y = Y + StackDocks[Dock] + StackDocks[Dock] = StackDocks[Dock] + H + DockPadding + else + X = X + StackDocks[Dock] + StackDocks[Dock] = StackDocks[Dock] + W + DockPadding + end + end + + Panel:SetPos(X, Y) + end + end + if RemoveValues then for _, Prompt in ipairs(RemoveValues) do self:PopUserPromptByValue(Prompt) From b68a9feb22e01e25b95124794c98891b636e2c97 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:08:07 -0700 Subject: [PATCH 17/31] WIP folder logic --- lua/advdupe2/file_browser.lua | 390 +++++++++++----------------------- 1 file changed, 127 insertions(+), 263 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index cd5eb4f0..18c15870 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -20,7 +20,6 @@ local NODETYPE_FILE = 2 local FileBrowserPrefix = "AdvDupe2" local LowercaseFileBrowserPrefix = string.lower(FileBrowserPrefix) -local History = {} local Narrow = {} -- Just in case this needs to be changed later @@ -514,6 +513,7 @@ do IRootFolder.UserRename = function(Impl, Browser, Node, RenameTo) end IRootFolder.UserMenu = function(Impl, Browser, Node, Menu) end IRootFolder.UserDelete = function(Impl, Browser, Node) end + IRootFolder.UserMakeFolder = function(Impl, Browser, Node, Foldername) end -- Ensures the implementor implemented the interface correctly -- if they didn't throw non-halting errors since it might be an optional method @@ -733,6 +733,10 @@ do end + function AdvDupe1Folder:UserMakeFolder(Browser, Node, Foldername) + + end + IRootFolder(AdvDupe1Folder) -- validation end @@ -791,7 +795,11 @@ do end function AdvDupe2Folder:UserDelete(Browser, Node) - + + end + + function AdvDupe2Folder:UserMakeFolder(Browser, Node, Foldername) + Browser:Notify("Not implemented.", NOTIFY_ERROR, 4) end IRootFolder(AdvDupe2Folder) -- validation @@ -842,10 +850,6 @@ end function BROWSERTREE:DoNodeRightClick(Node) self:SetSelected(Node) - local BrowserPanel = self:GetParent():GetParent() - BrowserPanel.FileName:KillFocus() - BrowserPanel.Desc:KillFocus() - local Menu = DermaMenu() local RootImpl = Node.Root.RootImpl @@ -1295,7 +1299,7 @@ function BROWSER:Notify(Message, Level, Time) timer.Simple(Time, function() if IsValid(Notif.Panel) then Notif:Close() end end) end -function BROWSER:StartSave(Node) +local function SharedFileFolderLogic(self, Node, DoDesc, TypeName, Icon, Completed) if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end local Prompt = self:PushUserPrompt() @@ -1303,93 +1307,132 @@ function BROWSER:StartSave(Node) Prompt:SetDock(BOTTOM) Prompt.Panel:DockPadding(4,4,4,4) - local Name, Desc + local Name, Desc, Cancel, Save local function FinishSave() -- Require filename local FileName = Name:GetText() if FileName == nil or FileName == "" then - self:Notify("You must specify a filename.", NOTIFY_ERROR, 2) + self:Notify("You must specify a file/folder path.", NOTIFY_ERROR, 2) return end + local RootImpl = self:GetRootImpl(Node) - RootImpl:UserSave(self, Node, FileName, Desc:GetText()) + Completed(self, RootImpl, Node, FileName, Desc and Desc:GetText() or "") Prompt:Close() end -- I tried SetTabPosition; it doesn't like to work, so we're doing it ourselves - Name = Prompt:Add("DTextEntry") - Name:SetAllowNonAsciiCharacters(true) - Name:SetTabbingDisabled(false) - Name:Dock(TOP) - Name:SetPlaceholderText("Dupe name") - Name:SetZPos(1) - - local DescParent = Prompt:Add("Panel") - DescParent:Dock(TOP) - DescParent:DockMargin(0, 4, 0, 0) - DescParent:SetSize(20, 20) - DescParent:SetPaintBackgroundEnabled(false) - DescParent:SetZPos(10000) - - local Cancel = DescParent:Add("DImageButton") - Cancel:Dock(RIGHT) - Cancel:SetSize(20) - Cancel:SetStretchToFit(false) - Cancel:SetImage("icon16/cancel.png") - - local Save = DescParent:Add("DImageButton") - Save:Dock(RIGHT) - Save:SetSize(24) - Save:SetStretchToFit(false) - Save:SetImage("icon16/disk.png") - - Desc = DescParent:Add("DTextEntry") - Desc:SetAllowNonAsciiCharacters(true) - Desc:SetTabbingDisabled(false) - Desc:Dock(FILL) - Desc:SelectAllOnFocus() - Desc:SetPlaceholderText("Dupe description (optional)") + if DoDesc then + Name = Prompt:Add("DTextEntry") + Name:SetAllowNonAsciiCharacters(true) + Name:SetTabbingDisabled(false) + Name:Dock(TOP) + Name:SetPlaceholderText(TypeName .. " name") + Name:SetZPos(1) + + local DescParent = Prompt:Add("Panel") + DescParent:Dock(TOP) + DescParent:DockMargin(0, 4, 0, 0) + DescParent:SetSize(20, 20) + DescParent:SetPaintBackgroundEnabled(false) + DescParent:SetZPos(10000) + + Cancel = DescParent:Add("DImageButton") + Cancel:Dock(RIGHT) + Cancel:SetSize(20) + Cancel:SetStretchToFit(false) + Cancel:SetImage("icon16/cancel.png") + + Save = DescParent:Add("DImageButton") + Save:Dock(RIGHT) + Save:SetSize(24) + Save:SetStretchToFit(false) + Save:SetImage("icon16/" .. Icon .. ".png") + + Desc = DescParent:Add("DTextEntry") + Desc:SetAllowNonAsciiCharacters(true) + Desc:SetTabbingDisabled(false) + Desc:Dock(FILL) + Desc:SelectAllOnFocus() + Desc:SetPlaceholderText(TypeName .. " description (optional)") + else + local DescParent = Prompt:Add("Panel") + DescParent:Dock(TOP) + DescParent:DockMargin(0, 4, 0, 0) + DescParent:SetSize(20, 20) + DescParent:SetPaintBackgroundEnabled(false) + DescParent:SetZPos(10000) + + Cancel = DescParent:Add("DImageButton") + Cancel:Dock(RIGHT) + Cancel:SetSize(20) + Cancel:SetStretchToFit(false) + Cancel:SetImage("icon16/cancel.png") + + Save = DescParent:Add("DImageButton") + Save:Dock(RIGHT) + Save:SetSize(24) + Save:SetStretchToFit(false) + Save:SetImage("icon16/" .. Icon .. ".png") + + Name = DescParent:Add("DTextEntry") + Name:SetAllowNonAsciiCharacters(true) + Name:SetTabbingDisabled(false) + Name:Dock(FILL) + Name:SetPlaceholderText(TypeName .. " name") + Name:SetZPos(1) + end function Name:OnEnter() self:KillFocus() - Desc:SelectAllOnFocus(true) - Desc:OnMousePressed() - Desc:RequestFocus() - end - function Name:OnKeyCode(KeyCode) - if KeyCode == KEY_TAB then - return timer.Simple(0, function() if IsValid(self) then self:OnEnter() end end) + if Desc then + Desc:SelectAllOnFocus(true) + Desc:OnMousePressed() + Desc:RequestFocus() + else + FinishSave() end + end - DTextEntry.OnKeyCode(self, KeyCode) + if Desc then + function Name:OnKeyCode(KeyCode) + if KeyCode == KEY_TAB then + return timer.Simple(0, function() if IsValid(self) then self:OnEnter() end end) + end + + DTextEntry.OnKeyCode(self, KeyCode) + end end + function Name:OnMousePressed() self:OnGetFocus() self:SelectAllOnFocus(true) end - function Desc:OnEnter(_, WasTab) - self:KillFocus() - if WasTab then -- Wrap back to Name. - Name:SelectAllOnFocus(true) - Name:OnMousePressed() - Name:RequestFocus() - else - FinishSave() - end - end - function Desc:OnKeyCode(KeyCode) - if KeyCode == KEY_TAB then - return timer.Simple(0, function() if IsValid(self) then self:OnEnter(nil, true) end end) + if Desc then + function Desc:OnEnter(_, WasTab) + self:KillFocus() + if WasTab then -- Wrap back to Name. + Name:SelectAllOnFocus(true) + Name:OnMousePressed() + Name:RequestFocus() + else + FinishSave() + end end + function Desc:OnKeyCode(KeyCode) + if KeyCode == KEY_TAB then + return timer.Simple(0, function() if IsValid(self) then self:OnEnter(nil, true) end end) + end - DTextEntry.OnKeyCode(self, KeyCode) - end - function Desc:OnMousePressed() - self:OnGetFocus() - self:SelectAllOnFocus(true) + DTextEntry.OnKeyCode(self, KeyCode) + end + function Desc:OnMousePressed() + self:OnGetFocus() + self:SelectAllOnFocus(true) + end end function Save:DoClick() @@ -1412,6 +1455,20 @@ function BROWSER:StartSave(Node) Name:OnMousePressed() end +function BROWSER:StartSave(Node) + SharedFileFolderLogic(self, Node, true, "Dupe", "disk", function(_, RootImpl, _, FileName, Desc) + RootImpl:UserSave(self, Node, FileName, Desc) + end) +end + +function BROWSER:StartFolder(Node) + SharedFileFolderLogic(self, Node, false, "Folder", "folder_add", function(_, RootImpl, _, FileName, Desc) + RootImpl:UserMakeFolder(self, Node, FileName, Desc) + end) +end + + + function BROWSER:GetUserPromptStack() local UserPrompts = self.UserPrompts @@ -1605,17 +1662,6 @@ function PANEL:PerformLayout() BtnX = BtnX - self.Refresh:GetWide() - 5 self.Refresh:SetPos(BtnX, 3) - BtnX = x - self.Submit:GetWide() - 15 - self.Cancel:SetPos(BtnX, self.Browser:GetTall() + 20) - BtnX = BtnX - self.Submit:GetWide() - 5 - self.Submit:SetPos(BtnX, self.Browser:GetTall() + 20) - - self.FileName:SetWide(BtnX - 10) - self.FileName:SetPos(5, self.Browser:GetTall() + 20) - self.Desc:SetWide(x - 10) - self.Desc:SetPos(5, self.Browser:GetTall() + 39) - self.Info:SetPos(5, self.Browser:GetTall() + 20) - self.LastX = x end @@ -1688,193 +1734,11 @@ function PANEL:Init() end) Menu:Open() end - - self.Submit = self:Add "DImageButton" - self.Submit:SetMaterial("icon16/page_save.png") - self.Submit:SizeToContents() - self.Submit:SetTooltip("Confirm Action") - self.Submit.DoClick = function() - self.Expanding = true - AdvDupe2.FileBrowser:Slide(false) - end - - self.Cancel = self:Add "DImageButton" - self.Cancel:SetMaterial("icon16/cross.png") - self.Cancel:SizeToContents() - self.Cancel:SetTooltip("Cancel Action") - self.Cancel.DoClick = function() - self.Expanding = true - AdvDupe2.FileBrowser:Slide(false) - end - - self.FileName = self:Add "DTextEntry" - self.FileName:SetAllowNonAsciiCharacters(true) - self.FileName:SetText("File_Name...") - self.FileName.Last = 0 - - self.FileName.OnEnter = function() - self.FileName:KillFocus() - self.Desc:SelectAllOnFocus(true) - self.Desc.OnMousePressed() - self.Desc:RequestFocus() - end - self.FileName.OnMousePressed = function() - self.FileName:OnGetFocus() - if (self.FileName:GetValue() == "File_Name..." or - self.FileName:GetValue() == "Folder_Name...") then - self.FileName:SelectAllOnFocus(true) - end - end - self.FileName:SetUpdateOnType(true) - self.FileName.OnTextChanged = function() - - if (self.FileName.FirstChar) then - if (string.lower(self.FileName:GetValue()[1] or "") == string.lower(input.LookupBinding("menu") or "q")) then - self.FileName:SetText(self.FileName.PrevText) - self.FileName:SelectAll() - self.FileName.FirstChar = false - else - self.FileName.FirstChar = false - end - end - - local new, changed = self.FileName:GetValue():gsub("[^%w_ ]", "") - if changed > 0 then - self.FileName:SetText(new) - self.FileName:SetCaretPos(#new) - end - if (#self.FileName:GetValue() > 0) then - NarrowHistory(self.FileName:GetValue(), self.FileName.Last) - local options = {} - if (#Narrow > 4) then - for i = 1, 4 do table.insert(options, Narrow[i]) end - else - options = Narrow - end - if (#options ~= 0 and #self.FileName:GetValue() ~= 0) then - self.FileName.HistoryPos = 0 - self.FileName:OpenAutoComplete(options) - self.FileName.Menu.Attempts = 1 - if (#Narrow > 4) then - self.FileName.Menu:AddOption("...", function() end) - end - elseif (IsValid(self.FileName.Menu)) then - self.FileName.Menu:Remove() - end - end - self.FileName.Last = #self.FileName:GetValue() - end - self.FileName.OnKeyCodeTyped = function(txtbox, code) - txtbox:OnKeyCode(code) - - if (code == KEY_ENTER and not txtbox:IsMultiline() and txtbox:GetEnterAllowed()) then - if (txtbox.HistoryPos == 5 and txtbox.Menu:ChildCount() == 5) then - if ((txtbox.Menu.Attempts + 1) * 4 < #Narrow) then - for i = 1, 4 do - txtbox.Menu:GetChild(i):SetText(Narrow[i + txtbox.Menu.Attempts * 4]) - end - else - txtbox.Menu:GetChild(5):Remove() - for i = 4, (txtbox.Menu.Attempts * 4 - #Narrow) * -1 + 1, -1 do - txtbox.Menu:GetChild(i):Remove() - end - - for i = 1, #Narrow - txtbox.Menu.Attempts * 4 do - txtbox.Menu:GetChild(i):SetText(Narrow[i + txtbox.Menu.Attempts * 4]) - end - end - txtbox.Menu:ClearHighlights() - txtbox.Menu:HighlightItem(txtbox.Menu:GetChild(1)) - txtbox.HistoryPos = 1 - txtbox.Menu.Attempts = txtbox.Menu.Attempts + 1 - return true - end - - if (IsValid(txtbox.Menu)) then - txtbox.Menu:Remove() - end - txtbox:FocusNext() - txtbox:OnEnter() - txtbox.HistoryPos = 0 - end - - if (txtbox.m_bHistory or IsValid(txtbox.Menu)) then - if (code == KEY_UP) then - txtbox.HistoryPos = txtbox.HistoryPos - 1; - if (txtbox.HistoryPos ~= -1 or txtbox.Menu:ChildCount() ~= 5) then - txtbox:UpdateFromHistory() - else - txtbox.Menu:ClearHighlights() - txtbox.Menu:HighlightItem(txtbox.Menu:GetChild(5)) - txtbox.HistoryPos = 5 - end - end - if (code == KEY_DOWN or code == KEY_TAB) then - txtbox.HistoryPos = txtbox.HistoryPos + 1; - if (txtbox.HistoryPos ~= 5 or txtbox.Menu:ChildCount() ~= 5) then - txtbox:UpdateFromHistory() - else - txtbox.Menu:ClearHighlights() - txtbox.Menu:HighlightItem(txtbox.Menu:GetChild(5)) - end - end - - end - end - self.FileName.OnValueChange = function() - if (self.FileName:GetValue() ~= "File_Name..." and - self.FileName:GetValue() ~= "Folder_Name...") then - local new, changed = self.FileName:GetValue():gsub("[^%w_ ]", "") - if changed > 0 then - self.FileName:SetText(new) - self.FileName:SetCaretPos(#new) - end - end - end - - self.Desc = self:Add "DTextEntry" - self.Desc.OnEnter = self.Submit.DoClick - self.Desc:SetText("Description...") - self.Desc.OnMousePressed = function() - self.Desc:OnGetFocus() - if (self.Desc:GetValue() == "Description...") then - self.Desc:SelectAllOnFocus(true) - end - end - - self.Info = self:Add "DLabel" - self.Info:SetVisible(false) - end function PANEL:Slide(expand) - if (expand) then - if (self.Expanded) then - self:SetTall(self:GetTall() - 40) - self.Expanded = false - else - self:SetTall(self:GetTall() + 5) - end - else - if (not self.Expanded) then - self:SetTall(self:GetTall() + 40) - self.Expanded = true - else - self:SetTall(self:GetTall() - 5) - end - end - count = count + 1 - if (count < 9) then - timer.Simple(0.01, function() self:Slide(expand) end) - else - if (expand) then - self.Expanded = true - else - self.Expanded = false - end - self.Expanding = false - count = 0 - end + -- Stub. Need to entirely remove this. + ErrorNoHalt("AdvDupe2:Slide is no longer implemented") end function PANEL:GetFullPath(node) From dbdd284f87756fc01feab8352efc851ae8a2b281 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:55:52 -0700 Subject: [PATCH 18/31] Fix a couple bugs, implement makefolder --- lua/advdupe2/cl_file.lua | 2 +- lua/advdupe2/file_browser.lua | 31 +++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lua/advdupe2/cl_file.lua b/lua/advdupe2/cl_file.lua index e31b927a..def88226 100644 --- a/lua/advdupe2/cl_file.lua +++ b/lua/advdupe2/cl_file.lua @@ -71,7 +71,7 @@ function AdvDupe2.ReceiveFile(data, autoSave) end end else - Browser:IncomingFile("advdupe2/" .. filename .. ".txt") + Browser:IncomingFile(path) end if not errored then diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 18c15870..39e4b1d8 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -20,8 +20,6 @@ local NODETYPE_FILE = 2 local FileBrowserPrefix = "AdvDupe2" local LowercaseFileBrowserPrefix = string.lower(FileBrowserPrefix) -local Narrow = {} - -- Just in case this needs to be changed later local MaxTimeToDoubleClick, NodeTall, NodePadding, TallOfOneNode, NodeDepthWidth, NodeFont @@ -763,7 +761,7 @@ do Browser:AwaitingFile(DataPath .. ".txt", function() Node:Expand() local File = Filename .. ".txt" - local NewNode = Node:AddFile() + local NewNode = Node:AddFile(File) SetupDataFile(NewNode, DataPath, File) end) @@ -799,7 +797,15 @@ do end function AdvDupe2Folder:UserMakeFolder(Browser, Node, Foldername) - Browser:Notify("Not implemented.", NOTIFY_ERROR, 4) + local DataPath = (Node.Path or "advdupe2") .. "/" .. Foldername + + file.CreateDir(DataPath) + + local NewNode = Node:AddFolder(Foldername) + SetupDataSubfolder(NewNode, DataPath, Foldername) + Node:Expand() + NewNode:Expand() + Browser:ScrollTo(NewNode) end IRootFolder(AdvDupe2Folder) -- validation @@ -1607,6 +1613,23 @@ function BROWSER:ThinkAboutUserPrompts() return not Blocking end +function BROWSER:ScrollTo(Node) + self.TreeView:SortRecheck() + local Index = -1 + + for K, ENode in ipairs(self.TreeView.ExpandedNodeArray) do + if ENode == Node then + Index = K + break + end + end + + if Index == -1 then return end + + local ScrollPos = math.Max(0, (Index * TallOfOneNode) - (self:GetTall() / 2)) + self.TreeView.VBar:SetScroll(ScrollPos) +end + function BROWSER:Think() local CanInput = self:ThinkAboutUserPrompts() self.TreeView:SetMouseInputEnabled(CanInput) From 2975225677b0eb9abc798d26a89aabcf12fedb6f Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:56:51 -0700 Subject: [PATCH 19/31] Simplify more of what's left of V1 --- lua/advdupe2/file_browser.lua | 76 +++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 39e4b1d8..404902ab 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -1670,7 +1670,7 @@ AccessorFunc(PANEL, "m_bgColor", "BackgroundColor") Derma_Hook(PANEL, "Paint", "Paint", "Panel") Derma_Hook(PANEL, "PerformLayout", "Layout", "Panel") -function PANEL:PerformLayout() +function PANEL:PerformLayout(w, h) if (self:GetWide() == self.LastX) then return end local x = self:GetWide() @@ -1679,11 +1679,24 @@ function PANEL:PerformLayout() end self.Browser:SetWide(x) - local x2, y2 = self.Browser:GetPos() - local BtnX = x - self.Help:GetWide() - 5 - self.Help:SetPos(BtnX, 3) - BtnX = BtnX - self.Refresh:GetWide() - 5 - self.Refresh:SetPos(BtnX, 3) + + local BtnX + local BtnPad = 6 + if self.LeftsideButtons then + BtnX = BtnPad + for _, Button in ipairs(self.LeftsideButtons) do + Button:SetPos(BtnX, 4) + BtnX = BtnX + Button:GetWide() + BtnPad + end + end + + if self.RightsideButtons then + BtnX = w + for _, Button in ipairs(self.RightsideButtons) do + BtnX = BtnX - Button:GetWide() - BtnPad + Button:SetPos(BtnX, 4) + end + end self.LastX = x end @@ -1693,12 +1706,12 @@ local function PanelSetSize(self, x, y) if (not self.LaidOut) then pnlorigsetsize(self, x, y) - self.Browser:SetSize(x, y - 20) - self.Browser:SetPos(0, 20) + self.Browser:SetSize(x, y - 24) + self.Browser:SetPos(0, 24) if (self.Search) then - self.Search:SetSize(x, y - 20) - self.Search:SetPos(0, 20) + self.Search:SetSize(x, y - 24) + self.Search:SetPos(0, 24) end self.LaidOut = true @@ -1718,6 +1731,32 @@ local function UpdateClientFiles(Browser) hook.Run(FileBrowserPrefix .. "_PostMenuFolders", Browser) end +function PANEL:AddLeftsideButton(Icon, Tooltip, Action) + self.LeftsideButtons = self.LeftsideButtons or {} + + local Button = self:Add "DImageButton" + Button:SetMaterial("icon16/" .. Icon .. ".png") + Button:SizeToContents() + Button:SetTooltip(Tooltip) + + self.LeftsideButtons[#self.LeftsideButtons + 1] = Button + + return Button +end + +function PANEL:AddRightsideButton(Icon, Tooltip, Action) + self.RightsideButtons = self.RightsideButtons or {} + + local Button = self:Add "DImageButton" + Button:SetMaterial("icon16/" .. Icon .. ".png") + Button:SizeToContents() + Button:SetTooltip(Tooltip) + + self.RightsideButtons[#self.RightsideButtons + 1] = Button + + return Button +end + function PANEL:Init() AdvDupe2.FileBrowser = self self.Expanded = false @@ -1733,17 +1772,9 @@ function PANEL:Init() self.Browser = self:Add(LowercaseFileBrowserPrefix .. "_browser_panel") UpdateClientFiles(self.Browser) - self.Refresh = self:Add "DImageButton" - self.Refresh:SetMaterial("icon16/arrow_refresh.png") - self.Refresh:SizeToContents() - self.Refresh:SetTooltip("Refresh Files") - self.Refresh.DoClick = function(button) UpdateClientFiles(self.Browser) end - - self.Help = self:Add "DImageButton" - self.Help:SetMaterial("icon16/help.png") - self.Help:SizeToContents() - self.Help:SetTooltip("Help Section") - self.Help.DoClick = function(btn) + + self.Refresh = self:AddRightsideButton("arrow_refresh", "Refresh Files", function(button) UpdateClientFiles(self.Browser) end) + self.Help = self:AddRightsideButton("help", "Help Section", function(btn) local Menu = DermaMenu() Menu:AddOption("Bug Reporting", function() gui.OpenURL("https://github.com/wiremod/advdupe2/issues") @@ -1756,7 +1787,8 @@ function PANEL:Init() "https://github.com/wiremod/advdupe2/wiki/Server-settings") end) Menu:Open() - end + end) + self.Settings = self:AddRightsideButton("cog", "Settings", function() self:OpenSettings() end) end function PANEL:Slide(expand) From fd3e35e39b3ae48e9eb9cfb7f930eb98a285ce47 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sun, 29 Jun 2025 00:01:04 -0700 Subject: [PATCH 20/31] Settings menu and further UI work --- lua/advdupe2/file_browser.lua | 197 ++++++++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 6 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 404902ab..4285c79c 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -15,6 +15,10 @@ local ADVDUPE2_AREA_ADVDUPE1 = AdvDupe2.AREA_ADVDUPE1 -- These are internal really, so i don't think these need to be exposed? local NODETYPE_FOLDER = 1 local NODETYPE_FILE = 2 +-- I may implement more of these later, just defining them now +local VIEWTYPE_TREE = 0 +local VIEWTYPE_LIST = 1 +local VIEWTYPE_TILES = 2 -- This lets us rip this stuff out if we need to. local FileBrowserPrefix = "AdvDupe2" @@ -40,7 +44,7 @@ local UserInterfaceTimeFunc = RealTime do TimeToOpenPrompts_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_promptopentime", "0.2", true, false, "The time it takes for a user-prompt to fully open, in seconds.", 0, 1000000) - TimeToClosePrompts_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_promptclose_time", "0.3", true, false, + TimeToClosePrompts_cv = CreateClientConVar(LowercaseFileBrowserPrefix .. "_menu_promptclosetime", "0.3", true, false, "The time it takes for a user-prompt to fully close, in seconds.", 0, 1000000) @@ -89,9 +93,16 @@ do CreateNodeTextRepresentation("Distance, in pixels, between the node icon and the node text.", 12), 0, 1000000) - ICON_FOLDER_EMPTY = Material(NodeIconFolderEmpty_cv:GetString(), "smooth") - ICON_FOLDER_CONTAINS = Material(NodeIconFolderContains_cv:GetString(), "smooth") - ICON_FILE = Material(NodeIconFile_cv:GetString(), "smooth") + local IconFolderEmpty, IconFolderContains, IconFile + + local function UpdateMaterial(Mat, Old, CV) + local New = CV:GetString() + if Old ~= New then + Mat = Material(New, "smooth") + end + + return Mat + end function FlushConvars() MaxTimeToDoubleClick = MaxTimeToDoubleClick_cv:GetFloat() @@ -110,6 +121,10 @@ do ExpanderXOffset = LeftmostToExpanderPadding IconXOffset = ExpanderXOffset + ExpanderSize + ExpanderToIconPadding TextXOffset = IconXOffset + IconSize + IconToTextPadding + + ICON_FOLDER_EMPTY = UpdateMaterial(ICON_FOLDER_EMPTY, IconFolderEmpty, NodeIconFolderEmpty_cv) + ICON_FOLDER_CONTAINS = UpdateMaterial(ICON_FOLDER_CONTAINS, IconFolderContains, NodeIconFolderContains_cv) + ICON_FILE = UpdateMaterial(ICON_FILE, IconFile, NodeIconFile_cv) end end @@ -539,7 +554,14 @@ do self.StartOpen = UserInterfaceTimeFunc() self.WillOpenAt = self.StartOpen + TimeToOpenPrompts_cv:GetFloat() - self.Panel = Browser:Add("DPanel") + self.Panel = Browser:Add("DScrollPanel") + local PanelColor = Color(255, 255, 255, 213) + self.Panel.Paint = function(self, w, h) + local Skin = self:GetSkin() + local SkinTex = Skin.tex + + SkinTex.Panels.Normal(0, 0, w, h, PanelColor) + end end function USERPROMPT:GetPanel() return self.Panel end @@ -607,7 +629,7 @@ do local SizeC local ContentWide, ContentTall = self.Panel:ChildrenSize() - + ContentTall = math.Min(ContentTall, self.Browser:GetTall() - 32) local Dock = self.Dock if not Dock then error("No dock??") end @@ -1220,9 +1242,17 @@ function BROWSER:Init() self:SetPaintBackgroundEnabled(false) self:SetPaintBorderEnabled(false) self:SetBackgroundColor(self:GetSkin().text_bright) + + self:SetViewType(VIEWTYPE_TREE) end -- Public facing API + +function BROWSER:SetViewType(ViewType) + self.TreeView.ViewType = ViewType or error("No viewtype provided") + self:MarkSortDirty() +end + function BROWSER:MarkSortDirty() if not self.TreeView then return end self.TreeView.SortDirty = true @@ -1731,6 +1761,26 @@ local function UpdateClientFiles(Browser) hook.Run(FileBrowserPrefix .. "_PostMenuFolders", Browser) end +function PANEL:AddLeftsideDivider() + self.LeftsideButtons = self.LeftsideButtons or {} + + local Panel = self:Add "DPanel" + Panel:SetSize(2, 16) + Panel.Paint = function(_, w, h) surface.SetDrawColor(0, 0, 0, 50) surface.DrawRect(0, 0, w, h) end + + self.LeftsideButtons[#self.LeftsideButtons + 1] = Panel +end + +function PANEL:AddRightsideDivider() + self.RightsideButtons = self.RightsideButtons or {} + + local Panel = self:Add "DPanel" + Panel:SetSize(2, 16) + Panel.Paint = function(_, w, h) surface.SetDrawColor(0, 0, 0, 50) surface.DrawRect(0, 0, w, h) end + + self.RightsideButtons[#self.RightsideButtons + 1] = Panel +end + function PANEL:AddLeftsideButton(Icon, Tooltip, Action) self.LeftsideButtons = self.LeftsideButtons or {} @@ -1738,6 +1788,7 @@ function PANEL:AddLeftsideButton(Icon, Tooltip, Action) Button:SetMaterial("icon16/" .. Icon .. ".png") Button:SizeToContents() Button:SetTooltip(Tooltip) + Button.DoClick = Action self.LeftsideButtons[#self.LeftsideButtons + 1] = Button @@ -1751,12 +1802,16 @@ function PANEL:AddRightsideButton(Icon, Tooltip, Action) Button:SetMaterial("icon16/" .. Icon .. ".png") Button:SizeToContents() Button:SetTooltip(Tooltip) + Button.DoClick = Action self.RightsideButtons[#self.RightsideButtons + 1] = Button return Button end +local VIEWTYPETREE_SELECTED = color_white +local VIEWTYPETREE_UNSELECTED = Color(143, 143, 143, 143) + function PANEL:Init() AdvDupe2.FileBrowser = self self.Expanded = false @@ -1773,6 +1828,16 @@ function PANEL:Init() self.Browser = self:Add(LowercaseFileBrowserPrefix .. "_browser_panel") UpdateClientFiles(self.Browser) + self.SearchAll = self:AddLeftsideButton("folder_magnify", "Search all root folders", function() self.Browser:Notify("Not yet implemented!", NOTIFY_ERROR, 3) end) + self:AddLeftsideDivider() + self.SwitchToTree = self:AddLeftsideButton("application_view_detail", "Tree view", function() self.Browser:Notify("Not yet implemented!", NOTIFY_ERROR, 3) end) + self.SwitchToList = self:AddLeftsideButton("application_view_list", "List view", function() self.Browser:Notify("Not yet implemented!", NOTIFY_ERROR, 3) end) + self.SwitchToTiles = self:AddLeftsideButton("application_view_tile", "Tile view", function() self.Browser:Notify("Not yet implemented!", NOTIFY_ERROR, 3) end) + + self.SwitchToTree.Think = function(b) b.m_Image:SetImageColor(self.Browser.TreeView.ViewType == VIEWTYPE_TREE and VIEWTYPETREE_SELECTED or VIEWTYPETREE_UNSELECTED) end + self.SwitchToList.Think = function(b) b.m_Image:SetImageColor(self.Browser.TreeView.ViewType == VIEWTYPE_LIST and VIEWTYPETREE_SELECTED or VIEWTYPETREE_UNSELECTED) end + self.SwitchToTiles.Think = function(b) b.m_Image:SetImageColor(self.Browser.TreeView.ViewType == VIEWTYPE_TILES and VIEWTYPETREE_SELECTED or VIEWTYPETREE_UNSELECTED) end + self.Refresh = self:AddRightsideButton("arrow_refresh", "Refresh Files", function(button) UpdateClientFiles(self.Browser) end) self.Help = self:AddRightsideButton("help", "Help Section", function(btn) local Menu = DermaMenu() @@ -1788,9 +1853,129 @@ function PANEL:Init() end) Menu:Open() end) + self:AddRightsideDivider() self.Settings = self:AddRightsideButton("cog", "Settings", function() self:OpenSettings() end) end +function PANEL:OpenSettings() + if self.SettingsPanel then + self.SettingsPanel:Close() + self.SettingsPanel = nil + self.Settings:SetImage("icon16/cog.png") + return + end + + self.Settings:SetImage("icon16/cog_delete.png") + + local Panel = self.Browser:PushUserPrompt() + self.SettingsPanel = Panel + + Panel:SetBlocking(true) + Panel:SetDock(BOTTOM) + + local function CreateDivider() + local Div = Panel:Add("DPanel") + Div:Dock(TOP) + Div:DockMargin(16, 4, 16, 4) + Div:SetSize(0, 2) + Div.Paint = function(_, w, h) surface.SetDrawColor(0, 0, 0, 90) surface.DrawRect(0, 0, w, h) end + end + local function CreateConvarSlider(ConVar, CName, Min, Max) + local CV = GetConVar(ConVar) + local Name = Panel:Add("DLabel") + Name:Dock(TOP) + Name:SetText(CName or CV:GetName()) + Name:SetDark(true) + Name:SetTextInset(8, 0) + Name:SetTooltip(CV:GetHelpText()) + local Slider = Panel:Add("DNumSlider") + Slider:SetDark(true) + Slider:Dock(TOP) + Slider:SetSize(0, 14) + Slider:SetConVar(ConVar) + Slider:SetMinMax(Min or CV:GetMin(), Max or CV:GetMax()) + Slider.Scratch.PaintScratchWindow = function(s) + if not s:GetActive() then return end + if s:GetZoom() == 0 then s:SetZoom(s:IdealZoom()) end + + local w, h = 400, 200 + local x, y = s:LocalToScreen(0, h + 24) + + x = x + s:GetWide() * 0.5 - w * 0.5 + y = y - 8 - h + + if x + w + 32 > ScrW() then x = ScrW() - w - 32 end + if y + h + 32 > ScrH() then y = ScrH() - h - 32 end + if x < 32 then x = 32 end + if y < 32 then y = 32 end + + if render then render.SetScissorRect(x, y, x + w, y + h, true) end + s:DrawScreen(x, y, w, h) + if render then render.SetScissorRect(x, y, w, h, false) end + end + end + local function CreateConvarEntry(ConVar, CName) + local CV = GetConVar(ConVar) + local Name = Panel:Add("DLabel") + Name:Dock(TOP) + Name:SetText(CName or CV:GetName()) + Name:SetDark(true) + Name:SetTextInset(8, 0) + Name:SetTooltip(CV:GetHelpText()) + local Entry = Panel:Add("DTextEntry") + Entry:Dock(TOP) + Entry:SetSize(0, 20) + Entry:DockMargin(8, 0, 8, 0) + Entry:SetConVar(ConVar) + return Entry + end + local function CreateConvarIconEntry(ConVar, CName) + local Entry = CreateConvarEntry(ConVar, CName) + local CV = GetConVar(ConVar) + + local IconSelector = Entry:Add("DImageButton") + IconSelector:SetSize(16, 16) + IconSelector:DockMargin(2, 2, 2, 2) + IconSelector:Dock(RIGHT) + IconSelector:SetImage(CV:GetString()) + + IconSelector.DoClick = function() + local Frame = vgui.Create("DFrame") + Frame:MakePopup() + Frame:SetSize(480, 360) + Frame:Center() + + local Icons = Frame:Add("DIconBrowser") + Icons:Dock(FILL) + Icons:SelectIcon(CV:GetString()) + Icons.OnChange = function() + CV:SetString(Icons:GetSelectedIcon()) + IconSelector:SetImage(Icons:GetSelectedIcon()) + end + end + end + + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_promptopentime", "Prompt Open Animation Time (seconds)", 0, 2) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_promptclosetime", "Prompt Close Animation Time (seconds)", 0, 2) + CreateDivider() + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_maxtimetodoubleclick", "Max Deltatime for Double Clicks (seconds)", 0, 2) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodetall", "Node Height (pixels)", 0, 256) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodepadding", "Node Height Padding (pixels)", 0, 256) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodedepthwidth", "Node Depth Width (pixels)", 0, 256) + CreateDivider() + CreateConvarEntry(LowercaseFileBrowserPrefix .. "_menu_nodefont", "Node Font") + CreateDivider() + CreateConvarIconEntry(LowercaseFileBrowserPrefix .. "_menu_nodeicon_folderempty", "Node Empty Folder Icon") + CreateConvarIconEntry(LowercaseFileBrowserPrefix .. "_menu_nodeicon_folder", "Node Folder Icon") + CreateConvarIconEntry(LowercaseFileBrowserPrefix .. "_menu_nodeicon_file", "Node File Icon") + CreateDivider() + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodeexpander_size", "Node Expander Size (pixels)", 0, 256) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodeicon_size", "Node Icon Size (pixels)", 0, 256) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodepadding_toexpander", "Left -> Expander Padding (pixels)", 0, 256) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodepadding_expandertoicon", "Expander -> Icon Padding (pixels)", 0, 256) + CreateConvarSlider(LowercaseFileBrowserPrefix .. "_menu_nodepadding_icontotext", "Icon -> Text Padding (pixels)", 0, 256) +end + function PANEL:Slide(expand) -- Stub. Need to entirely remove this. ErrorNoHalt("AdvDupe2:Slide is no longer implemented") From 40e277346d2809d79789219d0e0b588f6aa38e69 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:20:03 -0700 Subject: [PATCH 21/31] Add some IRootFolder methods --- lua/advdupe2/file_browser.lua | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 4285c79c..40740689 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -527,6 +527,8 @@ do IRootFolder.UserMenu = function(Impl, Browser, Node, Menu) end IRootFolder.UserDelete = function(Impl, Browser, Node) end IRootFolder.UserMakeFolder = function(Impl, Browser, Node, Foldername) end + IRootFolder.UserGetModTime = function(Impl, Browser, Node) return 0 end + IRootFolder.UserGetSize = function(Impl, Browser, Node) return 0 end -- Ensures the implementor implemented the interface correctly -- if they didn't throw non-halting errors since it might be an optional method @@ -757,11 +759,25 @@ do end + function AdvDupe1Folder:UserGetModTime(Browser, Node) + return 0 + end + + function AdvDupe1Folder:UserGetSize(Browser, Node) + return 0 + end + IRootFolder(AdvDupe1Folder) -- validation end do - AdvDupe2Folder = {} + AdvDupe2Folder = { + -- Key-weak LUT's for size/modtimes. + -- They're key-weak so if a node gets deleted it isn't hung up by GC thinking + -- we care about the reference still + SizeCache = setmetatable({}, {__mode = 'k'}), + TimeCache = setmetatable({}, {__mode = 'k'}) + } function AdvDupe2Folder:GetFolderName() return "Advanced Duplicator 2" end function AdvDupe2Folder:Init(Browser, Node) Node:LoadDataFolder("advdupe2/") @@ -830,6 +846,26 @@ do Browser:ScrollTo(NewNode) end + function AdvDupe2Folder:UserGetModTime(Browser, Node) + if not Node.Path then return end + + local Time = self.TimeCache[Node] + if Time then return Time end + + Time = file.Time(Node.Path, "DATA") + self.TimeCache[Node] = Time + end + + function AdvDupe2Folder:UserGetSize(Browser, Node) + if Node:IsFolder() then return -1 end + + local Size = self.SizeCache[Node] + if Size then return Size end + + Size = file.Size(Node.Path, "DATA") + self.SizeCache[Node] = Size + end + IRootFolder(AdvDupe2Folder) -- validation end From 55cc4969574b39bdce57eb63ee9212387f4c4893 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:24:16 -0700 Subject: [PATCH 22/31] Make my linter happier with me --- lua/advdupe2/file_browser.lua | 323 ++++++++++++---------------------- 1 file changed, 113 insertions(+), 210 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 40740689..f8366778 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -153,54 +153,123 @@ local function GetTextPosition(X, Y, W, H, Depth) return (Depth * NodeDepthWidth) + X + TextXOffset, Y + (H / 2) end -local count = 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +-- Defines GetNumericalFilename +-- May need optimization and refactoring later - especially for non-ASCII strings... +-- This handles things very similarly to how Windows does in terms of sorting, but also adds sorting by month +-- May also be a good idea in the future to add a setting for the above functionality. +local GetNumericalFilename +do + local isDigit = { + ['0'] = 0, + ['1'] = 1, + ['2'] = 2, + ['3'] = 3, + ['4'] = 4, + ['5'] = 5, + ['6'] = 6, + ['7'] = 7, + ['8'] = 8, + ['9'] = 9 + } + -- faster than string.byte calls + local char2byte = {} + for i = 1, 255 do char2byte[string.char(i)] = string.byte(string.lower(string.char(i))) end + char2byte['_'] = 2000 + + local buildMonth = {} + for k, v in ipairs{"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"} do + local tbl = buildMonth + for i = 1, #v do + local c = v[i] + if i == #v then + tbl[c] = k + else + if not tbl[c] then + tbl[c] = {} + end + tbl = tbl[c] + end + end + end + local numericalStore = {} + function GetNumericalFilename(name) + if numericalStore[name] then return numericalStore[name] end + + local ret = {} + local digit = nil + local monthTester = buildMonth + local monthStoreJustInCase = {} + + local function testMonth(i, c) + monthTester = monthTester[c] + monthStoreJustInCase[#monthStoreJustInCase + 1] = char2byte[c] + if type(monthTester) == "number" then + local nextC = name[i + 1] + local nextIfine = nextC == ' ' or nextC == '_' or nextC == '-' + if i == #name or nextIfine then + ret[#ret + 1] = monthTester + monthStoreJustInCase = {} + monthTester = buildMonth + if nextIfine then + i = i + 1 + end + else + for i2 = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i2] + end + monthStoreJustInCase = {} + monthTester = buildMonth + end + end + end + local function finalTest(i, c) + if monthTester ~= buildMonth then + for i2 = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i2] + end + monthStoreJustInCase = {} + monthTester = buildMonth + end + ret[#ret + 1] = char2byte[c] + end + for i = 1, #name do + local c = name[i] + local cIsDigit = isDigit[c] + if cIsDigit then + if digit == nil then + digit = 0 + end + digit = (digit * 10) + cIsDigit + else + if monthTester[c] then + testMonth(i, c) + elseif digit ~= nil then + ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) + digit = nil + else + finalTest(i, c) + end + end + end + if digit ~= nil then + ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) + end + if monthTester ~= buildMonth then + for i = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i] + end + end + numericalStore[name] = ret -- store so this doesnt have to be calculated multiple times for no reason + return ret + end +end local SetupDataFile, SetupDataSubfolder local NODE_MT = {} @@ -337,116 +406,6 @@ do LoadDataFolderInternal(self, Path) end - -- Defines GetNumericalFilename - -- May need optimization and refactoring later - especially for non-ASCII strings... - -- This handles things very similarly to how Windows does in terms of sorting, but also adds sorting by month - -- May also be a good idea in the future to add a setting for the above functionality. - local GetNumericalFilename - do - local isDigit = { - ['0'] = 0, - ['1'] = 1, - ['2'] = 2, - ['3'] = 3, - ['4'] = 4, - ['5'] = 5, - ['6'] = 6, - ['7'] = 7, - ['8'] = 8, - ['9'] = 9 - } - - -- faster than string.byte calls - local char2byte = {} - for i = 1, 255 do char2byte[string.char(i)] = string.byte(string.lower(string.char(i))) end - char2byte['_'] = 2000 - - local buildMonth = {} - for k, v in ipairs{"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"} do - local tbl = buildMonth - for i = 1, #v do - local c = v[i] - if i == #v then - tbl[c] = k - else - if not tbl[c] then - tbl[c] = {} - end - - tbl = tbl[c] - end - end - end - - local numericalStore = {} - function GetNumericalFilename(name) - if numericalStore[name] then return numericalStore[name] end - - local ret = {} - local digit = nil - local monthTester = buildMonth - local monthStoreJustInCase = {} - - for i = 1, #name do - local c = name[i] - local cIsDigit = isDigit[c] - if cIsDigit then - if digit == nil then - digit = 0 - end - digit = (digit * 10) + cIsDigit - else - if monthTester[c] then - monthTester = monthTester[c] - monthStoreJustInCase[#monthStoreJustInCase + 1] = char2byte[c] - if type(monthTester) == "number" then - local nextC = name[i + 1] - local nextIfine = nextC == ' ' or nextC == '_' or nextC == '-' - if i == #name or nextIfine then - ret[#ret + 1] = monthTester - monthStoreJustInCase = {} - monthTester = buildMonth - if nextIfine then - i = i + 1 - end - else - for i = 1, #monthStoreJustInCase do - ret[#ret + 1] = monthStoreJustInCase[i] - end - monthStoreJustInCase = {} - monthTester = buildMonth - end - end - elseif digit ~= nil then - ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) - digit = nil - else - if monthTester ~= buildMonth then - for i = 1, #monthStoreJustInCase do - ret[#ret + 1] = monthStoreJustInCase[i] - end - monthStoreJustInCase = {} - monthTester = buildMonth - end - ret[#ret + 1] = char2byte[c] - end - end - end - - if digit ~= nil then - ret[#ret + 1] = digit - (#ret == 0 and 100000000 or 0) - end - if monthTester ~= buildMonth then - for i = 1, #monthStoreJustInCase do - ret[#ret + 1] = monthStoreJustInCase[i] - end - end - - numericalStore[name] = ret -- store so this doesnt have to be calculated multiple times for no reason - return ret - end - end - function NODE.SortFunction(A, B) local IsFileA, IsFileB = A:IsFile(), B:IsFile() @@ -558,8 +517,8 @@ do self.Panel = Browser:Add("DScrollPanel") local PanelColor = Color(255, 255, 255, 213) - self.Panel.Paint = function(self, w, h) - local Skin = self:GetSkin() + self.Panel.Paint = function(panel, w, h) + local Skin = panel:GetSkin() local SkinTex = Skin.tex SkinTex.Panels.Normal(0, 0, w, h, PanelColor) @@ -630,7 +589,7 @@ do local SizeW, SizeH local SizeC - local ContentWide, ContentTall = self.Panel:ChildrenSize() + local _, ContentTall = self.Panel:ChildrenSize() ContentTall = math.Min(ContentTall, self.Browser:GetTall() - 32) local Dock = self.Dock if not Dock then error("No dock??") end @@ -1227,35 +1186,6 @@ end derma.DefineControl(LowercaseFileBrowserPrefix .. "_browser_tree", FileBrowserPrefix .. " File Browser", BROWSERTREE, "Panel") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - local BROWSER = {} AccessorFunc(BROWSER, "m_bBackground", "PaintBackground", FORCE_BOOL) AccessorFunc(BROWSER, "m_bgColor", "BackgroundColor") @@ -1703,33 +1633,6 @@ end derma.DefineControl(LowercaseFileBrowserPrefix .. "_browser_panel", "AD2 File Browser", BROWSER, "Panel") - - - - - - - - - - - - - - - - - - - - - - - - - - - local PANEL = {} AccessorFunc(PANEL, "m_bBackground", "PaintBackground", FORCE_BOOL) AccessorFunc(PANEL, "m_bgColor", "BackgroundColor") From ad6050bdf78139715da216fbe2e018546cb98166 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:12:49 -0700 Subject: [PATCH 23/31] Add renaming, fix duplicate files --- lua/advdupe2/cl_file.lua | 4 +-- lua/advdupe2/file_browser.lua | 60 ++++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/lua/advdupe2/cl_file.lua b/lua/advdupe2/cl_file.lua index def88226..7c245b9a 100644 --- a/lua/advdupe2/cl_file.lua +++ b/lua/advdupe2/cl_file.lua @@ -24,7 +24,7 @@ function AdvDupe2.ReceiveFile(data, autoSave) else path = AdvDupe2.GetFilename(AdvDupe2.SavePath) end - + local OriginalPath = AdvDupe2.SavePath .. ".txt" path = AdvDupe2.SanitizeFilename(path) local dupefile = file.Open(path, "wb", "DATA") if not dupefile then @@ -71,7 +71,7 @@ function AdvDupe2.ReceiveFile(data, autoSave) end end else - Browser:IncomingFile(path) + Browser:IncomingFile(OriginalPath, path) end if not errored then diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index f8366778..59a088bc 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -755,11 +755,11 @@ do AdvDupe2.SavePath = DataPath -- Enqueue handler - Browser:AwaitingFile(DataPath .. ".txt", function() + Browser:AwaitingFile(DataPath .. ".txt", function(NewFilepath) Node:Expand() - local File = Filename .. ".txt" - local NewNode = Node:AddFile(File) - SetupDataFile(NewNode, DataPath, File) + local NewFilename = string.GetFileFromFilename(NewFilepath) + local NewNode = Node:AddFile(NewFilename) + SetupDataFile(NewNode, File, NewFilename) end) if game.SinglePlayer() then @@ -770,7 +770,16 @@ do end function AdvDupe2Folder:UserRename(Browser, Node, RenameTo) + local NodePath = "advdupe2/" .. GetNodeDataPath(Node) .. ".txt" + local NewNodePath = string.GetPathFromFilename(NodePath) .. RenameTo .. ".txt" + if file.Rename(NodePath, NewNodePath) then + SetupDataFile(Node, NewNodePath, RenameTo .. ".txt") + Node.ParentNode:MarkSortDirty() + Browser:ScrollTo(Node) + return true + end + return false end function AdvDupe2Folder:UserMenu(Browser, Node, Menu) @@ -1301,9 +1310,7 @@ function BROWSER:Notify(Message, Level, Time) timer.Simple(Time, function() if IsValid(Notif.Panel) then Notif:Close() end end) end -local function SharedFileFolderLogic(self, Node, DoDesc, TypeName, Icon, Completed) - if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end - +local function SharedFileFolderLogic(self, Node, DoDesc, NameTextPlaceholder, DescTextPlaceholder, Icon, Completed, LastName, LastDesc, SkipToDesc, BlockName) local Prompt = self:PushUserPrompt() Prompt:SetBlocking(true) Prompt:SetDock(BOTTOM) @@ -1331,8 +1338,10 @@ local function SharedFileFolderLogic(self, Node, DoDesc, TypeName, Icon, Complet Name:SetAllowNonAsciiCharacters(true) Name:SetTabbingDisabled(false) Name:Dock(TOP) - Name:SetPlaceholderText(TypeName .. " name") + Name:SetPlaceholderText(NameTextPlaceholder) Name:SetZPos(1) + if LastName then Name:SetText(LastName) end + if BlockName then Name:SetEnabled(false) end local DescParent = Prompt:Add("Panel") DescParent:Dock(TOP) @@ -1358,7 +1367,8 @@ local function SharedFileFolderLogic(self, Node, DoDesc, TypeName, Icon, Complet Desc:SetTabbingDisabled(false) Desc:Dock(FILL) Desc:SelectAllOnFocus() - Desc:SetPlaceholderText(TypeName .. " description (optional)") + Desc:SetPlaceholderText(DescTextPlaceholder) + if LastDesc then Desc:SetText(LastDesc) end else local DescParent = Prompt:Add("Panel") DescParent:Dock(TOP) @@ -1383,8 +1393,9 @@ local function SharedFileFolderLogic(self, Node, DoDesc, TypeName, Icon, Complet Name:SetAllowNonAsciiCharacters(true) Name:SetTabbingDisabled(false) Name:Dock(FILL) - Name:SetPlaceholderText(TypeName .. " name") + Name:SetPlaceholderText(NameTextPlaceholder) Name:SetZPos(1) + if LastName then Name:SetText(LastName) end end function Name:OnEnter() @@ -1453,22 +1464,41 @@ local function SharedFileFolderLogic(self, Node, DoDesc, TypeName, Icon, Complet self.OnKeyCodeTyped = OnKeyCodeTyped end - Name:RequestFocus() - Name:OnMousePressed() + if SkipToDesc then + Desc:RequestFocus() + Desc:OnMousePressed() + else + Name:RequestFocus() + Name:OnMousePressed() + end end function BROWSER:StartSave(Node) - SharedFileFolderLogic(self, Node, true, "Dupe", "disk", function(_, RootImpl, _, FileName, Desc) + if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end + + SharedFileFolderLogic(self, Node, true, "Dupe name", "Dupe description", "disk", function(_, RootImpl, _, FileName, Desc) RootImpl:UserSave(self, Node, FileName, Desc) - end) + self.LastFileName = FileName + self.LastFileDesc = Desc + end, self.LastFileName, self.LastFileDesc) end function BROWSER:StartFolder(Node) - SharedFileFolderLogic(self, Node, false, "Folder", "folder_add", function(_, RootImpl, _, FileName, Desc) + if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end + + SharedFileFolderLogic(self, Node, false, "Dupe name", "Dupe description", "folder_add", function(_, RootImpl, _, FileName, Desc) RootImpl:UserMakeFolder(self, Node, FileName, Desc) end) end +function BROWSER:StartRename(Node) + SharedFileFolderLogic(self, Node, true, "", "New filename", "page_go", function(_, RootImpl, _, FileName, Desc) + if not RootImpl:UserRename(self, Node, Desc) then + self:Notify("Rename failed.", NOTIFY_ERROR, 5) + end + end, string.GetFileFromFilename(Node.Path), nil, true, true) +end + function BROWSER:GetUserPromptStack() From 1af89a63001499eef7dfc0cd9648eb984f222dab Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:26:09 -0700 Subject: [PATCH 24/31] AdvDupe1 stubs --- lua/advdupe2/file_browser.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 59a088bc..ac8b6664 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -703,15 +703,17 @@ do end function AdvDupe1Folder:UserRename(Browser, Node, RenameTo) - + return false end function AdvDupe1Folder:UserMenu(Browser, Node, Menu) - + if Node:IsFile() then + Menu:AddOption("Open", function() self:UserLoad(Browser, Node) end, "icon16/page_go.png") + end end function AdvDupe1Folder:UserDelete(Browser, Node) - + return false end function AdvDupe1Folder:UserMakeFolder(Browser, Node, Foldername) From 8fea35f364d6825641cca9eef56bbe9ccecb99c9 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:56:07 -0700 Subject: [PATCH 25/31] Fix userprompt panels not being cleaned up --- lua/advdupe2/file_browser.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index ac8b6664..055ca2ff 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -1559,6 +1559,7 @@ end function BROWSER:PopUserPromptByValue(UserPrompt) if table.RemoveByValue(self:GetUserPromptStack(), UserPrompt) then + if IsValid(UserPrompt.Panel) then UserPrompt.Panel:Remove() end self:DecrementUserPromptStackPtr() end end From 1527c0088d2d9230eeb6fd944cea126bebd3d465 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:58:51 -0700 Subject: [PATCH 26/31] Clear userprompts on refresh files --- lua/advdupe2/file_browser.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 055ca2ff..ad6bf8e1 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -1566,10 +1566,8 @@ end function BROWSER:ClearAllUserPrompts() for _, Prompt in ipairs(self:GetUserPromptStack()) do - Prompt.Panel:Remove() + Prompt:Close() end - table.Empty(self:GetUserPromptStack()) - self.UserPromptStackPtr = 0 end -- Sets input enabled on user prompt stack and determines if user input should be enabled/disabled on the main browser @@ -1810,7 +1808,11 @@ function PANEL:Init() self.SwitchToList.Think = function(b) b.m_Image:SetImageColor(self.Browser.TreeView.ViewType == VIEWTYPE_LIST and VIEWTYPETREE_SELECTED or VIEWTYPETREE_UNSELECTED) end self.SwitchToTiles.Think = function(b) b.m_Image:SetImageColor(self.Browser.TreeView.ViewType == VIEWTYPE_TILES and VIEWTYPETREE_SELECTED or VIEWTYPETREE_UNSELECTED) end - self.Refresh = self:AddRightsideButton("arrow_refresh", "Refresh Files", function(button) UpdateClientFiles(self.Browser) end) + self.Refresh = self:AddRightsideButton("arrow_refresh", "Refresh Files", function(button) + self.Browser:ClearAllUserPrompts() + self.Settings:SetImage("icon16/cog.png") + UpdateClientFiles(self.Browser) + end) self.Help = self:AddRightsideButton("help", "Help Section", function(btn) local Menu = DermaMenu() Menu:AddOption("Bug Reporting", function() From 02da163643ccbd4ce15a62868782222e09e5668c Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:00:54 -0700 Subject: [PATCH 27/31] Fix being unable to load dupes this way --- lua/advdupe2/file_browser.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index ad6bf8e1..64e28594 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -761,7 +761,7 @@ do Node:Expand() local NewFilename = string.GetFileFromFilename(NewFilepath) local NewNode = Node:AddFile(NewFilename) - SetupDataFile(NewNode, File, NewFilename) + SetupDataFile(NewNode, NewFilepath, NewFilename) end) if game.SinglePlayer() then From 5762031a563a364f421a5c9013edbb4daf214c1c Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:16:28 -0700 Subject: [PATCH 28/31] Menu changes? --- lua/weapons/gmod_tool/stools/advdupe2.lua | 64 ++++++++++++++++++----- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/lua/weapons/gmod_tool/stools/advdupe2.lua b/lua/weapons/gmod_tool/stools/advdupe2.lua index 34f2e326..a7cbdc49 100644 --- a/lua/weapons/gmod_tool/stools/advdupe2.lua +++ b/lua/weapons/gmod_tool/stools/advdupe2.lua @@ -1064,6 +1064,14 @@ if(CLIENT) then CreateClientConVar("advdupe2_paste_protectoveride", 1, false, true) CreateClientConVar("advdupe2_debug_openfile", 1, false, true) + local function AddSpacer(Panel) + local Spacer = vgui.Create("DPanel", Panel) + Spacer:SetSize(2, 1) + Spacer:DockMargin(32, 0, 32, 0) + Spacer.Paint = function(self, w, h) self:GetSkin():PaintMenuSpacer(self, w, h) end + Panel:AddItem(Spacer) + end + local BuildCPanel function BuildCPanel(CPanel) CPanel:ClearControls() @@ -1093,7 +1101,7 @@ if(CLIENT) then Check:SetDark(true) Check:SetConVar( "advdupe2_paste_constraints" ) Check:SetValue( 1 ) - Check:SetToolTip("Paste with or without constraints") + Check:SetTooltip("Paste with or without constraints") CPanel:AddItem(Check) Check = vgui.Create("DCheckBoxLabel") @@ -1101,42 +1109,64 @@ if(CLIENT) then Check:SetDark(true) Check:SetConVar( "advdupe2_paste_parents" ) Check:SetValue( 1 ) - Check:SetToolTip("Paste with or without parenting") + Check:SetTooltip("Paste with or without parenting") CPanel:AddItem(Check) + AddSpacer(CPanel) + local Check_1 = vgui.Create("DCheckBoxLabel") local Check_2 = vgui.Create("DCheckBoxLabel") + local Check_3 = vgui.Create("DCheckBoxLabel") - Check_1:SetText( "Unfreeze all after paste" ) + Check_1:SetText( "Keep props frozen after paste" ) Check_1:SetDark(true) - Check_1:SetConVar( "advdupe2_paste_unfreeze" ) - Check_1:SetValue( 0 ) + Check_1:SetValue( 1 ) Check_1.OnChange = function() - if(Check_1:GetChecked() and Check_2:GetChecked()) then - Check_2:SetValue(0) + if Check_1:GetChecked() then + if Check_2:GetChecked() then Check_2:SetValue(0) end + if Check_3:GetChecked() then Check_3:SetValue(0) end end end - Check_1:SetToolTip("Unfreeze all props after pasting") + Check_1:SetTooltip("Keeps all props frozen after pasting") + Check_1.Button.Paint = function(self, w, h) self:GetSkin():PaintRadioButton(self, w, h) end CPanel:AddItem(Check_1) - Check_2:SetText( "Preserve frozen state after paste" ) + Check_2:SetText( "Unfreeze all after paste" ) Check_2:SetDark(true) - Check_2:SetConVar( "advdupe2_preserve_freeze" ) + Check_2:SetConVar( "advdupe2_paste_unfreeze" ) Check_2:SetValue( 0 ) Check_2.OnChange = function() - if(Check_2:GetChecked() and Check_1:GetChecked()) then - Check_1:SetValue(0) + if Check_2:GetChecked() then + if Check_1:GetChecked() then Check_1:SetValue(0) end + if Check_3:GetChecked() then Check_3:SetValue(0) end end end - Check_2:SetToolTip("Makes props have the same frozen state as when they were copied") + Check_2:SetTooltip("Unfreeze all props after pasting") + Check_2.Button.Paint = function(self, w, h) self:GetSkin():PaintRadioButton(self, w, h) end CPanel:AddItem(Check_2) + Check_3:SetText( "Preserve frozen state after paste" ) + Check_3:SetDark(true) + Check_3:SetConVar( "advdupe2_preserve_freeze" ) + Check_3:SetValue( 0 ) + Check_3.OnChange = function() + if Check_3:GetChecked() then + if Check_1:GetChecked() then Check_1:SetValue(0) end + if Check_2:GetChecked() then Check_2:SetValue(0) end + end + end + Check_3:SetTooltip("Makes props have the same frozen state as when they were copied") + Check_3.Button.Paint = function(self, w, h) self:GetSkin():PaintRadioButton(self, w, h) end + CPanel:AddItem(Check_3) + + AddSpacer(CPanel) + Check = vgui.Create("DCheckBoxLabel") Check:SetText( "Area copy constrained props outside of box" ) Check:SetDark(true) Check:SetConVar( "advdupe2_copy_outside" ) Check:SetValue( 0 ) - Check:SetToolTip("Copy entities outside of the area copy that are constrained to entities insde") + Check:SetTooltip("Copy entities outside of the area copy that are constrained to entities insde") CPanel:AddItem(Check) Check = vgui.Create("DCheckBoxLabel") @@ -1147,6 +1177,8 @@ if(CLIENT) then Check:SetToolTip("Copy entities outside of the area copy that are constrained to entities insde") CPanel:AddItem(Check) + AddSpacer(CPanel) + Check = vgui.Create("DCheckBoxLabel") Check:SetText( "Sort constraints by their connections" ) Check:SetDark(true) @@ -1155,6 +1187,8 @@ if(CLIENT) then Check:SetToolTip( "Orders constraints so that they build a rigid constraint system." ) CPanel:AddItem(Check) + AddSpacer(CPanel) + -- Ghost Percentage local NumSlider = vgui.Create( "DNumSlider" ) NumSlider:SetText( "Ghost Percentage:" ) @@ -1195,6 +1229,8 @@ if(CLIENT) then NumSlider:SetToolTip("Change the size of the area copy") CPanel:AddItem(NumSlider) + AddSpacer(CPanel) + local Category1 = vgui.Create("DCollapsibleCategory") CPanel:AddItem(Category1) Category1:SetLabel("Offsets") From f90e44559e0ecc78716862fbfa2c8248ac3d9873 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Tue, 1 Jul 2025 20:40:57 -0700 Subject: [PATCH 29/31] Download prompts --- lua/advdupe2/file_browser.lua | 76 +++++++++++++++++++++-- lua/weapons/gmod_tool/stools/advdupe2.lua | 11 +++- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 64e28594..4443aaa7 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -587,25 +587,27 @@ do local PosX, PosY local SizeW, SizeH - local SizeC - local _, ContentTall = self.Panel:ChildrenSize() + local ContentWide, ContentTall = self.Panel:ChildrenSize() ContentTall = math.Min(ContentTall, self.Browser:GetTall() - 32) local Dock = self.Dock if not Dock then error("No dock??") end if Dock == TOP then - SizeC = ContentTall SizeW = math.ceil((RatioX * ParentWidth) - (DockPadding * 2)) - SizeH = math.ceil(SizeC * RatioY) + SizeH = math.ceil(ContentTall * RatioY) PosX = ((ParentWidth / 2) - (SizeW / 2)) PosY = DockPadding elseif Dock == BOTTOM then - SizeC = ContentTall SizeW = math.ceil((RatioX * ParentWidth) - (DockPadding * 2)) - SizeH = math.ceil(SizeC * RatioY) + SizeH = math.ceil(ContentTall * RatioY) PosX = ((ParentWidth / 2) - (SizeW / 2)) PosY = ParentHeight - SizeH - DockPadding + elseif Dock == FILL then + SizeW = math.ceil((RatioX * ContentWide) - (DockPadding * 2)) + SizeH = math.ceil(ContentTall * RatioY) + PosX = ((ParentWidth / 2) - (SizeW / 2)) + PosY = (ParentHeight / 2) - (SizeH / 2) - (DockPadding / 2) end self.Panel:SetPos(PosX, PosY) @@ -756,12 +758,28 @@ do local DataPath = (Node.Path or "advdupe2") .. "/" .. Filename AdvDupe2.SavePath = DataPath + -- This is kinda weird but it's the only way I've been able to reliably avoid deadlocks here + hook.Add("AdvDupe2_InitProgressBar", "AdvDupe2_BrowserDownloadPrompt", function(Txt) + hook.Remove("AdvDupe2_InitProgressBar", "AdvDupe2_BrowserDownloadPrompt") + if Txt ~= "Saving:" then return end + + Browser:ShowSavePrompt() + hook.Add("AdvDupe2_RemoveProgressBar", "AdvDupe2_BrowserDownloadPrompt", function() + hook.Remove("AdvDupe2_RemoveProgressBar", "AdvDupe2_BrowserDownloadPrompt") + Browser:HideSavePrompt() + end) + end) -- Enqueue handler Browser:AwaitingFile(DataPath .. ".txt", function(NewFilepath) + if not NewFilepath then + Browser:HideSavePrompt() + return + end Node:Expand() local NewFilename = string.GetFileFromFilename(NewFilepath) local NewNode = Node:AddFile(NewFilename) SetupDataFile(NewNode, NewFilepath, NewFilename) + Browser:HideSavePrompt() end) if game.SinglePlayer() then @@ -1570,6 +1588,52 @@ function BROWSER:ClearAllUserPrompts() end end +local WORLD = Material("icon16/world.png", "smooth") +local FOLDER = Material("icon16/folder.png", "smooth") +local PAGE = Material("icon16/page.png", "smooth") + +function BROWSER:ShowSavePrompt() + if self.BrowserWait then return end + + local BrowserWait = self:PushUserPrompt() + self.BrowserWait = BrowserWait + + BrowserWait:SetDock(FILL) + BrowserWait.Panel.ChildrenSize = function(notif) + local Parent = notif:GetParent() + local W, _ = Parent:GetSize() + return W / 1.5, 128 + end + BrowserWait:SetBlocking(true) + BrowserWait.Panel:SetPaintBackground(true) + + local Text = BrowserWait.Panel:Add("DLabel") + Text:SetText("Downloading...") + Text:Dock(TOP) + Text:SetTextInset(0, 6) + Text:SetDark(true) + Text:SetContentAlignment(8) + + function BrowserWait.Panel:Paint(w, h) + DPanel.Paint(self, w, h) + local AnimTime = 1.5 + local Time = (UserInterfaceTimeFunc() % AnimTime) / AnimTime + local SinTime = math.sin(Time * math.pi) + local Size = math.ease.OutQuad(SinTime) * 32 + + surface.SetDrawColor(255, 255, 255, 255) + surface.SetMaterial(WORLD) surface.DrawTexturedRectRotated(24, h / 2, 32, 32, 0) + surface.SetMaterial(FOLDER) surface.DrawTexturedRectRotated(w - 24, h / 2, 32, 32, 0) + surface.SetMaterial(PAGE) surface.DrawTexturedRectRotated(math.Remap(Time, 0, 1, 24, w - 24), math.Remap(SinTime, 0, 1, h / 2, h / 3), Size, Size, 0) + end +end + +function BROWSER:HideSavePrompt() + if not self.BrowserWait then return end + self.BrowserWait:Close() + self.BrowserWait = nil +end + -- Sets input enabled on user prompt stack and determines if user input should be enabled/disabled on the main browser -- Returns true if input is enabled -- This stuff is kinda weird, but doesnt run that much and seems to be pretty OK. Real docking seems to cause diff --git a/lua/weapons/gmod_tool/stools/advdupe2.lua b/lua/weapons/gmod_tool/stools/advdupe2.lua index a7cbdc49..dd63cdba 100644 --- a/lua/weapons/gmod_tool/stools/advdupe2.lua +++ b/lua/weapons/gmod_tool/stools/advdupe2.lua @@ -1894,11 +1894,15 @@ if(CLIENT) then AdvDupe2.BusyBar = true end net.Receive("AdvDupe2_InitProgressBar", function() - AdvDupe2.InitProgressBar(net.ReadString()) + local Label = net.ReadString() + AdvDupe2.InitProgressBar(Label) + hook.Run("AdvDupe2_InitProgressBar", Label) end) net.Receive("AdvDupe2_UpdateProgressBar", function() - AdvDupe2.ProgressBar.Percent = net.ReadFloat() + local Percent = net.ReadFloat() + AdvDupe2.ProgressBar.Percent = Percent + hook.Run("AdvDupe2_UpdateProgressBar", Percent) end) function AdvDupe2.RemoveProgressBar() @@ -1912,6 +1916,7 @@ if(CLIENT) then end net.Receive("AdvDupe2_RemoveProgressBar", function() AdvDupe2.RemoveProgressBar() + hook.Run("AdvDupe2_RemoveProgressBar") end) net.Receive("AdvDupe2_ResetOffsets", function() @@ -1963,7 +1968,7 @@ if(CLIENT) then end end) - net.Receive("AdvDupe2_SetDupeInfo", function(len, ply, len2) + net.Receive("AdvDupe2_SetDupeInfo", function(len, ply) if AdvDupe2.Info then AdvDupe2.Info.File:SetText("File: "..net.ReadString()) AdvDupe2.Info.Creator:SetText("Creator: "..net.ReadString()) From 6c02dbdba0565fc6265fe788cd899c8903eb5336 Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:13:33 -0700 Subject: [PATCH 30/31] Remove attribution --- lua/advdupe2/file_browser.lua | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index 4443aaa7..d73b914d 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -1,13 +1,3 @@ ---[[ - Title: Adv. Dupe 2 File Browser - - Desc: Displays and interfaces with duplication files. - - Authors: March (v2.0), TB (v1.0) - - Version: 2.0 -]] - -- Enums local ADVDUPE2_AREA_ADVDUPE2 = AdvDupe2.AREA_ADVDUPE2 local ADVDUPE2_AREA_PUBLIC = AdvDupe2.AREA_PUBLIC From 97a5e03f7d11e6c7e8e1d78e0fdba09c98dfe88f Mon Sep 17 00:00:00 2001 From: march <106459595+marchc1@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:24:20 -0700 Subject: [PATCH 31/31] Implement StartDelete, AdvDupe2Folder:UserDelete --- lua/advdupe2/file_browser.lua | 52 ++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/lua/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index d73b914d..e275cd84 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -809,7 +809,12 @@ do end function AdvDupe2Folder:UserDelete(Browser, Node) - + local NodePath = "advdupe2/" .. GetNodeDataPath(Node) .. ".txt" + local Success = file.Delete(NodePath) + if Success then + Node:Remove(Node) + end + return Success end function AdvDupe2Folder:UserMakeFolder(Browser, Node, Foldername) @@ -1509,7 +1514,52 @@ function BROWSER:StartRename(Node) end, string.GetFileFromFilename(Node.Path), nil, true, true) end +function BROWSER:StartDelete(Node) + local Prompt = self:PushUserPrompt() + Prompt:SetBlocking(true) + Prompt:SetDock(BOTTOM) + Prompt.Panel:DockPadding(4,4,4,4) + + local DescParent = Prompt:Add("Panel") + DescParent:Dock(TOP) + DescParent:DockMargin(0, 4, 0, 0) + DescParent:SetSize(20, 32) + DescParent:SetPaintBackgroundEnabled(false) + DescParent:SetZPos(10000) + + local Cancel = DescParent:Add("DImageButton") + Cancel:Dock(RIGHT) + Cancel:SetSize(20) + Cancel:SetStretchToFit(false) + Cancel:SetImage("icon16/cancel.png") + + local Delete = DescParent:Add("DImageButton") + Delete:Dock(RIGHT) + Delete:SetSize(24) + Delete:SetStretchToFit(false) + Delete:SetImage("icon16/bin.png") + + local Name = DescParent:Add("DLabel") + Name:Dock(FILL) + Name:SetDark(true) + Name:SetText("Are you sure to want to delete\n" .. Node.Path .. "?") + Name:SetAutoStretchVertical(true) + Name:SetZPos(1) + function Delete.DoClick() + local RootImpl = self:GetRootImpl(Node) + if RootImpl:UserDelete(self, Node) then + self:Notify("Deleted " .. Node.Text .. ".", NOTIFY_CLEANUP, 5) + else + self:Notify("Delete failed.", NOTIFY_ERROR, 5) + end + Prompt:Close() + end + + function Cancel:DoClick() + Prompt:Close() + end +end function BROWSER:GetUserPromptStack() local UserPrompts = self.UserPrompts