瀏覽代碼

Simplify example infrastructure with Create(example) parameter

- Added bool example parameter to Application.Create()
- Added static ObservableCollection<IApplication> Apps for external observers
- When example=true, metadata is collected and demo keys are sent when first TopRunnable is modal
- Removed ExampleContextInjector complexity
- Examples now use Application.Create(example: isExample)
- Key injection happens via SessionBegun event monitoring TopRunnable.IsModal
- Clean, simple architecture that allows external observers to subscribe to Apps collection

This addresses @tig's feedback to simplify the approach.

Co-authored-by: tig <[email protected]>
copilot-swe-agent[bot] 1 周之前
父節點
當前提交
401db78b45

+ 3 - 5
Examples/Example/Example.cs

@@ -23,18 +23,16 @@ ConfigurationManager.Enable (ConfigLocations.All);
 // Check for test context to determine driver
 string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
 string? driverName = null;
+var isExample = false;
 
 if (!string.IsNullOrEmpty (contextJson))
 {
     ExampleContext? context = ExampleContext.FromJson (contextJson);
     driverName = context?.DriverName;
+    isExample = true;
 }
 
-IApplication app = Application.Create ();
-
-// Setup automatic key injection for testing
-ExampleContextInjector.SetupAutomaticInjection (app);
-
+IApplication app = Application.Create (example: isExample);
 app.Init (driverName);
 app.Run<ExampleWindow> ();
 

+ 3 - 4
Examples/FluentExample/Program.cs

@@ -17,20 +17,19 @@ using Terminal.Gui.Views;
 // Check for test context to determine driver
 string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
 string? driverName = null;
+var isExample = false;
 
 if (!string.IsNullOrEmpty (contextJson))
 {
     ExampleContext? context = ExampleContext.FromJson (contextJson);
     driverName = context?.DriverName;
+    isExample = true;
 }
 
-IApplication? app = Application.Create ()
+IApplication? app = Application.Create (example: isExample)
                                .Init (driverName)
                                .Run<ColorPickerView> ();
 
-// Setup automatic key injection for testing
-ExampleContextInjector.SetupAutomaticInjection (app);
-
 // Run the application with fluent API - automatically creates, runs, and disposes the runnable
 Color? result = app.GetResult () as Color?;
 

+ 3 - 5
Examples/RunnableWrapperExample/Program.cs

@@ -19,18 +19,16 @@ using Terminal.Gui.Views;
 // Check for test context to determine driver
 string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
 string? driverName = null;
+var isExample = false;
 
 if (!string.IsNullOrEmpty (contextJson))
 {
     ExampleContext? context = ExampleContext.FromJson (contextJson);
     driverName = context?.DriverName;
+    isExample = true;
 }
 
-IApplication app = Application.Create ();
-
-// Setup automatic key injection for testing
-ExampleContextInjector.SetupAutomaticInjection (app);
-
+IApplication app = Application.Create (example: isExample);
 app.Init (driverName);
 
 // Example 1: Use extension method with result extraction

+ 15 - 2
Terminal.Gui/App/Application.Lifecycle.cs

@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
@@ -10,6 +11,11 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Lifecycle (Init/Shutdown)
 {
+    /// <summary>
+    ///     Gets the observable collection of all application instances.
+    ///     External observers can subscribe to this collection to monitor application lifecycle.
+    /// </summary>
+    public static ObservableCollection<IApplication> Apps { get; } = [];
     /// <summary>
     ///     Gets the singleton <see cref="IApplication"/> instance used by the legacy static Application model.
     /// </summary>
@@ -29,6 +35,10 @@ public static partial class Application // Lifecycle (Init/Shutdown)
     /// <summary>
     ///     Creates a new <see cref="IApplication"/> instance.
     /// </summary>
+    /// <param name="example">
+    ///     If <see langword="true"/>, the application will run in example mode where metadata is collected
+    ///     and demo keys are automatically sent when the first TopRunnable is modal.
+    /// </param>
     /// <remarks>
     ///     The recommended pattern is for developers to call <c>Application.Create()</c> and then use the returned
     ///     <see cref="IApplication"/> instance for all subsequent application operations.
@@ -37,12 +47,15 @@ public static partial class Application // Lifecycle (Init/Shutdown)
     /// <exception cref="InvalidOperationException">
     ///     Thrown if the legacy static Application model has already been used in this process.
     /// </exception>
-    public static IApplication Create ()
+    public static IApplication Create (bool example = false)
     {
         //Debug.Fail ("Application.Create() called");
         ApplicationImpl.MarkInstanceBasedModelUsed ();
 
-        return new ApplicationImpl ();
+        ApplicationImpl app = new () { IsExample = example };
+        Apps.Add (app);
+
+        return app;
     }
 
     /// <inheritdoc cref="IApplication.Init"/>

+ 100 - 0
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

@@ -11,6 +11,9 @@ internal partial class ApplicationImpl
     /// <inheritdoc/>
     public bool Initialized { get; set; }
 
+    /// <inheritdoc/>
+    public bool IsExample { get; set; }
+
     /// <inheritdoc/>
     public event EventHandler<EventArgs<bool>>? InitializedChanged;
 
@@ -93,6 +96,12 @@ internal partial class ApplicationImpl
         RaiseInitializedChanged (this, new (true));
         SubscribeDriverEvents ();
 
+        // Setup example mode if requested
+        if (IsExample)
+        {
+            SetupExampleMode ();
+        }
+
         SynchronizationContext.SetSynchronizationContext (new ());
         MainThreadId = Thread.CurrentThread.ManagedThreadId;
 
@@ -381,4 +390,95 @@ internal partial class ApplicationImpl
         Application.Force16ColorsChanged -= OnForce16ColorsChanged;
         Application.ForceDriverChanged -= OnForceDriverChanged;
     }
