瀏覽代碼

Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop

Tig 3 月之前
父節點
當前提交
455ebc4548

+ 15 - 2
.github/workflows/build-release.yml

@@ -30,5 +30,18 @@ jobs:
     - name: Pack Release Terminal.Gui
       run: dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages
 
-    - name: Build Release Solution
-      run: dotnet build --configuration Release
+    - name: Restore AOT and Self-Contained projects
+      run: |
+        dotnet restore ./Examples/NativeAot/NativeAot.csproj -f
+        dotnet restore ./Examples/SelfContained/SelfContained.csproj -f
+
+    - name: Restore Solution Packages
+      run: dotnet restore
+
+    - name: Build Release AOT and Self-Contained
+      run: |
+        dotnet build ./Examples/NativeAot/NativeAot.csproj --configuration Release
+        dotnet build ./Examples/SelfContained/SelfContained.csproj --configuration Release
+
+    - name: Build Release Solution without restore
+      run: dotnet build --configuration Release --no-restore

+ 2 - 8
Terminal.Gui/App/Application.Keyboard.cs

@@ -98,17 +98,11 @@ public static partial class Application // Keyboard handling
             }
             else
             {
-                // BUGBUG: this seems unneeded.
-                if (!KeyBindings.TryGet (key, out KeyBinding keybinding))
-                {
-                    return null;
-                }
-
                 bool? toReturn = null;
 
-                foreach (Command command in keybinding.Commands)
+                foreach (Command command in binding.Commands)
                 {
-                    toReturn = InvokeCommand (command, key, keybinding);
+                    toReturn = InvokeCommand (command, key, binding);
                 }
 
                 handled = toReturn ?? true;

+ 5 - 9
Terminal.Gui/App/Application.Mouse.cs

@@ -185,11 +185,7 @@ public static partial class Application // Mouse handling
             && Popover?.GetActivePopover () as View is { Visible: true } visiblePopover
             && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false)
         {
-            // TODO: Build a use/test case for the popover not handling Quit
-            if (visiblePopover.InvokeCommand (Command.Quit) is true && visiblePopover.Visible)
-            {
-                visiblePopover.Visible = false;
-            }
+            ApplicationPopover.HideWithQuitCommand (visiblePopover);
 
             // Recurse once so the event can be handled below the popover
             RaiseMouseEvent (mouseEvent);
@@ -205,10 +201,10 @@ public static partial class Application // Mouse handling
         if (Initialized)
         {
             WantContinuousButtonPressedView = deepestViewUnderMouse switch
-                                              {
-                                                  { WantContinuousButtonPressed: true } => deepestViewUnderMouse,
-                                                  _ => null
-                                              };
+            {
+                { WantContinuousButtonPressed: true } => deepestViewUnderMouse,
+                _ => null
+            };
         }
 
         // May be null before the prior condition or the condition may set it as null.

+ 1 - 5
Terminal.Gui/App/Application.Run.cs

@@ -577,11 +577,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
 
         if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
         {
-            // TODO: Build a use/test case for the popover not handling Quit
-            if (visiblePopover.InvokeCommand (Command.Quit) is true && visiblePopover.Visible)
-            {
-                visiblePopover.Visible = false;
-            }
+            ApplicationPopover.HideWithQuitCommand (visiblePopover);
         }
 
         runState.Toplevel.OnUnloaded ();

+ 43 - 17
Terminal.Gui/App/ApplicationPopover.cs

@@ -1,5 +1,6 @@
 #nullable enable
 
+using System.Diagnostics;
 
 namespace Terminal.Gui.App;
 
@@ -36,12 +37,8 @@ public sealed class ApplicationPopover : IDisposable
     {
         if (popover is { } && !_popovers.Contains (popover))
         {
-
             // When created, set IPopover.Toplevel to the current Application.Top
-            if (popover.Toplevel is null)
-            {
-                popover.Toplevel = Application.Top;
-            }
+            popover.Toplevel ??= Application.Top;
 
             _popovers.Add (popover);
         }
@@ -62,19 +59,20 @@ public sealed class ApplicationPopover : IDisposable
     /// <returns></returns>
     public bool DeRegister (IPopover? popover)
     {
-        if (popover is { } && _popovers.Contains (popover))
+        if (popover is null || !_popovers.Contains (popover))
         {
-            if (GetActivePopover () == popover)
-            {
-                _activePopover = null;
-            }
-
-            _popovers.Remove (popover);
+            return false;
+        }
 
-            return true;
+        if (GetActivePopover () == popover)
+        {
+            _activePopover = null;
         }
 
-        return false;
+        _popovers.Remove (popover);
+
+        return true;
+
     }
 
     private IPopover? _activePopover;
@@ -111,6 +109,18 @@ public sealed class ApplicationPopover : IDisposable
 
         if (popover is View newPopover)
         {
+            if (!(newPopover.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) &&
+                  newPopover.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse)))
+            {
+                throw new InvalidOperationException ("Popovers must have ViewportSettings.Transparent and ViewportSettings.TransparentMouse set.");
+            }
+
+            if (newPopover.KeyBindings.GetFirstFromCommands (Command.Quit) is null)
+            {
+                throw new InvalidOperationException ("Popovers must have a key binding for Command.Quit.");
+            }
+
+
             Register (popover);
 
             if (!newPopover.IsInitialized)
@@ -127,7 +137,7 @@ public sealed class ApplicationPopover : IDisposable
 
     /// <summary>
     ///     Causes the specified popover to be hidden.
-    ///     If the popover is dervied from <see cref="PopoverBaseImpl"/>, this is the same as setting
+    ///     If the popover is derived from <see cref="PopoverBaseImpl"/>, this is the same as setting
     ///     <see cref="View.Visible"/> to <see langword="false"/>.
     /// </summary>
     /// <param name="popover"></param>
@@ -142,6 +152,22 @@ public sealed class ApplicationPopover : IDisposable
         }
     }
 
