diff --git a/Examples/UICatalog/Scenarios/ThemeFallback.cs b/Examples/UICatalog/Scenarios/ThemeFallback.cs
new file mode 100644
index 0000000000..b93a5706c7
--- /dev/null
+++ b/Examples/UICatalog/Scenarios/ThemeFallback.cs
@@ -0,0 +1,143 @@
+#nullable enable
+
+namespace UICatalog.Scenarios;
+
+///
+/// Demonstrates the SchemeName fallback chain introduced in v2.
+/// When a view's is not found in the active theme, the view no longer
+/// throws a . Instead, it walks the fallback chain:
+///
+/// -
+/// Named scheme (if found in current theme)
+///
+/// -
+/// SuperView's scheme (recursive)
+///
+/// -
+/// "Base" scheme from the current theme
+///
+/// -
+/// Hard-coded "Base" scheme (always present)
+///
+///
+///
+[ScenarioMetadata ("Theme Fallback", "Demonstrates graceful SchemeName fallback when a named scheme is missing from the active theme.")]
+[ScenarioCategory ("Colors")]
+[ScenarioCategory ("Configuration")]
+public sealed class ThemeFallback : Scenario
+{
+ private const string CUSTOM_SCHEME_NAME = "CustomHighlight";
+ private const string MISSING_SCHEME_NAME = "NonExistentScheme";
+
+ public override void Main ()
+ {
+ ConfigurationManager.Enable (ConfigLocations.All);
+
+ using IApplication app = Application.Create ();
+ app.Init ();
+
+ // Extend the Default theme with a custom scheme that has TextStyle.Blink
+ // so it stands out visually. Other built-in themes do NOT contain this scheme,
+ // so switching themes lets you watch the fallback chain activate in real time.
+ SchemeManager.AddScheme (CUSTOM_SCHEME_NAME, new () { Normal = new Attribute (Color.BrightYellow, Color.Blue, TextStyle.Blink) });
+
+ using Window appWindow = new ();
+ appWindow.Title = GetQuitKeyAndName ();
+
+ // --- Theme selector ---
+ string [] themeLabels = ThemeManager.GetThemeNames ().Select (n => "_" + n).ToArray ();
+
+ OptionSelector themeSelector = new ()
+ {
+ Title = "_Theme",
+ BorderStyle = LineStyle.Rounded,
+ X = 1,
+ Y = 1,
+ Width = Dim.Auto (),
+ Height = Dim.Auto (),
+ Labels = themeLabels,
+ Value = ThemeManager.GetThemeNames ().IndexOf (ThemeManager.Theme)
+ };
+
+ themeSelector.ValueChanged += (sender, args) =>
+ {
+ if (sender is not OptionSelector sel)
+ {
+ return;
+ }
+
+ string rawLabel = sel.Labels! [(int)args.NewValue!];
+
+ // Strip the leading underscore added for keyboard shortcut.
+ ThemeManager.Theme = rawLabel [1..];
+ ConfigurationManager.Apply ();
+
+ // Re-add the custom scheme to the newly-active theme so the
+ // "Default" theme always demonstrates the found case.
+ if (ThemeManager.Theme == ThemeManager.DEFAULT_THEME_NAME)
+ {
+ SchemeManager.AddScheme (CUSTOM_SCHEME_NAME,
+ new () { Normal = new Attribute (Color.BrightYellow, Color.Blue, TextStyle.Blink) });
+ }
+ };
+
+ // --- Explanation ---
+ Label intro = new ()
+ {
+ X = Pos.Right (themeSelector) + 1,
+ Y = 1,
+ Width = Dim.Fill (1),
+ Text = $"Switch to a non-Default theme to see the fallback activate.\n"
+ + $" • \"{CUSTOM_SCHEME_NAME}\" is only in the Default theme.\n"
+ + $" • \"{MISSING_SCHEME_NAME}\" is never in any theme.\n"
+ + $"In both missing cases the view falls back gracefully instead of throwing."
+ };
+
+ // --- View 1: scheme FOUND in the active theme ---
+ FrameView foundFrame = new ()
+ {
+ Title = $"SchemeName = \"{CUSTOM_SCHEME_NAME}\"",
+ X = Pos.Right (themeSelector) + 1,
+ Y = Pos.Bottom (intro) + 1,
+ Width = Dim.Fill (1),
+ Height = 5,
+ SchemeName = CUSTOM_SCHEME_NAME
+ };
+
+ Label foundLabel = new ()
+ {
+ X = 1,
+ Y = 1,
+ Width = Dim.Fill (2),
+ Text = $"On the Default theme this scheme exists (BrightYellow/Blue + Blink).\n"
+ + $"On any other theme the scheme is missing → fallback chain activates."
+ };
+ foundFrame.Add (foundLabel);
+
+ // --- View 2: scheme NEVER found — fallback always activates ---
+ FrameView missingFrame = new ()
+ {
+ Title = $"SchemeName = \"{MISSING_SCHEME_NAME}\"",
+ X = Pos.Right (themeSelector) + 1,
+ Y = Pos.Bottom (foundFrame) + 1,
+ Width = Dim.Fill (1),
+ Height = 5,
+ SchemeName = MISSING_SCHEME_NAME
+ };
+
+ Label missingLabel = new ()
+ {
+ X = 1,
+ Y = 1,
+ Width = Dim.Fill (2),
+ Text = $"This scheme does not exist in any theme.\n"
+ + $"The view silently falls back to its SuperView's scheme (no exception).\n"
+ + $"A warning is written to the debug log."
+ };
+ missingFrame.Add (missingLabel);
+
+ appWindow.Add (themeSelector, intro, foundFrame, missingFrame);
+
+ app.Run (appWindow);
+ }
+}
diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs
index b4b60f1e31..0fb8a60f06 100644
--- a/Examples/UICatalog/UICatalogRunnable.cs
+++ b/Examples/UICatalog/UICatalogRunnable.cs
@@ -641,6 +641,7 @@ private ListView CreateCategoryList ()
SuperViewRendersLineCanvas = true,
Source = new ListWrapper (CachedCategories)
};
+
//categoryList.Border.Settings = BorderSettings.Title | BorderSettings.Tab;
//categoryList.Border.TabSide = Side.Top;
//categoryList.Border.Thickness = new Thickness (1, 2, 1, 1);
@@ -721,20 +722,31 @@ public static bool ShowStatusBar
/// or assigned to a on a menu item. Falls back to
/// if all are taken.
///
- private Key GetFirstUnboundFKey ()
+ ///
+ private Key GetFirstUnboundFKey (HashSet ignoreKeys)
{
Key [] fKeys =
[
- Key.F1, Key.F2, Key.F3, Key.F4, Key.F5, Key.F6,
- Key.F7, Key.F8, Key.F9, Key.F10, Key.F11, Key.F12
+ Key.F1,
+ Key.F2,
+ Key.F3,
+ Key.F4,
+ Key.F5,
+ Key.F6,
+ Key.F7,
+ Key.F8,
+ Key.F9,
+ Key.F10,
+ Key.F11,
+ Key.F12
];
// Collect keys already bound at the Application level
- HashSet boundKeys = [];
+ HashSet boundKeys = ignoreKeys;
foreach (Key fKey in fKeys)
{
- if (Application.KeyBindings.TryGet (fKey, out _))
+ if (App?.Keyboard.KeyBindings.TryGet (fKey, out _) is true)
{
boundKeys.Add (fKey);
}
@@ -753,6 +765,16 @@ private Key GetFirstUnboundFKey ()
}
}
+ // Exclude keys used by StatusBar items
+ if (_statusBar is { })
+ {
+ foreach (View view in _statusBar.SubViews)
+ {
+ var shortcut = (Shortcut)view;
+ boundKeys.Add (shortcut.Key);
+ }
+ }
+
foreach (Key fKey in fKeys)
{
if (!boundKeys.Contains (fKey))
@@ -773,7 +795,10 @@ private StatusBar CreateStatusBar ()
_shVersion = new Shortcut { Title = "Version Info", CanFocus = false };
- Shortcut statusBarShortcut = new () { Key = GetFirstUnboundFKey (), Title = "Show/Hide Status Bar", CanFocus = false, Action = () => ShowStatusBar = !ShowStatusBar };
+ Shortcut statusBarShortcut = new ()
+ {
+ Key = GetFirstUnboundFKey ([]), Title = "Show/Hide Status Bar", CanFocus = false, Action = () => ShowStatusBar = !ShowStatusBar
+ };
_force16ColorsShortcutCb = new CheckBox
{
@@ -786,7 +811,7 @@ private StatusBar CreateStatusBar ()
CommandView = _force16ColorsShortcutCb,
HelpText = "",
BindKeyToApplication = true,
- Key = Key.F7,
+ Key = GetFirstUnboundFKey ([statusBarShortcut.Key]),
Action = () =>
{
Driver.Force16Colors = !Driver.Force16Colors;
diff --git a/Terminal.Gui/Configuration/SchemeManager.cs b/Terminal.Gui/Configuration/SchemeManager.cs
index f117181805..407839b0c9 100644
--- a/Terminal.Gui/Configuration/SchemeManager.cs
+++ b/Terminal.Gui/Configuration/SchemeManager.cs
@@ -136,9 +136,67 @@ public static Scheme GetScheme (Schemes schemeName)
///
///
///
- ///
+ /// If is not found in the current theme.
public static Scheme GetScheme (string schemeName) { return GetSchemesForCurrentTheme ()! [schemeName]!; }
+ ///
+ /// Attempts to get the for the specified name without throwing.
+ /// Returns and sets to if the scheme is
+ /// not found, or if the configuration is not in a state where schemes can be resolved.
+ ///
+ /// The name of the scheme to retrieve.
+ ///
+ /// When this method returns , contains the resolved ; otherwise
+ /// .
+ ///
+ /// if the scheme was found; otherwise .
+ public static bool TryGetScheme (string schemeName, [NotNullWhen (true)] out Scheme? scheme)
+ {
+ lock (_schemesLock)
+ {
+ Dictionary schemes;
+
+ if (!ConfigurationManager.IsInitialized ())
+ {
+ // Module initializer / unit-test path — fall back to hard-coded defaults.
+ ImmutableSortedDictionary? hardCoded = GetHardCodedSchemes ();
+
+ if (hardCoded is null)
+ {
+ scheme = null;
+
+ return false;
+ }
+
+ schemes = hardCoded.ToDictionary (StringComparer.InvariantCultureIgnoreCase);
+ }
+ else
+ {
+ // Avoid GetSchemesForCurrentTheme() — it throws if the Schemes property is absent.
+ if (ThemeManager.GetCurrentTheme () ["Schemes"].PropertyValue
+ is not Dictionary themeSchemes)
+ {
+ scheme = null;
+
+ return false;
+ }
+
+ schemes = themeSchemes;
+ }
+
+ if (schemes.TryGetValue (schemeName, out Scheme? s) && s is not null)
+ {
+ scheme = s;
+
+ return true;
+ }
+
+ scheme = null;
+
+ return false;
+ }
+ }
+
///
/// Gets the name of the specified . Will throw an exception if
/// is not a built-in Scheme.
diff --git a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs
index ee8bc63161..1cc8384ed3 100644
--- a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs
+++ b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs
@@ -134,20 +134,47 @@ public Scheme GetScheme ()
Scheme DefaultAction ()
{
- if (!HasScheme && !string.IsNullOrEmpty (SchemeName))
+ if (HasScheme)
{
- return SchemeManager.GetScheme (SchemeName);
+ return _scheme!;
}
- if (!HasScheme)
+ if (string.IsNullOrEmpty (SchemeName))
{
- return SuperView?.GetScheme () ?? SchemeManager.GetScheme (Schemes.Base);
+ return ResolveFallbackScheme ();
}
- return _scheme!;
+ if (SchemeManager.TryGetScheme (SchemeName, out Scheme? namedScheme))
+ {
+ return namedScheme;
+ }
+
+ Logging.Warning ($"SchemeName '{SchemeName}' not found in current theme. Falling back.");
+
+ return ResolveFallbackScheme ();
}
}
+ ///
+ /// Resolves a scheme using the fallback chain when no explicit scheme or valid is
+ /// available: 's scheme → "Base" in the current theme → hard-coded "Base".
+ ///
+ private Scheme ResolveFallbackScheme ()
+ {
+ if (SuperView is { })
+ {
+ return SuperView.GetScheme ();
+ }
+
+ if (SchemeManager.TryGetScheme ("Base", out Scheme? baseScheme))
+ {
+ return baseScheme;
+ }
+
+ // Last resort: hard-coded defaults are always available regardless of configuration state.
+ return SchemeManager.GetHardCodedSchemes ()! ["Base"]!;
+ }
+
///
/// Called when the for the is being retrieved. Subclasses can return
/// true to stop further processing and optionally set to a different value.
diff --git a/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs
index 1365d63f0b..2707872253 100644
--- a/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs
+++ b/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs
@@ -77,4 +77,36 @@ public void GetScheme_Throws_On_Invalid_String ()
Assert.Throws (() => SchemeManager.GetScheme ("NotAScheme"));
}
+ // Copilot
+
+ [Fact]
+ public void TryGetScheme_ExistingScheme_ReturnsTrueAndScheme ()
+ {
+ bool found = SchemeManager.TryGetScheme ("Base", out Scheme? scheme);
+
+ Assert.True (found);
+ Assert.NotNull (scheme);
+ }
+
+ [Fact]
+ public void TryGetScheme_MissingScheme_ReturnsFalseAndNull ()
+ {
+ bool found = SchemeManager.TryGetScheme ("DoesNotExist", out Scheme? scheme);
+
+ Assert.False (found);
+ Assert.Null (scheme);
+ }
+
+ [Fact]
+ public void TryGetScheme_AllBuiltInSchemes_ReturnsTrue ()
+ {
+ foreach (string name in SchemeManager.GetSchemeNames ())
+ {
+ bool found = SchemeManager.TryGetScheme (name, out Scheme? scheme);
+
+ Assert.True (found, $"Expected TryGetScheme to return true for built-in scheme '{name}'");
+ Assert.NotNull (scheme);
+ }
+ }
+
}
diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs
index 4f031c2cc8..1e13c5fd41 100644
--- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs
+++ b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs
@@ -1,5 +1,5 @@
using System.Reflection;
-using System.Text.Json;
+
namespace ConfigurationTests;
public class SourcesManagerTests
@@ -54,7 +54,6 @@ public void Load_WithValidStream_UpdatesSettingsScope ()
Assert.Contains (source, sourcesManager.Sources.Values);
}
-
#endregion
#region Update (FilePath)
@@ -91,7 +90,7 @@ public void Load_WithValidFile_UpdatesSettingsScope ()
"Driver.Force16Colors": true
}
""";
- var source = Path.GetTempFileName ();
+ string source = Path.GetTempFileName ();
var location = ConfigLocations.HardCoded;
File.WriteAllText (source, json);
@@ -192,7 +191,6 @@ public void Load_WithValidJson_UpdatesSettingsScope ()
Assert.Contains (source, sourcesManager.Sources.Values);
}
-
//[Fact]
//public void Update_WithValidJson_UpdatesThemeScope ()
//{
@@ -329,7 +327,7 @@ public void ToStream_WithValidScope_ReturnsStream ()
settingsScope ["Driver.Force16Colors"].PropertyValue = true;
// Act
- Stream stream = sourcesManager.ToStream (settingsScope);
+ var stream = sourcesManager.ToStream (settingsScope);
// Assert
Assert.NotNull (stream);
@@ -381,16 +379,10 @@ public void Load_WithDifferentLocations_AddsAllSourcesToCollection ()
var sourcesManager = new SourcesManager ();
var settingsScope = new SettingsScope ();
- ConfigLocations [] locations =
- [
- ConfigLocations.LibraryResources,
- ConfigLocations.Runtime,
- ConfigLocations.AppCurrent,
- ConfigLocations.GlobalHome
- ];
+ ConfigLocations [] locations = [ConfigLocations.LibraryResources, ConfigLocations.Runtime, ConfigLocations.AppCurrent, ConfigLocations.GlobalHome];
// Act - Update with different sources for different locations
- foreach (var location in locations)
+ foreach (ConfigLocations location in locations)
{
var source = $"config-{location}.json";
sourcesManager.Load (settingsScope, """{"Driver.Force16Colors": true}""", source, location);
@@ -398,7 +390,8 @@ public void Load_WithDifferentLocations_AddsAllSourcesToCollection ()
// Assert
Assert.Equal (locations.Length, sourcesManager.Sources.Count);
- foreach (var location in locations)
+
+ foreach (ConfigLocations location in locations)
{
Assert.Contains (location, sourcesManager.Sources.Keys);
Assert.Equal ($"config-{location}.json", sourcesManager.Sources [location]);
@@ -433,22 +426,21 @@ public void Load_WithNonExistentFileAndDifferentLocations_TracksAllSources ()
var settingsScope = new SettingsScope ();
// Define multiple files and locations
- var fileLocations = new Dictionary (StringComparer.InvariantCultureIgnoreCase)
- {
- { "file1.json", ConfigLocations.AppCurrent },
- { "file2.json", ConfigLocations.GlobalHome },
- { "file3.json", ConfigLocations.AppHome }
- };
+ Dictionary fileLocations = new (StringComparer.InvariantCultureIgnoreCase)
+ {
+ { "file1.json", ConfigLocations.AppCurrent }, { "file2.json", ConfigLocations.GlobalHome }, { "file3.json", ConfigLocations.AppHome }
+ };
// Act
- foreach (var pair in fileLocations)
+ foreach (KeyValuePair pair in fileLocations)
{
sourcesManager.Load (settingsScope, pair.Key, pair.Value);
}
// Assert
Assert.Equal (fileLocations.Count, sourcesManager.Sources.Count);
- foreach (var pair in fileLocations)
+
+ foreach (KeyValuePair pair in fileLocations)
{
Assert.Contains (pair.Value, sourcesManager.Sources.Keys);
Assert.Equal (pair.Key, sourcesManager.Sources [pair.Value]);
@@ -490,5 +482,4 @@ public void Sources_IsPreservedAcrossOperations ()
}
#endregion
-
}
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs
index 13db3b7252..7eff4a2ce5 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs
+++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs
@@ -1,18 +1,16 @@
-#nullable enable
+using System.Collections.Immutable;
using UnitTests;
-using Xunit;
namespace ViewBaseTests.Drawing;
[Trait ("Category", "View.Scheme")]
public class SchemeTests : TestDriverBase
{
-
[Fact]
public void GetScheme_Default_ReturnsBaseScheme ()
{
var view = new View ();
- var baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"];
+ Scheme? baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"];
Assert.Equal (baseScheme, view.GetScheme ());
view.Dispose ();
@@ -22,7 +20,7 @@ public void GetScheme_Default_ReturnsBaseScheme ()
public void SetScheme_Explicitly_SetsSchemeCorrectly ()
{
var view = new View ();
- var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
view.SetScheme (dialogScheme);
@@ -39,7 +37,7 @@ public void GetScheme_InheritsFromSuperView_WhenNotExplicitlySet ()
superView.Add (subView);
- var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
superView.SetScheme (dialogScheme);
Assert.Equal (dialogScheme, subView.GetScheme ());
@@ -55,7 +53,7 @@ public void SetSchemeName_OverridesInheritedScheme ()
var view = new View ();
view.SchemeName = "Dialog";
- var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
Assert.Equal (dialogScheme, view.GetScheme ());
view.Dispose ();
}
@@ -68,7 +66,7 @@ public void GetAttribute_ReturnsCorrectAttribute_Via_Mock ()
view.Driver.SetAttribute (new Attribute (Color.Red, Color.Green));
// Act
- var attribute = view.GetCurrentAttribute ();
+ Attribute attribute = view.GetCurrentAttribute ();
// Assert
Assert.Equal (new Attribute (Color.Red, Color.Green), attribute);
@@ -107,7 +105,7 @@ public void SetAttributeForRole_SetsCorrectAttribute ()
view.Driver = CreateTestDriver ();
view.Driver.SetAttribute (new Attribute (Color.Red, Color.Green));
- var previousAttribute = view.SetAttributeForRole (VisualRole.Focus);
+ Attribute? previousAttribute = view.SetAttributeForRole (VisualRole.Focus);
Assert.Equal (view.GetScheme ().Focus, view.GetCurrentAttribute ());
Assert.NotEqual (previousAttribute, view.GetCurrentAttribute ());
@@ -118,7 +116,7 @@ public void SetAttributeForRole_SetsCorrectAttribute ()
public void OnGettingScheme_Override_StopsDefaultBehavior ()
{
var view = new CustomView ();
- var customScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"];
+ Scheme? customScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"];
Assert.Equal (customScheme, view.GetScheme ());
view.Dispose ();
@@ -128,7 +126,7 @@ public void OnGettingScheme_Override_StopsDefaultBehavior ()
public void OnSettingScheme_Override_PreventsSettingScheme ()
{
var view = new CustomView ();
- var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
view.SetScheme (dialogScheme);
@@ -140,7 +138,7 @@ public void OnSettingScheme_Override_PreventsSettingScheme ()
public void GettingScheme_Event_CanOverrideScheme ()
{
var view = new View ();
- var customScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"]! with { Normal = Attribute.Default };
+ Scheme customScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"]! with { Normal = Attribute.Default };
Assert.NotEqual (Attribute.Default, view.GetScheme ().Normal);
@@ -159,7 +157,7 @@ public void GettingScheme_Event_CanOverrideScheme ()
public void SettingScheme_Event_CanCancelSchemeChange ()
{
var view = new View ();
- var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
view.SchemeChanging += (sender, args) => args.Handled = true;
@@ -191,7 +189,7 @@ public void GetAttributeForRole_Event_CanOverrideAttribute ()
[Fact]
public void GetHardCodedSchemes_ReturnsExpectedSchemes ()
{
- var schemes = Scheme.GetHardCodedSchemes ();
+ ImmutableSortedDictionary schemes = Scheme.GetHardCodedSchemes ();
Assert.NotNull (schemes);
Assert.Contains ("Base", schemes.Keys);
@@ -201,7 +199,6 @@ public void GetHardCodedSchemes_ReturnsExpectedSchemes ()
Assert.Contains ("Runnable", schemes.Keys);
}
-
[Fact]
public void SchemeName_OverridesSuperViewScheme ()
{
@@ -212,7 +209,7 @@ public void SchemeName_OverridesSuperViewScheme ()
subView.SchemeName = "Error";
- var errorScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"];
+ Scheme? errorScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"];
Assert.Equal (errorScheme, subView.GetScheme ());
subView.Dispose ();
@@ -223,7 +220,7 @@ public void SchemeName_OverridesSuperViewScheme ()
public void Scheme_DefaultsToBase_WhenNotSet ()
{
var view = new View ();
- var baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"];
+ Scheme? baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"];
Assert.Equal (baseScheme, view.GetScheme ());
view.Dispose ();
@@ -235,7 +232,7 @@ public void Scheme_HandlesNullSuperViewGracefully ()
var view = new View ();
view.SchemeName = "Dialog";
- var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
Assert.Equal (dialogScheme, view.GetScheme ());
view.Dispose ();
@@ -250,10 +247,7 @@ protected override bool OnGettingScheme (out Scheme? scheme)
return true;
}
- protected override bool OnSettingScheme (ValueChangingEventArgs args)
- {
- return true; // Prevent setting the scheme
- }
+ protected override bool OnSettingScheme (ValueChangingEventArgs args) => true; // Prevent setting the scheme
}
[Fact]
@@ -265,14 +259,15 @@ public void GetAttributeForRole_SubView_DefersToSuperView_WhenNoExplicitScheme (
// 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;
- }
- };
+ {
+ 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));
@@ -289,19 +284,20 @@ public void GetAttributeForRole_SubView_UsesOwnScheme_WhenExplicitlySet ()
parentView.Add (childView);
// Set explicit scheme on child
- var childScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ Scheme? 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;
- }
- };
+ {
+ 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));
@@ -315,8 +311,9 @@ public void GetAttributeForRole_SubView_UsesOwnScheme_WhenExplicitlySet ()
public void GetAttributeForRole_Border_UsesParentScheme ()
{
// Border (an Adornment) doesn't have a SuperView but should use its Parent's scheme
- View view = new View { SchemeName = "Dialog" };
+ var view = new View { SchemeName = "Dialog" };
Border? border = view.Border;
+
// Force GetOrCreateView
border.LineStyle = LineStyle.Dashed;
@@ -344,18 +341,19 @@ public void GetAttributeForRole_SubView_UsesSchemeName_WhenSet ()
// 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;
- }
- };
+ {
+ 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"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
Assert.NotEqual (customAttribute, childView.GetAttributeForRole (VisualRole.Normal));
Assert.Equal (dialogScheme!.Normal, childView.GetAttributeForRole (VisualRole.Normal));
@@ -378,14 +376,15 @@ public void GetAttributeForRole_NestedHierarchy_DefersCorrectly ()
// 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;
- }
- };
+ {
+ 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));
@@ -413,17 +412,18 @@ public void GetAttributeForRole_ParentWithSchemeNameBreaksChain ()
// 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;
- }
- };
+ {
+ 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"];
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
Assert.NotEqual (customAttribute, parentView.GetAttributeForRole (VisualRole.Normal));
Assert.Equal (dialogScheme!.Normal, parentView.GetAttributeForRole (VisualRole.Normal));
@@ -447,14 +447,15 @@ public void GetAttributeForRole_OnGettingAttributeForRole_TakesPrecedence ()
// 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;
- }
- };
+ {
+ 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);
@@ -481,23 +482,28 @@ public void GetAttributeForRole_MultipleRoles_DeferCorrectly ()
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;
- }
- };
+ {
+ 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));
@@ -517,10 +523,94 @@ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attri
if (OverrideAttribute.HasValue && role == VisualRole.Normal)
{
currentAttribute = OverrideAttribute.Value;
+
return true;
}
+
return base.OnGettingAttributeForRole (role, ref currentAttribute);
}
}
-}
\ No newline at end of file
+ // Copilot - fallback chain tests for discussion #4457
+
+ [Fact]
+ public void GetScheme_SchemeName_MissingScheme_FallsBackToSuperView ()
+ {
+ // A view with SchemeName set to a non-existent scheme should fall back to SuperView's scheme,
+ // not throw a KeyNotFoundException.
+ View superView = new ();
+ View subView = new ();
+ superView.Add (subView);
+
+ Scheme? dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"];
+ superView.SetScheme (dialogScheme);
+
+ subView.SchemeName = "NonExistentScheme";
+
+ // Should not throw; should fall back to superView's Dialog scheme
+ Scheme resolved = subView.GetScheme ();
+
+ Assert.Equal (dialogScheme, resolved);
+
+ subView.Dispose ();
+ superView.Dispose ();
+ }
+
+ [Fact]
+ public void GetScheme_SchemeName_MissingScheme_NoSuperView_FallsBackToBase ()
+ {
+ // A view with SchemeName set to a non-existent scheme and no SuperView should fall back
+ // to the "Base" scheme, not throw a KeyNotFoundException.
+ View view = new ();
+ view.SchemeName = "NonExistentScheme";
+
+ Scheme? baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"];
+
+ Scheme resolved = view.GetScheme ();
+
+ Assert.Equal (baseScheme, resolved);
+
+ view.Dispose ();
+ }
+
+ [Fact]
+ public void GetScheme_SchemeName_ExistingScheme_NoFallback ()
+ {
+ // Regression: a view with SchemeName pointing to an existing scheme should still
+ // return that scheme and not be affected by the fallback logic.
+ View view = new ();
+ view.SchemeName = "Error";
+
+ Scheme? errorScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"];
+
+ Assert.Equal (errorScheme, view.GetScheme ());
+
+ view.Dispose ();
+ }
+
+ [Fact]
+ public void GetScheme_SchemeName_MissingScheme_SuperViewAlsoMissingScheme_FallsBackToBase ()
+ {
+ // A view whose SchemeName is missing AND whose SuperView has no scheme either
+ // should ultimately fall back all the way to "Base".
+ View grandparent = new ();
+ View parent = new ();
+ View child = new ();
+
+ grandparent.Add (parent);
+ parent.Add (child);
+
+ // Neither grandparent nor parent have explicit schemes
+ child.SchemeName = "NonExistentScheme";
+
+ Scheme? baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"];
+
+ Scheme resolved = child.GetScheme ();
+
+ Assert.Equal (baseScheme, resolved);
+
+ child.Dispose ();
+ parent.Dispose ();
+ grandparent.Dispose ();
+ }
+}