+
+    #region Example Mode
+
+    private bool _exampleModeDemoKeysSent;
+
+    /// <summary>
+    ///     Sets up example mode functionality - collecting metadata and sending demo keys
+    ///     when the first TopRunnable is modal.
+    /// </summary>
+    private void SetupExampleMode ()
+    {
+        // Subscribe to SessionBegun to wait for the first modal runnable
+        SessionBegun += OnSessionBegunForExample;
+    }
+
+    private void OnSessionBegunForExample (object? sender, SessionTokenEventArgs e)
+    {
+        // Only send demo keys once, when the first modal runnable appears
+        if (_exampleModeDemoKeysSent)
+        {
+            return;
+        }
+
+        // Check if the TopRunnable is modal
+        if (TopRunnable?.IsModal != true)
+        {
+            return;
+        }
+
+        // Mark that we've sent the keys
+        _exampleModeDemoKeysSent = true;
+
+        // Unsubscribe - we only need to do this once
+        SessionBegun -= OnSessionBegunForExample;
+
+        // Send demo keys from assembly attributes
+        SendDemoKeys ();
+    }
+
+    private void SendDemoKeys ()
+    {
+        // Get the entry assembly to read example metadata
+        var assembly = System.Reflection.Assembly.GetEntryAssembly ();
+
+        if (assembly is null)
+        {
+            return;
+        }
+
+        // Look for ExampleDemoKeyStrokesAttribute
+        var demoKeyAttributes = assembly.GetCustomAttributes (typeof (Terminal.Gui.Examples.ExampleDemoKeyStrokesAttribute), false)
+                                        .OfType<Terminal.Gui.Examples.ExampleDemoKeyStrokesAttribute> ()
+                                        .ToList ();
+
+        if (!demoKeyAttributes.Any ())
+        {
+            return;
+        }
+
+        // Sort by Order and collect all keystrokes
+        var sortedSequences = demoKeyAttributes.OrderBy<Terminal.Gui.Examples.ExampleDemoKeyStrokesAttribute, int> (a => a.Order);
+
+        foreach (var attr in sortedSequences)
+        {
+            // Handle KeyStrokes array
+            if (attr.KeyStrokes is { Length: > 0 })
+            {
+                foreach (string keyStr in attr.KeyStrokes)
+                {
+                    if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { })
+                    {
+                        Keyboard?.RaiseKeyDownEvent (key);
+                    }
+                }
+            }
+
+            // Handle RepeatKey
+            if (!string.IsNullOrEmpty (attr.RepeatKey))
+            {
+                if (Input.Key.TryParse (attr.RepeatKey, out Input.Key? key) && key is { })
+                {
+                    for (var i = 0; i < attr.RepeatCount; i++)
+                    {
+                        Keyboard?.RaiseKeyDownEvent (key);
+                    }
+                }
+            }
+        }
+    }
+
+    #endregion Example Mode
 }

+ 6 - 0
Terminal.Gui/App/IApplication.cs

@@ -86,6 +86,12 @@ public interface IApplication : IDisposable
     /// <summary>Gets or sets whether the application has been initialized.</summary>
     bool Initialized { get; set; }
 
+    /// <summary>
+    ///     Gets or sets a value indicating whether this application is running in example mode.
+    ///     When <see langword="true"/>, metadata is collected and demo keys are automatically sent.
+    /// </summary>
+    bool IsExample { get; set; }
+
     /// <summary>
     ///     INTERNAL: Resets the state of this instance. Called by Dispose.
     /// </summary>