+    /// <summary>
+    ///     Hides a popover view if it supports the quit command and is currently visible. It checks for the command's
+    ///     support before hiding.
+    /// </summary>
+    /// <param name="visiblePopover">The view that is being checked and potentially hidden based on its visibility and command support.</param>
+    internal static void HideWithQuitCommand (View visiblePopover)
+    {
+        if (visiblePopover.Visible
+            && (!visiblePopover.GetSupportedCommands ().Contains (Command.Quit)
+            || (visiblePopover.InvokeCommand (Command.Quit) is true && visiblePopover.Visible)))
+        {
+            visiblePopover.Visible = false;
+        }
+    }
+
+
     /// <summary>
     ///     Called when the user presses a key. Dispatches the key to the active popover, if any,
     ///     otherwise to the popovers in the order they were registered. Inactive popovers only get hotkeys.
@@ -155,7 +181,7 @@ public sealed class ApplicationPopover : IDisposable
 
         if (activePopover is { Visible: true })
         {
-            Logging.Debug ($"Active - Calling NewKeyDownEvent ({key}) on {activePopover.Title}");
+            //Logging.Debug ($"Active - Calling NewKeyDownEvent ({key}) on {activePopover.Title}");
 
             if (activePopover.NewKeyDownEvent (key))
             {
@@ -177,7 +203,7 @@ public sealed class ApplicationPopover : IDisposable
             }
 
             // hotKeyHandled = popoverView.InvokeCommandsBoundToHotKey (key);
-            Logging.Debug ($"Inactive - Calling NewKeyDownEvent ({key}) on {popoverView.Title}");
+            //Logging.Debug ($"Inactive - Calling NewKeyDownEvent ({key}) on {popoverView.Title}");
             hotKeyHandled = popoverView.NewKeyDownEvent (key);
 
             if (hotKeyHandled is true)

+ 26 - 2
Terminal.Gui/App/IPopover.cs

@@ -3,8 +3,32 @@
 namespace Terminal.Gui.App;
 
 /// <summary>
-///     Interface identifying a View as being capable of being a Popover.
+///     Defines the contract for a popover view in Terminal.Gui.
 /// </summary>
+/// <remarks>
+///     <para>
+///         A popover is a transient UI element that appears above other content to display contextual information or UI,
+///         such as menus, tooltips, or dialogs.
+///         Popovers are managed by <see cref="ApplicationPopover"/> and are typically shown using
+///         <see cref="ApplicationPopover.Show"/>.
+///     </para>
+///     <para>
+///         Popovers are not modal; they do not block input to the rest of the application, but they do receive focus and
+///         input events while visible.
+///         When a popover is shown, it is responsible for handling its own layout and content.
+///     </para>
+///     <para>
+///         Popovers are automatically hidden when:
+///         <list type="bullet">
+///             <item>The user clicks outside the popover (unless occluded by a subview of the popover).</item>
+///             <item>The user presses <see cref="Application.QuitKey"/> (typically <c>Esc</c>).</item>
+///             <item>Another popover is shown.</item>
+///         </list>
+///     </para>
+///     <para>
+///         To implement a custom popover, inherit from <see cref="PopoverBaseImpl"/> or implement this interface directly.
+///     </para>
+/// </remarks>
 public interface IPopover
 {
     /// <summary>
@@ -15,5 +39,5 @@ public interface IPopover
     ///     When <see cref="ApplicationPopover.Register"/> is called, the <see cref="Toplevel"/> is set to the current
     ///     <see cref="Application.Top"/> if not already set.
     /// </summary>
-    public Toplevel? Toplevel { get; set; }
+    Toplevel? Toplevel { get; set; }
 }

+ 53 - 25
Terminal.Gui/App/PopoverBaseImpl.cs

@@ -3,24 +3,41 @@
 namespace Terminal.Gui.App;
 
 /// <summary>
-///     Abstract base class for Popover Views.
+///     Abstract base class for popover views in Terminal.Gui.
 /// </summary>
 /// <remarks>
 ///     <para>
-///         To show a Popover, use <see cref="ApplicationPopover.Show"/>. To hide a popover,
-///         call <see cref="ApplicationPopover.Show"/> with <see langword="null"/> set <see cref="View.Visible"/> to <see langword="false"/>.
+///         <b>Popover Lifecycle:</b><br/>
+///         To display a popover, use <see cref="ApplicationPopover.Show"/>. To hide a popover, either call
+///         <see cref="ApplicationPopover.Hide"/>,
+///         set <see cref="View.Visible"/> to <see langword="false"/>, or show another popover.
 ///     </para>
 ///     <para>
-///         If the user clicks anywhere not occluded by a SubView of the Popover, presses <see cref="Application.QuitKey"/>,
-///         or causes another popover to show, the Popover will be hidden.
+///         <b>Focus and Input:</b><br/>
+///         When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
+///         subview),
+///         presses <see cref="Application.QuitKey"/>, or another popover is shown, the popover will be hidden
+///         automatically.
+///     </para>
+///     <para>
+///         <b>Layout:</b><br/>
+///         When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
+///         this behavior
+///         by setting <see cref="View.Width"/> and <see cref="View.Height"/> in your derived class.
+///     </para>
+///     <para>
+///         <b>Custom Popovers:</b><br/>
+///         To create a custom popover, inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
 ///     </para>
 /// </remarks>
 public abstract class PopoverBaseImpl : View, IPopover
 {
-
     /// <summary>
-    ///     Creates a new PopoverBaseImpl.
+    ///     Initializes a new instance of the <see cref="PopoverBaseImpl"/> class.
     /// </summary>
+    /// <remarks>
+    ///     By default, the popover fills the available screen area and is focusable.
+    /// </remarks>
     protected PopoverBaseImpl ()
     {
         Id = "popoverBaseImpl";
@@ -52,32 +69,43 @@ public abstract class PopoverBaseImpl : View, IPopover
         }
     }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
+    public Toplevel? Toplevel { get; set; }
+
+    /// <summary>
+    ///     Called when the <see cref="View.Visible"/> property is changing.
+    /// </summary>
+    /// <remarks>
+    ///     When becoming visible, the popover is laid out to fit the screen.
+    ///     When becoming hidden, focus is restored to the previous view.
+    /// </remarks>
+    /// <returns>
+    ///     <see langword="true"/> to cancel the visibility change; otherwise, <see langword="false"/>.
+    /// </returns>
     protected override bool OnVisibleChanging ()
     {
         bool ret = base.OnVisibleChanging ();
-        if (ret is not true)
+
+        if (ret)
         {
-            if (!Visible)
-            {
-                // Whenever visible is changing to true, we need to resize;
-                // it's our only chance because we don't get laid out until we're visible
-                Layout (Application.Screen.Size);
-            }
-            else
+            return ret;
+        }
+
+        if (!Visible)
+        {
+            // Whenever visible is changing to true, we need to resize;
+            // it's our only chance because we don't get laid out until we're visible
+            Layout (Application.Screen.Size);
+        }
+        else
+        {
+            // Whenever visible is changing to false, we need to reset the focus
+            if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation?.GetFocused ()))
             {
-                // Whenever visible is changing to false, we need to reset the focus
-                if (ApplicationNavigation.IsInHierarchy(this, Application.Navigation?.GetFocused ()))
-                {
-                   Application.Navigation?.SetFocused (Application.Top?.MostFocused);
-                }
+                Application.Navigation?.SetFocused (Application.Top?.MostFocused);
             }
         }
 
         return ret;
     }
