diff --git a/.gitignore b/.gitignore
index 1a111a2ca..cbf9d62d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,9 +34,6 @@ _ReSharper.**
~$*
*~
-# NuGet Stuff
-*.nupkg
-*.snupkg
# Exclude everything in packages directory except the packages/build directory
**/[Pp]ackages/*
!**/[Pp]ackages/build/
@@ -61,4 +58,3 @@ demo.*
*.tui/
*.dotCover
-/local_packages/
diff --git a/NativeAot/PackTerminalGui.ps1 b/NativeAot/PackTerminalGui.ps1
deleted file mode 100644
index ea6bff054..000000000
--- a/NativeAot/PackTerminalGui.ps1
+++ /dev/null
@@ -1,9 +0,0 @@
-# Step 1: Build and pack Terminal.Gui
-dotnet build ../Terminal.Gui/Terminal.Gui.csproj --configuration Release
-dotnet pack ../Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ../local_packages
-
-# Step 2: Restore NativeAot with the new package
-dotnet restore ./NativeAot.csproj --source ./local_packages
-
-# Step 3: Build NativeAot
-dotnet build ./NativeAot.csproj --configuration Release
diff --git a/NativeAot/PackTerminalGui.sh b/NativeAot/PackTerminalGui.sh
deleted file mode 100644
index ab0cc7e17..000000000
--- a/NativeAot/PackTerminalGui.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-# Step 1: Build and pack Terminal.Gui
-dotnet build ../Terminal.Gui/Terminal.Gui.csproj --configuration Release
-dotnet pack ../Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ../local_packages
-
-# Step 2: Restore NativeAot with the new package
-dotnet restore ./NativeAot.csproj --source ./local_packages
-
-# Step 3: Build NativeAot
-dotnet build ./NativeAot.csproj --configuration Release
diff --git a/SelfContained/PackTerminalGui.ps1 b/SelfContained/PackTerminalGui.ps1
deleted file mode 100644
index 6e5cd4192..000000000
--- a/SelfContained/PackTerminalGui.ps1
+++ /dev/null
@@ -1,9 +0,0 @@
-# Step 1: Build and pack Terminal.Gui
-dotnet build ../Terminal.Gui/Terminal.Gui.csproj --configuration Release
-dotnet pack ../Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ../local_packages
-
-# Step 2: Restore SelfContained with the new package
-dotnet restore ./SelfContained.csproj --source ./local_packages
-
-# Step 3: Build SelfContained
-dotnet build ./SelfContained.csproj --configuration Release
diff --git a/SelfContained/PackTerminalGui.sh b/SelfContained/PackTerminalGui.sh
deleted file mode 100644
index d2cba3e87..000000000
--- a/SelfContained/PackTerminalGui.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-# Step 1: Build and pack Terminal.Gui
-dotnet build ../Terminal.Gui/Terminal.Gui.csproj --configuration Release
-dotnet pack ../Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ../local_packages
-
-# Step 2: Restore SelfContained with the new package
-dotnet restore ./SelfContained.csproj --source ./local_packages
-
-# Step 3: Build SelfContained
-dotnet build ./SelfContained.csproj --configuration Release
diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs
index e3c78bdd7..966fb04c6 100644
--- a/Terminal.Gui/Application/Application.Run.cs
+++ b/Terminal.Gui/Application/Application.Run.cs
@@ -80,14 +80,14 @@ public static partial class Application // Run (Begin, Run, End, Stop)
{
ArgumentNullException.ThrowIfNull (toplevel);
-#if DEBUG_IDISPOSABLE
- Debug.Assert (!toplevel.WasDisposed);
+//#if DEBUG_IDISPOSABLE
+// Debug.Assert (!toplevel.WasDisposed);
- if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
- {
- Debug.Assert (_cachedRunStateToplevel.WasDisposed);
- }
-#endif
+// if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
+// {
+// Debug.Assert (_cachedRunStateToplevel.WasDisposed);
+// }
+//#endif
// Ensure the mouse is ungrabbed.
MouseGrabView = null;
diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs
index 2523709d0..5835957a2 100644
--- a/Terminal.Gui/Application/ApplicationNavigation.cs
+++ b/Terminal.Gui/Application/ApplicationNavigation.cs
@@ -27,7 +27,19 @@ public class ApplicationNavigation
///
/// Gets the most focused in the application, if there is one.
///
- public View? GetFocused () { return _focused; }
+ public View? GetFocused ()
+ {
+ return _focused;
+
+ if (_focused is { CanFocus: true, HasFocus: true })
+ {
+ return _focused;
+ }
+
+ _focused = null;
+
+ return null;
+ }
///
/// Gets whether is in the Subview hierarchy of .
@@ -77,6 +89,7 @@ public class ApplicationNavigation
{
return;
}
+ Debug.Assert (value is null or { CanFocus: true, HasFocus: true });
_focused = value;
diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs
index 1268a576d..2cf77699b 100644
--- a/Terminal.Gui/Drawing/Glyphs.cs
+++ b/Terminal.Gui/Drawing/Glyphs.cs
@@ -97,6 +97,9 @@ public class GlyphDefinitions
/// Dot. Default is (U+2219) - ∙.
public Rune Dot { get; set; } = (Rune)'∙';
+ /// Dotted Square - ⬚ U+02b1a┝
+ public Rune DottedSquare { get; set; } = (Rune)'⬚';
+
/// Black Circle . Default is (U+025cf) - ●.
public Rune BlackCircle { get; set; } = (Rune)'●'; // Black Circle - ● U+025cf
diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs
index 43af4e906..e0af3c2af 100644
--- a/Terminal.Gui/Input/Command.cs
+++ b/Terminal.Gui/Input/Command.cs
@@ -166,6 +166,11 @@ public enum Command
///
EnableOverwrite,
+ ///
+ /// Inserts a character.
+ ///
+ Insert,
+
/// Disables overwrite mode ()
DisableOverwrite,
diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj
index b11044a16..08813a50f 100644
--- a/Terminal.Gui/Terminal.Gui.csproj
+++ b/Terminal.Gui/Terminal.Gui.csproj
@@ -1,4 +1,4 @@
-
+
@@ -142,4 +142,36 @@
Miguel de Icaza, Tig Kindel (@tig), @BDisp
+
+
+
+
+ $(MSBuildThisFileDirectory)..\local_packages\
+
+ $(MSBuildThisFileDirectory)bin\$(Configuration)\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs
index 4e1647149..3ca31898c 100644
--- a/Terminal.Gui/View/Adornment/ShadowView.cs
+++ b/Terminal.Gui/View/Adornment/ShadowView.cs
@@ -109,7 +109,7 @@ internal class ShadowView : View
Rectangle screen = ViewportToScreen (viewport);
// Fill the rest of the rectangle - note we skip the last since vertical will draw it
- for (int i = screen.X + 1; i < screen.X + screen.Width - 1; i++)
+ for (int i = Math.Max(0, screen.X + 1); i < screen.X + screen.Width - 1; i++)
{
Driver.Move (i, screen.Y);
diff --git a/Terminal.Gui/View/CancelEventArgs.cs b/Terminal.Gui/View/CancelEventArgs.cs
index 74e23ce2f..c41fb5d14 100644
--- a/Terminal.Gui/View/CancelEventArgs.cs
+++ b/Terminal.Gui/View/CancelEventArgs.cs
@@ -27,6 +27,17 @@ public class CancelEventArgs : CancelEventArgs where T : notnull
NewValue = newValue;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The current (old) value of the property.
+ /// The value the property will be set to if the event is not cancelled.
+ protected CancelEventArgs (T currentValue, T newValue)
+ {
+ CurrentValue = currentValue;
+ NewValue = newValue;
+ }
+
/// The current value of the property.
public T CurrentValue { get; }
diff --git a/Terminal.Gui/View/Navigation/AdvanceFocusEventArgs.cs b/Terminal.Gui/View/Navigation/AdvanceFocusEventArgs.cs
new file mode 100644
index 000000000..fd3a64951
--- /dev/null
+++ b/Terminal.Gui/View/Navigation/AdvanceFocusEventArgs.cs
@@ -0,0 +1,18 @@
+namespace Terminal.Gui;
+
+/// The event arguments for events.
+public class AdvanceFocusEventArgs : CancelEventArgs
+{
+ /// Initializes a new instance.
+ public AdvanceFocusEventArgs (NavigationDirection direction, TabBehavior? behavior) : base (false, false)
+ {
+ Direction = direction;
+ Behavior = behavior;
+ }
+
+ /// Gets or sets the view that is losing focus.
+ public NavigationDirection Direction { get; set; }
+
+ /// Gets or sets the view that is gaining focus.
+ public TabBehavior? Behavior { get; set; }
+}
diff --git a/Terminal.Gui/View/Navigation/FocusEventArgs.cs b/Terminal.Gui/View/Navigation/FocusEventArgs.cs
index 63c38bbe2..c895e4e88 100644
--- a/Terminal.Gui/View/Navigation/FocusEventArgs.cs
+++ b/Terminal.Gui/View/Navigation/FocusEventArgs.cs
@@ -20,4 +20,4 @@ public class HasFocusEventArgs : CancelEventArgs
/// Gets or sets the view that is gaining focus.
public View NewFocused { get; set; }
-}
+}
\ No newline at end of file
diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs
index 02b737896..01cf01beb 100644
--- a/Terminal.Gui/View/View.Hierarchy.cs
+++ b/Terminal.Gui/View/View.Hierarchy.cs
@@ -169,6 +169,13 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
Debug.Assert (!view.HasFocus);
_subviews.Remove (view);
+
+ // Clean up focus stuff
+ _previouslyFocused = null;
+ if (view._superView is { } && view._superView._previouslyFocused == this)
+ {
+ view._superView._previouslyFocused = null;
+ }
view._superView = null;
SetNeedsLayout ();
diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs
index a8a2022aa..e68b3c7d3 100644
--- a/Terminal.Gui/View/View.Navigation.cs
+++ b/Terminal.Gui/View/View.Navigation.cs
@@ -1,6 +1,5 @@
#nullable enable
using System.Diagnostics;
-using System.Reflection.PortableExecutable;
namespace Terminal.Gui;
@@ -18,7 +17,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// If there is no next/previous view to advance to, the focus is set to the view itself.
///
///
- /// See the View Navigation Deep Dive for more information:
+ /// See the View Navigation Deep Dive for more information:
+ ///
///
///
///
@@ -34,6 +34,11 @@ public partial class View // Focus and cross-view navigation management (TabStop
return false;
}
+ if (RaiseAdvancingFocus (direction, behavior))
+ {
+ return true;
+ }
+
View? focused = Focused;
if (focused is { } && focused.AdvanceFocus (direction, behavior))
@@ -128,6 +133,14 @@ public partial class View // Focus and cross-view navigation management (TabStop
if (view.HasFocus)
{
// We could not advance
+ if (view != this)
+ {
+ // Tell it to try the other way.
+ return view.RaiseAdvancingFocus (
+ direction == NavigationDirection.Forward ? NavigationDirection.Backward : NavigationDirection.Forward,
+ behavior);
+ }
+
return view == this;
}
@@ -137,10 +150,61 @@ public partial class View // Focus and cross-view navigation management (TabStop
return focusSet;
}
+ private bool RaiseAdvancingFocus (NavigationDirection direction, TabBehavior? behavior)
+ {
+ // Call the virtual method
+ if (OnAdvancingFocus (direction, behavior))
+ {
+ // The event was cancelled
+ return true;
+ }
+
+ var args = new AdvanceFocusEventArgs (direction, behavior);
+ AdvancingFocus?.Invoke (this, args);
+
+ if (args.Cancel)
+ {
+ // The event was cancelled
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Called when is about to advance focus.
+ ///
+ ///
+ ///
+ /// If a view cancels the event and the focus could not otherwise advance, the Navigation direction will be
+ /// reversed and the event will be raised again.
+ ///
+ ///
+ ///
+ /// , if the focus advance is to be cancelled,
+ /// otherwise.
+ ///
+ protected virtual bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) { return false; }
+
+ ///
+ /// Raised when is about to advance focus.
+ ///
+ ///
+ ///
+ /// Cancel the event to prevent the focus from advancing.
+ ///
+ ///
+ /// If a view cancels the event and the focus could not otherwise advance, the Navigation direction will be
+ /// reversed and the event will be raised again.
+ ///
+ ///
+ public event EventHandler? AdvancingFocus;
+
/// Gets or sets a value indicating whether this can be focused.
///
///
- /// See the View Navigation Deep Dive for more information:
+ /// See the View Navigation Deep Dive for more information:
+ ///
///
///
/// must also have set to .
@@ -180,7 +244,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
if (!_canFocus && HasFocus)
{
// If CanFocus is set to false and this view has focus, make it leave focus
- HasFocus = false;
+ // Set transversing down so we don't go back up the hierarchy...
+ SetHasFocusFalse (null, false);
}
if (_canFocus && !HasFocus && Visible && SuperView is { Focused: null })
@@ -296,7 +361,12 @@ public partial class View // Focus and cross-view navigation management (TabStop
if (Focused is null && _previouslyFocused is { } && indicies.Contains (_previouslyFocused))
{
- return _previouslyFocused.SetFocus ();
+ if (_previouslyFocused.SetFocus ())
+ {
+ return true;
+ }
+
+ _previouslyFocused = null;
}
return false;
@@ -324,7 +394,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
///
///
///
- /// See the View Navigation Deep Dive for more information:
+ /// See the View Navigation Deep Dive for more information:
+ ///
///
///
/// Only Views that are visible, enabled, and have set to are
@@ -375,6 +446,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
{
SetHasFocusFalse (null);
+ Debug.Assert (!_hasFocus);
+
if (_hasFocus)
{
// force it.
@@ -391,7 +464,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
///
///
///
- /// See the View Navigation Deep Dive for more information:
+ /// See the View Navigation Deep Dive for more information:
+ ///
///
///
/// if the focus changed; false otherwise.
@@ -412,14 +486,14 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// other methods that
/// set or remove focus from a view.
///
- ///
- /// The previously focused view. If there is no previously focused
+ ///
+ /// The currently focused view. If there is no previously focused
/// view.
///
///
/// if was changed to .
///
- private (bool focusSet, bool cancelled) SetHasFocusTrue (View? previousFocusedView, bool traversingUp = false)
+ private (bool focusSet, bool cancelled) SetHasFocusTrue (View? currentFocusedView, bool traversingUp = false)
{
Debug.Assert (SuperView is null || ApplicationNavigation.IsInHierarchy (SuperView, this));
@@ -429,6 +503,11 @@ public partial class View // Focus and cross-view navigation management (TabStop
return (false, false);
}
+ if (currentFocusedView is { HasFocus: false })
+ {
+ throw new ArgumentException ("SetHasFocusTrue: currentFocusedView must HasFocus.");
+ }
+
var thisAsAdornment = this as Adornment;
View? superViewOrParent = thisAsAdornment?.Parent ?? SuperView;
@@ -451,7 +530,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
bool previousValue = HasFocus;
- bool cancelled = NotifyFocusChanging (false, true, previousFocusedView, this);
+ bool cancelled = RaiseFocusChanging (false, true, currentFocusedView, this);
if (cancelled)
{
@@ -462,7 +541,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
// Any of them may cancel gaining focus. In which case we need to back out.
if (superViewOrParent is { HasFocus: false } sv)
{
- (bool focusSet, bool svCancelled) = sv.SetHasFocusTrue (previousFocusedView, true);
+ (bool focusSet, bool svCancelled) = sv.SetHasFocusTrue (currentFocusedView, true);
if (!focusSet)
{
@@ -488,31 +567,32 @@ public partial class View // Focus and cross-view navigation management (TabStop
if (!traversingUp)
{
- // Restore focus to the previously focused subview
+ // Restore focus to the previously focused subview, if any
if (!RestoreFocus ())
{
- // Couldn't restore focus, so use Advance to navigate to the next focusable subview
- if (!AdvanceFocus (NavigationDirection.Forward, null))
- {
- // Couldn't advance, so we're the most focused view in the application
- Application.Navigation?.SetFocused (this);
- }
+ // Couldn't restore focus, so use Advance to navigate to the next focusable subview, if any
+ AdvanceFocus (NavigationDirection.Forward, null);
}
}
- if (previousFocusedView is { HasFocus: true } && GetFocusChain (NavigationDirection.Forward, TabStop).Contains (previousFocusedView))
+ // Now make sure the old focused view loses focus
+ if (currentFocusedView is { HasFocus: true } && GetFocusChain (NavigationDirection.Forward, TabStop).Contains (currentFocusedView))
{
- previousFocusedView.SetHasFocusFalse (this);
+ currentFocusedView.SetHasFocusFalse (this);
}
- _previouslyFocused = null;
+ if (_previouslyFocused is { })
+ {
+ _previouslyFocused = null;
+ }
if (Arrangement.HasFlag (ViewArrangement.Overlapped))
{
SuperView?.MoveSubviewToEnd (this);
}
- NotifyFocusChanged (HasFocus, previousFocusedView, this);
+ // Focus work is done. Notify.
+ RaiseFocusChanged (HasFocus, currentFocusedView, this);
SetNeedsDisplay ();
@@ -525,8 +605,11 @@ public partial class View // Focus and cross-view navigation management (TabStop
return (true, false);
}
- private bool NotifyFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused)
+ private bool RaiseFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused)
{
+ Debug.Assert (currentFocused is null || currentFocused is { HasFocus: true });
+ Debug.Assert (newFocused is null || newFocused is { CanFocus: true });
+
// Call the virtual method
if (OnHasFocusChanging (currentHasFocus, newHasFocus, currentFocused, newFocused))
{
@@ -543,6 +626,20 @@ public partial class View // Focus and cross-view navigation management (TabStop
return true;
}
+ View? appFocused = Application.Navigation?.GetFocused ();
+
+ if (appFocused == currentFocused)
+ {
+ if (newFocused is { HasFocus: true })
+ {
+ Application.Navigation?.SetFocused (newFocused);
+ }
+ else
+ {
+ Application.Navigation?.SetFocused (null);
+ }
+ }
+
return false;
}
@@ -586,8 +683,9 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// focused.
///
///
- /// Set to true to indicate method is being called recurively, traversing down the focus
- /// chain.
+ /// Set to true to traverse down the focus
+ /// chain only. If false, the method will attempt to AdvanceFocus on the superview or restorefocus on
+ /// Application.Navigation.GetFocused().
///
///
private void SetHasFocusFalse (View? newFocusedView, bool traversingDown = false)
@@ -598,45 +696,86 @@ public partial class View // Focus and cross-view navigation management (TabStop
throw new InvalidOperationException ("SetHasFocusFalse should not be called if the view does not have focus.");
}
+ if (newFocusedView is { HasFocus: false })
+ {
+ throw new InvalidOperationException ("SetHasFocusFalse new focused view does not have focus.");
+ }
+
var thisAsAdornment = this as Adornment;
View? superViewOrParent = thisAsAdornment?.Parent ?? SuperView;
// If newFocusedVew is null, we need to find the view that should get focus, and SetFocus on it.
if (!traversingDown && newFocusedView is null)
{
- if (superViewOrParent?._previouslyFocused is { })
+ // Restore focus?
+ if (superViewOrParent?._previouslyFocused is { CanFocus: true })
{
- if (superViewOrParent._previouslyFocused != this)
+ // TODO: Why don't we call RestoreFocus here?
+ if (superViewOrParent._previouslyFocused != this && superViewOrParent._previouslyFocused.SetFocus ())
{
- superViewOrParent?._previouslyFocused?.SetFocus ();
-
// The above will cause SetHasFocusFalse, so we can return
+ Debug.Assert (!_hasFocus);
+
return;
}
}
- if (superViewOrParent is { })
+ // AdvanceFocus?
+ if (superViewOrParent is { CanFocus: true })
{
if (superViewOrParent.AdvanceFocus (NavigationDirection.Forward, TabStop))
{
- // The above will cause SetHasFocusFalse, so we can return
- return;
+ // The above might have SetHasFocusFalse, so we can return
+ if (!_hasFocus)
+ {
+ return;
+ }
}
- newFocusedView = superViewOrParent;
+ if (superViewOrParent is { HasFocus: true, CanFocus: true })
+ {
+ newFocusedView = superViewOrParent;
+ }
}
- if (Application.Navigation is { } && Application.Top is { })
+ // Application.Navigation.GetFocused?
+ View? applicationFocused = Application.Navigation?.GetFocused ();
+
+ if (newFocusedView is null && applicationFocused != this && applicationFocused is { CanFocus: true })
{
// Temporarily ensure this view can't get focus
bool prevCanFocus = _canFocus;
_canFocus = false;
- bool restoredFocus = Application.Top!.RestoreFocus ();
+ bool restoredFocus = applicationFocused!.RestoreFocus ();
_canFocus = prevCanFocus;
if (restoredFocus)
{
// The above caused SetHasFocusFalse, so we can return
+ Debug.Assert (!_hasFocus);
+
+ return;
+ }
+ }
+
+ // Application.Top?
+ if (newFocusedView is null && Application.Top is { CanFocus: true, HasFocus: false })
+ {
+ // Temporarily ensure this view can't get focus
+ bool prevCanFocus = _canFocus;
+ _canFocus = false;
+ bool restoredFocus = Application.Top.RestoreFocus ();
+ _canFocus = prevCanFocus;
+
+ if (Application.Top is { CanFocus: true, HasFocus: true })
+ {
+ newFocusedView = Application.Top;
+ }
+ else if (restoredFocus)
+ {
+ // The above caused SetHasFocusFalse, so we can return
+ Debug.Assert (!_hasFocus);
+
return;
}
}
@@ -644,6 +783,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
// No other focusable view to be found. Just "leave" us...
}
+ Debug.Assert (_hasFocus);
// Before we can leave focus, we need to make sure that all views down the subview-hierarchy have left focus.
View? mostFocused = MostFocused;
@@ -665,15 +805,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
bottom = bottom.SuperView;
}
- if (bottom == this && bottom.SuperView is Adornment a)
- {
- //a.SetHasFocusFalse (newFocusedView, true);
-
- Debug.Assert (_hasFocus);
- }
-
Debug.Assert (_hasFocus);
-
}
if (superViewOrParent is { })
@@ -683,8 +815,17 @@ public partial class View // Focus and cross-view navigation management (TabStop
bool previousValue = HasFocus;
+ Debug.Assert (_hasFocus);
+
// Note, can't be cancelled.
- NotifyFocusChanging (HasFocus, !HasFocus, newFocusedView, this);
+ RaiseFocusChanging (HasFocus, !HasFocus, this, newFocusedView);
+
+ // Even though the change can't be cancelled, some listener may have changed the focus to another view.
+ if (!_hasFocus)
+ {
+ // Notify caused HasFocus to change to false.
+ return;
+ }
// Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused
View? focusedPeer = superViewOrParent?.Focused;
@@ -692,17 +833,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
// Set HasFocus false
_hasFocus = false;
- if (Application.Navigation is { })
- {
- View? appFocused = Application.Navigation.GetFocused ();
-
- if (appFocused is { } || appFocused == this)
- {
- Application.Navigation.SetFocused (newFocusedView ?? superViewOrParent);
- }
- }
-
- NotifyFocusChanged (HasFocus, this, newFocusedView);
+ RaiseFocusChanged (HasFocus, this, newFocusedView);
if (_hasFocus)
{
@@ -719,8 +850,13 @@ public partial class View // Focus and cross-view navigation management (TabStop
SetNeedsDisplay ();
}
- private void NotifyFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew)
+ private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew)
{
+ if (newHasFocus && focusedVew?.Focused is null)
+ {
+ Application.Navigation?.SetFocused (focusedVew);
+ }
+
// Call the virtual method
OnHasFocusChanged (newHasFocus, previousFocusedView, focusedVew);
@@ -756,7 +892,8 @@ public partial class View // Focus and cross-view navigation management (TabStop
#region Tab/Focus Handling
///
- /// Gets the subviews and Adornments of this view that are scoped to the specified behavior and direction. If behavior is null, all focusable subviews and
+ /// Gets the subviews and Adornments of this view that are scoped to the specified behavior and direction. If behavior
+ /// is null, all focusable subviews and
/// Adornments are returned.
///
///
@@ -775,7 +912,6 @@ public partial class View // Focus and cross-view navigation management (TabStop
filteredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true });
}
-
// How about in Adornments?
if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior)
{
@@ -806,11 +942,14 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// Gets or sets the behavior of for keyboard navigation.
///
///
- ///
+ ///
+ ///
+ /// See the View Navigation Deep Dive for more information:
+ ///
+ ///
+ ///
+ /// ///
///
- /// See the View Navigation Deep Dive for more information:
- ///
- /// ///
/// If the tab stop has not been set and setting to true will set it
/// to
/// .
diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs
index e1cca01ed..8c614062b 100644
--- a/Terminal.Gui/Views/HexView.cs
+++ b/Terminal.Gui/Views/HexView.cs
@@ -1,18 +1,22 @@
-//
+#nullable enable
+
+//
// HexView.cs: A hexadecimal viewer
//
-// TODO:
-// - Support searching and highlighting of the search result
-// - Bug showing the last line
+// TODO: Support searching and highlighting of the search result
+// TODO: Support shrinking the stream (e.g. del/backspace should work).
//
+using System.Buffers;
+
namespace Terminal.Gui;
-/// An hex viewer and editor over a
+/// Hex viewer and editor over a
///
///
/// provides a hex editor on top of a seekable with the left side
-/// showing an hex dump of the values in the and the right side showing the contents (filtered
+/// showing the hex values of the bytes in the and the right side showing the contents
+/// (filtered
/// to non-control sequence ASCII characters).
///
/// Users can switch from one side to the other by using the tab key.
@@ -23,69 +27,67 @@ namespace Terminal.Gui;
/// changes were made and the new values. A convenience method, will apply the edits to
/// the .
///
+///
+/// Control the byte at the caret for editing by setting the property to an offset in the
+/// stream.
+///
/// Control the first byte shown by setting the property to an offset in the stream.
///
public class HexView : View, IDesignable
{
- private const int bsize = 4;
- private const int displayWidth = 9;
+ private const int DEFAULT_ADDRESS_WIDTH = 8; // The default value for AddressWidth
+ private const int NUM_BYTES_PER_HEX_COLUMN = 4;
+ private const int HEX_COLUMN_WIDTH = NUM_BYTES_PER_HEX_COLUMN * 3 + 2; // 3 cols per byte + 1 for vert separator + right space
- private int bpl;
- private long displayStart, pos;
- private SortedDictionary edits = [];
- private bool firstNibble;
- private bool leftSide;
- private Stream source;
- private static readonly Rune SpaceCharRune = new (' ');
- private static readonly Rune PeriodCharRune = new ('.');
+ private bool _firstNibble;
+ private bool _leftSideHasFocus;
+ private static readonly Rune _spaceCharRune = new (' ');
+ private static readonly Rune _periodCharRune = Glyphs.DottedSquare;
+ private static readonly Rune _columnSeparatorRune = Glyphs.VLineDa4;
/// Initializes a class.
///
/// The to view and edit as hex, this must support seeking,
/// or an exception will be thrown.
///
- public HexView (Stream source)
+ public HexView (Stream? source)
{
Source = source;
CanFocus = true;
CursorVisibility = CursorVisibility.Default;
- leftSide = true;
- firstNibble = true;
+ _leftSideHasFocus = true;
+ _firstNibble = true;
// PERF: Closure capture of 'this' creates a lot of overhead.
// BUG: Closure capture of 'this' may have unexpected results depending on how this is called.
- // The above two comments apply to all of the lambdas passed to all calls to AddCommand below.
- // Things this view knows how to do
+ // The above two comments apply to all the lambdas passed to all calls to AddCommand below.
AddCommand (Command.Left, () => MoveLeft ());
AddCommand (Command.Right, () => MoveRight ());
- AddCommand (Command.Down, () => MoveDown (bytesPerLine));
- AddCommand (Command.Up, () => MoveUp (bytesPerLine));
- AddCommand (Command.Tab, () => Navigate (NavigationDirection.Forward));
- AddCommand (Command.BackTab, () => Navigate (NavigationDirection.Backward));
- AddCommand (Command.PageUp, () => MoveUp (bytesPerLine * Frame.Height));
- AddCommand (Command.PageDown, () => MoveDown (bytesPerLine * Frame.Height));
+ AddCommand (Command.Down, () => MoveDown (BytesPerLine));
+ AddCommand (Command.Up, () => MoveUp (BytesPerLine));
+ AddCommand (Command.PageUp, () => MoveUp (BytesPerLine * Viewport.Height));
+ AddCommand (Command.PageDown, () => MoveDown (BytesPerLine * Viewport.Height));
AddCommand (Command.Start, () => MoveHome ());
AddCommand (Command.End, () => MoveEnd ());
AddCommand (Command.LeftStart, () => MoveLeftStart ());
AddCommand (Command.RightEnd, () => MoveEndOfLine ());
- AddCommand (Command.StartOfPage, () => MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine)));
-
+ AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - _displayStart) / BytesPerLine)));
AddCommand (
Command.EndOfPage,
- () => MoveDown (bytesPerLine * (Frame.Height - 1 - (int)(position - displayStart) / bytesPerLine))
+ () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - _displayStart) / BytesPerLine))
);
+ AddCommand (Command.DeleteCharLeft, () => true);
+ AddCommand (Command.DeleteCharRight, () => true);
+ AddCommand (Command.Insert, () => true);
- // Default keybindings for this view
KeyBindings.Add (Key.CursorLeft, Command.Left);
KeyBindings.Add (Key.CursorRight, Command.Right);
KeyBindings.Add (Key.CursorDown, Command.Down);
KeyBindings.Add (Key.CursorUp, Command.Up);
- KeyBindings.Add (Key.V.WithAlt, Command.PageUp);
KeyBindings.Add (Key.PageUp, Command.PageUp);
- KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
KeyBindings.Add (Key.PageDown, Command.PageDown);
KeyBindings.Add (Key.Home, Command.Start);
@@ -95,8 +97,12 @@ public class HexView : View, IDesignable
KeyBindings.Add (Key.CursorUp.WithCtrl, Command.StartOfPage);
KeyBindings.Add (Key.CursorDown.WithCtrl, Command.EndOfPage);
- KeyBindings.Add (Key.Tab, Command.Tab);
- KeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
+ KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
+ KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
+ KeyBindings.Add (Key.InsertChar, Command.Insert);
+
+ KeyBindings.Remove (Key.Space);
+ KeyBindings.Remove (Key.Enter);
LayoutComplete += HexView_LayoutComplete;
}
@@ -105,128 +111,202 @@ public class HexView : View, IDesignable
public HexView () : this (new MemoryStream ()) { }
///
- /// Gets or sets whether this allow editing of the of the underlying
+ /// Gets or sets whether this allows editing of the of the underlying
/// .
///
- /// true if allow edits; otherwise, false.
+ /// true to allow edits; otherwise, false.
public bool AllowEdits { get; set; } = true;
- /// The bytes length per line.
- public int BytesPerLine => bytesPerLine;
-
- /// Gets the current cursor position starting at one for both, line and column.
- public Point CursorPosition
+ /// Gets the current edit position.
+ public Point Position
{
get
{
- if (!IsInitialized)
+ if (_source is null || BytesPerLine == 0)
{
return Point.Empty;
}
- var delta = (int)position;
- int line = delta / bytesPerLine + 1;
- int item = delta % bytesPerLine + 1;
+ var delta = (int)Address;
- return new Point (item, line);
+ int line = delta / BytesPerLine;
+ int item = delta % BytesPerLine;
+
+ return new (item, line);
}
}
- ///
- /// Sets or gets the offset into the that will be displayed at the top of the
- ///
- ///
- /// The display start.
- public long DisplayStart
+ ///
+ public override Point? PositionCursor ()
{
- get => displayStart;
- set
- {
- position = value;
+ var delta = (int)(Address - _displayStart);
+ int line = delta / BytesPerLine;
+ int item = delta % BytesPerLine;
+ int block = item / NUM_BYTES_PER_HEX_COLUMN;
+ int column = item % NUM_BYTES_PER_HEX_COLUMN * 3;
- SetDisplayStart (value);
+ int x = GetLeftSideStartColumn () + block * HEX_COLUMN_WIDTH + column + (_firstNibble ? 0 : 1);
+ int y = line;
+
+ if (!_leftSideHasFocus)
+ {
+ x = GetLeftSideStartColumn () + BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH + item - 1;
}
+
+ Move (x, y);
+
+ return new (x, y);
}
+ private SortedDictionary _edits = [];
+
///
/// Gets a describing the edits done to the .
/// Each Key indicates an offset where an edit was made and the Value is the changed byte.
///
/// The edits.
- public IReadOnlyDictionary Edits => edits;
+ public IReadOnlyDictionary Edits => _edits;
- /// Gets the current character position starting at one, related to the .
- public long Position => position + 1;
+ private Stream? _source;
///
/// Sets or gets the the is operating on; the stream must support
/// seeking ( == true).
///
/// The source.
- public Stream Source
+ public Stream? Source
{
- get => source;
+ get => _source;
set
{
- if (value is null)
+ ArgumentNullException.ThrowIfNull (value);
+
+ if (!value!.CanSeek)
{
- throw new ArgumentNullException ("source");
+ throw new ArgumentException (@"The source stream must be seekable (CanSeek property)");
}
- if (!value.CanSeek)
- {
- throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source");
- }
+ _source = value;
- source = value;
-
- if (displayStart > source.Length)
+ if (_displayStart > _source.Length)
{
DisplayStart = 0;
}
- if (position > source.Length)
+ if (Address > _source.Length)
{
- position = 0;
+ Address = 0;
}
SetNeedsDisplay ();
}
}
- private int bytesPerLine
+ private int _bpl;
+
+ /// The bytes length per line.
+ public int BytesPerLine
{
- get => bpl;
+ get => _bpl;
set
{
- bpl = value;
- OnPositionChanged ();
+ _bpl = value;
+ RaisePositionChanged ();
}
}
- private long position
+ private long _address;
+
+ /// Gets or sets the current byte position in the .
+ public long Address
{
- get => pos;
+ get => _address;
set
{
- pos = value;
- OnPositionChanged ();
+ if (_address == value)
+ {
+ return;
+ }
+
+ //ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual (value, Source!.Length, $"Position");
+
+ _address = value;
+ RaisePositionChanged ();
}
}
+ private long _displayStart;
+
+ // TODO: Use Viewport content scrolling instead
+ ///
+ /// Sets or gets the offset into the that will be displayed at the top of the
+ /// .
+ ///
+ /// The display start.
+ public long DisplayStart
+ {
+ get => _displayStart;
+ set
+ {
+ Address = value;
+
+ SetDisplayStart (value);
+ }
+ }
+
+ private int _addressWidth = DEFAULT_ADDRESS_WIDTH;
+
+ ///
+ /// Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8.
+ ///
+ public int AddressWidth
+ {
+ get => _addressWidth;
+ set
+ {
+ if (_addressWidth == value)
+ {
+ return;
+ }
+
+ _addressWidth = value;
+ SetNeedsDisplay ();
+ }
+ }
+
+ private int GetLeftSideStartColumn () { return AddressWidth == 0 ? 0 : AddressWidth + 1; }
+
+ internal void SetDisplayStart (long value)
+ {
+ if (value > 0 && value >= _source?.Length)
+ {
+ _displayStart = _source.Length - 1;
+ }
+ else if (value < 0)
+ {
+ _displayStart = 0;
+ }
+ else
+ {
+ _displayStart = value;
+ }
+
+ SetNeedsDisplay ();
+ }
+
///
- /// This method applies and edits made to the and resets the contents of the
+ /// Applies and edits made to the and resets the contents of the
/// property.
///
- /// If provided also applies the changes to the passed
+ /// If provided also applies the changes to the passed .
/// .
- public void ApplyEdits (Stream stream = null)
+ public void ApplyEdits (Stream? stream = null)
{
- foreach (KeyValuePair kv in edits)
+ foreach (KeyValuePair kv in _edits)
{
- source.Position = kv.Key;
- source.WriteByte (kv.Value);
- source.Flush ();
+ _source!.Position = kv.Key;
+ _source.WriteByte (kv.Value);
+ _source.Flush ();
if (stream is { })
{
@@ -236,22 +316,24 @@ public class HexView : View, IDesignable
}
}
- edits = new SortedDictionary ();
+ _edits = new ();
SetNeedsDisplay ();
}
///
- /// This method discards the edits made to the by resetting the contents of the
+ /// Discards the edits made to the by resetting the contents of the
/// property.
///
- public void DiscardEdits () { edits = new SortedDictionary (); }
-
- /// Event to be invoked when an edit is made on the .
- public event EventHandler Edited;
+ public void DiscardEdits () { _edits = new (); }
///
protected override bool OnMouseEvent (MouseEventArgs me)
{
+ if (_source is null)
+ {
+ return false;
+ }
+
if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
&& !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
&& !me.Flags.HasFlag (MouseFlags.WheeledDown)
@@ -267,67 +349,67 @@ public class HexView : View, IDesignable
if (me.Flags == MouseFlags.WheeledDown)
{
- DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length);
+ DisplayStart = Math.Min (DisplayStart + BytesPerLine, GetEditedSize ());
return true;
}
if (me.Flags == MouseFlags.WheeledUp)
{
- DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0);
+ DisplayStart = Math.Max (DisplayStart - BytesPerLine, 0);
return true;
}
- if (me.Position.X < displayWidth)
+ if (me.Position.X < GetLeftSideStartColumn ())
{
return true;
}
- int nblocks = bytesPerLine / bsize;
- int blocksSize = nblocks * 14;
- int blocksRightOffset = displayWidth + blocksSize - 1;
+ int nblocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
+ int blocksSize = nblocks * HEX_COLUMN_WIDTH;
+ int blocksRightOffset = GetLeftSideStartColumn () + blocksSize - 1;
- if (me.Position.X > blocksRightOffset + bytesPerLine - 1)
+ if (me.Position.X > blocksRightOffset + BytesPerLine - 1)
{
return true;
}
- leftSide = me.Position.X >= blocksRightOffset;
- long lineStart = me.Position.Y * bytesPerLine + displayStart;
- int x = me.Position.X - displayWidth + 1;
- int block = x / 14;
+ bool clickIsOnLeftSide = me.Position.X >= blocksRightOffset;
+ long lineStart = me.Position.Y * BytesPerLine + _displayStart;
+ int x = me.Position.X - GetLeftSideStartColumn () + 1;
+ int block = x / HEX_COLUMN_WIDTH;
x -= block * 2;
int empty = x % 3;
int item = x / 3;
- if (!leftSide && item > 0 && (empty == 0 || x == block * 14 + 14 - 1 - block * 2))
+ if (!clickIsOnLeftSide && item > 0 && (empty == 0 || x == block * HEX_COLUMN_WIDTH + HEX_COLUMN_WIDTH - 1 - block * 2))
{
return true;
}
- firstNibble = true;
+ _firstNibble = true;
- if (leftSide)
+ if (clickIsOnLeftSide)
{
- position = Math.Min (lineStart + me.Position.X - blocksRightOffset, source.Length);
+ Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, GetEditedSize ());
}
else
{
- position = Math.Min (lineStart + item, source.Length);
+ Address = Math.Min (lineStart + item, GetEditedSize ());
}
if (me.Flags == MouseFlags.Button1DoubleClicked)
{
- leftSide = !leftSide;
+ _leftSideHasFocus = !clickIsOnLeftSide;
- if (leftSide)
+ if (_leftSideHasFocus)
{
- firstNibble = empty == 1;
+ _firstNibble = empty == 1;
}
else
{
- firstNibble = true;
+ _firstNibble = true;
}
}
@@ -339,19 +421,24 @@ public class HexView : View, IDesignable
///
public override void OnDrawContent (Rectangle viewport)
{
+ if (Source is null)
+ {
+ return;
+ }
+
Attribute currentAttribute;
- Attribute current = ColorScheme.Focus;
+ Attribute current = GetFocusColor ();
Driver.SetAttribute (current);
Move (0, 0);
- int nblocks = bytesPerLine / bsize;
- var data = new byte [nblocks * bsize * viewport.Height];
- Source.Position = displayStart;
- int n = source.Read (data, 0, data.Length);
-
- Attribute activeColor = ColorScheme.HotNormal;
- Attribute trackingColor = ColorScheme.HotFocus;
+ int nBlocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
+ var data = new byte [nBlocks * NUM_BYTES_PER_HEX_COLUMN * viewport.Height];
+ Source.Position = _displayStart;
+ int n = _source!.Read (data, 0, data.Length);
+ Attribute selectedAttribute = GetHotNormalColor ();
+ Attribute editedAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background);
+ Attribute editingAttribute = new Attribute (GetFocusColor ().Background, GetFocusColor ().Foreground);
for (var line = 0; line < viewport.Height; line++)
{
Rectangle lineRect = new (0, line, viewport.Width, 1);
@@ -362,72 +449,100 @@ public class HexView : View, IDesignable
}
Move (0, line);
- Driver.SetAttribute (ColorScheme.HotNormal);
- Driver.AddStr ($"{displayStart + line * nblocks * bsize:x8} ");
+ currentAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background);
+ Driver.SetAttribute (currentAttribute);
+ var address = $"{_displayStart + line * nBlocks * NUM_BYTES_PER_HEX_COLUMN:x8}";
+ Driver.AddStr ($"{address.Substring (8 - AddressWidth)}");
+
+ if (AddressWidth > 0)
+ {
+ Driver.AddStr (" ");
+ }
- currentAttribute = ColorScheme.HotNormal;
SetAttribute (GetNormalColor ());
- for (var block = 0; block < nblocks; block++)
+ for (var block = 0; block < nBlocks; block++)
{
- for (var b = 0; b < bsize; b++)
+ for (var b = 0; b < NUM_BYTES_PER_HEX_COLUMN; b++)
{
- int offset = line * nblocks * bsize + block * bsize + b;
+ int offset = line * nBlocks * NUM_BYTES_PER_HEX_COLUMN + block * NUM_BYTES_PER_HEX_COLUMN + b;
byte value = GetData (data, offset, out bool edited);
- if (offset + displayStart == position || edited)
+ if (offset + _displayStart == Address)
{
- SetAttribute (leftSide ? activeColor : trackingColor);
+ // Selected
+ SetAttribute (_leftSideHasFocus ? editingAttribute : (edited ? editedAttribute : selectedAttribute));
}
else
{
- SetAttribute (GetNormalColor ());
+ SetAttribute (edited ? editedAttribute : GetNormalColor ());
}
Driver.AddStr (offset >= n && !edited ? " " : $"{value:x2}");
SetAttribute (GetNormalColor ());
- Driver.AddRune (SpaceCharRune);
+ Driver.AddRune (_spaceCharRune);
}
- Driver.AddStr (block + 1 == nblocks ? " " : "| ");
+ Driver.AddStr (block + 1 == nBlocks ? " " : $"{_columnSeparatorRune} ");
}
- for (var bitem = 0; bitem < nblocks * bsize; bitem++)
+ for (var byteIndex = 0; byteIndex < nBlocks * NUM_BYTES_PER_HEX_COLUMN; byteIndex++)
{
- int offset = line * nblocks * bsize + bitem;
+ int offset = line * nBlocks * NUM_BYTES_PER_HEX_COLUMN + byteIndex;
byte b = GetData (data, offset, out bool edited);
Rune c;
+ var utf8BytesConsumed = 0;
+
if (offset >= n && !edited)
{
- c = SpaceCharRune;
+ c = _spaceCharRune;
}
else
{
- if (b < 32)
+ switch (b)
{
- c = PeriodCharRune;
- }
- else if (b > 127)
- {
- c = PeriodCharRune;
- }
- else
- {
- Rune.DecodeFromUtf8 (new ReadOnlySpan (ref b), out c, out _);
+ //case < 32:
+ // c = _periodCharRune;
+
+ // break;
+ case > 127:
+ {
+ var utf8 = GetData (data, offset, 4, out bool _);
+
+ OperationStatus status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed);
+
+ while (status == OperationStatus.NeedMoreData)
+ {
+ status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed);
+ }
+
+ break;
+ }
+ default:
+ Rune.DecodeFromUtf8 (new (ref b), out c, out _);
+
+ break;
}
}
- if (offset + displayStart == position || edited)
+ if (offset + _displayStart == Address)
{
- SetAttribute (leftSide ? trackingColor : activeColor);
+ // Selected
+ SetAttribute (_leftSideHasFocus ? editingAttribute : (edited ? editedAttribute : selectedAttribute));
}
else
{
- SetAttribute (GetNormalColor ());
+ SetAttribute (edited ? editedAttribute : GetNormalColor ());
}
Driver.AddRune (c);
+
+ for (var i = 1; i < utf8BytesConsumed; i++)
+ {
+ byteIndex++;
+ Driver.AddRune (_periodCharRune);
+ }
}
}
@@ -441,44 +556,67 @@ public class HexView : View, IDesignable
}
}
- /// Method used to invoke the event passing the .
- /// The key value pair.
- public virtual void OnEdited (HexViewEditEventArgs e) { Edited?.Invoke (this, e); }
+ /// Raises the event.
+ protected void RaiseEdited (HexViewEditEventArgs e)
+ {
+ OnEdited (e);
+ Edited?.Invoke (this, e);
+ }
+
+ /// Event to be invoked when an edit is made on the .
+ public event EventHandler? Edited;
///
- /// Method used to invoke the event passing the
- /// arguments.
///
- public virtual void OnPositionChanged () { PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); }
+ ///
+ protected virtual void OnEdited (HexViewEditEventArgs e) { }
+
+ ///
+ /// Call this when (and ) has changed. Raises the
+ /// event.
+ ///
+ protected void RaisePositionChanged ()
+ {
+ HexViewEventArgs args = new (Address, Position, BytesPerLine);
+ OnPositionChanged (args);
+ PositionChanged?.Invoke (this, args);
+ }
+
+ ///
+ /// Called when (and ) has changed.
+ ///
+ protected virtual void OnPositionChanged (HexViewEventArgs e) { }
+
+ /// Raised when (and ) has changed.
+ public event EventHandler? PositionChanged;
///
protected override bool OnKeyDownNotHandled (Key keyEvent)
{
- if (!AllowEdits)
+ if (!AllowEdits || _source is null)
{
return false;
}
- // Ignore control characters and other special keys
- if (keyEvent < Key.Space || keyEvent.KeyCode > KeyCode.CharMask)
- {
- return false;
- }
-
- if (leftSide)
+ if (_leftSideHasFocus)
{
int value;
var k = (char)keyEvent.KeyCode;
- if (k >= 'A' && k <= 'F')
+ if (!char.IsAsciiHexDigit ((char)keyEvent.KeyCode))
+ {
+ return false;
+ }
+
+ if (k is >= 'A' and <= 'F')
{
value = k - 'A' + 10;
}
- else if (k >= 'a' && k <= 'f')
+ else if (k is >= 'a' and <= 'f')
{
value = k - 'a' + 10;
}
- else if (k >= '0' && k <= '9')
+ else if (k is >= '0' and <= '9')
{
value = k - '0';
}
@@ -487,78 +625,66 @@ public class HexView : View, IDesignable
return false;
}
- byte b;
-
- if (!edits.TryGetValue (position, out b))
+ if (!_edits.TryGetValue (Address, out byte b))
{
- source.Position = position;
- b = (byte)source.ReadByte ();
+ _source.Position = Address;
+ b = (byte)_source.ReadByte ();
}
- RedisplayLine (position);
+ // BUGBUG: This makes no sense here.
+ RedisplayLine (Address);
- if (firstNibble)
+ if (_firstNibble)
{
- firstNibble = false;
- b = (byte)((b & 0xf) | (value << bsize));
- edits [position] = b;
- OnEdited (new HexViewEditEventArgs (position, edits [position]));
+ _firstNibble = false;
+ b = (byte)((b & 0xf) | (value << NUM_BYTES_PER_HEX_COLUMN));
+ _edits [Address] = b;
+ RaiseEdited (new (Address, _edits [Address]));
}
else
{
b = (byte)((b & 0xf0) | value);
- edits [position] = b;
- OnEdited (new HexViewEditEventArgs (position, edits [position]));
+ _edits [Address] = b;
+ RaiseEdited (new (Address, _edits [Address]));
MoveRight ();
}
return true;
}
- return false;
- }
+ keyEvent = keyEvent.NoAlt.NoCtrl;
+ Rune r = keyEvent.AsRune;
- /// Event to be invoked when the position and cursor position changes.
- public event EventHandler PositionChanged;
-
- ///
- public override Point? PositionCursor ()
- {
- var delta = (int)(position - displayStart);
- int line = delta / bytesPerLine;
- int item = delta % bytesPerLine;
- int block = item / bsize;
- int column = item % bsize * 3;
-
- int x = displayWidth + block * 14 + column + (firstNibble ? 0 : 1);
- int y = line;
-
- if (!leftSide)
+ if (Rune.IsControl (r))
{
- x = displayWidth + bytesPerLine / bsize * 14 + item - 1;
+ return false;
}
- Move (x, y);
+ var utf8 = new byte [4];
- return new (x, y);
- }
+ // If the rune is a wide char, encode as utf8
+ if (r.TryEncodeToUtf8 (utf8, out int bytesWritten))
+ {
+ if (bytesWritten > 1)
+ {
+ bytesWritten = 4;
+ }
- internal void SetDisplayStart (long value)
- {
- if (value > 0 && value >= source.Length)
- {
- displayStart = source.Length - 1;
- }
- else if (value < 0)
- {
- displayStart = 0;
+ for (var utfIndex = 0; utfIndex < bytesWritten; utfIndex++)
+ {
+ _edits [Address] = utf8 [utfIndex];
+ RaiseEdited (new (Address, _edits [Address]));
+ MoveRight ();
+ }
}
else
{
- displayStart = value;
+ _edits [Address] = (byte)r.Value;
+ RaiseEdited (new (Address, _edits [Address]));
+ MoveRight ();
}
- SetNeedsDisplay ();
+ return true;
}
//
@@ -572,7 +698,7 @@ public class HexView : View, IDesignable
{
long pos = DisplayStart + offset;
- if (edits.TryGetValue (pos, out byte v))
+ if (_edits.TryGetValue (pos, out byte v))
{
edited = true;
@@ -584,47 +710,76 @@ public class HexView : View, IDesignable
return buffer [offset];
}
- private void HexView_LayoutComplete (object sender, LayoutEventArgs e)
+ private byte [] GetData (byte [] buffer, int offset, int count, out bool edited)
+ {
+ var returnBytes = new byte [count];
+ edited = false;
+
+ long pos = DisplayStart + offset;
+ for (long i = pos; i < pos + count; i++)
+ {
+ if (_edits.TryGetValue (i, out byte v))
+ {
+ edited = true;
+ returnBytes [i - pos] = v;
+ }
+ else
+ {
+ if (pos < buffer.Length - 1)
+ {
+ returnBytes [i - pos] = buffer [pos];
+ }
+ }
+ }
+
+ return returnBytes;
+ }
+
+ private void HexView_LayoutComplete (object? sender, LayoutEventArgs e)
{
// Small buffers will just show the position, with the bsize field value (4 bytes)
- bytesPerLine = bsize;
+ BytesPerLine = NUM_BYTES_PER_HEX_COLUMN;
- if (Viewport.Width - displayWidth > 17)
+ if (Viewport.Width - GetLeftSideStartColumn () >= HEX_COLUMN_WIDTH)
{
- bytesPerLine = bsize * ((Viewport.Width - displayWidth) / 18);
+ BytesPerLine = Math.Max (
+ NUM_BYTES_PER_HEX_COLUMN,
+ NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / (HEX_COLUMN_WIDTH + NUM_BYTES_PER_HEX_COLUMN)));
}
}
private bool MoveDown (int bytes)
{
- RedisplayLine (position);
+ RedisplayLine (Address);
- if (position + bytes < source.Length)
+ if (Address + bytes < GetEditedSize ())
{
- position += bytes;
+ // We can move down lines cleanly (without extending stream)
+ Address += bytes;
}
- else if ((bytes == bytesPerLine * Viewport.Height && source.Length >= DisplayStart + bytesPerLine * Viewport.Height)
- || (bytes <= bytesPerLine * Viewport.Height - bytesPerLine
- && source.Length <= DisplayStart + bytesPerLine * Viewport.Height))
+ else if ((bytes == BytesPerLine * Viewport.Height && _source!.Length >= DisplayStart + BytesPerLine * Viewport.Height)
+ || (bytes <= BytesPerLine * Viewport.Height - BytesPerLine
+ && _source!.Length <= DisplayStart + BytesPerLine * Viewport.Height))
{
- long p = position;
+ long p = Address;
- while (p + bytesPerLine < source.Length)
+ // This lets address go past the end of the stream one, enabling adding to the stream.
+ while (p + BytesPerLine <= GetEditedSize ())
{
- p += bytesPerLine;
+ p += BytesPerLine;
}
- position = p;
+ Address = p;
}
- if (position >= DisplayStart + bytesPerLine * Viewport.Height)
+ if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
{
SetDisplayStart (DisplayStart + bytes);
SetNeedsDisplay ();
}
else
{
- RedisplayLine (position);
+ RedisplayLine (Address);
}
return true;
@@ -632,16 +787,17 @@ public class HexView : View, IDesignable
private bool MoveEnd ()
{
- position = source.Length;
+ // This lets address go past the end of the stream one, enabling adding to the stream.
+ Address = GetEditedSize ();
- if (position >= DisplayStart + bytesPerLine * Viewport.Height)
+ if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
{
- SetDisplayStart (position);
+ SetDisplayStart (Address);
SetNeedsDisplay ();
}
else
{
- RedisplayLine (position);
+ RedisplayLine (Address);
}
return true;
@@ -649,7 +805,8 @@ public class HexView : View, IDesignable
private bool MoveEndOfLine ()
{
- position = Math.Min (position / bytesPerLine * bytesPerLine + bytesPerLine - 1, source.Length);
+ // This lets address go past the end of the stream one, enabling adding to the stream.
+ Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, GetEditedSize ());
SetNeedsDisplay ();
return true;
@@ -665,77 +822,90 @@ public class HexView : View, IDesignable
private bool MoveLeft ()
{
- RedisplayLine (position);
+ RedisplayLine (Address);
- if (leftSide)
+ if (_leftSideHasFocus)
{
- if (!firstNibble)
+ if (!_firstNibble)
{
- firstNibble = true;
+ _firstNibble = true;
return true;
}
- firstNibble = false;
+ _firstNibble = false;
}
- if (position == 0)
+ if (Address == 0)
{
return true;
}
- if (position - 1 < DisplayStart)
+ if (Address - 1 < DisplayStart)
{
- SetDisplayStart (displayStart - bytesPerLine);
+ SetDisplayStart (_displayStart - BytesPerLine);
SetNeedsDisplay ();
}
else
{
- RedisplayLine (position);
+ RedisplayLine (Address);
}
- position--;
+ Address--;
return true;
}
private bool MoveRight ()
{
- RedisplayLine (position);
+ RedisplayLine (Address);
- if (leftSide)
+ if (_leftSideHasFocus)
{
- if (firstNibble)
+ if (_firstNibble)
{
- firstNibble = false;
+ _firstNibble = false;
return true;
}
- firstNibble = true;
+ _firstNibble = true;
}
- if (position < source.Length)
+ // This lets address go past the end of the stream one, enabling adding to the stream.
+ if (Address < GetEditedSize ())
{
- position++;
+ Address++;
}
- if (position >= DisplayStart + bytesPerLine * Viewport.Height)
+ if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
{
- SetDisplayStart (DisplayStart + bytesPerLine);
+ SetDisplayStart (DisplayStart + BytesPerLine);
SetNeedsDisplay ();
}
else
{
- RedisplayLine (position);
+ RedisplayLine (Address);
}
return true;
}
+ private long GetEditedSize ()
+ {
+ if (_edits.Count == 0)
+ {
+ return _source!.Length;
+ }
+
+ long maxEditAddress = _edits.Keys.Max ();
+
+ return Math.Max (_source!.Length, maxEditAddress + 1);
+ }
+
private bool MoveLeftStart ()
{
- position = position / bytesPerLine * bytesPerLine;
+ Address = Address / BytesPerLine * BytesPerLine;
SetNeedsDisplay ();
return true;
@@ -743,21 +913,21 @@ public class HexView : View, IDesignable
private bool MoveUp (int bytes)
{
- RedisplayLine (position);
+ RedisplayLine (Address);
- if (position - bytes > -1)
+ if (Address - bytes > -1)
{
- position -= bytes;
+ Address -= bytes;
}
- if (position < DisplayStart)
+ if (Address < DisplayStart)
{
SetDisplayStart (DisplayStart - bytes);
SetNeedsDisplay ();
}
else
{
- RedisplayLine (position);
+ RedisplayLine (Address);
}
return true;
@@ -765,54 +935,42 @@ public class HexView : View, IDesignable
private void RedisplayLine (long pos)
{
- if (bytesPerLine == 0)
+ if (BytesPerLine == 0)
{
return;
}
var delta = (int)(pos - DisplayStart);
- int line = delta / bytesPerLine;
+ int line = delta / BytesPerLine;
SetNeedsDisplay (new (0, line, Viewport.Width, 1));
}
- private bool Navigate (NavigationDirection direction)
+ ///
+ protected override bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior)
{
- switch (direction)
+ if (behavior is { } && behavior != TabStop)
{
- case NavigationDirection.Forward:
- if (leftSide)
- {
- leftSide = false;
- RedisplayLine (position);
- firstNibble = true;
+ return false;
+ }
- return true;
- }
+ if ((direction == NavigationDirection.Forward && _leftSideHasFocus)
+ || (direction == NavigationDirection.Backward && !_leftSideHasFocus))
+ {
+ _leftSideHasFocus = !_leftSideHasFocus;
+ RedisplayLine (Address);
+ _firstNibble = true;
- break;
-
- case NavigationDirection.Backward:
- if (!leftSide)
- {
- leftSide = true;
- RedisplayLine (position);
- firstNibble = true;
- return true;
- }
-
-
- break;
+ return true;
}
return false;
}
-
- ///
+ ///
bool IDesignable.EnableForDesign ()
{
- Source = new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"));
+ Source = new MemoryStream (Encoding.UTF8.GetBytes ("HexView data with wide codepoints: 𝔹Aℝ𝔽!"));
return true;
}
diff --git a/Terminal.Gui/Views/HexViewEventArgs.cs b/Terminal.Gui/Views/HexViewEventArgs.cs
index 662d5cc2d..49586aac9 100644
--- a/Terminal.Gui/Views/HexViewEventArgs.cs
+++ b/Terminal.Gui/Views/HexViewEventArgs.cs
@@ -12,41 +12,41 @@ namespace Terminal.Gui;
public class HexViewEventArgs : EventArgs
{
/// Initializes a new instance of
- /// The character position.
- /// The cursor position.
+ /// The byte position in the steam.
+ /// The edit position.
/// Line bytes length.
- public HexViewEventArgs (long pos, Point cursor, int lineLength)
+ public HexViewEventArgs (long address, Point position, int lineLength)
{
- Position = pos;
- CursorPosition = cursor;
+ Address = address;
+ Position = position;
BytesPerLine = lineLength;
}
/// The bytes length per line.
public int BytesPerLine { get; private set; }
- /// Gets the current cursor position starting at one for both, line and column.
- public Point CursorPosition { get; private set; }
+ /// Gets the current edit position.
+ public Point Position { get; private set; }
- /// Gets the current character position starting at one, related to the .
- public long Position { get; private set; }
+ /// Gets the byte position in the .
+ public long Address { get; private set; }
}
/// Defines the event arguments for event.
public class HexViewEditEventArgs : EventArgs
{
/// Creates a new instance of the class.
- ///
+ ///
///
- public HexViewEditEventArgs (long position, byte newValue)
+ public HexViewEditEventArgs (long address, byte newValue)
{
- Position = position;
+ Address = address;
NewValue = newValue;
}
- /// Gets the new value for that .
+ /// Gets the new value for that .
public byte NewValue { get; }
- /// Gets the location of the edit.
- public long Position { get; }
+ /// Gets the address of the edit in the stream.
+ public long Address { get; }
}
diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs
index c1aa85ea4..f23ec23e3 100644
--- a/Terminal.Gui/Views/Shortcut.cs
+++ b/Terminal.Gui/Views/Shortcut.cs
@@ -58,7 +58,7 @@ public class Shortcut : View, IOrientation, IDesignable
///
///
///
- /// The View that will be invoked when user does something that causes the Shortcut's Accept
+ /// The View that will be invoked on when user does something that causes the Shortcut's Accept
/// event to be raised.
///
///
diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs
index 08029a0e7..134f8d410 100644
--- a/UICatalog/Scenarios/HexEditor.cs
+++ b/UICatalog/Scenarios/HexEditor.cs
@@ -16,20 +16,23 @@ public class HexEditor : Scenario
private HexView _hexView;
private MenuItem _miAllowEdits;
private bool _saved = true;
- private Shortcut _siPositionChanged;
+ private Shortcut _scAddress;
+ private Shortcut _scInfo;
+ private Shortcut _scPosition;
private StatusBar _statusBar;
public override void Main ()
{
Application.Init ();
- Toplevel app = new Toplevel ()
+
+ var app = new Toplevel
{
ColorScheme = Colors.ColorSchemes ["Base"]
};
CreateDemoFile (_fileName);
- _hexView = new HexView (new MemoryStream (Encoding.UTF8.GetBytes ("Demo text.")))
+ _hexView = new (new MemoryStream (Encoding.UTF8.GetBytes ("Demo text.")))
{
X = 0,
Y = 1,
@@ -46,71 +49,97 @@ public class HexEditor : Scenario
{
Menus =
[
- new MenuBarItem (
- "_File",
- new MenuItem []
- {
- new ("_New", "", () => New ()),
- new ("_Open", "", () => Open ()),
- new ("_Save", "", () => Save ()),
- null,
- new ("_Quit", "", () => Quit ())
- }
- ),
- new MenuBarItem (
- "_Edit",
- new MenuItem []
- {
- new ("_Copy", "", () => Copy ()),
- new ("C_ut", "", () => Cut ()),
- new ("_Paste", "", () => Paste ())
- }
- ),
- new MenuBarItem (
- "_Options",
- new []
- {
- _miAllowEdits = new MenuItem (
- "_AllowEdits",
- "",
- () => ToggleAllowEdits ()
- )
- {
- Checked = _hexView.AllowEdits,
- CheckType = MenuItemCheckStyle
- .Checked
- }
- }
- )
+ new (
+ "_File",
+ new MenuItem []
+ {
+ new ("_New", "", () => New ()),
+ new ("_Open", "", () => Open ()),
+ new ("_Save", "", () => Save ()),
+ null,
+ new ("_Quit", "", () => Quit ())
+ }
+ ),
+ new (
+ "_Edit",
+ new MenuItem []
+ {
+ new ("_Copy", "", () => Copy ()),
+ new ("C_ut", "", () => Cut ()),
+ new ("_Paste", "", () => Paste ())
+ }
+ ),
+ new (
+ "_Options",
+ new []
+ {
+ _miAllowEdits = new (
+ "_AllowEdits",
+ "",
+ () => ToggleAllowEdits ()
+ )
+ {
+ Checked = _hexView.AllowEdits,
+ CheckType = MenuItemCheckStyle
+ .Checked
+ }
+ }
+ )
]
};
app.Add (menu);
- _statusBar = new StatusBar (
- new []
- {
- new (Key.F2, "Open", () => Open ()),
- new (Key.F3, "Save", () => Save ()),
- new (
- Application.QuitKey,
- $"Quit",
- () => Quit ()
- ),
- _siPositionChanged = new Shortcut (
- Key.Empty,
- $"Position: {
- _hexView.Position
- } Line: {
- _hexView.CursorPosition.Y
- } Col: {
- _hexView.CursorPosition.X
- } Line length: {
- _hexView.BytesPerLine
- }",
- () => { }
- )
- }
- )
+ var addressWidthUpDown = new NumericUpDown
+ {
+ Value = _hexView.AddressWidth
+ };
+
+ NumericUpDown addressUpDown = new NumericUpDown
+ {
+ Value = _hexView.Address,
+ Format = $"0x{{0:X{_hexView.AddressWidth}}}"
+ };
+
+ addressWidthUpDown.ValueChanging += (sender, args) =>
+ {
+ args.Cancel = args.NewValue is < 0 or > 8;
+
+ if (!args.Cancel)
+ {
+ _hexView.AddressWidth = args.NewValue;
+
+ // ReSharper disable once AccessToDisposedClosure
+ addressUpDown.Format = $"0x{{0:X{_hexView.AddressWidth}}}";
+ }
+ };
+
+ addressUpDown.ValueChanging += (sender, args) =>
+ {
+ args.Cancel = args.NewValue is < 0;
+
+ if (!args.Cancel)
+ {
+ _hexView.Address = args.NewValue;
+ }
+ };
+
+ _statusBar = new (
+ [
+ new (Key.F2, "Open", Open),
+ new (Key.F3, "Save", Save),
+ new ()
+ {
+ CommandView = addressWidthUpDown,
+ HelpText = "Address Width"
+ },
+ _scAddress = new ()
+ {
+ CommandView = addressUpDown,
+ HelpText = "Address:"
+ },
+ _scInfo = new (Key.Empty, string.Empty, () => { }),
+ _scPosition = new (Key.Empty, string.Empty, () => { })
+ ])
{
AlignmentModes = AlignmentModes.IgnoreFirstOrLast
};
@@ -119,6 +148,8 @@ public class HexEditor : Scenario
_hexView.Source = LoadFile ();
Application.Run (app);
+ addressUpDown.Dispose ();
+ addressWidthUpDown.Dispose ();
app.Dispose ();
Application.Shutdown ();
}
@@ -127,8 +158,15 @@ public class HexEditor : Scenario
private void _hexView_PositionChanged (object sender, HexViewEventArgs obj)
{
- _siPositionChanged.Title =
- $"Position: {obj.Position} Line: {obj.CursorPosition.Y} Col: {obj.CursorPosition.X} Line length: {obj.BytesPerLine}";
+ _scInfo.Title =
+ $"Bytes: {_hexView.Source!.Length}";
+ _scPosition.Title =
+ $"L: {obj.Position.Y} C: {obj.Position.X} Per Line: {obj.BytesPerLine}";
+
+ if (_scAddress.CommandView is NumericUpDown addrNumericUpDown)
+ {
+ addrNumericUpDown.Value = obj.Address;
+ }
}
private void Copy () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); }
@@ -147,7 +185,7 @@ public class HexEditor : Scenario
private void CreateUnicodeDemoFile (string fileName)
{
var sb = new StringBuilder ();
- sb.Append ("Hello world.\n");
+ sb.Append ("Hello world with wide codepoints: 𝔹Aℝ𝔽.\n");
sb.Append ("This is a test of the Emergency Broadcast System.\n");
byte [] buffer = Encoding.Unicode.GetBytes (sb.ToString ());
@@ -169,8 +207,8 @@ public class HexEditor : Scenario
if (MessageBox.ErrorQuery (
"Save",
"The changes were not saved. Want to open without saving?",
- "Yes",
- "No"
+ "_Yes",
+ "_No"
)
== 1)
{
@@ -190,7 +228,7 @@ public class HexEditor : Scenario
}
else
{
- _hexView.Title = (_fileName ?? "Untitled");
+ _hexView.Title = _fileName ?? "Untitled";
}
return stream;
@@ -213,10 +251,11 @@ public class HexEditor : Scenario
_hexView.Source = LoadFile ();
_hexView.DisplayStart = 0;
}
+
d.Dispose ();
}
- private void Paste () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); }
+ private void Paste () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "_Ok"); }
private void Quit () { Application.RequestStop (); }
private void Save ()
diff --git a/UICatalog/Scenarios/Text.cs b/UICatalog/Scenarios/Text.cs
index 9c5ef0979..84d6fd01c 100644
--- a/UICatalog/Scenarios/Text.cs
+++ b/UICatalog/Scenarios/Text.cs
@@ -177,7 +177,7 @@ public class Text : Scenario
new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"))
)
{
- X = Pos.Right (label) + 1, Y = Pos.Bottom (chxMultiline) + 1, Width = Dim.Percent (50) - 1, Height = Dim.Percent (30)
+ X = Pos.Right (label) + 1, Y = Pos.Bottom (chxMultiline) + 1, Width = Dim.Percent (50) - 1, Height = Dim.Percent (30),
};
win.Add (hexEditor);
diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs
index 3bc368f57..63dc511fc 100644
--- a/UICatalog/UICatalog.cs
+++ b/UICatalog/UICatalog.cs
@@ -387,6 +387,7 @@ public class UICatalogApp
// 'app' closed cleanly.
foreach (Responder? inst in Responder.Instances)
{
+
Debug.Assert (inst.WasDisposed);
}
diff --git a/UnitTests/Application/Application.NavigationTests.cs b/UnitTests/Application/Application.NavigationTests.cs
index 2f5768ace..26732eaca 100644
--- a/UnitTests/Application/Application.NavigationTests.cs
+++ b/UnitTests/Application/Application.NavigationTests.cs
@@ -68,7 +68,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
Application.Navigation.FocusedChanged += ApplicationNavigationOnFocusedChanged;
- Application.Navigation.SetFocused (new ());
+ Application.Navigation.SetFocused (new () { CanFocus = true, HasFocus = true });
Assert.True (raised);
@@ -89,7 +89,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
{
Application.Navigation = new ();
- Application.Top = new()
+ Application.Top = new ()
{
Id = "top",
CanFocus = true
@@ -125,7 +125,7 @@ public class ApplicationNavigationTests (ITestOutputHelper output)
{
Application.Navigation = new ();
- Application.Top = new()
+ Application.Top = new ()
{
Id = "top",
CanFocus = true
diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs
index 215e7e050..9fe01a892 100644
--- a/UnitTests/Application/ApplicationTests.cs
+++ b/UnitTests/Application/ApplicationTests.cs
@@ -939,10 +939,10 @@ public class ApplicationTests
w.Dispose ();
Assert.True (w.WasDisposed);
- exception = Record.Exception (
- () => Application.Run (
- w)); // Invalid - w has been disposed. Run it in debug mode will throw, otherwise the user may want to run it again
- Assert.NotNull (exception);
+ //exception = Record.Exception (
+ // () => Application.Run (
+ // w)); // Invalid - w has been disposed. Run it in debug mode will throw, otherwise the user may want to run it again
+ //Assert.NotNull (exception);
exception = Record.Exception (() => Assert.Equal (string.Empty, w.Title)); // Invalid - w has been disposed and cannot be accessed
Assert.NotNull (exception);
diff --git a/UnitTests/LocalPackagesTests.cs b/UnitTests/LocalPackagesTests.cs
new file mode 100644
index 000000000..5de7b371f
--- /dev/null
+++ b/UnitTests/LocalPackagesTests.cs
@@ -0,0 +1,33 @@
+namespace Terminal.Gui;
+
+public class LocalPackagesTests
+{
+ private readonly string _localPackagesPath;
+
+ public LocalPackagesTests ()
+ {
+ // Define the local_packages path relative to the solution directory
+ _localPackagesPath = Path.Combine (Directory.GetCurrentDirectory (), "..", "..", "..", "..", "local_packages");
+ }
+
+ [Fact]
+ public void LocalPackagesFolderExists ()
+ {
+ Assert.True (Directory.Exists (_localPackagesPath),
+ $"The local_packages folder does not exist: {_localPackagesPath}");
+ }
+
+ [Fact]
+ public void NupkgFilesExist ()
+ {
+ var nupkgFiles = Directory.GetFiles (_localPackagesPath, "*.nupkg");
+ Assert.NotEmpty (nupkgFiles);
+ }
+
+ [Fact]
+ public void SnupkgFilesExist ()
+ {
+ var snupkgFiles = Directory.GetFiles (_localPackagesPath, "*.snupkg");
+ Assert.NotEmpty (snupkgFiles);
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/View/Navigation/NavigationTests.cs b/UnitTests/View/Navigation/NavigationTests.cs
index 19b4757f9..97aef5df5 100644
--- a/UnitTests/View/Navigation/NavigationTests.cs
+++ b/UnitTests/View/Navigation/NavigationTests.cs
@@ -66,7 +66,6 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews
// Try once more (HexView)
Application.RaiseKeyDownEvent (key);
}
-
break;
default:
Application.RaiseKeyDownEvent (Key.Tab);
@@ -78,12 +77,11 @@ public class NavigationTests (ITestOutputHelper _output) : TestsAllViews
{
left = true;
_output.WriteLine ($"{view.GetType ().Name} - {key} Left.");
- view.SetFocus ();
- }
- else
- {
- _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave.");
+
+ break;
}
+
+ _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave.");
}
top.Dispose ();
diff --git a/UnitTests/Views/HexViewTests.cs b/UnitTests/Views/HexViewTests.cs
index 566c4117a..7ab45260b 100644
--- a/UnitTests/Views/HexViewTests.cs
+++ b/UnitTests/Views/HexViewTests.cs
@@ -1,36 +1,65 @@
-using System.Text;
+#nullable enable
+using System.Text;
+using JetBrains.Annotations;
namespace Terminal.Gui.ViewsTests;
public class HexViewTests
{
+ [Theory]
+ [InlineData (0, 4)]
+ [InlineData (4, 4)]
+ [InlineData (8, 4)]
+ [InlineData (35, 4)]
+ [InlineData (36, 8)]
+ [InlineData (37, 8)]
+ [InlineData (41, 8)]
+ [InlineData (54, 12)]
+ [InlineData (55, 12)]
+ [InlineData (71, 12)]
+ [InlineData (72, 16)]
+ [InlineData (73, 16)]
+ public void BytesPerLine_Calculates_Correctly (int width, int expectedBpl)
+ {
+ var hv = new HexView (LoadStream (null, out long _)) { Width = width, Height = 10, AddressWidth = 0 };
+ hv.LayoutSubviews ();
+
+ Assert.Equal (expectedBpl, hv.BytesPerLine);
+ }
+
[Fact]
public void AllowEdits_Edits_ApplyEdits ()
{
- var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 };
+ var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 };
+ Application.Navigation = new ApplicationNavigation ();
+ Application.Top = new Toplevel ();
+ Application.Top.Add (hv);
+ Application.Top.SetFocus ();
// Needed because HexView relies on LayoutComplete to calc sizes
hv.LayoutSubviews ();
+ Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to left side
+
Assert.Empty (hv.Edits);
hv.AllowEdits = false;
- Assert.True (hv.NewKeyDownEvent (Key.Home));
- Assert.False (hv.NewKeyDownEvent (Key.A));
+ Assert.True (Application.RaiseKeyDownEvent (Key.Home));
+ Assert.False (Application.RaiseKeyDownEvent (Key.A));
Assert.Empty (hv.Edits);
- Assert.Equal (126, hv.Source.Length);
+ Assert.Equal (126, hv.Source!.Length);
hv.AllowEdits = true;
- Assert.True (hv.NewKeyDownEvent (Key.D4));
- Assert.True (hv.NewKeyDownEvent (Key.D1));
+ Assert.True (Application.RaiseKeyDownEvent (Key.D4));
+ Assert.True (Application.RaiseKeyDownEvent (Key.D1));
Assert.Single (hv.Edits);
Assert.Equal (65, hv.Edits.ToList () [0].Value);
Assert.Equal ('A', (char)hv.Edits.ToList () [0].Value);
Assert.Equal (126, hv.Source.Length);
// Appends byte
- Assert.True (hv.NewKeyDownEvent (Key.End));
- Assert.True (hv.NewKeyDownEvent (Key.D4));
- Assert.True (hv.NewKeyDownEvent (Key.D2));
+ Assert.True (Application.RaiseKeyDownEvent (Key.End));
+ Assert.True (Application.RaiseKeyDownEvent (Key.D4));
+ Assert.True (Application.RaiseKeyDownEvent (Key.D2));
Assert.Equal (2, hv.Edits.Count);
Assert.Equal (66, hv.Edits.ToList () [1].Value);
Assert.Equal ('B', (char)hv.Edits.ToList () [1].Value);
@@ -39,11 +68,18 @@ public class HexViewTests
hv.ApplyEdits ();
Assert.Empty (hv.Edits);
Assert.Equal (127, hv.Source.Length);
+
+ Application.Top.Dispose ();
+ Application.ResetState (true);
+
}
[Fact]
public void ApplyEdits_With_Argument ()
{
+ Application.Navigation = new ApplicationNavigation ();
+ Application.Top = new Toplevel ();
+
byte [] buffer = Encoding.Default.GetBytes ("Fest");
var original = new MemoryStream ();
original.Write (buffer, 0, buffer.Length);
@@ -53,28 +89,40 @@ public class HexViewTests
original.CopyTo (copy);
copy.Flush ();
var hv = new HexView (copy) { Width = Dim.Fill (), Height = Dim.Fill () };
+ Application.Top.Add (hv);
+ Application.Top.SetFocus ();
// Needed because HexView relies on LayoutComplete to calc sizes
hv.LayoutSubviews ();
- var readBuffer = new byte [hv.Source.Length];
+ var readBuffer = new byte [hv.Source!.Length];
hv.Source.Position = 0;
hv.Source.Read (readBuffer);
Assert.Equal ("Fest", Encoding.Default.GetString (readBuffer));
- Assert.True (hv.NewKeyDownEvent (Key.D5));
- Assert.True (hv.NewKeyDownEvent (Key.D4));
+ Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to left side
+ Assert.True (Application.RaiseKeyDownEvent (Key.D5));
+ Assert.True (Application.RaiseKeyDownEvent (Key.D4));
readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value;
Assert.Equal ("Test", Encoding.Default.GetString (readBuffer));
+ Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to right side
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft));
+ Assert.True (Application.RaiseKeyDownEvent (Key.Z.WithShift));
+ readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value;
+ Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer));
+
hv.ApplyEdits (original);
original.Position = 0;
original.Read (buffer);
copy.Position = 0;
copy.Read (readBuffer);
- Assert.Equal ("Test", Encoding.Default.GetString (buffer));
- Assert.Equal ("Test", Encoding.Default.GetString (readBuffer));
+ Assert.Equal ("Zest", Encoding.Default.GetString (buffer));
+ Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer));
Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer));
+
+ Application.Top.Dispose ();
+ Application.ResetState (true);
}
[Fact]
@@ -94,69 +142,84 @@ public class HexViewTests
}
[Fact]
- [AutoInitShutdown]
- public void CursorPosition_Encoding_Default ()
+ public void Position_Encoding_Default ()
{
- var hv = new HexView (LoadStream ()) { Width = Dim.Fill (), Height = Dim.Fill () };
- var top = new Toplevel ();
- top.Add (hv);
- Application.Begin (top);
+ Application.Navigation = new ApplicationNavigation ();
- Assert.Equal (new (1, 1), hv.CursorPosition);
+ var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 };
+ Application.Top = new Toplevel ();
+ Application.Top.Add (hv);
- Assert.True (hv.NewKeyDownEvent (Key.Tab));
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
- Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine);
- Assert.True (hv.NewKeyDownEvent (Key.Home));
+ Application.Top.LayoutSubviews ();
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (new (2, 1), hv.CursorPosition);
+ Assert.Equal (63, hv.Source!.Length);
+ Assert.Equal (20, hv.BytesPerLine);
- Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
- Assert.Equal (new (2, 2), hv.CursorPosition);
+ Assert.Equal (new (0, 0), hv.Position);
- Assert.True (hv.NewKeyDownEvent (Key.End));
- int col = hv.CursorPosition.X;
- int line = hv.CursorPosition.Y;
- int offset = (line - 1) * (hv.BytesPerLine - col);
- Assert.Equal (hv.Position, col * line + offset);
- top.Dispose ();
+ Assert.True (Application.RaiseKeyDownEvent (Key.Tab));
+ Assert.Equal (new (0, 0), hv.Position);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl));
+ Assert.Equal (hv.BytesPerLine - 1, hv.Position.X);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.Home));
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight));
+ Assert.Equal (new (1, 0), hv.Position);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
+ Assert.Equal (new (1, 1), hv.Position);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.End));
+ Assert.Equal (new (3, 3), hv.Position);
+
+ Assert.Equal (hv.Source!.Length, hv.Address);
+ Application.Top.Dispose ();
+ Application.ResetState (true);
}
[Fact]
- [AutoInitShutdown]
- public void CursorPosition_Encoding_Unicode ()
+ public void Position_Encoding_Unicode ()
{
- var hv = new HexView (LoadStream (true)) { Width = Dim.Fill (), Height = Dim.Fill () };
- var top = new Toplevel ();
- top.Add (hv);
- Application.Begin (top);
+ Application.Navigation = new ApplicationNavigation ();
- Assert.Equal (new (1, 1), hv.CursorPosition);
+ var hv = new HexView (LoadStream (null, out _, unicode: true)) { Width = 100, Height = 100 };
+ Application.Top = new Toplevel ();
+ Application.Top.Add (hv);
- Assert.True (hv.NewKeyDownEvent (Key.Tab));
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
- Assert.Equal (hv.CursorPosition.X, hv.BytesPerLine);
- Assert.True (hv.NewKeyDownEvent (Key.Home));
+ hv.LayoutSubviews ();
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (new (2, 1), hv.CursorPosition);
+ Assert.Equal (126, hv.Source!.Length);
+ Assert.Equal (20, hv.BytesPerLine);
- Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
- Assert.Equal (new (2, 2), hv.CursorPosition);
+ Assert.Equal (new (0, 0), hv.Position);
- Assert.True (hv.NewKeyDownEvent (Key.End));
- int col = hv.CursorPosition.X;
- int line = hv.CursorPosition.Y;
- int offset = (line - 1) * (hv.BytesPerLine - col);
- Assert.Equal (hv.Position, col * line + offset);
- top.Dispose ();
+ Assert.True (Application.RaiseKeyDownEvent (Key.Tab));
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl));
+ Assert.Equal (hv.BytesPerLine - 1, hv.Position.X);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.Home));
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight));
+ Assert.Equal (new (1, 0), hv.Position);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
+ Assert.Equal (new (1, 1), hv.Position);
+
+ Assert.True (Application.RaiseKeyDownEvent (Key.End));
+ Assert.Equal (new (6, 6), hv.Position);
+
+ Assert.Equal (hv.Source!.Length, hv.Address);
+ Application.Top.Dispose ();
+ Application.ResetState (true);
}
[Fact]
public void DiscardEdits_Method ()
{
- var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 };
+ var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 };
// Needed because HexView relies on LayoutComplete to calc sizes
hv.LayoutSubviews ();
@@ -166,7 +229,7 @@ public class HexViewTests
Assert.Single (hv.Edits);
Assert.Equal (65, hv.Edits.ToList () [0].Value);
Assert.Equal ('A', (char)hv.Edits.ToList () [0].Value);
- Assert.Equal (126, hv.Source.Length);
+ Assert.Equal (126, hv.Source!.Length);
hv.DiscardEdits ();
Assert.Empty (hv.Edits);
@@ -175,7 +238,7 @@ public class HexViewTests
[Fact]
public void DisplayStart_Source ()
{
- var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 };
+ var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 };
// Needed because HexView relies on LayoutComplete to calc sizes
hv.LayoutSubviews ();
@@ -184,7 +247,7 @@ public class HexViewTests
Assert.True (hv.NewKeyDownEvent (Key.PageDown));
Assert.Equal (4 * hv.Frame.Height, hv.DisplayStart);
- Assert.Equal (hv.Source.Length, hv.Source.Position);
+ Assert.Equal (hv.Source!.Length, hv.Source.Position);
Assert.True (hv.NewKeyDownEvent (Key.End));
@@ -196,13 +259,13 @@ public class HexViewTests
[Fact]
public void Edited_Event ()
{
- var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 };
+ var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 };
// Needed because HexView relies on LayoutComplete to calc sizes
hv.LayoutSubviews ();
KeyValuePair keyValuePair = default;
- hv.Edited += (s, e) => keyValuePair = new (e.Position, e.NewValue);
+ hv.Edited += (s, e) => keyValuePair = new (e.Address, e.NewValue);
Assert.True (hv.NewKeyDownEvent (Key.D4));
Assert.True (hv.NewKeyDownEvent (Key.D6));
@@ -220,231 +283,168 @@ public class HexViewTests
}
[Fact]
- [AutoInitShutdown]
- public void KeyBindings_Command ()
+ public void KeyBindings_Test_Movement_LeftSide ()
{
- var hv = new HexView (LoadStream ()) { Width = 20, Height = 10 };
- var top = new Toplevel ();
- top.Add (hv);
- Application.Begin (top);
+ Application.Navigation = new ApplicationNavigation ();
+ Application.Top = new Toplevel ();
+ var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 };
+ Application.Top.Add (hv);
- Assert.Equal (63, hv.Source.Length);
- Assert.Equal (1, hv.Position);
+ hv.LayoutSubviews ();
+
+ Assert.Equal (MEM_STRING_LENGTH, hv.Source!.Length);
+ Assert.Equal (0, hv.Address);
Assert.Equal (4, hv.BytesPerLine);
- // right side only needed to press one time
- Assert.True (hv.NewKeyDownEvent (Key.Tab));
+ // Default internal focus is on right side. Move back to left.
+ Assert.True (Application.RaiseKeyDownEvent (Key.Tab.WithShift));
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (2, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight));
+ Assert.Equal (1, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.CursorLeft));
- Assert.Equal (1, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft));
+ Assert.Equal (0, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
- Assert.Equal (5, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
+ Assert.Equal (4, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.CursorUp));
- Assert.Equal (1, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp));
+ Assert.Equal (0, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.V.WithCtrl));
- Assert.Equal (41, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.PageDown));
+ Assert.Equal (40, hv.Address);
- Assert.True (hv.NewKeyDownEvent (new (Key.V.WithAlt)));
- Assert.Equal (1, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.PageUp));
+ Assert.Equal (0, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.PageDown));
- Assert.Equal (41, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.End));
+ Assert.Equal (MEM_STRING_LENGTH, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.PageUp));
- Assert.Equal (1, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.Home));
+ Assert.Equal (0, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.End));
- Assert.Equal (64, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl));
+ Assert.Equal (3, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.Home));
- Assert.Equal (1, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft.WithCtrl));
+ Assert.Equal (0, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight.WithCtrl));
- Assert.Equal (4, hv.Position);
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown.WithCtrl));
+ Assert.Equal (36, hv.Address);
- Assert.True (hv.NewKeyDownEvent (Key.CursorLeft.WithCtrl));
- Assert.Equal (1, hv.Position);
-
- Assert.True (hv.NewKeyDownEvent (Key.CursorDown.WithCtrl));
- Assert.Equal (37, hv.Position);
-
- Assert.True (hv.NewKeyDownEvent (Key.CursorUp.WithCtrl));
- Assert.Equal (1, hv.Position);
- top.Dispose ();
+ Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp.WithCtrl));
+ Assert.Equal (0, hv.Address);
+ Application.Top.Dispose ();
+ Application.ResetState (true);
}
[Fact]
- public void Position_Using_Encoding_Default ()
- {
- var hv = new HexView (LoadStream ()) { Width = 20, Height = 20 };
-
- // Needed because HexView relies on LayoutComplete to calc sizes
- hv.LayoutSubviews ();
- Assert.Equal (63, hv.Source.Length);
- Assert.Equal (63, hv.Source.Position);
- Assert.Equal (1, hv.Position);
-
- // left side needed to press twice
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (63, hv.Source.Position);
- Assert.Equal (1, hv.Position);
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (63, hv.Source.Position);
- Assert.Equal (2, hv.Position);
-
- // right side only needed to press one time
- Assert.True (hv.NewKeyDownEvent (Key.Tab));
- Assert.Equal (63, hv.Source.Position);
- Assert.Equal (2, hv.Position);
- Assert.True (hv.NewKeyDownEvent (Key.CursorLeft));
- Assert.Equal (63, hv.Source.Position);
- Assert.Equal (1, hv.Position);
-
- // last position is equal to the source length
- Assert.True (hv.NewKeyDownEvent (Key.End));
- Assert.Equal (63, hv.Source.Position);
- Assert.Equal (64, hv.Position);
- Assert.Equal (hv.Position - 1, hv.Source.Length);
- }
-
- [Fact]
- public void Position_Using_Encoding_Unicode ()
- {
- var hv = new HexView (LoadStream (true)) { Width = 20, Height = 20 };
-
- // Needed because HexView relies on LayoutComplete to calc sizes
- hv.LayoutSubviews ();
- Assert.Equal (126, hv.Source.Length);
- Assert.Equal (126, hv.Source.Position);
- Assert.Equal (1, hv.Position);
-
- // left side needed to press twice
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (126, hv.Source.Position);
- Assert.Equal (1, hv.Position);
- Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
- Assert.Equal (126, hv.Source.Position);
- Assert.Equal (2, hv.Position);
-
- // right side only needed to press one time
- Assert.True (hv.NewKeyDownEvent (Key.Tab));
- Assert.Equal (126, hv.Source.Position);
- Assert.Equal (2, hv.Position);
- Assert.True (hv.NewKeyDownEvent (Key.CursorLeft));
- Assert.Equal (126, hv.Source.Position);
- Assert.Equal (1, hv.Position);
-
- // last position is equal to the source length
- Assert.True (hv.NewKeyDownEvent (Key.End));
- Assert.Equal (126, hv.Source.Position);
- Assert.Equal (127, hv.Position);
- Assert.Equal (hv.Position - 1, hv.Source.Length);
- }
-
- [Fact]
- [AutoInitShutdown]
public void PositionChanged_Event ()
{
- var hv = new HexView (LoadStream ()) { Width = Dim.Fill (), Height = Dim.Fill () };
- HexViewEventArgs hexViewEventArgs = null;
+ var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 };
+ Application.Top = new Toplevel ();
+ Application.Top.Add (hv);
+
+ Application.Top.LayoutSubviews ();
+
+ HexViewEventArgs hexViewEventArgs = null!;
hv.PositionChanged += (s, e) => hexViewEventArgs = e;
- var top = new Toplevel ();
- top.Add (hv);
- Application.Begin (top);
+
+ Assert.Equal (4, hv.BytesPerLine);
Assert.True (hv.NewKeyDownEvent (Key.CursorRight)); // left side must press twice
Assert.True (hv.NewKeyDownEvent (Key.CursorRight));
Assert.True (hv.NewKeyDownEvent (Key.CursorDown));
- Assert.Equal (12, hexViewEventArgs.BytesPerLine);
- Assert.Equal (new (2, 2), hexViewEventArgs.CursorPosition);
- Assert.Equal (14, hexViewEventArgs.Position);
- top.Dispose ();
+ Assert.Equal (4, hexViewEventArgs.BytesPerLine);
+ Assert.Equal (new (1, 1), hexViewEventArgs.Position);
+ Assert.Equal (5, hexViewEventArgs.Address);
+ Application.Top.Dispose ();
+ Application.ResetState (true);
}
[Fact]
- [AutoInitShutdown]
public void Source_Sets_DisplayStart_And_Position_To_Zero_If_Greater_Than_Source_Length ()
{
- var hv = new HexView (LoadStream ()) { Width = 10, Height = 5 };
- var top = new Toplevel ();
- top.Add (hv);
- Application.Begin (top);
+ var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 };
+ Application.Top = new Toplevel ();
+ Application.Top.Add (hv);
+
+ hv.LayoutSubviews ();
Assert.True (hv.NewKeyDownEvent (Key.End));
- Assert.Equal (62, hv.DisplayStart);
- Assert.Equal (64, hv.Position);
+ Assert.Equal (MEM_STRING_LENGTH - 1, hv.DisplayStart);
+ Assert.Equal (MEM_STRING_LENGTH, hv.Address);
hv.Source = new MemoryStream ();
Assert.Equal (0, hv.DisplayStart);
- Assert.Equal (0, hv.Position - 1);
+ Assert.Equal (0, hv.Address);
- hv.Source = LoadStream ();
+ hv.Source = LoadStream (null, out _);
hv.Width = Dim.Fill ();
hv.Height = Dim.Fill ();
- top.LayoutSubviews ();
+ Application.Top.LayoutSubviews ();
Assert.Equal (0, hv.DisplayStart);
- Assert.Equal (0, hv.Position - 1);
+ Assert.Equal (0, hv.Address);
Assert.True (hv.NewKeyDownEvent (Key.End));
Assert.Equal (0, hv.DisplayStart);
- Assert.Equal (64, hv.Position);
+ Assert.Equal (MEM_STRING_LENGTH, hv.Address);
hv.Source = new MemoryStream ();
Assert.Equal (0, hv.DisplayStart);
- Assert.Equal (0, hv.Position - 1);
- top.Dispose ();
+ Assert.Equal (0, hv.Address);
+ Application.Top.Dispose ();
+ Application.ResetState (true);
}
- private Stream LoadStream (bool unicode = false)
+ private const string MEM_STRING = "Hello world.\nThis is a test of the Emergency Broadcast System.\n";
+ private const int MEM_STRING_LENGTH = 63;
+
+ private Stream LoadStream (string? memString, out long numBytesInMemString, bool unicode = false)
{
var stream = new MemoryStream ();
byte [] bArray;
- var memString = "Hello world.\nThis is a test of the Emergency Broadcast System.\n";
- Assert.Equal (63, memString.Length);
+ Assert.Equal (MEM_STRING_LENGTH, MEM_STRING.Length);
+
+ if (memString is null)
+ {
+ memString = MEM_STRING;
+ }
if (unicode)
{
bArray = Encoding.Unicode.GetBytes (memString);
- Assert.Equal (126, bArray.Length);
}
else
{
bArray = Encoding.Default.GetBytes (memString);
- Assert.Equal (63, bArray.Length);
}
+ numBytesInMemString = bArray.Length;
stream.Write (bArray);
return stream;
}
- private class NonSeekableStream : Stream
+ private class NonSeekableStream (Stream baseStream) : Stream
{
- private readonly Stream m_stream;
- public NonSeekableStream (Stream baseStream) { m_stream = baseStream; }
- public override bool CanRead => m_stream.CanRead;
+ public override bool CanRead => baseStream.CanRead;
public override bool CanSeek => false;
- public override bool CanWrite => m_stream.CanWrite;
+ public override bool CanWrite => baseStream.CanWrite;
public override long Length => throw new NotSupportedException ();
public override long Position
{
- get => m_stream.Position;
+ get => baseStream.Position;
set => throw new NotSupportedException ();
}
- public override void Flush () { m_stream.Flush (); }
- public override int Read (byte [] buffer, int offset, int count) { return m_stream.Read (buffer, offset, count); }
+ public override void Flush () { baseStream.Flush (); }
+ public override int Read (byte [] buffer, int offset, int count) { return baseStream.Read (buffer, offset, count); }
public override long Seek (long offset, SeekOrigin origin) { throw new NotImplementedException (); }
public override void SetLength (long value) { throw new NotSupportedException (); }
- public override void Write (byte [] buffer, int offset, int count) { m_stream.Write (buffer, offset, count); }
+ public override void Write (byte [] buffer, int offset, int count) { baseStream.Write (buffer, offset, count); }
}
}
diff --git a/UnitTests/Views/ScrollViewTests.cs b/UnitTests/Views/ScrollViewTests.cs
index b179cd0ca..e7caf1d31 100644
--- a/UnitTests/Views/ScrollViewTests.cs
+++ b/UnitTests/Views/ScrollViewTests.cs
@@ -874,7 +874,8 @@ public class ScrollViewTests (ITestOutputHelper output)
X = 3,
Y = 3,
Width = 10,
- Height = 10
+ Height = 10,
+ TabStop = TabBehavior.TabStop
};
sv.SetContentSize (new (50, 50));
diff --git a/docfx/docs/cursor.md b/docfx/docs/cursor.md
index 7b6de830f..8e7299396 100644
--- a/docfx/docs/cursor.md
+++ b/docfx/docs/cursor.md
@@ -4,19 +4,19 @@ See end for list of issues this design addresses.
## Tenets for Cursor Support (Unless you know better ones...)
-1. **More GUI than Command Line**. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keybaord import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a `ColorPicker`). Terminal.Gui's cursor system is biased towards the broader TUI experiences.
+1. **More GUI than Command Line**. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keyboard import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a `ColorPicker`). Terminal.Gui's cursor system is biased towards the broader TUI experiences.
2. **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and the cursor should behave in a way consistent with the terminal.
## Lexicon & Taxonomy
- Navigation - Refers to the user-experience for moving Focus between views in the application view-hierarchy. See [Navigation](navigation.md) for a deep-dive.
-- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-heirachy in an applicstion can have focus (`view.HasFocus == true`), and there is only one View in a focused heirarchy that is the most-focused; the one recieving keyboard input. See [Navigation](navigation.md) for a deep-dive.
-- Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal sesssion.
+- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-hexarchy in an application can have focus (`view.HasFocus == true`), and there is only one View in a focused hierarchy that is the most-focused; the one receiving keyboard input. See [Navigation](navigation.md) for a deep-dive.
+- Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal session.
- Cursor Location - The top-left corner of the Cursor. In text entry scenarios, new text will be inserted to the left/top of the Cursor Location.
- Cursor Size - The width and height of the cursor. Currently the size is limited to 1x1.
- Cursor Style - How the cursor renders. Some terminals support various cursor styles such as Block and Underline.
-- Cursor Visibilty - Whether the cursor is visible to the user or not. NOTE: Some ConsoleDrivers overload Cursor Style and Cursor Visibility, making "invisible" a style. Terminal.Gui HIDES this from developers and changing the visibilty of the cursor does NOT change the style.
+- Cursor Visibility - Whether the cursor is visible to the user or not. NOTE: Some ConsoleDrivers overload Cursor Style and Cursor Visibility, making "invisible" a style. Terminal.Gui HIDES this from developers and changing the visibility of the cursor does NOT change the style.
- Caret - Visual indicator that where text entry will occur.
- Selection - A visual indicator to the user that something is selected. It is common for the Selection and Cursor to be the same. It is also common for the Selection and Cursor to be distinct. In a `ListView` the Cursor and Selection (`SelectedItem`) are the same, but the `Cursor` is not visible. In a `TextView` with text selected, the `Cursor` is at either the start or end of the `Selection`. A `TableView' supports mutliple things being selected at once.
diff --git a/local_packages/Terminal.Gui.2.0.0.nupkg b/local_packages/Terminal.Gui.2.0.0.nupkg
new file mode 100644
index 000000000..c0ee67dd0
Binary files /dev/null and b/local_packages/Terminal.Gui.2.0.0.nupkg differ
diff --git a/local_packages/Terminal.Gui.2.0.0.snupkg b/local_packages/Terminal.Gui.2.0.0.snupkg
new file mode 100644
index 000000000..301976b83
Binary files /dev/null and b/local_packages/Terminal.Gui.2.0.0.snupkg differ