diff --git a/lua/advdupe2/cl_file.lua b/lua/advdupe2/cl_file.lua index 58dc4bcd..7c245b9a 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) @@ -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 @@ -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) @@ -52,25 +52,29 @@ 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 + 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 - AdvDupe2.FileBrowser.AutoSaveNode:AddFile(filename) - AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.AutoSaveNode) + if add then + 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(OriginalPath, path) end - if(!errored)then + + if not errored then AdvDupe2.Notify("File successfully saved!",NOTIFY_GENERIC, 5) end end @@ -94,17 +98,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 +120,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/advdupe2/file_browser.lua b/lua/advdupe2/file_browser.lua index c1381950..e275cd84 100644 --- a/lua/advdupe2/file_browser.lua +++ b/lua/advdupe2/file_browser.lua @@ -1,1107 +1,1772 @@ ---[[ - Title: Adv. Dupe 2 File Browser +-- Enums +local ADVDUPE2_AREA_ADVDUPE2 = AdvDupe2.AREA_ADVDUPE2 +local ADVDUPE2_AREA_PUBLIC = AdvDupe2.AREA_PUBLIC +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" +local LowercaseFileBrowserPrefix = string.lower(FileBrowserPrefix) + +-- 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 TimeToOpenPrompts_cv, TimeToClosePrompts_cv + +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 + 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_promptclosetime", "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, + "How tall a single file/directory node is in the file browser, in pixels.", 0, 1000000) + 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(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(LowercaseFileBrowserPrefix .. "_menu_nodeicon_folderempty", "icon16/folder.png", true, false, + "The materials/ localized path for an empty folder.") + 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(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] New folder/file", "\n", + string.rep(" ", Offset), "^\n", + string.rep(" ", Offset), "^--- You are here" + } + end - Desc: Displays and interfaces with duplication files. + 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(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(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(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(LowercaseFileBrowserPrefix .. "_menu_nodepadding_icontotext", "6", true, false, + CreateNodeTextRepresentation("Distance, in pixels, between the node icon and the node text.", 12), 0, 1000000) - Author: TB - Version: 1.0 -]] + local IconFolderEmpty, IconFolderContains, IconFile -local History = {} -local Narrow = {} + local function UpdateMaterial(Mat, Old, CV) + local New = CV:GetString() + if Old ~= New then + Mat = Material(New, "smooth") + end -local switch = true -local count = 0 + return Mat + end -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 + 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() + + 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 + + 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 - table.insert(History, txt) - table.sort(History, function(a, b) return a < b end) +local function GetNodeBounds(ScrollOffset, AbsIndex, Width, Depth) + return + Depth * NodeDepthWidth, + (TallOfOneNode * (AbsIndex - 1)) - ScrollOffset, + Width - (Depth * NodeDepthWidth), + NodeTall 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 +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 + +-- 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 - 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 + + 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 - table.insert(temp, History[i]) + for i2 = 1, #monthStoreJustInCase do + ret[#ret + 1] = monthStoreJustInCase[i2] + end + monthStoreJustInCase = {} + monthTester = buildMonth end - elseif (char1 < char2) then - break end end - end - Narrow = temp -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 -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] + function NODE:GetSortedChildNodes() + self:PerformResort() + return ipairs(self.Sorted) + end + + function NODE.InjectIntoBrowser(TreeView, Browser) + for FuncName, Func in pairs(NODE) do + TreeView[FuncName] = Func + end + + NODE.Init(TreeView, NODETYPE_FOLDER, Browser) 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 +-- This interface describes the logic behind a root folder (like AdvDupe1 or AdvDupe2). + +local IRootFolder_MT = {} +local IRootFolder = setmetatable({}, IRootFolder_MT) + +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.UserLoad = 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 + 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 + 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 - 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 + +-- 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 + + self.StartOpen = UserInterfaceTimeFunc() + self.WillOpenAt = self.StartOpen + TimeToOpenPrompts_cv:GetFloat() + + self.Panel = Browser:Add("DScrollPanel") + local PanelColor = Color(255, 255, 255, 213) + self.Panel.Paint = function(panel, w, h) + local Skin = panel:GetSkin() + local SkinTex = Skin.tex + + SkinTex.Panels.Normal(0, 0, w, h, PanelColor) + end 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 + + function USERPROMPT:GetPanel() return self.Panel end + + function USERPROMPT:Add(Type) + return self.Panel:Add(Type) 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 + 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 - file.CreateDir(path) - local Folder = node:AddFolder(name) - node.Control:Sort(node) + function USERPROMPT:GetBlocking() return self.Blocking or false end + function USERPROMPT:SetBlocking(Blocking) self.Blocking = Blocking and true or false end - if (not node.m_bExpanded) then - node:SetExpanded() + function USERPROMPT:Destroy() + if IsValid(self.Panel) then + self.Panel:Remove() + end end - node.Control:SetSelected(Folder) - if (Controller.Expanded) then - AdvDupe2.FileBrowser:Slide(false) + function USERPROMPT:SetDock(Dock) + self.Dock = Dock 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]) + 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 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 + SizeW = math.ceil((RatioX * ParentWidth) - (DockPadding * 2)) + SizeH = math.ceil(ContentTall * RatioY) + PosX = ((ParentWidth / 2) - (SizeW / 2)) + PosY = DockPadding + elseif Dock == BOTTOM then + SizeW = math.ceil((RatioX * ParentWidth) - (DockPadding * 2)) + 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 - node.ChildrenExpanded = {} + + self.Panel:SetPos(PosX, PosY) + self.Panel:SetSize(SizeW, SizeH) + self.Panel:SetAlpha(RatioA * 255) + + return true end -end -local function CollapseParentsComplete(node) - if (not node.ParentNode.ParentNode) then - node:SetExpanded(false) - return + -- Call this to close and pop later. + -- This also forces Blocking back to false for main UI panel. + function USERPROMPT:Close() + self.Blocking = false + self.Closing = true + self.StartClose = UserInterfaceTimeFunc() + self.WillCloseAt = self.StartClose + TimeToClosePrompts_cv:GetFloat() end - 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 +-- This turns a data-folder path name into something AdvDupe2.UploadFile can tolerate +local function GetNodeDataPath(Node) + local Path = Node.Path + if not Path then return "" end + + 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 - return path .. ".txt" + + if not file.Exists(Path, "DATA") then + AdvDupe2.Notify("File '" .. Path .. "' does not exist.") + return false + end + + return true end -local function GetFullPath(node) - local path, area = GetNodePath(node) - if (area == 0) then - path = AdvDupe2.DataFolder .. "/" .. path .. "/" - elseif (area == 1) then +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 - path = "adv_duplicator/" .. path .. "/" + 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 - return path end -local function GetNodeRoot(node) - local Root - while (true) do - if (not node.ParentNode.ParentNode) then - Root = node - break + +-- These are the builtin IRootFolder implementations. +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 - node = node.ParentNode end - return Root -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 + function AdvDupe1Folder:UserLoad(Browser, Node) + AdvDupe2.UploadFile(GetNodeDataPath(Node), ADVDUPE2_AREA_ADVDUPE1) + end + + function AdvDupe1Folder:UserPreview(Browser, Node) + OpenPreview(Node, ADVDUPE2_AREA_ADVDUPE1) 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 + function AdvDupe1Folder:UserSave(Browser, Node, Filename, Description) + 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) + 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) + end - node.Control:Sort(node.ParentNode) + function AdvDupe1Folder:UserGetModTime(Browser, Node) + return 0 + end + + function AdvDupe1Folder:UserGetSize(Browser, Node) + return 0 + end + + IRootFolder(AdvDupe1Folder) -- validation end -local function MoveFileClient(node) - if (not node) then - AdvDupe2.Notify("Select a folder to move the file to.", NOTIFY_ERROR) - return +do + 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/") end - if (node.Derma.ClassName == "advdupe2_browser_file") then - AdvDupe2.Notify("You muse select a folder as a destination.", NOTIFY_ERROR) - return + + function AdvDupe2Folder:UserLoad(Browser, Node) + AdvDupe2.UploadFile(GetNodeDataPath(Node), ADVDUPE2_AREA_ADVDUPE2) end - local base = AdvDupe2.DataFolder - local ParentNode - local node2 = node.Control.ActionNode - local path, area = GetNodePath(node2) - local path2, area2 = GetNodePath(node) + function AdvDupe2Folder:UserPreview(Browser, Node) + OpenPreview(Node, ADVDUPE2_AREA_ADVDUPE2) + end - if (area ~= area2 or path == path2) then - AdvDupe2.Notify("Cannot move files between these directories.", NOTIFY_ERROR) - return + function AdvDupe2Folder:UserSave(Browser, Node, Filename, Description) + 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 + RunConsoleCommand("AdvDupe2_SaveFile", Filename, Description, GetNodePath(Node)) + else + RunConsoleCommand("AdvDupe2_SaveFile", Filename) + end + 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) + if Node:IsFile() then + Menu:AddOption("Open", function() self:UserLoad(Browser, Node) end, "icon16/page_go.png") + Menu:AddOption("Preview", function() self:UserPreview(Browser, Node) end, "icon16/information.png") + Menu:AddSpacer() + 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", 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", function() Browser:StartSearch(Node) end, "icon16/magnifier.png") + end + 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) + 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 - if (area == 2) then base = "adv_duplicator" end - local savepath = AdvDupe2.GetFilename( - base .. "/" .. path2 .. "/" .. node2.Label:GetText()) - local OldFile = base .. "/" .. path .. ".txt" + function AdvDupe2Folder:UserGetModTime(Browser, Node) + if not Node.Path then return end - 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) + 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 -local function DeleteFilesInFolders(path) - local files, folders = file.Find(path .. "*", "DATA") +-- This is the base browser panel. Most VGUI interactions happen here + +local BROWSERTREE = {} +AccessorFunc(BROWSERTREE, "m_pSelectedItem", "SelectedItem") - for k, v in pairs(files) do file.Delete(path .. v) end +local origSetTall +local function SetTall(self, val) + origSetTall(self, val) + self.VBar:SetUp(self:GetParent():GetTall(), self:GetTall()) +end + +function BROWSERTREE:Init() + self:SetTall(0) + origSetTall = self.SetTall + self.SetTall = SetTall + + self.VBar = self:GetParent():Add "DVScrollBar" + self.VBar:Dock(RIGHT) - for k, v in pairs(folders) do DeleteFilesInFolders(path .. v .. "/") end + -- Implement NODE + NODE.InjectIntoBrowser(self, self:GetParent()) + self.SortDirty = true + self.ExpandedNodeArray = {} - file.Delete(path) + self.LastClick = UserInterfaceTimeFunc() 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) +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:UserLoad(self.Browser, Node) end + else + self:SetSelected(Node) -- A node was clicked, select it end - for k, v in pairs(node.Folders) do - for i, j in pairs(SearchNodes(v, name)) do - table.insert(tab, j) + self.LastClick = UserInterfaceTimeFunc() +end + +function BROWSERTREE:DoNodeRightClick(Node) + self:SetSelected(Node) + + 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 end - return tab + RootImpl:UserMenu(self.Browser, Node, Menu) + + Menu:SetAlpha(0) + Menu:AlphaTo(255, 0.1, 0) + Menu:Open() +end + +function BROWSERTREE:OnMouseWheeled(dlta) + return self.VBar:OnMouseWheeled(dlta) 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 +function BROWSERTREE:SetSelected(node) + if self.m_pSelectedItem then + self.m_pSelectedItem.Selected = false + end + self.m_pSelectedItem = node + if node then + node.Selected = true end end -function BROWSER:DoNodeRightClick(node) - self:SetSelected(node) +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 - 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 +-- This function collapses the current node state into a single sequential array +function BROWSERTREE:SortRecheck() + if not self.SortDirty then 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 + table.Empty(self.ExpandedNodeArray) + DoRecursiveVistesting(self, self.ExpandedNodeArray) - 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) + -- 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 BROWSERTREE: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 + +-- 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 BROWSERTREE:FlushImmediateState() + 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() + + for I = MOUSE_LEFT, MOUSE_LAST do + local Mouse = ImmediateState.Mouse[I] + if not Mouse then + Mouse = {} + ImmediateState.Mouse[I] = Mouse 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) + + Mouse.LastDown = Mouse.Down or false + Mouse.Down = input.IsMouseDown(I) and CanInput + 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 = not CanInput -- if can't input, never even do input testing + 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 - 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) + + if ImmediateState.Mouse.Released then + ImmediateState.ReleasedNode = ImmediateState.Depressed + ImmediateState.IsExpanderReleased = ImmediateState.IsExpanderHovered end - parent.Cancel:SetVisible(false) + + BreakInputTesting = true + else + ImmediateState.Hovered = false + ImmediateState.IsExpanderHovered = 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 end - if (not node.Control.Search) then - Menu:AddSpacer() - Menu:AddOption("Collapse Folder", function() - if (node.ParentNode.ParentNode) then - node.ParentNode:SetExpanded(false) - end - end) - Menu:AddOption("Collapse Root", function() CollapseParentsComplete(node) end) - if (parent.Expanded) then - Menu:AddOption("Cancel Action", function() parent.Cancel:DoClick() end) - end + + if ImmediateState.Mouse.Released or not ImmediateState.Mouse.Down then + ImmediateState.Depressed = nil + ImmediateState.IsExpanderDepressed = nil end - Menu:Open() + if ImmediateState.IsExpanderHovered then + ImmediateState.Mouse.Cursor = "hand" + end end -local function CollapseParents(node, val) - if (not node) then return end - node.ChildList:SetTall(node.ChildList:GetTall() - val) - CollapseParents(node.ParentNode, val) -end +-- This function considers the current immediate state and triggers events/sets cursor +function BROWSERTREE:ConsiderCurrentState() + local ImmediateState = self.ImmediateState -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 + -- 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 - 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 + + if Released then + local Node = ImmediateState.ReleasedNode + local ExpanderReleased = ImmediateState.IsExpanderReleased + + if Node and ExpanderReleased then + Node:ToggleExpanded() 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 + + self:SetCursor(ImmediateState.Mouse.Cursor) +end + +-- This function paints the current immediate state to the DPanel. +function BROWSERTREE:PaintCurrentState(PanelWidth, PanelHeight) + 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 - CollapseParents(parent, 20) - node:Remove() - if (#parent.Files == 0 and #parent.Folders == 0) then - parent.Expander:Remove() - parent.Expander = nil - parent.m_bExpanded = false + + -- 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 - if (self.VBar.Scroll > self.VBar.CanvasSize) then - self.VBar:SetScroll(self.VBar.Scroll) - end - if (self.m_pSelectedItem) then - self.m_pSelectedItem = nil + + 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) * 66 + local OldClipping = DisableClipping(true) + surface.SetDrawColor(0, 0, 0, Alpha) + surface.DrawRect(0, 0, self.Browser:GetSize()) + DisableClipping(OldClipping) end end -function BROWSER:OnMouseWheeled(dlta) - return self.VBar:OnMouseWheeled(dlta) +function BROWSERTREE:Think() + FlushConvars() + -- 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 BROWSERTREE:Paint(w, h) + DPanel.Paint(self, w, h) + -- Renders the immediate state to the screen + self:PaintCurrentState(w, h) end -function BROWSER:AddFolder(text) - local node = vgui.Create("advdupe2_browser_folder", self) - node.Control = self +derma.DefineControl(LowercaseFileBrowserPrefix .. "_browser_tree", FileBrowserPrefix .. " File Browser", BROWSERTREE, "Panel") - 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) +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") - return node +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 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) +function BROWSER:Init() + setbrowserpnlsize = self.SetSize + self.SetSize = SetBrowserPnlSize + self.TreeView = vgui.Create(LowercaseFileBrowserPrefix .. "_browser_tree", self) + + self:SetPaintBackground(true) + self:SetPaintBackgroundEnabled(false) + self:SetPaintBorderEnabled(false) + self:SetBackgroundColor(self:GetSkin().text_bright) - return node + self:SetViewType(VIEWTYPE_TREE) end -function BROWSER:Sort(node) - tableSortNodes(node.Folders) - tableSortNodes(node.Files) +-- Public facing API - 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 +function BROWSER:SetViewType(ViewType) + self.TreeView.ViewType = ViewType or error("No viewtype provided") + self:MarkSortDirty() end -function BROWSER:SetSelected(node) - if (IsValid(self.m_pSelectedItem)) then - self.m_pSelectedItem:SetSelected(false) - end - self.m_pSelectedItem = node - if (node) then node:SetSelected(true) end +function BROWSER:MarkSortDirty() + if not self.TreeView then return end + self.TreeView.SortDirty = true end -local function ExpandParents(node, val) - if (not node) then return end - node.ChildList:SetTall(node.ChildList:GetTall() + val) - ExpandParents(node.ParentNode, val) +-- Call this to enqueue a file callback, if you need one. +-- The filepath should be unique. +function BROWSER:AwaitingFile(Filepath, Callback) + self.WaitingQueue = self.WaitingQueue or {} + self.WaitingQueue[Filepath] = Callback end -function BROWSER:Expand(node) - node.ChildList:SetTall(node.Nodes * 20) - table.insert(node.ParentNode.ChildrenExpanded, node) - ExpandParents(node.ParentNode, node.Nodes * 20) -end +-- Call this to call any file callback with that name and remove it from the callback list. +function BROWSER:IncomingFile(Filepath, ...) + if not self.WaitingQueue then return end -local function ExtendParents(node) - if (not node) then return end - node.ChildList:SetTall(node.ChildList:GetTall() + 20) - ExtendParents(node.ParentNode) -end + local Callback = self.WaitingQueue[Filepath] + if not Callback then ErrorNoHalt("File Browser ['" .. FileBrowserPrefix .. "'] error: incoming file '" .. Filepath .. "' had no event handler.\n") return end -function BROWSER:Extend(node) - node.ChildList:SetTall(node.ChildList:GetTall() + 20) - ExtendParents(node.ParentNode) + self.WaitingQueue[Filepath] = nil + return Callback(...) end -function BROWSER:Collapse(node) - CollapseParents(node.ParentNode, node.ChildList:GetTall()) +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()) - 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 + RealNode.Root = RealNode + RealNode.RootImpl = RootFolderType -function BROWSER:RenameNode(name) - self.ActionNode.Label:SetText(name) - self.ActionNode.Label:SizeToContents() - self:Sort(self.ActionNode.ParentNode) -end + RootFolderType:Init(self, RealNode) -function BROWSER:MoveNode(name) - self:RemoveNode(self.ActionNode) - self.ActionNode2:AddFile(name) - self:Sort(self.ActionNode2) + return RealNode end -function BROWSER:DeleteNode() - self:RemoveNode(self.ActionNode) +function BROWSER:GetRootImpl(Node) + return Node.Root.RootImpl or error "Cannot find IRootFolder implementation!" end -derma.DefineControl("advdupe2_browser_tree", "AD2 File Browser", BROWSER, "Panel") +local NotifIcons = { + [NOTIFY_CLEANUP] = "bin", + [NOTIFY_ERROR] = "error", + [NOTIFY_GENERIC] = "information", + [NOTIFY_HINT] = "lightbulb", + [NOTIFY_UNDO] = "arrow_undo", +} + +function BROWSER:Notify(Message, Level, Time) + Level = Level or NOTIFY_GENERIC + Time = Time or 5 + + local Notif = self:PushUserPrompt() + Notif:SetDock(TOP) + + local Text = Notif:Add("DLabel") + Text:SetText(Message or "") + 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 -local FOLDER = {} + timer.Simple(Time, function() if IsValid(Notif.Panel) then Notif:Close() end end) +end -AccessorFunc(FOLDER, "m_bBackground", "PaintBackground", FORCE_BOOL) -AccessorFunc(FOLDER, "m_bgColor", "BackgroundColor") +local function SharedFileFolderLogic(self, Node, DoDesc, NameTextPlaceholder, DescTextPlaceholder, Icon, Completed, LastName, LastDesc, SkipToDesc, BlockName) + local Prompt = self:PushUserPrompt() + Prompt:SetBlocking(true) + Prompt:SetDock(BOTTOM) + Prompt.Panel:DockPadding(4,4,4,4) -Derma_Hook(FOLDER, "Paint", "Paint", "Panel") + local Name, Desc, Cancel, Save -function FOLDER:Init() - self:SetMouseInputEnabled(true) + local function FinishSave() + -- Require filename + local FileName = Name:GetText() + if FileName == nil or FileName == "" then + self:Notify("You must specify a file/folder path.", NOTIFY_ERROR, 2) + return + end - self:SetTall(20) - self:SetPaintBackground(true) - self:SetPaintBackgroundEnabled(false) - self:SetPaintBorderEnabled(false) - self:SetBackgroundColor(Color(0, 0, 0, 0)) + local RootImpl = self:GetRootImpl(Node) + Completed(self, RootImpl, Node, FileName, Desc and Desc:GetText() or "") + Prompt:Close() + end - self.Icon = vgui.Create("DImage", self) - self.Icon:SetImage("icon16/folder.png") + -- I tried SetTabPosition; it doesn't like to work, so we're doing it ourselves + + if DoDesc then + Name = Prompt:Add("DTextEntry") + Name:SetAllowNonAsciiCharacters(true) + Name:SetTabbingDisabled(false) + Name:Dock(TOP) + 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) + 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(DescTextPlaceholder) + if LastDesc then Desc:SetText(LastDesc) end + 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(NameTextPlaceholder) + Name:SetZPos(1) + if LastName then Name:SetText(LastName) end + end - self.Icon:SizeToContents() + function Name:OnEnter() + self:KillFocus() + if Desc then + Desc:SelectAllOnFocus(true) + Desc:OnMousePressed() + Desc:RequestFocus() + else + FinishSave() + end + end - self.Label = vgui.Create("DLabel", self) - self.Label:SetDark(true) + 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 - self.m_bExpanded = false - self.Nodes = 0 - self.ChildrenExpanded = {} + DTextEntry.OnKeyCode(self, KeyCode) + end + end - self:Dock(TOP) + function Name:OnMousePressed() + self:OnGetFocus() + self:SelectAllOnFocus(true) + end - self.ChildList = vgui.Create("Panel", self:GetParent()) - self.ChildList:Dock(TOP) - self.ChildList:SetTall(0) -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 -local function ExpandNode(self) - self:GetParent():SetExpanded() -end + DTextEntry.OnKeyCode(self, KeyCode) + end + function Desc:OnMousePressed() + self:OnGetFocus() + self:SelectAllOnFocus(true) + end + 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) + function Save:DoClick() + FinishSave() + end + function Cancel:DoClick() + Prompt:Close() end - local node = vgui.Create("advdupe2_browser_folder", self.ChildList) - node.Control = self.Control + -- 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 - node.Offset = self.Offset + 20 + if SkipToDesc then + Desc:RequestFocus() + Desc:OnMousePressed() + else + Name:RequestFocus() + Name:OnMousePressed() + end +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 = {} +function BROWSER:StartSave(Node) + if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end - self.Nodes = self.Nodes + 1 - self.Folders[#self.Folders + 1] = node + SharedFileFolderLogic(self, Node, true, "Dupe name", "Dupe description", "disk", function(_, RootImpl, _, FileName, Desc) + RootImpl:UserSave(self, Node, FileName, Desc) + self.LastFileName = FileName + self.LastFileDesc = Desc + end, self.LastFileName, self.LastFileDesc) +end - if (self.m_bExpanded) then - self.Control:Extend(self) - end +function BROWSER:StartFolder(Node) + if not Node:IsFolder() then ErrorNoHaltWithStack("AdvDupe2: Attempted to call StartSave on a non-folder. Operation canceled.") return false end - return node + SharedFileFolderLogic(self, Node, false, "Dupe name", "Dupe description", "folder_add", function(_, RootImpl, _, FileName, Desc) + RootImpl:UserMakeFolder(self, Node, FileName, Desc) + end) 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 +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 FOLDER:AddFile(text) - if (self.Nodes == 0) then - self.Expander = vgui.Create("DExpandButton", self) - self.Expander.DoClick = ExpandNode - self.Expander:SetPos(self.Offset, 2) +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 - 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 + function Cancel:DoClick() + Prompt:Close() + end +end - self.Nodes = self.Nodes + 1 - table.insert(self.Files, 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 +-- 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 -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 + return StackPtr 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 +-- 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 - self.FileI = fileI - self.DirI = dirI + StackPtr = StackPtr - 1 + if StackPtr < 0 then ErrorNoHaltWithStack("AdvDupe2: User prompt stack underflow???") StackPtr = 0 end + self.UserPromptStackPtr = StackPtr - if self.LoadingFirst then - if self.LoadingPath == "advdupe2/" then self:SetExpanded(true) end - self.LoadingFirst = false - end - end + 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 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) +function BROWSER:PushUserPrompt() + local UserPrompts = self:GetUserPromptStack() + local StackPointer = self:IncrementUserPromptStackPtr() + local Prompt = USERPROMPT(self) + 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 + +function BROWSER:PopUserPromptByValue(UserPrompt) + if table.RemoveByValue(self:GetUserPromptStack(), UserPrompt) then + if IsValid(UserPrompt.Panel) then UserPrompt.Panel:Remove() end + self:DecrementUserPromptStackPtr() end end -function FOLDER:SetSelected(bool) - if (bool) then - self:SetBackgroundColor(self:GetSkin().bg_color_bright) - else - self:SetBackgroundColor(Color(0, 0, 0, 0)) +function BROWSER:ClearAllUserPrompts() + for _, Prompt in ipairs(self:GetUserPromptStack()) do + Prompt:Close() end end -function FOLDER:OnMousePressed(code) - if (code == 107) then - self.Control:DoNodeLeftClick(self) - elseif (code == 108) then - self.Control:DoNodeRightClick(self) +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 -derma.DefineControl("advdupe2_browser_folder", "AD2 Browser Folder node", FOLDER, "Panel") +function BROWSER:HideSavePrompt() + if not self.BrowserWait then return end + self.BrowserWait:Close() + self.BrowserWait = nil +end -local FILE = {} +-- 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 StackDocks = self.StackDocks or {} + StackDocks[TOP] = 0 + StackDocks[LEFT] = 0 + StackDocks[RIGHT] = 0 + StackDocks[BOTTOM] = 0 -AccessorFunc(FILE, "m_bBackground", "PaintBackground", FORCE_BOOL) -AccessorFunc(FILE, "m_bgColor", "BackgroundColor") -Derma_Hook(FILE, "Paint", "Paint", "Panel") + local UserPrompts = self:GetUserPromptStack() + local Blocking = false -function FILE:Init() - self:SetMouseInputEnabled(true) + local LastBlockingPanel = false - self:SetTall(20) - self:SetPaintBackground(true) - self:SetPaintBackgroundEnabled(false) - self:SetPaintBorderEnabled(false) - self:SetBackgroundColor(Color(0, 0, 0, 0)) + local RemoveValues + for K, Prompt in ipairs(UserPrompts) do + local Panel = Prompt:GetPanel() + Panel:SetMouseInputEnabled(LastBlockingPanel and false or true) + + Panel:SetZPos(1000 + K) + Blocking = Blocking or Prompt.Blocking + + if Prompt.Blocking then + LastBlockingPanel = Panel + else + LastBlockingPanel = false + end - self.Icon = vgui.Create("DImage", self) - self.Icon:SetImage("icon16/page.png") + if not Prompt:DoThink() then + RemoveValues = RemoveValues or {} -- Only allocate this table if we need to remove prompts + RemoveValues[#RemoveValues + 1] = Prompt + end + end - self.Icon:SizeToContents() + local DockPadding = 4 - self.Label = vgui.Create("DLabel", self) - self.Label:SetDark(true) + for I = #UserPrompts, 1, -1 do + local Prompt = UserPrompts[I] + if Prompt and IsValid(Prompt.Panel) then + local Panel = Prompt.Panel - self:Dock(TOP) -end + local X, Y = Panel:GetPos() + local W, H = Panel:GetSize() + local Dock = Prompt.Dock -function FILE:SetSelected(bool) - if (bool) then - self:SetBackgroundColor(self:GetSkin().bg_color_bright) - else - self:SetBackgroundColor(Color(0, 0, 0, 0)) + 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) + end end + + return not Blocking end -function FILE:OnMousePressed(code) - if (code == 107) then - self.Control:DoNodeLeftClick(self) - elseif (code == 108) then - self.Control:DoNodeRightClick(self) +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) end -derma.DefineControl("advdupe2_browser_file", "AD2 Browser File node", FILE, "Panel") +derma.DefineControl(LowercaseFileBrowserPrefix .. "_browser_panel", "AD2 File Browser", BROWSER, "Panel") local PANEL = {} AccessorFunc(PANEL, "m_bBackground", "PaintBackground", FORCE_BOOL) @@ -1109,7 +1774,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() @@ -1118,22 +1783,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) - - 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) + + 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 @@ -1143,12 +1810,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 @@ -1158,37 +1825,68 @@ local function PanelSetSize(self, x, y) end -local function UpdateClientFiles() +local function UpdateClientFiles(Browser) + Browser.TreeView:Clear() + Browser.TreeView:Expand() - local pnlCanvas = AdvDupe2.FileBrowser.Browser.pnlCanvas + Browser:AddRootFolder(AdvDupe1Folder) + Browser:AddRootFolder(AdvDupe2Folder) - for i = 1, 2 do - if (pnlCanvas.Folders[1]) then - pnlCanvas:RemoveNode(pnlCanvas.Folders[1]) - end - end + hook.Run(FileBrowserPrefix .. "_PostMenuFolders", Browser) +end - local advdupe2 = pnlCanvas:AddFolder("Advanced Duplicator 2") - local advdupe1 = pnlCanvas:AddFolder("Advanced Duplicator 1") +function PANEL:AddLeftsideDivider() + self.LeftsideButtons = self.LeftsideButtons or {} - advdupe1:LoadDataFolder("adv_duplicator/") - advdupe2:LoadDataFolder("advdupe2/") + 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 - if (pnlCanvas.Folders[2]) then - if (#pnlCanvas.Folders[2].Folders == 0 and #pnlCanvas.Folders[2].Files == 0) then - pnlCanvas:RemoveNode(pnlCanvas.Folders[2]) - end + self.LeftsideButtons[#self.LeftsideButtons + 1] = Panel +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 +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:Init() +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) + Button.DoClick = Action + + 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) + 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 self.Expanding = false @@ -1201,19 +1899,25 @@ 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.Refresh:SetMaterial("icon16/arrow_refresh.png") - self.Refresh:SizeToContents() - self.Refresh:SetTooltip("Refresh Files") - self.Refresh.DoClick = function(button) UpdateClientFiles() end - - self.Help = vgui.Create("DImageButton", self) - self.Help:SetMaterial("icon16/help.png") - self.Help:SizeToContents() - self.Help:SetTooltip("Help Section") - self.Help.DoClick = function(btn) + 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) + 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() gui.OpenURL("https://github.com/wiremod/advdupe2/issues") @@ -1226,194 +1930,133 @@ function PANEL:Init() "https://github.com/wiremod/advdupe2/wiki/Server-settings") end) Menu:Open() - end - - self.Submit = vgui.Create("DImageButton", self) - 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 = vgui.Create("DImageButton", self) - 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 = vgui.Create("DTextEntry", self) - 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 + end) + self:AddRightsideDivider() + self.Settings = self:AddRightsideButton("cog", "Settings", function() self:OpenSettings() 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() +function PANEL:OpenSettings() + if self.SettingsPanel then + self.SettingsPanel:Close() + self.SettingsPanel = nil + self.Settings:SetImage("icon16/cog.png") + return 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 + self.Settings:SetImage("icon16/cog_delete.png") - if (IsValid(txtbox.Menu)) then - txtbox.Menu:Remove() - end - txtbox:FocusNext() - txtbox:OnEnter() - txtbox.HistoryPos = 0 - end + local Panel = self.Browser:PushUserPrompt() + self.SettingsPanel = Panel - 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 + Panel:SetBlocking(true) + Panel:SetDock(BOTTOM) - end + 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 - 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 + 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 - - self.Desc = vgui.Create("DTextEntry", self) - 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) + 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 - self.Info = vgui.Create("DLabel", self) - self.Info:SetVisible(false) - + 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) - 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) @@ -1424,24 +2067,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/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 diff --git a/lua/autorun/advdupe2_sh_init.lua b/lua/autorun/advdupe2_sh_init.lua new file mode 100644 index 00000000..bc37f5df --- /dev/null +++ b/lua/autorun/advdupe2_sh_init.lua @@ -0,0 +1,11 @@ +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 \ 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..d30a873c 100644 --- a/lua/autorun/client/advdupe2_cl_init.lua +++ b/lua/autorun/client/advdupe2_cl_init.lua @@ -1,29 +1,25 @@ -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 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" ) 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 fdc98937..aeeefb92 100644 --- a/lua/autorun/server/advdupe2_sv_init.lua +++ b/lua/autorun/server/advdupe2_sv_init.lua @@ -1,25 +1,21 @@ -AdvDupe2 = { - Version = "1.1.0", - Revision = 51 -} +AdvDupe2 = AdvDupe2 or {} -AdvDupe2.DataFolder = "advdupe2" --name of the folder in data where dupes will be saved - -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 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" ) @@ -124,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" ) diff --git a/lua/weapons/gmod_tool/stools/advdupe2.lua b/lua/weapons/gmod_tool/stools/advdupe2.lua index 02c70b98..dd63cdba 100644 --- a/lua/weapons/gmod_tool/stools/advdupe2.lua +++ b/lua/weapons/gmod_tool/stools/advdupe2.lua @@ -1064,10 +1064,25 @@ if(CLIENT) then CreateClientConVar("advdupe2_paste_protectoveride", 1, false, true) CreateClientConVar("advdupe2_debug_openfile", 1, false, true) - local function BuildCPanel(CPanel) + 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() - local FileBrowser = vgui.Create("advdupe2_browser") + 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) CPanel:AddItem(FileBrowser) FileBrowser:SetSize(CPanel:GetWide(), 405) AdvDupe2.FileBrowser = FileBrowser @@ -1078,7 +1093,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") @@ -1086,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") @@ -1094,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") @@ -1140,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) @@ -1148,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:" ) @@ -1188,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") @@ -1851,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() @@ -1869,6 +1916,7 @@ if(CLIENT) then end net.Receive("AdvDupe2_RemoveProgressBar", function() AdvDupe2.RemoveProgressBar() + hook.Run("AdvDupe2_RemoveProgressBar") end) net.Receive("AdvDupe2_ResetOffsets", function() @@ -1920,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())