-
-    /// <inheritdoc />
-    public Toplevel? Toplevel { get; set; }
-
 }

+ 16 - 16
Terminal.Gui/Configuration/DeepCloner.cs

@@ -46,22 +46,22 @@ public static class DeepCloner
         {
             return default (T?);
         }
-        // For AOT environments, use source generation exclusively
-        if (IsAotEnvironment ())
-        {
-            if (TryUseSourceGeneratedCloner<T> (source, out T? result))
-            {
-                return result;
-            }
-
-            // If in AOT but source generation failed, throw an exception
-            // instead of silently falling back to reflection
-            //throw new InvalidOperationException (
-            //                                     $"Type {typeof (T).FullName} is not properly registered in SourceGenerationContext " +
-            //                                     $"for AOT-compatible cloning.");
-            Logging.Error ($"Type {typeof (T).FullName} is not properly registered in SourceGenerationContext " +
-                          $"for AOT-compatible cloning.");
-        }
+        //// For AOT environments, use source generation exclusively
+        //if (IsAotEnvironment ())
+        //{
+        //    if (TryUseSourceGeneratedCloner<T> (source, out T? result))
+        //    {
+        //        return result;
+        //    }
+
+        //    // If in AOT but source generation failed, throw an exception
+        //    // instead of silently falling back to reflection
+        //    //throw new InvalidOperationException (
+        //    //                                     $"Type {typeof (T).FullName} is not properly registered in SourceGenerationContext " +
+        //    //                                     $"for AOT-compatible cloning.");
+        //    Logging.Error ($"Type {typeof (T).FullName} is not properly registered in SourceGenerationContext " +
+        //                  $"for AOT-compatible cloning.");
+        //}
 
         // Use reflection-based approach, which should have better performance in non-AOT environments
         ConcurrentDictionary<object, object> visited = new (ReferenceEqualityComparer.Instance);

