diff --git a/src/Files.App/Data/Settings/BaseJsonSettings.cs b/src/Files.App/Data/Settings/BaseJsonSettings.cs new file mode 100644 index 000000000000..58f552f40270 --- /dev/null +++ b/src/Files.App/Data/Settings/BaseJsonSettings.cs @@ -0,0 +1,197 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using Windows.Storage; + +namespace Files.App.Data.Settings; + +public abstract class BaseJsonSettings : IDisposable, INotifyPropertyChanged +{ + private readonly object gate = new(); + private readonly string filePath; + private readonly TimeSpan saveDelay; + private Timer? saveTimer; + private bool isDisposed; + private bool isLoaded; + private bool isDirty; + private bool isHydrating = true; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected BaseJsonSettings(string fileName, TimeSpan? saveDelay = null) + { + var folderPath = SystemIO.Path.Combine(ApplicationData.Current.LocalFolder.Path, Constants.LocalSettings.SettingsFolderName); + filePath = SystemIO.Path.Combine(folderPath, fileName); + this.saveDelay = saveDelay ?? TimeSpan.FromMilliseconds(250); + } + + protected void Initialize() + { + lock (gate) + { + ThrowIfDisposed(); + if (isLoaded) + return; + + var directory = SystemIO.Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory)) + SystemIO.Directory.CreateDirectory(directory); + + if (!SystemIO.File.Exists(filePath)) + { + isLoaded = true; + isHydrating = false; + return; + } + + var raw = SystemIO.File.ReadAllText(filePath); + if (!string.IsNullOrWhiteSpace(raw)) + { + DeserializeCore(raw); + } + + isLoaded = true; + isHydrating = false; + } + } + + protected bool SetProperty(ref T storage, T value, [CallerMemberName] string? propertyName = null) + { + lock (gate) + { + ThrowIfDisposed(); + if (EqualityComparer.Default.Equals(storage, value)) + return false; + + storage = value; + if (!isHydrating) + { + isDirty = true; + QueueSave_NoLock(); + } + } + + if (!isHydrating && !string.IsNullOrEmpty(propertyName)) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + return true; + } + + protected IDisposable BeginHydrationScope() + { + return new HydrationScope(this); + } + + public void SaveNow() + { + lock (gate) + { + ThrowIfDisposed(); + SaveCore_NoLock(); + } + } + + public string ExportSettings() + { + lock (gate) + { + ThrowIfDisposed(); + return ExportCore(); + } + } + + public bool ImportSettings(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return false; + + lock (gate) + { + ThrowIfDisposed(); + try + { + return ImportCore(json); + } + catch (JsonException) + { + return false; + } + } + } + + private void QueueSave_NoLock() + { + saveTimer ??= new Timer(static s => + { + var self = (BaseJsonSettings)s!; + lock (self.gate) + { + if (self.isDisposed) + return; + self.SaveCore_NoLock(); + } + }, this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + + saveTimer.Change(saveDelay, Timeout.InfiniteTimeSpan); + } + + private void SaveCore_NoLock() + { + if (!isDirty) + return; + + var json = SerializeCore(); + SystemIO.File.WriteAllText(filePath, json); + isDirty = false; + } + + protected abstract string SerializeCore(); + protected abstract void DeserializeCore(string json); + protected abstract string ExportCore(); + protected abstract bool ImportCore(string json); + + public void Dispose() + { + lock (gate) + { + if (isDisposed) + return; + + SaveCore_NoLock(); + saveTimer?.Dispose(); + saveTimer = null; + isDisposed = true; + } + + GC.SuppressFinalize(this); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(isDisposed, this); + } + + private sealed class HydrationScope : IDisposable + { + private readonly BaseJsonSettings owner; + private readonly bool previous; + private bool disposed; + + public HydrationScope(BaseJsonSettings owner) + { + this.owner = owner; + previous = owner.isHydrating; + owner.isHydrating = true; + } + + public void Dispose() + { + if (disposed) + return; + + owner.isHydrating = previous; + disposed = true; + } + } +} diff --git a/src/Files.App/Data/Settings/Settings.cs b/src/Files.App/Data/Settings/Settings.cs new file mode 100644 index 000000000000..64c487cfd243 --- /dev/null +++ b/src/Files.App/Data/Settings/Settings.cs @@ -0,0 +1,609 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using System.Text.Json.Nodes; + +namespace Files.App.Data.Settings; + +public sealed partial class Settings : BaseJsonSettings +{ + private static readonly Lazy lazyDefault = new(() => new Settings(initialize: true)); + public static Settings Default => lazyDefault.Value; + + public Settings() : this(initialize: false) + { + } + + private Settings(bool initialize) : base("settings.json") + { + if (initialize) + Initialize(); + } + + [GeneratedSettingsProperty] + public partial List? ActionsV2 { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowStatusCenterTeachingTip { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowBackgroundRunningNotification { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool RestoreTabsOnStartup { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 255d, GetValueCallback = nameof(GetSidebarWidth))] + public partial double SidebarWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool IsSidebarOpen { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "Default")] + public partial string AppThemeMode { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "#00000000")] + public partial string AppThemeBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeAddressBarBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeToolbarBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeSidebarBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeFileAreaBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeFileAreaSecondaryBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeInfoPaneBackgroundColor { get; set; } + + [GeneratedSettingsProperty(DefaultValueCallback = nameof(GetDefaultAppThemeFontFamily))] + public partial string AppThemeFontFamily { get; set; } + + [GeneratedSettingsProperty(DefaultValue = BackdropMaterialType.MicaAlt)] + public partial BackdropMaterialType AppThemeBackdropMaterial { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string AppThemeBackgroundImageSource { get; set; } + + [GeneratedSettingsProperty(DefaultValue = Stretch.UniformToFill)] + public partial Stretch AppThemeBackgroundImageFit { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 1f)] + public partial float AppThemeBackgroundImageOpacity { get; set; } + + [GeneratedSettingsProperty(DefaultValue = VerticalAlignment.Center)] + public partial VerticalAlignment AppThemeBackgroundImageVerticalAlignment { get; set; } + + [GeneratedSettingsProperty(DefaultValue = HorizontalAlignment.Center)] + public partial HorizontalAlignment AppThemeBackgroundImageHorizontalAlignment { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowToolbar { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowStatusBar { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowTabActions { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowShelfPaneToggleButton { get; set; } + + [GeneratedSettingsProperty(DefaultValue = StatusCenterVisibility.Always)] + public partial StatusCenterVisibility StatusCenterVisibility { get; set; } + + [GeneratedSettingsProperty] + public partial Dictionary>? CustomToolbarItems { get; set; } + + [GeneratedSettingsProperty] + public partial Dictionary>? LastKnownToolbarDefaults { get; set; } + + private static double GetSidebarWidth(double value) + { + return Math.Min(Math.Max(value, Constants.UI.MinimumSidebarWidth), 500d); + } + + private static string GetDefaultAppThemeFontFamily() + { + return Constants.Appearance.StandardFont; + } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool HasClickedReviewPrompt { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool HasClickedSponsorPrompt { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowRunningAsAdminPrompt { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowDataStreamsAreHiddenPrompt { get; set; } + + [GeneratedSettingsProperty(DefaultValue = OpenInIDEOption.GitRepos)] + public partial OpenInIDEOption OpenInIDEOption { get; set; } + + [GeneratedSettingsProperty(DefaultValueCallback = nameof(GetDefaultIDEPath))] + public partial string IDEPath { get; set; } + + [GeneratedSettingsProperty(DefaultValueCallback = nameof(GetDefaultIDEName))] + public partial string IDEName { get; set; } + + private static string GetDefaultIDEPath() + { + return SoftwareHelpers.IsVSCodeInstalled() ? "code" : string.Empty; + } + + private static string GetDefaultIDEName() + { + return SoftwareHelpers.IsVSCodeInstalled() ? Strings.VisualStudioCode.GetLocalizedResource() : string.Empty; + } + + [GeneratedSettingsProperty] + public partial List? FileTagList { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowHiddenItems { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowProtectedSystemFiles { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool AreAlternateStreamsVisible { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowDotFiles { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SingleClickOpenMode.OnlyForTouch, MigrateValueCallback = nameof(MigrateLegacySingleClickSettings))] + public partial SingleClickOpenMode OpenFilesWithSingleClick { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SingleClickOpenMode.OnlyForTouch)] + public partial SingleClickOpenMode OpenFoldersWithSingleClick { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SingleClickOpenMode.Always)] + public partial SingleClickOpenMode OpenFoldersInColumnsViewWithSingleClick { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool OpenFoldersInNewTab { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ScrollToPreviousFolderWhenNavigatingUp { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool CalculateFolderSizes { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowFileExtensions { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowThumbnails { get; set; } + + [GeneratedSettingsProperty(DefaultValue = DeleteConfirmationPolicies.Always)] + public partial DeleteConfirmationPolicies DeleteConfirmationPolicy { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool SelectFilesOnHover { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool DoubleClickToGoUp { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowFileExtensionWarning { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowCheckboxesWhenSelectingItems { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SizeUnitTypes.BinaryUnits)] + public partial SizeUnitTypes SizeUnitFormat { get; set; } + + private void MigrateLegacySingleClickSettings(JsonObject settings) + { + if (settings.TryGetPropertyValue("OpenItemsWithOneClick", out var openItemsWithOneClick) && + openItemsWithOneClick is not null) + { + var legacy = openItemsWithOneClick.GetValue(); + OpenFilesWithSingleClick = legacy + ? SingleClickOpenMode.Always + : SingleClickOpenMode.Never; + } + + if (settings.TryGetPropertyValue("OpenFoldersWithOneClick", out var openFoldersWithOneClick) && + openFoldersWithOneClick is not null) + { + var legacy = openFoldersWithOneClick.GetValue(); + switch (legacy) + { + case 0: + OpenFoldersWithSingleClick = SingleClickOpenMode.Never; + OpenFoldersInColumnsViewWithSingleClick = SingleClickOpenMode.Always; + break; + case 1: + OpenFoldersWithSingleClick = SingleClickOpenMode.Always; + OpenFoldersInColumnsViewWithSingleClick = SingleClickOpenMode.Always; + break; + case 2: + OpenFoldersWithSingleClick = SingleClickOpenMode.Never; + OpenFoldersInColumnsViewWithSingleClick = SingleClickOpenMode.Never; + break; + } + } + } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool OpenSpecificPageOnStartup { get; set; } + + [GeneratedSettingsProperty(DefaultValue = "")] + public partial string OpenSpecificPageOnStartupPath { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ContinueLastSessionOnStartUp { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool OpenNewTabOnStartup { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool OpenTabInExistingInstance { get; set; } + + [GeneratedSettingsProperty] + public partial List? TabsOnStartupList { get; set; } + + [GeneratedSettingsProperty(ExportIgnore = true)] + public partial List? LastSessionTabList { get; set; } + + [GeneratedSettingsProperty(ExportIgnore = true)] + public partial List? LastCrashedTabList { get; set; } + + [GeneratedSettingsProperty(ExportIgnore = true)] + public partial List? PathHistoryList { get; set; } + + [GeneratedSettingsProperty(ExportIgnore = true)] + public partial List? PreviousSearchQueriesList { get; set; } + + [GeneratedSettingsProperty(ExportIgnore = true)] + public partial List? PreviousArchiveExtractionLocations { get; set; } + + [GeneratedSettingsProperty(DefaultValue = DateTimeFormats.Application)] + public partial DateTimeFormats DateTimeFormat { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool AlwaysOpenDualPaneInNewTab { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool AlwaysSwitchToNewlyOpenedTab { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowQuickAccessWidget { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowRecentFilesWidget { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowDrivesWidget { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowNetworkLocationsWidget { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowFileTagsWidget { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool FoldersWidgetExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool RecentFilesWidgetExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool DrivesWidgetExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool NetworkLocationsWidgetExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool FileTagsWidgetExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowPinnedSection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool IsPinnedSectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowLibrarySection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsLibrarySectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowDrivesSection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsDriveSectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowCloudDrivesSection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsCloudDriveSectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowNetworkSection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsNetworkSectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowWslSection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsWslSectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowFileTagsSection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsFileTagsSectionExpanded { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool MoveShellExtensionsToSubMenu { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowPinToSideBar { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowPinToStart { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowEditTagsMenu { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowCompressionOptions { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowFlattenOptions { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowSendToMenu { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowOpenInNewTab { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowOpenInNewWindow { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowOpenInNewPane { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowOpenTerminal { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowCopyPath { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowCreateFolderWithSelection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowCreateAlternateDataStream { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowCreateShortcut { get; set; } + +#if DEBUG + [GeneratedSettingsProperty(DefaultValue = false)] +#else + [GeneratedSettingsProperty(DefaultValue = true)] +#endif + public partial bool LeaveAppRunning { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowSystemTrayIcon { get; set; } + + [GeneratedSettingsProperty(DefaultValue = FileNameConflictResolveOptionType.GenerateNewName)] + public partial FileNameConflictResolveOptionType ConflictsResolveOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ArchiveFormats.Zip)] + public partial ArchiveFormats ArchiveFormatsOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ArchiveCompressionLevels.Normal)] + public partial ArchiveCompressionLevels ArchiveCompressionLevelsOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ArchiveSplittingSizes.None)] + public partial ArchiveSplittingSizes ArchiveSplittingSizesOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ArchiveDictionarySizes.Auto)] + public partial ArchiveDictionarySizes ArchiveDictionarySizesOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ArchiveWordSizes.Auto)] + public partial ArchiveWordSizes ArchiveWordSizesOption { get; set; } + + [GeneratedSettingsProperty] + public partial Dictionary? ShowHashesDictionary { get; set; } + + [GeneratedSettingsProperty(DefaultValueCallback = nameof(GetDefaultUserId))] + public partial string UserId { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ShellPaneArrangement.Vertical)] + public partial ShellPaneArrangement ShellPaneArrangementOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowShelfPane { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowFilterHeader { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool EnableThumbnailCache { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool EnableSmoothScrolling { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 512d)] + public partial double ThumbnailCacheSizeLimit { get; set; } + + private static string GetDefaultUserId() + { + return Guid.NewGuid().ToString(); + } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool IsInfoPaneEnabled { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 300d, GetValueCallback = nameof(GetInfoPaneSize))] + public partial double HorizontalSizePx { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 250d, GetValueCallback = nameof(GetInfoPaneSize))] + public partial double VerticalSizePx { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 1d, GetValueCallback = nameof(GetMediaVolume))] + public partial double MediaVolume { get; set; } + + [GeneratedSettingsProperty(DefaultValue = InfoPaneTabs.Details)] + public partial InfoPaneTabs SelectedTab { get; set; } + + private static double GetInfoPaneSize(double value) + { + return Math.Max(100d, value); + } + + private static double GetMediaVolume(double value) + { + return Math.Min(Math.Max(value, 0d), 1d); + } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool SyncFolderPreferencesAcrossDirectories { get; set; } + + [GeneratedSettingsProperty(DefaultValue = FolderLayoutModes.Adaptive)] + public partial FolderLayoutModes DefaultLayoutMode { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SortOption.Name)] + public partial SortOption DefaultSortOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SortDirection.Ascending)] + public partial SortDirection DefaultDirectorySortDirection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool DefaultSortDirectoriesAlongsideFiles { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool DefaultSortFilesFirst { get; set; } + + [GeneratedSettingsProperty(DefaultValue = GroupOption.None)] + public partial GroupOption DefaultGroupOption { get; set; } + + [GeneratedSettingsProperty(DefaultValue = SortDirection.Ascending)] + public partial SortDirection DefaultDirectoryGroupDirection { get; set; } + + [GeneratedSettingsProperty(DefaultValue = GroupByDateUnit.Year)] + public partial GroupByDateUnit DefaultGroupByDateUnit { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 80d)] + public partial double GitStatusColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 140d)] + public partial double GitLastCommitDateColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 140d)] + public partial double GitLastCommitMessageColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 140d)] + public partial double GitCommitAuthorColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 80d)] + public partial double GitLastCommitShaColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 140d)] + public partial double TagColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 240d)] + public partial double NameColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 200d)] + public partial double DateModifiedColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 140d)] + public partial double TypeColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 200d)] + public partial double DateCreatedColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 100d)] + public partial double SizeColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 200d)] + public partial double DateDeletedColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 200d)] + public partial double PathColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 200d)] + public partial double OriginalPathColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = 50d)] + public partial double SyncStatusColumnWidth { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowDateColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowDateCreatedColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowTypeColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowSizeColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowGitStatusColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowGitLastCommitDateColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowGitLastCommitMessageColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowGitCommitAuthorColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = false)] + public partial bool ShowGitLastCommitShaColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowFileTagColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowDateDeletedColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowPathColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowOriginalPathColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = true)] + public partial bool ShowSyncStatusColumn { get; set; } + + [GeneratedSettingsProperty(DefaultValue = DetailsViewSizeKind.Small)] + public partial DetailsViewSizeKind DetailsViewSize { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ListViewSizeKind.Small)] + public partial ListViewSizeKind ListViewSize { get; set; } + + [GeneratedSettingsProperty(DefaultValue = CardsViewSizeKind.Small)] + public partial CardsViewSizeKind CardsViewSize { get; set; } + + [GeneratedSettingsProperty(DefaultValue = GridViewSizeKind.Large)] + public partial GridViewSizeKind GridViewSize { get; set; } + + [GeneratedSettingsProperty(DefaultValue = ColumnsViewSizeKind.Small)] + public partial ColumnsViewSizeKind ColumnsViewSize { get; set; } +} diff --git a/src/Files.App/Data/Settings/SettingsJsonContext.cs b/src/Files.App/Data/Settings/SettingsJsonContext.cs new file mode 100644 index 000000000000..1c16299c8f32 --- /dev/null +++ b/src/Files.App/Data/Settings/SettingsJsonContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Data.Settings; + +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default)] +[JsonSerializable(typeof(Settings))] +[JsonSerializable(typeof(ActionWithParameterItem))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(TagViewModel))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(ToolbarItemSettingsEntry))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary>))] +[JsonSerializable(typeof(Dictionary>))] +internal sealed partial class SettingsJsonContext : JsonSerializerContext +{ +} diff --git a/src/Files.Core.SourceGenerator/Generators/SettingsPropertyGenerator.cs b/src/Files.Core.SourceGenerator/Generators/SettingsPropertyGenerator.cs new file mode 100644 index 000000000000..5b89c215af81 --- /dev/null +++ b/src/Files.Core.SourceGenerator/Generators/SettingsPropertyGenerator.cs @@ -0,0 +1,306 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.Core.SourceGenerator.Generators; + +[Generator] +internal sealed class SettingsPropertyGenerator : IIncrementalGenerator +{ + private const string AttributeMetadataName = "Files.Shared.Attributes.GeneratedSettingsPropertyAttribute"; + + private static readonly SymbolDisplayFormat FullyQualifiedWithNullable = SymbolDisplayFormat.FullyQualifiedFormat + .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.ForAttributeWithMetadataName( + AttributeMetadataName, + static (node, _) => node is PropertyDeclarationSyntax, + static (ctx, _) => (IPropertySymbol)ctx.TargetSymbol); + + var collected = provider.Collect(); + context.RegisterSourceOutput(collected, Execute); + } + + private static void Execute(SourceProductionContext context, ImmutableArray properties) + { + if (properties.IsDefaultOrEmpty) + return; + + foreach (var group in properties.GroupBy(static p => p.ContainingType, SymbolEqualityComparer.Default)) + { + if (group.Key is not INamedTypeSymbol typeSymbol) + continue; + + var diagnostics = new List(); + var code = EmitType(typeSymbol, group, diagnostics); + foreach (var diagnostic in diagnostics) + context.ReportDiagnostic(diagnostic); + + if (code is not null) + context.AddSource($"{typeSymbol.Name}.GeneratedSettingsProperties.g.cs", code); + } + } + + private static string? EmitType(INamedTypeSymbol typeSymbol, IEnumerable props, List diagnostics) + { + var properties = props + .OfType() + .GroupBy(static p => p.Name, StringComparer.Ordinal) + .Select(static g => g.First()) + .OrderBy(static p => p.Name) + .ToArray(); + if (properties.Length == 0) + return null; + var migrateValueCallbacks = properties + .Select(static p => TryGetStringNamedArgument(p, "MigrateValueCallback")) + .Where(static callback => callback is not null) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + var ns = typeSymbol.ContainingNamespace?.IsGlobalNamespace is false + ? typeSymbol.ContainingNamespace.ToDisplayString() + : null; + + var sb = new StringBuilder(); + _ = sb.AppendLine("// "); + _ = sb.AppendLine("#nullable enable"); + _ = sb.AppendLine("using System.Buffers;"); + _ = sb.AppendLine("using System;"); + _ = sb.AppendLine("using System.Text;"); + _ = sb.AppendLine("using System.Text.Json;"); + _ = sb.AppendLine("using System.Text.Json.Nodes;"); + _ = sb.AppendLine("using System.Text.Json.Serialization;"); + _ = sb.AppendLine(); + + if (ns is not null) + { + _ = sb.AppendLine($"namespace {ns};"); + _ = sb.AppendLine(); + } + + _ = sb.AppendLine($"partial class {typeSymbol.Name}"); + _ = sb.AppendLine("{"); + + _ = sb.AppendLine("\tprotected override string SerializeCore()"); + _ = sb.AppendLine("\t{"); + _ = sb.AppendLine("\t\tvar buffer = new ArrayBufferWriter();"); + _ = sb.AppendLine("\t\tusing var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = true });"); + _ = sb.AppendLine($"\t\tJsonSerializer.Serialize(writer, this, SettingsJsonContext.Default.{typeSymbol.Name});"); + _ = sb.AppendLine("\t\twriter.Flush();"); + _ = sb.AppendLine("\t\treturn Encoding.UTF8.GetString(buffer.WrittenSpan);"); + _ = sb.AppendLine("\t}"); + _ = sb.AppendLine(); + _ = sb.AppendLine("\tprotected override void DeserializeCore(string json)"); + _ = sb.AppendLine("\t{"); + _ = sb.AppendLine($"\t\tvar loaded = JsonSerializer.Deserialize(json, SettingsJsonContext.Default.{typeSymbol.Name}) as {typeSymbol.Name};"); + _ = sb.AppendLine("\t\tif (loaded is null)"); + _ = sb.AppendLine("\t\t\treturn;"); + _ = sb.AppendLine(); + _ = sb.AppendLine("\t\tusing (BeginHydrationScope())"); + _ = sb.AppendLine("\t\t{"); + foreach (var p in properties) + { + _ = sb.AppendLine($"\t\t\t__{p.Name} = loaded.__{p.Name};"); + } + EmitMigrateValueCallbacks(sb, migrateValueCallbacks); + _ = sb.AppendLine("\t\t}"); + _ = sb.AppendLine("\t}"); + _ = sb.AppendLine(); + _ = sb.AppendLine("\tprotected override string ExportCore()"); + _ = sb.AppendLine("\t{"); + _ = sb.AppendLine("\t\tvar exported = JsonNode.Parse(SerializeCore()) as JsonObject;"); + _ = sb.AppendLine("\t\tif (exported is null)"); + _ = sb.AppendLine("\t\t\treturn \"{}\";"); + _ = sb.AppendLine(); + foreach (var p in properties.Where(IsExportIgnored)) + { + _ = sb.AppendLine($"\t\texported.Remove(nameof({p.Name}));"); + } + _ = sb.AppendLine(); + _ = sb.AppendLine("\t\treturn exported.ToJsonString(new JsonSerializerOptions { WriteIndented = true });"); + _ = sb.AppendLine("\t}"); + _ = sb.AppendLine(); + _ = sb.AppendLine("\tprotected override bool ImportCore(string json)"); + _ = sb.AppendLine("\t{"); + _ = sb.AppendLine("\t\tusing var document = JsonDocument.Parse(json);"); + _ = sb.AppendLine("\t\tif (document.RootElement.ValueKind != JsonValueKind.Object)"); + _ = sb.AppendLine("\t\t\treturn false;"); + _ = sb.AppendLine(); + _ = sb.AppendLine($"\t\tvar loaded = JsonSerializer.Deserialize(json, SettingsJsonContext.Default.{typeSymbol.Name}) as {typeSymbol.Name};"); + _ = sb.AppendLine("\t\tif (loaded is null)"); + _ = sb.AppendLine("\t\t\treturn false;"); + _ = sb.AppendLine(); + _ = sb.AppendLine("\t\tvar imported = false;"); + foreach (var p in properties.Where(static p => !IsExportIgnored(p))) + { + _ = sb.AppendLine($"\t\tif (document.RootElement.TryGetProperty(nameof({p.Name}), out _))"); + _ = sb.AppendLine($"\t\t\timported |= SetProperty(ref __{p.Name}, loaded.__{p.Name}, nameof({p.Name}));"); + } + if (migrateValueCallbacks.Length > 0) + { + _ = sb.AppendLine(); + EmitMigrateValueCallbacks(sb, migrateValueCallbacks, "\t\t"); + _ = sb.AppendLine("\t\timported = true;"); + } + _ = sb.AppendLine(); + _ = sb.AppendLine("\t\treturn imported;"); + _ = sb.AppendLine("\t}"); + _ = sb.AppendLine(); + + foreach (var p in properties) + { + var defaultExpr = TryGetDefaultCallbackExpression(p) ?? TryGetDefaultExpression(p, diagnostics); + defaultExpr ??= GetTypeDefaultExpression(p.Type); + + var getValueCallback = TryGetStringNamedArgument(p, "GetValueCallback"); + var typeName = p.Type.ToDisplayString(FullyQualifiedWithNullable); + _ = sb.AppendLine("\t[JsonIgnore]"); + _ = sb.AppendLine($"\tprivate {typeName} __{p.Name} = {defaultExpr};"); + _ = sb.AppendLine(); + _ = sb.AppendLine($"\tpublic partial {typeName} {p.Name}"); + _ = sb.AppendLine("\t{"); + _ = sb.AppendLine(getValueCallback is null + ? $"\t\tget => __{p.Name};" + : $"\t\tget => {getValueCallback}(__{p.Name});"); + _ = sb.AppendLine($"\t\tset => SetProperty(ref __{p.Name}, value);"); + _ = sb.AppendLine("\t}"); + _ = sb.AppendLine(); + } + + _ = sb.AppendLine("}"); + return sb.ToString(); + } + + private static void EmitMigrateValueCallbacks(StringBuilder sb, string?[] migrateValueCallbacks, string indent = "\t\t\t") + { + if (migrateValueCallbacks.Length == 0) + return; + + _ = sb.AppendLine(); + _ = sb.AppendLine($"{indent}var settingsJson = JsonNode.Parse(json) as JsonObject;"); + _ = sb.AppendLine($"{indent}if (settingsJson is not null)"); + _ = sb.AppendLine($"{indent}{{"); + foreach (var callback in migrateValueCallbacks) + { + _ = sb.AppendLine($"{indent}\t{callback}(settingsJson);"); + } + _ = sb.AppendLine($"{indent}}}"); + } + + private static bool IsExportIgnored(IPropertySymbol property) + { + var attr = property.GetAttributes().FirstOrDefault(static a => a.AttributeClass?.ToDisplayString() == AttributeMetadataName); + if (attr is null) + return false; + + var named = attr.NamedArguments.FirstOrDefault(static kv => kv.Key == "ExportIgnore"); + return !named.Equals(default(KeyValuePair)) && named.Value.Value is true; + } + + private static string? TryGetDefaultCallbackExpression(IPropertySymbol property) + { + var callback = TryGetStringNamedArgument(property, "DefaultValueCallback"); + return callback is null + ? null + : $"{callback}()"; + } + + private static string? TryGetStringNamedArgument(IPropertySymbol property, string name) + { + var attr = property.GetAttributes().FirstOrDefault(static a => a.AttributeClass?.ToDisplayString() == AttributeMetadataName); + if (attr is null) + return null; + + var named = attr.NamedArguments.FirstOrDefault(kv => kv.Key == name); + if (named.Equals(default(KeyValuePair))) + return null; + + return named.Value.Value as string; + } + + private static string? TryGetDefaultExpression(IPropertySymbol property, List diagnostics) + { + var attr = property.GetAttributes().FirstOrDefault(static a => a.AttributeClass?.ToDisplayString() == AttributeMetadataName); + if (attr is null) + return null; + + var named = attr.NamedArguments.FirstOrDefault(static kv => kv.Key == "DefaultValue"); + if (named.Equals(default(KeyValuePair))) + return null; + + var constant = named.Value; + var expr = FormatDefault(constant, property.Type); + if (expr is null) + { + diagnostics.Add(Diagnostic.Create( + new DiagnosticDescriptor( + "FSG2001", + "Unsupported default value", + "DefaultValue for property '{0}' has an unsupported type", + "Design", + DiagnosticSeverity.Error, + isEnabledByDefault: true), + property.Locations.FirstOrDefault(), + property.Name)); + } + + return expr; + } + + private static string GetTypeDefaultExpression(ITypeSymbol type) + { + if (type.NullableAnnotation == NullableAnnotation.Annotated || type.IsReferenceType) + return "default!"; + + var typeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return $"default({typeName})"; + } + + private static string? FormatDefault(TypedConstant constant, ITypeSymbol targetType) + { + if (constant.IsNull) + return targetType.IsReferenceType || targetType.NullableAnnotation == NullableAnnotation.Annotated + ? "null" + : GetTypeDefaultExpression(targetType); + + if (targetType.TypeKind == TypeKind.Enum) + { + var enumType = targetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var enumMember = targetType.GetMembers() + .OfType() + .FirstOrDefault(m => m.HasConstantValue && Equals(m.ConstantValue, constant.Value)); + + return enumMember is not null + ? $"{enumType}.{enumMember.Name}" + : $"({enumType}){FormatNumericLiteral(constant.Value)}"; + } + + return constant.Value switch + { + string s => $"\"{s.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"", + bool b => b ? "true" : "false", + char c => $"'{(c == '\'' ? "\\'" : c.ToString())}'", + byte or sbyte or short or ushort or int => Convert.ToString(constant.Value, System.Globalization.CultureInfo.InvariantCulture), + uint u => $"{u}U", + long l => $"{l}L", + ulong ul => $"{ul}UL", + float f => $"{f.ToString(System.Globalization.CultureInfo.InvariantCulture)}F", + double d => $"{d.ToString(System.Globalization.CultureInfo.InvariantCulture)}D", + decimal m => $"{m.ToString(System.Globalization.CultureInfo.InvariantCulture)}M", + _ => null, + }; + } + + private static string FormatNumericLiteral(object? value) + { + return value switch + { + byte or sbyte or short or ushort or int => Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)!, + uint u => $"{u}U", + long l => $"{l}L", + ulong ul => $"{ul}UL", + _ => "0", + }; + } +} diff --git a/src/Files.Shared/Attributes/GeneratedSettingsPropertyAttribute.cs b/src/Files.Shared/Attributes/GeneratedSettingsPropertyAttribute.cs new file mode 100644 index 000000000000..c550468be53b --- /dev/null +++ b/src/Files.Shared/Attributes/GeneratedSettingsPropertyAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Files.Shared.Attributes; + +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public sealed class GeneratedSettingsPropertyAttribute : Attribute +{ + public object? DefaultValue { get; set; } + public string? DefaultValueCallback { get; set; } + public string? GetValueCallback { get; set; } + public string? MigrateValueCallback { get; set; } + public bool ExportIgnore { get; set; } +}