From ba3d28028e3f4ecba324e22d46e86c77411fd9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20La=C5=A1t=C5=AFvka?= Date: Sun, 24 Nov 2024 00:47:32 +0100 Subject: [PATCH 01/34] CQ: Add Win32 methods --- src/Files.App.CsWin32/NativeMethods.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 3b611b024af5..fa41b782c00f 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -156,3 +156,5 @@ IApplicationDestinations ApplicationDestinations IApplicationDocumentLists ApplicationDocumentLists +SetWindowLongPtr +GetWindowLongPtr From bfc80029e10d1835d05b494f0fdbe2958942c951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20La=C5=A1t=C5=AFvka?= Date: Sun, 24 Nov 2024 00:54:41 +0100 Subject: [PATCH 02/34] CQ: Create SourceGenerator --- src/Files.Core.SourceGenerator/Constants.cs | 14 ++ .../Generators/RealTimeLayoutGenerator.cs | 154 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 src/Files.Core.SourceGenerator/Generators/RealTimeLayoutGenerator.cs diff --git a/src/Files.Core.SourceGenerator/Constants.cs b/src/Files.Core.SourceGenerator/Constants.cs index 5da27ceba8e1..77034e8ebcbf 100644 --- a/src/Files.Core.SourceGenerator/Constants.cs +++ b/src/Files.Core.SourceGenerator/Constants.cs @@ -98,5 +98,19 @@ internal class StringsPropertyGenerator /// internal const char ConstantSeparator = '/'; } + + internal class RealTimeLayoutGenerator + { + internal const string SpecificationWindowName = "IRealTimeWindow"; + + internal const string SpecificationControlName = "IRealTimeControl"; + + internal enum SpecificationType + { + None = 0, + Window, + Control + } + } } } diff --git a/src/Files.Core.SourceGenerator/Generators/RealTimeLayoutGenerator.cs b/src/Files.Core.SourceGenerator/Generators/RealTimeLayoutGenerator.cs new file mode 100644 index 000000000000..44e1448dc45b --- /dev/null +++ b/src/Files.Core.SourceGenerator/Generators/RealTimeLayoutGenerator.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using static Files.Core.SourceGenerator.Constants.RealTimeLayoutGenerator; + +namespace Files.Core.SourceGenerator.Generators +{ + [Generator] + public class RealTimeLayoutGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var candidateClasses = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (node, cancellationToken) => IsValidCandidate(node), + transform: (context, cancellationToken) => GetCandidateClass(context)) + .Where(candidate => candidate != null); + + context.RegisterSourceOutput(candidateClasses, (context, candidate) => + { + var classDeclaration = candidate!.Value.Class; + var type = candidate!.Value.Type; + var className = classDeclaration.Identifier.Text; + var namespaceName = GetNamespace(classDeclaration); + + var source = GenerateClass(namespaceName, className, type); + context.AddSource($"{className}.g.cs", SourceText.From(source, Encoding.UTF8)); + }); + } + + private static bool IsValidCandidate(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.BaseList?.Types.Any(baseType => baseType.Type is IdentifierNameSyntax identifier && + (identifier.Identifier.Text == SpecificationWindowName || identifier.Identifier.Text == SpecificationControlName)) == true; + } + return false; + } + + private static (ClassDeclarationSyntax Class, SpecificationType Type)? GetCandidateClass(GeneratorSyntaxContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + var type = SpecificationType.None; + foreach (var baseType in classDeclaration.BaseList!.Types) + { + if (baseType.Type is IdentifierNameSyntax identifier) + { + if (identifier.Identifier.Text == SpecificationWindowName) + type = SpecificationType.Window; + else if (identifier.Identifier.Text == SpecificationControlName) + type = SpecificationType.Control; + } + } + + if (type != SpecificationType.None) + return (classDeclaration, type); + + return null; + } + + private static string GenerateClass(string namespaceName, string className, SpecificationType type) + { + // Namespace + var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(namespaceName)) + .NormalizeWhitespace(); + + // Usings + var usings = new[] + { + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Windows")), + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("Microsoft.UI.Xaml")), + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("Microsoft.UI.Xaml.Controls")) + }; + + // Field declaration: private IRealTimeLayoutService RTLayoutService; + var fieldDeclaration = SyntaxFactory.FieldDeclaration( + SyntaxFactory.VariableDeclaration( + SyntaxFactory.IdentifierName("IRealTimeLayoutService")) + .AddVariables(SyntaxFactory.VariableDeclarator("RTLayoutService"))) + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); + + // Method: InitializeContentLayout + var initializeContentLayoutMethod = SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + "InitializeContentLayout") + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) + .WithBody(SyntaxFactory.Block(CreateInitializeContentLayoutBody(type))); + + // Method: UpdateContentLayout + var updateContentLayoutMethod = SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + "UpdateContentLayout") + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) + .WithBody(SyntaxFactory.Block(CreateUpdateContentLayoutBody(type))); + + // Class declaration + var classDeclaration = SyntaxFactory.ClassDeclaration(className) + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)) + .AddMembers(fieldDeclaration, initializeContentLayoutMethod, updateContentLayoutMethod); + + // Add class to namespace + namespaceDeclaration = namespaceDeclaration.AddMembers(classDeclaration); + + var compilationUnit = SyntaxFactory.CompilationUnit() + .AddUsings(usings) + .AddMembers(namespaceDeclaration) + .NormalizeWhitespace(); + + return compilationUnit.ToFullString(); + } + + private static IEnumerable CreateInitializeContentLayoutBody(SpecificationType type) + { + var statements = new List + { + + SyntaxFactory.ParseStatement("RTLayoutService = Ioc.Default.GetRequiredService();") + }; + + if (type == SpecificationType.Window) + statements.Add(SyntaxFactory.ParseStatement("RTLayoutService.UpdateTitleBar(this);")); + + statements.Add(SyntaxFactory.ParseStatement("RTLayoutService.UpdateContent(this);")); + statements.Add(SyntaxFactory.ParseStatement("RTLayoutService.AddCallback(this, UpdateContentLayout);")); + + return statements; + } + + private static IEnumerable CreateUpdateContentLayoutBody(SpecificationType type) + { + var statements = new List(); + + if (type == SpecificationType.Window) + statements.Add(SyntaxFactory.ParseStatement("RTLayoutService.UpdateTitleBar(this);")); + + statements.Add(SyntaxFactory.ParseStatement("RTLayoutService.UpdateContent(this);")); + + return statements; + } + + private static string GetNamespace(SyntaxNode node) + { + while (node != null) + { + if (node is NamespaceDeclarationSyntax namespaceDeclaration) + return namespaceDeclaration.Name.ToString(); + node = node.Parent!; + } + return "GlobalNamespace"; + } + } +} \ No newline at end of file From 52daef503659bb1882a2e6e4a2343ac10e27233b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20La=C5=A1t=C5=AFvka?= Date: Sun, 24 Nov 2024 00:58:58 +0100 Subject: [PATCH 03/34] Feature: Implemented service `RealTimeLayoutService` --- src/Files.App/App.xaml.cs | 6 ++ .../Data/Contracts/IRealTimeControl.cs | 12 +++ .../Data/Contracts/IRealTimeLayoutService.cs | 18 ++++ .../Data/Contracts/IRealTimeWindow.cs | 12 +++ .../Helpers/Application/AppLifecycleHelper.cs | 2 + src/Files.App/MainWindow.xaml.cs | 2 +- .../Services/Content/RealTimeLayoutService.cs | 84 +++++++++++++++++++ .../ViewModels/Settings/GeneralViewModel.cs | 4 + 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/Files.App/Data/Contracts/IRealTimeControl.cs create mode 100644 src/Files.App/Data/Contracts/IRealTimeLayoutService.cs create mode 100644 src/Files.App/Data/Contracts/IRealTimeWindow.cs create mode 100644 src/Files.App/Services/Content/RealTimeLayoutService.cs diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 4d305570a86d..638e0f53cb88 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -3,6 +3,9 @@ using CommunityToolkit.WinUI.Helpers; using Files.App.Helpers.Application; +using Files.App.Services.Content; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -97,6 +100,9 @@ async Task ActivateAsync() var userSettingsService = Ioc.Default.GetRequiredService(); var isLeaveAppRunning = userSettingsService.GeneralSettingsService.LeaveAppRunning; + Ioc.Default.GetRequiredService().UpdateCulture(new(AppLanguageHelper.PreferredLanguage.Code)); + MainWindow.Instance.InitializeContentLayout(); + if (isStartupTask && !isLeaveAppRunning) { // Initialize and activate MainWindow diff --git a/src/Files.App/Data/Contracts/IRealTimeControl.cs b/src/Files.App/Data/Contracts/IRealTimeControl.cs new file mode 100644 index 000000000000..22f5a0883517 --- /dev/null +++ b/src/Files.App/Data/Contracts/IRealTimeControl.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Contracts +{ + internal interface IRealTimeControl + { + void InitializeContentLayout(); + + void UpdateContentLayout(); + } +} \ No newline at end of file diff --git a/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs b/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs new file mode 100644 index 000000000000..76360ea59e3f --- /dev/null +++ b/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs @@ -0,0 +1,18 @@ +using Microsoft.UI.Xaml; +using System.Globalization; + +namespace Files.App.Data.Contracts +{ + public interface IRealTimeLayoutService + { + void AddCallback(object target, Action callback); + + void UpdateContent(FrameworkElement frameworkElement); + + void UpdateContent(Window window); + + void UpdateCulture(CultureInfo culture); + + bool UpdateTitleBar(Window window); + } +} diff --git a/src/Files.App/Data/Contracts/IRealTimeWindow.cs b/src/Files.App/Data/Contracts/IRealTimeWindow.cs new file mode 100644 index 000000000000..d3a1e099eaa2 --- /dev/null +++ b/src/Files.App/Data/Contracts/IRealTimeWindow.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Contracts +{ + internal interface IRealTimeWindow + { + void InitializeContentLayout() { } + + void UpdateContentLayout() { } + } +} diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 4804f6c001e4..8b06324763c8 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -3,6 +3,7 @@ using CommunityToolkit.WinUI.Helpers; using Files.App.Helpers.Application; +using Files.App.Services.Content; using Files.App.Services.SizeProvider; using Files.App.Utils.Logger; using Files.App.ViewModels.Settings; @@ -208,6 +209,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() // ViewModels .AddSingleton() .AddSingleton() diff --git a/src/Files.App/MainWindow.xaml.cs b/src/Files.App/MainWindow.xaml.cs index e80e55247649..4b96dfb4c54d 100644 --- a/src/Files.App/MainWindow.xaml.cs +++ b/src/Files.App/MainWindow.xaml.cs @@ -13,7 +13,7 @@ namespace Files.App { - public sealed partial class MainWindow : WinUIEx.WindowEx + public sealed partial class MainWindow : WinUIEx.WindowEx, IRealTimeWindow { private static MainWindow? _Instance; public static MainWindow Instance => _Instance ??= new(); diff --git a/src/Files.App/Services/Content/RealTimeLayoutService.cs b/src/Files.App/Services/Content/RealTimeLayoutService.cs new file mode 100644 index 000000000000..f0447cbdd198 --- /dev/null +++ b/src/Files.App/Services/Content/RealTimeLayoutService.cs @@ -0,0 +1,84 @@ +using Microsoft.UI.Xaml; +using System.Globalization; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; + +namespace Files.App.Services.Content +{ + internal sealed class RealTimeLayoutService : IRealTimeLayoutService + { + private readonly List<(WeakReference Reference, Action Callback)> _callbacks = []; + private static FlowDirection FlowDirection => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; + + public void UpdateCulture(CultureInfo culture) + { + FlowDirection tmp = FlowDirection; + + CultureInfo.CurrentUICulture = culture; + + if (tmp != FlowDirection) + InvokeCallbacks(); + } + + public void AddCallback(object target, Action callback) + { + var weakReference = new WeakReference(target); + _callbacks.Add((weakReference, callback)); + + if (target is Window window) + window.Closed += (sender, args) => RemoveCallback(target); + + if (target is FrameworkElement element) + element.Unloaded += (sender, args) => RemoveCallback(target); + } + + public bool UpdateTitleBar(Window window) + { + try + { + var hwnd = new HWND(WindowNative.GetWindowHandle(window)); + var exStyle = PInvoke.GetWindowLongPtr(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + exStyle = FlowDirection is FlowDirection.RightToLeft + ? new((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_LAYOUTRTL) // Set RTL layout + : new((uint)exStyle.ToInt64() & ~(uint)WINDOW_EX_STYLE.WS_EX_LAYOUTRTL); // Set LTR layout + + if (PInvoke.SetWindowLongPtr(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, exStyle) == 0) + return false; + } + catch (Exception) + { + return false; + } + + return true; + } + + public void UpdateContent(Window window) + { + if (window.Content is FrameworkElement frameworkElement) + frameworkElement.FlowDirection = FlowDirection; + } + + public void UpdateContent(FrameworkElement frameworkElement) + { + frameworkElement.FlowDirection = FlowDirection; + } + + private void RemoveCallback(object target) + { + _callbacks.RemoveAll(item => + item.Reference.TryGetTarget(out var targetObject) && targetObject == target); + } + + private void InvokeCallbacks() + { + _callbacks.Where(item => + item.Reference.TryGetTarget(out var targetObject) && targetObject != null) + .Select(item => item.Callback).ForEach(callback => callback()); + _callbacks.RemoveAll(item => !item.Reference.TryGetTarget(out _)); + } + } +} diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index 004d51fa6a09..337f82343089 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -77,6 +77,10 @@ public int SelectedAppLanguageIndex { selectedAppLanguageIndex = value; OnPropertyChanged(nameof(SelectedAppLanguageIndex)); + + var RTLayoutService = Ioc.Default.GetRequiredService(); + RTLayoutService.UpdateCulture(new(AppLanguageHelper.PreferredLanguage.Code)); + ShowRestartControl = true; } } From 85b8f95f5b51b0093154ca9c5e857a1f269d81b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20La=C5=A1t=C5=AFvka?= Date: Sun, 24 Nov 2024 01:01:29 +0100 Subject: [PATCH 04/34] CQ: Change visibility --- src/Files.App/Services/Content/RealTimeLayoutService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Services/Content/RealTimeLayoutService.cs b/src/Files.App/Services/Content/RealTimeLayoutService.cs index f0447cbdd198..690aa08fd22d 100644 --- a/src/Files.App/Services/Content/RealTimeLayoutService.cs +++ b/src/Files.App/Services/Content/RealTimeLayoutService.cs @@ -10,7 +10,7 @@ namespace Files.App.Services.Content internal sealed class RealTimeLayoutService : IRealTimeLayoutService { private readonly List<(WeakReference Reference, Action Callback)> _callbacks = []; - private static FlowDirection FlowDirection => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; + public static FlowDirection FlowDirection => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; public void UpdateCulture(CultureInfo culture) { From e7668e415cd4f37c197c411b54a04b2ca29fe944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20La=C5=A1t=C5=AFvka?= Date: Sun, 24 Nov 2024 01:15:41 +0100 Subject: [PATCH 05/34] CQ: Correct change CurrentCulture --- src/Files.App/Services/Content/RealTimeLayoutService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Files.App/Services/Content/RealTimeLayoutService.cs b/src/Files.App/Services/Content/RealTimeLayoutService.cs index 690aa08fd22d..1ab3b93ba198 100644 --- a/src/Files.App/Services/Content/RealTimeLayoutService.cs +++ b/src/Files.App/Services/Content/RealTimeLayoutService.cs @@ -16,6 +16,7 @@ public void UpdateCulture(CultureInfo culture) { FlowDirection tmp = FlowDirection; + CultureInfo.DefaultThreadCurrentUICulture = culture; CultureInfo.CurrentUICulture = culture; if (tmp != FlowDirection) From ae06d2dbe83575ca405ccce3e8bf04d6c9e771a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20La=C5=A1t=C5=AFvka?= Date: Sun, 24 Nov 2024 01:47:51 +0100 Subject: [PATCH 06/34] CQ: Fix DragRegions --- .../Data/Contracts/IRealTimeLayoutService.cs | 2 + .../Services/Content/RealTimeLayoutService.cs | 3 +- .../UserControls/AddressToolbar.xaml | 133 ++++++++++-------- .../UserControls/Sidebar/SidebarView.xaml.cs | 9 +- src/Files.App/Views/MainPage.xaml.cs | 6 +- 5 files changed, 92 insertions(+), 61 deletions(-) diff --git a/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs b/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs index 76360ea59e3f..71735fdcaeb3 100644 --- a/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs +++ b/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs @@ -5,6 +5,8 @@ namespace Files.App.Data.Contracts { public interface IRealTimeLayoutService { + + FlowDirection FlowDirection { get; } void AddCallback(object target, Action callback); void UpdateContent(FrameworkElement frameworkElement); diff --git a/src/Files.App/Services/Content/RealTimeLayoutService.cs b/src/Files.App/Services/Content/RealTimeLayoutService.cs index 1ab3b93ba198..abc9c7c11fa9 100644 --- a/src/Files.App/Services/Content/RealTimeLayoutService.cs +++ b/src/Files.App/Services/Content/RealTimeLayoutService.cs @@ -10,7 +10,8 @@ namespace Files.App.Services.Content internal sealed class RealTimeLayoutService : IRealTimeLayoutService { private readonly List<(WeakReference Reference, Action Callback)> _callbacks = []; - public static FlowDirection FlowDirection => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; + + public FlowDirection FlowDirection => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; public void UpdateCulture(CultureInfo culture) { diff --git a/src/Files.App/UserControls/AddressToolbar.xaml b/src/Files.App/UserControls/AddressToolbar.xaml index fcf48e0c9648..e43cd78af2a2 100644 --- a/src/Files.App/UserControls/AddressToolbar.xaml +++ b/src/Files.App/UserControls/AddressToolbar.xaml @@ -263,63 +263,68 @@ - - - + + + + +