+ 4 - 0
Terminal.Gui/Configuration/SourceGenerationContext.cs

@@ -36,6 +36,10 @@ namespace Terminal.Gui.Configuration;
 [JsonSerializable (typeof (Scope<string>))]
 [JsonSerializable (typeof (AppSettingsScope))]
 [JsonSerializable (typeof (SettingsScope))]
+[JsonSerializable (typeof (ThemeScope))]
+[JsonSerializable (typeof (Scope<ThemeScope>))]
+[JsonSerializable (typeof (Scope<AppSettingsScope>))]
+[JsonSerializable (typeof (Scope<SettingsScope>))]
 [JsonSerializable (typeof (ConcurrentDictionary<string, ThemeScope>))]
 [JsonSerializable (typeof (Dictionary<string, Scheme>))]
 

+ 2 - 2
Terminal.Gui/Input/InputBindings.cs

@@ -160,7 +160,7 @@ public abstract class InputBindings<TEvent, TBinding> where TBinding : IInputBin
     /// <param name="commands">The set of commands to search.</param>
     /// <returns>
     ///     The first matching <typeparamref name="TEvent"/> bound to the set of commands specified by
-    ///     <paramref name="commands"/>. <see langword="null"/> if the set of caommands was not found.
+    ///     <paramref name="commands"/>. <see langword="null"/> if the set of commands was not found.
     /// </returns>
     public TEvent? GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; }
 
@@ -169,7 +169,7 @@ public abstract class InputBindings<TEvent, TBinding> where TBinding : IInputBin
     /// <returns>
     ///     The <typeparamref name="TEvent"/>s bound to the set of commands specified by <paramref name="commands"/>. An empty list if
     ///     the
-    ///     set of caommands was not found.
+    ///     set of commands was not found.
     /// </returns>
     public IEnumerable<TEvent> GetAllFromCommands (params Command [] commands)
     {

+ 13 - 0
Terminal.Gui/Terminal.Gui.csproj

@@ -156,7 +156,16 @@
         </VisualStudio>
     </ProjectExtensions>
 
+    <!--<Target Name="PreBuildCleanup" BeforeTargets="BeforeBuild" Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+        <Exec Command="rmdir /s /q &quot;$(UserProfile)\.nuget\packages\terminal.gui\2.0.0&quot;" Condition=" '$(OS)' == 'Windows_NT' " />
+        <Exec Command="rm -rf ~/.nuget/packages/terminal.gui/2.0.0" Condition=" '$(OS)' != 'Windows_NT' " />
+    </Target>-->
+        
     <Target Name="CopyNuGetPackagesToLocalPackagesFolder" AfterTargets="Pack" Condition="'$(Configuration)' == 'Release'">
+	    <!-- Remove older NuGet Package from Global Cache -->
+	    <Exec Command="rmdir /s /q &quot;$(UserProfile)\.nuget\packages\terminal.gui\2.0.0&quot;" Condition=" '$(OS)' == 'Windows_NT' " />
+	    <Exec Command="rm -rf ~/.nuget/packages/terminal.gui/2.0.0" Condition=" '$(OS)' != 'Windows_NT' " />
+
         <PropertyGroup>
             <!-- Define the path for local_packages relative to the project directory -->
             <LocalPackagesPath>$(MSBuildThisFileDirectory)..\local_packages\</LocalPackagesPath>
@@ -181,5 +190,9 @@
 
         <!-- Log success -->
         <Message Text="Copy completed successfully." Importance="high" />
+
+        <!-- Install NuGet Package to Global Cache -->
+        <Exec Command="dotnet nuget push &quot;$(MSBuildThisFileDirectory)bin\$(Configuration)\Terminal.Gui.$(Version).nupkg&quot; --source &quot;$(UserProfile)\.nuget\packages&quot; --skip-duplicate" Condition=" '$(OS)' == 'Windows_NT' "/>
+        <Exec Command="dotnet nuget push &quot;$(MSBuildThisFileDirectory)bin\$(Configuration)/Terminal.Gui.$(Version).nupkg&quot; --source ~/.nuget/packages" Condition=" '$(OS)' != 'Windows_NT' "/>
     </Target>
 </Project>

+ 19 - 0
Terminal.Gui/ViewBase/View.Hierarchy.cs

@@ -469,10 +469,29 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
 
     /// <summary>
     ///     Moves <paramref name="subview"/> to the end of the <see cref="SubViews"/> list.
+    ///     If the <see cref="Arrangement"/> is <see cref="ViewArrangement.Overlapped"/>, keeps the original sorting.
     /// </summary>
     /// <param name="subview">The subview to move.</param>
     public void MoveSubViewToEnd (View subview)
     {
+        if (Arrangement.HasFlag (ViewArrangement.Overlapped))
+        {
+            PerformActionForSubView (
+                                     subview,
+                                     x =>
+                                     {
+                                         while (InternalSubViews!.IndexOf (x) != InternalSubViews.Count - 1)
+                                         {
+                                             View v = InternalSubViews [0];
+                                             InternalSubViews!.Remove (v);
+                                             InternalSubViews.Add (v);
+                                         }
+                                     }
+                                    );
+
+            return;
+        }
+
         PerformActionForSubView (
                                  subview,
                                  x =>

+ 45 - 25
Terminal.Gui/ViewBase/View.Navigation.cs

@@ -62,38 +62,18 @@ public partial class View // Focus and cross-view navigation management (TabStop
             if (direction == NavigationDirection.Forward && focused == focusChain [^1] && SuperView is null)
             {
                 // We're at the top of the focus chain. Go back down the focus chain and focus the first TabGroup
-                View [] views = GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup);
-
-                if (views.Length > 0)
+                if (AdvanceFocusChain ())
                 {
-                    View [] subViews = views [0].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop);
-
-                    if (subViews.Length > 0)
-                    {
-                        if (subViews [0].SetFocus ())
-                        {
-                            return true;
-                        }
-                    }
+                    return true;
                 }
             }
 
-            if (direction == NavigationDirection.Backward && focused == focusChain [0])
+            if (direction == NavigationDirection.Backward && focused == focusChain [0] && SuperView is null)
             {
                 // We're at the bottom of the focus chain
-                View [] views = GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup);
-
-                if (views.Length > 0)
+                if (AdvanceFocusChain ())
                 {
-                    View [] subViews = views [^1].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop);
-
-                    if (subViews.Length > 0)
-                    {
-                        if (subViews [0].SetFocus ())
-                        {
-                            return true;
-                        }
-                    }
+                    return true;
                 }
             }
         }
