diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 0e4c9657dd82..a9113d1741e2 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -83,6 +83,7 @@ async Task ActivateAsync() { // Build the DI container off-thread while the window initializes var appModel = AppModel; + Task commandsWarmupTask = Task.CompletedTask; var servicesTask = Task.Run(() => { try @@ -92,31 +93,36 @@ async Task ActivateAsync() // Configure Ioc here so Ioc.Default-dependent constructions warm off-thread too Ioc.Default.ConfigureServices(provider); - // Warm the settings file reads off the UI thread + // Load the appearance settings needed for the first frame off the UI thread. _ = provider.GetRequiredService().LeaveAppRunning; - _ = provider.GetRequiredService().AppThemeBackdropMaterial; - - // Read through these statics by the action/context ctors warmed below - QuickAccessManager = provider.GetRequiredService(); - HistoryWrapper = provider.GetRequiredService(); - FileTagsManager = provider.GetRequiredService(); - LibraryManager = provider.GetRequiredService(); - - // Warm every command and hotkey off-thread, below normal so window creation wins the cores - var previousPriority = Thread.CurrentThread.Priority; - Thread.CurrentThread.Priority = ThreadPriority.BelowNormal; - try - { - _ = provider.GetRequiredService(); - } - catch (Exception) - { - // A command ctor that needs the UI thread aborts the warm-up; it runs on first use instead - } - finally + var appearanceSettings = provider.GetRequiredService(); + _ = appearanceSettings.AppThemeMode; + _ = appearanceSettings.AppThemeBackdropMaterial; + + // Commands are needed for the main page, not for the initial window and backdrop. + commandsWarmupTask = Task.Run(() => { - Thread.CurrentThread.Priority = previousPriority; - } + var previousPriority = Thread.CurrentThread.Priority; + Thread.CurrentThread.Priority = ThreadPriority.BelowNormal; + try + { + // Action and context constructors access these statics. + QuickAccessManager = provider.GetRequiredService(); + HistoryWrapper = provider.GetRequiredService(); + FileTagsManager = provider.GetRequiredService(); + LibraryManager = provider.GetRequiredService(); + + _ = provider.GetRequiredService(); + } + catch (Exception) + { + // UI-thread-only constructors run on first use instead. + } + finally + { + Thread.CurrentThread.Priority = previousPriority; + } + }); return provider; } @@ -136,17 +142,8 @@ async Task ActivateAsync() if (!isStartupTask) { - // Initialize and activate MainWindow - MainWindow.Instance.Activate(); - - if (showSplashScreen) - { - // Wait for the Window to initialize - await Task.Delay(10); - - SplashScreenLoadingTCS = new TaskCompletionSource(); - MainWindow.Instance.ShowSplashScreen(); - } + // Construct the window while appearance settings are loaded. + _ = MainWindow.Instance; } // Configure the DI (dependency injection) container @@ -157,28 +154,27 @@ async Task ActivateAsync() Ioc.Default.ConfigureServices(serviceProvider); } - // Configure Sentry - if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev) - AppLifecycleHelper.ConfigureSentry(); - var userSettingsService = Ioc.Default.GetRequiredService(); var isLeaveAppRunning = userSettingsService.GeneralSettingsService.LeaveAppRunning; - if (isStartupTask && !isLeaveAppRunning) + if (!isStartupTask || !isLeaveAppRunning) { - // Initialize and activate MainWindow - MainWindow.Instance.Activate(); - if (showSplashScreen) { - // Wait for the Window to initialize - await Task.Delay(10); - SplashScreenLoadingTCS = new TaskCompletionSource(); MainWindow.Instance.ShowSplashScreen(); } + + if (!await MainWindow.Instance.ActivateWithBackdropAsync()) + return; } + await commandsWarmupTask; + + // Configure Sentry + if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev) + AppLifecycleHelper.ConfigureSentry(); + // TODO: Replace with DI QuickAccessManager = Ioc.Default.GetRequiredService(); HistoryWrapper = Ioc.Default.GetRequiredService(); diff --git a/src/Files.App/Data/Items/WidgetFolderCardItem.cs b/src/Files.App/Data/Items/WidgetFolderCardItem.cs index c3ea40506d40..3b56f4eae2ce 100644 --- a/src/Files.App/Data/Items/WidgetFolderCardItem.cs +++ b/src/Files.App/Data/Items/WidgetFolderCardItem.cs @@ -3,6 +3,8 @@ using Microsoft.UI.Xaml.Media.Imaging; using Windows.Win32; +using Windows.Win32.System.Com; +using Windows.Win32.System.WinRT; using Windows.Win32.UI.Shell; namespace Files.App.Data.Items @@ -23,16 +25,17 @@ public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardIt private BitmapImage? _Thumbnail; public BitmapImage? Thumbnail { get => _Thumbnail; set => SetProperty(ref _Thumbnail, value); } + private bool _isDisposed; // Constructor - public WidgetFolderCardItem(IWindowsStorable item, string text, bool isPinned, string tooltip) + public WidgetFolderCardItem(IWindowsStorable item, string text, string path, bool isPinned, string tooltip) { AutomationProperties = text; Item = item; Text = text; IsPinned = isPinned; - Path = item.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING); + Path = path; Tooltip = tooltip; } @@ -40,21 +43,36 @@ public WidgetFolderCardItem(IWindowsStorable item, string text, bool isPinned, s public async Task LoadCardThumbnailAsync() { - if (string.IsNullOrEmpty(Path)) + if (_isDisposed || string.IsNullOrEmpty(Path)) return; var thumbnailSize = (int)(Constants.ShellIconSizes.Large * App.AppModel.AppWindowDPI); // Ensure thumbnail size is at least 1 to prevent layout errors thumbnailSize = Math.Max(1, thumbnailSize); - Item.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var rawThumbnailData); - if (rawThumbnailData is null) + var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, Item.ThisPtr, out IAgileReference shellItemReference); + if (hr.ThrowIfFailedOnDebug().Failed) return; - Thumbnail = await rawThumbnailData.ToBitmapAsync(); + var rawThumbnailData = await STATask.RunPooled(() => + { + if (shellItemReference.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed) + return null; + + using var folder = new WindowsFolder(shellItem); + folder.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var data); + return data; + }, App.Logger); + if (_isDisposed || rawThumbnailData is null) + return; + + var thumbnail = await rawThumbnailData.ToBitmapAsync(); + if (!_isDisposed) + Thumbnail = thumbnail; } public void Dispose() { + _isDisposed = true; Item.Dispose(); } } diff --git a/src/Files.App/Extensions/StringExtensions.cs b/src/Files.App/Extensions/StringExtensions.cs index c1409a2446f5..09525c655076 100644 --- a/src/Files.App/Extensions/StringExtensions.cs +++ b/src/Files.App/Extensions/StringExtensions.cs @@ -55,26 +55,29 @@ public static string WithEnding(this string str, string ending) return result; } - private static readonly ResourceMap resourcesTree = new ResourceManager().MainResourceMap.TryGetSubtree("Resources"); + private static readonly ResourceManager resourceManager = new(); + private static readonly ResourceMap resourcesTree = resourceManager.MainResourceMap.TryGetSubtree("Resources"); + private static readonly Lock resourceLookupLock = new(); + private static ResourceContext? resourceContext; private static readonly ConcurrentDictionary cachedResources = new(); private static readonly Dictionary abbreviations = new() { - { ByteSize.KiloByteSymbol, Strings.KiloByteSymbol.GetLocalizedResource() }, - { ByteSize.MegaByteSymbol, Strings.MegaByteSymbol.GetLocalizedResource() }, - { ByteSize.GigaByteSymbol, Strings.GigaByteSymbol.GetLocalizedResource() }, - { ByteSize.TeraByteSymbol, Strings.TeraByteSymbol.GetLocalizedResource() }, - { ByteSize.PetaByteSymbol, Strings.PetaByteSymbol.GetLocalizedResource() }, - { ByteSize.BitSymbol, Strings.ByteSymbol.GetLocalizedResource() }, - { ByteSize.ByteSymbol, Strings.ByteSymbol.GetLocalizedResource() } + { ByteSize.KiloByteSymbol, Strings.KiloByteSymbol }, + { ByteSize.MegaByteSymbol, Strings.MegaByteSymbol }, + { ByteSize.GigaByteSymbol, Strings.GigaByteSymbol }, + { ByteSize.TeraByteSymbol, Strings.TeraByteSymbol }, + { ByteSize.PetaByteSymbol, Strings.PetaByteSymbol }, + { ByteSize.BitSymbol, Strings.ByteSymbol }, + { ByteSize.ByteSymbol, Strings.ByteSymbol } }; public static string ConvertSizeAbbreviation(this string value) { foreach (var item in abbreviations) { - value = value.Replace(item.Key, item.Value, StringComparison.Ordinal); + value = value.Replace(item.Key, item.Value.GetLocalizedResource(), StringComparison.Ordinal); } return value; @@ -102,9 +105,24 @@ public static string GetLocalizedResource(this string resourceKey) return value; } - value = resourcesTree?.TryGetValue(resourceKey)?.ValueAsString; + lock (resourceLookupLock) + { + if (cachedResources.TryGetValue(resourceKey, out value)) + return value; + + resourceContext ??= resourceManager.CreateResourceContext(); + value = resourcesTree?.TryGetValue(resourceKey, resourceContext)?.ValueAsString; + return cachedResources[resourceKey] = value ?? string.Empty; + } + } - return cachedResources[resourceKey] = value ?? string.Empty; + internal static void ResetLocalizedResources() + { + lock (resourceLookupLock) + { + resourceContext = null; + cachedResources.Clear(); + } } } } diff --git a/src/Files.App/Helpers/Application/AppLanguageHelper.cs b/src/Files.App/Helpers/Application/AppLanguageHelper.cs index 673e69673c5d..fa7eb6ec5700 100644 --- a/src/Files.App/Helpers/Application/AppLanguageHelper.cs +++ b/src/Files.App/Helpers/Application/AppLanguageHelper.cs @@ -3,6 +3,7 @@ using System.Globalization; using Windows.Globalization; +using StringExtensions = Files.App.Extensions.StringExtensions; namespace Files.App.Helpers { @@ -87,6 +88,7 @@ public static bool TryChange(int index) // Update the primary language override ApplicationLanguages.PrimaryLanguageOverride = index == 0 ? _defaultCode : PreferredLanguage.Code; + StringExtensions.ResetLocalizedResources(); return true; } @@ -117,6 +119,7 @@ public static bool TryChange(string code) // Update the primary language override ApplicationLanguages.PrimaryLanguageOverride = index == 0 ? _defaultCode : PreferredLanguage.Code; + StringExtensions.ResetLocalizedResources(); return true; } } diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index c51d27d0267b..630158d15873 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -86,13 +86,15 @@ static AppLifecycleHelper() /// /// Gets application icon path. /// - public static string AppIconPath { get; } = - SystemIO.Path.Combine(Package.Current.InstalledLocation.Path, AppEnvironment switch + public static string AppIconPath => _appIconPath.Value; + + private static readonly Lazy _appIconPath = new(() => + SystemIO.Path.Combine(Package.Current.InstalledPath, AppEnvironment switch { AppEnvironment.Dev => Constants.AssetPaths.DevLogo, AppEnvironment.SideloadPreview or AppEnvironment.StorePreview => Constants.AssetPaths.PreviewLogo, _ => Constants.AssetPaths.StableLogo - }); + })); /// /// Initializes the app components. diff --git a/src/Files.App/Helpers/UI/AppSystemBackdrop.cs b/src/Files.App/Helpers/UI/AppSystemBackdrop.cs index 334ce02d2773..033c9c030386 100644 --- a/src/Files.App/Helpers/UI/AppSystemBackdrop.cs +++ b/src/Files.App/Helpers/UI/AppSystemBackdrop.cs @@ -16,6 +16,7 @@ internal sealed partial class AppSystemBackdrop : SystemBackdrop private XamlRoot? root; private SystemBackdropConfiguration? configuration; private SystemBackdropTheme? prevTheme = null; + private bool? isInputActive; public AppSystemBackdrop(bool isSecondaryWindow = false) { @@ -33,6 +34,8 @@ protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop con this.target = connectedTarget; this.root = xamlRoot; configuration = GetDefaultSystemBackdropConfiguration(connectedTarget, xamlRoot); + if (isInputActive is bool active) + configuration.IsInputActive = active; controller = GetSystemBackdropController(userSettingsService.AppearanceSettingsService.AppThemeBackdropMaterial, configuration.Theme); controller?.SetSystemBackdropConfiguration(configuration); controller?.AddSystemBackdropTarget(connectedTarget); @@ -82,6 +85,8 @@ private void OnSettingChanged(object? sender, SettingChangedEventArgs e) controller?.RemoveAllSystemBackdropTargets(); controller?.Dispose(); configuration = GetDefaultSystemBackdropConfiguration(target, root); + if (isInputActive is bool active) + configuration.IsInputActive = active; var newController = GetSystemBackdropController(userSettingsService.AppearanceSettingsService.AppThemeBackdropMaterial, configuration.Theme); newController?.SetSystemBackdropConfiguration(configuration); newController?.AddSystemBackdropTarget(target); @@ -93,6 +98,7 @@ private void OnSettingChanged(object? sender, SettingChangedEventArgs e) // Driven from the window's Activated event since the default configuration doesn't reliably flip IsInputActive on pointerless activation (e.g. taskbar). public void SetInputActive(bool isInputActive) { + this.isInputActive = isInputActive; if (configuration is not null) configuration.IsInputActive = isInputActive; } diff --git a/src/Files.App/Helpers/UI/UIHelpers.cs b/src/Files.App/Helpers/UI/UIHelpers.cs index 3d470d670e47..1b0a248dabe6 100644 --- a/src/Files.App/Helpers/UI/UIHelpers.cs +++ b/src/Files.App/Helpers/UI/UIHelpers.cs @@ -19,6 +19,38 @@ public static class UIHelpers { public static event PropertyChangedEventHandler? PropertyChanged; + public static void RunAfterNextRender(this FrameworkElement element, Action action) + { + if (!element.IsLoaded) + return; + + var canceled = false; + element.Unloaded += OnUnloaded; + CompositionTarget.Rendered += OnRendered; + + void OnUnloaded(object sender, RoutedEventArgs e) + { + canceled = true; + element.Unloaded -= OnUnloaded; + CompositionTarget.Rendered -= OnRendered; + } + + void OnRendered(object? sender, RenderedEventArgs e) + { + CompositionTarget.Rendered -= OnRendered; + // Keep the deferred work outside the rendering callback itself. + if (!element.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () => + { + element.Unloaded -= OnUnloaded; + if (!canceled && element.IsLoaded) + action(); + })) + { + element.Unloaded -= OnUnloaded; + } + } + } + /// /// True if a user-editable text input currently owns keyboard focus within the given XamlRoot. /// Used to gate code that programmatically reassigns focus on a background-completion diff --git a/src/Files.App/MainWindow.xaml.cs b/src/Files.App/MainWindow.xaml.cs index 3402f616f9bc..f369ddbe536c 100644 --- a/src/Files.App/MainWindow.xaml.cs +++ b/src/Files.App/MainWindow.xaml.cs @@ -4,7 +4,9 @@ using Microsoft.Extensions.Logging; using Microsoft.UI; using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Animation; using System.IO; using System.Runtime.InteropServices; @@ -24,6 +26,7 @@ public sealed partial class MainWindow : WindowEx private bool CanWindowToFront { get; set; } = true; private readonly Lock _canWindowToFrontLock = new(); + private bool _isWindowIconInitialized; protected override bool PersistPlacement => true; @@ -37,10 +40,49 @@ public MainWindow() : base(416, 316) AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent; AppWindow.TitleBar.ButtonPressedBackgroundColor = Colors.Transparent; AppWindow.TitleBar.ButtonHoverBackgroundColor = Colors.Transparent; + } + + private void InitializeWindowIcon() + { + if (_isWindowIconInitialized) + return; + + AppWindow.SetIcon(AppLifecycleHelper.AppIconPath); + _isWindowIconInitialized = true; + } + + internal async Task ActivateWithBackdropAsync() + { + var rootFrame = EnsureWindowIsInitialized(); + if (rootFrame is null) + return false; + + // Submit the lightweight backdrop before showing the window with its normal animation. + await CompositionTarget.GetCompositorForCurrentThread().RequestCommitAsync(); + + var firstFrame = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + CompositionTarget.Rendered += OnRendered; + Closed += OnClosed; + try + { + Activate(); + // Let the backdrop paint before main-page construction occupies the UI thread. + return await firstFrame.Task; + } + finally + { + CompositionTarget.Rendered -= OnRendered; + Closed -= OnClosed; + } + + void OnRendered(object? sender, RenderedEventArgs args) + { + if (rootFrame.IsLoaded) + firstFrame.TrySetResult(true); + } - // Deferred: reads the .ico from disk - DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () => - AppWindow.SetIcon(AppLifecycleHelper.AppIconPath)); + void OnClosed(object sender, WindowEventArgs args) + => firstFrame.TrySetResult(false); } public void ShowSplashScreen() @@ -58,16 +100,13 @@ public async Task InitializeApplicationAsync(object? activatedEventArgs) if (rootFrame is null) return; - // Reuse the existing backdrop on resume; rebuilding the Mica controller leaves the window on the fallback color for ~1s - SystemBackdrop ??= new AppSystemBackdrop(); - switch (activatedEventArgs) { case ILaunchActivatedEventArgs launchArgs: if (launchArgs.Arguments is not null && (CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].EndsWith($"files-dev.exe", StringComparison.OrdinalIgnoreCase) || CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].EndsWith($"files-dev", StringComparison.OrdinalIgnoreCase) - || CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].Equals(Path.Join(Package.Current.InstalledLocation.Path, "Files.exe"), StringComparison.OrdinalIgnoreCase))) + || CommandLineParser.SplitArguments(launchArgs.Arguments, true)[0].Equals(Path.Join(Package.Current.InstalledPath, "Files.exe"), StringComparison.OrdinalIgnoreCase))) { // WINUI3: When launching from commandline the argument is not ICommandLineActivatedEventArgs (#10370) var ppm = CommandLineParser.ParseUntrustedCommands(launchArgs.Arguments); @@ -250,6 +289,11 @@ private async Task EnsureContentHasKeyboardFocusAsync() { // Create a Frame to act as the navigation context and navigate to the first page rootFrame = new() { CacheSize = 1 }; + rootFrame.Loaded += (_, _) => + { + if (!_isWindowIconInitialized) + rootFrame.RunAfterNextRender(InitializeWindowIcon); + }; rootFrame.NavigationFailed += (s, e) => { throw new Exception("Failed to load Page " + e.SourcePageType.FullName); @@ -257,6 +301,15 @@ private async Task EnsureContentHasKeyboardFocusAsync() // Place the frame in the current Window Instance.Content = rootFrame; + Ioc.Default.GetRequiredService().SetAppThemeMode(this, callThemeModeChangedEvent: false); + } + + if (SystemBackdrop is null) + { + // Initialize the active material directly instead of transitioning from its inactive fallback. + var backdrop = new AppSystemBackdrop(); + backdrop.SetInputActive(true); + SystemBackdrop = backdrop; } return rootFrame; diff --git a/src/Files.App/Services/App/AppThemeModeService.cs b/src/Files.App/Services/App/AppThemeModeService.cs index d79825e47746..dbfa00ba2bd0 100644 --- a/src/Files.App/Services/App/AppThemeModeService.cs +++ b/src/Files.App/Services/App/AppThemeModeService.cs @@ -83,7 +83,7 @@ public void SetAppThemeMode(Window? window = null, AppWindowTitleBar? titleBar = try { window ??= MainWindow.Instance; - titleBar ??= MainWindow.Instance.AppWindow?.TitleBar; + titleBar ??= window.AppWindow?.TitleBar; rootTheme ??= AppThemeMode; if (window.Content is FrameworkElement rootElement) diff --git a/src/Files.App/Services/Settings/GeneralSettingsService.cs b/src/Files.App/Services/Settings/GeneralSettingsService.cs index e754b3c7705e..7801fd3143b8 100644 --- a/src/Files.App/Services/Settings/GeneralSettingsService.cs +++ b/src/Files.App/Services/Settings/GeneralSettingsService.cs @@ -401,7 +401,17 @@ public Dictionary? ShowHashesDictionary public string UserId { - get => Get(Guid.NewGuid().ToString()); + get + { + var userId = Get(null); + if (userId is null) + { + userId = Guid.NewGuid().ToString(); + Set(userId); + } + + return userId; + } set => Set(value); } diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml b/src/Files.App/UserControls/NavigationToolbar.xaml index c3cab3a58c54..667099c68de3 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml +++ b/src/Files.App/UserControls/NavigationToolbar.xaml @@ -21,6 +21,7 @@ xmlns:ucs="using:Files.App.UserControls.StatusCenter" xmlns:vm="using:Files.App.ViewModels.UserControls" x:Name="NavToolbar" + Loaded="NavToolbar_Loaded" Loading="NavToolbar_Loading" mc:Ignorable="d"> @@ -73,7 +74,11 @@ - + diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml.cs b/src/Files.App/UserControls/NavigationToolbar.xaml.cs index c47b9484809c..3e3769d5866b 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml.cs +++ b/src/Files.App/UserControls/NavigationToolbar.xaml.cs @@ -51,6 +51,9 @@ public NavigationToolbar() // Methods + private void NavToolbar_Loaded(object sender, RoutedEventArgs e) + => this.RunAfterNextRender(() => FindName(nameof(ListaryIntegration))); + private void NavToolbar_Loading(FrameworkElement _, object e) { Loading -= NavToolbar_Loading; diff --git a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs index 58e6e793ae22..dd67e005e3c8 100644 --- a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs +++ b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs @@ -426,11 +426,11 @@ private void TabViewItem_Loaded(object sender, RoutedEventArgs e) } } - private async void DragAreaRectangle_Loaded(object sender, RoutedEventArgs e) - { - if (HorizontalTabView.ActualWidth <= 0 && TabBarAddNewTabButton.Width <= 0) - await Task.Delay(100); + private void DragAreaRectangle_Loaded(object sender, RoutedEventArgs e) + => this.RunAfterNextRender(UpdateTitleBarInsets); + private void UpdateTitleBarInsets() + { var titleBarInset = ((AppLanguageHelper.IsPreferredLanguageRtl ? MainWindow.Instance.AppWindow.TitleBar.LeftInset : MainWindow.Instance.AppWindow.TitleBar.RightInset) / DragAreaRectangle.XamlRoot.RasterizationScale) + 40; diff --git a/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs b/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs index d7df6480c46c..e6f6b268fff7 100644 --- a/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs +++ b/src/Files.App/Utils/Serialization/Implementation/CachingJsonSettingsDatabase.cs @@ -28,8 +28,8 @@ public CachingJsonSettingsDatabase( else { var defaultElement = GetElementFromValue(defaultValue); - if (_settingsCache.TryAdd(key, defaultElement) && !SaveSettings(_settingsCache)) - _settingsCache.TryRemove(key, out _); + // Persist cached defaults with the next settings change, not during reads. + _settingsCache.TryAdd(key, defaultElement); return defaultValue; } diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index 8f365c3b4938..d0f75b8aa275 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -36,6 +36,9 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW // TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable private readonly SystemIO.FileSystemWatcher? _quickAccessFolderWatcher; private bool isDisposed; + private int _refreshVersion; + + private sealed record FolderSnapshot(IAgileReference ShellItem, string Text, string Path, bool IsPinned, string Tooltip); // Constructor @@ -75,23 +78,47 @@ public Task RefreshWidgetAsync() { return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => { + if (isDisposed) + return; + + var refreshVersion = ++_refreshVersion; + var homeFolder = HomePageContext.HomeFolder; + var folders = await STATask.RunPooled(() => + { + List result = []; + // The Shell enumerator and its metadata reads stay on this persistent STA. + foreach (IWindowsStorable folder in homeFolder.GetQuickAccessFolderAsync().ToBlockingEnumerable()) + { + using (folder) + { + folder.GetPropertyValue("System.Home.IsPinned", out var isPinned); + folder.TryGetShellTooltip(out var tooltip); + var text = folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI); + var path = folder.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING); + var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, folder.ThisPtr, out IAgileReference shellItem); + if (hr.ThrowIfFailedOnDebug().Failed) + continue; + + result.Add(new(shellItem, text, path, isPinned, tooltip ?? string.Empty)); + } + } + return result; + }, App.Logger); + + if (isDisposed || refreshVersion != _refreshVersion || folders is null) + return; + foreach (var item in Items) item.Dispose(); Items.Clear(); - await foreach (IWindowsStorable folder in HomePageContext.HomeFolder.GetQuickAccessFolderAsync(default)) + foreach (var folder in folders) { - folder.GetPropertyValue("System.Home.IsPinned", out var isPinned); - var tooltip = await STATask.RunPooled(() => folder.TryGetShellTooltip(out var t).Succeeded ? t : null, App.Logger); - - Items.Insert( - Items.Count, - new WidgetFolderCardItem( - folder, - folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI), - isPinned, - tooltip ?? string.Empty)); + if (folder.ShellItem.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed) + continue; + + Items.Add(new WidgetFolderCardItem(new WindowsFolder(shellItem), folder.Text, folder.Path, folder.IsPinned, folder.Tooltip)); } }); } diff --git a/src/Files.App/Views/MainPage.xaml.cs b/src/Files.App/Views/MainPage.xaml.cs index c480f651b73f..3577f29d915b 100644 --- a/src/Files.App/Views/MainPage.xaml.cs +++ b/src/Files.App/Views/MainPage.xaml.cs @@ -70,6 +70,11 @@ public MainPage() App.AppModel.PropertyChanged += AppModel_PropertyChanged; ApplySidebarWidthState(); + + // Realize the navigation controls before the first measure pass. + FindName(nameof(InnerNavigationToolbar)); + FindName(nameof(TabControl)); + FindName(nameof(NavToolbar)); } private void NumberedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs e) @@ -131,6 +136,9 @@ private void HorizontalMultitaskingControl_Loaded(object sender, RoutedEventArgs private int SetTitleBarDragRegion(InputNonClientPointerSource source, SizeInt32 size, double scaleFactor, Func getScaledRect) { + if (TabControl is null) + return -1; + 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))]); AttachTitleBarMessageMonitor(); @@ -328,10 +336,7 @@ private void Page_Loaded(object sender, RoutedEventArgs e) MainWindow.Instance.AppWindow.Changed += (_, _) => MainWindow.Instance.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion); - // Defers loading until after the page has loaded to improve startup perf - FindName(nameof(InnerNavigationToolbar)); - FindName(nameof(TabControl)); - FindName(nameof(NavToolbar)); + MainWindow.Instance.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion); // Notify user that drag and drop is disabled // Prompt is disabled in the dev environment to prevent issues with the automation testing diff --git a/src/Files.App/Views/ShellPanesPage.xaml.cs b/src/Files.App/Views/ShellPanesPage.xaml.cs index 7b6c35798fb6..0900abda6c73 100644 --- a/src/Files.App/Views/ShellPanesPage.xaml.cs +++ b/src/Files.App/Views/ShellPanesPage.xaml.cs @@ -962,6 +962,7 @@ private void Pane_ContentChanged(object? sender, TabBarItemParameter e) [DynamicWindowsRuntimeCast(typeof(UIElement))] [DynamicWindowsRuntimeCast(typeof(ButtonBase))] + [DynamicWindowsRuntimeCast(typeof(DependencyObject))] private void Pane_PointerPressed(object sender, PointerRoutedEventArgs e) { // A button cancels its press once it loses focus, so leave focus alone while one is being pressed