diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 68837251b7f1..5bbe10d32b16 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -160,3 +160,5 @@ IApplicationDocumentLists ApplicationDocumentLists IApplicationActivationManager MENU_ITEM_TYPE +SetWindowLongPtr +GetWindowLongPtr diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 4d305570a86d..88fa149d473e 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,11 @@ async Task ActivateAsync() var userSettingsService = Ioc.Default.GetRequiredService(); var isLeaveAppRunning = userSettingsService.GeneralSettingsService.LeaveAppRunning; + var realTimeLayoutService = Ioc.Default.GetRequiredService(); + realTimeLayoutService.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..1343da4a9ab0 --- /dev/null +++ b/src/Files.App/Data/Contracts/IRealTimeControl.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Contracts +{ + /// + /// Defines an interface for real-time control components that manage content layout updates. + /// + internal interface IRealTimeControl + { + /// + /// Gets the service for managing real-time layout updates. + /// + IRealTimeLayoutService RealTimeLayoutService { get; } + + /// + /// Initializes the content layout for the control. + /// + void InitializeContentLayout(); + + /// + /// Updates the content layout of the control. + /// + 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..e4fb9a554187 --- /dev/null +++ b/src/Files.App/Data/Contracts/IRealTimeLayoutService.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Microsoft.UI.Xaml; +using System.Globalization; + +namespace Files.App.Data.Contracts +{ + /// + /// Provides an interface for managing real-time layout updates and related operations. + /// + public interface IRealTimeLayoutService + { + /// + /// Occurs when the flow direction of the layout changes. + /// + public event EventHandler? FlowDirectionChanged; + + /// + /// Gets the current flow direction for layout (e.g., LeftToRight or RightToLeft). + /// + FlowDirection FlowDirection { get; } + + /// + /// Updates the culture settings for the layout. + /// + /// The culture information to apply. + void UpdateCulture(CultureInfo culture); + + /// + /// Adds a callback for a implementing . + /// The callback is automatically removed when the window is closed. + /// + /// The instance that implements . + /// The action to be executed when the callback is triggered. + void AddCallback(Window target, Action callback); + + /// + /// Adds a callback for a implementing . + /// The callback is automatically removed when the element is unloaded. + /// + /// The instance that implements . + /// The action to be executed when the callback is triggered. + void AddCallback(FrameworkElement target, Action callback); + + /// + /// Updates the title bar layout of the specified window based on the current flow direction. + /// + /// The window whose title bar layout needs updating. + /// True if the title bar layout was successfully updated; otherwise, false. + bool UpdateTitleBar(Window window); + + /// + /// Updates the content layout of the specified window to match the current flow direction. + /// + /// The window whose content layout needs updating. + void UpdateContent(Window window); + + /// + /// Updates the content layout of the specified framework element to match the current flow direction. + /// + /// The framework element whose content layout needs updating. + void UpdateContent(FrameworkElement frameworkElement); + } +} diff --git a/src/Files.App/Data/Contracts/IRealTimeWindow.cs b/src/Files.App/Data/Contracts/IRealTimeWindow.cs new file mode 100644 index 000000000000..c388fafa16d8 --- /dev/null +++ b/src/Files.App/Data/Contracts/IRealTimeWindow.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Contracts +{ + /// + /// Defines an interface for real-time window components that manage content layout updates. + /// + internal interface IRealTimeWindow + { + /// + /// Gets the service for managing real-time layout updates. + /// + IRealTimeLayoutService RealTimeLayoutService { get; } + + /// + /// Initializes the content layout for the window. + /// + void InitializeContentLayout(); + + /// + /// Updates the content layout of the window. + /// + void UpdateContentLayout(); + } +} diff --git a/src/Files.App/Data/Enums/ArrowSymbolType.cs b/src/Files.App/Data/Enums/ArrowSymbolType.cs new file mode 100644 index 000000000000..7465fd0b144b --- /dev/null +++ b/src/Files.App/Data/Enums/ArrowSymbolType.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Enums +{ + public enum ArrowSymbolType + { + Forward, + Back, + ChevronLeft, + ChevronRight + } +} diff --git a/src/Files.App/Dialogs/AddBranchDialog.xaml.cs b/src/Files.App/Dialogs/AddBranchDialog.xaml.cs index 0087cbab901b..df052315edfd 100644 --- a/src/Files.App/Dialogs/AddBranchDialog.xaml.cs +++ b/src/Files.App/Dialogs/AddBranchDialog.xaml.cs @@ -7,7 +7,7 @@ namespace Files.App.Dialogs { - public sealed partial class AddBranchDialog : ContentDialog, IDialog + public sealed partial class AddBranchDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -21,6 +21,7 @@ public AddBranchDialogViewModel ViewModel public AddBranchDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() => (DialogResult)await base.ShowAsync(); diff --git a/src/Files.App/Dialogs/AddItemDialog.xaml.cs b/src/Files.App/Dialogs/AddItemDialog.xaml.cs index 938f17d42495..ad40fa3f1980 100644 --- a/src/Files.App/Dialogs/AddItemDialog.xaml.cs +++ b/src/Files.App/Dialogs/AddItemDialog.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Dialogs { - public sealed partial class AddItemDialog : ContentDialog, IDialog + public sealed partial class AddItemDialog : ContentDialog, IDialog, IRealTimeControl { private readonly IAddItemService addItemService = Ioc.Default.GetRequiredService(); @@ -24,6 +24,7 @@ public AddItemDialogViewModel ViewModel public AddItemDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() diff --git a/src/Files.App/Dialogs/BulkRenameDialog.xaml.cs b/src/Files.App/Dialogs/BulkRenameDialog.xaml.cs index a7f5b5478b65..f91483dd91e3 100644 --- a/src/Files.App/Dialogs/BulkRenameDialog.xaml.cs +++ b/src/Files.App/Dialogs/BulkRenameDialog.xaml.cs @@ -5,7 +5,7 @@ namespace Files.App.Dialogs { - public sealed partial class BulkRenameDialog : ContentDialog, IDialog + public sealed partial class BulkRenameDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -19,6 +19,7 @@ public BulkRenameDialogViewModel ViewModel public BulkRenameDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() diff --git a/src/Files.App/Dialogs/CreateArchiveDialog.xaml.cs b/src/Files.App/Dialogs/CreateArchiveDialog.xaml.cs index f54d0227afbc..2a81627028a6 100644 --- a/src/Files.App/Dialogs/CreateArchiveDialog.xaml.cs +++ b/src/Files.App/Dialogs/CreateArchiveDialog.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Dialogs { - public sealed partial class CreateArchiveDialog : ContentDialog + public sealed partial class CreateArchiveDialog : ContentDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -63,6 +63,7 @@ public int CPUThreads public CreateArchiveDialog() { InitializeComponent(); + InitializeContentLayout(); ViewModel.PropertyChanged += ViewModel_PropertyChanged; } diff --git a/src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs b/src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs index 09cd2f963e9f..aefb33cf5775 100644 --- a/src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs +++ b/src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs @@ -9,7 +9,7 @@ namespace Files.App.Dialogs { - public sealed partial class CreateShortcutDialog : ContentDialog, IDialog + public sealed partial class CreateShortcutDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -23,6 +23,7 @@ public CreateShortcutDialogViewModel ViewModel public CreateShortcutDialog() { InitializeComponent(); + InitializeContentLayout(); this.Closing += CreateShortcutDialog_Closing; InvalidPathWarning.SetBinding(TeachingTip.TargetProperty, new Binding() diff --git a/src/Files.App/Dialogs/CredentialDialog.xaml.cs b/src/Files.App/Dialogs/CredentialDialog.xaml.cs index 92ba79ba03c5..df9bd17bb03e 100644 --- a/src/Files.App/Dialogs/CredentialDialog.xaml.cs +++ b/src/Files.App/Dialogs/CredentialDialog.xaml.cs @@ -7,7 +7,7 @@ namespace Files.App.Dialogs { - public sealed partial class CredentialDialog : ContentDialog, IDialog + public sealed partial class CredentialDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -21,6 +21,7 @@ public CredentialDialogViewModel ViewModel public CredentialDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() diff --git a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs index 7a4543fd8476..39f8a1f43623 100644 --- a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs +++ b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Dialogs { - public sealed partial class DecompressArchiveDialog : ContentDialog + public sealed partial class DecompressArchiveDialog : ContentDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -22,6 +22,7 @@ public DecompressArchiveDialogViewModel ViewModel public DecompressArchiveDialog() { InitializeComponent(); + InitializeContentLayout(); } private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) diff --git a/src/Files.App/Dialogs/DynamicDialog.xaml.cs b/src/Files.App/Dialogs/DynamicDialog.xaml.cs index fe0f628440a7..82c108a6e8f0 100644 --- a/src/Files.App/Dialogs/DynamicDialog.xaml.cs +++ b/src/Files.App/Dialogs/DynamicDialog.xaml.cs @@ -7,7 +7,7 @@ namespace Files.App.Dialogs { - public sealed partial class DynamicDialog : ContentDialog, IDisposable + public sealed partial class DynamicDialog : ContentDialog, IDisposable, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -31,6 +31,7 @@ public DynamicDialogResult DynamicResult public DynamicDialog(DynamicDialogViewModel dynamicDialogViewModel) { InitializeComponent(); + InitializeContentLayout(); dynamicDialogViewModel.HideDialog = Hide; ViewModel = dynamicDialogViewModel; diff --git a/src/Files.App/Dialogs/ElevateConfirmDialog.xaml.cs b/src/Files.App/Dialogs/ElevateConfirmDialog.xaml.cs index 924602ce2b3d..75f7700d5e64 100644 --- a/src/Files.App/Dialogs/ElevateConfirmDialog.xaml.cs +++ b/src/Files.App/Dialogs/ElevateConfirmDialog.xaml.cs @@ -9,7 +9,7 @@ namespace Files.App.Dialogs { - public sealed partial class ElevateConfirmDialog : ContentDialog, IDialog + public sealed partial class ElevateConfirmDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -23,6 +23,7 @@ public ElevateConfirmDialogViewModel ViewModel public ElevateConfirmDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() diff --git a/src/Files.App/Dialogs/FileTooLargeDialog.xaml.cs b/src/Files.App/Dialogs/FileTooLargeDialog.xaml.cs index 4daf884822b9..016edc1a90fc 100644 --- a/src/Files.App/Dialogs/FileTooLargeDialog.xaml.cs +++ b/src/Files.App/Dialogs/FileTooLargeDialog.xaml.cs @@ -6,7 +6,7 @@ namespace Files.App.Dialogs { - public sealed partial class FileTooLargeDialog : ContentDialog, IDialog + public sealed partial class FileTooLargeDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -20,6 +20,7 @@ public FileTooLargeDialogViewModel ViewModel public FileTooLargeDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() diff --git a/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs b/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs index 9760cfa332dd..1c01f7ff6044 100644 --- a/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs +++ b/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Dialogs { - public sealed partial class FilesystemOperationDialog : ContentDialog, IDialog + public sealed partial class FilesystemOperationDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -30,6 +30,7 @@ public FileSystemDialogViewModel ViewModel public FilesystemOperationDialog() { InitializeComponent(); + InitializeContentLayout(); MainWindow.Instance.SizeChanged += Current_SizeChanged; } diff --git a/src/Files.App/Dialogs/GitHubLoginDialog.xaml.cs b/src/Files.App/Dialogs/GitHubLoginDialog.xaml.cs index 973909dec988..315771f2b7d8 100644 --- a/src/Files.App/Dialogs/GitHubLoginDialog.xaml.cs +++ b/src/Files.App/Dialogs/GitHubLoginDialog.xaml.cs @@ -7,7 +7,7 @@ namespace Files.App.Dialogs { - public sealed partial class GitHubLoginDialog : ContentDialog, IDialog + public sealed partial class GitHubLoginDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -29,6 +29,7 @@ public GitHubLoginDialogViewModel ViewModel public GitHubLoginDialog() { InitializeComponent(); + InitializeContentLayout(); } public new async Task ShowAsync() diff --git a/src/Files.App/Dialogs/ReleaseNotesDialog.xaml.cs b/src/Files.App/Dialogs/ReleaseNotesDialog.xaml.cs index d5aae7b549ea..cffbd71f8966 100644 --- a/src/Files.App/Dialogs/ReleaseNotesDialog.xaml.cs +++ b/src/Files.App/Dialogs/ReleaseNotesDialog.xaml.cs @@ -7,7 +7,7 @@ namespace Files.App.Dialogs { - public sealed partial class ReleaseNotesDialog : ContentDialog, IDialog + public sealed partial class ReleaseNotesDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -21,6 +21,7 @@ public ReleaseNotesDialogViewModel ViewModel public ReleaseNotesDialog() { InitializeComponent(); + InitializeContentLayout(); MainWindow.Instance.SizeChanged += Current_SizeChanged; UpdateDialogLayout(); diff --git a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs index 598d1da9edf6..ecc5d76be03d 100644 --- a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs +++ b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs @@ -15,7 +15,7 @@ namespace Files.App.Dialogs { - public sealed partial class ReorderSidebarItemsDialog : ContentDialog, IDialog + public sealed partial class ReorderSidebarItemsDialog : ContentDialog, IDialog, IRealTimeControl { private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; @@ -29,6 +29,7 @@ public ReorderSidebarItemsDialogViewModel ViewModel public ReorderSidebarItemsDialog() { InitializeComponent(); + InitializeContentLayout(); } private async void MoveItemAsync(object sender, PointerRoutedEventArgs e) diff --git a/src/Files.App/Dialogs/SettingsDialog.xaml.cs b/src/Files.App/Dialogs/SettingsDialog.xaml.cs index c9e10b18e812..79330a3173cb 100644 --- a/src/Files.App/Dialogs/SettingsDialog.xaml.cs +++ b/src/Files.App/Dialogs/SettingsDialog.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Dialogs { - public sealed partial class SettingsDialog : ContentDialog, IDialog + public sealed partial class SettingsDialog : ContentDialog, IDialog, IRealTimeControl { public SettingsDialogViewModel ViewModel { get; set; } @@ -18,6 +18,7 @@ private FrameworkElement RootAppElement public SettingsDialog() { InitializeComponent(); + InitializeContentLayout(); MainWindow.Instance.SizeChanged += Current_SizeChanged; UpdateDialogLayout(); diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 6d5b75f5eba4..72469a6369e9 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/Helpers/Navigation/NavigationInteractionTracker.cs b/src/Files.App/Helpers/Navigation/NavigationInteractionTracker.cs index ab2eaa4a65c6..bc2fb4a1d141 100644 --- a/src/Files.App/Helpers/Navigation/NavigationInteractionTracker.cs +++ b/src/Files.App/Helpers/Navigation/NavigationInteractionTracker.cs @@ -14,6 +14,12 @@ namespace Files.App.Helpers { internal sealed class NavigationInteractionTracker : IDisposable { + private static IRealTimeLayoutService RealTimeLayoutService => Ioc.Default.GetRequiredService(); + + private static int RePos => RealTimeLayoutService.FlowDirection is FlowDirection.LeftToRight ? 1 : -1; + private static (float Min, float Max) RePosB => RealTimeLayoutService.FlowDirection is FlowDirection.LeftToRight ? (-96f, 0f) : (0f, 96f); + private static (float Min, float Max) RePosF => RealTimeLayoutService.FlowDirection is FlowDirection.LeftToRight ? (0f, 96f) : (-96f, 0f); + public bool CanNavigateForward { get @@ -26,7 +32,11 @@ public bool CanNavigateForward if (!_disposed) { _props.InsertBoolean(nameof(CanNavigateForward), value); - _tracker.MaxPosition = new(value ? 96f : 0f); + if (RePos == 1) + _tracker.MaxPosition = new(value ? 96 : 0); + else + _tracker.MinPosition = new(value ? -96 : 0); + } } } @@ -43,7 +53,10 @@ public bool CanNavigateBackward if (!_disposed) { _props.InsertBoolean(nameof(CanNavigateBackward), value); - _tracker.MinPosition = new(value ? -96f : 0f); + if (RePos == 1) + _tracker.MinPosition = new(value ? -96 : 0); + else + _tracker.MaxPosition = new(value ? 96 : 0); } } } @@ -89,6 +102,19 @@ public NavigationInteractionTracker(UIElement rootElement, UIElement backIcon, U _pointerPressedHandler = new(PointerPressed); _rootElement.AddHandler(UIElement.PointerPressedEvent, _pointerPressedHandler, true); + + RealTimeLayoutService.FlowDirectionChanged += RealTimeLayoutService_FlowDirectionChanged; + } + + private void RealTimeLayoutService_FlowDirectionChanged(object? sender, FlowDirection e) + { + var canBack = CanNavigateBackward; + var canForward = CanNavigateForward; + + CanNavigateBackward = canBack; + CanNavigateForward = canForward; + + SetupAnimations(); } [MemberNotNull(nameof(_tracker), nameof(_source), nameof(_trackerOwner))] @@ -113,17 +139,17 @@ private void SetupAnimations() { var compositor = _rootVisual.Compositor; - var backResistance = CreateResistanceCondition(-96f, 0f); - var forwardResistance = CreateResistanceCondition(0f, 96f); + var backResistance = CreateResistanceCondition(RePosB.Min, RePosB.Max); + var forwardResistance = CreateResistanceCondition(RePosF.Min, RePosF.Max); List conditionalValues = [backResistance, forwardResistance]; _source.ConfigureDeltaPositionXModifiers(conditionalValues); - var backAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, -96, 0) * 2) - 48"); + var backAnim = compositor.CreateExpressionAnimation($"(clamp(tracker.Position.X, {RePosB.Min}, {RePosB.Max}) * 2 * ({-RePos})) - 48"); backAnim.SetReferenceParameter("tracker", _tracker); backAnim.SetReferenceParameter("props", _props); _backVisual.StartAnimation("Translation.X", backAnim); - var forwardAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, 0, 96) * 2) + 48"); + var forwardAnim = compositor.CreateExpressionAnimation($"(clamp(tracker.Position.X, {RePosF.Min}, {RePosF.Max}) * 2 * ({-RePos})) + 48"); forwardAnim.SetReferenceParameter("tracker", _tracker); forwardAnim.SetReferenceParameter("props", _props); _forwardVisual.StartAnimation("Translation.X", forwardAnim); @@ -171,6 +197,7 @@ public void Dispose() _tracker.Dispose(); _source.Dispose(); _props.Dispose(); + RealTimeLayoutService.FlowDirectionChanged -= RealTimeLayoutService_FlowDirectionChanged; GC.SuppressFinalize(this); } @@ -210,11 +237,11 @@ public void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleSt EventHandler? navEvent = _parent.NavigationRequested; if (navEvent is not null) { - if (sender.Position.X > 0 && _parent.CanNavigateForward) + if (sender.Position.X * RePos > 0 && _parent.CanNavigateForward) { navEvent(_parent, OverscrollNavigationEventArgs.Forward); } - else if (sender.Position.X < 0 && _parent.CanNavigateBackward) + else if (sender.Position.X * RePos < 0 && _parent.CanNavigateBackward) { navEvent(_parent, OverscrollNavigationEventArgs.Back); } @@ -238,12 +265,12 @@ public void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesCha if (!_shouldAnimate) return; - if (args.Position.X <= -64) + if (args.Position.X * RePos <= -64) { _parent._backVisual.StartAnimation("Scale", _scaleAnimation); _shouldAnimate = false; } - else if (args.Position.X >= 64) + else if (args.Position.X * RePos >= 64) { _parent._forwardVisual.StartAnimation("Scale", _scaleAnimation); _shouldAnimate = false; 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..1ff84b7836ee --- /dev/null +++ b/src/Files.App/Services/Content/RealTimeLayoutService.cs @@ -0,0 +1,132 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +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 +{ + /// + /// Provides a service to manage real-time layout updates, including content layout and title bar updates, + /// while supporting flow direction based on the current culture. + /// + internal sealed class RealTimeLayoutService : IRealTimeLayoutService + { + /// + /// List of weak references to target objects and their associated callbacks. + /// + private readonly List<(WeakReference Reference, Action Callback)> _callbacks = []; + + /// + public event EventHandler? FlowDirectionChanged; + + /// + public FlowDirection FlowDirection => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; + + /// /// + public void UpdateCulture(CultureInfo culture) + { + FlowDirection tmp = FlowDirection; + + CultureInfo.DefaultThreadCurrentUICulture = culture; + CultureInfo.CurrentUICulture = culture; +// TODO: Remove this after work RealTime string resources change work +#if DEBUG + if (tmp != FlowDirection) + InvokeCallbacks(); +#endif + } + + /// + public void AddCallback(Window target, Action callback) + { +// TODO: Remove this after work RealTime string resources change work +#if DEBUG + var weakReference = new WeakReference(target); + _callbacks.Add((weakReference, callback)); + + if (!IsExistTarget(target)) + target.Closed += (sender, args) => RemoveCallback(target); +#endif + } + + /// + public void AddCallback(FrameworkElement target, Action callback) + { +// TODO: Remove this after work RealTime string resources change work +#if DEBUG + var weakReference = new WeakReference(target); + _callbacks.Add((weakReference, callback)); + + if (!IsExistTarget(target)) + target.Unloaded += (sender, args) => RemoveCallback(target); +#endif + } + + /// + 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 bool IsExistTarget(object target) + => _callbacks.FindIndex(item => item.Reference.TryGetTarget(out var targetObject) && targetObject == target) >= 0; + + /// + /// Removes the callback associated with the specified target. + /// + /// The target object whose callback needs to be removed. + private void RemoveCallback(object target) + { + _callbacks.RemoveAll(item => + item.Reference.TryGetTarget(out var targetObject) && targetObject == target); + } + + /// + /// Invokes all registered callbacks for targets that are still valid. + /// + 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 _)); + + FlowDirectionChanged?.Invoke(this, FlowDirection); + } + } +} diff --git a/src/Files.App/Styles/TabBarStyles.xaml b/src/Files.App/Styles/TabBarStyles.xaml index b57dc3d3d9fe..02fe97e8dfa7 100644 --- a/src/Files.App/Styles/TabBarStyles.xaml +++ b/src/Files.App/Styles/TabBarStyles.xaml @@ -168,6 +168,7 @@ MaxWidth="{ThemeResource TabViewItemHeaderIconSize}" MaxHeight="{ThemeResource TabViewItemHeaderIconSize}" Margin="{ThemeResource TabViewItemHeaderIconMargin}" + FlowDirection="LeftToRight" Visibility="Collapsed" /> - + FontSize="12" /> diff --git a/src/Files.App/UserControls/Sidebar/SidebarView.xaml.cs b/src/Files.App/UserControls/Sidebar/SidebarView.xaml.cs index 0fc4c3bf2674..8330766f9b0b 100644 --- a/src/Files.App/UserControls/Sidebar/SidebarView.xaml.cs +++ b/src/Files.App/UserControls/Sidebar/SidebarView.xaml.cs @@ -15,6 +15,10 @@ namespace Files.App.UserControls.Sidebar [ContentProperty(Name = "InnerContent")] public sealed partial class SidebarView : UserControl, INotifyPropertyChanged { + private static readonly IRealTimeLayoutService RealTimeLayoutService = Ioc.Default.GetRequiredService(); + + private static readonly int RePos = RealTimeLayoutService.FlowDirection == FlowDirection.LeftToRight ? 1 : -1; + private const double COMPACT_MAX_WIDTH = 200; public event EventHandler? ItemInvoked; @@ -130,7 +134,7 @@ private void SidebarResizer_ManipulationStarted(object sender, ManipulationStart private void SidebarResizer_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) { - var newWidth = preManipulationSidebarWidth + e.Cumulative.Translation.X; + var newWidth = preManipulationSidebarWidth + (e.Cumulative.Translation.X * RePos); UpdateDisplayModeForPaneWidth(newWidth); e.Handled = true; } @@ -156,14 +160,16 @@ private void SidebarResizerControl_KeyDown(object sender, KeyRoutedEventArgs e) return; } + var rtls = Ioc.Default.GetRequiredService(); var ctrl = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control); var increment = ctrl.HasFlag(CoreVirtualKeyStates.Down) ? 5 : 1; + var rePos = rtls.FlowDirection == FlowDirection.LeftToRight ? 1 : -1; // Left makes the pane smaller so we invert the increment if (e.Key == VirtualKey.Left) increment = -increment; - var newWidth = OpenPaneLength + increment; + var newWidth = OpenPaneLength + (increment * rePos); UpdateDisplayModeForPaneWidth(newWidth); e.Handled = true; return; diff --git a/src/Files.App/UserControls/Symbols/ArrowGlyph.xaml b/src/Files.App/UserControls/Symbols/ArrowGlyph.xaml new file mode 100644 index 000000000000..416fb03faafe --- /dev/null +++ b/src/Files.App/UserControls/Symbols/ArrowGlyph.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/src/Files.App/UserControls/Symbols/ArrowGlyph.xaml.cs b/src/Files.App/UserControls/Symbols/ArrowGlyph.xaml.cs new file mode 100644 index 000000000000..1019e46105a5 --- /dev/null +++ b/src/Files.App/UserControls/Symbols/ArrowGlyph.xaml.cs @@ -0,0 +1,105 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.AnimatedVisuals; +using Microsoft.UI.Xaml.Media; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Files.App.UserControls.Symbols +{ + [DependencyProperty("FontSize", DefaultValue = "(double)12.0")] + [DependencyProperty("Foreground", nameof(OnColorChange), DefaultValue = "null")] + [DependencyProperty("Arrow", nameof(OnArrowChange), DefaultValue = "Files.App.Data.Enums.ArrowSymbolType.ChevronRight")] + [DependencyProperty("UseAnimatedIcon", nameof(OnUseAnimatedIconChanged), DefaultValue = "false")] + [DependencyProperty("Glyph", DefaultValue = "null")] + public partial class ArrowGlyph : UserControl, INotifyPropertyChanged, IRealTimeControl + { + private static ICommandManager Commands { get; } = Ioc.Default.GetRequiredService(); + + private string ForwardGlyph { get; } = Commands.NavigateForward.Glyph.BaseGlyph; + private string BackGlyph { get; } = Commands.NavigateBack.Glyph.BaseGlyph; + private string ChevronLeft { get; } = "\uE76B"; + private string ChevronRight { get; } = "\uE76C"; + + private long _token; + + public event PropertyChangedEventHandler? PropertyChanged; + + public bool IsStaticIconVisible => !UseAnimatedIcon; + + public bool IsAnimatedIconVisible => UseAnimatedIcon; + + public ArrowGlyph() + { + InitializeComponent(); + InitializeContentLayout(); + UpdateGlyph(); + RealTimeLayoutService.AddCallback(this, UpdateGlyph); + } + + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void UpdateGlyph() + { + if (UseAnimatedIcon) + { + SymbolGrid.Rotation = Arrow switch + { + ArrowSymbolType.Forward => FlowDirection == FlowDirection.LeftToRight ? 180 : 0, + ArrowSymbolType.Back => FlowDirection == FlowDirection.LeftToRight ? 0 : 180, + _ => 0 + }; + return; + } + + Glyph = Arrow switch + { + ArrowSymbolType.Forward => FlowDirection == FlowDirection.LeftToRight ? ForwardGlyph : BackGlyph, + ArrowSymbolType.Back => FlowDirection == FlowDirection.LeftToRight ? BackGlyph : ForwardGlyph, + ArrowSymbolType.ChevronLeft => FlowDirection == FlowDirection.LeftToRight ? ChevronLeft : ChevronRight, + ArrowSymbolType.ChevronRight => FlowDirection == FlowDirection.LeftToRight ? ChevronRight : ChevronLeft, + _ => ChevronLeft + }; + } + + private void OnColorChange(Brush oldValue, Brush newValue) + { + if (oldValue != newValue) + { + SymbolIcon.Foreground = newValue; + UpdateGlyph(); + } + } + + private void OnArrowChange(ArrowSymbolType oldValue, ArrowSymbolType newValue) + { + if (oldValue != newValue) + UpdateGlyph(); + } + + private void OnUseAnimatedIconChanged(bool oldValue, bool newValue) + { + if (oldValue != newValue) + { + UpdateGlyph(); + OnPropertyChanged(nameof(IsStaticIconVisible)); + OnPropertyChanged(nameof(IsAnimatedIconVisible)); + } + } + + private void SymbolGrid_Loaded(object sender, RoutedEventArgs e) + { + if (UseAnimatedIcon) + { + var centerX = SymbolGrid.ActualWidth / 2; + var centerY = SymbolGrid.ActualHeight / 2; + SymbolGrid.CenterPoint = new Vector3((float)centerX, (float)centerY, 0); + } + } + } + +} diff --git a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs index 9ce963a44fff..274eb8bed483 100644 --- a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs +++ b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs @@ -19,6 +19,7 @@ public sealed partial class TabBar : BaseTabBar, INotifyPropertyChanged private readonly ICommandManager Commands = Ioc.Default.GetRequiredService(); private readonly IAppearanceSettingsService AppearanceSettingsService = Ioc.Default.GetRequiredService(); private readonly IWindowContext WindowContext = Ioc.Default.GetRequiredService(); + private readonly IRealTimeLayoutService RealTimeLayoutService = Ioc.Default.GetRequiredService(); // Fields @@ -67,7 +68,7 @@ public TabBar() var appWindow = MainWindow.Instance.AppWindow; double rightPaddingColumnWidth = - FilePropertiesHelpers.FlowDirectionSettingIsRightToLeft + RealTimeLayoutService.FlowDirection is FlowDirection.RightToLeft ? appWindow.TitleBar.LeftInset : appWindow.TitleBar.RightInset; diff --git a/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs b/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs index e5329b0dfdbd..bf233472d2b7 100644 --- a/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs @@ -21,11 +21,9 @@ public static class FilePropertiesHelpers { private static IAppThemeModeService AppThemeModeService { get; } = Ioc.Default.GetRequiredService(); - /// - /// Whether LayoutDirection (FlowDirection) is set to right-to-left (RTL) - /// - public static readonly bool FlowDirectionSettingIsRightToLeft = - new ResourceManager().CreateResourceContext().QualifierValues["LayoutDirection"] == "RTL"; + private static readonly IRealTimeLayoutService RealTimeLayoutService = Ioc.Default.GetRequiredService(); + + private static readonly int RePos = RealTimeLayoutService.FlowDirection == FlowDirection.LeftToRight ? 1 : -1; /// /// Get window handle (hWnd) of the given properties window instance @@ -121,6 +119,7 @@ public static void OpenPropertiesWindow(object item, IShellPage associatedInstan appWindow.TitleBar.ExtendsContentIntoTitleBar = true; appWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent; appWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent; + RealTimeLayoutService.UpdateTitleBar(propertiesWindow); appWindow.SetIcon(AppLifecycleHelper.AppIconPath); @@ -140,7 +139,7 @@ public static void OpenPropertiesWindow(object item, IShellPage associatedInstan var appWindowPos = new PointInt32 { X = displayArea.WorkArea.X - + Math.Max(0, Math.Min(displayArea.WorkArea.Width - appWindow.Size.Width, pointerPosition.X - displayArea.WorkArea.X)), + + Math.Max(0, Math.Min(displayArea.WorkArea.Width - appWindow.Size.Width,( pointerPosition.X * RePos) - displayArea.WorkArea.X)), Y = displayArea.WorkArea.Y + Math.Max(0, Math.Min(displayArea.WorkArea.Height - appWindow.Size.Height, pointerPosition.Y - displayArea.WorkArea.Y)), }; diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index 0ede5c4fcaf6..4521c49d8d84 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -17,6 +17,8 @@ public sealed class GeneralViewModel : ObservableObject, IDisposable private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); + private static readonly IRealTimeLayoutService RealTimeLayoutService = Ioc.Default.GetRequiredService(); + private bool disposed; private ReadOnlyCollection addFlyoutItemsSource; @@ -76,6 +78,9 @@ public int SelectedAppLanguageIndex { selectedAppLanguageIndex = value; OnPropertyChanged(nameof(SelectedAppLanguageIndex)); + + RealTimeLayoutService.UpdateCulture(new(AppLanguageHelper.PreferredLanguage.Code)); + ShowRestartControl = true; } } diff --git a/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml b/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml index aca14800c17f..9072a5e7a779 100644 --- a/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml +++ b/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml @@ -11,6 +11,7 @@ xmlns:helpers="using:Files.App.Helpers" xmlns:local="using:Files.App.Views.Layouts" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:s="using:Files.App.UserControls.Symbols" xmlns:storage="using:Files.App.Utils.Storage" xmlns:uc="using:Files.App.UserControls" xmlns:wct="using:CommunityToolkit.WinUI.UI" @@ -337,14 +338,14 @@ Visibility="{x:Bind FileTagsUI, Converter={StaticResource EmptyObjectToObjectConverter}, Mode=OneWay}" /> - + Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> diff --git a/src/Files.App/Views/MainPage.xaml.cs b/src/Files.App/Views/MainPage.xaml.cs index 81d6f68c4d7b..1dd758fca199 100644 --- a/src/Files.App/Views/MainPage.xaml.cs +++ b/src/Files.App/Views/MainPage.xaml.cs @@ -26,6 +26,7 @@ public sealed partial class MainPage : Page private IGeneralSettingsService generalSettingsService { get; } = Ioc.Default.GetRequiredService(); public IUserSettingsService UserSettingsService { get; } private readonly IWindowContext WindowContext = Ioc.Default.GetRequiredService(); + private readonly IRealTimeLayoutService RealTimeLayoutService = Ioc.Default.GetRequiredService(); public ICommandManager Commands { get; } public SidebarViewModel SidebarAdaptiveViewModel { get; } public MainPageViewModel ViewModel { get; } @@ -47,9 +48,6 @@ public MainPage() ViewModel = Ioc.Default.GetRequiredService(); OngoingTasksViewModel = Ioc.Default.GetRequiredService(); - if (FilePropertiesHelpers.FlowDirectionSettingIsRightToLeft) - FlowDirection = FlowDirection.RightToLeft; - ViewModel.PropertyChanged += ViewModel_PropertyChanged; UserSettingsService.OnSettingChangedEvent += UserSettingsService_OnSettingChangedEvent; @@ -138,7 +136,10 @@ private void HorizontalMultitaskingControl_Loaded(object sender, RoutedEventArgs private int SetTitleBarDragRegion(InputNonClientPointerSource source, SizeInt32 size, double scaleFactor, Func getScaledRect) { var height = (int)TabControl.ActualHeight; - source.SetRegionRects(NonClientRegionKind.Passthrough, [getScaledRect(this, new RectInt32(0, 0, (int)(TabControl.ActualWidth + TabControl.Margin.Left - TabControl.DragArea.ActualWidth), height))]); + var x = RealTimeLayoutService.FlowDirection == FlowDirection.LeftToRight ? 0 : (int)TabControl.ActualWidth; + var width = (int)(TabControl.ActualWidth + TabControl.Margin.Left - TabControl.DragArea.ActualWidth); + + source.SetRegionRects(NonClientRegionKind.Passthrough, [getScaledRect(this, new RectInt32(x, 0, width, height))]); return height; } diff --git a/src/Files.App/Views/Properties/DetailsPage.xaml b/src/Files.App/Views/Properties/DetailsPage.xaml index 65b785b3ba4a..b4a7010f7d08 100644 --- a/src/Files.App/Views/Properties/DetailsPage.xaml +++ b/src/Files.App/Views/Properties/DetailsPage.xaml @@ -31,7 +31,10 @@ VerticalAlignment="Center" x:Load="{x:Bind ViewModel.IsPropertiesLoaded, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" Spacing="8"> - + diff --git a/src/Files.App/Views/Properties/MainPropertiesPage.xaml b/src/Files.App/Views/Properties/MainPropertiesPage.xaml index 7e26136c5faf..8483c28601a2 100644 --- a/src/Files.App/Views/Properties/MainPropertiesPage.xaml +++ b/src/Files.App/Views/Properties/MainPropertiesPage.xaml @@ -9,6 +9,7 @@ xmlns:dataitems="using:Files.App.Data.Items" xmlns:helpers="using:Files.App.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:s="using:Files.App.UserControls.Symbols" xmlns:uc="using:Files.App.UserControls" xmlns:vm="using:Files.App.ViewModels.Properties" xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters" @@ -69,14 +70,7 @@ - - - - - - - - + diff --git a/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs b/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs index 1b00d63022cc..53c8fbd1738b 100644 --- a/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs +++ b/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs @@ -15,7 +15,7 @@ namespace Files.App.Views.Properties { - public sealed partial class MainPropertiesPage : BasePropertiesPage + public sealed partial class MainPropertiesPage : BasePropertiesPage, IRealTimeControl { private IAppThemeModeService AppThemeModeService { get; } = Ioc.Default.GetRequiredService(); @@ -28,9 +28,7 @@ public sealed partial class MainPropertiesPage : BasePropertiesPage public MainPropertiesPage() { InitializeComponent(); - - if (FilePropertiesHelpers.FlowDirectionSettingIsRightToLeft) - FlowDirection = FlowDirection.RightToLeft; + InitializeContentLayout(); } @@ -69,8 +67,12 @@ private void Page_Loaded(object sender, RoutedEventArgs e) private int SetTitleBarDragRegion(InputNonClientPointerSource source, SizeInt32 size, double scaleFactor, Func getScaledRect) { - source.SetRegionRects(NonClientRegionKind.Passthrough, [getScaledRect(BackwardNavigationButton, null)]); - return (int)TitlebarArea.ActualHeight; + var height = (int)TitlebarArea.ActualHeight; + var x = RealTimeLayoutService.FlowDirection == FlowDirection.LeftToRight ? 0 : (int)((TitleDragArea.ActualWidth + 50) * scaleFactor); + var width = (int)((BackwardNavigationButton.ActualWidth + 2) * scaleFactor); + + source.SetRegionRects(NonClientRegionKind.Passthrough, [getScaledRect(BackwardNavigationButton, new RectInt32(x, 0, width, height))]); + return height; } private void Page_SizeChanged(object sender, SizeChangedEventArgs e) diff --git a/src/Files.App/Views/Properties/SecurityPage.xaml b/src/Files.App/Views/Properties/SecurityPage.xaml index 0ef10f6d6691..47c9c1639917 100644 --- a/src/Files.App/Views/Properties/SecurityPage.xaml +++ b/src/Files.App/Views/Properties/SecurityPage.xaml @@ -7,6 +7,7 @@ xmlns:dataitems="using:Files.App.Data.Items" xmlns:helpers="using:Files.App.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:s="using:Files.App.UserControls.Symbols" xmlns:vm="using:Files.App.ViewModels.Properties" xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters" DataContext="{x:Bind SecurityViewModel, Mode=OneWay}" @@ -219,7 +220,7 @@ ToolTipService.ToolTip="{helpers:ResourceString Name=AdvancedPermissions}"> - + diff --git a/src/Files.App/Views/Shells/BaseShellPage.cs b/src/Files.App/Views/Shells/BaseShellPage.cs index 3c9e57ffd48d..a3b86beb7869 100644 --- a/src/Files.App/Views/Shells/BaseShellPage.cs +++ b/src/Files.App/Views/Shells/BaseShellPage.cs @@ -176,9 +176,6 @@ public BaseShellPage(CurrentInstanceViewModel instanceViewModel) DisplayFilesystemConsentDialogAsync(); - if (FilePropertiesHelpers.FlowDirectionSettingIsRightToLeft) - FlowDirection = FlowDirection.RightToLeft; - ToolbarViewModel.ToolbarPathItemInvoked += ShellPage_NavigationRequested; ToolbarViewModel.ToolbarFlyoutOpened += ShellPage_ToolbarFlyoutOpened; ToolbarViewModel.ToolbarPathItemLoaded += ShellPage_ToolbarPathItemLoaded; diff --git a/src/Files.App/Views/Shells/ModernShellPage.xaml b/src/Files.App/Views/Shells/ModernShellPage.xaml index 7c802a932b8d..56c18b3935bb 100644 --- a/src/Files.App/Views/Shells/ModernShellPage.xaml +++ b/src/Files.App/Views/Shells/ModernShellPage.xaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:Files.App.Views.Shells" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:s="using:Files.App.UserControls.Symbols" xmlns:wct="using:CommunityToolkit.WinUI.UI" xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters" x:Name="RootPage" @@ -71,11 +72,12 @@ BorderThickness="1" Canvas.ZIndex="64" CornerRadius="24"> - + Arrow="Back" + FontSize="14" + Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}" /> @@ -92,11 +94,12 @@ BorderThickness="1" Canvas.ZIndex="64" CornerRadius="24"> - + Arrow="Forward" + FontSize="14" + Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}" /> diff --git a/src/Files.App/Views/SplashScreenPage.xaml b/src/Files.App/Views/SplashScreenPage.xaml index 76afaf002a1f..29b867752f92 100644 --- a/src/Files.App/Views/SplashScreenPage.xaml +++ b/src/Files.App/Views/SplashScreenPage.xaml @@ -42,6 +42,7 @@ x:Name="SplashScreenLoadingProgressRing" Margin="0,0,0,48" VerticalAlignment="Bottom" + FlowDirection="LeftToRight" Foreground="{ThemeResource ApplicationForegroundThemeBrush}" IsIndeterminate="True" /> diff --git a/src/Files.Core.SourceGenerator/Constants.cs b/src/Files.Core.SourceGenerator/Constants.cs index 5da27ceba8e1..f09358831d6b 100644 --- a/src/Files.Core.SourceGenerator/Constants.cs +++ b/src/Files.Core.SourceGenerator/Constants.cs @@ -98,5 +98,52 @@ internal class StringsPropertyGenerator /// internal const char ConstantSeparator = '/'; } + + /// + /// Provides functionality for generating real-time layout specifications. + /// + internal class RealTimeLayoutGenerator + { + /// + /// The name of the real-time service interface. + /// + internal const string ServiceInterfaceName = "IRealTimeLayoutService"; + + /// + /// The name of the real-time service generated variable. + /// + internal const string ServiceVariableName = "RealTimeLayoutService"; + + /// + /// The name of the real-time specification window. + /// + internal const string SpecificationWindowName = "IRealTimeWindow"; + + /// + /// The name of the real-time specification control. + /// + internal const string SpecificationControlName = "IRealTimeControl"; + + /// + /// Specifies the types of real-time layout specifications. + /// + internal enum SpecificationType + { + /// + /// No specification type. + /// + None = 0, + + /// + /// Specifies a window layout. + /// + Window, + + /// + /// Specifies a control layout. + /// + 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..73f06e88fd94 --- /dev/null +++ b/src/Files.Core.SourceGenerator/Generators/RealTimeLayoutGenerator.cs @@ -0,0 +1,243 @@ +// 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 +{ + /// + /// Generates additional source code for classes based on their inheritance from specific types + /// (e.g., IRealTimeWindow or IRealTimeControl). + /// + [Generator] + public class RealTimeLayoutGenerator : IIncrementalGenerator + { + /// + /// Initializes the incremental source generator with context-specific configuration. + /// + /// The incremental generator initialization context. + 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}.{Guid.NewGuid()}.g.cs", SourceText.From(source, Encoding.UTF8)); + }); + } + + /// + /// Determines if the syntax node is a valid candidate for generation. + /// + /// The syntax node to evaluate. + /// True if the node is a valid class candidate; otherwise, false. + 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; + } + + /// + /// Retrieves a class declaration and its specification type if it matches the criteria. + /// + /// The syntax context for the generator. + /// A tuple containing the class declaration and its specification type, or null if no match. + 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; + } + } + + return type != SpecificationType.None ? (classDeclaration, type) : null; + } + + /// + /// Generates the source code for a class based on the provided namespace, class name, and type. + /// + /// The namespace of the class. + /// The name of the class. + /// The type of the specification (Window or Control). + /// The generated source code as a string. + private static string GenerateClass(string namespaceName, string className, SpecificationType type) + { + // Namespace + var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(namespaceName)) + .WithLeadingTrivia(SourceGeneratorHelper.GetLicenceHeader()) + .NormalizeWhitespace(); + + // Usings + var usings = Array.Empty(); + + // Property: IRealTimeLayoutService RealTimeLayoutService; + var propertyDeclaration = SyntaxFactory.PropertyDeclaration( + SyntaxFactory.IdentifierName(ServiceInterfaceName), + SyntaxFactory.Identifier(ServiceVariableName)) + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), + SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))) + .AddAttributeLists(SourceGeneratorHelper.GetAttributeForField(nameof(RealTimeLayoutGenerator))); + + // Method: InitializeContentLayout + var initializeContentLayoutMethod = SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + "InitializeContentLayout") + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) + .WithBody(SyntaxFactory.Block(CreateContentLayoutBody(type, isInitialize: true))) + .AddAttributeLists(SourceGeneratorHelper.GetAttributeForMethod(nameof(RealTimeLayoutGenerator))); + + // Method: UpdateContentLayout + var updateContentLayoutMethod = SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + "UpdateContentLayout") + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) + .WithBody(SyntaxFactory.Block(CreateContentLayoutBody(type))) + .AddAttributeLists(SourceGeneratorHelper.GetAttributeForMethod(nameof(RealTimeLayoutGenerator))); + + // Class declaration + var classDeclaration = SyntaxFactory.ClassDeclaration(className) + .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)) + .AddBaseListTypes(SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName( + type is SpecificationType.Window ? SpecificationWindowName : SpecificationControlName))) + .AddMembers(propertyDeclaration, initializeContentLayoutMethod, updateContentLayoutMethod) + .AddAttributeLists(SourceGeneratorHelper.GetAttributeForField(nameof(RealTimeLayoutGenerator))); + + // Add class to namespace + namespaceDeclaration = namespaceDeclaration.AddMembers(classDeclaration); + + var compilationUnit = SyntaxFactory.CompilationUnit() + .AddUsings(usings) + .AddMembers(namespaceDeclaration) + .NormalizeWhitespace(); + + return SyntaxFactory.SyntaxTree(compilationUnit, encoding: Encoding.UTF8).GetText().ToString(); + } + + /// + /// Creates a collection of statements for updating the content layout body. + /// Depending on the specification type, it will update the title bar and content layout. + /// If the flag is set to true, a callback is added for updating the content layout. + /// + /// The specification type, used to determine if the title bar should be updated. + /// A flag indicating whether to add a callback for content layout initialization. + /// An IEnumerable of representing the generated statements. + private static IEnumerable CreateContentLayoutBody(SpecificationType type, bool isInitialize = false) + { + var statements = new List(); + + if (isInitialize) + { + + statements.Add( + SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.IdentifierName(ServiceVariableName), + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Ioc"), + SyntaxFactory.IdentifierName("Default")), + SyntaxFactory.GenericName( + SyntaxFactory.Identifier("GetRequiredService")) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.IdentifierName(ServiceInterfaceName))))))))); + } + + if (type == SpecificationType.Window) + { + statements.Add( + SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(ServiceVariableName), + SyntaxFactory.IdentifierName("UpdateTitleBar"))) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(SyntaxFactory.ThisExpression())))))); + } + + statements.Add( + SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(ServiceVariableName), + SyntaxFactory.IdentifierName("UpdateContent"))) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(SyntaxFactory.ThisExpression())))))); + + if (isInitialize) + { + statements.Add( + SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(ServiceVariableName), + SyntaxFactory.IdentifierName("AddCallback"))) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList( + new[]{ + SyntaxFactory.Argument(SyntaxFactory.ThisExpression()), + SyntaxFactory.Argument(SyntaxFactory.IdentifierName("UpdateContentLayout")) + }))))); + } + + return statements; + } + + /// + /// Retrieves the namespace of a given syntax node. + /// + /// The syntax node to evaluate. + /// The namespace name as a string. + 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 diff --git a/src/Files.Core.SourceGenerator/Utilities/SourceGeneratorHelper.cs b/src/Files.Core.SourceGenerator/Utilities/SourceGeneratorHelper.cs index 8b529119f692..c0fde03ab689 100644 --- a/src/Files.Core.SourceGenerator/Utilities/SourceGeneratorHelper.cs +++ b/src/Files.Core.SourceGenerator/Utilities/SourceGeneratorHelper.cs @@ -377,5 +377,17 @@ internal static StringBuilder GenerateFileHeader(this HashSet namespaces _ = stringBuilder.AppendLine($"using {s};"); return stringBuilder; } + + /// + /// Generates a license header comment for the code. + /// + /// A containing the license header comment and a line break. + internal static SyntaxTriviaList GetLicenceHeader() + { + return SyntaxFactory.TriviaList( + SyntaxFactory.Comment( + new StringBuilder().AppendLicenceHeader().ToString()), + SyntaxFactory.CarriageReturnLineFeed); + } } }