using NStack; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using Terminal.Gui; using Rune = System.Rune; /// /// /// UI Catalog attempts to satisfy the following goals: /// /// /// /// /// /// Be an easy to use showcase for Terminal.Gui concepts and features. /// /// /// /// /// Provide sample code that illustrates how to properly implement said concepts & features. /// /// /// /// /// Make it easy for contributors to add additional samples in a structured way. /// /// /// /// /// /// See the project README for more details (https://github.com/migueldeicaza/gui.cs/tree/master/UICatalog/README.md). /// /// namespace UICatalog { /// /// UI Catalog is a comprehensive sample app and scenario library for /// public class UICatalogApp { private static Toplevel _top; private static MenuBar _menu; private static int _nameColumnWidth; private static FrameView _leftPane; private static List _categories; private static ListView _categoryListView; private static FrameView _rightPane; private static List _scenarios; private static ListView _scenarioListView; private static StatusBar _statusBar; private static StatusItem _capslock; private static StatusItem _numlock; private static StatusItem _scrolllock; private static int _categoryListViewItem; private static int _scenarioListViewItem; private static Scenario _runningScenario = null; private static bool _useSystemConsole = false; private static ConsoleDriver.DiagnosticFlags _diagnosticFlags; private static bool _heightAsBuffer = false; private static bool _alwaysSetPosition; private static bool _isFirstRunning = true; static void Main (string [] args) { Console.OutputEncoding = Encoding.Default; if (Debugger.IsAttached) CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); _scenarios = Scenario.GetDerivedClasses ().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList (); if (args.Length > 0 && args.Contains ("-usc")) { _useSystemConsole = true; args = args.Where (val => val != "-usc").ToArray (); } if (args.Length > 0) { var item = _scenarios.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals (args [0], StringComparison.OrdinalIgnoreCase)); _runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item]); Application.UseSystemConsole = _useSystemConsole; Application.Init (); _runningScenario.Init (Application.Top, _baseColorScheme); _runningScenario.Setup (); _runningScenario.Run (); _runningScenario = null; Application.Shutdown (); return; } Scenario scenario; while ((scenario = GetScenarioToRun ()) != null) { #if DEBUG_IDISPOSABLE // Validate there are no outstanding Responder-based instances // after a scenario was selected to run. This proves the main UI Catalog // 'app' closed cleanly. foreach (var inst in Responder.Instances) { Debug.Assert (inst.WasDisposed); } Responder.Instances.Clear (); #endif scenario.Init (Application.Top, _baseColorScheme); scenario.Setup (); scenario.Run (); //static void LoadedHandler () //{ // _rightPane.SetFocus (); // _top.Loaded -= LoadedHandler; //} //_top.Loaded += LoadedHandler; Application.Shutdown (); #if DEBUG_IDISPOSABLE // After the scenario runs, validate all Responder-based instances // were disposed. This proves the scenario 'app' closed cleanly. foreach (var inst in Responder.Instances) { Debug.Assert (inst.WasDisposed); } Responder.Instances.Clear (); #endif } Application.Shutdown (); #if DEBUG_IDISPOSABLE // This proves that when the user exited the UI Catalog app // it cleaned up properly. foreach (var inst in Responder.Instances) { Debug.Assert (inst.WasDisposed); } Responder.Instances.Clear (); #endif } /// /// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything. /// /// private static Scenario GetScenarioToRun () { Application.UseSystemConsole = _useSystemConsole; Application.Init (); Application.HeightAsBuffer = _heightAsBuffer; Application.AlwaysSetPosition = _alwaysSetPosition; // Set this here because not initialized until driver is loaded _baseColorScheme = Colors.Base; StringBuilder aboutMessage = new StringBuilder (); aboutMessage.AppendLine ("UI Catalog is a comprehensive sample library for Terminal.Gui"); aboutMessage.AppendLine (@" _ "); aboutMessage.AppendLine (@" __ _ _ _(_) ___ ___ "); aboutMessage.AppendLine (@" / _` | | | | | / __/ __|"); aboutMessage.AppendLine (@"| (_| | |_| | || (__\__ \"); aboutMessage.AppendLine (@" \__, |\__,_|_(_)___|___/"); aboutMessage.AppendLine (@" |___/ "); aboutMessage.AppendLine (""); aboutMessage.AppendLine ($"Version: {typeof (UICatalogApp).Assembly.GetName ().Version}"); aboutMessage.AppendLine ($"Using Terminal.Gui Version: {FileVersionInfo.GetVersionInfo (typeof (Terminal.Gui.Application).Assembly.Location).ProductVersion}"); aboutMessage.AppendLine (""); _menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Quit", "", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask) }), new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()), new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()), new MenuBarItem ("_Help", new MenuItem [] { new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://migueldeicaza.github.io/gui.cs/articles/overview.html"), null, null, Key.F1), new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/migueldeicaza/gui.cs"), null, null, Key.F2), new MenuItem ("_About...", "About this app", () => MessageBox.Query ("About UI Catalog", aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), }) }); _leftPane = new FrameView ("Categories") { X = 0, Y = 1, // for menu Width = 25, Height = Dim.Fill (1), CanFocus = false, Shortcut = Key.CtrlMask | Key.C }; _leftPane.Title = $"{_leftPane.Title} ({_leftPane.ShortcutTag})"; _leftPane.ShortcutAction = () => _leftPane.SetFocus (); _categories = Scenario.GetAllCategories ().OrderBy (c => c).ToList (); _categoryListView = new ListView (_categories) { X = 0, Y = 0, Width = Dim.Fill (0), Height = Dim.Fill (0), AllowsMarking = false, CanFocus = true, }; _categoryListView.OpenSelectedItem += (a) => { _rightPane.SetFocus (); }; _categoryListView.SelectedItemChanged += CategoryListView_SelectedChanged; _leftPane.Add (_categoryListView); _rightPane = new FrameView ("Scenarios") { X = 25, Y = 1, // for menu Width = Dim.Fill (), Height = Dim.Fill (1), CanFocus = true, Shortcut = Key.CtrlMask | Key.S }; _rightPane.Title = $"{_rightPane.Title} ({_rightPane.ShortcutTag})"; _rightPane.ShortcutAction = () => _rightPane.SetFocus (); _nameColumnWidth = Scenario.ScenarioMetadata.GetName (_scenarios.OrderByDescending (t => Scenario.ScenarioMetadata.GetName (t).Length).FirstOrDefault ()).Length; _scenarioListView = new ListView () { X = 0, Y = 0, Width = Dim.Fill (0), Height = Dim.Fill (0), AllowsMarking = false, CanFocus = true, }; _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem; _rightPane.Add (_scenarioListView); _categoryListView.SelectedItem = _categoryListViewItem; _categoryListView.OnSelectedChanged (); _capslock = new StatusItem (Key.CharMask, "Caps", null); _numlock = new StatusItem (Key.CharMask, "Num", null); _scrolllock = new StatusItem (Key.CharMask, "Scroll", null); _statusBar = new StatusBar () { Visible = true, }; _statusBar.Items = new StatusItem [] { _capslock, _numlock, _scrolllock, new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => { if (_runningScenario is null){ // This causes GetScenarioToRun to return null _runningScenario = null; Application.RequestStop(); } else { _runningScenario.RequestStop(); } }), new StatusItem(Key.F10, "~F10~ Hide/Show Status Bar", () => { _statusBar.Visible = !_statusBar.Visible; _leftPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0); _rightPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0); _top.LayoutSubviews(); _top.SetChildNeedsDisplay(); }), new StatusItem (Key.CharMask, Application.Driver.GetType ().Name, null), }; SetColorScheme (); _top = Application.Top; _top.KeyDown += KeyDownHandler; _top.Add (_menu); _top.Add (_leftPane); _top.Add (_rightPane); _top.Add (_statusBar); void TopHandler () { if (_runningScenario != null) { _runningScenario = null; _isFirstRunning = false; } if (!_isFirstRunning) { _rightPane.SetFocus (); } _top.Loaded -= TopHandler; } _top.Loaded += TopHandler; // The following code was moved to the TopHandler event // because in the MainLoop.EventsPending (wait) // from the Application.RunLoop with the WindowsDriver // the OnReady event is triggered due the Focus event. // On CursesDriver and NetDriver the focus event won't be triggered // and if it's possible I don't know how to do it. //void ReadyHandler () //{ // if (!_isFirstRunning) { // _rightPane.SetFocus (); // } // _top.Ready -= ReadyHandler; //} //_top.Ready += ReadyHandler; Application.Run (_top); return _runningScenario; } static List CreateDiagnosticMenuItems () { List menuItems = new List (); menuItems.Add (CreateDiagnosticFlagsMenuItems ()); menuItems.Add (new MenuItem [] { null }); menuItems.Add (CreateSizeStyle ()); menuItems.Add (CreateAlwaysSetPosition ()); return menuItems; } static MenuItem [] CreateAlwaysSetPosition () { List menuItems = new List (); var item = new MenuItem (); item.Title = "_Always set position (NetDriver only)"; item.Shortcut = Key.CtrlMask | Key.AltMask | (Key)item.Title.ToString ().Substring (1, 1) [0]; item.CheckType |= MenuItemCheckStyle.Checked; item.Checked = Application.AlwaysSetPosition; item.Action += () => { Application.AlwaysSetPosition = !item.Checked; item.Checked = _alwaysSetPosition = Application.AlwaysSetPosition; }; menuItems.Add (item); return menuItems.ToArray (); } static MenuItem [] CreateSizeStyle () { List menuItems = new List (); var item = new MenuItem (); item.Title = "_Height As Buffer"; item.Shortcut = Key.CtrlMask | Key.AltMask | (Key)item.Title.ToString ().Substring (1, 1) [0]; item.CheckType |= MenuItemCheckStyle.Checked; item.Checked = Application.HeightAsBuffer; item.Action += () => { item.Checked = !item.Checked; _heightAsBuffer = item.Checked; Application.HeightAsBuffer = _heightAsBuffer; }; menuItems.Add (item); return menuItems.ToArray (); } static MenuItem [] CreateDiagnosticFlagsMenuItems () { const string OFF = "Diagnostics: _Off"; const string FRAME_RULER = "Diagnostics: Frame _Ruler"; const string FRAME_PADDING = "Diagnostics: _Frame Padding"; var index = 0; List menuItems = new List (); foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { var item = new MenuItem (); item.Title = GetDiagnosticsTitle (diag); item.Shortcut = Key.AltMask + index.ToString () [0]; index++; item.CheckType |= MenuItemCheckStyle.Checked; if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) { item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0; } else { item.Checked = _diagnosticFlags.HasFlag (diag); } item.Action += () => { var t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off); if (item.Title == t && !item.Checked) { _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); item.Checked = true; } else if (item.Title == t && item.Checked) { _diagnosticFlags |= (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); item.Checked = false; } else { var f = GetDiagnosticsEnumValue (item.Title); if (_diagnosticFlags.HasFlag (f)) { SetDiagnosticsFlag (f, false); } else { SetDiagnosticsFlag (f, true); } } foreach (var menuItem in menuItems) { if (menuItem.Title == t) { menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding); } else if (menuItem.Title != t) { menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); } } ConsoleDriver.Diagnostics = _diagnosticFlags; _top.SetNeedsDisplay (); }; menuItems.Add (item); } return menuItems.ToArray (); string GetDiagnosticsTitle (Enum diag) { switch (Enum.GetName (_diagnosticFlags.GetType (), diag)) { case "Off": return OFF; case "FrameRuler": return FRAME_RULER; case "FramePadding": return FRAME_PADDING; } return ""; } Enum GetDiagnosticsEnumValue (ustring title) { switch (title.ToString ()) { case FRAME_RULER: return ConsoleDriver.DiagnosticFlags.FrameRuler; case FRAME_PADDING: return ConsoleDriver.DiagnosticFlags.FramePadding; } return null; } void SetDiagnosticsFlag (Enum diag, bool add) { switch (diag) { case ConsoleDriver.DiagnosticFlags.FrameRuler: if (add) { _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler; } else { _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler; } break; case ConsoleDriver.DiagnosticFlags.FramePadding: if (add) { _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding; } else { _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding; } break; default: _diagnosticFlags = default; break; } } //MenuItem CheckedMenuMenuItem (ustring menuItem, Action action, Func checkFunction) //{ // var mi = new MenuItem (); // mi.Title = menuItem; // mi.Shortcut = Key.AltMask + index.ToString () [0]; // index++; // mi.CheckType |= MenuItemCheckStyle.Checked; // mi.Checked = checkFunction (); // mi.Action = () => { // action?.Invoke (); // mi.Title = menuItem; // mi.Checked = checkFunction (); // }; // return mi; //} //return new MenuItem [] { // CheckedMenuMenuItem ("Use _System Console", // () => { // _useSystemConsole = !_useSystemConsole; // }, // () => _useSystemConsole), // CheckedMenuMenuItem ("Diagnostics: _Frame Padding", // () => { // ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding; // _top.SetNeedsDisplay (); // }, // () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding), // CheckedMenuMenuItem ("Diagnostics: Frame _Ruler", // () => { // ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler; // _top.SetNeedsDisplay (); // }, // () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler), //}; } static void SetColorScheme () { _leftPane.ColorScheme = _baseColorScheme; _rightPane.ColorScheme = _baseColorScheme; _top?.SetNeedsDisplay (); } static ColorScheme _baseColorScheme; static MenuItem [] CreateColorSchemeMenuItems () { List menuItems = new List (); foreach (var sc in Colors.ColorSchemes) { var item = new MenuItem (); item.Title = $"_{sc.Key}"; item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0]; item.CheckType |= MenuItemCheckStyle.Radio; item.Checked = sc.Value == _baseColorScheme; item.Action += () => { _baseColorScheme = sc.Value; SetColorScheme (); foreach (var menuItem in menuItems) { menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _baseColorScheme; } }; menuItems.Add (item); } return menuItems.ToArray (); } private static void _scenarioListView_OpenSelectedItem (EventArgs e) { if (_runningScenario is null) { _scenarioListViewItem = _scenarioListView.SelectedItem; var source = _scenarioListView.Source as ScenarioListDataSource; _runningScenario = (Scenario)Activator.CreateInstance (source.Scenarios [_scenarioListView.SelectedItem]); Application.RequestStop (); } } internal class ScenarioListDataSource : IListDataSource { private readonly int len; public List Scenarios { get; set; } public bool IsMarked (int item) => false; public int Count => Scenarios.Count; public int Length => len; public ScenarioListDataSource (List itemList) { Scenarios = itemList; len = GetMaxLengthItem (); } public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0) { container.Move (col, line); // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item])); RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start); } public void SetMark (int item, bool value) { } int GetMaxLengthItem () { if (Scenarios?.Count == 0) { return 0; } int maxLength = 0; for (int i = 0; i < Scenarios.Count; i++) { var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i])); var sc = $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}"; var l = sc.Length; if (l > maxLength) { maxLength = l; } } return maxLength; } // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461 private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0) { int used = 0; int index = start; while (index < ustr.Length) { (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length); var count = Rune.ColumnWidth (rune); if (used + count >= width) break; driver.AddRune (rune); used += count; index += size; } while (used < width) { driver.AddRune (' '); used++; } } public IList ToList () { return Scenarios; } } /// /// When Scenarios are running we need to override the behavior of the Menu /// and Statusbar to enable Scenarios that use those (or related key input) /// to not be impacted. Same as for tabs. /// /// private static void KeyDownHandler (View.KeyEventEventArgs a) { //if (a.KeyEvent.Key == Key.Tab || a.KeyEvent.Key == Key.BackTab) { // // BUGBUG: Work around Issue #434 by implementing our own TAB navigation // if (_top.MostFocused == _categoryListView) // _top.SetFocus (_rightPane); // else // _top.SetFocus (_leftPane); //} if (a.KeyEvent.IsCapslock) { _capslock.Title = "Caps: On"; _statusBar.SetNeedsDisplay (); } else { _capslock.Title = "Caps: Off"; _statusBar.SetNeedsDisplay (); } if (a.KeyEvent.IsNumlock) { _numlock.Title = "Num: On"; _statusBar.SetNeedsDisplay (); } else { _numlock.Title = "Num: Off"; _statusBar.SetNeedsDisplay (); } if (a.KeyEvent.IsScrolllock) { _scrolllock.Title = "Scroll: On"; _statusBar.SetNeedsDisplay (); } else { _scrolllock.Title = "Scroll: Off"; _statusBar.SetNeedsDisplay (); } } private static void CategoryListView_SelectedChanged (ListViewItemEventArgs e) { if (_categoryListViewItem != _categoryListView.SelectedItem) { _scenarioListViewItem = 0; } _categoryListViewItem = _categoryListView.SelectedItem; var item = _categories [_categoryListView.SelectedItem]; List newlist; if (item.Equals ("All")) { newlist = _scenarios; } else { newlist = _scenarios.Where (t => Scenario.ScenarioCategory.GetCategories (t).Contains (item)).ToList (); } _scenarioListView.Source = new ScenarioListDataSource (newlist); _scenarioListView.SelectedItem = _scenarioListViewItem; } private static void OpenUrl (string url) { try { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { url = url.Replace ("&", "^&"); Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { using (var process = new Process { StartInfo = new ProcessStartInfo { FileName = "xdg-open", Arguments = url, RedirectStandardError = true, RedirectStandardOutput = true, CreateNoWindow = true, UseShellExecute = false } }) { process.Start (); } } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { Process.Start ("open", url); } } catch { throw; } } } }