Browse Source

FileDialog work, HexViewer view

Miguel de Icaza 7 years ago
parent
commit
4cf9c7b138
4 changed files with 341 additions and 27 deletions
  1. 46 9
      Example/demo.cs
  2. 10 1
      Terminal.Gui/Core.cs
  3. 71 17
      Terminal.Gui/Dialogs/FileDialog.cs
  4. 214 0
      Terminal.Gui/Views/HexView.cs

+ 46 - 9
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; })
 			}),

+ 10 - 1
Terminal.Gui/Core.cs

@@ -1247,6 +1247,15 @@ namespace Terminal.Gui {
 			}
 			return false;
 		}
+
+		/// <summary>
+		/// 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.
+		/// </summary>
+		public virtual void WillPresent ()
+		{
+			FocusFirst ();
+		}
 	}
 
 	/// <summary>
@@ -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 ();

+ 71 - 17
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<ustring> DirectoryChanged;
+		public Action<ustring> 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);
 		}
 
 		/// <summary>
@@ -403,10 +428,34 @@ namespace Terminal.Gui {
 		}
 	}
 
+	/// <summary>
+	///  The save dialog provides an interactive dialog box for users to pick a file to 
+	///  save.
+	/// </summary>
+	/// <remarks>
+	/// <para>
+	///   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. 
+	/// </remarks>
 	public class SaveDialog : FileDialog {
 		public SaveDialog (ustring title, ustring message) : base (title, prompt: "Save", nameFieldLabel: "Save as:", message: message)
 		{
 		}
+
+		/// <summary>
+		/// Gets the name of the file the user selected for saving, or null
+		/// if the user canceled the dialog box.
+		/// </summary>
+		/// <value>The name of the file.</value>
+		public ustring FileName {
+			get {
+				if (canceled)
+					return null;
+				return FilePath;
+			}
+		}
 	}
 
 	/// <summary>
@@ -414,9 +463,14 @@ namespace Terminal.Gui {
 	/// </summary>
 	/// <remarks>
 	/// <para>
-	/// 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.
+	/// </para>
+	/// <para>
+	///   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.
 	/// </para>
 	/// <para>
 	/// To select more than one file, users can use the spacebar, or control-t.

+ 214 - 0
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;
+
+		/// <summary>
+		/// Creates and instance of the HexView that will render a seekable stream in hex on the allocated view region.
+		/// </summary>
+		/// <param name="source">Source stream, this stream should support seeking, or this will raise an exceotion.</param>
+		public HexView (Stream source) : base()
+		{
+			Source = source;
+			this.source = source;
+			CanFocus = true;
+		}
+
+		/// <summary>
+		/// The source stream to display on the hex view, the stream should support seeking.
+		/// </summary>
+		/// <value>The source.</value>
+		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 ();
+		}
+
+		/// <summary>
+		/// Configures the initial offset to be displayed at the top
+		/// </summary>
+		/// <value>The display start.</value>
+		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;
+		}
+	}
+}