@@ -149,6 +129,46 @@ public partial class View // Focus and cross-view navigation management (TabStop
         (bool focusSet, bool _) = view.SetHasFocusTrue (Focused);
 
         return focusSet;
+
+        bool AdvanceFocusChain ()
+        {
+            if (focusChain.Length > 0)
+            {
+                // Get the index of the currently focused view
+                int focusedTabGroupIndex = focusChain.IndexOf (Focused); // Will return -1 if Focused can't be found or is null
+
+                if (focusedTabGroupIndex + 1 > focusChain.Length - 1)
+                {
+                    focusedTabGroupIndex = 0;
+                }
+                else
+                {
+                    focusedTabGroupIndex++;
+                }
+
+                View [] subViews = focusChain [focusedTabGroupIndex].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop);
+
+                if (subViews.Length > 0)
+                {
+                    if (focusChain [focusedTabGroupIndex]._previouslyFocused is { }
+                        && subViews.Any (v => v == focusChain [focusedTabGroupIndex]._previouslyFocused))
+                    {
+                        if (focusChain [focusedTabGroupIndex]._previouslyFocused!.SetFocus ())
+                        {
+                            return true;
+                        }
+                    }
+
+                    // We have a subview that can be focused
+                    if (subViews [0].SetFocus ())
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
     }
 
     private bool RaiseAdvancingFocus (NavigationDirection direction, TabBehavior? behavior)

+ 2 - 0
Terminal.Gui/Views/Menu/PopoverMenu.cs

@@ -65,6 +65,8 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         AddCommand (Command.Left, MoveLeft);
         KeyBindings.Add (Key.CursorLeft, Command.Left);
 
+        // PopoverBaseImpl sets a key binding for Quit, so we
+        // don't need to do it here.
         AddCommand (Command.Quit, Quit);
 
         return;

+ 6 - 0
Terminal.sln

@@ -50,8 +50,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{C7A51224-5
 	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfContained", "Examples\SelfContained\SelfContained.csproj", "{524DEA78-7E7C-474D-B42D-52ED4C04FF14}"
+	ProjectSection(ProjectDependencies) = postProject
+		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5} = {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}
+	EndProjectSection
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "Examples\NativeAot\NativeAot.csproj", "{E6D716C6-AC94-4150-B10A-44AE13F79344}"
+	ProjectSection(ProjectDependencies) = postProject
+		{00F366F8-DEE4-482C-B9FD-6DB0200B79E5} = {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}
+	EndProjectSection
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Tests\Benchmarks\Benchmarks.csproj", "{242FBD3E-2EC6-4274-BD40-8E62AF9327B2}"
 EndProject

+ 72 - 2
Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs

@@ -12,7 +12,6 @@ public class BasicFluentAssertionTests
         _out = new TestOutputWriter (outputHelper);
     }
 
