From 4cf9c7b138f37c486b85969cb2d852c1b4ebdc41 Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Wed, 9 May 2018 23:12:06 -0400 Subject: [PATCH] FileDialog work, HexViewer view --- Example/demo.cs | 55 ++++++-- Terminal.Gui/Core.cs | 11 +- Terminal.Gui/Dialogs/FileDialog.cs | 88 +++++++++--- Terminal.Gui/Views/HexView.cs | 214 +++++++++++++++++++++++++++++ 4 files changed, 341 insertions(+), 27 deletions(-) create mode 100644 Terminal.Gui/Views/HexView.cs diff --git a/Example/demo.cs b/Example/demo.cs index aecbd4212..8c4eb891e 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -97,14 +97,14 @@ static class Demo { // layout based on referencing elements of another view: var login = new Label ("Login: ") { X = 3, Y = 6 }; - var password = new Label ("Password: ") { - X = Pos.Left (login), - Y = Pos.Bottom (login) + 1 + var password = new Label ("Password: ") { + X = Pos.Left (login), + Y = Pos.Bottom (login) + 1 }; - var loginText = new TextField ("") { - X = Pos.Right (password), - Y = Pos.Top (login), - Width = 40 + var loginText = new TextField ("") { + X = Pos.Right (password), + Y = Pos.Top (login), + Width = 40 }; var passText = new TextField ("") { Secret = true, @@ -173,7 +173,12 @@ static class Demo { }); ntop.Add (menu); - var win = new Window (new Rect (0, 1, tframe.Width, tframe.Height - 1), "/etc/passwd"); + var win = new Window ("/etc/passwd") { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; ntop.Add (win); var text = new TextView (new Rect (0, 0, tframe.Width - 2, tframe.Height - 3)); @@ -204,6 +209,37 @@ static class Demo { Application.Run (d); } + public static void ShowHex (Toplevel top) + { + var tframe = top.Frame; + var ntop = new Toplevel (tframe); + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Close", "", () => {Application.RequestStop ();}), + }), + }); + ntop.Add (menu); + + var win = new Window ("/etc/passwd") { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + ntop.Add (win); + + var source = System.IO.File.OpenRead ("/etc/passwd"); + var hex = new HexView (source) { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + win.Add (hex); + Application.Run (ntop); + + } + public static Label ml; static void Main () { @@ -211,7 +247,7 @@ static class Demo { Application.Init (); var top = Application.Top; var tframe = top.Frame; - Open (); + //Open (); #if true var win = new Window ("Hello") { X = 0, @@ -227,6 +263,7 @@ static class Demo { new MenuItem ("Text Editor Demo", "", () => { Editor (top); }), new MenuItem ("_New", "Creates new file", NewFile), new MenuItem ("_Open", "", Open), + new MenuItem ("_Hex", "", () => ShowHex (top)), new MenuItem ("_Close", "", () => Close ()), new MenuItem ("_Quit", "", () => { if (Quit ()) top.Running = false; }) }), diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index d80a6c4ff..4f69396fb 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1247,6 +1247,15 @@ namespace Terminal.Gui { } return false; } + + /// + /// This method is invoked by Application.Begin as part of the Application.Run after + /// the views have been laid out, and before the views are drawn for the first time. + /// + public virtual void WillPresent () + { + FocusFirst (); + } } /// @@ -1765,7 +1774,7 @@ namespace Terminal.Gui { if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.RelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); toplevel.LayoutSubviews (); - toplevel.FocusFirst (); + toplevel.WillPresent (); Redraw (toplevel); toplevel.PositionCursor (); Driver.Refresh (); diff --git a/Terminal.Gui/Dialogs/FileDialog.cs b/Terminal.Gui/Dialogs/FileDialog.cs index 8f0e8df01..075a3d4af 100644 --- a/Terminal.Gui/Dialogs/FileDialog.cs +++ b/Terminal.Gui/Dialogs/FileDialog.cs @@ -2,16 +2,12 @@ // FileDialog.cs: File system dialogs for open and save // // TODO: -// * Raise event on file selected // * Add directory selector -// * Update file name on cursor changes -// * Figure out why Ok/Cancel buttons do not work // * Implement subclasses // * Figure out why message text does not show // * Remove the extra space when message does not show // * Use a line separator to show the file listing, so we can use same colors as the rest -// * Implement support for the subclass properties. -// * Add mouse support +// * DirListView: Add mouse support using System; using System.Collections.Generic; @@ -138,6 +134,7 @@ namespace Terminal.Gui { public Action<(string,bool)> SelectedChanged; public Action DirectoryChanged; + public Action FileChanged; void SelectionChanged () { @@ -190,12 +187,20 @@ namespace Terminal.Gui { return true; case Key.Enter: - if (infos [selected].Item2) { + var isDir = infos [selected].Item2; + + if (isDir) { Directory = Path.GetFullPath (Path.Combine (Path.GetFullPath (Directory.ToString ()), infos [selected].Item1)); if (DirectoryChanged != null) DirectoryChanged (Directory); } else { - // File Selected + if (FileChanged != null) + FileChanged (infos [selected].Item1); + if (canChooseFiles) { + // Let the OK handler take it over + return false; + } + // No files allowed, do not let the default handler take it. } return true; @@ -284,18 +289,18 @@ namespace Terminal.Gui { }; dirEntry = new TextField ("") { - X = 11, + X = Pos.Right (dirLabel), Y = 1 + msgLines, Width = Dim.Fill () - 1 }; Add (dirLabel, dirEntry); - this.nameFieldLabel = new Label (nameFieldLabel) { - X = 1, + this.nameFieldLabel = new Label ("Open: ") { + X = 6, Y = 3 + msgLines, }; nameEntry = new TextField ("") { - X = 1 + nameFieldLabel.RuneCount + 1, + X = Pos.Left (dirEntry), Y = 3 + msgLines, Width = Dim.Fill () - 1 }; @@ -305,17 +310,37 @@ namespace Terminal.Gui { X = 1, Y = 3 + msgLines + 2, Width = Dim.Fill (), - Height = Dim.Fill ()-2, - Directory = "." + Height = Dim.Fill () - 2, }; + DirectoryPath = Path.GetFullPath (Environment.CurrentDirectory); Add (dirListView); dirListView.DirectoryChanged = (dir) => dirEntry.Text = dir; + dirListView.FileChanged = (file) => { + nameEntry.Text = file; + }; this.cancel = new Button ("Cancel"); AddButton (cancel); - this.prompt = new Button (prompt); + this.prompt = new Button (prompt) { + IsDefault = true, + }; + this.prompt.Clicked += () => { + canceled = false; + Application.RequestStop (); + }; AddButton (this.prompt); + + // On success, we will set this to false. + canceled = true; + } + + internal bool canceled; + + public override void WillPresent () + { + base.WillPresent (); + //SetFocus (nameEntry); } /// @@ -403,10 +428,34 @@ namespace Terminal.Gui { } } + /// + /// The save dialog provides an interactive dialog box for users to pick a file to + /// save. + /// + /// + /// + /// To use it, create an instance of the SaveDialog, and then + /// call Application.Run on the resulting instance. This will run the dialog modally, + /// and when this returns, the FileName property will contain the selected value or + /// null if the user canceled. + /// public class SaveDialog : FileDialog { public SaveDialog (ustring title, ustring message) : base (title, prompt: "Save", nameFieldLabel: "Save as:", message: message) { } + + /// + /// Gets the name of the file the user selected for saving, or null + /// if the user canceled the dialog box. + /// + /// The name of the file. + public ustring FileName { + get { + if (canceled) + return null; + return FilePath; + } + } } /// @@ -414,9 +463,14 @@ namespace Terminal.Gui { /// /// /// - /// The open dialog can be used to select files for opening, it can be configured to allow - /// multiple items to be selected (based on the AllowsMultipleSelection) variable and - /// you can control whether this should allow files or directories to be selected. + /// The open dialog can be used to select files for opening, it can be configured to allow + /// multiple items to be selected (based on the AllowsMultipleSelection) variable and + /// you can control whether this should allow files or directories to be selected. + /// + /// + /// To use it, create an instance of the OpenDialog, configure its properties, and then + /// call Application.Run on the resulting instance. This will run the dialog modally, + /// and when this returns, the list of filds will be available on the FilePaths property. /// /// /// To select more than one file, users can use the spacebar, or control-t. diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs new file mode 100644 index 000000000..2662155fb --- /dev/null +++ b/Terminal.Gui/Views/HexView.cs @@ -0,0 +1,214 @@ +// +// HexView.cs: A hexadecimal viewer +// +// TODO: +// - Support an operation to switch between hex and values +// - Tab perhaps to switch? +// - Support nibble-based navigation +// - Support editing, perhaps via list of changes? +// - Support selection with highlighting +// - Redraw should support just repainted affected region +// - Process Key needs to just queue affected region for cursor changes (as we repaint the text) + +using System; +using System.IO; + +namespace Terminal.Gui { + public class HexView : View { + Stream source; + long displayStart, position; + + /// + /// Creates and instance of the HexView that will render a seekable stream in hex on the allocated view region. + /// + /// Source stream, this stream should support seeking, or this will raise an exceotion. + public HexView (Stream source) : base() + { + Source = source; + this.source = source; + CanFocus = true; + } + + /// + /// The source stream to display on the hex view, the stream should support seeking. + /// + /// The source. + public Stream Source { + get => source; + set { + if (value == null) + throw new ArgumentNullException ("source"); + if (!value.CanSeek) + throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source"); + source = value; + + SetNeedsDisplay (); + } + } + + internal void SetDisplayStart (long value) + { + if (value >= source.Length) + displayStart = source.Length - 1; + else if (value < 0) + displayStart = 0; + else + displayStart = value; + SetNeedsDisplay (); + } + + /// + /// Configures the initial offset to be displayed at the top + /// + /// The display start. + public long DisplayStart { + get => displayStart; + set { + position = value; + + SetDisplayStart (value); + } + } + + const int displayWidth = 9; + const int bsize = 4; + int bytesPerLine; + + public override Rect Frame { + get => base.Frame; + set { + base.Frame = value; + + // Small buffers will just show the position, with 4 bytes + bytesPerLine = 4; + if (value.Width - displayWidth > 17) + bytesPerLine = 4 * ((value.Width - displayWidth) / 18); + } + } + + public override void Redraw (Rect region) + { + Attribute currentAttribute; + var current = ColorScheme.Focus; + Driver.SetAttribute (current); + Move (0, 0); + + var frame = Frame; + + var nblocks = bytesPerLine / 4; + var data = new byte [nblocks * 4 * frame.Height]; + Source.Position = displayStart; + var n = source.Read (data, 0, data.Length); + + for (int line = 0; line < frame.Height; line++) { + Move (0, line); + Driver.SetAttribute (ColorScheme.HotNormal); + Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * 4)); + + currentAttribute = ColorScheme.HotNormal; + SetAttribute (ColorScheme.Normal); + + for (int block = 0; block < nblocks; block++) { + for (int b = 0; b < 4; b++) { + var offset = (line * nblocks * 4) + block * 4 + b; + if (offset + displayStart == position) + SetAttribute (ColorScheme.HotNormal); + else + SetAttribute (ColorScheme.Normal); + + Driver.AddStr (offset >= n ? " " : string.Format ("{0:x2} ", data [offset])); + } + Driver.AddStr (block + 1 == nblocks ? " " : "| "); + } + for (int bitem = 0; bitem < nblocks * 4; bitem++) { + var offset = line * nblocks * 4 + bitem; + + if (offset + displayStart == position) + SetAttribute (ColorScheme.HotFocus); + else + SetAttribute (ColorScheme.Normal); + + Rune c = ' '; + if (offset >= n) + c = ' '; + else { + var b = data [offset]; + if (b < 32) + c = '.'; + else if (b > 127) + c = '.'; + else + c = b; + } + Driver.AddRune (c); + } + } + + void SetAttribute (Attribute attribute) + { + if (currentAttribute != attribute) { + currentAttribute = attribute; + Driver.SetAttribute (attribute); + } + } + + } + + public override void PositionCursor () + { + var delta = (int)(position - displayStart); + var line = delta / bytesPerLine; + var item = delta % bytesPerLine; + var block = item / 4; + var column = (item % 4) * 3; + + Move (displayWidth + block * 14 + column, line); + } + + public override bool ProcessKey (KeyEvent keyEvent) + { + switch (keyEvent.Key) { + case Key.CursorLeft: + if (position == 0) + return true; + if (position - 1 < DisplayStart) { + SetDisplayStart (displayStart - bytesPerLine); + SetNeedsDisplay (); + } + position--; + break; + case Key.CursorRight: + if (position < source.Length) + position++; + if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { + SetDisplayStart (DisplayStart + bytesPerLine); + SetNeedsDisplay (); + } + break; + case Key.CursorDown: + if (position + bytesPerLine < source.Length) + position += bytesPerLine; + if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { + SetDisplayStart (DisplayStart + bytesPerLine); + SetNeedsDisplay (); + } + break; + case Key.CursorUp: + position -= bytesPerLine; + if (position < 0) + position = 0; + if (position < DisplayStart) { + SetDisplayStart (DisplayStart - bytesPerLine); + SetNeedsDisplay (); + } + break; + default: + return false; + } + // TODO: just se the NeedDispay for the affected region, not all + SetNeedsDisplay (); + PositionCursor (); + return false; + } + } +}