#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 (); }