-
     [Theory]
     [ClassData (typeof (V2TestDrivers))]
     public void GuiTestContext_NewInstance_Runs (V2TestDriver d)
@@ -24,7 +23,6 @@ public class BasicFluentAssertionTests
         context.Stop ();
     }
 
-
     [Theory]
     [ClassData (typeof (V2TestDrivers))]
     public void GuiTestContext_QuitKey_Stops (V2TestDriver d)
@@ -153,4 +151,76 @@ public class BasicFluentAssertionTests
                                      .WriteOutLogs (_out);
         Assert.True (clicked);
     }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void Toplevel_TabGroup_Forward_Backward (V2TestDriver d)
+    {
+        var v1 = new View { Id = "v1", CanFocus = true };
+        var v2 = new View { Id = "v2", CanFocus = true };
+        var v3 = new View { Id = "v3", CanFocus = true };
+        var v4 = new View { Id = "v4", CanFocus = true };
+        var v5 = new View { Id = "v5", CanFocus = true };
+        var v6 = new View { Id = "v6", CanFocus = true };
+
+        using GuiTestContext c = With.A<Window> (50, 20, d)
+                                     .Then (
+                                            () =>
+                                            {
+                                                var w1 = new Window { Id = "w1" };
+                                                w1.Add (v1, v2);
+                                                var w2 = new Window { Id = "w2" };
+                                                w2.Add (v3, v4);
+                                                var w3 = new Window { Id = "w3" };
+                                                w3.Add (v5, v6);
+                                                Toplevel top = Application.Top!;
+                                                Application.Top!.Add (w1, w2, w3);
+                                            })
+                                     .WaitIteration ()
+                                     .Then (() => Assert.True (v5.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v1.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v3.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v1.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v5.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v3.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v5.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v1.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v3.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v1.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v5.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v3.HasFocus))
+                                     .RaiseKeyDownEvent (Key.Tab)
+                                     .Then (() => Assert.True (v4.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v5.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v1.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v5.HasFocus))
+                                     .RaiseKeyDownEvent (Key.Tab)
+                                     .Then (() => Assert.True (v6.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6.WithShift)
+                                     .Then (() => Assert.True (v4.HasFocus))
+                                     .RaiseKeyDownEvent (Key.F6)
+                                     .Then (() => Assert.True (v6.HasFocus))
+                                     .WriteOutLogs (_out)
+                                     .Stop ();
+        Assert.False (v1.HasFocus);
+        Assert.False (v2.HasFocus);
+        Assert.False (v3.HasFocus);
+        Assert.False (v4.HasFocus);
+        Assert.False (v5.HasFocus);
+        Assert.False (v6.HasFocus);
+    }
 }

+ 8 - 0
Tests/UnitTests/Application/ApplicationPopoverTests.cs

@@ -21,6 +21,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Application_Shutdown_Resets_PopoverManager ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -37,6 +38,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Application_End_Does_Not_Reset_PopoverManager ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -59,6 +61,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Application_End_Hides_Active ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -87,6 +90,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Application_Shutdown_Disposes_Registered_Popovers ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -104,6 +108,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Application_Shutdown_Does_Not_Dispose_DeRegistered_Popovers ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -126,6 +131,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Application_Shutdown_Does_Not_Dispose_ActiveNotRegistered_Popover ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -148,6 +154,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Register_SetsTopLevel ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());
@@ -166,6 +173,7 @@ public class ApplicationPopoverTests
     [Fact]
     public void Keyboard_Events_Go_Only_To_Popover_Associated_With_Toplevel ()
     {
+        Application.ResetState (true);
         // Arrange
         Assert.Null (Application.Popover);
         Application.Init (new FakeDriver ());

+ 1 - 1
Tests/UnitTests/Application/KeyboardTests.cs

@@ -215,7 +215,7 @@ public class KeyboardTests
                                      Assert.True (v3.HasFocus);
 
                                      Application.RaiseKeyDownEvent (Key.F6);
-                                     Assert.True (v1.HasFocus);
+                                     Assert.True (v2.HasFocus); // previously focused view was preserved
 
                                      Application.RequestStop ();
                                  };

+ 37 - 3
Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs

