using Annulus; using Azimuth; using Clave; using System; using System.Drawing; namespace Glass.Testbed.TestCases.Controls { /// /// A FileDialog over an in-memory file system, to open and save through. Saving puts a file in the file system, /// and saving over a file asks first. The dialog's own buttons make new folders. /// public class FileDialogTestCase : TestCase { private const int StageHeight = 420; private const int PendingBytes = 100; private static readonly Vector DialogSize = new Vector(520, 400); private static readonly TextMaterialStyle TextStyle = Widgets.MenuLabelStyle; private static readonly TextMaterialStyle ButtonTextStyle = Widgets.MenuLabelStyle with { HorizontalAlignment = Alignment.Horizontal.Center }; private static readonly WindowMaterial WindowLook = new WindowMaterial { Background = UiPacksheetMaterials.Panel, TitleBar = UiPacksheetMaterials.BlueFlatPanel, Caption = Material.Invisible, Content = Material.Invisible, CloseButton = Widgets.CloseButton, }; private static readonly ButtonMaterial ArrowButton = new ButtonMaterial(UiPacksheetMaterials.NubDown with { Scale = 2, }); private static readonly DropdownMaterial DropdownLook = new DropdownMaterial { Background = UiPacksheetMaterials.TextField, ArrowButton = ArrowButton, ArrowGap = Margin, Popup = Widgets.PopupList, }; private static readonly FileFilter[] Filters = { new FileFilter("Text files", new[] { ".txt" }), new FileFilter("Markdown files", new[] { ".md" }), FileFilter.AllFiles, }; private static readonly string[] SuggestedNames = { "notes.txt", "readme.md", "untitled.txt" }; private static readonly FileDialogMaterial DialogLook = new FileDialogMaterial { HeaderDivider = default, AscendingIcon = default, DescendingIcon = default, SortIconInset = default, Window = WindowLook, BackButton = ButtonFacing("Back"), ForwardButton = ButtonFacing("Forward"), UpButton = ButtonFacing("Up"), RefreshButton = UiPacksheetMaterials.Button.Framing(FileIconMaterials.Refresh), NewFolderButton = ButtonFacing("New folder"), UndoButton = ButtonFacing("Undo"), RedoButton = ButtonFacing("Redo"), OptionsButton = ButtonFacing("Options"), HiddenFilesOnButton = UiPacksheetMaterials.CheckboxChecked, HiddenFilesOffButton = UiPacksheetMaterials.CheckboxUnchecked, HiddenFilesLabel = "Hidden", AcceptButton = ButtonFacing("OK"), CancelButton = ButtonFacing("Cancel"), EntryList = Widgets.PopupListFrame, FolderIcon = FileIconMaterials.Folder, FileIcon = FileIconMaterials.File, EntryRow = Widgets.ListItemButton, SelectedEntryRow = Widgets.CurrentListItemButton, FileNameBox = Widgets.TextBox with { ScrollBar = null, HorizontalScrollBar = null }, Dropdown = DropdownLook, OverwriteBox = new MessageBoxMaterial { Background = UiPacksheetMaterials.Panel, TextStyle = TextStyle, Spacing = Margin, } .WithButton(FileDialog.OverwriteAnswer.Replace, ButtonFacing("Replace")) .WithButton(FileDialog.OverwriteAnswer.Cancel, ButtonFacing("Cancel")), OverwriteQuestionFormat = "'{0}' already exists. Replace it?", TypedPatternFormat = "Showing {0}", DeleteBox = new MessageBoxMaterial { Background = UiPacksheetMaterials.Panel, TextStyle = TextStyle, Spacing = Margin, } .WithButton(FileDialog.DeleteAnswer.Delete, ButtonFacing("Delete")) .WithButton(FileDialog.DeleteAnswer.Cancel, ButtonFacing("Cancel")), DeleteQuestionFormat = "'{0}' has things in it. Delete it anyway?", ContextMenu = new PopupMenuMaterial { Background = UiPacksheetMaterials.Panel, Spacing = Margin } .WithItem(FileDialog.ContextMenuChoice.Open, ButtonFacing("Open")) .WithItem(FileDialog.ContextMenuChoice.Rename, ButtonFacing("Rename")) .WithItem(FileDialog.ContextMenuChoice.Delete, ButtonFacing("Delete")) .WithItem(FileDialog.ContextMenuChoice.NewFolder, ButtonFacing("New folder")) .WithItem(FileDialog.ContextMenuChoice.CopyPath, ButtonFacing("Copy path")), HeaderChrome = UiPacksheetMaterials.Button, ColumnTitles = new[] { "Name", "Size", "Modified" }, AscendingMark = " ^", DescendingMark = " v", SaveToClipboardButton = ButtonFacing("Copy"), LoadFromClipboardButton = ButtonFacing("Paste"), ClipboardBox = new MessageBoxMaterial { Background = UiPacksheetMaterials.Panel, TextStyle = TextStyle, Spacing = Margin, } .WithButton(FileDialog.ClipboardAnswer.Yes, ButtonFacing("Yes")) .WithButton(FileDialog.ClipboardAnswer.Cancel, ButtonFacing("Cancel")), SaveToClipboardQuestion = "Copy this save to the clipboard?", LoadFromClipboardQuestion = "Try to load from the clipboard?", TextStyle = TextStyle, Spacing = Margin, MinimumEntryRows = 6, }; private InMemoryFileSystem files = SampleFiles(); private FileDialog dialog; private Label resultLabel; private int savedCount; // Stands in for the system clipboard. Null is an empty clipboard. private string clipboard; private static ButtonMaterial ButtonFacing(string text) { return UiPacksheetMaterials.Button.Framing(new TextMaterial { Text = text, Style = ButtonTextStyle }); } private static InMemoryFileSystem SampleFiles() { var returnMe = new InMemoryFileSystem(); returnMe.AddFile("/docs/readme.md", size: 1200, modified: new DateTime(2026, 3, 14, 9, 30, 0)); returnMe.AddFile("/docs/notes.txt", size: 340, modified: new DateTime(2026, 5, 2, 16, 5, 0)); returnMe.AddFile( "/docs/drafts/chapter one.txt", size: 8100, modified: new DateTime(2025, 12, 1, 8, 0, 0)); returnMe.AddFile( "/docs/drafts/chapter two.txt", size: 7600, modified: new DateTime(2026, 1, 20, 21, 45, 0)); returnMe.AddDirectory("/empty folder"); returnMe.AddFile("/music/song.txt", size: 90, modified: new DateTime(2024, 7, 4, 12, 0, 0)); returnMe.AddFile("/todo.txt", size: 56, modified: new DateTime(2026, 10, 30, 7, 15, 0)); returnMe.AddFile("/notes.txt", size: 12, modified: new DateTime(2026, 10, 31, 22, 10, 0)); return returnMe; } private static string CellText(FileEntry entry, FileColumn column) { switch (column) { case FileColumn.Name: return entry.IsDirectory ? entry.Name + "/" : entry.Name; case FileColumn.Size: return entry.Size == null ? string.Empty : $"{entry.Size} bytes"; default: return entry.Modified?.ToString("yyyy-MM-dd HH:mm") ?? string.Empty; } } private static Button NewButton(string text) { return new Label(Widgets.ButtonLabelStyle, text, Constraints.Unbounded) .WrapIn(UiPacksheetMaterials.Button, ButtonBehavior.Default); } private void ShowResult(string text) { resultLabel.Text = text; } private void RemoveDialog() { if (dialog != null) { Root.CanvasChildren.Remove(dialog); dialog = null; } } // Built once, since the dialog keeps the user's column widths, sort and last directory between shows. private FileDialog BuildDialog(FileDialogModel firstModel, Rect stage) { var returnMe = new FileDialog(firstModel, CellText, FileDialogBehavior.Default); returnMe.FileDialogMaterial = DialogLook; returnMe.ResizeCursors = ResizeCursorMaterials.ByZone; returnMe.MoveCursor = CursorExtraMaterials.Move; returnMe.ColumnHeader.GripCursor = ResizeCursorMaterials.ByZone[ResizeZone.E]; returnMe.Caption.Material = Material.Invisible; returnMe.TitleStyle = Widgets.WindowTitleStyle; returnMe.Bounds = Rect.FromPositionAndSize( stage.TopLeft + new Vector(20, 10), Vector.Max(DialogSize, returnMe.EffectiveMinSize)); new Label(Widgets.CloseButtonLabelStyle, "x").FillParent(returnMe.CloseButton); returnMe.ShowsClipboardButton = true; returnMe.IsClipboardEmpty = () => clipboard == null; returnMe.OnClipboardChosen += () => ClipboardChosen(returnMe.Model.Mode, returnMe.Model); return returnMe; } private void ShowDialog(string title, FileDialogMode mode, Rect stage) { // The testbed's own file system always lists its root. FileDialogModel model = FileDialogModel.Create(files, mode, Filters).Value; model.OnAccepted += path => Accepted(mode, path); model.OnCancelled += () => ShowResult($"{title}: cancelled"); if (dialog == null) { dialog = BuildDialog(model, stage); Add(dialog); } else { dialog.Reopen(model); } dialog.Title = title; if (mode == FileDialogMode.Save) { dialog.SuggestedNames = SuggestedNames; } } private void ClipboardChosen(FileDialogMode mode, FileDialogModel model) { if (mode == FileDialogMode.Save) { clipboard = $"A save named '{model.FileName}'"; ShowResult($"Copied: {clipboard}"); } else { ShowResult($"Loaded from the clipboard: {clipboard}"); } } private void Accepted(FileDialogMode mode, string path) { if (mode == FileDialogMode.Save) { ++savedCount; files.AddFile(path, size: PendingBytes + savedCount); ShowResult($"Saved {path}"); } else { ShowResult($"Opened {path}"); } } protected override void Populate() { AddCaption("Open picks an existing file. Save picks a name, with suggestions, and asks before replacing."); AddCaption("Double-click a folder to go in, use Up to come out, and New folder to make one."); Button open = NewButton("Open a file..."); Button save = NewButton("Save a file..."); Button reset = NewButton("Reset files"); Rect buttonRow = ContentAggregator.NewRow(open.Bounds.Height); foreach (Button button in new[] { open, save, reset }) { Rect slot = buttonRow.NewCol(button.Bounds.Width, margin: Gutter); button.Bounds = button.Bounds.WithPosition(slot.TopLeft); Add(button); } resultLabel = new Label(Widgets.CaptionStyle, "Nothing chosen yet"); resultLabel.Bounds = ContentAggregator.NewRow(resultLabel.Bounds.Height); Add(resultLabel); Rect stageArea = ContentAggregator.NewRow(StageHeight); open.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___) { ShowDialog("Open file", FileDialogMode.Open, stageArea); }; save.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___) { ShowDialog("Save file", FileDialogMode.Save, stageArea); }; reset.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___) { RemoveDialog(); files = SampleFiles(); savedCount = 0; clipboard = null; ShowResult("Files reset"); }; } protected override void Teardown() { RemoveDialog(); } } }