Dialog.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. --[[
  2. MIT License
  3. Copyright (c) 2019 Mitchell Davis <coding.jackalope@gmail.com>
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. --]]
  20. local Button = require(SLAB_PATH .. '.Internal.UI.Button')
  21. local ComboBox = require(SLAB_PATH .. '.Internal.UI.ComboBox')
  22. local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor')
  23. local FileSystem = require(SLAB_PATH .. '.Internal.Core.FileSystem')
  24. local Image = require(SLAB_PATH .. '.Internal.UI.Image')
  25. local Input = require(SLAB_PATH .. '.Internal.UI.Input')
  26. local Keyboard = require(SLAB_PATH .. '.Internal.Input.Keyboard')
  27. local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager')
  28. local ListBox = require(SLAB_PATH .. '.Internal.UI.ListBox')
  29. local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse')
  30. local Region = require(SLAB_PATH .. '.Internal.UI.Region')
  31. local Style = require(SLAB_PATH .. '.Style')
  32. local Text = require(SLAB_PATH .. '.Internal.UI.Text')
  33. local Tree = require(SLAB_PATH .. '.Internal.UI.Tree')
  34. local Utility = require(SLAB_PATH .. '.Internal.Core.Utility')
  35. local Window = require(SLAB_PATH .. '.Internal.UI.Window')
  36. local Dialog = {}
  37. local Instances = {}
  38. local ActiveInstance = nil
  39. local Stack = {}
  40. local InstanceStack = {}
  41. local FileDialog_AskOverwrite = false
  42. local FilterW = 0.0
  43. local function ValidateSaveFile(Files, Extension)
  44. if Extension == nil or Extension == "" then
  45. return
  46. end
  47. if Files ~= nil and #Files == 1 then
  48. local Index = string.find(Files[1], ".", 1, true)
  49. if Index ~= nil then
  50. Files[1] = string.sub(Files[1], 1, Index - 1)
  51. end
  52. Files[1] = Files[1] .. Extension
  53. end
  54. end
  55. local function UpdateInputText(Instance)
  56. if Instance ~= nil then
  57. if #Instance.Return > 0 then
  58. Instance.Text = #Instance.Return > 1 and "<Multiple>" or Instance.Return[1]
  59. else
  60. Instance.Text = ""
  61. end
  62. end
  63. end
  64. local function PruneResults(Items, DirectoryOnly)
  65. local Result = {}
  66. for I, V in ipairs(Items) do
  67. if FileSystem.IsDirectory(V) then
  68. if DirectoryOnly then
  69. table.insert(Result, V)
  70. end
  71. else
  72. if not DirectoryOnly then
  73. table.insert(Result, V)
  74. end
  75. end
  76. end
  77. return Result
  78. end
  79. local function OpenDirectory(Dir)
  80. if ActiveInstance ~= nil and ActiveInstance.Directory ~= nil then
  81. ActiveInstance.Parsed = false
  82. if Dir == ".." then
  83. ActiveInstance.Directory = FileSystem.Parent(ActiveInstance.Directory)
  84. else
  85. if string.sub(Dir, #Dir, #Dir) == FileSystem.Separator() then
  86. Dir = string.sub(Dir, 1, #Dir - 1)
  87. end
  88. ActiveInstance.Directory = Dir
  89. end
  90. end
  91. end
  92. local function FileDialogItem(Id, Label, IsDirectory, Index)
  93. ListBox.BeginItem(Id, {Selected = Utility.HasValue(ActiveInstance.Selected, Index)})
  94. if IsDirectory then
  95. Image.Begin('FileDialog_Folder', {Path = SLAB_PATH .. "/Internal/Resources/Textures/Folder.png"})
  96. Cursor.SameLine({CenterY = true})
  97. end
  98. Text.Begin(Label)
  99. if ListBox.IsItemClicked(1) then
  100. local Set = true
  101. if ActiveInstance.AllowMultiSelect then
  102. if Keyboard.IsDown('lctrl') or Keyboard.IsDown('rctrl') then
  103. Set = false
  104. if Utility.HasValue(ActiveInstance.Selected, Index) then
  105. Utility.Remove(ActiveInstance.Selected, Index)
  106. Utility.Remove(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. Label)
  107. else
  108. table.insert(ActiveInstance.Selected, Index)
  109. table.insert(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. Label)
  110. end
  111. elseif Keyboard.IsDown('lshift') or Keyboard.IsDown('rshift') then
  112. if #ActiveInstance.Selected > 0 then
  113. Set = false
  114. local Anchor = ActiveInstance.Selected[#ActiveInstance.Selected]
  115. local Min = math.min(Anchor, Index)
  116. local Max = math.max(Anchor, Index)
  117. ActiveInstance.Selected = {}
  118. ActiveInstance.Return = {}
  119. for I = Min, Max, 1 do
  120. table.insert(ActiveInstance.Selected, I)
  121. if I > #ActiveInstance.Directories then
  122. I = I - #ActiveInstance.Directories
  123. table.insert(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. ActiveInstance.Files[I])
  124. else
  125. table.insert(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. ActiveInstance.Directories[I])
  126. end
  127. end
  128. end
  129. end
  130. end
  131. if Set then
  132. ActiveInstance.Selected = {Index}
  133. ActiveInstance.Return = {ActiveInstance.Directory .. "/" .. Label}
  134. end
  135. UpdateInputText(ActiveInstance)
  136. end
  137. if ListBox.IsItemClicked(1, true) and IsDirectory then
  138. OpenDirectory(ActiveInstance.Directory .. "/" .. Label)
  139. end
  140. ListBox.EndItem()
  141. end
  142. local function AddDirectoryItem(Path)
  143. local Separator = FileSystem.Separator()
  144. local Item = {}
  145. Item.Path = Path
  146. Item.Name = FileSystem.GetBaseName(Path)
  147. Item.Name = Item.Name == "" and Separator or Item.Name
  148. -- Remove the starting slash for Unix style directories.
  149. if string.sub(Item.Name, 1, 1) == Separator and Item.Name ~= Separator then
  150. Item.Name = string.sub(Item.Name, 2)
  151. end
  152. Item.Children = nil
  153. return Item
  154. end
  155. local function FileDialogExplorer(Instance, Root)
  156. if Instance == nil then
  157. return
  158. end
  159. if Root ~= nil then
  160. local ShouldOpen = string.find(Instance.Directory, Root.Path, 1, true) ~= nil
  161. local Options = {
  162. Label = Root.Name,
  163. OpenWithHighlight = false,
  164. IsSelected = ActiveInstance.Directory == Root.Path,
  165. IsOpen = ShouldOpen
  166. }
  167. local IsOpen = Tree.Begin(Root.Path, Options)
  168. if Mouse.IsClicked(1) and Window.IsItemHot() then
  169. OpenDirectory(Root.Path)
  170. end
  171. if IsOpen then
  172. if Root.Children == nil then
  173. Root.Children = {}
  174. local Separator = FileSystem.Separator()
  175. local Directories = FileSystem.GetDirectoryItems(Root.Path .. Separator, {Files = false})
  176. for I, V in ipairs(Directories) do
  177. local Path = Root.Path
  178. if string.sub(Path, #Path) ~= Separator and Path ~= Separator then
  179. Path = Path .. Separator
  180. end
  181. if string.sub(V, 1, 1) == Separator then
  182. V = string.sub(V, 2)
  183. end
  184. local Item = AddDirectoryItem(Path .. FileSystem.GetBaseName(V))
  185. table.insert(Root.Children, Item)
  186. end
  187. end
  188. for I, V in ipairs(Root.Children) do
  189. FileDialogExplorer(Instance, V)
  190. end
  191. Tree.End()
  192. end
  193. end
  194. end
  195. local function GetFilter(Instance, Index)
  196. local Filter = "*.*"
  197. local Desc = "All Files"
  198. if Instance ~= nil and #Instance.Filters > 0 then
  199. if Index == nil then
  200. Index = Instance.SelectedFilter
  201. end
  202. local Item = Instance.Filters[Index]
  203. if Item ~= nil then
  204. if type(Item) == "table" then
  205. if #Item == 1 then
  206. Filter = Item[1]
  207. Desc = ""
  208. elseif #Item == 2 then
  209. Filter = Item[1]
  210. Desc = Item[2]
  211. end
  212. else
  213. Filter = tostring(Item)
  214. Desc = ""
  215. end
  216. end
  217. end
  218. return Filter, Desc
  219. end
  220. local function GetExtension(Instance)
  221. local Filter, Desc = GetFilter(Instance)
  222. local Result = ""
  223. if Filter ~= "*.*" then
  224. local Index = string.find(Filter, ".", 1, true)
  225. if Index ~= nil then
  226. Result = string.sub(Filter, Index)
  227. end
  228. end
  229. return Result
  230. end
  231. local function IsInstanceOpen(Id)
  232. local Instance = Instances[Id]
  233. if Instance ~= nil then
  234. return Instance.IsOpen
  235. end
  236. return false
  237. end
  238. local function GetInstance(Id)
  239. if Instances[Id] == nil then
  240. local Instance = {}
  241. Instance.Id = Id
  242. Instance.IsOpen = false
  243. Instance.W = 0.0
  244. Instance.H = 0.0
  245. Instances[Id] = Instance
  246. end
  247. return Instances[Id]
  248. end
  249. function Dialog.Begin(Id, Options)
  250. local Instance = GetInstance(Id)
  251. if not Instance.IsOpen then
  252. return false
  253. end
  254. Options = Options == nil and {} or Options
  255. Options.X = math.floor(love.graphics.getWidth() * 0.5 - Instance.W * 0.5)
  256. Options.Y = math.floor(love.graphics.getHeight() * 0.5 - Instance.H * 0.5)
  257. Options.Layer = 'Dialog'
  258. Options.AllowFocus = false
  259. Options.AllowMove = false
  260. Options.AutoSizeWindow = Options.AutoSizeWindow == nil and true or Options.AutoSizeWindow
  261. Window.Begin(Instance.Id, Options)
  262. ActiveInstance = Instance
  263. table.insert(InstanceStack, 1, ActiveInstance)
  264. return true
  265. end
  266. function Dialog.End()
  267. ActiveInstance.W, ActiveInstance.H = Window.GetSize()
  268. Window.End()
  269. ActiveInstance = nil
  270. table.remove(InstanceStack, 1)
  271. if #InstanceStack > 0 then
  272. ActiveInstance = InstanceStack[1]
  273. end
  274. end
  275. function Dialog.Open(Id)
  276. local Instance = GetInstance(Id)
  277. if not Instance.IsOpen then
  278. Instance.IsOpen = true
  279. table.insert(Stack, 1, Instance)
  280. Window.SetStackLock(Instance.Id)
  281. Window.PushToTop(Instance.Id)
  282. end
  283. end
  284. function Dialog.Close()
  285. if ActiveInstance ~= nil and ActiveInstance.IsOpen then
  286. ActiveInstance.IsOpen = false
  287. table.remove(Stack, 1)
  288. Window.SetStackLock(nil)
  289. if #Stack > 0 then
  290. Instance = Stack[1]
  291. Window.SetStackLock(Instance.Id)
  292. Window.PushToTop(Instance.Id)
  293. end
  294. end
  295. end
  296. function Dialog.IsOpen()
  297. return #Stack > 0
  298. end
  299. function Dialog.FileDialog(Options)
  300. Options = Options == nil and {} or Options
  301. Options.AllowMultiSelect = Options.AllowMultiSelect == nil and true or Options.AllowMultiSelect
  302. Options.Directory = Options.Directory == nil and nil or Options.Directory
  303. Options.Type = Options.Type == nil and 'openfile' or Options.Type
  304. Options.Filters = Options.Filters == nil and {{"*.*", "All Files"}} or Options.Filters
  305. local Title = "Open File"
  306. if Options.Type == 'savefile' then
  307. Options.AllowMultiSelect = false
  308. Title = "Save File"
  309. elseif Options.Type == 'opendirectory' then
  310. Title = "Open Directory"
  311. end
  312. local Result = {Button = "", Files = {}}
  313. local WasOpen = IsInstanceOpen('FileDialog')
  314. Dialog.Open("FileDialog")
  315. local W = love.graphics.getWidth() * 0.65
  316. local H = love.graphics.getHeight() * 0.65
  317. if Dialog.Begin('FileDialog', {
  318. Title = Title,
  319. AutoSizeWindow = false,
  320. W = W,
  321. H = H,
  322. AutoSizeContent = false,
  323. AllowResize = false
  324. }) then
  325. ActiveInstance.AllowMultiSelect = Options.AllowMultiSelect
  326. if not WasOpen then
  327. ActiveInstance.Text = ""
  328. if ActiveInstance.Directory == nil then
  329. ActiveInstance.Directory = love.filesystem.getSourceBaseDirectory()
  330. end
  331. if Options.Directory ~= nil and FileSystem.IsDirectory(Options.Directory) then
  332. ActiveInstance.Directory = Options.Directory
  333. end
  334. ActiveInstance.Filters = Options.Filters
  335. ActiveInstance.SelectedFilter = 1
  336. end
  337. local Clear = false
  338. if not ActiveInstance.Parsed then
  339. local Filter = GetFilter(ActiveInstance)
  340. ActiveInstance.Root = AddDirectoryItem(FileSystem.GetRootDirectory(ActiveInstance.Directory))
  341. ActiveInstance.Selected = {}
  342. ActiveInstance.Directories = FileSystem.GetDirectoryItems(ActiveInstance.Directory .. "/", {Files = false})
  343. ActiveInstance.Files = FileSystem.GetDirectoryItems(ActiveInstance.Directory .. "/", {Directories = false, Filter = Filter})
  344. ActiveInstance.Return = {ActiveInstance.Directory .. "/"}
  345. ActiveInstance.Text = ""
  346. ActiveInstance.Parsed = true
  347. UpdateInputText(ActiveInstance)
  348. for I, V in ipairs(ActiveInstance.Directories) do
  349. ActiveInstance.Directories[I] = FileSystem.GetBaseName(V)
  350. end
  351. for I, V in ipairs(ActiveInstance.Files) do
  352. ActiveInstance.Files[I] = FileSystem.GetBaseName(V)
  353. end
  354. Clear = true
  355. end
  356. local WinW, WinH = Window.GetSize()
  357. local ButtonW, ButtonH = Button.GetSize("OK")
  358. local ExplorerW = 150.0
  359. local ListH = WinH - Text.GetHeight() - ButtonH * 3.0 - Cursor.PadY() * 2.0
  360. local PrevAnchorX = Cursor.GetAnchorX()
  361. Text.Begin(ActiveInstance.Directory)
  362. local CursorX, CursorY = Cursor.GetPosition()
  363. local MouseX, MouseY = Window.GetMousePosition()
  364. Region.Begin('FileDialog_DirectoryExplorer', {
  365. X = CursorX,
  366. Y = CursorY,
  367. W = ExplorerW,
  368. H = ListH,
  369. AutoSizeContent = true,
  370. NoBackground = true,
  371. Intersect = true,
  372. MouseX = MouseX,
  373. MouseY = MouseY,
  374. IsObstructed = Window.IsObstructedAtMouse(),
  375. Rounding = Style.WindowRounding
  376. })
  377. Cursor.AdvanceX(0.0)
  378. Cursor.SetAnchorX(Cursor.GetX())
  379. FileDialogExplorer(ActiveInstance, ActiveInstance.Root)
  380. Region.End()
  381. Region.ApplyScissor()
  382. Cursor.AdvanceX(ExplorerW + 4.0)
  383. Cursor.SetY(CursorY)
  384. LayoutManager.Begin('FileDialog_ListBox_Expand', {AnchorX = true, ExpandW = true})
  385. ListBox.Begin('FileDialog_ListBox', {H = ListH, Clear = Clear})
  386. local Index = 1
  387. for I, V in ipairs(ActiveInstance.Directories) do
  388. FileDialogItem('Item_' .. Index, V, true, Index)
  389. Index = Index + 1
  390. end
  391. for I, V in ipairs(ActiveInstance.Files) do
  392. FileDialogItem('Item_' .. Index, V, false, Index)
  393. Index = Index + 1
  394. end
  395. ListBox.End()
  396. LayoutManager.End()
  397. local ListBoxX, ListBoxY, ListBoxW, ListBoxH = Cursor.GetItemBounds()
  398. local InputW = ListBoxX + ListBoxW - PrevAnchorX - FilterW - Cursor.PadX()
  399. Cursor.SetAnchorX(PrevAnchorX)
  400. Cursor.SetX(PrevAnchorX)
  401. local ReadOnly = Options.Type ~= 'savefile'
  402. if Input.Begin('FileDialog_Input', {W = InputW, ReadOnly = ReadOnly, Text = ActiveInstance.Text, Align = 'left'}) then
  403. ActiveInstance.Text = Input.GetText()
  404. ActiveInstance.Return[1] = ActiveInstance.Text
  405. end
  406. Cursor.SameLine()
  407. local Filter, Desc = GetFilter(ActiveInstance)
  408. if ComboBox.Begin('FileDialog_Filter', {Selected = Filter .. " " .. Desc}) then
  409. for I, V in ipairs(ActiveInstance.Filters) do
  410. Filter, Desc = GetFilter(ActiveInstance, I)
  411. if Text.Begin(Filter .. " " .. Desc, {IsSelectable = true}) then
  412. ActiveInstance.SelectedFilter = I
  413. ActiveInstance.Parsed = false
  414. end
  415. end
  416. ComboBox.End()
  417. end
  418. local FilterCBX, FilterCBY, FilterCBW, FilterCBH = Cursor.GetItemBounds()
  419. FilterW = FilterCBW
  420. LayoutManager.Begin('FileDialog_Buttons_Layout', {AlignX = 'right', AlignY = 'bottom'})
  421. if Button.Begin("OK") then
  422. local OpeningDirectory = false
  423. if #ActiveInstance.Return == 1 and Options.Type ~= 'opendirectory' then
  424. local Path = ActiveInstance.Return[1]
  425. if FileSystem.IsDirectory(Path) then
  426. OpeningDirectory = true
  427. OpenDirectory(Path)
  428. elseif Options.Type == 'savefile' then
  429. if FileSystem.Exists(Path) then
  430. FileDialog_AskOverwrite = true
  431. OpeningDirectory = true
  432. end
  433. end
  434. end
  435. if not OpeningDirectory then
  436. Result.Button = "OK"
  437. Result.Files = PruneResults(ActiveInstance.Return, Options.Type == 'opendirectory')
  438. if Options.Type == 'savefile' then
  439. ValidateSaveFile(Result.Files, GetExtension(ActiveInstance))
  440. end
  441. end
  442. end
  443. Cursor.SameLine()
  444. LayoutManager.SameLine()
  445. if Button.Begin("Cancel") then
  446. Result.Button = "Cancel"
  447. end
  448. LayoutManager.End()
  449. if FileDialog_AskOverwrite then
  450. local FileName = #ActiveInstance.Return > 0 and ActiveInstance.Return[1] or ""
  451. local AskOverwrite = Dialog.MessageBox("Overwriting", "Are you sure you would like to overwrite file " .. FileName, {Buttons = {"Cancel", "No", "Yes"}})
  452. if AskOverwrite ~= "" then
  453. if AskOverwrite == "No" then
  454. Result.Button = "Cancel"
  455. Result.Files = {}
  456. elseif AskOverwrite == "Yes" then
  457. Result.Button = "OK"
  458. Result.Files = PruneResults(ActiveInstance.Return, Options.Type == 'opendirectory')
  459. end
  460. FileDialog_AskOverwrite = false
  461. end
  462. end
  463. if Result.Button ~= "" then
  464. ActiveInstance.Parsed = false
  465. Dialog.Close()
  466. end
  467. Dialog.End()
  468. end
  469. return Result
  470. end
  471. return Dialog