Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 41 additions & 45 deletions src/Files.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<IGeneralSettingsService>().LeaveAppRunning;
_ = provider.GetRequiredService<IAppearanceSettingsService>().AppThemeBackdropMaterial;

// Read through these statics by the action/context ctors warmed below
QuickAccessManager = provider.GetRequiredService<QuickAccessManager>();
HistoryWrapper = provider.GetRequiredService<StorageHistoryWrapper>();
FileTagsManager = provider.GetRequiredService<FileTagsManager>();
LibraryManager = provider.GetRequiredService<LibraryManager>();

// 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<ICommandManager>();
}
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<IAppearanceSettingsService>();
_ = 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<QuickAccessManager>();
HistoryWrapper = provider.GetRequiredService<StorageHistoryWrapper>();
FileTagsManager = provider.GetRequiredService<FileTagsManager>();
LibraryManager = provider.GetRequiredService<LibraryManager>();

_ = provider.GetRequiredService<ICommandManager>();
}
catch (Exception)
{
// UI-thread-only constructors run on first use instead.
}
finally
{
Thread.CurrentThread.Priority = previousPriority;
}
});

return provider;
}
Expand All @@ -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
Expand All @@ -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<IUserSettingsService>();
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<QuickAccessManager>();
HistoryWrapper = Ioc.Default.GetRequiredService<StorageHistoryWrapper>();
Expand Down
30 changes: 24 additions & 6 deletions src/Files.App/Data/Items/WidgetFolderCardItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,38 +25,54 @@ 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;
}

// Methods

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();
}
}
Expand Down
40 changes: 29 additions & 11 deletions src/Files.App/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> cachedResources = new();

private static readonly Dictionary<string, string> 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;
Expand Down Expand Up @@ -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();
}
}
}
}
3 changes: 3 additions & 0 deletions src/Files.App/Helpers/Application/AppLanguageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Globalization;
using Windows.Globalization;
using StringExtensions = Files.App.Extensions.StringExtensions;

namespace Files.App.Helpers
{
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/Files.App/Helpers/Application/AppLifecycleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ static AppLifecycleHelper()
/// <summary>
/// Gets application icon path.
/// </summary>
public static string AppIconPath { get; } =
SystemIO.Path.Combine(Package.Current.InstalledLocation.Path, AppEnvironment switch
public static string AppIconPath => _appIconPath.Value;

private static readonly Lazy<string> _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
});
}));

/// <summary>
/// Initializes the app components.
Expand Down
6 changes: 6 additions & 0 deletions src/Files.App/Helpers/UI/AppSystemBackdrop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
32 changes: 32 additions & 0 deletions src/Files.App/Helpers/UI/UIHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

/// <summary>
/// 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
Expand Down
Loading
Loading