From 433f204c819372d7605d901e5088936d2fc18a59 Mon Sep 17 00:00:00 2001 From: doyaGu Date: Sat, 3 Oct 2026 02:59:47 -0400 Subject: [PATCH] feat: apply ui flow direction to modal windows and guard code-behind resources usage --- .../Conventions/CodeBehindConventionTests.cs | 60 +++++++++++++++++++ YKanBan/App.axaml.cs | 21 ++++--- YKanBan/Views/MainWindow.axaml.cs | 16 ++++- 3 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 YKanBan.Tests/Conventions/CodeBehindConventionTests.cs diff --git a/YKanBan.Tests/Conventions/CodeBehindConventionTests.cs b/YKanBan.Tests/Conventions/CodeBehindConventionTests.cs new file mode 100644 index 0000000..28390c0 --- /dev/null +++ b/YKanBan.Tests/Conventions/CodeBehindConventionTests.cs @@ -0,0 +1,60 @@ +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace YKanBan.Tests.Conventions; + +/// +/// Source-level guards for code-behind conventions (plan ยง8.1): inside control, +/// window and application code-behind a bare Resources.Key resolves to +/// the StyledElement.Resources instance property, so the generated class +/// must be written as YKanBan.Resources (or through a using alias). +/// +[TestClass] +public partial class CodeBehindConventionTests +{ + /// A Resources. member access not preceded by a qualifier or identifier character. + [GeneratedRegex(@"(? + /// Locates the application project's source folder from this file's compile-time path. + /// + /// Filled in by the compiler. + /// The YKanBan project folder. + private static string ApplicationSourceFolder([CallerFilePath] string thisFile = "") + { + DirectoryInfo? directory = new FileInfo(thisFile).Directory; + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "YKanBan", "YKanBan.csproj"))) + { + directory = directory.Parent; + } + + Assert.IsNotNull(directory, "Repository root not found from the test source path."); + return Path.Combine(directory.FullName, "YKanBan"); + } + + [TestMethod] + public void CodeBehindNeverUsesBareResourcesClassName() + { + string root = ApplicationSourceFolder(); + string[] files = Directory.GetFiles(root, "*.axaml.cs", SearchOption.AllDirectories) + .Where(path => !path.Split(Path.DirectorySeparatorChar).Any(part => part is "obj" or "bin")) + .ToArray(); + Assert.IsTrue(files.Length > 0, "No code-behind files found."); + + var offenders = new List(); + foreach (string file in files) + { + string[] lines = File.ReadAllLines(file); + for (int index = 0; index < lines.Length; index++) + { + if (BareResourcesAccess().IsMatch(lines[index])) + { + offenders.Add($"{Path.GetRelativePath(root, file)}:{index + 1}: {lines[index].Trim()}"); + } + } + } + + Assert.AreEqual(0, offenders.Count, "Use YKanBan.Resources in code-behind:" + Environment.NewLine + string.Join(Environment.NewLine, offenders)); + } +} diff --git a/YKanBan/App.axaml.cs b/YKanBan/App.axaml.cs index 1977aec..dae177c 100644 --- a/YKanBan/App.axaml.cs +++ b/YKanBan/App.axaml.cs @@ -26,6 +26,12 @@ public partial class App : Application /// public static string StartupLanguage { get; private set; } = AppConfig.DefaultLanguage; + /// + /// Gets the flow direction of the startup culture. FlowDirection does not + /// inherit across windows, so the main window and every modal window apply it. + /// + public static FlowDirection UiFlowDirection { get; private set; } = FlowDirection.LeftToRight; + public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -43,19 +49,20 @@ public partial class App : Application CultureInfo culture = ResolveCulture(Services.Config.Language); StartupLanguage = Services.Config.Language; Resx.Culture = culture; + + // RTL detection drives the flow direction of every window; the two + // supported languages are LTR, but the infrastructure stays in + // place for future languages. + UiFlowDirection = culture.TextInfo.IsRightToLeft + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; ApplyTheme(Services.Config.Theme); var viewModel = new MainWindowViewModel(Services, desktop.Args ?? []); desktop.MainWindow = new MainWindow { DataContext = viewModel, - - // RTL detection drives the whole window flow direction; the two - // supported languages are LTR, but the infrastructure stays in - // place for future languages. - FlowDirection = culture.TextInfo.IsRightToLeft - ? FlowDirection.RightToLeft - : FlowDirection.LeftToRight, + FlowDirection = UiFlowDirection, }; } diff --git a/YKanBan/Views/MainWindow.axaml.cs b/YKanBan/Views/MainWindow.axaml.cs index 6cbaf8f..0f19084 100644 --- a/YKanBan/Views/MainWindow.axaml.cs +++ b/YKanBan/Views/MainWindow.axaml.cs @@ -37,7 +37,7 @@ public partial class MainWindow : Window { DataContext = new SettingsDialogViewModel(App.Services.Config, App.StartupLanguage), }; - await dialog.ShowDialog(this); + await ShowModalAsync(dialog); } /// Shows the about dialog centered on this window. @@ -47,6 +47,18 @@ public partial class MainWindow : Window { DataContext = new AboutDialogViewModel(), }; - await dialog.ShowDialog(this); + await ShowModalAsync(dialog); + } + + /// + /// Shows a modal window owned by this window. Every modal goes through here: + /// FlowDirection does not inherit across windows, so it is applied explicitly. + /// + /// The modal window to show. + /// A task completing when the dialog closes. + private Task ShowModalAsync(Window dialog) + { + dialog.FlowDirection = App.UiFlowDirection; + return dialog.ShowDialog(this); } }