// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Containers; using osuTK; namespace osu.Framework.Graphics.UserInterface { /// /// A component which allows a user to select a directory. /// public abstract class DirectorySelector : CompositeDrawable { private FillFlowContainer directoryFlow; protected abstract ScrollContainer CreateScrollContainer(); /// /// Create the breadcrumb part of the control. /// protected abstract DirectorySelectorBreadcrumbDisplay CreateBreadcrumb(); protected abstract DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null); /// /// Create the directory item that resolves the parent directory. /// protected abstract DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory); [Cached] public readonly Bindable CurrentPath = new Bindable(); protected DirectorySelector(string initialPath = null) { CurrentPath.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } [BackgroundDependencyLoader] private void load() { InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), }, Content = new[] { new Drawable[] { CreateBreadcrumb() }, new Drawable[] { CreateScrollContainer().With(d => { d.RelativeSizeAxes = Axes.Both; d.Child = directoryFlow = new FillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Direction = FillDirection.Vertical, Spacing = new Vector2(2), }; }) } } }; CurrentPath.BindValueChanged(updateDisplay, true); } /// /// Because changes may not necessarily lead to directories that exist/are accessible, /// may need to change again to lead to a directory that is actually accessible. /// This flag intends to prevent recursive calls from taking place during the process of finding an accessible directory. /// private bool directoryChanging; private void updateDisplay(ValueChangedEvent directory) { if (directoryChanging) return; try { directoryChanging = true; directoryFlow.Clear(); var newDirectory = directory.NewValue; bool notifyError = false; ICollection items = Array.Empty(); while (newDirectory != null) { newDirectory.Refresh(); if (TryGetEntriesForPath(newDirectory, out items)) break; notifyError = true; newDirectory = newDirectory.Parent; } if (notifyError) NotifySelectionError(); if (newDirectory == null) { var drives = DriveInfo.GetDrives(); foreach (var drive in drives) directoryFlow.Add(CreateDirectoryItem(drive.RootDirectory)); return; } CurrentPath.Value = newDirectory; directoryFlow.Add(CreateParentDirectoryItem(newDirectory.Parent)); directoryFlow.AddRange(items); } finally { directoryChanging = false; } } /// /// Attempts to create entries to display for the given . /// A return value of is used to indicate a non-specific I/O failure, signaling to the selector that it should attempt /// to find another directory to display (since is inaccessible). /// /// The directory to create entries for. /// /// The created s, provided that the could be entered. /// Not valid for reading if the return value of the method is . /// protected virtual bool TryGetEntriesForPath(DirectoryInfo path, out ICollection items) { items = new List(); try { foreach (var dir in path.GetDirectories().OrderBy(d => d.Name)) { if (!dir.Attributes.HasFlagFast(FileAttributes.Hidden)) items.Add(CreateDirectoryItem(dir)); } return true; } catch { return false; } } /// /// Called when an error has occured. Usually happens when trying to access protected directories. /// protected virtual void NotifySelectionError() { } } }