#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Terminal.Gui;
namespace UICatalog;
///
/// Base class for each demo/scenario.
///
/// To define a new scenario:
///
/// -
///
/// Create a new .cs file in the Scenarios directory that derives from
/// .
///
///
/// -
///
/// Annotate the derived class with a
/// attribute specifying the scenario's name and description.
///
///
/// -
///
/// Add one or more attributes to the class specifying
/// which categories the scenario belongs to. If you don't specify a category the scenario will show up
/// in "_All".
///
///
/// -
///
/// Implement the override which will be called when a user selects the
/// scenario to run.
///
///
///
///
///
/// The UI Catalog program uses reflection to find all scenarios and adds them to the ListViews. Press ENTER to
/// run the selected scenario. Press the default quit key to quit.
///
///
///
/// The example below is provided in the `Scenarios` directory as a generic sample that can be copied and re-named:
///
/// using Terminal.Gui;
///
/// namespace UICatalog.Scenarios;
///
/// [ScenarioMetadata ("Generic", "Generic sample - A template for creating new Scenarios")]
/// [ScenarioCategory ("Controls")]
/// public sealed class MyScenario : Scenario
/// {
/// public override void Main ()
/// {
/// // Init
/// Application.Init ();
///
/// // Setup - Create a top-level application window and configure it.
/// Window appWindow = new ()
/// {
/// Title = GetQuitKeyAndName (),
/// };
///
/// var button = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" };
/// button.Accept += (s, e) => MessageBox.ErrorQuery ("Error", "You pressed the button!", "Ok");
/// appWindow.Add (button);
///
/// // Run - Start the application.
/// Application.Run (appWindow);
/// appWindow.Dispose ();
///
/// // Shutdown - Calling Application.Shutdown is required.
/// Application.Shutdown ();
/// }
/// }
///
///
public class Scenario : IDisposable
{
private static int _maxScenarioNameLen = 30;
public string TopLevelColorScheme { get; set; } = "Base";
public BenchmarkResults BenchmarkResults
{
get { return _benchmarkResults; }
}
private bool _disposedValue;
///
/// Helper function to get the list of categories a belongs to (defined in
/// )
///
/// list of category names
public List GetCategories () { return ScenarioCategory.GetCategories (GetType ()); }
/// Helper to get the Description (defined in )
///
public string GetDescription () { return ScenarioMetadata.GetDescription (GetType ()); }
/// Helper to get the Name (defined in )
///
public string GetName () { return ScenarioMetadata.GetName (GetType ()); }
///
/// Helper to get the and the Name (defined in
/// )
///
///
public string GetQuitKeyAndName () { return $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"; }
///
/// Returns a list of all instanaces defined in the project, sorted by
/// .
/// https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class
///
public static ObservableCollection GetScenarios ()
{
List objects = [];
foreach (Type type in typeof (Scenario).Assembly.ExportedTypes
.Where (
myType => myType is { IsClass: true, IsAbstract: false }
&& myType.IsSubclassOf (typeof (Scenario))
))
{
if (Activator.CreateInstance (type) is not Scenario { } scenario)
{
continue;
}
objects.Add (scenario);
_maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1);
}
return new (objects.OrderBy (s => s.GetName ()).ToList ());
}
///
/// Called by UI Catalog to run the . This is the main entry point for the
/// .
///
public virtual void Main () { }
private const uint MAX_NATURAL_ITERATIONS = 500; // not including needed for demo keys
private const uint ABORT_TIMEOUT_MS = 2500;
private const int DEMO_KEY_PACING_MS = 1; // Must be non-zero
private readonly object _timeoutLock = new ();
private object? _timeout;
private Stopwatch? _stopwatch;
private readonly BenchmarkResults _benchmarkResults = new BenchmarkResults ();
public void StartBenchmark ()
{
BenchmarkResults.Scenario = GetName ();
Application.InitializedChanged += OnApplicationOnInitializedChanged;
}
public BenchmarkResults EndBenchmark ()
{
Application.InitializedChanged -= OnApplicationOnInitializedChanged;
lock (_timeoutLock)
{
if (_timeout is { })
{
_timeout = null;
}
}
return _benchmarkResults;
}
private List _demoKeys;
private int _currentDemoKey = 0;
private void OnApplicationOnInitializedChanged (object? s, EventArgs a)
{
if (a.CurrentValue)
{
lock (_timeoutLock!)
{
_timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (ABORT_TIMEOUT_MS), ForceCloseCallback);
}
Application.Iteration += OnApplicationOnIteration;
Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++;
Application.Driver!.Refreshed += (sender, args) =>
{
BenchmarkResults.RefreshedCount++;
if (args.CurrentValue)
{
BenchmarkResults.UpdatedCount++;
}
};
Application.NotifyNewRunState += OnApplicationNotifyNewRunState;
_stopwatch = Stopwatch.StartNew ();
}
else
{
Application.NotifyNewRunState -= OnApplicationNotifyNewRunState;
Application.Iteration -= OnApplicationOnIteration;
BenchmarkResults.Duration = _stopwatch!.Elapsed;
_stopwatch?.Stop ();
}
}
private void OnApplicationOnIteration (object? s, IterationEventArgs a)
{
BenchmarkResults.IterationCount++;
if (BenchmarkResults.IterationCount > MAX_NATURAL_ITERATIONS + (_demoKeys.Count* DEMO_KEY_PACING_MS))
{
Application.RequestStop ();
}
}
private void OnApplicationNotifyNewRunState (object? sender, RunStateEventArgs e)
{
// Get a list of all subviews under Application.Top (and their subviews, etc.)
// and subscribe to their DrawComplete event
void SubscribeAllSubviews (View view)
{
view.DrawComplete += (s, a) => BenchmarkResults.DrawCompleteCount++;
view.SubviewsLaidOut += (s, a) => BenchmarkResults.LaidOutCount++;
foreach (View subview in view.Subviews)
{
SubscribeAllSubviews (subview);
}
}
SubscribeAllSubviews (Application.Top!);
_currentDemoKey = 0;
_demoKeys = GetDemoKeyStrokes ();
Application.AddTimeout (
new TimeSpan (0, 0, 0, 0, DEMO_KEY_PACING_MS),
() =>
{
if (_currentDemoKey >= _demoKeys.Count)
{
return false;
}
Application.RaiseKeyDownEvent (_demoKeys [_currentDemoKey++]);
return true;
});
}
// If the scenario doesn't close within the abort time, this will force it to quit
private bool ForceCloseCallback ()
{
lock (_timeoutLock)
{
if (_timeout is { })
{
_timeout = null;
}
}
Debug.WriteLine ($@" Failed to Quit with {Application.QuitKey} after {ABORT_TIMEOUT_MS}ms and {BenchmarkResults.IterationCount} iterations. Force quit.");
Application.RequestStop ();
return false;
}
/// Gets the Scenario Name + Description with the Description padded based on the longest known Scenario name.
///
public override string ToString () { return $"{GetName ().PadRight (_maxScenarioNameLen)}{GetDescription ()}"; }
#region IDispose
public void Dispose ()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose (true);
GC.SuppressFinalize (this);
}
protected virtual void Dispose (bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{ }
_disposedValue = true;
}
}
#endregion IDispose
/// Returns a list of all Categories set by all of the s defined in the project.
internal static ObservableCollection GetAllCategories ()
{
List aCategories = [];
aCategories = typeof (Scenario).Assembly.GetTypes ()
.Where (
myType => myType is { IsClass: true, IsAbstract: false }
&& myType.IsSubclassOf (typeof (Scenario)))
.Select (type => System.Attribute.GetCustomAttributes (type).ToList ())
.Aggregate (
aCategories,
(current, attrs) => current
.Union (
attrs.Where (a => a is ScenarioCategory)
.Select (a => ((ScenarioCategory)a).Name))
.ToList ());
// Sort
ObservableCollection categories = new (aCategories.OrderBy (c => c).ToList ());
// Put "All" at the top
categories.Insert (0, "All Scenarios");
return categories;
}
public virtual List GetDemoKeyStrokes () => new List ();
}