@@ -25,7 +25,10 @@ public class ApplicationV2Tests
     [Fact]
     public void Init_CreatesKeybindings ()
     {
+        var orig = ApplicationImpl.Instance;
+
         var v2 = NewApplicationV2 ();
+        ApplicationImpl.ChangeInstance (v2);
 
         Application.KeyBindings.Clear ();
 
@@ -36,12 +39,17 @@ public class ApplicationV2Tests
         Assert.NotEmpty (Application.KeyBindings.GetBindings ());
 
         v2.Shutdown ();
+
+        ApplicationImpl.ChangeInstance (orig);
     }
 
     [Fact]
     public void Init_DriverIsFacade ()
     {
+        var orig = ApplicationImpl.Instance;
+
         var v2 = NewApplicationV2 ();
+        ApplicationImpl.ChangeInstance (v2);
 
         Assert.Null (Application.Driver);
         v2.Init ();
@@ -53,11 +61,15 @@ public class ApplicationV2Tests
         v2.Shutdown ();
 
         Assert.Null (Application.Driver);
+
+        ApplicationImpl.ChangeInstance (orig);
     }
 
     [Fact]
     public void Init_ExplicitlyRequestWin ()
     {
+        var orig = ApplicationImpl.Instance;
+
         Assert.Null (Application.Driver);
         var netInput = new Mock<INetInput> (MockBehavior.Strict);
         var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
@@ -77,6 +89,7 @@ public class ApplicationV2Tests
                                     () => netOutput.Object,
                                     () => winInput.Object,
                                     () => winOutput.Object);
+        ApplicationImpl.ChangeInstance (v2);
 
         Assert.Null (Application.Driver);
         v2.Init (null, "v2win");
@@ -90,11 +103,15 @@ public class ApplicationV2Tests
         Assert.Null (Application.Driver);
 
         winInput.VerifyAll ();
+
+        ApplicationImpl.ChangeInstance (orig);
     }
 
     [Fact]
     public void Init_ExplicitlyRequestNet ()
     {
+        var orig = ApplicationImpl.Instance;
+
         var netInput = new Mock<INetInput> (MockBehavior.Strict);
         var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
         var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
@@ -112,6 +129,7 @@ public class ApplicationV2Tests
                                     () => netOutput.Object,
                                     () => winInput.Object,
                                     () => winOutput.Object);
+        ApplicationImpl.ChangeInstance (v2);
 
         Assert.Null (Application.Driver);
         v2.Init (null, "v2net");
@@ -125,6 +143,8 @@ public class ApplicationV2Tests
         Assert.Null (Application.Driver);
 
         netInput.VerifyAll ();
+
+        ApplicationImpl.ChangeInstance (orig);
     }
 
     private void SetupRunInputMockMethodToBlock (Mock<IWindowsInput> winInput)
@@ -159,12 +179,17 @@ public class ApplicationV2Tests
     [Fact]
     public void NoInitThrowOnRun ()
     {
+        var orig = ApplicationImpl.Instance;
+
         Assert.Null (Application.Driver);
         var app = NewApplicationV2 ();
+        ApplicationImpl.ChangeInstance (app);
 
         var ex = Assert.Throws<NotInitializedException> (() => app.Run (new Window ()));
         Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
         app.Shutdown();
+
+        ApplicationImpl.ChangeInstance (orig);
     }
 
     [Fact]
@@ -449,6 +474,8 @@ public class ApplicationV2Tests
     [Fact]
     public void Shutdown_Called_Repeatedly_DoNotDuplicateDisposeOutput ()
     {
+        var orig = ApplicationImpl.Instance;
+
         var netInput = new Mock<INetInput> ();
         SetupRunInputMockMethodToBlock (netInput);
         Mock<IConsoleOutput>? outputMock = null;
@@ -459,6 +486,7 @@ public class ApplicationV2Tests
                                    () => (outputMock = new Mock<IConsoleOutput> ()).Object,
                                    Mock.Of<IWindowsInput>,
                                    Mock.Of<IConsoleOutput>);
+        ApplicationImpl.ChangeInstance (v2);
 
         v2.Init (null, "v2net");
 
@@ -466,11 +494,17 @@ public class ApplicationV2Tests
         v2.Shutdown ();
         v2.Shutdown ();
         outputMock!.Verify (o => o.Dispose (), Times.Once);
+
+        ApplicationImpl.ChangeInstance (orig);
     }
+
     [Fact]
     public void Init_Called_Repeatedly_WarnsAndIgnores ()
     {
+        var orig = ApplicationImpl.Instance;
+
         var v2 = NewApplicationV2 ();
+        ApplicationImpl.ChangeInstance (v2);
 
         Assert.Null (Application.Driver);
         v2.Init ();
@@ -496,12 +530,12 @@ public class ApplicationV2Tests
 
         // Restore the original null logger to be polite to other tests
         Logging.Logger = beforeLogger;
-    }
 
+        ApplicationImpl.ChangeInstance (orig);
+    }
 
-    // QUESTION: What does this test really test? It's poorly named.
     [Fact]
