Skip to content

Commit c1a5788

Browse files
tznindtig
andauthored
Fixes #2150. Revamping FileDialog (#2259)
* Investigating new file dialog * Column sorting * Add navigationStack * Append autocomplete half working * Change autocomplete append to use draw effect instead of selection effect * WIP on FileDialog2 * Refactor sort order and add more 'ls' colors * Refactor history to its own class * FileDialog2 navigation fixes/improvements * Added centered Title * Add tree view and split container * Add FileDialogState * Add AllowsMultipleSelection * Add result fields and Scenario * Added FileDialo2 test file * Fix FileDialog2 Redraw padding to respect max/min view bounds * Fix unit tests and warnings * Started on better keyboard navigation in FileDialog2 * Update to match new SplitContainer API * Quality of Life improvements * Fix recommending parent folder * Remove border from SplitContainer in FileDialog2 and fixed tests * Bugfixes and improvements to FileDialog2 * usability improvements to FileDialog2 * Add multi select and OpenMode * Enforce OpenMode when making a multi selection * Prevent typing illegal characters * Added AllowedTypes to FileDialog2 * Added combo box filter AllowedType * Improve code readability by reordering members * Do not update FileDialog2 text box when selecting ".." in TableView * Fix history navigation in FileDialog2 * Restore selection after navigating backwards in history * Add FileDialog2 tests * Make FileDialog2 Title user configurable * Fix DirectTyping_Allowed unit test when running on linux * Change Home/End to go to first/last cell in table in FileDialog2 * Remove overloaded Title property * Switch to `ustring.IsNullOrEmpty` * Update to latest TileView API * Add TableView navigation by letter using CollectionNavigator * Fix recreating search navigator too often * Add tests for proper disposing * Make Ctrl+F10 toggle split line focusability * Fix layout bug in first tile when orientation is horizontal * Switch to GenerateImage * Fix not calling base constructor * Revert "Merges latest LineCanvas into TileView" * Fix keyboard tab navigation problems * Workaround for changing CanFocus throwing Exceptions sometimes * Update to latest splitcontainer API * Adjust suggestions to be gray and properly update on keystrokes * Move ok and cancel to bottom * Add MustExist and fix multi selection of 1 result * bugfixes and quality of life * Navigating to .. clears path up to current dir * Better arrow key navigation * Make title pretty and informative * Fix test * Fix test * Trim default Titles to be more compact and readable * Fix bad merge changes * Remove EscSeqReq files that are not in v2... came from develop?! * Fix nullable and enable toggle select on table view * Fix multi select return value * Add icon and monochrome support * Add search elements * Add search for current directory * WIP: Async search * Thread safety and disposal * Improve UX * Fix for rapid search results * Fix warnings and whitespace * Don't add more than 10000 search results * Add support for adjusting search matching * Added ISearchMatcher example to FileDialog2Example * Remove double spaces after periods * Make MaxSearchResults a config setting * Localization for FileDialog2 * Fix build error * Support for custom open button Text * Improve file dialog scenario UX * Change feedback to a drawing effect in center of screen * Explore MenuBar instead of ComboBox for AllowedTypes * Fix prompt and move file open into try/catch for errors reading files * Open menu when tabbed to * FileDialog2 improvements - Expose table/tree style properties - Rename Monochrome to UseColors and default to false - IconGetter no longer forces space - On Windows in Scenario just use a backslash for folder icon (i.e. not unicode) - * Add style settings in scenario and make autocomplete case insensitive on Windows * Move ok button text to Style * xmldoc * Remove old FileDialog and re-wire OpenDialog and SaveDialog to use FileDialog2 base * localization * Move open/save dialog to their own files * Rename FileDialog2 to FileDialog * Fix repetition in string * Add IAllowedType * Get rid of AllowedTypesIsStrict User now adds the `IAllowedType` implementation `AllowedTypeAny` * Add max length to AllowedType ToString * Pressing Enter in find restarts search instead of confirm selection * Add TreeRootGetter for customizing the quick access items in FileDialog * Add FilesSelected event Allows user to do things like confirm dialogs on selecting existing file(s) * Update to new sender, event args signature * Fix naming on MouseEventArgs * Fix mouse events naming * Revert "Fix naming on MouseEventArgs" This reverts commit 2f557f5. * Add deletion support * Move delete keybinding to tableview * Scaffold for rename and new operations * Prevent delete dialog popping up again on cancel * Add rename and new folder implementations * Add rename,delete,new to context menu * On rename or new, reselect the file in its new location in tree * Support searching on multiple terms * Localization support for new/rename/delete * Refactor internal classes and add class diagram * Move some visual properties to FileDialogStyle * Ensure MultiSelected is never null and always contains Path if relevant * Fix spacing/indentation * WIP: Add new namespace Terminal.Gui.FileServices * Improve appearance of back/forward/up * Move SpinnerLabel to Views * Add SpinnerView * Code formatting * Add AutoSpin test * Avoid ever removing spinner timeout twice * Make SpinnerView show/hide instead of stopping * WIP: Refactor to use 3 sub PRs - SpinnerView - Suggest Append Autocomplete - Caption TextField * Add FilepathSuggestionGenerator * WIP: FileDialog autocomplete append mostly working again * Improve file autocomplete * Move IconGetter to Style and provide default implementation - Depends on `UseUnicodeCharacters` - Also updated up/back/collapse/expand tree to use spicier icons * Fix failing unit test * Improved colors and layout * Adjust use of unicode * Fix UseUnicodeCharacters * Update table style to include scroll indicators and lines * Fix cycle suggestion with down cursor * Adjust sort indicators * Add default sort order on isDir then name * Always use left/right unicode arrows * Fix autocomplete suggesting in empty textbox * Press escape to cancel ongoing search (when search box is focused) * When entering a TreeView if there is no selection then select first object * Move CursorIsAtEnd to TextField * Improve layout * Change UseColors to be a cell color fill * Fxied tests for new apis * Remove manual title drawing code * Fix MoveEnd name conflicting with base class * Fix merge conflicts * Switched to IFileSystem for unit testing * Add unit test * Revert "Fix MoveEnd name conflicting with base class" This reverts commit a5f9c07. * Fix MoveEnd name collision with 'new' keyword * Fixed tree not toggling * DateTime fixes and mocking * Fix TestDirectoryContents_Windows * Expose UseColors and UseUnicodeCharacters as config settings * Fix linter settings having removed curly brackets * Fix namespace on test * Move tests to file services folder * Remove the FileServices namespace * Updated class diagram * Clear title from tests for futureproofing * Localization support for FileDialog title * Remove trailing whitespace in "open existing" * Fix listing suggestions immediately after folder path entered --------- Co-authored-by: Tig <tig@users.noreply.github.com>
1 parent 8ac9273 commit c1a5788

