Parcourir la source

Fixes #4177: View.GetAttributeForRole now defers to SuperView for proper attribute hierarchy (#4292)

* Initial plan

* Fix GetAttributeForRole to defer to SuperView when no explicit scheme

Co-authored-by: tig <[email protected]>

* Add test for Adornment attribute resolution

Co-authored-by: tig <[email protected]>

* Fix: Also check SchemeName when deferring to SuperView in GetAttributeForRole

Co-authored-by: tig <[email protected]>

* Add test for StatusBar/Bar not deferring when SchemeName is set

Co-authored-by: tig <[email protected]>

* Add comprehensive low-level tests for GetAttributeForRole hierarchy

Co-authored-by: tig <[email protected]>

* Update AGENTS.md with PR branch pull instructions

Co-authored-by: tig <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: tig <[email protected]>
Co-authored-by: Tig <[email protected]>
Copilot il y a 1 mois
Parent
commit
549436e343

+ 47 - 0
AGENTS.md

@@ -130,3 +130,50 @@ dotnet test
 1. Maintain existing code structure and organization unless explicitly told
 2. View sub-classes must not use private APIs
 3. Suggest changes to the `./docfx/docs/` folder when appropriate
+
+## Working with Pull Request Branches
+
+When creating PRs, include instructions at the end of each PR description for how to pull the branch down locally. Use the following template, adapted for the typical remote setup where `origin` points to the user's fork and `upstream` points to `gui-cs/Terminal.Gui`:
+
+```markdown
+## How to Pull This PR Branch Locally
+
+If you want to test or modify this PR locally, use one of these approaches based on your remote setup:
+
+### Method 1: Fetch from upstream (if branch exists there)
+```bash
+# Fetch the branch from upstream
+git fetch upstream <branch-name>
+
+# Switch to the branch
+git checkout <branch-name>
+
+# Make your changes, then commit them
+git add .
+git commit -m "Your commit message"
+
+# Push to your fork (origin)
+git push origin <branch-name>
+```
+
+### Method 2: Fetch by PR number
+```bash
+# Fetch the PR branch from upstream by PR number
+git fetch upstream pull/<PR_NUMBER>/head:<branch-name>
+
+# Switch to the branch
+git checkout <branch-name>
+
+# Make your changes, then commit them
+git add .
+git commit -m "Your commit message"
+
+# Push to your fork (origin)
+git push origin <branch-name>
+```
+
+The PR will automatically update when you push to the branch in your fork.
+```
+
+**Note:** Adjust the remote names if your setup differs (e.g., if `origin` points to `gui-cs/Terminal.Gui` and you have a separate remote for your fork).
+

+ 14 - 1
Terminal.Gui/ViewBase/View.Drawing.Attribute.cs

