浏览代码

Merge branch 'copilot/restructure-scenarios-standalone' of https://github.com/gui-cs/Terminal.Gui into copilot/restructure-scenarios-standalone

Tig 1 周之前
父节点
当前提交
e8410f5bd9

+ 8 - 22
Examples/Example/Example.cs

@@ -6,36 +6,22 @@
 
 using Terminal.Gui.App;
 using Terminal.Gui.Configuration;
-using Terminal.Gui.Examples;
 using Terminal.Gui.ViewBase;
 using Terminal.Gui.Views;
 
-[assembly: ExampleMetadata ("Simple Example", "A basic login form demonstrating Terminal.Gui fundamentals")]
-[assembly: ExampleCategory ("Getting Started")]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["a", "d", "m", "i", "n", "Tab", "p", "a", "s", "s", "w", "o", "r", "d", "Enter"], DelayMs = 500, Order = 1)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter"], DelayMs = 500, Order = 2)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Esc"], DelayMs = 100, Order = 3)]
+// Example metadata
+[assembly: Terminal.Gui.Examples.ExampleMetadata ("Simple Example", "A basic login form demonstrating Terminal.Gui fundamentals")]
+[assembly: Terminal.Gui.Examples.ExampleCategory ("Getting Started")]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:500", "a", "d", "m", "i", "n", "Tab", "p", "a", "s", "s", "w", "o", "r", "d", "Enter"], Order = 1)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:500", "Enter"], Order = 2)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Esc"], Order = 3)]
 
 // Override the default configuration for the application to use the Light theme
 ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }""";
 ConfigurationManager.Enable (ConfigLocations.All);
 
-// Check for test context to determine driver
-string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
-string? driverName = null;
-
-if (!string.IsNullOrEmpty (contextJson))
-{
-    ExampleContext? context = ExampleContext.FromJson (contextJson);
-    driverName = context?.DriverName;
-}
-
-IApplication app = Application.Create ();
-
-// Setup automatic key injection for testing
-ExampleContextInjector.SetupAutomaticInjection (app);
-
-app.Init (driverName);
+IApplication app = Application.Create (example: true);
+app.Init ();
 app.Run<ExampleWindow> ();
 
 // Dispose the app to clean up and enable Console.WriteLine below

+ 34 - 4
Examples/ExampleRunner/Program.cs

@@ -2,11 +2,35 @@
 // Example Runner - Demonstrates discovering and running all examples using the example infrastructure
 
 using System.Diagnostics.CodeAnalysis;
+using Terminal.Gui.Configuration;
 using Terminal.Gui.Examples;
 
 [assembly: ExampleMetadata ("Example Runner", "Discovers and runs all examples sequentially")]
 [assembly: ExampleCategory ("Infrastructure")]
 
+// Parse command line arguments
+bool useFakeDriver = args.Contains ("--fake-driver") || args.Contains ("-f");
+int timeout = 5000; // Default timeout in milliseconds
+
+for (var i = 0; i < args.Length; i++)
+{
+    if ((args [i] == "--timeout" || args [i] == "-t") && i + 1 < args.Length)
+    {
+        if (int.TryParse (args [i + 1], out int parsedTimeout))
+        {
+            timeout = parsedTimeout;
+        }
+    }
+}
+
+// Configure ForceDriver via ConfigurationManager if requested
+if (useFakeDriver)
+{
+    Console.WriteLine ("Using FakeDriver (forced via ConfigurationManager)\n");
+    ConfigurationManager.RuntimeConfig = """{ "ForceDriver": "FakeDriver" }""";
+    ConfigurationManager.Enable (ConfigLocations.All);
+}
+
 // Discover examples from the Examples directory
 string? assemblyDir = Path.GetDirectoryName (System.Reflection.Assembly.GetExecutingAssembly ().Location);
 
@@ -63,12 +87,13 @@ foreach (ExampleInfo example in examples)
     Console.Write ($"Running: {example.Name,-40} ");
 
     // Create context for running the example
+    // Note: When running with example mode, the demo keys from attributes will be used
+    // We don't need to inject additional keys via the context
     ExampleContext context = new ()
     {
-        KeysToInject = example.DemoKeyStrokes.OrderBy (ks => ks.Order)
-                          .SelectMany (ks => ks.KeyStrokes)
-                          .ToList (),
-        TimeoutMs = 5000,
+        DriverName = useFakeDriver ? "FakeDriver" : null,
+        KeysToInject = [], // Empty - let example mode handle keys from attributes
+        TimeoutMs = timeout,
         Mode = ExecutionMode.InProcess
     };
 
@@ -101,4 +126,9 @@ foreach (ExampleInfo example in examples)
 
 Console.WriteLine ($"\n=== Summary: {successCount} passed, {failCount} failed ===");
 
+if (useFakeDriver)
+{
+    Console.WriteLine ("\nNote: Tests run with FakeDriver. Some examples may timeout if they don't respond to Esc key.");
+}
+
 return failCount == 0 ? 0 : 1;

+ 8 - 22
Examples/FluentExample/Program.cs

@@ -3,34 +3,20 @@
 
 using Terminal.Gui.App;
 using Terminal.Gui.Drawing;
-using Terminal.Gui.Examples;
 using Terminal.Gui.ViewBase;
 using Terminal.Gui.Views;
 
-[assembly: ExampleMetadata ("Fluent API Example", "Demonstrates the fluent IApplication API with IRunnable pattern")]
-[assembly: ExampleCategory ("API Patterns")]
-[assembly: ExampleCategory ("Controls")]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["CursorDown", "CursorDown", "CursorRight", "Enter"], Order = 1)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Esc"], DelayMs = 100, Order = 2)]
+// Example metadata
+[assembly: Terminal.Gui.Examples.ExampleMetadata ("Fluent API Example", "Demonstrates the fluent IApplication API with IRunnable pattern")]
+[assembly: Terminal.Gui.Examples.ExampleCategory ("API Patterns")]
+[assembly: Terminal.Gui.Examples.ExampleCategory ("Controls")]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["CursorDown", "CursorDown", "CursorRight", "Enter"], Order = 1)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Esc"], Order = 2)]
 
-
-// Check for test context to determine driver
-string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
-string? driverName = null;
-
-if (!string.IsNullOrEmpty (contextJson))
-{
-    ExampleContext? context = ExampleContext.FromJson (contextJson);
-    driverName = context?.DriverName;
-}
-
-IApplication? app = Application.Create ()
-                               .Init (driverName)
+IApplication? app = Application.Create (example: true)
+                               .Init ()
                                .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?;
 

+ 12 - 26
Examples/RunnableWrapperExample/Program.cs

@@ -3,35 +3,21 @@
 
 using Terminal.Gui.App;
 using Terminal.Gui.Drawing;
-using Terminal.Gui.Examples;
 using Terminal.Gui.ViewBase;
 using Terminal.Gui.Views;
 
-[assembly: ExampleMetadata ("Runnable Wrapper Example", "Shows how to wrap any View to make it runnable without implementing IRunnable")]
-[assembly: ExampleCategory ("API Patterns")]
-[assembly: ExampleCategory ("Views")]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["t", "e", "s", "t", "Esc"], Order = 1)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 2)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 3)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 4)]
-[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 5)]
-
-// Check for test context to determine driver
-string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
-string? driverName = null;
-
-if (!string.IsNullOrEmpty (contextJson))
-{
-    ExampleContext? context = ExampleContext.FromJson (contextJson);
-    driverName = context?.DriverName;
-}
-
-IApplication app = Application.Create ();
-
-// Setup automatic key injection for testing
-ExampleContextInjector.SetupAutomaticInjection (app);
-
-app.Init (driverName);
+// Example metadata
+[assembly: Terminal.Gui.Examples.ExampleMetadata ("Runnable Wrapper Example", "Shows how to wrap any View to make it runnable without implementing IRunnable")]
+[assembly: Terminal.Gui.Examples.ExampleCategory ("API Patterns")]
+[assembly: Terminal.Gui.Examples.ExampleCategory ("Views")]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "t", "e", "s", "t", "Esc"], Order = 1)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 2)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 3)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 4)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 5)]
+
+IApplication app = Application.Create (example: true);
+app.Init ();
 
 // Example 1: Use extension method with result extraction
 var textField = new TextField { Width = 40, Text = "Default text" };

+ 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"/>

+ 156 - 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,151 @@ 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 becomes modal.
+    /// </summary>
+    private void SetupExampleMode ()
+    {
+        // Subscribe to SessionBegun to monitor when runnables start
+        SessionBegun += OnSessionBegunForExample;
+    }
+
+    private void OnSessionBegunForExample (object? sender, SessionTokenEventArgs e)
+    {
+        // Only send demo keys once
+        if (_exampleModeDemoKeysSent)
+        {
+            return;
+        }
+
+        // Subscribe to IsModalChanged event on the TopRunnable
+        if (TopRunnable is { })
+        {
+            TopRunnable.IsModalChanged += OnIsModalChangedForExample;
+            
+            // Check if already modal - if so, send keys immediately
+            if (TopRunnable.IsModal)
+            {
+                _exampleModeDemoKeysSent = true;
+                TopRunnable.IsModalChanged -= OnIsModalChangedForExample;
+                SendDemoKeys ();
+            }
+        }
+
+        // Unsubscribe from SessionBegun - we only need to set up the modal listener once
+        SessionBegun -= OnSessionBegunForExample;
+    }
+
+    private void OnIsModalChangedForExample (object? sender, EventArgs<bool> e)
+    {
+        // Only send demo keys once, when a runnable becomes modal (not when it stops being modal)
+        if (_exampleModeDemoKeysSent || !e.Value)
+        {
+            return;
+        }
+
+        // Mark that we've sent the keys
+        _exampleModeDemoKeysSent = true;
+
+        // Unsubscribe - we only need to do this once
+        if (TopRunnable is { })
+        {
+            TopRunnable.IsModalChanged -= OnIsModalChangedForExample;
+        }
+
+        // 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);
+
+        // Send keys asynchronously to avoid blocking the UI thread
+        Task.Run (async () =>
+        {
+            // Default delay between keys is 100ms
+            int currentDelay = 100;
+
+            foreach (var attr in sortedSequences)
+            {
+                // Handle KeyStrokes array
+                if (attr.KeyStrokes is { Length: > 0 })
+                {
+                    foreach (string keyStr in attr.KeyStrokes)
+                    {
+                        // Check for SetDelay command
+                        if (keyStr.StartsWith ("SetDelay:", StringComparison.OrdinalIgnoreCase))
+                        {
+                            string delayValue = keyStr.Substring ("SetDelay:".Length);
+
+                            if (int.TryParse (delayValue, out int newDelay))
+                            {
+                                currentDelay = newDelay;
+                            }
+
+                            continue;
+                        }
+
+                        // Regular key
+                        if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { })
+                        {
+                            // Apply delay before sending key
+                            if (currentDelay > 0)
+                            {
+                                await Task.Delay (currentDelay);
+                            }
+
+                            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++)
+                        {
+                            // Apply delay before sending key
+                            if (currentDelay > 0)
+                            {
+                                await Task.Delay (currentDelay);
+                            }
+
+                            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>

+ 1 - 5
Terminal.Gui/Examples/DemoKeyStrokeSequence.cs

@@ -7,14 +7,10 @@ public class DemoKeyStrokeSequence
 {
     /// <summary>
     ///     Gets or sets the array of keystroke names to inject.
+    ///     Can include special "SetDelay:nnn" commands to change the delay between keys.
     /// </summary>
     public string [] KeyStrokes { get; set; } = [];
 
-    /// <summary>
-    ///     Gets or sets the delay in milliseconds before injecting these keystrokes.
-    /// </summary>
-    public int DelayMs { get; set; } = 0;
-
     /// <summary>
     ///     Gets or sets the order in which this sequence should be executed.
     /// </summary>

+ 8 - 8
Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs

@@ -9,11 +9,15 @@ namespace Terminal.Gui.Examples;
 ///         Multiple instances of this attribute can be applied to a single assembly to define a sequence
 ///         of keystroke injections. The <see cref="Order"/> property controls the execution sequence.
 ///     </para>
+///     <para>
+///         Keystrokes can include special "SetDelay:nnn" entries to change the delay between subsequent keys.
+///         The default delay is 100ms. For example: KeyStrokes = ["SetDelay:500", "Enter", "SetDelay:100", "Tab"]
+///     </para>
 /// </remarks>
 /// <example>
 ///     <code>
-///     [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1, DelayMs = 100)]
-///     [assembly: ExampleDemoKeyStrokes(KeyStrokes = new[] { "Enter" }, Order = 2, DelayMs = 200)]
+///     [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1)]
+///     [assembly: ExampleDemoKeyStrokes(KeyStrokes = ["SetDelay:500", "Enter", "SetDelay:100", "Esc"], Order = 2)]
 ///     </code>
 /// </example>
 [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
@@ -21,7 +25,8 @@ public class ExampleDemoKeyStrokesAttribute : System.Attribute
 {
     /// <summary>
     ///     Gets or sets an array of keystroke names to inject.
-    ///     Each string should be a valid key name that can be parsed by <see cref="Input.Key.TryParse"/>.
+    ///     Each string should be a valid key name that can be parsed by <see cref="Input.Key.TryParse"/>,
+    ///     or a special "SetDelay:nnn" command to change the delay between subsequent keys.
     /// </summary>
     public string []? KeyStrokes { get; set; }
 
@@ -37,11 +42,6 @@ public class ExampleDemoKeyStrokesAttribute : System.Attribute
     /// </summary>
     public int RepeatCount { get; set; } = 1;
 
-    /// <summary>
-    ///     Gets or sets the delay in milliseconds before injecting these keystrokes.
-    /// </summary>
-    public int DelayMs { get; set; } = 0;
-
     /// <summary>
     ///     Gets or sets the order in which this keystroke sequence should be executed
     ///     relative to other <see cref="ExampleDemoKeyStrokesAttribute"/> instances.

+ 0 - 1
Terminal.Gui/Examples/ExampleDiscovery.cs

@@ -110,7 +110,6 @@ public static class ExampleDiscovery
                                new ()
                                {
                                    KeyStrokes = keys.ToArray (),
-                                   DelayMs = attr.DelayMs,
                                    Order = attr.Order
                                });
             }