-    public void Open_CallsContinueWithOnUIThread ()
+    public void Open_Calls_ContinueWith_On_UIThread ()
     {
         var orig = ApplicationImpl.Instance;
 

+ 18 - 0
Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs

@@ -144,10 +144,28 @@ public class ApplicationPopoverTests
 
         public PopoverTestClass ()
         {
+            ViewportSettings = ViewportSettingsFlags.Transparent | ViewportSettingsFlags.TransparentMouse;
             CanFocus = true;
             AddCommand (Command.New, NewCommandHandler!);
             HotKeyBindings.Add (Key.N.WithCtrl, Command.New);
 
+            AddCommand (Command.Quit, Quit);
+            KeyBindings.Add (Application.QuitKey, Command.Quit);
+
+            return;
+
+            bool? Quit (ICommandContext? ctx)
+            {
+                if (!Visible)
+                {
+                    return false;
+                }
+
+                Visible = false;
+
+                return true;
+            }
+
             bool? NewCommandHandler (ICommandContext ctx)
             {
                 NewCommandInvokeCount++;

+ 68 - 0
Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs

@@ -0,0 +1,68 @@
+using System;
+using Terminal.Gui;
+using Terminal.Gui.App;
+using Xunit;
+
+public class PopoverBaseImplTests
+{
+    // Minimal concrete implementation for testing
+    private class TestPopover : PopoverBaseImpl { }
+
+    [Fact]
+    public void Constructor_SetsDefaults ()
+    {
+        var popover = new TestPopover ();
+
+        Assert.Equal ("popoverBaseImpl", popover.Id);
+        Assert.True (popover.CanFocus);
+        Assert.Equal (Dim.Fill (), popover.Width);
+        Assert.Equal (Dim.Fill (), popover.Height);
+        Assert.True (popover.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent));
+        Assert.True (popover.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse));
+    }
+
+    [Fact]
+    public void Toplevel_Property_CanBeSetAndGet ()
+    {
+        var popover = new TestPopover ();
+        var top = new Toplevel ();
+        popover.Toplevel = top;
+        Assert.Same (top, popover.Toplevel);
+    }
+
+    [Fact]
+    public void Show_ThrowsIfPopoverMissingRequiredFlags ()
+    {
+        var popover = new TestPopover ();
+
+        // Popover missing Transparent flags
+        popover.ViewportSettings = ViewportSettingsFlags.None; // Remove required flags
+
+        var popoverManager = new ApplicationPopover ();
+        // Test missing Transparent flags
+        Assert.ThrowsAny<Exception> (() => popoverManager.Show (popover));
+
+    }
+
+
+    [Fact]
+    public void Show_ThrowsIfPopoverMissingQuitCommand ()
+    {
+        var popover = new TestPopover ();
+
+        // Popover missing Command.Quit binding
+        popover.KeyBindings.Clear (); // Remove all key bindings
+
+        var popoverManager = new ApplicationPopover ();
+        Assert.ThrowsAny<Exception> (() => popoverManager.Show (popover));
+    }
+
+    [Fact]
+    public void Show_DoesNotThrow_BasePopoverImpl ()
+    {
+        var popover = new TestPopover ();
+
+        var popoverManager = new ApplicationPopover ();
+        popoverManager.Show (popover);
+    }
+}

+ 34 - 0
Tests/UnitTestsParallelizable/View/SubviewTests.cs

@@ -118,6 +118,40 @@ public class SubViewTests
         Assert.Equal (new (5, 5), view.GetContentSize ());
     }
 
+    [Theory]
+    [InlineData (ViewArrangement.Fixed)]
+    [InlineData (ViewArrangement.Overlapped)]
+    public void MoveSubViewToEnd_ViewArrangement (ViewArrangement arrangement)
+    {
+        View superView = new () { Arrangement = arrangement };
+
+        var subview1 = new View
+        {
+            Id = "subview1"
+        };
+
+        var subview2 = new View
+        {
+            Id = "subview2"
+        };
+
+        var subview3 = new View
+        {
+            Id = "subview3"
+        };
+
+        superView.Add (subview1, subview2, subview3);
+
+        superView.MoveSubViewToEnd (subview1);
+        Assert.Equal ([subview2, subview3, subview1], superView.SubViews.ToArray ());
+
+        superView.MoveSubViewToEnd (subview2);
+        Assert.Equal ([subview3, subview1, subview2], superView.SubViews.ToArray ());
+
+        superView.MoveSubViewToEnd (subview3);
+        Assert.Equal ([subview1, subview2, subview3], superView.SubViews.ToArray ());
+    }
+
     [Fact]
     public void MoveSubViewToStart ()
     {

二進制
local_packages/Terminal.Gui.2.0.0.nupkg


二進制
local_packages/Terminal.Gui.2.0.0.snupkg


+ 3 - 3
nuget.config

@@ -15,8 +15,8 @@
     <packageSource key="nuget">
       <package pattern="*" />
     </packageSource>
-	<packageSource key="LocalPackages">
-       <package pattern="Terminal.Gui*" />
-	</packageSource>
+    <packageSource key="LocalPackages">
+      <package pattern="Terminal.Gui*" />
+    </packageSource>
   </packageSourceMapping>
 </configuration>