@@ -35,7 +35,20 @@ public partial class View
     /// <returns>The corresponding <see cref="Attribute"/> from the <see cref="Drawing.Scheme"/>.</returns>
     public Attribute GetAttributeForRole (VisualRole role)
     {
-        Attribute schemeAttribute = GetScheme ()!.GetAttributeForRole (role);
+        // Get the base attribute
+        // If this view doesn't have an explicit scheme or scheme name, defer to SuperView for attribute resolution.
+        // This allows parent views to customize attribute resolution for their children via
+        // OnGettingAttributeForRole/GettingAttributeForRole.
+        // This matches the logic in GetScheme() where SchemeName takes precedence over SuperView inheritance.
+        Attribute schemeAttribute;
+        if (!HasScheme && string.IsNullOrEmpty (SchemeName) && SuperView is { })
+        {
+            schemeAttribute = SuperView.GetAttributeForRole (role);
+        }
+        else
+        {
+            schemeAttribute = GetScheme ()!.GetAttributeForRole (role);
+        }
 
         if (OnGettingAttributeForRole (role, ref schemeAttribute))
         {

+ 265 - 0
Tests/UnitTestsParallelizable/View/SchemeTests.cs

@@ -269,4 +269,269 @@ public class SchemeTests
         view.Dispose ();
     }
 
+    [Fact]
+    public void GetAttributeForRole_SubView_DefersToSuperView_WhenNoExplicitScheme ()
+    {
+        var parentView = new View { SchemeName = "Base" };
+        var childView = new View ();
+        parentView.Add (childView);
+
+        // Parent customizes attribute resolution
+        var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
+        parentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = customAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // Child without explicit scheme should get customized attribute from parent
+        Assert.Equal (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_SubView_UsesOwnScheme_WhenExplicitlySet ()
+    {
+        var parentView = new View { SchemeName = "Base" };
+        var childView = new View ();
+        parentView.Add (childView);
+
+        // Set explicit scheme on child
+        var childScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+        childView.SetScheme (childScheme);
+
+        // Parent customizes attribute resolution
+        var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
+        parentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = customAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // Child with explicit scheme should NOT get customized attribute from parent
+        Assert.NotEqual (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
+        Assert.Equal (childScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_Adornment_UsesParentScheme ()
+    {
+        // Border (an Adornment) doesn't have a SuperView but should use its Parent's scheme
+        var view = new View { SchemeName = "Dialog" };
+        var border = view.Border!;
+        
+        Assert.NotNull (border);
+        Assert.Null (border.SuperView); // Adornments don't have SuperView
+        Assert.NotNull (border.Parent);
+        
+        var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+        
+        // Border should use its Parent's scheme, not Base
+        Assert.Equal (dialogScheme!.Normal, border.GetAttributeForRole (VisualRole.Normal));
+        
+        view.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_SubView_UsesSchemeName_WhenSet ()
+    {
+        var parentView = new View { SchemeName = "Base" };
+        var childView = new View ();
+        parentView.Add (childView);
+
+        // Set SchemeName on child (not explicit scheme)
+        childView.SchemeName = "Dialog";
+
+        // Parent customizes attribute resolution
+        var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
+        parentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = customAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // Child with SchemeName should NOT get customized attribute from parent
+        // It should use the Dialog scheme instead
+        var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+        Assert.NotEqual (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
+        Assert.Equal (dialogScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_NestedHierarchy_DefersCorrectly ()
+    {
+        // Test: grandchild without explicit scheme defers through parent to grandparent
+        // Would fail without the SuperView deferral fix (commit 154ac15)
+        
+        var grandparentView = new View { SchemeName = "Base" };
+        var parentView = new View (); // No scheme or SchemeName
+        var childView = new View (); // No scheme or SchemeName
+        
+        grandparentView.Add (parentView);
+        parentView.Add (childView);
+
+        // Grandparent customizes attributes
+        var customAttribute = new Attribute (Color.BrightYellow, Color.BrightBlue);
+        grandparentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = customAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // Child should get attribute from grandparent through parent
+        Assert.Equal (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
+        
+        // Parent should also get attribute from grandparent
+        Assert.Equal (customAttribute, parentView.GetAttributeForRole (VisualRole.Normal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+        grandparentView.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_ParentWithSchemeNameBreaksChain ()
+    {
+        // Test: parent with SchemeName stops deferral chain
+        // Would fail without the SchemeName check (commit 866e002)
+        
+        var grandparentView = new View { SchemeName = "Base" };
+        var parentView = new View { SchemeName = "Dialog" }; // Sets SchemeName
+        var childView = new View (); // No scheme or SchemeName
+        
+        grandparentView.Add (parentView);
+        parentView.Add (childView);
+
+        // Grandparent customizes attributes
+        var customAttribute = new Attribute (Color.BrightYellow, Color.BrightBlue);
+        grandparentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = customAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // Parent should NOT get grandparent's customization (it has SchemeName)
+        var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+        Assert.NotEqual (customAttribute, parentView.GetAttributeForRole (VisualRole.Normal));
+        Assert.Equal (dialogScheme!.Normal, parentView.GetAttributeForRole (VisualRole.Normal));
+        
+        // Child should get parent's Dialog scheme (defers to parent, parent uses Dialog scheme)
+        Assert.Equal (dialogScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+        grandparentView.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_OnGettingAttributeForRole_TakesPrecedence ()
+    {
+        // Test: view's own OnGettingAttributeForRole takes precedence over parent
+        // This should work with or without the fix, but validates precedence
+        
+        var parentView = new View { SchemeName = "Base" };
+        var childView = new TestViewWithAttributeOverride ();
+        parentView.Add (childView);
+
+        // Parent customizes attributes
+        var parentAttribute = new Attribute (Color.BrightYellow, Color.BrightBlue);
+        parentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = parentAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // Child's own override should take precedence
+        var childOverrideAttribute = new Attribute (Color.BrightRed, Color.BrightCyan);
+        childView.OverrideAttribute = childOverrideAttribute;
+        
+        Assert.Equal (childOverrideAttribute, childView.GetAttributeForRole (VisualRole.Normal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+    }
+
+    [Fact]
+    public void GetAttributeForRole_MultipleRoles_DeferCorrectly ()
+    {
+        // Test: multiple VisualRoles all defer correctly
+        // Would fail without the SuperView deferral fix for any role
+        
+        var parentView = new View { SchemeName = "Base" };
+        var childView = new View ();
+        parentView.Add (childView);
+
+        var normalAttr = new Attribute (Color.Red, Color.Blue);
+        var focusAttr = new Attribute (Color.Green, Color.Yellow);
+        var hotNormalAttr = new Attribute (Color.Magenta, Color.Cyan);
+
+        parentView.GettingAttributeForRole += (sender, args) =>
+        {
+            switch (args.Role)
+            {
+                case VisualRole.Normal:
+                    args.Result = normalAttr;
+                    args.Handled = true;
+                    break;
+                case VisualRole.Focus:
+                    args.Result = focusAttr;
+                    args.Handled = true;
+                    break;
+                case VisualRole.HotNormal:
+                    args.Result = hotNormalAttr;
+                    args.Handled = true;
+                    break;
+            }
+        };
+
+        // All roles should defer to parent
+        Assert.Equal (normalAttr, childView.GetAttributeForRole (VisualRole.Normal));
+        Assert.Equal (focusAttr, childView.GetAttributeForRole (VisualRole.Focus));
+        Assert.Equal (hotNormalAttr, childView.GetAttributeForRole (VisualRole.HotNormal));
+
+        childView.Dispose ();
+        parentView.Dispose ();
+    }
+
+    private class TestViewWithAttributeOverride : View
+    {
+        public Attribute? OverrideAttribute { get; set; }
+
+        protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
+        {
+            if (OverrideAttribute.HasValue && role == VisualRole.Normal)
+            {
+                currentAttribute = OverrideAttribute.Value;
+                return true;
+            }
+            return base.OnGettingAttributeForRole (role, ref currentAttribute);
+        }
+    }
+
 }

+ 33 - 0
Tests/UnitTestsParallelizable/Views/BarTests.cs

@@ -102,4 +102,37 @@ public class BarTests
         bar.LayoutSubViews ();
         // TODO: Assert specific layout expectations for vertical orientation
     }
+
+    [Fact]
+    public void GetAttributeForRole_DoesNotDeferToSuperView_WhenSchemeNameIsSet ()
+    {
+        // This test would fail before the fix that checks SchemeName in GetAttributeForRole
+        // StatusBar and MenuBarv2 set SchemeName = "Menu", and should use Menu scheme
+        // instead of deferring to parent's customized attributes
+        
+        var parentView = new View { SchemeName = "Base" };
+        var statusBar = new StatusBar ();
+        parentView.Add (statusBar);
+
+        // Parent customizes attribute resolution
+        var customAttribute = new Attribute (Color.BrightMagenta, Color.BrightGreen);
+        parentView.GettingAttributeForRole += (sender, args) =>
+        {
+            if (args.Role == VisualRole.Normal)
+            {
+                args.Result = customAttribute;
+                args.Handled = true;
+            }
+        };
+
+        // StatusBar sets SchemeName = "Menu" in its constructor
+        // Before the fix: StatusBar would defer to parent and get customAttribute (WRONG)
+        // After the fix: StatusBar uses Menu scheme (CORRECT)
+        var menuScheme = SchemeManager.GetHardCodedSchemes ()? ["Menu"];
+        Assert.NotEqual (customAttribute, statusBar.GetAttributeForRole (VisualRole.Normal));
+        Assert.Equal (menuScheme!.Normal, statusBar.GetAttributeForRole (VisualRole.Normal));
+
+        statusBar.Dispose ();
+        parentView.Dispose ();
+    }
 }