A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.IO;
7using System.Linq;
8using osu.Framework.Allocation;
9using osu.Framework.Bindables;
10using osu.Framework.Extensions.EnumExtensions;
11using osu.Framework.Graphics.Containers;
12using osuTK;
13
14namespace osu.Framework.Graphics.UserInterface
15{
16 /// <summary>
17 /// A component which allows a user to select a directory.
18 /// </summary>
19 public abstract class DirectorySelector : CompositeDrawable
20 {
21 private FillFlowContainer directoryFlow;
22
23 protected abstract ScrollContainer<Drawable> CreateScrollContainer();
24
25 /// <summary>
26 /// Create the breadcrumb part of the control.
27 /// </summary>
28 protected abstract DirectorySelectorBreadcrumbDisplay CreateBreadcrumb();
29
30 protected abstract DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null);
31
32 /// <summary>
33 /// Create the directory item that resolves the parent directory.
34 /// </summary>
35 protected abstract DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory);
36
37 [Cached]
38 public readonly Bindable<DirectoryInfo> CurrentPath = new Bindable<DirectoryInfo>();
39
40 protected DirectorySelector(string initialPath = null)
41 {
42 CurrentPath.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
43 }
44
45 [BackgroundDependencyLoader]
46 private void load()
47 {
48 InternalChild = new GridContainer
49 {
50 RelativeSizeAxes = Axes.Both,
51 RowDimensions = new[]
52 {
53 new Dimension(GridSizeMode.AutoSize),
54 new Dimension(),
55 },
56 Content = new[]
57 {
58 new Drawable[]
59 {
60 CreateBreadcrumb()
61 },
62 new Drawable[]
63 {
64 CreateScrollContainer().With(d =>
65 {
66 d.RelativeSizeAxes = Axes.Both;
67 d.Child = directoryFlow = new FillFlowContainer
68 {
69 AutoSizeAxes = Axes.Y,
70 RelativeSizeAxes = Axes.X,
71 Direction = FillDirection.Vertical,
72 Spacing = new Vector2(2),
73 };
74 })
75 }
76 }
77 };
78
79 CurrentPath.BindValueChanged(updateDisplay, true);
80 }
81
82 /// <summary>
83 /// Because <see cref="CurrentPath"/> changes may not necessarily lead to directories that exist/are accessible,
84 /// <see cref="updateDisplay"/> may need to change <see cref="CurrentPath"/> again to lead to a directory that is actually accessible.
85 /// This flag intends to prevent recursive <see cref="updateDisplay"/> calls from taking place during the process of finding an accessible directory.
86 /// </summary>
87 private bool directoryChanging;
88
89 private void updateDisplay(ValueChangedEvent<DirectoryInfo> directory)
90 {
91 if (directoryChanging)
92 return;
93
94 try
95 {
96 directoryChanging = true;
97
98 directoryFlow.Clear();
99
100 var newDirectory = directory.NewValue;
101 bool notifyError = false;
102 ICollection<DirectorySelectorItem> items = Array.Empty<DirectorySelectorItem>();
103
104 while (newDirectory != null)
105 {
106 newDirectory.Refresh();
107
108 if (TryGetEntriesForPath(newDirectory, out items))
109 break;
110
111 notifyError = true;
112 newDirectory = newDirectory.Parent;
113 }
114
115 if (notifyError)
116 NotifySelectionError();
117
118 if (newDirectory == null)
119 {
120 var drives = DriveInfo.GetDrives();
121
122 foreach (var drive in drives)
123 directoryFlow.Add(CreateDirectoryItem(drive.RootDirectory));
124
125 return;
126 }
127
128 CurrentPath.Value = newDirectory;
129
130 directoryFlow.Add(CreateParentDirectoryItem(newDirectory.Parent));
131 directoryFlow.AddRange(items);
132 }
133 finally
134 {
135 directoryChanging = false;
136 }
137 }
138
139 /// <summary>
140 /// Attempts to create entries to display for the given <paramref name="path"/>.
141 /// A return value of <see langword="false"/> is used to indicate a non-specific I/O failure, signaling to the selector that it should attempt
142 /// to find another directory to display (since <paramref name="path"/> is inaccessible).
143 /// </summary>
144 /// <param name="path">The directory to create entries for.</param>
145 /// <param name="items">
146 /// The created <see cref="DirectorySelectorItem"/>s, provided that the <paramref name="path"/> could be entered.
147 /// Not valid for reading if the return value of the method is <see langword="false"/>.
148 /// </param>
149 protected virtual bool TryGetEntriesForPath(DirectoryInfo path, out ICollection<DirectorySelectorItem> items)
150 {
151 items = new List<DirectorySelectorItem>();
152
153 try
154 {
155 foreach (var dir in path.GetDirectories().OrderBy(d => d.Name))
156 {
157 if (!dir.Attributes.HasFlagFast(FileAttributes.Hidden))
158 items.Add(CreateDirectoryItem(dir));
159 }
160
161 return true;
162 }
163 catch
164 {
165 return false;
166 }
167 }
168
169 /// <summary>
170 /// Called when an error has occured. Usually happens when trying to access protected directories.
171 /// </summary>
172 protected virtual void NotifySelectionError()
173 {
174 }
175 }
176}