mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-29 09:18:01 +01:00
* Replace all 342 `== null` with `is null`
* Replace 354 `!= null` with `is { }`
* Wrap these in conditionals since they break tests against Release configuration
The members they depend on do not exist in Release configuration
* Split these up and dispose properly
This test needs to be revisited for several reasons at some point.
* Fix release configuration tests
* Declare interface these already support
* Annotate constructor properly and use throw helper
* Move class to its own file
* Rename these files so they nest in the solution explorer
* Make this a record type and remove now-redundant/illegal members
* Reference passing to avoid some struct copies
* Simplify this
* Carry reference passing through as appropriate
* Turn this into a record struct
* Remove unused internal constructor and its test
It was only used by that test.
* Simplify this constructor
* This should be a property
* Simplify constructor
* Simplify GetHashCode
* Mark this ignored just in case
* Missed a couple of opportunities for reference passing
* record struct already does this by value
* Remove unused class
* Simplify the type initializer and Reset method
* Implement INotifyCollectionChanged and IDictionary by delegating to ColorSchemes
* Fix for reflection-based configuration
* Make CI build happy by disambiguiating this attribute
* Add PERF, NOTE, QUESTION, and CONCURRENCY tags for the todo explorer
* Make this string comparison faster.
* Add a tag for unclear intent
* This is a constant
* Turn this into a constant via use of a unicode literal
* Remove this method and its test
It is unused
There's no guarantee at all that the parent process is the terminal.
There are good reasons, including that one, why there's no simple way to do it in .net.
It's also of course a windows-only thing, if using WMI.
* With the WMI method gone, we no longer need this
* Make this more efficient
* Add detail to this property's XmlDoc
* Move the general properties up top because order matters
* Make sure any constants defined at higher levels are not clobbered and define a couple more
* Put InternalsVisibleTo in its own group
* Sort dependencies alphabetically and update
* Global usings
* Split to one type per file
* Collection expression
* Fix naming
* Inline to avoid copies
* This is already a value copy (struct)
* Combine to one non-destructive mutation
* Avoid some potential boxing
* Turn on null analysis here
* Remove unnecessary cast and use real type name
* Seal this
* Fix name
* Move nested class to a nested file (no type layout change made)
* Undo naming change that isn't changed globally until next batch
* Rename Rect to Rectangle in preparation for removal
* Add baseline test for ToString checking for current behavior.
* Change to behavior matching System.Drawing.Rectangle
* Fix this test
This is not a test of Rectangle, so trust that Rectangle gets it right.
* Fix these tests the same way as the previous commit
* These should be testing against the Rectangles, not the strings
* Slightly de-couple these as well
* Test against Rectangles, not strings
* Collection expressions and constants
* Remove this
* Perform proper platform-agnostic normalization
* Make this easier to follow (naming only)
* Add a category to this
* Use raw strings for better clarity
* Some more categorization
* Re-apply backed-out naming change from parent branch
* Change GetHashCode to be equivalent to System.Drawing.Rectangle
* Update this since 6.0.0 is no longer available and prevents build
* This check is redundant with the rectangle check below
* Re-apply Rect->Rectangle name changes in these files
1172 lines
42 KiB
C#
1172 lines
42 KiB
C#
#define DRAW_CONTENT
|
|
|
|
//#define BASE_DRAW_CONTENT
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
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 use ScrollView to do infinite scrolling
|
|
/// </summary>
|
|
[ScenarioMetadata ("Character Map", "Unicode viewer demonstrating the ScrollView control.")]
|
|
[ScenarioCategory ("Text and Formatting")]
|
|
[ScenarioCategory ("Controls")]
|
|
[ScenarioCategory ("ScrollView")]
|
|
public class CharacterMap : Scenario
|
|
{
|
|
public Label _errorLabel;
|
|
private TableView _categoryList;
|
|
private CharMap _charMap;
|
|
|
|
// Don't create a Window, just return the top-level view
|
|
public override void Init ()
|
|
{
|
|
Application.Init ();
|
|
Application.Top.ColorScheme = Colors.ColorSchemes ["Base"];
|
|
}
|
|
|
|
public override void Setup ()
|
|
{
|
|
_charMap = new CharMap { X = 0, Y = 1, Height = Dim.Fill () };
|
|
Application.Top.Add (_charMap);
|
|
|
|
var jumpLabel = new Label
|
|
{
|
|
X = Pos.Right (_charMap) + 1,
|
|
Y = Pos.Y (_charMap),
|
|
HotKeySpecifier = (Rune)'_',
|
|
Text = "_Jump To Code Point:"
|
|
};
|
|
Application.Top.Add (jumpLabel);
|
|
|
|
var jumpEdit = new TextField
|
|
{
|
|
X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3"
|
|
};
|
|
Application.Top.Add (jumpEdit);
|
|
|
|
_errorLabel = new Label
|
|
{
|
|
X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"], Text = "err"
|
|
};
|
|
Application.Top.Add (_errorLabel);
|
|
|
|
#if TEXT_CHANGED_TO_JUMP
|
|
jumpEdit.TextChanged += JumpEdit_TextChanged;
|
|
#else
|
|
jumpEdit.Accept += JumpEditOnAccept;
|
|
|
|
void JumpEditOnAccept (object sender, CancelEventArgs e)
|
|
{
|
|
JumpEdit_TextChanged (sender, new StateEventArgs<string> (jumpEdit.Text, jumpEdit.Text));
|
|
// Cancel the event to prevent ENTER from being handled elsewhere
|
|
e.Cancel = true;
|
|
}
|
|
#endif
|
|
_categoryList = new TableView { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () };
|
|
|
|
_categoryList.FullRowSelect = true;
|
|
|
|
//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.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
|
|
|
|
if (clickedCol != null && e.MouseEvent.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 ColumnStyle { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
|
|
);
|
|
_categoryList.Style.ColumnStyles.Add (1, new ColumnStyle { MaxWidth = 1, MinWidth = 6 });
|
|
_categoryList.Style.ColumnStyles.Add (2, new ColumnStyle { 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;
|
|
};
|
|
|
|
Application.Top.Add (_categoryList);
|
|
|
|
_charMap.SelectedCodePoint = 0;
|
|
_charMap.SetFocus ();
|
|
|
|
// TODO: Replace this with Dim.Auto when that's ready
|
|
_categoryList.Initialized += _categoryList_Initialized;
|
|
|
|
var menu = new MenuBar
|
|
{
|
|
Menus =
|
|
[
|
|
new MenuBarItem (
|
|
"_File",
|
|
new MenuItem []
|
|
{
|
|
new (
|
|
"_Quit",
|
|
$"{Application.QuitKey}",
|
|
() => Application.RequestStop ()
|
|
)
|
|
}
|
|
),
|
|
new MenuBarItem (
|
|
"_Options",
|
|
new [] { CreateMenuShowWidth () }
|
|
)
|
|
]
|
|
};
|
|
Application.Top.Add (menu);
|
|
}
|
|
|
|
private void _categoryList_Initialized (object sender, EventArgs e) { _charMap.Width = Dim.Fill () - _categoryList.Width; }
|
|
|
|
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 EnumerableTableSource<UnicodeRange> (
|
|
sortedRanges,
|
|
new Dictionary<string, Func<UnicodeRange, object>>
|
|
{
|
|
{ $"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;
|
|
}
|
|
|
|
private void JumpEdit_TextChanged (object sender, StateEventArgs<string> e)
|
|
{
|
|
var jumpEdit = sender as TextField;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
internal class CharMap : ScrollView
|
|
{
|
|
private const CursorVisibility _cursor = CursorVisibility.Default;
|
|
private const int COLUMN_WIDTH = 3;
|
|
|
|
private ContextMenu _contextMenu = new ();
|
|
private int _rowHeight = 1;
|
|
private int _selected;
|
|
private int _start;
|
|
|
|
public CharMap ()
|
|
{
|
|
ColorScheme = Colors.ColorSchemes ["Dialog"];
|
|
CanFocus = true;
|
|
|
|
ContentSize = new Size (
|
|
RowWidth,
|
|
(MaxCodePoint / 16 + (ShowHorizontalScrollIndicator ? 2 : 1)) * _rowHeight
|
|
);
|
|
|
|
AddCommand (
|
|
Command.ScrollUp,
|
|
() =>
|
|
{
|
|
if (SelectedCodePoint >= 16)
|
|
{
|
|
SelectedCodePoint -= 16;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.ScrollDown,
|
|
() =>
|
|
{
|
|
if (SelectedCodePoint < MaxCodePoint - 16)
|
|
{
|
|
SelectedCodePoint += 16;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.ScrollLeft,
|
|
() =>
|
|
{
|
|
if (SelectedCodePoint > 0)
|
|
{
|
|
SelectedCodePoint--;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.ScrollRight,
|
|
() =>
|
|
{
|
|
if (SelectedCodePoint < MaxCodePoint)
|
|
{
|
|
SelectedCodePoint++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.PageUp,
|
|
() =>
|
|
{
|
|
int page = (Bounds.Height / _rowHeight - 1) * 16;
|
|
SelectedCodePoint -= Math.Min (page, SelectedCodePoint);
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.PageDown,
|
|
() =>
|
|
{
|
|
int page = (Bounds.Height / _rowHeight - 1) * 16;
|
|
SelectedCodePoint += Math.Min (page, MaxCodePoint - SelectedCodePoint);
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.TopHome,
|
|
() =>
|
|
{
|
|
SelectedCodePoint = 0;
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
AddCommand (
|
|
Command.BottomEnd,
|
|
() =>
|
|
{
|
|
SelectedCodePoint = MaxCodePoint;
|
|
|
|
return true;
|
|
}
|
|
);
|
|
KeyBindings.Add (Key.Enter, Command.Accept);
|
|
|
|
AddCommand (
|
|
Command.Accept,
|
|
() =>
|
|
{
|
|
ShowDetails ();
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
MouseClick += Handle_MouseClick;
|
|
}
|
|
|
|
/// <summary>Gets the coordinates of the Cursor based on the SelectedCodePoint in screen coordinates</summary>
|
|
public Point Cursor
|
|
{
|
|
get
|
|
{
|
|
int row = SelectedCodePoint / 16 * _rowHeight + ContentOffset.Y + 1;
|
|
|
|
int col = SelectedCodePoint % 16 * COLUMN_WIDTH + ContentOffset.X + RowLabelWidth + 1; // + 1 for padding
|
|
|
|
return new Point (col, row);
|
|
}
|
|
set => throw new NotImplementedException ();
|
|
}
|
|
|
|
public static int MaxCodePoint => 0x10FFFF;
|
|
|
|
/// <summary>
|
|
/// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
|
|
/// characters.
|
|
/// </summary>
|
|
public int SelectedCodePoint
|
|
{
|
|
get => _selected;
|
|
set
|
|
{
|
|
_selected = value;
|
|
|
|
if (IsInitialized)
|
|
{
|
|
int row = SelectedCodePoint / 16 * _rowHeight;
|
|
int col = SelectedCodePoint % 16 * COLUMN_WIDTH;
|
|
|
|
int height = Bounds.Height - (ShowHorizontalScrollIndicator ? 2 : 1);
|
|
|
|
if (row + ContentOffset.Y < 0)
|
|
{
|
|
// Moving up.
|
|
ContentOffset = new Point (ContentOffset.X, row);
|
|
}
|
|
else if (row + ContentOffset.Y >= height)
|
|
{
|
|
// Moving down.
|
|
ContentOffset = new Point (
|
|
ContentOffset.X,
|
|
Math.Min (row, row - height + _rowHeight)
|
|
);
|
|
}
|
|
|
|
int width = Bounds.Width / COLUMN_WIDTH * COLUMN_WIDTH - (ShowVerticalScrollIndicator ? RowLabelWidth + 1 : RowLabelWidth);
|
|
|
|
if (col + ContentOffset.X < 0)
|
|
{
|
|
// Moving left.
|
|
ContentOffset = new Point (col, ContentOffset.Y);
|
|
}
|
|
else if (col + ContentOffset.X >= width)
|
|
{
|
|
// Moving right.
|
|
ContentOffset = new Point (
|
|
Math.Min (col, col - width + COLUMN_WIDTH),
|
|
ContentOffset.Y
|
|
);
|
|
}
|
|
}
|
|
|
|
SetNeedsDisplay ();
|
|
SelectedCodePointChanged?.Invoke (this, new ListViewItemEventArgs (SelectedCodePoint, null));
|
|
}
|
|
}
|
|
|
|
public bool ShowGlyphWidths
|
|
{
|
|
get => _rowHeight == 2;
|
|
set
|
|
{
|
|
_rowHeight = value ? 2 : 1;
|
|
SetNeedsDisplay ();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
|
|
/// characters.
|
|
/// </summary>
|
|
public int StartCodePoint
|
|
{
|
|
get => _start;
|
|
set
|
|
{
|
|
_start = value;
|
|
SelectedCodePoint = value;
|
|
SetNeedsDisplay ();
|
|
}
|
|
}
|
|
|
|
private static int RowLabelWidth => $"U+{MaxCodePoint:x5}".Length + 1;
|
|
private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16;
|
|
public event EventHandler<ListViewItemEventArgs> Hover;
|
|
|
|
public override void OnDrawContent (Rectangle contentArea)
|
|
{
|
|
//if (ShowHorizontalScrollIndicator && ContentSize.Height < (int)(MaxCodePoint / 16 + 2)) {
|
|
// //ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePoint / 16 + 2));
|
|
// //ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePoint / 16) * _rowHeight + 2);
|
|
// var width = (Bounds.Width / COLUMN_WIDTH * COLUMN_WIDTH) - (ShowVerticalScrollIndicator ? RowLabelWidth + 1 : RowLabelWidth);
|
|
// if (Cursor.X + ContentOffset.X >= width) {
|
|
// // Snap to the selected glyph.
|
|
// ContentOffset = new Point (
|
|
// Math.Min (Cursor.X, Cursor.X - width + COLUMN_WIDTH),
|
|
// ContentOffset.Y == -ContentSize.Height + Bounds.Height ? ContentOffset.Y - 1 : ContentOffset.Y);
|
|
// } else {
|
|
// ContentOffset = new Point (
|
|
// ContentOffset.X - Cursor.X,
|
|
// ContentOffset.Y == -ContentSize.Height + Bounds.Height ? ContentOffset.Y - 1 : ContentOffset.Y);
|
|
// }
|
|
//} else if (!ShowHorizontalScrollIndicator && ContentSize.Height > (int)(MaxCodePoint / 16 + 1)) {
|
|
// //ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePoint / 16 + 1));
|
|
// // Snap 1st column into view if it's been scrolled horizontally
|
|
// ContentOffset = new Point (0, ContentOffset.Y < -ContentSize.Height + Bounds.Height ? ContentOffset.Y - 1 : ContentOffset.Y);
|
|
//}
|
|
base.OnDrawContent (contentArea);
|
|
}
|
|
|
|
//public void CharMap_DrawContent (object s, DrawEventArgs a)
|
|
public override void OnDrawContentComplete (Rectangle contentArea)
|
|
{
|
|
if (contentArea.Height == 0 || contentArea.Width == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var viewport = new Rectangle (
|
|
ContentOffset,
|
|
new Size (
|
|
Math.Max (Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0), 0),
|
|
Math.Max (Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0), 0)
|
|
)
|
|
);
|
|
|
|
Rectangle oldClip = ClipToBounds ();
|
|
|
|
if (ShowHorizontalScrollIndicator)
|
|
{
|
|
// ClipToBounds doesn't know about the scroll indicators, so if off, subtract one from height
|
|
Driver.Clip = new Rectangle (
|
|
Driver.Clip.Location,
|
|
new Size (Driver.Clip.Width, Driver.Clip.Height - 1)
|
|
);
|
|
}
|
|
|
|
if (ShowVerticalScrollIndicator)
|
|
{
|
|
// ClipToBounds doesn't know about the scroll indicators, so if off, subtract one from width
|
|
Driver.Clip = new Rectangle (
|
|
Driver.Clip.Location,
|
|
new Size (Driver.Clip.Width - 1, Driver.Clip.Height)
|
|
);
|
|
}
|
|
|
|
int cursorCol = Cursor.X - ContentOffset.X - RowLabelWidth - 1;
|
|
int cursorRow = Cursor.Y - ContentOffset.Y - 1;
|
|
|
|
Driver.SetAttribute (GetHotNormalColor ());
|
|
Move (0, 0);
|
|
Driver.AddStr (new string (' ', RowLabelWidth + 1));
|
|
|
|
for (var hexDigit = 0; hexDigit < 16; hexDigit++)
|
|
{
|
|
int x = ContentOffset.X + RowLabelWidth + hexDigit * COLUMN_WIDTH;
|
|
|
|
if (x > RowLabelWidth - 2)
|
|
{
|
|
Move (x, 0);
|
|
Driver.SetAttribute (GetHotNormalColor ());
|
|
Driver.AddStr (" ");
|
|
|
|
Driver.SetAttribute (
|
|
HasFocus && cursorCol + ContentOffset.X + RowLabelWidth == x
|
|
? ColorScheme.HotFocus
|
|
: GetHotNormalColor ()
|
|
);
|
|
Driver.AddStr ($"{hexDigit:x}");
|
|
Driver.SetAttribute (GetHotNormalColor ());
|
|
Driver.AddStr (" ");
|
|
}
|
|
}
|
|
|
|
int firstColumnX = viewport.X + RowLabelWidth;
|
|
|
|
for (var y = 1; y < Bounds.Height; y++)
|
|
{
|
|
// What row is this?
|
|
int row = (y - ContentOffset.Y - 1) / _rowHeight;
|
|
|
|
int val = row * 16;
|
|
|
|
if (val > MaxCodePoint)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Move (firstColumnX + COLUMN_WIDTH, y);
|
|
Driver.SetAttribute (GetNormalColor ());
|
|
|
|
for (var col = 0; col < 16; col++)
|
|
{
|
|
int x = firstColumnX + COLUMN_WIDTH * col + 1;
|
|
|
|
Move (x, y);
|
|
|
|
if (cursorRow + ContentOffset.Y + 1 == y && cursorCol + ContentOffset.X + firstColumnX + 1 == x && !HasFocus)
|
|
{
|
|
Driver.SetAttribute (GetFocusColor ());
|
|
}
|
|
|
|
int scalar = val + col;
|
|
var rune = (Rune)'?';
|
|
|
|
if (Rune.IsValid (scalar))
|
|
{
|
|
rune = new Rune (scalar);
|
|
}
|
|
|
|
int width = rune.GetColumns ();
|
|
|
|
// are we at first row of the row?
|
|
if (!ShowGlyphWidths || (y - ContentOffset.Y) % _rowHeight > 0)
|
|
{
|
|
if (width > 0)
|
|
{
|
|
Driver.AddRune (rune);
|
|
}
|
|
else
|
|
{
|
|
if (rune.IsCombiningMark ())
|
|
{
|
|
// This is a hack to work around the fact that combining marks
|
|
// a) can't be rendered on their own
|
|
// b) that don't normalize are not properly supported in
|
|
// any known terminal (esp Windows/AtlasEngine).
|
|
// See Issue #2616
|
|
var sb = new StringBuilder ();
|
|
sb.Append ('a');
|
|
sb.Append (rune);
|
|
|
|
// Try normalizing after combining with 'a'. If it normalizes, at least
|
|
// it'll show on the 'a'. If not, just show the replacement char.
|
|
string normal = sb.ToString ().Normalize (NormalizationForm.FormC);
|
|
|
|
if (normal.Length == 1)
|
|
{
|
|
Driver.AddRune (normal [0]);
|
|
}
|
|
else
|
|
{
|
|
Driver.AddRune (Rune.ReplacementChar);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Driver.SetAttribute (ColorScheme.HotNormal);
|
|
Driver.AddStr ($"{width}");
|
|
}
|
|
|
|
if (cursorRow + ContentOffset.Y + 1 == y && cursorCol + ContentOffset.X + firstColumnX + 1 == x && !HasFocus)
|
|
{
|
|
Driver.SetAttribute (GetNormalColor ());
|
|
}
|
|
}
|
|
|
|
Move (0, y);
|
|
|
|
Driver.SetAttribute (
|
|
HasFocus && cursorRow + ContentOffset.Y + 1 == y
|
|
? ColorScheme.HotFocus
|
|
: ColorScheme.HotNormal
|
|
);
|
|
|
|
if (!ShowGlyphWidths || (y - ContentOffset.Y) % _rowHeight > 0)
|
|
{
|
|
Driver.AddStr ($"U+{val / 16:x5}_ ");
|
|
}
|
|
else
|
|
{
|
|
Driver.AddStr (new string (' ', RowLabelWidth));
|
|
}
|
|
}
|
|
|
|
Driver.Clip = oldClip;
|
|
}
|
|
|
|
public override bool OnEnter (View view)
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
Application.Driver.SetCursorVisibility (_cursor);
|
|
}
|
|
|
|
return base.OnEnter (view);
|
|
}
|
|
|
|
public override bool OnLeave (View view)
|
|
{
|
|
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
|
|
|
return base.OnLeave (view);
|
|
}
|
|
|
|
public override void PositionCursor ()
|
|
{
|
|
if (HasFocus
|
|
&& Cursor.X >= RowLabelWidth
|
|
&& Cursor.X < Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0)
|
|
&& Cursor.Y > 0
|
|
&& Cursor.Y < Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0))
|
|
{
|
|
Driver.SetCursorVisibility (_cursor);
|
|
Move (Cursor.X, Cursor.Y);
|
|
}
|
|
else
|
|
{
|
|
Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
|
}
|
|
}
|
|
|
|
public event EventHandler<ListViewItemEventArgs> SelectedCodePointChanged;
|
|
|
|
public static string ToCamelCase (string str)
|
|
{
|
|
if (string.IsNullOrEmpty (str))
|
|
{
|
|
return str;
|
|
}
|
|
|
|
TextInfo textInfo = new CultureInfo ("en-US", false).TextInfo;
|
|
|
|
str = textInfo.ToLower (str);
|
|
str = textInfo.ToTitleCase (str);
|
|
|
|
return str;
|
|
}
|
|
|
|
private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
|
|
private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
|
|
|
|
private void Handle_MouseClick (object sender, MouseEventEventArgs args)
|
|
{
|
|
MouseEvent me = args.MouseEvent;
|
|
|
|
if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (me.Y == 0)
|
|
{
|
|
me.Y = Cursor.Y;
|
|
}
|
|
|
|
if (me.Y > 0)
|
|
{ }
|
|
|
|
if (me.X < RowLabelWidth || me.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
|
|
{
|
|
me.X = Cursor.X;
|
|
}
|
|
|
|
int row = (me.Y - 1 - ContentOffset.Y) / _rowHeight; // -1 for header
|
|
int col = (me.X - RowLabelWidth - ContentOffset.X) / COLUMN_WIDTH;
|
|
|
|
if (col > 15)
|
|
{
|
|
col = 15;
|
|
}
|
|
|
|
int val = row * 16 + col;
|
|
|
|
if (val > MaxCodePoint)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (me.Flags == MouseFlags.ReportMousePosition)
|
|
{
|
|
Hover?.Invoke (this, new ListViewItemEventArgs (val, null));
|
|
}
|
|
|
|
if (me.Flags == MouseFlags.Button1Clicked)
|
|
{
|
|
SelectedCodePoint = val;
|
|
|
|
return;
|
|
}
|
|
|
|
if (me.Flags == MouseFlags.Button1DoubleClicked)
|
|
{
|
|
SelectedCodePoint = val;
|
|
ShowDetails ();
|
|
|
|
return;
|
|
}
|
|
|
|
if (me.Flags == _contextMenu.MouseFlags)
|
|
{
|
|
SelectedCodePoint = val;
|
|
|
|
_contextMenu = new ContextMenu
|
|
{
|
|
Position = new Point (me.X + 1, me.Y + 1),
|
|
MenuItems = new MenuBarItem (
|
|
new MenuItem []
|
|
{
|
|
new (
|
|
"_Copy Glyph",
|
|
"",
|
|
CopyGlyph,
|
|
null,
|
|
null,
|
|
(KeyCode)Key.C.WithCtrl
|
|
),
|
|
new (
|
|
"Copy Code _Point",
|
|
"",
|
|
CopyCodePoint,
|
|
null,
|
|
null,
|
|
(KeyCode)Key.C.WithCtrl
|
|
.WithShift
|
|
)
|
|
}
|
|
)
|
|
};
|
|
_contextMenu.Show ();
|
|
}
|
|
}
|
|
|
|
private void ShowDetails ()
|
|
{
|
|
var client = new UcdApiClient ();
|
|
var decResponse = string.Empty;
|
|
|
|
var waitIndicator = new Dialog
|
|
{
|
|
Title = "Getting Code Point Information",
|
|
X = Pos.Center (),
|
|
Y = Pos.Center (),
|
|
Height = 7,
|
|
Width = 50,
|
|
Buttons = [new Button { Text = "Cancel" }]
|
|
};
|
|
|
|
var errorLabel = new Label
|
|
{
|
|
Text = UcdApiClient.BaseUrl,
|
|
AutoSize = false,
|
|
X = 0,
|
|
Y = 1,
|
|
Width = Dim.Fill (),
|
|
Height = Dim.Fill (1),
|
|
TextAlignment = TextAlignment.Centered
|
|
};
|
|
var spinner = new SpinnerView { X = Pos.Center (), Y = Pos.Center (), Style = new Aesthetic () };
|
|
spinner.AutoSpin = true;
|
|
waitIndicator.Add (errorLabel);
|
|
waitIndicator.Add (spinner);
|
|
|
|
waitIndicator.Ready += async (s, a) =>
|
|
{
|
|
try
|
|
{
|
|
decResponse = await client.GetCodepointDec (SelectedCodePoint);
|
|
}
|
|
catch (HttpRequestException e)
|
|
{
|
|
(s as Dialog).Text = e.Message;
|
|
|
|
Application.Invoke (
|
|
() =>
|
|
{
|
|
spinner.Visible = false;
|
|
errorLabel.Text = e.Message;
|
|
errorLabel.ColorScheme = Colors.ColorSchemes ["Error"];
|
|
errorLabel.Visible = true;
|
|
}
|
|
);
|
|
}
|
|
|
|
(s as Dialog)?.RequestStop ();
|
|
};
|
|
Application.Run (waitIndicator);
|
|
|
|
if (!string.IsNullOrEmpty (decResponse))
|
|
{
|
|
var name = string.Empty;
|
|
|
|
using (JsonDocument document = JsonDocument.Parse (decResponse))
|
|
{
|
|
JsonElement root = document.RootElement;
|
|
|
|
// Get a property by name and output its value
|
|
if (root.TryGetProperty ("name", out JsonElement nameElement))
|
|
{
|
|
name = nameElement.GetString ();
|
|
}
|
|
|
|
//// Navigate to a nested property and output its value
|
|
//if (root.TryGetProperty ("property3", out JsonElement property3Element)
|
|
//&& property3Element.TryGetProperty ("nestedProperty", out JsonElement nestedPropertyElement)) {
|
|
// Console.WriteLine (nestedPropertyElement.GetString ());
|
|
//}
|
|
decResponse = JsonSerializer.Serialize (
|
|
document.RootElement,
|
|
new
|
|
JsonSerializerOptions { WriteIndented = true }
|
|
);
|
|
}
|
|
|
|
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" };
|
|
|
|
var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] };
|
|
|
|
copyGlyph.Accept += (s, a) =>
|
|
{
|
|
CopyGlyph ();
|
|
dlg.RequestStop ();
|
|
};
|
|
|
|
copyCP.Accept += (s, a) =>
|
|
{
|
|
CopyCodePoint ();
|
|
dlg.RequestStop ();
|
|
};
|
|
cancel.Accept += (s, a) => dlg.RequestStop ();
|
|
|
|
var rune = (Rune)SelectedCodePoint;
|
|
var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
dlg.Add (label);
|
|
|
|
label = new Label
|
|
{
|
|
Text =
|
|
$"Code Point Information from {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
|
|
X = 0,
|
|
Y = Pos.Bottom (label)
|
|
};
|
|
dlg.Add (label);
|
|
|
|
var json = new TextView
|
|
{
|
|
X = 0,
|
|
Y = Pos.Bottom (label),
|
|
Width = Dim.Fill (),
|
|
Height = Dim.Fill (2),
|
|
ReadOnly = true,
|
|
Text = decResponse
|
|
};
|
|
dlg.Add (json);
|
|
|
|
Application.Run (dlg);
|
|
}
|
|
else
|
|
{
|
|
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"
|
|
);
|
|
}
|
|
|
|
// 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 UnicodeRange (
|
|
0x1F130,
|
|
0x1F149,
|
|
"Squared Latin Capital Letters"
|
|
),
|
|
new UnicodeRange (
|
|
0x12400,
|
|
0x1240f,
|
|
"Cuneiform Numbers and Punctuation"
|
|
),
|
|
new UnicodeRange (0x10000, 0x1007F, "Linear B Syllabary"),
|
|
new UnicodeRange (0x10080, 0x100FF, "Linear B Ideograms"),
|
|
new UnicodeRange (0x10100, 0x1013F, "Aegean Numbers"),
|
|
new UnicodeRange (0x10300, 0x1032F, "Old Italic"),
|
|
new UnicodeRange (0x10330, 0x1034F, "Gothic"),
|
|
new UnicodeRange (0x10380, 0x1039F, "Ugaritic"),
|
|
new UnicodeRange (0x10400, 0x1044F, "Deseret"),
|
|
new UnicodeRange (0x10450, 0x1047F, "Shavian"),
|
|
new UnicodeRange (0x10480, 0x104AF, "Osmanya"),
|
|
new UnicodeRange (0x10800, 0x1083F, "Cypriot Syllabary"),
|
|
new UnicodeRange (
|
|
0x1D000,
|
|
0x1D0FF,
|
|
"Byzantine Musical Symbols"
|
|
),
|
|
new UnicodeRange (0x1D100, 0x1D1FF, "Musical Symbols"),
|
|
new UnicodeRange (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
|
|
new UnicodeRange (
|
|
0x1D400,
|
|
0x1D7FF,
|
|
"Mathematical Alphanumeric Symbols"
|
|
),
|
|
new UnicodeRange (0x1F600, 0x1F532, "Emojis Symbols"),
|
|
new UnicodeRange (
|
|
0x20000,
|
|
0x2A6DF,
|
|
"CJK Unified Ideographs Extension B"
|
|
),
|
|
new UnicodeRange (
|
|
0x2F800,
|
|
0x2FA1F,
|
|
"CJK Compatibility Ideographs Supplement"
|
|
),
|
|
new UnicodeRange (0xE0000, 0xE007F, "Tags")
|
|
};
|
|
|
|
return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
|
|
}
|
|
}
|