using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
#nullable enable
namespace UICatalog;
///
/// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on
/// the command line) and each time a Scenario ends.
///
public class UICatalogRunnable : Runnable
{
// When a scenario is run, the main app is killed. The static
// members are cached so that when the scenario exits the
// main app UI can be restored to previous state
// Note, we used to pass this to scenarios that run, but it just added complexity
// So that was removed. But we still have this here to demonstrate how changing
// the scheme works.
public static string? CachedRunnableScheme { get; set; }
// Diagnostics
private static ViewDiagnosticFlags _diagnosticFlags;
public UICatalogRunnable ()
{
_diagnosticFlags = Diagnostics;
_menuBar = CreateMenuBar ();
_statusBar = CreateStatusBar ();
_categoryList = CreateCategoryList ();
_scenarioList = CreateScenarioList ();
Add (_menuBar, _categoryList, _scenarioList, _statusBar);
IsModalChanged += IsModalChangedHandler;
IsRunningChanged += IsRunningChangedHandler;
// Restore previous selections
if (_categoryList.Source?.Count > 0)
{
_categoryList.SelectedItem = _cachedCategoryIndex ?? 0;
}
else
{
_categoryList.SelectedItem = null;
}
_scenarioList.SelectedRow = _cachedScenarioIndex;
SchemeName = CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base);
ConfigurationManager.Applied += ConfigAppliedHandler;
}
private static bool _isFirstRunning = true;
private void IsModalChangedHandler (object? sender, EventArgs args)
{
if (!args.Value)
{
return;
}
if (_disableMouseCb is { })
{
_disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked;
}
if (_shVersion is { })
{
_shVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Application.Driver!.GetVersionInfo ()}";
}
if (CachedSelectedScenario != null)
{
CachedSelectedScenario = null;
_isFirstRunning = false;
}
if (!_isFirstRunning)
{
_scenarioList.SetFocus ();
}
if (_statusBar is { })
{
_statusBar.VisibleChanged += (s, e) => { ShowStatusBar = _statusBar.Visible; };
}
IsModalChanged -= IsModalChangedHandler;
_categoryList!.EnsureSelectedItemVisible ();
_scenarioList.EnsureSelectedCellIsVisible ();
}
private void IsRunningChangedHandler (object? sender, EventArgs args)
{
if (!args.Value)
{
ConfigurationManager.Applied -= ConfigAppliedHandler;
IsRunningChanged -= IsRunningChangedHandler;
}
}
#region MenuBar
private readonly MenuBar? _menuBar;
private CheckBox? _force16ColorsMenuItemCb;
private OptionSelector? _themesSelector;
private OptionSelector? _topSchemesSelector;
private OptionSelector? _logLevelSelector;
private FlagSelector? _diagnosticFlagsSelector;
private CheckBox? _disableMouseCb;
private MenuBar CreateMenuBar ()
{
MenuBar menuBar = new (
[
new (
"_File",
[
new MenuItem ()
{
Title ="_Quit",
HelpText = "Quit UI Catalog",
Key = Application.QuitKey,
// By not specifying TargetView the Key Binding will be Application-level
Command = Command.Quit
}
]),
new ("_Themes", CreateThemeMenuItems ()),
new ("Diag_nostics", CreateDiagnosticMenuItems ()),
new ("_Logging", CreateLoggingMenuItems ()),
new (
"_Help",
[
new MenuItem (
"_Documentation",
"API docs",
() => OpenUrl ("https://gui-cs.github.io/Terminal.Gui"),
Key.F1
),
new MenuItem (
"_README",
"Project readme",
() => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"),
Key.F2
),
new MenuItem (
"_About...",
"About UI Catalog",
() => MessageBox.Query (
App,
"",
GetAboutBoxMessage (),
wrapMessage: false,
buttons: "_Ok"
),
Key.A.WithCtrl
)
])
])
{
Title = "menuBar",
Id = "menuBar"
};
return menuBar;
View [] CreateThemeMenuItems ()
{
List menuItems = [];
_force16ColorsMenuItemCb = new ()
{
Title = "Force _16 Colors",
CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
// Best practice for CheckBoxes in menus is to disable focus and highlight states
CanFocus = false,
HighlightStates = MouseState.None
};
_force16ColorsMenuItemCb.CheckedStateChanging += (sender, args) =>
{
if (Application.Driver!.Force16Colors
&& args.Result == CheckState.UnChecked
&& !Application.Driver!.SupportsTrueColor)
{
args.Handled = true;
}
};
_force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) =>
{
Application.Driver!.Force16Colors = args.Value == CheckState.Checked;
_force16ColorsShortcutCb!.CheckedState = args.Value;
SetNeedsDraw ();
};
menuItems.Add (
new MenuItem
{
CommandView = _force16ColorsMenuItemCb
});
menuItems.Add (new Line ());
if (ConfigurationManager.IsEnabled)
{
_themesSelector = new ()
{
// HighlightStates = MouseState.In,
CanFocus = true,
// InvertFocusAttribute = true
};
_themesSelector.ValueChanged += (_, args) =>
{
if (args.Value is null)
{
return;
}
ThemeManager.Theme = ThemeManager.GetThemeNames () [(int)args.Value];
};
var menuItem = new MenuItem
{
CommandView = _themesSelector,
HelpText = "Cycle Through Themes",
Key = Key.T.WithCtrl
};
menuItems.Add (menuItem);
menuItems.Add (new Line ());
_topSchemesSelector = new ()
{
// HighlightStates = MouseState.In,
};
_topSchemesSelector.ValueChanged += (_, args) =>
{
if (args.Value is null)
{
return;
}
CachedRunnableScheme = SchemeManager.GetSchemesForCurrentTheme ()!.Keys.ToArray () [(int)args.Value];
SchemeName = CachedRunnableScheme;
SetNeedsDraw ();
};
menuItem = new ()
{
Title = "Scheme for Runnable",
SubMenu = new (
[
new ()
{
CommandView = _topSchemesSelector,
HelpText = "Cycle Through schemes",
Key = Key.S.WithCtrl
}
])
};
menuItems.Add (menuItem);
UpdateThemesMenu ();
}
else
{
menuItems.Add (new MenuItem ()
{
Title = "Configuration Manager is not Enabled",
Enabled = false
});
}
return menuItems.ToArray ();
}
View [] CreateDiagnosticMenuItems ()
{
List menuItems = [];
_diagnosticFlagsSelector = new ()
{
Styles = SelectorStyles.ShowNoneFlag,
CanFocus = true
};
_diagnosticFlagsSelector.UsedHotKeys.Add (Key.D);
_diagnosticFlagsSelector.AssignHotKeys = true;
_diagnosticFlagsSelector.Value = Diagnostics;
_diagnosticFlagsSelector.Activating += (sender, args) =>
{
_diagnosticFlags = (ViewDiagnosticFlags)((int)args.Context!.Source!.Data!);// (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
Diagnostics = _diagnosticFlags;
};
MenuItem diagFlagMenuItem = new MenuItem ()
{
CommandView = _diagnosticFlagsSelector,
HelpText = "View Diagnostics"
};
diagFlagMenuItem.Accepting += (sender, args) =>
{
//_diagnosticFlags = (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
//Diagnostics = _diagnosticFlags;
//args.Handled = true;
};
menuItems.Add (diagFlagMenuItem);
menuItems.Add (new Line ());
_disableMouseCb = new ()
{
Title = "_Disable Mouse",
CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked,
// Best practice for CheckBoxes in menus is to disable focus and highlight states
CanFocus = false,
HighlightStates = MouseState.None
};
//_disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; };
_disableMouseCb.Activating += (sender, args) =>
{
Application.IsMouseDisabled = !Application.IsMouseDisabled;
_disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.None;
};
menuItems.Add (
new MenuItem
{
CommandView = _disableMouseCb,
HelpText = "Disable Mouse"
});
return menuItems.ToArray ();
}
View [] CreateLoggingMenuItems ()
{
List menuItems = [];
LogLevel [] logLevels = Enum.GetValues ();
_logLevelSelector = new ()
{
AssignHotKeys = true,
Labels = Enum.GetNames (),
Value = logLevels.ToList ().IndexOf (Enum.Parse (UICatalog.Options.DebugLogLevel)),
// HighlightStates = MouseState.In,
};
_logLevelSelector.ValueChanged += (_, args) =>
{
UICatalog.Options = UICatalog.Options with { DebugLogLevel = Enum.GetName (logLevels [args.Value!.Value])! };
UICatalog.LogLevelSwitch.MinimumLevel =
UICatalog.LogLevelToLogEventLevel (Enum.Parse (UICatalog.Options.DebugLogLevel));
};
menuItems.Add (
new MenuItem
{
CommandView = _logLevelSelector,
HelpText = "Cycle Through Log Levels",
Key = Key.L.WithCtrl
});
// add a separator
menuItems.Add (new Line ());
menuItems.Add (
new MenuItem (
"_Open Log Folder",
string.Empty,
() => OpenUrl (UICatalog.LOGFILE_LOCATION)
));
return menuItems.ToArray ()!;
}
}
private void UpdateThemesMenu ()
{
if (_themesSelector is null)
{
return;
}
_themesSelector.Value = null;
_themesSelector.AssignHotKeys = true;
_themesSelector.UsedHotKeys.Clear ();
_themesSelector.Labels = ThemeManager.GetThemeNames ().ToArray ();
_themesSelector.Value = ThemeManager.GetThemeNames ().IndexOf (ThemeManager.GetCurrentThemeName ());
if (_topSchemesSelector is null)
{
return;
}
_topSchemesSelector.AssignHotKeys = true;
_topSchemesSelector.UsedHotKeys.Clear ();
int? selectedScheme = _topSchemesSelector.Value;
_topSchemesSelector.Labels = SchemeManager.GetSchemeNames ().ToArray ();
_topSchemesSelector.Value = selectedScheme;
if (CachedRunnableScheme is null || !SchemeManager.GetSchemeNames ().Contains (CachedRunnableScheme))
{
CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base);
}
int newSelectedItem = SchemeManager.GetSchemeNames ().IndexOf (CachedRunnableScheme!);
// if the item is in bounds then select it
if (newSelectedItem >= 0 && newSelectedItem < SchemeManager.GetSchemeNames ().Count)
{
_topSchemesSelector.Value = newSelectedItem;
}
}
#endregion MenuBar
#region Scenario List
private readonly TableView _scenarioList;
private static int _cachedScenarioIndex;
public static ObservableCollection? CachedScenarios { get; set; }
// If set, holds the scenario the user selected to run
public static Scenario? CachedSelectedScenario { get; set; }
private TableView CreateScenarioList ()
{
// Create the scenario list. The contents of the scenario list changes whenever the
// Category list selection changes (to show just the scenarios that belong to the selected
// category).
TableView scenarioList = new ()
{
X = Pos.Right (_categoryList!) - 1,
Y = Pos.Bottom (_menuBar!),
Width = Dim.Fill (),
Height = Dim.Fill (Dim.Func (v => v!.Frame.Height, _statusBar)),
//AllowsMarking = false,
CanFocus = true,
Title = "_Scenarios",
BorderStyle = _categoryList!.BorderStyle,
SuperViewRendersLineCanvas = true
};
// TableView provides many options for table headers. For simplicity, we turn all
// of these off. By enabling FullRowSelect and turning off headers, TableView looks just
// like a ListView
scenarioList.FullRowSelect = true;
scenarioList.Style.ShowHeaders = false;
scenarioList.Style.ShowHorizontalHeaderOverline = false;
scenarioList.Style.ShowHorizontalHeaderUnderline = false;
scenarioList.Style.ShowHorizontalBottomline = false;
scenarioList.Style.ShowVerticalCellLines = false;
scenarioList.Style.ShowVerticalHeaderLines = false;
/* By default, TableView lays out columns at render time and only
* measures y rows of data at a time. Where y is the height of the
* console. This is for the following reasons:
*
* - Performance, when tables have a large amount of data
* - Defensive, prevents a single wide cell value pushing other
* columns off-screen (requiring horizontal scrolling
*
* In the case of UICatalog here, such an approach is overkill so
* we just measure all the data ourselves and set the appropriate
* max widths as ColumnStyles
*/
int longestName = CachedScenarios!.Max (s => s.GetName ().Length);
scenarioList.Style.ColumnStyles.Add (
0,
new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
);
scenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 });
scenarioList.CellActivated += ScenarioView_OpenSelectedItem;
// TableView typically is a grid where nav keys are biased for moving left/right.
scenarioList.KeyBindings.Remove (Key.Home);
scenarioList.KeyBindings.Add (Key.Home, Command.Start);
scenarioList.KeyBindings.Remove (Key.End);
scenarioList.KeyBindings.Add (Key.End, Command.End);
// Ideally, TableView.MultiSelect = false would turn off any keybindings for
// multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
// a shortcut to About.
scenarioList.MultiSelect = false;
scenarioList.KeyBindings.Remove (Key.A.WithCtrl);
return scenarioList;
}
/// Launches the selected scenario, setting the global _selectedScenario
///
private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e)
{
if (CachedSelectedScenario is null)
{
// Save selected item state
_cachedCategoryIndex = _categoryList!.SelectedItem;
_cachedScenarioIndex = _scenarioList.SelectedRow;
// Create new instance of scenario (even though Scenarios contains instances)
var selectedScenarioName = (string)_scenarioList.Table [_scenarioList.SelectedRow, 0];
CachedSelectedScenario = (Scenario)Activator.CreateInstance (
CachedScenarios!.FirstOrDefault (
s => s.GetName ()
== selectedScenarioName
)!
.GetType ()
)!;
// Tell the main app to stop
Application.RequestStop ();
}
}
#endregion Scenario List
#region Category List
private readonly ListView? _categoryList;
private static int? _cachedCategoryIndex;
public static ObservableCollection? CachedCategories { get; set; }
private ListView CreateCategoryList ()
{
// Create the Category list view. This list never changes.
ListView categoryList = new ()
{
X = 0,
Y = Pos.Bottom (_menuBar!),
Width = Dim.Auto (),
Height = Dim.Fill (Dim.Func (v => v!.Frame.Height, _statusBar)),
AllowsMarking = false,
CanFocus = true,
Title = "_Categories",
BorderStyle = LineStyle.Rounded,
SuperViewRendersLineCanvas = true,
Source = new ListWrapper (CachedCategories)
};
categoryList.OpenSelectedItem += (s, a) => { _scenarioList!.SetFocus (); };
categoryList.SelectedItemChanged += CategoryView_SelectedChanged;
// This enables the scrollbar by causing lazy instantiation to happen
categoryList.VerticalScrollBar.AutoShow = true;
return categoryList;
}
private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
{
if (e is null or { Item: null })
{
return;
}
string item = CachedCategories! [e.Item.Value];
ObservableCollection newScenarioList;
if (e.Item == 0)
{
// First category is "All"
newScenarioList = CachedScenarios!;
}
else
{
newScenarioList = new (CachedScenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ());
}
_scenarioList.Table = new EnumerableTableSource (
newScenarioList,
new ()
{
{ "Name", s => s.GetName () }, { "Description", s => s.GetDescription () }
}
);
}
#endregion Category List
#region StatusBar
private readonly StatusBar? _statusBar;
[ConfigurationProperty (Scope = typeof (AppSettingsScope), OmitClassName = true)]
[JsonPropertyName ("UICatalog.StatusBar")]
public static bool ShowStatusBar { get; set; } = true;
private Shortcut? _shQuit;
private Shortcut? _shVersion;
private CheckBox? _force16ColorsShortcutCb;
private StatusBar CreateStatusBar ()
{
StatusBar statusBar = new ()
{
Visible = ShowStatusBar,
AlignmentModes = AlignmentModes.IgnoreFirstOrLast,
CanFocus = false
};
// ReSharper disable All
statusBar.Height = Dim.Auto (
DimAutoStyle.Auto,
minimumContentDim: Dim.Func (_ => statusBar.Visible ? 1 : 0),
maximumContentDim: Dim.Func (_ => statusBar.Visible ? 1 : 0));
// ReSharper restore All
_shQuit = new ()
{
CanFocus = false,
Title = "Quit",
Key = Application.QuitKey
};
_shVersion = new ()
{
Title = "Version Info",
CanFocus = false
};
var statusBarShortcut = new Shortcut
{
Key = Key.F10,
Title = "Show/Hide Status Bar",
CanFocus = false
};
statusBarShortcut.Accepting += (sender, args) =>
{
statusBar.Visible = !_statusBar!.Visible;
args.Handled = true;
};
_force16ColorsShortcutCb = new ()
{
Title = "16 color mode",
CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
CanFocus = true
};
Shortcut force16ColorsShortcut = new ()
{
CanFocus = false,
CommandView = _force16ColorsShortcutCb,
HelpText = "",
BindKeyToApplication = true,
Key = Key.F7
};
force16ColorsShortcut.Accepting += (sender, args) =>
{
Application.Driver.Force16Colors = !Application.Driver.Force16Colors;
_force16ColorsMenuItemCb!.CheckedState = Application.Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
SetNeedsDraw ();
args.Handled = true;
};
statusBar.Add (
_shQuit,
statusBarShortcut,
force16ColorsShortcut,
_shVersion
);
if (UICatalog.Options.DontEnableConfigurationManagement)
{
statusBar.AddShortcutAt (statusBar.SubViews.ToList ().IndexOf (_shVersion), new Shortcut () { Title = "CM is Disabled" });
}
return statusBar;
}
#endregion StatusBar
#region Configuration Manager
///
/// Called when CM has applied changes.
///
private void ConfigApplied ()
{
UpdateThemesMenu ();
SchemeName = CachedRunnableScheme;
if (_shQuit is { })
{
_shQuit.Key = Application.QuitKey;
}
if (_statusBar is { })
{
_statusBar.Visible = ShowStatusBar;
}
_disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked;
_force16ColorsShortcutCb!.CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
Application.TopRunnableView?.SetNeedsDraw ();
}
private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigApplied (); }
#endregion Configuration Manager
///
/// Gets the message displayed in the About Box. `public` so it can be used from Unit tests.
///
///
public static string GetAboutBoxMessage ()
{
// NOTE: Do not use multiline verbatim strings here.
// WSL gets all confused.
StringBuilder msg = new ();
msg.AppendLine ("UI Catalog: A comprehensive sample library and test app for");
msg.AppendLine ();
msg.AppendLine (
"""
_______ _ _ _____ _
|__ __| (_) | | / ____| (_)
| | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _
| |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | |
| | __/ | | | | | | | | | | | (_| | || |__| | |_| | |
|_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_|
""");
msg.AppendLine ();
msg.AppendLine ("v2 - Pre-Alpha");
msg.AppendLine ();
msg.AppendLine ("https://github.com/gui-cs/Terminal.Gui");
return msg.ToString ();
}
public static void OpenUrl (string url)
{
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 ()
{
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);
}
}
}