36 files changed

+3803
-886
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System;
2+
using System.CodeDom;
3+
using System.Data;
4+
using System.IO;
5+
using System.Linq;
6+
using Terminal.Gui.Resources;
7+
8+
namespace Terminal.Gui {
9+
10+
/// <summary>
11+
/// Interface for <see cref="FileDialog"/> restrictions on which file type(s) the
12+
/// user is allowed to select/enter.
13+
/// </summary>
14+
public interface IAllowedType
15+
{
16+
/// <summary>
17+
/// Returns true if the file at <paramref name="path"/> is compatible with this
18+
/// allow option. Note that the file may not exist (e.g. in the case of saving).
19+
/// </summary>
20+
/// <param name="path"></param>
21+
/// <returns></returns>
22+
bool IsAllowed (string path);
23+
}
24+
25+
26+
/// <summary>
27+
/// <see cref="IAllowedType"/> that allows selection of any types (*.*).
28+
/// </summary>
29+
public class AllowedTypeAny : IAllowedType {
30+
31+
/// <inheritdoc/>
32+
public bool IsAllowed (string path)
33+
{
34+
return true;
35+
}
36+
37+
/// <inheritdoc/>
38+
public override string ToString ()
39+
{
40+
return Strings.fdAnyFiles + "(*.*)";
41+
}
42+
}
43+
44+
/// <summary>
45+
/// Describes a requirement on what <see cref="FileInfo"/> can be selected.
46+
/// This can be combined with other <see cref="IAllowedType"/> in a <see cref="FileDialog"/>
47+
/// to for example show only .csv files but let user change to open any if they want.
48+
/// </summary>
49+
public class AllowedType : IAllowedType {
50+
51+
/// <summary>
52+
/// Initializes a new instance of the <see cref="AllowedType"/> class.
53+
/// </summary>
54+
/// <param name="description">The human readable text to display.</param>
55+
/// <param name="extensions">Extension(s) to match e.g. .csv.</param>
56+
public AllowedType (string description, params string [] extensions)
57+
{
58+
if (extensions.Length == 0) {
59+
throw new ArgumentException ("You must supply at least one extension");
60+
}
61+
62+
this.Description = description;
63+
this.Extensions = extensions;
64+
}
65+
66+
/// <summary>
67+
/// Gets or Sets the human readable description for the file type
68+
/// e.g. "Comma Separated Values".
69+
/// </summary>
70+
public string Description { get; set; }
71+
72+
/// <summary>
73+
/// Gets or Sets the permitted file extension(s) (e.g. ".csv").
74+
/// </summary>
75+
public string [] Extensions { get; set; }
76+
77+
78+
/// <summary>
79+
/// Returns <see cref="Description"/> plus all <see cref="Extensions"/> separated by semicolons.
80+
/// </summary>
81+
public override string ToString ()
82+
{
83+
const int maxLength = 30;
84+
85+
var desc = $"{this.Description} ({string.Join (";", this.Extensions.Select (e => '*' + e).ToArray ())})";
86+
87+
if(desc.Length > maxLength) {
88+
return desc.Substring (0, maxLength-2) + "…";
89+
}
90+
return desc;
91+
}
92+
93+
/// <inheritdoc/>
94+
public bool IsAllowed(string path)
95+
{
96+
var extension = Path.GetExtension (path);
97+
98+
// There is a requirement to have a particular extension and we have none
99+
if (string.IsNullOrEmpty (extension)) {
100+
return false;
101+
}
102+
103+
104+
return this.Extensions.Any (e => e.Equals (extension));
105+
}
106+
}
107+
108+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.IO.Abstractions;
5+
using System.Linq;
6+
using Terminal.Gui.Resources;
7+
8+
namespace Terminal.Gui {
9+
/// <summary>
10+
/// Default file operation handlers using modal dialogs.
11+
/// </summary>
12+
public class DefaultFileOperations : IFileOperations {
13+
14+
/// <inheritdoc/>
15+
public bool Delete (IEnumerable<IFileSystemInfo> toDelete)
16+
{
17+
// Default implementation does not allow deleting multiple files
18+
if (toDelete.Count () != 1) {
19+
return false;
20+
}
21+
var d = toDelete.Single ();
22+
var adjective = d.Name;
23+
24+
int result = MessageBox.Query (
25+
string.Format (Strings.fdDeleteTitle, adjective),
26+
string.Format (Strings.fdDeleteBody, adjective),
27+
Strings.fdYes, Strings.fdNo);
28+
29+
try {
30+
if (result == 0) {
31+
if (d is IFileInfo) {
32+
d.Delete ();
33+
} else {
34+
((IDirectoryInfo)d).Delete (true);
35+
}
36+
37+
return true;
38+
}
39+
} catch (Exception ex) {
40+
MessageBox.ErrorQuery (Strings.fdDeleteFailedTitle, ex.Message, "Ok");
41+
}
42+
43+
return false;
44+
}
45+
46+
private bool Prompt (string title, string defaultText, out string result)
47+
{
48+
49+
bool confirm = false;
50+
var btnOk = new Button ("Ok") {
51+
IsDefault = true,
52+
};
53+
btnOk.Clicked += (s, e) => {
54+
confirm = true;
55+
Application.RequestStop ();
56+
};
57+
var btnCancel = new Button ("Cancel");
58+
btnCancel.Clicked += (s, e) => {
59+
confirm = false;
60+
Application.RequestStop ();
61+
};
62+
63+
var lbl = new Label (Strings.fdRenamePrompt);
64+
var tf = new TextField (defaultText) {
65+
X = Pos.Right (lbl),
66+
Width = Dim.Fill (),
67+
};
68+
tf.SelectAll ();
69+
70+
var dlg = new Dialog (title) {
71+
Width = Dim.Percent (50),
72+
Height = 4
73+
};
74+
dlg.Add (lbl);
75+
dlg.Add (tf);
76+
77+
// Add buttons last so tab order is friendly
78+
// and TextField gets focus
79+
dlg.AddButton (btnOk);
80+
dlg.AddButton (btnCancel);
81+
82+
Application.Run (dlg);
83+
84+
result = tf.Text?.ToString ();
85+
86+
return confirm;
87+
}
88+
89+
/// <inheritdoc/>
90+
public IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename)
91+
{
92+
// Don't allow renaming C: or D: or / (on linux) etc
93+
if (toRename is IDirectoryInfo dir && dir.Parent == null) {
94+
return null;
95+
}
96+
97+
if (Prompt (Strings.fdRenameTitle, toRename.Name, out var newName)) {
98+
if (!string.IsNullOrWhiteSpace (newName)) {
99+
try {
100+
if (toRename is IFileInfo f) {
101+
102+
var newLocation = fileSystem.FileInfo.New (Path.Combine (f.Directory.FullName, newName));
103+
f.MoveTo (newLocation.FullName);
104+
return newLocation;
105+
106+
} else {
107+
var d = (IDirectoryInfo)toRename;
108+
109+
var newLocation = fileSystem.DirectoryInfo.New (Path.Combine (d.Parent.FullName, newName));
110+
d.MoveTo (newLocation.FullName);
111+
return newLocation;
112+
}
113+
} catch (Exception ex) {
114+
MessageBox.ErrorQuery (Strings.fdRenameFailedTitle, ex.Message, "Ok");
115+
}
116+
}
117+
}
118+
119+
return null;
120+
}
121+
122+
/// <inheritdoc/>
123+
public IFileSystemInfo New (IFileSystem fileSystem, IDirectoryInfo inDirectory)
124+
{
125+
if (Prompt (Strings.fdNewTitle, "", out var named)) {
126+
if (!string.IsNullOrWhiteSpace (named)) {
127+
try {
128+
var newDir = fileSystem.DirectoryInfo.New (Path.Combine (inDirectory.FullName, named));
129+
newDir.Create ();
130+
return newDir;
131+
} catch (Exception ex) {
132+
MessageBox.ErrorQuery (Strings.fdNewFailed, ex.Message, "Ok");
133+
}
134+
}
135+
}
136+
return null;
137+
}
138+
}
139+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Abstractions;
4+
using System.Linq;
5+
6+
namespace Terminal.Gui {
7+
class DefaultSearchMatcher : ISearchMatcher {
8+
string [] terms;
9+
10+
public void Initialize (string terms)
11+
{
12+
this.terms = terms.Split (new string [] { " " }, StringSplitOptions.RemoveEmptyEntries);
13+
}
14+
15+
public bool IsMatch (IFileSystemInfo f)
16+
{
17+
//Contains overload with StringComparison is not available in (net472) or (netstandard2.0)
18+
//return f.Name.Contains (terms, StringComparison.OrdinalIgnoreCase);
19+
20+
return
21+
// At least one term must match the file name only e.g. "my" in "myfile.csv"
22+
terms.Any (t => f.Name.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0)
23+
&&
24+
// All terms must exist in full path e.g. "dos my" can match "c:\documents\myfile.csv"
25+
terms.All (t => f.FullName.IndexOf (t, StringComparison.OrdinalIgnoreCase) >= 0);
26+
}
27+
}
28+
29+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.IO.Abstractions;
4+
5+
namespace Terminal.Gui {
6+
7+
internal class FileDialogHistory {
8+
private Stack<FileDialogState> back = new Stack<FileDialogState> ();
9+
private Stack<FileDialogState> forward = new Stack<FileDialogState> ();
10+
private FileDialog dlg;
11+
12+
public FileDialogHistory (FileDialog dlg)
13+
{
14+
this.dlg = dlg;
15+
}
16+
17+
public bool Back ()
18+
{
19+
20+
IDirectoryInfo goTo = null;
21+
FileSystemInfoStats restoreSelection = null;
22+
23+
if (this.CanBack ()) {
24+
25+
var backTo = this.back.Pop ();
26+
goTo = backTo.Directory;
27+
restoreSelection = backTo.Selected;
28+
} else if (this.CanUp ()) {
29+
goTo = this.dlg.State?.Directory.Parent;
30+
}
31+
32+
// nowhere to go
33+
if (goTo == null) {
34+
return false;
35+
}
36+
37+
this.forward.Push (this.dlg.State);
38+
this.dlg.PushState (goTo, false, true, false);
39+
40+
if (restoreSelection != null) {
41+
this.dlg.RestoreSelection (restoreSelection.FileSystemInfo);
42+
}
43+
44+
return true;
45+
}
46+
47+
internal bool CanBack ()
48+
{
49+
return this.back.Count > 0;
50+
}
51+
52+
internal bool Forward ()
53+
{
54+
if (this.forward.Count > 0) {
55+
56+
this.dlg.PushState (this.forward.Pop ().Directory, true, true, false);
57+
return true;
58+
}
59+
60+
return false;
61+
}
62+
63+
internal bool Up ()
64+
{
65+
var parent = this.dlg.State?.Directory.Parent;
66+
if (parent != null) {
67+
68+
this.back.Push (new FileDialogState (parent, this.dlg));
69+
this.dlg.PushState (parent, false);
70+
return true;
71+
}
72+
73+
return false;
74+
}
75+
76+
internal bool CanUp ()
77+
{
78+
return this.dlg.State?.Directory.Parent != null;
79+
}
80+
81+
82+
internal void Push (FileDialogState state, bool clearForward)
83+
{
84+
if (state == null) {
85+
return;
86+
}
87+
88+
// if changing to a new directory push onto the Back history
89+
if (this.back.Count == 0 || this.back.Peek ().Directory.FullName != state.Directory.FullName) {
90+
91+
this.back.Push (state);
92+
if (clearForward) {
93+
this.ClearForward ();
94+
}
95+
}
96+
}
97+
98+
internal bool CanForward ()
99+
{
100+
return this.forward.Count > 0;
101+
}
102+
103+
internal void ClearForward ()
104+
{
105+
this.forward.Clear ();
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)