|
@@ -1,353 +1,38 @@
|
|
|
-#define OTHER_CONTROLS
|
|
|
-
|
|
|
+#nullable enable
|
|
|
using System;
|
|
|
-using System.Collections.Generic;
|
|
|
using System.Globalization;
|
|
|
using System.Linq;
|
|
|
using System.Net.Http;
|
|
|
-using System.Reflection;
|
|
|
using System.Text;
|
|
|
using System.Text.Json;
|
|
|
-using System.Text.Unicode;
|
|
|
-using System.Threading.Tasks;
|
|
|
using Terminal.Gui;
|
|
|
-using static Terminal.Gui.SpinnerStyle;
|
|
|
|
|
|
namespace UICatalog.Scenarios;
|
|
|
|
|
|
/// <summary>
|
|
|
-/// This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
|
|
|
-/// "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
|
|
|
-/// Illustrates how to do infinite scrolling
|
|
|
+/// A scrollable map of the Unicode codepoints.
|
|
|
/// </summary>
|
|
|
-[ScenarioMetadata ("Character Map", "Unicode viewer demonstrating infinite content, scrolling, and Unicode.")]
|
|
|
-[ScenarioCategory ("Text and Formatting")]
|
|
|
-[ScenarioCategory ("Drawing")]
|
|
|
-[ScenarioCategory ("Controls")]
|
|
|
-[ScenarioCategory ("Layout")]
|
|
|
-[ScenarioCategory ("Scrolling")]
|
|
|
-public class CharacterMap : Scenario
|
|
|
+/// <remarks>
|
|
|
+/// See <see href="CharacterMap/README.md"/> for details.
|
|
|
+/// </remarks>
|
|
|
+public class CharMap : View, IDesignable
|
|
|
{
|
|
|
- public Label _errorLabel;
|
|
|
- private TableView _categoryList;
|
|
|
- private CharMap _charMap;
|
|
|
-
|
|
|
- // Don't create a Window, just return the top-level view
|
|
|
- public override void Main ()
|
|
|
- {
|
|
|
- Application.Init ();
|
|
|
-
|
|
|
- var top = new Window
|
|
|
- {
|
|
|
- BorderStyle = LineStyle.None
|
|
|
- };
|
|
|
-
|
|
|
- _charMap = new ()
|
|
|
- {
|
|
|
- X = 0,
|
|
|
- Y = 0,
|
|
|
- Width = Dim.Fill (Dim.Func (() => _categoryList.Frame.Width)),
|
|
|
- Height = Dim.Fill ()
|
|
|
- };
|
|
|
- top.Add (_charMap);
|
|
|
-
|
|
|
-#if OTHER_CONTROLS
|
|
|
- _charMap.Y = 1;
|
|
|
-
|
|
|
- var jumpLabel = new Label
|
|
|
- {
|
|
|
- X = Pos.Right (_charMap) + 1,
|
|
|
- Y = Pos.Y (_charMap),
|
|
|
- HotKeySpecifier = (Rune)'_',
|
|
|
- Text = "_Jump To Code Point:"
|
|
|
- };
|
|
|
- top.Add (jumpLabel);
|
|
|
-
|
|
|
- var jumpEdit = new TextField
|
|
|
- {
|
|
|
- X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3"
|
|
|
- };
|
|
|
- top.Add (jumpEdit);
|
|
|
-
|
|
|
- _errorLabel = new ()
|
|
|
- {
|
|
|
- X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"], Text = "err"
|
|
|
- };
|
|
|
- top.Add (_errorLabel);
|
|
|
-
|
|
|
- jumpEdit.Accepting += JumpEditOnAccept;
|
|
|
-
|
|
|
- _categoryList = new () { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () };
|
|
|
- _categoryList.FullRowSelect = true;
|
|
|
- _categoryList.MultiSelect = false;
|
|
|
-
|
|
|
- //jumpList.Style.ShowHeaders = false;
|
|
|
- //jumpList.Style.ShowHorizontalHeaderOverline = false;
|
|
|
- //jumpList.Style.ShowHorizontalHeaderUnderline = false;
|
|
|
- _categoryList.Style.ShowHorizontalBottomline = true;
|
|
|
-
|
|
|
- //jumpList.Style.ShowVerticalCellLines = false;
|
|
|
- //jumpList.Style.ShowVerticalHeaderLines = false;
|
|
|
- _categoryList.Style.AlwaysShowHeaders = true;
|
|
|
-
|
|
|
- var isDescending = false;
|
|
|
-
|
|
|
- _categoryList.Table = CreateCategoryTable (0, isDescending);
|
|
|
-
|
|
|
- // if user clicks the mouse in TableView
|
|
|
- _categoryList.MouseClick += (s, e) =>
|
|
|
- {
|
|
|
- _categoryList.ScreenToCell (e.Position, out int? clickedCol);
|
|
|
-
|
|
|
- if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked))
|
|
|
- {
|
|
|
- EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
|
|
|
- string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
|
|
|
- isDescending = !isDescending;
|
|
|
-
|
|
|
- _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
|
|
|
-
|
|
|
- table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
|
|
|
-
|
|
|
- _categoryList.SelectedRow = table.Data
|
|
|
- .Select ((item, index) => new { item, index })
|
|
|
- .FirstOrDefault (x => x.item.Category == prevSelection)
|
|
|
- ?.index
|
|
|
- ?? -1;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
|
|
|
-
|
|
|
- _categoryList.Style.ColumnStyles.Add (
|
|
|
- 0,
|
|
|
- new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
|
|
|
- );
|
|
|
- _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
|
|
|
- _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
|
|
|
-
|
|
|
- _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
|
|
|
-
|
|
|
- _categoryList.SelectedCellChanged += (s, args) =>
|
|
|
- {
|
|
|
- EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
|
|
|
- _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
|
|
|
- };
|
|
|
-
|
|
|
- top.Add (_categoryList);
|
|
|
-
|
|
|
- var menu = new MenuBar
|
|
|
- {
|
|
|
- Menus =
|
|
|
- [
|
|
|
- new (
|
|
|
- "_File",
|
|
|
- new MenuItem []
|
|
|
- {
|
|
|
- new (
|
|
|
- "_Quit",
|
|
|
- $"{Application.QuitKey}",
|
|
|
- () => Application.RequestStop ()
|
|
|
- )
|
|
|
- }
|
|
|
- ),
|
|
|
- new (
|
|
|
- "_Options",
|
|
|
- new [] { CreateMenuShowWidth () }
|
|
|
- )
|
|
|
- ]
|
|
|
- };
|
|
|
- top.Add (menu);
|
|
|
-#endif // OTHER_CONTROLS
|
|
|
-
|
|
|
- _charMap.SelectedCodePoint = 0;
|
|
|
- _charMap.SetFocus ();
|
|
|
-
|
|
|
- Application.Run (top);
|
|
|
- top.Dispose ();
|
|
|
- Application.Shutdown ();
|
|
|
-
|
|
|
- return;
|
|
|
-
|
|
|
- void JumpEditOnAccept (object sender, CommandEventArgs e)
|
|
|
- {
|
|
|
- if (jumpEdit.Text.Length == 0)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- uint result = 0;
|
|
|
-
|
|
|
- if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
|
|
|
- }
|
|
|
- catch (FormatException)
|
|
|
- {
|
|
|
- _errorLabel.Text = "Invalid hex value";
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
|
|
|
- }
|
|
|
- catch (FormatException)
|
|
|
- {
|
|
|
- _errorLabel.Text = "Invalid hex value";
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
|
|
|
- }
|
|
|
- catch (FormatException)
|
|
|
- {
|
|
|
- _errorLabel.Text = "Invalid value";
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (result > RuneExtensions.MaxUnicodeCodePoint)
|
|
|
- {
|
|
|
- _errorLabel.Text = "Beyond maximum codepoint";
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- _errorLabel.Text = $"U+{result:x5}";
|
|
|
-
|
|
|
- EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
|
|
|
-
|
|
|
- _categoryList.SelectedRow = table.Data
|
|
|
- .Select ((item, index) => new { item, index })
|
|
|
- .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
|
|
|
- ?.index
|
|
|
- ?? -1;
|
|
|
- _categoryList.EnsureSelectedCellIsVisible ();
|
|
|
-
|
|
|
- // Ensure the typed glyph is selected
|
|
|
- _charMap.SelectedCodePoint = (int)result;
|
|
|
-
|
|
|
- // Cancel the event to prevent ENTER from being handled elsewhere
|
|
|
- e.Cancel = true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
|
|
|
- {
|
|
|
- Func<UnicodeRange, object> orderBy;
|
|
|
- var categorySort = string.Empty;
|
|
|
- var startSort = string.Empty;
|
|
|
- var endSort = string.Empty;
|
|
|
-
|
|
|
- string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString ();
|
|
|
-
|
|
|
- switch (sortByColumn)
|
|
|
- {
|
|
|
- case 0:
|
|
|
- orderBy = r => r.Category;
|
|
|
- categorySort = sortIndicator;
|
|
|
-
|
|
|
- break;
|
|
|
- case 1:
|
|
|
- orderBy = r => r.Start;
|
|
|
- startSort = sortIndicator;
|
|
|
-
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- orderBy = r => r.End;
|
|
|
- endSort = sortIndicator;
|
|
|
-
|
|
|
- break;
|
|
|
- default:
|
|
|
- throw new ArgumentException ("Invalid column number.");
|
|
|
- }
|
|
|
-
|
|
|
- IOrderedEnumerable<UnicodeRange> sortedRanges = descending
|
|
|
- ? UnicodeRange.Ranges.OrderByDescending (orderBy)
|
|
|
- : UnicodeRange.Ranges.OrderBy (orderBy);
|
|
|
-
|
|
|
- return new (
|
|
|
- sortedRanges,
|
|
|
- new ()
|
|
|
- {
|
|
|
- { $"Category{categorySort}", s => s.Category },
|
|
|
- { $"Start{startSort}", s => $"{s.Start:x5}" },
|
|
|
- { $"End{endSort}", s => $"{s.End:x5}" }
|
|
|
- }
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- private MenuItem CreateMenuShowWidth ()
|
|
|
- {
|
|
|
- var item = new MenuItem { Title = "_Show Glyph Width" };
|
|
|
- item.CheckType |= MenuItemCheckStyle.Checked;
|
|
|
- item.Checked = _charMap?.ShowGlyphWidths;
|
|
|
- item.Action += () => { _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked); };
|
|
|
-
|
|
|
- return item;
|
|
|
- }
|
|
|
-
|
|
|
- public override List<Key> GetDemoKeyStrokes ()
|
|
|
- {
|
|
|
- List<Key> keys = new ();
|
|
|
-
|
|
|
- for (var i = 0; i < 200; i++)
|
|
|
- {
|
|
|
- keys.Add (Key.CursorDown);
|
|
|
- }
|
|
|
-
|
|
|
- // Category table
|
|
|
- keys.Add (Key.Tab.WithShift);
|
|
|
-
|
|
|
- // Block elements
|
|
|
- keys.Add (Key.B);
|
|
|
- keys.Add (Key.L);
|
|
|
-
|
|
|
- keys.Add (Key.Tab);
|
|
|
-
|
|
|
- for (var i = 0; i < 200; i++)
|
|
|
- {
|
|
|
- keys.Add (Key.CursorLeft);
|
|
|
- }
|
|
|
-
|
|
|
- return keys;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-internal class CharMap : View, IDesignable
|
|
|
-{
|
|
|
- private const int COLUMN_WIDTH = 3;
|
|
|
+ private const int COLUMN_WIDTH = 3; // Width of each column of glyphs
|
|
|
+ private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested
|
|
|
|
|
|
private ContextMenu _contextMenu = new ();
|
|
|
- private int _rowHeight = 1;
|
|
|
- private int _selected;
|
|
|
- private int _start;
|
|
|
-
|
|
|
private readonly ScrollBar _vScrollBar;
|
|
|
private readonly ScrollBar _hScrollBar;
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Initalizes a new instance.
|
|
|
+ /// </summary>
|
|
|
public CharMap ()
|
|
|
{
|
|
|
- ColorScheme = Colors.ColorSchemes ["Dialog"];
|
|
|
+ base.ColorScheme = Colors.ColorSchemes ["Dialog"];
|
|
|
CanFocus = true;
|
|
|
CursorVisibility = CursorVisibility.Default;
|
|
|
|
|
|
- //ViewportSettings = ViewportSettings.AllowLocationGreaterThanContentSize;
|
|
|
-
|
|
|
- SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, _maxCodePoint / 16 * _rowHeight + 1)); // +1 for Header
|
|
|
-
|
|
|
AddCommand (
|
|
|
Command.Up,
|
|
|
() =>
|
|
@@ -424,7 +109,7 @@ internal class CharMap : View, IDesignable
|
|
|
Command.End,
|
|
|
() =>
|
|
|
{
|
|
|
- SelectedCodePoint = _maxCodePoint;
|
|
|
+ SelectedCodePoint = MAX_CODE_POINT;
|
|
|
|
|
|
return true;
|
|
|
}
|
|
@@ -453,7 +138,9 @@ internal class CharMap : View, IDesignable
|
|
|
MouseEvent += Handle_MouseEvent;
|
|
|
|
|
|
// Add scrollbars
|
|
|
- Padding.Thickness = new (0, 0, 1, 0);
|
|
|
+ Padding!.Thickness = new (0, 0, 1, 0);
|
|
|
+
|
|
|
+ SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + 1)); // +1 for Header
|
|
|
|
|
|
_hScrollBar = new ()
|
|
|
{
|
|
@@ -463,7 +150,7 @@ internal class CharMap : View, IDesignable
|
|
|
Orientation = Orientation.Horizontal,
|
|
|
Width = Dim.Fill (1),
|
|
|
Size = GetContentSize ().Width - RowLabelWidth,
|
|
|
- Increment = COLUMN_WIDTH
|
|
|
+ Increment = COLUMN_WIDTH,
|
|
|
};
|
|
|
|
|
|
_vScrollBar = new ()
|
|
@@ -510,48 +197,45 @@ internal class CharMap : View, IDesignable
|
|
|
Padding.Thickness = Padding.Thickness with { Bottom = 0 };
|
|
|
}
|
|
|
|
|
|
- _hScrollBar.ContentPosition = Viewport.X;
|
|
|
- _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
+ //_hScrollBar.ContentPosition = Viewport.X;
|
|
|
+ //_vScrollBar.ContentPosition = Viewport.Y;
|
|
|
};
|
|
|
+
|
|
|
+ SubviewsLaidOut += (sender, args) =>
|
|
|
+ {
|
|
|
+ //_vScrollBar.ContentPosition = Viewport.Y;
|
|
|
+ //_hScrollBar.ContentPosition = Viewport.X;
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- private void Handle_MouseEvent (object sender, MouseEventArgs e)
|
|
|
+ private void ScrollToMakeCursorVisible (Point newCursor)
|
|
|
{
|
|
|
- if (e.Flags == MouseFlags.WheeledDown)
|
|
|
+ // Adjust vertical scrolling
|
|
|
+ if (newCursor.Y < 1) // Header is at Y = 0
|
|
|
{
|
|
|
- ScrollVertical (1);
|
|
|
- _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
- e.Handled = true;
|
|
|
-
|
|
|
- return;
|
|
|
+ ScrollVertical (newCursor.Y - 1);
|
|
|
}
|
|
|
-
|
|
|
- if (e.Flags == MouseFlags.WheeledUp)
|
|
|
+ else if (newCursor.Y >= Viewport.Height)
|
|
|
{
|
|
|
- ScrollVertical (-1);
|
|
|
- _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
- e.Handled = true;
|
|
|
-
|
|
|
- return;
|
|
|
+ ScrollVertical (newCursor.Y - Viewport.Height + 1);
|
|
|
}
|
|
|
|
|
|
- if (e.Flags == MouseFlags.WheeledRight)
|
|
|
+ // Adjust horizontal scrolling
|
|
|
+ if (newCursor.X < RowLabelWidth + 1)
|
|
|
{
|
|
|
- ScrollHorizontal (1);
|
|
|
- _hScrollBar.ContentPosition = Viewport.X;
|
|
|
- e.Handled = true;
|
|
|
-
|
|
|
- return;
|
|
|
+ ScrollHorizontal (newCursor.X - (RowLabelWidth + 1));
|
|
|
}
|
|
|
-
|
|
|
- if (e.Flags == MouseFlags.WheeledLeft)
|
|
|
+ else if (newCursor.X >= Viewport.Width)
|
|
|
{
|
|
|
- ScrollHorizontal (-1);
|
|
|
- _hScrollBar.ContentPosition = Viewport.X;
|
|
|
- e.Handled = true;
|
|
|
+ ScrollHorizontal (newCursor.X - Viewport.Width + 1);
|
|
|
}
|
|
|
+
|
|
|
+ _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
+ _hScrollBar.ContentPosition = Viewport.X;
|
|
|
}
|
|
|
|
|
|
+ #region Cursor
|
|
|
+
|
|
|
/// <summary>Gets or sets the coordinates of the Cursor based on the SelectedCodePoint in Viewport-relative coordinates</summary>
|
|
|
public Point Cursor
|
|
|
{
|
|
@@ -565,7 +249,30 @@ internal class CharMap : View, IDesignable
|
|
|
set => throw new NotImplementedException ();
|
|
|
}
|
|
|
|
|
|
- public static int _maxCodePoint = UnicodeRange.Ranges.Max (r => r.End);
|
|
|
+ public override Point? PositionCursor ()
|
|
|
+ {
|
|
|
+ if (HasFocus
|
|
|
+ && Cursor.X >= RowLabelWidth
|
|
|
+ && Cursor.X < Viewport.Width
|
|
|
+ && Cursor.Y > 0
|
|
|
+ && Cursor.Y < Viewport.Height)
|
|
|
+ {
|
|
|
+ Move (Cursor.X, Cursor.Y);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return Cursor;
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion Cursor
|
|
|
+
|
|
|
+ // ReSharper disable once InconsistentNaming
|
|
|
+ private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
|
|
|
+ private int _selectedCodepoint; // Currently selected codepoint
|
|
|
+ private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport
|
|
|
|
|
|
/// <summary>
|
|
|
/// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
|
|
@@ -573,16 +280,15 @@ internal class CharMap : View, IDesignable
|
|
|
/// </summary>
|
|
|
public int SelectedCodePoint
|
|
|
{
|
|
|
- get => _selected;
|
|
|
+ get => _selectedCodepoint;
|
|
|
set
|
|
|
{
|
|
|
- if (_selected == value)
|
|
|
+ if (_selectedCodepoint == value)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- Point prevCursor = Cursor;
|
|
|
- int newSelectedCodePoint = Math.Clamp (value, 0, _maxCodePoint);
|
|
|
+ int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT);
|
|
|
|
|
|
Point newCursor = new ()
|
|
|
{
|
|
@@ -590,71 +296,54 @@ internal class CharMap : View, IDesignable
|
|
|
Y = newSelectedCodePoint / 16 * _rowHeight + 1 - Viewport.Y
|
|
|
};
|
|
|
|
|
|
+ _selectedCodepoint = newSelectedCodePoint;
|
|
|
+
|
|
|
// Ensure the new cursor position is visible
|
|
|
- EnsureCursorIsVisible (newCursor);
|
|
|
+ ScrollToMakeCursorVisible (newCursor);
|
|
|
|
|
|
- _selected = newSelectedCodePoint;
|
|
|
SetNeedsDraw ();
|
|
|
- SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null));
|
|
|
+ SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void EnsureCursorIsVisible (Point newCursor)
|
|
|
- {
|
|
|
- // Adjust vertical scrolling
|
|
|
- if (newCursor.Y < 1) // Header is at Y = 0
|
|
|
- {
|
|
|
- ScrollVertical (newCursor.Y - 1);
|
|
|
- }
|
|
|
- else if (newCursor.Y >= Viewport.Height)
|
|
|
- {
|
|
|
- ScrollVertical (newCursor.Y - Viewport.Height + 1);
|
|
|
- }
|
|
|
-
|
|
|
- _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
-
|
|
|
- // Adjust horizontal scrolling
|
|
|
- if (newCursor.X < RowLabelWidth + 1)
|
|
|
- {
|
|
|
- ScrollHorizontal (newCursor.X - (RowLabelWidth + 1));
|
|
|
- }
|
|
|
- else if (newCursor.X >= Viewport.Width)
|
|
|
- {
|
|
|
- ScrollHorizontal (newCursor.X - Viewport.Width + 1);
|
|
|
- }
|
|
|
-
|
|
|
- _hScrollBar.ContentPosition = Viewport.X;
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// Raised when the selected code point changes.
|
|
|
+ /// </summary>
|
|
|
+ public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
|
|
|
|
|
|
- public bool ShowGlyphWidths
|
|
|
+ /// <summary>
|
|
|
+ /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
|
|
|
+ /// characters.
|
|
|
+ /// </summary>
|
|
|
+ public int StartCodePoint
|
|
|
{
|
|
|
- get => _rowHeight == 2;
|
|
|
+ get => _startCodepoint;
|
|
|
set
|
|
|
{
|
|
|
- _rowHeight = value ? 2 : 1;
|
|
|
- SetNeedsDraw ();
|
|
|
+ _startCodepoint = value;
|
|
|
+ SelectedCodePoint = value;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
|
|
|
- /// characters.
|
|
|
+ /// Gets or sets whether the number of columns each glyph is displayed.
|
|
|
/// </summary>
|
|
|
- public int StartCodePoint
|
|
|
+ public bool ShowGlyphWidths
|
|
|
{
|
|
|
- get => _start;
|
|
|
+ get => _rowHeight == 2;
|
|
|
set
|
|
|
{
|
|
|
- _start = value;
|
|
|
- SelectedCodePoint = value;
|
|
|
- Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
|
|
|
+ _rowHeight = value ? 2 : 1;
|
|
|
SetNeedsDraw ();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static int RowLabelWidth => $"U+{_maxCodePoint:x5}".Length + 1;
|
|
|
- private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16;
|
|
|
- public event EventHandler<ListViewItemEventArgs> Hover;
|
|
|
+ private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
|
|
|
+ private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
|
|
|
+
|
|
|
+ #region Drawing
|
|
|
+
|
|
|
+ private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
|
|
|
|
|
|
protected override bool OnDrawingContent ()
|
|
|
{
|
|
@@ -682,7 +371,7 @@ internal class CharMap : View, IDesignable
|
|
|
Move (x, 0);
|
|
|
SetAttribute (GetHotNormalColor ());
|
|
|
AddStr (" ");
|
|
|
- SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ());
|
|
|
+ SetAttribute (HasFocus && cursorCol + firstColumnX == x ? GetHotFocusColor () : GetHotNormalColor ());
|
|
|
AddStr ($"{hexDigit:x}");
|
|
|
SetAttribute (GetHotNormalColor ());
|
|
|
AddStr (" ");
|
|
@@ -697,7 +386,7 @@ internal class CharMap : View, IDesignable
|
|
|
|
|
|
int val = row * 16;
|
|
|
|
|
|
- if (val > _maxCodePoint)
|
|
|
+ if (val > MAX_CODE_POINT)
|
|
|
{
|
|
|
break;
|
|
|
}
|
|
@@ -770,7 +459,7 @@ internal class CharMap : View, IDesignable
|
|
|
else
|
|
|
{
|
|
|
// Draw the width of the rune
|
|
|
- SetAttribute (ColorScheme.HotNormal);
|
|
|
+ SetAttribute (GetHotNormalColor ());
|
|
|
AddStr ($"{width}");
|
|
|
}
|
|
|
|
|
@@ -784,7 +473,7 @@ internal class CharMap : View, IDesignable
|
|
|
// Draw row label (U+XXXX_)
|
|
|
Move (0, y);
|
|
|
|
|
|
- SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal);
|
|
|
+ SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? GetHotFocusColor () : GetHotNormalColor ());
|
|
|
|
|
|
if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
|
|
|
{
|
|
@@ -799,26 +488,11 @@ internal class CharMap : View, IDesignable
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- public override Point? PositionCursor ()
|
|
|
- {
|
|
|
- if (HasFocus
|
|
|
- && Cursor.X >= RowLabelWidth
|
|
|
- && Cursor.X < Viewport.Width
|
|
|
- && Cursor.Y > 0
|
|
|
- && Cursor.Y < Viewport.Height)
|
|
|
- {
|
|
|
- Move (Cursor.X, Cursor.Y);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return Cursor;
|
|
|
- }
|
|
|
-
|
|
|
- public event EventHandler<ListViewItemEventArgs> SelectedCodePointChanged;
|
|
|
-
|
|
|
+ /// <summary>
|
|
|
+ /// Helper to convert a string into camel case.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="str"></param>
|
|
|
+ /// <returns></returns>
|
|
|
public static string ToCamelCase (string str)
|
|
|
{
|
|
|
if (string.IsNullOrEmpty (str))
|
|
@@ -834,10 +508,51 @@ internal class CharMap : View, IDesignable
|
|
|
return str;
|
|
|
}
|
|
|
|
|
|
- private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
|
|
|
- private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
|
|
|
+ #endregion Drawing
|
|
|
+
|
|
|
+ #region Mouse Handling
|
|
|
|
|
|
- private void Handle_MouseClick (object sender, MouseEventArgs me)
|
|
|
+ // TODO: Use this to demonstrate using a popover to show glyph info on hover
|
|
|
+ public event EventHandler<ListViewItemEventArgs>? Hover;
|
|
|
+
|
|
|
+ private void Handle_MouseEvent (object? sender, MouseEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.Flags == MouseFlags.WheeledDown)
|
|
|
+ {
|
|
|
+ ScrollVertical (1);
|
|
|
+ _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
+ e.Handled = true;
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (e.Flags == MouseFlags.WheeledUp)
|
|
|
+ {
|
|
|
+ ScrollVertical (-1);
|
|
|
+ _vScrollBar.ContentPosition = Viewport.Y;
|
|
|
+ e.Handled = true;
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (e.Flags == MouseFlags.WheeledRight)
|
|
|
+ {
|
|
|
+ ScrollHorizontal (1);
|
|
|
+ _hScrollBar.ContentPosition = Viewport.X;
|
|
|
+ e.Handled = true;
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (e.Flags == MouseFlags.WheeledLeft)
|
|
|
+ {
|
|
|
+ ScrollHorizontal (-1);
|
|
|
+ _hScrollBar.ContentPosition = Viewport.X;
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Handle_MouseClick (object? sender, MouseEventArgs me)
|
|
|
{
|
|
|
if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
|
|
|
{
|
|
@@ -864,7 +579,7 @@ internal class CharMap : View, IDesignable
|
|
|
|
|
|
int val = row * 16 + col;
|
|
|
|
|
|
- if (val > _maxCodePoint)
|
|
|
+ if (val > MAX_CODE_POINT)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
@@ -931,32 +646,41 @@ internal class CharMap : View, IDesignable
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ #endregion Mouse Handling
|
|
|
+
|
|
|
+ #region Details Dialog
|
|
|
+
|
|
|
private void ShowDetails ()
|
|
|
{
|
|
|
- var client = new UcdApiClient ();
|
|
|
+ UcdApiClient? client = new ();
|
|
|
var decResponse = string.Empty;
|
|
|
var getCodePointError = string.Empty;
|
|
|
|
|
|
- var waitIndicator = new Dialog
|
|
|
+ Dialog? waitIndicator = new ()
|
|
|
{
|
|
|
Title = "Getting Code Point Information",
|
|
|
X = Pos.Center (),
|
|
|
Y = Pos.Center (),
|
|
|
- Height = 7,
|
|
|
- Width = 50,
|
|
|
- Buttons = [new () { Text = "Cancel" }]
|
|
|
+ Width = 40,
|
|
|
+ Height = 10,
|
|
|
+ Buttons = [new () { Text = "_Cancel" }]
|
|
|
};
|
|
|
|
|
|
var errorLabel = new Label
|
|
|
{
|
|
|
Text = UcdApiClient.BaseUrl,
|
|
|
X = 0,
|
|
|
- Y = 1,
|
|
|
+ Y = 0,
|
|
|
Width = Dim.Fill (),
|
|
|
- Height = Dim.Fill (1),
|
|
|
+ Height = Dim.Fill (3),
|
|
|
TextAlignment = Alignment.Center
|
|
|
};
|
|
|
- var spinner = new SpinnerView { X = Pos.Center (), Y = Pos.Center (), Style = new Aesthetic () };
|
|
|
+ var spinner = new SpinnerView
|
|
|
+ {
|
|
|
+ X = Pos.Center (),
|
|
|
+ Y = Pos.Bottom (errorLabel),
|
|
|
+ Style = new SpinnerStyle.Aesthetic ()
|
|
|
+ };
|
|
|
spinner.AutoSpin = true;
|
|
|
waitIndicator.Add (errorLabel);
|
|
|
waitIndicator.Add (spinner);
|
|
@@ -1000,30 +724,30 @@ internal class CharMap : View, IDesignable
|
|
|
document.RootElement,
|
|
|
new
|
|
|
JsonSerializerOptions
|
|
|
- { WriteIndented = true }
|
|
|
+ { WriteIndented = true }
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- var title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
|
|
|
+ var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
|
|
|
|
|
|
- var copyGlyph = new Button { Text = "Copy _Glyph" };
|
|
|
- var copyCP = new Button { Text = "Copy Code _Point" };
|
|
|
- var cancel = new Button { Text = "Cancel" };
|
|
|
+ Button? copyGlyph = new () { Text = "Copy _Glyph" };
|
|
|
+ Button? copyCodepoint = new () { Text = "Copy Code _Point" };
|
|
|
+ Button? cancel = new () { Text = "Cancel" };
|
|
|
|
|
|
- var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] };
|
|
|
+ var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
|
|
|
|
|
|
copyGlyph.Accepting += (s, a) =>
|
|
|
{
|
|
|
CopyGlyph ();
|
|
|
- dlg.RequestStop ();
|
|
|
+ dlg!.RequestStop ();
|
|
|
};
|
|
|
|
|
|
- copyCP.Accepting += (s, a) =>
|
|
|
- {
|
|
|
- CopyCodePoint ();
|
|
|
- dlg.RequestStop ();
|
|
|
- };
|
|
|
- cancel.Accepting += (s, a) => dlg.RequestStop ();
|
|
|
+ copyCodepoint.Accepting += (s, a) =>
|
|
|
+ {
|
|
|
+ CopyCodePoint ();
|
|
|
+ dlg!.RequestStop ();
|
|
|
+ };
|
|
|
+ cancel.Accepting += (s, a) => dlg!.RequestStop ();
|
|
|
|
|
|
var rune = (Rune)SelectedCodePoint;
|
|
|
var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
|
|
@@ -1097,129 +821,13 @@ internal class CharMap : View, IDesignable
|
|
|
MessageBox.ErrorQuery (
|
|
|
"Code Point API",
|
|
|
$"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
|
|
|
- "Ok"
|
|
|
+ "_Ok"
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug
|
|
|
Application.GrabMouse (this);
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-public class UcdApiClient
|
|
|
-{
|
|
|
- public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
|
|
|
- private static readonly HttpClient _httpClient = new ();
|
|
|
-
|
|
|
- public async Task<string> GetChars (string chars)
|
|
|
- {
|
|
|
- HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
|
|
|
- response.EnsureSuccessStatusCode ();
|
|
|
-
|
|
|
- return await response.Content.ReadAsStringAsync ();
|
|
|
- }
|
|
|
-
|
|
|
- public async Task<string> GetCharsName (string chars)
|
|
|
- {
|
|
|
- HttpResponseMessage response =
|
|
|
- await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
|
|
|
- response.EnsureSuccessStatusCode ();
|
|
|
-
|
|
|
- return await response.Content.ReadAsStringAsync ();
|
|
|
- }
|
|
|
|
|
|
- public async Task<string> GetCodepointDec (int dec)
|
|
|
- {
|
|
|
- HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
|
|
|
- response.EnsureSuccessStatusCode ();
|
|
|
-
|
|
|
- return await response.Content.ReadAsStringAsync ();
|
|
|
- }
|
|
|
-
|
|
|
- public async Task<string> GetCodepointHex (string hex)
|
|
|
- {
|
|
|
- HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
|
|
|
- response.EnsureSuccessStatusCode ();
|
|
|
-
|
|
|
- return await response.Content.ReadAsStringAsync ();
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-internal class UnicodeRange
|
|
|
-{
|
|
|
- public static List<UnicodeRange> Ranges = GetRanges ();
|
|
|
-
|
|
|
- public string Category;
|
|
|
- public int End;
|
|
|
- public int Start;
|
|
|
-
|
|
|
- public UnicodeRange (int start, int end, string category)
|
|
|
- {
|
|
|
- Start = start;
|
|
|
- End = end;
|
|
|
- Category = category;
|
|
|
- }
|
|
|
-
|
|
|
- public static List<UnicodeRange> GetRanges ()
|
|
|
- {
|
|
|
- IEnumerable<UnicodeRange> ranges =
|
|
|
- from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public)
|
|
|
- let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange
|
|
|
- let name = string.IsNullOrEmpty (r.Name)
|
|
|
- ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}"
|
|
|
- : r.Name
|
|
|
- where name != "None" && name != "All"
|
|
|
- select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name);
|
|
|
-
|
|
|
- // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0
|
|
|
- List<UnicodeRange> nonBmpRanges = new ()
|
|
|
- {
|
|
|
- new (
|
|
|
- 0x1F130,
|
|
|
- 0x1F149,
|
|
|
- "Squared Latin Capital Letters"
|
|
|
- ),
|
|
|
- new (
|
|
|
- 0x12400,
|
|
|
- 0x1240f,
|
|
|
- "Cuneiform Numbers and Punctuation"
|
|
|
- ),
|
|
|
- new (0x10000, 0x1007F, "Linear B Syllabary"),
|
|
|
- new (0x10080, 0x100FF, "Linear B Ideograms"),
|
|
|
- new (0x10100, 0x1013F, "Aegean Numbers"),
|
|
|
- new (0x10300, 0x1032F, "Old Italic"),
|
|
|
- new (0x10330, 0x1034F, "Gothic"),
|
|
|
- new (0x10380, 0x1039F, "Ugaritic"),
|
|
|
- new (0x10400, 0x1044F, "Deseret"),
|
|
|
- new (0x10450, 0x1047F, "Shavian"),
|
|
|
- new (0x10480, 0x104AF, "Osmanya"),
|
|
|
- new (0x10800, 0x1083F, "Cypriot Syllabary"),
|
|
|
- new (
|
|
|
- 0x1D000,
|
|
|
- 0x1D0FF,
|
|
|
- "Byzantine Musical Symbols"
|
|
|
- ),
|
|
|
- new (0x1D100, 0x1D1FF, "Musical Symbols"),
|
|
|
- new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
|
|
|
- new (
|
|
|
- 0x1D400,
|
|
|
- 0x1D7FF,
|
|
|
- "Mathematical Alphanumeric Symbols"
|
|
|
- ),
|
|
|
- new (0x1F600, 0x1F532, "Emojis Symbols"),
|
|
|
- new (
|
|
|
- 0x20000,
|
|
|
- 0x2A6DF,
|
|
|
- "CJK Unified Ideographs Extension B"
|
|
|
- ),
|
|
|
- new (
|
|
|
- 0x2F800,
|
|
|
- 0x2FA1F,
|
|
|
- "CJK Compatibility Ideographs Supplement"
|
|
|
- ),
|
|
|
- new (0xE0000, 0xE007F, "Tags")
|
|
|
- };
|
|
|
-
|
|
|
- return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
|
|
|
- }
|
|
|
+ #endregion Details Dialog
|
|
|
}
|