Browse Source

Merge pull request #617 from PixiEditor/settings-wrapper

Added Settings Wrapper
CPK 1 year ago
parent
commit
9e55583b30
38 changed files with 1078 additions and 172 deletions
  1. 1 1
      src/PixiEditor.AvaloniaUI/Models/Constants.cs
  2. 1 0
      src/PixiEditor.AvaloniaUI/Models/ExceptionHandling/CrashReport.cs
  3. 1 0
      src/PixiEditor.AvaloniaUI/Models/Handlers/IToolsHandler.cs
  4. 5 4
      src/PixiEditor.AvaloniaUI/Models/Palettes/LocalPalettesFetcher.cs
  5. 26 0
      src/PixiEditor.AvaloniaUI/Models/Preferences/PreferencesSettings.cs
  6. 5 4
      src/PixiEditor.AvaloniaUI/Models/Services/NewsFeed/NewsProvider.cs
  7. 8 7
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DebugViewModel.cs
  8. 7 55
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DiscordViewModel.cs
  9. 9 13
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs
  10. 1 0
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/IoViewModel.cs
  11. 3 3
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/StylusViewModel.cs
  12. 5 6
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ToolsViewModel.cs
  13. 50 43
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/UpdateViewModel.cs
  14. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/Tools/Tools/PenToolViewModel.cs
  15. 3 3
      src/PixiEditor.AvaloniaUI/Views/Dialogs/Debugging/Localization/LocalizationDataContext.cs
  16. 4 4
      src/PixiEditor.AvaloniaUI/Views/Dialogs/NewFileDialog.cs
  17. 1 0
      src/PixiEditor.AvaloniaUI/Views/Dock/DocumentTemplate.axaml.cs
  18. 4 4
      src/PixiEditor.AvaloniaUI/Views/Windows/HelloTherePopup.axaml.cs
  19. 15 18
      src/PixiEditor.AvaloniaUI/Views/Windows/PalettesBrowser.axaml.cs
  20. 1 1
      src/PixiEditor.AvaloniaUI/Views/Windows/Settings/SettingsWindow.axaml
  21. 11 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/DiagnosticConstants.cs
  22. 22 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/DiagnosticHelpers.cs
  23. 94 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseGenericEnumerableForListArrayDiagnostic.cs
  24. 91 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseNonOwnedDiagnostic.cs
  25. 84 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseOwnedDiagnostic.cs
  26. 83 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseGenericEnumerableForListArrayFix.cs
  27. 95 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseNonOwnedFix.cs
  28. 67 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseOwnedFix.cs
  29. 17 0
      src/PixiEditor.Extensions.CommonApi.Diagnostics/PixiEditor.Extensions.CommonApi.Diagnostics.csproj
  30. 46 0
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  31. 29 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/LocalSetting.cs
  32. 67 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs
  33. 1 1
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/RightClickMode.cs
  34. 116 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/Setting.cs
  35. 3 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/SettingChangedHandler.cs
  36. 40 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/SettingHelpers.cs
  37. 29 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/SyncedSetting.cs
  38. 31 3
      src/PixiEditor.sln

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Constants.cs

@@ -10,4 +10,4 @@ internal class Constants
 
 
     public const string NativeExtensionNoDot = "pixi";
     public const string NativeExtensionNoDot = "pixi";
     public const string NativeExtension = "." + NativeExtensionNoDot;
     public const string NativeExtension = "." + NativeExtensionNoDot;
-}
+}

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/ExceptionHandling/CrashReport.cs

@@ -15,6 +15,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 
 
 namespace PixiEditor.AvaloniaUI.Models.ExceptionHandling;
 namespace PixiEditor.AvaloniaUI.Models.ExceptionHandling;

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/Handlers/IToolsHandler.cs

@@ -4,6 +4,7 @@ using PixiEditor.AvaloniaUI.Models.Events;
 using PixiEditor.AvaloniaUI.Models.Preferences;
 using PixiEditor.AvaloniaUI.Models.Preferences;
 using PixiEditor.AvaloniaUI.ViewModels.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 namespace PixiEditor.AvaloniaUI.Models.Handlers;

+ 5 - 4
src/PixiEditor.AvaloniaUI/Models/Palettes/LocalPalettesFetcher.cs

@@ -8,7 +8,8 @@ using PixiEditor.AvaloniaUI.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Extensions.CommonApi.Async;
 using PixiEditor.Extensions.CommonApi.Async;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 
 
 namespace PixiEditor.AvaloniaUI.Models.Palettes;
 namespace PixiEditor.AvaloniaUI.Models.Palettes;
 
 
@@ -39,11 +40,11 @@ internal class LocalPalettesFetcher : PaletteListDataSource
         watcher.Created += FileSystemChanged;
         watcher.Created += FileSystemChanged;
 
 
         watcher.EnableRaisingEvents = true;
         watcher.EnableRaisingEvents = true;
-        cachedFavoritePalettes = IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
+        cachedFavoritePalettes = PixiEditorSettings.Palettes.FavouritePalettes.AsList();
 
 
-        IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, (_, updated) =>
+        PixiEditorSettings.Palettes.FavouritePalettes.AddListCallback(updated =>
         {
         {
-            cachedFavoritePalettes = (List<string>)updated;
+            cachedFavoritePalettes = updated;
             cachedPalettes.ForEach(x => x.IsFavourite = cachedFavoritePalettes.Contains(x.Name));
             cachedPalettes.ForEach(x => x.IsFavourite = cachedFavoritePalettes.Contains(x.Name));
         });
         });
     }
     }

+ 26 - 0
src/PixiEditor.AvaloniaUI/Models/Preferences/PreferencesSettings.cs

@@ -42,6 +42,8 @@ internal class PreferencesSettings : IPreferences
 
 
     public void UpdatePreference<T>(string name, T value)
     public void UpdatePreference<T>(string name, T value)
     {
     {
+        name = TrimPrefix(name);
+
         if (IsLoaded == false)
         if (IsLoaded == false)
         {
         {
             Init();
             Init();
@@ -62,6 +64,8 @@ internal class PreferencesSettings : IPreferences
 
 
     public void UpdateLocalPreference<T>(string name, T value)
     public void UpdateLocalPreference<T>(string name, T value)
     {
     {
+        name = TrimPrefix(name);
+
         if (IsLoaded == false)
         if (IsLoaded == false)
         {
         {
             Init();
             Init();
@@ -95,6 +99,8 @@ internal class PreferencesSettings : IPreferences
 
 
     public void AddCallback(string name, Action<string, object> action)
     public void AddCallback(string name, Action<string, object> action)
     {
     {
+        name = TrimPrefix(name);
+
         if (action == null)
         if (action == null)
         {
         {
             throw new ArgumentNullException(nameof(action));
             throw new ArgumentNullException(nameof(action));
@@ -111,6 +117,8 @@ internal class PreferencesSettings : IPreferences
 
 
     public void AddCallback<T>(string name, Action<string, T> action)
     public void AddCallback<T>(string name, Action<string, T> action)
     {
     {
+        name = TrimPrefix(name);
+
         if (action == null)
         if (action == null)
         {
         {
             throw new ArgumentNullException(nameof(action));
             throw new ArgumentNullException(nameof(action));
@@ -121,6 +129,8 @@ internal class PreferencesSettings : IPreferences
 
 
     public void RemoveCallback(string name, Action<string, object> action)
     public void RemoveCallback(string name, Action<string, object> action)
     {
     {
+        name = TrimPrefix(name);
+
         if (action == null)
         if (action == null)
         {
         {
             throw new ArgumentNullException(nameof(action));
             throw new ArgumentNullException(nameof(action));
@@ -134,6 +144,8 @@ internal class PreferencesSettings : IPreferences
 
 
     public void RemoveCallback<T>(string name, Action<string, T> action)
     public void RemoveCallback<T>(string name, Action<string, T> action)
     {
     {
+        name = TrimPrefix(name);
+
         if (action == null)
         if (action == null)
         {
         {
             throw new ArgumentNullException(nameof(action));
             throw new ArgumentNullException(nameof(action));
@@ -146,11 +158,15 @@ internal class PreferencesSettings : IPreferences
 
 
     public T? GetPreference<T>(string name)
     public T? GetPreference<T>(string name)
     {
     {
+        name = TrimPrefix(name);
+
         return GetPreference(name, default(T));
         return GetPreference(name, default(T));
     }
     }
 
 
     public T? GetPreference<T>(string name, T? fallbackValue)
     public T? GetPreference<T>(string name, T? fallbackValue)
     {
     {
+        name = TrimPrefix(name);
+
         if (IsLoaded == false)
         if (IsLoaded == false)
         {
         {
             Init();
             Init();
@@ -171,11 +187,15 @@ internal class PreferencesSettings : IPreferences
 
 
     public T? GetLocalPreference<T>(string name)
     public T? GetLocalPreference<T>(string name)
     {
     {
+        name = TrimPrefix(name);
+
         return GetLocalPreference(name, default(T));
         return GetLocalPreference(name, default(T));
     }
     }
 
 
     public T? GetLocalPreference<T>(string name, T? fallbackValue)
     public T? GetLocalPreference<T>(string name, T? fallbackValue)
     {
     {
+        name = TrimPrefix(name);
+        
         if (IsLoaded == false)
         if (IsLoaded == false)
         {
         {
             Init();
             Init();
@@ -196,6 +216,8 @@ internal class PreferencesSettings : IPreferences
 
 
     private T? GetValue<T>(Dictionary<string, object> dict, string name, T? fallbackValue)
     private T? GetValue<T>(Dictionary<string, object> dict, string name, T? fallbackValue)
     {
     {
+        name = TrimPrefix(name);
+        
         if (!dict.ContainsKey(name)) return fallbackValue;
         if (!dict.ContainsKey(name)) return fallbackValue;
         var preference = dict[name];
         var preference = dict[name];
         if (typeof(T) == preference.GetType()) return (T)preference;
         if (typeof(T) == preference.GetType()) return (T)preference;
@@ -250,4 +272,8 @@ internal class PreferencesSettings : IPreferences
 
 
         return new Dictionary<string, object>();
         return new Dictionary<string, object>();
     }
     }
+
+    private const string Prefix = "PixiEditor:";
+
+    private string TrimPrefix(string value) => value.StartsWith("PixiEditor:") ? value[Prefix.Length..] : value;
 }
 }

+ 5 - 4
src/PixiEditor.AvaloniaUI/Models/Services/NewsFeed/NewsProvider.cs

@@ -4,7 +4,8 @@ using System.Net;
 using System.Net.Http;
 using System.Net.Http;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Newtonsoft.Json;
 using Newtonsoft.Json;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Platform;
 using PixiEditor.Platform;
 
 
 namespace PixiEditor.AvaloniaUI.Models.Services.NewsFeed;
 namespace PixiEditor.AvaloniaUI.Models.Services.NewsFeed;
@@ -14,11 +15,11 @@ internal class NewsProvider
     private const int MaxNewsCount = 20;
     private const int MaxNewsCount = 20;
     private const string FeedUrl = "https://raw.githubusercontent.com/PixiEditor/news-feed/main/";
     private const string FeedUrl = "https://raw.githubusercontent.com/PixiEditor/news-feed/main/";
 
 
-    private List<int> _lastCheckedIds = new List<int>();
+    private List<int> _lastCheckedIds;
 
 
     public NewsProvider()
     public NewsProvider()
     {
     {
-        _lastCheckedIds = IPreferences.Current.GetPreference(PreferencesConstants.LastCheckedNewsIds, new List<int>());
+        _lastCheckedIds = PixiEditorSettings.StartupWindow.LastCheckedNewsIds.AsList();
     }
     }
 
 
     public async Task<List<News>?> FetchNewsAsync()
     public async Task<List<News>?> FetchNewsAsync()
@@ -60,6 +61,6 @@ internal class NewsProvider
             }
             }
         }
         }
 
 
-        IPreferences.Current.UpdatePreference(PreferencesConstants.LastCheckedNewsIds, _lastCheckedIds);
+        PixiEditorSettings.StartupWindow.LastCheckedNewsIds.Value = _lastCheckedIds;
     }
     }
 }
 }

+ 8 - 7
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DebugViewModel.cs

@@ -17,7 +17,8 @@ using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.AvaloniaUI.Views.Dialogs.Debugging;
 using PixiEditor.AvaloniaUI.Views.Dialogs.Debugging;
 using PixiEditor.AvaloniaUI.Views.Dialogs.Debugging.Localization;
 using PixiEditor.AvaloniaUI.Views.Dialogs.Debugging.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
@@ -67,12 +68,12 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
-    public DebugViewModel(ViewModelMain owner, IPreferences preferences)
+    public DebugViewModel(ViewModelMain owner)
         : base(owner)
         : base(owner)
     {
     {
         SetDebug();
         SetDebug();
-        preferences.AddCallback<bool>("IsDebugModeEnabled", UpdateDebugMode);
-        UpdateDebugMode("IsDebugModeEnabled", preferences.GetPreference<bool>("IsDebugModeEnabled"));
+        PixiEditorSettings.Debug.IsDebugModeEnabled.ValueChanged += UpdateDebugMode;
+        UpdateDebugMode(null, PixiEditorSettings.Debug.IsDebugModeEnabled.Value);
     }
     }
 
 
     public static void OpenFolder(string path)
     public static void OpenFolder(string path)
@@ -218,7 +219,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     public void ClearRecentDocuments()
     public void ClearRecentDocuments()
     {
     {
         Owner.FileSubViewModel.RecentlyOpened.Clear();
         Owner.FileSubViewModel.RecentlyOpened.Clear();
-        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, Array.Empty<object>());
+        PixiEditorSettings.File.RecentlyOpened.Value = [];
     }
     }
 
 
     [Command.Debug("PixiEditor.Debug.OpenCommandDebugWindow", "OPEN_CMD_DEBUG_WINDOW", "OPEN_CMD_DEBUG_WINDOW",
     [Command.Debug("PixiEditor.Debug.OpenCommandDebugWindow", "OPEN_CMD_DEBUG_WINDOW", "OPEN_CMD_DEBUG_WINDOW",
@@ -315,9 +316,9 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     [Conditional("DEBUG")]
     [Conditional("DEBUG")]
     private static void SetDebug() => IsDebugBuild = true;
     private static void SetDebug() => IsDebugBuild = true;
 
 
-    private void UpdateDebugMode(string name, bool setting)
+    private void UpdateDebugMode(Setting<bool> setting, bool value)
     {
     {
-        IsDebugModeEnabled = setting;
+        IsDebugModeEnabled = value;
         UseDebug = IsDebugBuild || IsDebugModeEnabled;
         UseDebug = IsDebugBuild || IsDebugModeEnabled;
     }
     }
 }
 }

+ 7 - 55
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DiscordViewModel.cs

@@ -3,7 +3,7 @@ using DiscordRPC;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 
 
@@ -32,62 +32,14 @@ internal class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
         }
         }
     }
     }
 
 
-    private bool showDocumentName = IPreferences.Current.GetPreference(nameof(ShowDocumentName), false);
-
-    public bool ShowDocumentName
-    {
-        get => showDocumentName;
-        set
-        {
-            if (showDocumentName != value)
-            {
-                showDocumentName = value;
-                UpdatePresence(currentDocument);
-            }
-        }
-    }
-
-    private bool showDocumentSize = IPreferences.Current.GetPreference(nameof(ShowDocumentSize), true);
-
-    public bool ShowDocumentSize
-    {
-        get => showDocumentSize;
-        set
-        {
-            if (showDocumentSize != value)
-            {
-                showDocumentSize = value;
-                UpdatePresence(currentDocument);
-            }
-        }
-    }
-
-    private bool showLayerCount = IPreferences.Current.GetPreference(nameof(ShowLayerCount), true);
-
-    public bool ShowLayerCount
-    {
-        get => showLayerCount;
-        set
-        {
-            if (showLayerCount != value)
-            {
-                showLayerCount = value;
-                UpdatePresence(currentDocument);
-            }
-        }
-    }
-
     public DiscordViewModel(ViewModelMain owner, string clientId)
     public DiscordViewModel(ViewModelMain owner, string clientId)
         : base(owner)
         : base(owner)
     {
     {
         Owner.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
         Owner.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
         this.clientId = clientId;
         this.clientId = clientId;
 
 
-        Enabled = IPreferences.Current.GetPreference("EnableRichPresence", true);
-        IPreferences.Current.AddCallback("EnableRichPresence", (_, x) => Enabled = (bool)x);
-        IPreferences.Current.AddCallback(nameof(ShowDocumentName), (_, x) => ShowDocumentName = (bool)x);
-        IPreferences.Current.AddCallback(nameof(ShowDocumentSize), (_, x) => ShowDocumentSize = (bool)x);
-        IPreferences.Current.AddCallback(nameof(ShowLayerCount), (_, x) => ShowLayerCount = (bool)x);
+        Enabled = PixiEditorSettings.Discord.EnableRichPresence.Value;
+        PixiEditorSettings.Discord.EnableRichPresence.ValueChanged += (_, value) => Enabled = value;
         AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
         AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
     }
     }
 
 
@@ -118,22 +70,22 @@ internal class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
         {
         {
             richPresence.WithTimestamps(new Timestamps(document.OpenedUTC));
             richPresence.WithTimestamps(new Timestamps(document.OpenedUTC));
 
 
-            richPresence.Details = ShowDocumentName
+            richPresence.Details = PixiEditorSettings.Discord.ShowDocumentName.Value
                 ? $"Editing {document.FileName.Limit(128)}" : "Editing an image";
                 ? $"Editing {document.FileName.Limit(128)}" : "Editing an image";
 
 
             string state = string.Empty;
             string state = string.Empty;
 
 
-            if (ShowDocumentSize)
+            if (PixiEditorSettings.Discord.ShowDocumentSize.Value)
             {
             {
                 state = $"{document.Width}x{document.Height}";
                 state = $"{document.Width}x{document.Height}";
             }
             }
 
 
-            if (ShowDocumentSize && ShowLayerCount)
+            if (PixiEditorSettings.Discord.ShowDocumentSize.Value && PixiEditorSettings.Discord.ShowLayerCount.Value)
             {
             {
                 state += ", ";
                 state += ", ";
             }
             }
 
 
-            if (ShowLayerCount)
+            if (PixiEditorSettings.Discord.ShowLayerCount.Value)
             {
             {
                 int count = CountLayers(document.StructureRoot);
                 int count = CountLayers(document.StructureRoot);
                 state += count == 1 ? "1 layer" : $"{count} layers";
                 state += count == 1 ? "1 layer" : $"{count} layers";

+ 9 - 13
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs

@@ -21,7 +21,7 @@ using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.AvaloniaUI.Views.Windows;
 using PixiEditor.AvaloniaUI.Views.Windows;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
@@ -57,7 +57,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             HasRecent = true;
             HasRecent = true;
         }
         }
 
 
-        IPreferences.Current.AddCallback(PreferencesConstants.MaxOpenedRecently, UpdateMaxRecentlyOpened);
+        PixiEditorSettings.File.MaxOpenedRecently.ValueChanged += (_, value) => UpdateMaxRecentlyOpened(value);
     }
     }
 
 
     public void AddRecentlyOpened(string path)
     public void AddRecentlyOpened(string path)
@@ -71,14 +71,14 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             RecentlyOpened.Insert(0, path);
             RecentlyOpened.Insert(0, path);
         }
         }
 
 
-        int maxCount = IPreferences.Current.GetPreference(PreferencesConstants.MaxOpenedRecently, PreferencesConstants.MaxOpenedRecentlyDefault);
+        int maxCount = PixiEditorSettings.File.MaxOpenedRecently.Value;
 
 
         while (RecentlyOpened.Count > maxCount)
         while (RecentlyOpened.Count > maxCount)
         {
         {
             RecentlyOpened.RemoveAt(RecentlyOpened.Count - 1);
             RecentlyOpened.RemoveAt(RecentlyOpened.Count - 1);
         }
         }
 
 
-        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
+        PixiEditorSettings.File.RecentlyOpened.Value = RecentlyOpened.Select(x => x.FilePath);
     }
     }
 
 
     [Command.Internal("PixiEditor.File.RemoveRecent")]
     [Command.Internal("PixiEditor.File.RemoveRecent")]
@@ -90,7 +90,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
 
 
         RecentlyOpened.Remove(path);
         RecentlyOpened.Remove(path);
-        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
+        PixiEditorSettings.File.RecentlyOpened.Value = RecentlyOpened.Select(x => x.FilePath);
     }
     }
 
 
     private void OpenHelloTherePopup()
     private void OpenHelloTherePopup()
@@ -108,7 +108,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
         else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
         else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
         {
         {
-            if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
+            if (PixiEditorSettings.StartupWindow.ShowStartupWindow.Value)
             {
             {
                 OpenHelloTherePopup();
                 OpenHelloTherePopup();
             }
             }
@@ -123,7 +123,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
         {
             NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
             NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
             RecentlyOpened.Remove(path);
             RecentlyOpened.Remove(path);
-            IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
+            PixiEditorSettings.File.RecentlyOpened.Value = RecentlyOpened.Select(x => x.FilePath);
             return;
             return;
         }
         }
 
 
@@ -399,10 +399,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
-    private void UpdateMaxRecentlyOpened(string name, object parameter)
+    private void UpdateMaxRecentlyOpened(int newAmount)
     {
     {
-        int newAmount = (int)parameter;
-
         if (newAmount >= RecentlyOpened.Count)
         if (newAmount >= RecentlyOpened.Count)
         {
         {
             return;
             return;
@@ -420,9 +418,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
 
     private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
     private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
     {
     {
-        IEnumerable<string> paths = IPreferences.Current.GetLocalPreference(nameof(RecentlyOpened), new JArray()).ToObject<string[]>()
-            .Take(IPreferences.Current.GetPreference(PreferencesConstants.MaxOpenedRecently, 8));
-
+        var paths = PixiEditorSettings.File.RecentlyOpened.Value.Take(PixiEditorSettings.File.MaxOpenedRecently.Value);
         List<RecentlyOpenedDocument> documents = new List<RecentlyOpenedDocument>();
         List<RecentlyOpenedDocument> documents = new List<RecentlyOpenedDocument>();
 
 
         foreach (string path in paths)
         foreach (string path in paths)

+ 1 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/IoViewModel.cs

@@ -14,6 +14,7 @@ using PixiEditor.AvaloniaUI.Models.Preferences;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;

+ 3 - 3
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/StylusViewModel.cs

@@ -1,7 +1,7 @@
 using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.ViewModels.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 
 
@@ -23,7 +23,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
         {
         {
             if (SetProperty(ref isPenModeEnabled, value))
             if (SetProperty(ref isPenModeEnabled, value))
             {
             {
-                IPreferences.Current.UpdateLocalPreference(nameof(IsPenModeEnabled), value);
+                PixiEditorSettings.Tools.IsPenModeEnabled.Value = value;
                 UpdateUseTouchGesture();
                 UpdateUseTouchGesture();
             }
             }
         }
         }
@@ -40,7 +40,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
     public StylusViewModel(ViewModelMain owner)
     public StylusViewModel(ViewModelMain owner)
         : base(owner)
         : base(owner)
     {
     {
-        isPenModeEnabled = IPreferences.Current.GetLocalPreference<bool>(nameof(IsPenModeEnabled));
+        isPenModeEnabled = PixiEditorSettings.Tools.IsPenModeEnabled.Value;
         Owner.ToolsSubViewModel.AddPropertyChangedCallback(nameof(ToolsViewModel.ActiveTool), UpdateUseTouchGesture);
         Owner.ToolsSubViewModel.AddPropertyChangedCallback(nameof(ToolsViewModel.ActiveTool), UpdateUseTouchGesture);
 
 
         UpdateUseTouchGesture();
         UpdateUseTouchGesture();

+ 5 - 6
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -14,7 +14,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
@@ -22,7 +22,7 @@ namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 [Command.Group("PixiEditor.Tools", "TOOLS")]
 [Command.Group("PixiEditor.Tools", "TOOLS")]
 internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 {
 {
-    private RightClickMode rightClickMode;
+    private RightClickMode rightClickMode = PixiEditorSettings.Tools.RightClickMode.Value;
     public ZoomToolViewModel? ZoomTool => GetTool<ZoomToolViewModel>();
     public ZoomToolViewModel? ZoomTool => GetTool<ZoomToolViewModel>();
 
 
     public IToolHandler? LastActionTool { get; private set; }
     public IToolHandler? LastActionTool { get; private set; }
@@ -34,14 +34,14 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         {
         {
             if (SetProperty(ref rightClickMode, value))
             if (SetProperty(ref rightClickMode, value))
             {
             {
-                IPreferences.Current.UpdatePreference(nameof(RightClickMode), value);
+                PixiEditorSettings.Tools.RightClickMode.Value = value;
             }
             }
         }
         }
     }
     }
 
 
     public bool EnableSharedToolbar
     public bool EnableSharedToolbar
     {
     {
-        get => IPreferences.Current.GetPreference<bool>(nameof(EnableSharedToolbar));
+        get => PixiEditorSettings.Tools.EnableSharedToolbar.Value;
         set
         set
         {
         {
             if (EnableSharedToolbar == value)
             if (EnableSharedToolbar == value)
@@ -49,7 +49,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
                 return;
                 return;
             }
             }
 
 
-            IPreferences.Current.UpdatePreference(nameof(EnableSharedToolbar), value);
+            PixiEditorSettings.Tools.EnableSharedToolbar.Value = value;
             OnPropertyChanged(nameof(EnableSharedToolbar));
             OnPropertyChanged(nameof(EnableSharedToolbar));
         }
         }
     }
     }
@@ -92,7 +92,6 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     public ToolsViewModel(ViewModelMain owner)
     public ToolsViewModel(ViewModelMain owner)
         : base(owner)
         : base(owner)
     {
     {
-        rightClickMode = IPreferences.Current.GetPreference<RightClickMode>(nameof(RightClickMode));
     }
     }
 
 
     public void SetupTools(IServiceProvider services)
     public void SetupTools(IServiceProvider services)

+ 50 - 43
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -11,7 +11,7 @@ using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Platform;
 using PixiEditor.Platform;
 using PixiEditor.UpdateModule;
 using PixiEditor.UpdateModule;
 
 
@@ -55,15 +55,15 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         : base(owner)
         : base(owner)
     {
     {
         Owner.OnStartupEvent += Owner_OnStartupEvent;
         Owner.OnStartupEvent += Owner_OnStartupEvent;
-        IPreferences.Current.AddCallback<string>("UpdateChannel", (_, val) =>
+        PixiEditorSettings.Update.UpdateChannel.ValueChanged += (_, value) =>
         {
         {
             string prevChannel = UpdateChecker.Channel.ApiUrl;
             string prevChannel = UpdateChecker.Channel.ApiUrl;
-            UpdateChecker.Channel = GetUpdateChannel(val);
+            UpdateChecker.Channel = GetUpdateChannel(value);
             if (prevChannel != UpdateChecker.Channel.ApiUrl)
             if (prevChannel != UpdateChecker.Channel.ApiUrl)
             {
             {
                 ConditionalUPDATE();
                 ConditionalUPDATE();
             }
             }
-        });
+        };
         InitUpdateChecker();
         InitUpdateChecker();
     }
     }
 
 
@@ -111,45 +111,52 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     private void AskToInstall()
     private void AskToInstall()
     {
     {
 #if RELEASE || DEVRELEASE
 #if RELEASE || DEVRELEASE
-            if (IPreferences.Current.GetPreference("CheckUpdatesOnStartup", true))
-            {
-                string dir = AppDomain.CurrentDomain.BaseDirectory;
-                
-                UpdateDownloader.CreateTempDirectory();
-                if(UpdateChecker.LatestReleaseInfo == null || string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
-                bool updateFileExists = File.Exists(
-                    Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
-                string exePath = Path.Join(UpdateDownloader.DownloadLocation,
-                    $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
-
-                bool updateExeExists = File.Exists(exePath);
-
-                if (updateExeExists && !UpdateChecker.VersionDifferent(UpdateChecker.LatestReleaseInfo.TagName, UpdateChecker.CurrentVersionTag))
-                {
-                    File.Delete(exePath);
-                    updateExeExists = false;
-                }
+        if (!PixiEditorSettings.Update.CheckUpdatesOnStartup.Value)
+        {
+            return;
+        }
 
 
-                string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
+        string dir = AppDomain.CurrentDomain.BaseDirectory;
 
 
-                if (updateFileExists || updateExeExists)
-                {
-                    ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
-                    var result = ConfirmationDialog.Show("UPDATE_READY", "NEW_UPDATE");
-                    result.Wait();
-                    if (result.Result == ConfirmationType.Yes)
-                    {
-                        if (updateFileExists && File.Exists(updaterPath))
-                        {
-                            InstallHeadless(updaterPath);
-                        }
-                        else if (updateExeExists)
-                        {
-                            OpenExeInstaller(exePath);
-                        }
-                    }
-                }
-            }
+        UpdateDownloader.CreateTempDirectory();
+        if(UpdateChecker.LatestReleaseInfo == null || string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
+        bool updateFileExists = File.Exists(
+            Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
+        string exePath = Path.Join(UpdateDownloader.DownloadLocation,
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
+
+        bool updateExeExists = File.Exists(exePath);
+
+        if (updateExeExists && !UpdateChecker.VersionDifferent(UpdateChecker.LatestReleaseInfo.TagName, UpdateChecker.CurrentVersionTag))
+        {
+            File.Delete(exePath);
+            updateExeExists = false;
+        }
+
+        string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
+
+        if (!updateFileExists && !updateExeExists)
+        {
+            return;
+        }
+
+        ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
+        var result = ConfirmationDialog.Show("UPDATE_READY", "NEW_UPDATE");
+        result.Wait();
+        
+        if (result.Result != ConfirmationType.Yes)
+        {
+            return;
+        }
+
+        if (updateFileExists && File.Exists(updaterPath))
+        {
+            InstallHeadless(updaterPath);
+        }
+        else if (updateExeExists)
+        {
+            OpenExeInstaller(exePath);
+        }
 #endif
 #endif
     }
     }
 
 
@@ -217,7 +224,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     [Conditional("UPDATE")]
     [Conditional("UPDATE")]
     private async void ConditionalUPDATE()
     private async void ConditionalUPDATE()
     {
     {
-        if (IPreferences.Current.GetPreference("CheckUpdatesOnStartup", true))
+        if (PixiEditorSettings.Update.CheckUpdatesOnStartup.Value)
         {
         {
             try
             try
             {
             {
@@ -247,7 +254,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         UpdateChannels.Add(new UpdateChannel(platformName, "", ""));
         UpdateChannels.Add(new UpdateChannel(platformName, "", ""));
 #endif
 #endif
 
 
-        string updateChannel = IPreferences.Current.GetPreference<string>("UpdateChannel");
+        string updateChannel = PixiEditorSettings.Update.UpdateChannel.Value;
 
 
         string version = VersionHelpers.GetCurrentAssemblyVersionString();
         string version = VersionHelpers.GetCurrentAssemblyVersionString();
         UpdateChecker = new UpdateChecker(version, GetUpdateChannel(updateChannel));
         UpdateChecker = new UpdateChecker(version, GetUpdateChannel(updateChannel));

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -8,7 +8,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.AvaloniaUI.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.AvaloniaUI.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Tools.Tools
 namespace PixiEditor.AvaloniaUI.ViewModels.Tools.Tools
@@ -58,7 +58,7 @@ namespace PixiEditor.AvaloniaUI.ViewModels.Tools.Tools
                 setting.Value = 1;
                 setting.Value = 1;
             }
             }
             
             
-            if (!IPreferences.Current.GetPreference<bool>("EnableSharedToolbar"))
+            if (!PixiEditorSettings.Tools.EnableSharedToolbar.Value)
             {
             {
                 return;
                 return;
             }
             }

+ 3 - 3
src/PixiEditor.AvaloniaUI/Views/Dialogs/Debugging/Localization/LocalizationDataContext.cs

@@ -16,7 +16,7 @@ using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.ViewModels;
 using PixiEditor.AvaloniaUI.ViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Dialogs.Debugging.Localization;
 namespace PixiEditor.AvaloniaUI.Views.Dialogs.Debugging.Localization;
@@ -40,7 +40,7 @@ internal class LocalizationDataContext : PixiObservableObject
         {
         {
             if (SetProperty(ref apiKey, value))
             if (SetProperty(ref apiKey, value))
             {
             {
-                IPreferences.Current.UpdateLocalPreference("POEditor_API_Key", apiKey);
+                PixiEditorSettings.Debug.PoEditorApiKey.Value = value;
             }
             }
         }
         }
     }
     }
@@ -76,7 +76,7 @@ internal class LocalizationDataContext : PixiObservableObject
     public LocalizationDataContext()
     public LocalizationDataContext()
     {
     {
         dispatcher = Dispatcher.UIThread;
         dispatcher = Dispatcher.UIThread;
-        apiKey = IPreferences.Current.GetLocalPreference<string>("POEditor_API_Key");
+        apiKey = PixiEditorSettings.Debug.PoEditorApiKey.Value;
         LoadApiKeyCommand = new RelayCommand(LoadApiKey, () => !string.IsNullOrWhiteSpace(apiKey));
         LoadApiKeyCommand = new RelayCommand(LoadApiKey, () => !string.IsNullOrWhiteSpace(apiKey));
         ApplyLanguageCommand =
         ApplyLanguageCommand =
             new RelayCommand(ApplyLanguage, () => loggedIn && SelectedLanguage != null);
             new RelayCommand(ApplyLanguage, () => loggedIn && SelectedLanguage != null);

+ 4 - 4
src/PixiEditor.AvaloniaUI/Views/Dialogs/NewFileDialog.cs

@@ -2,15 +2,15 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
 using PixiEditor.AvaloniaUI.Models;
 using PixiEditor.AvaloniaUI.Models;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Dialogs;
 namespace PixiEditor.AvaloniaUI.Views.Dialogs;
 
 
 internal class NewFileDialog : CustomDialog
 internal class NewFileDialog : CustomDialog
 {
 {
-    private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
-
-    private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
+    private int width = PixiEditorSettings.File.DefaultNewFileWidth.Value;
+    
+    private int height = PixiEditorSettings.File.DefaultNewFileHeight.Value;
 
 
     public int Width
     public int Width
     {
     {

+ 1 - 0
src/PixiEditor.AvaloniaUI/Views/Dock/DocumentTemplate.axaml.cs

@@ -7,6 +7,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Dock;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.Views.Palettes;
 using PixiEditor.AvaloniaUI.Views.Palettes;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Dock;
 namespace PixiEditor.AvaloniaUI.Views.Dock;
 
 

+ 4 - 4
src/PixiEditor.AvaloniaUI/Views/Windows/HelloTherePopup.axaml.cs

@@ -12,7 +12,7 @@ using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.Models.UserData;
 using PixiEditor.AvaloniaUI.Models.UserData;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.AvaloniaUI.Views.Dialogs;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Windows;
 namespace PixiEditor.AvaloniaUI.Views.Windows;
@@ -106,7 +106,7 @@ internal partial class HelloTherePopup : PixiEditorPopup
         RecentlyOpenedEmpty = RecentlyOpened.Count == 0;
         RecentlyOpenedEmpty = RecentlyOpened.Count == 0;
         RecentlyOpened.CollectionChanged += RecentlyOpened_CollectionChanged;
         RecentlyOpened.CollectionChanged += RecentlyOpened_CollectionChanged;
 
 
-        _newsDisabled = IPreferences.Current.GetPreference<bool>(PreferencesConstants.DisableNewsPanel);
+        _newsDisabled = PixiEditorSettings.StartupWindow.DisableNewsPanel.Value;
 
 
         NewsProvider = new NewsProvider();
         NewsProvider = new NewsProvider();
 
 
@@ -116,7 +116,7 @@ internal partial class HelloTherePopup : PixiEditorPopup
 
 
         int newsWidth = 300;
         int newsWidth = 300;
 
 
-        NewsPanelCollapsed = IPreferences.Current.GetPreference<bool>(PreferencesConstants.NewsPanelCollapsed);
+        NewsPanelCollapsed = PixiEditorSettings.StartupWindow.NewsPanelCollapsed.Value;
 
 
         if (_newsDisabled || NewsPanelCollapsed)
         if (_newsDisabled || NewsPanelCollapsed)
         {
         {
@@ -159,7 +159,7 @@ internal partial class HelloTherePopup : PixiEditorPopup
             Enumerable.Last<ColumnDefinition>(helloTherePopup.grid.ColumnDefinitions).Width = new GridLength(300);
             Enumerable.Last<ColumnDefinition>(helloTherePopup.grid.ColumnDefinitions).Width = new GridLength(300);
         }
         }
 
 
-        IPreferences.Current.UpdatePreference(PreferencesConstants.NewsPanelCollapsed, e.NewValue.Value);
+        PixiEditorSettings.StartupWindow.NewsPanelCollapsed.Value = e.NewValue.Value;
     }
     }
 
 
     private void RecentlyOpened_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
     private void RecentlyOpened_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)

+ 15 - 18
src/PixiEditor.AvaloniaUI/Views/Windows/PalettesBrowser.axaml.cs

@@ -25,7 +25,8 @@ using PixiEditor.AvaloniaUI.Views.Palettes;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
-using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 using PaletteColor = PixiEditor.Extensions.CommonApi.Palettes.PaletteColor;
 using PaletteColor = PixiEditor.Extensions.CommonApi.Palettes.PaletteColor;
@@ -151,7 +152,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
 
 
     public FilteringSettings Filtering => filteringSettings ??=
     public FilteringSettings Filtering => filteringSettings ??=
         new FilteringSettings(ColorsNumberMode, ColorsNumber, NameFilter, ShowOnlyFavourites,
         new FilteringSettings(ColorsNumberMode, ColorsNumber, NameFilter, ShowOnlyFavourites,
-            IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes, new List<string>()));
+            PixiEditorSettings.Palettes.FavouritePalettes.AsList());
 
 
     private char[] separators = new char[] { ' ', ',' };
     private char[] separators = new char[] { ' ', ',' };
 
 
@@ -208,7 +209,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
             LocalPalettesFetcher.CacheUpdated -= LocalCacheRefreshed;
             LocalPalettesFetcher.CacheUpdated -= LocalCacheRefreshed;
         };
         };
 
 
-        IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, OnFavouritePalettesChanged);
+        PixiEditorSettings.Palettes.FavouritePalettes.ValueChanged += OnFavouritePalettesChanged;
     }
     }
 
 
     public async Task<bool?> ShowDialog()
     public async Task<bool?> ShowDialog()
@@ -246,10 +247,9 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
         return palette != null && palette.Source.GetType() == typeof(LocalPalettesFetcher);
         return palette != null && palette.Source.GetType() == typeof(LocalPalettesFetcher);
     }
     }
 
 
-    private void OnFavouritePalettesChanged(string preferenceName, object value)
+    private void OnFavouritePalettesChanged(Setting<IEnumerable<string>> setting, IEnumerable<string> value)
     {
     {
-        Filtering.Favourites =
-            IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
+        Filtering.Favourites = PixiEditorSettings.Palettes.FavouritePalettes.AsList();
     }
     }
 
 
     public static PalettesBrowser Open()
     public static PalettesBrowser Open()
@@ -354,7 +354,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
     private async void ToggleFavourite(Palette palette)
     private async void ToggleFavourite(Palette palette)
     {
     {
         palette.IsFavourite = !palette.IsFavourite;
         palette.IsFavourite = !palette.IsFavourite;
-        var favouritePalettes = IPreferences.Current.GetLocalPreference(PreferencesConstants.FavouritePalettes, new List<string>());
+        var favouritePalettes = PixiEditorSettings.Palettes.FavouritePalettes.AsList();
 
 
         if (palette.IsFavourite && !favouritePalettes.Contains(palette.Name))
         if (palette.IsFavourite && !favouritePalettes.Contains(palette.Name))
         {
         {
@@ -365,13 +365,13 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
             favouritePalettes.RemoveAll(x => x == palette.Name);
             favouritePalettes.RemoveAll(x => x == palette.Name);
         }
         }
 
 
-        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favouritePalettes);
+        PixiEditorSettings.Palettes.FavouritePalettes.Value = favouritePalettes;
         await UpdatePaletteList();
         await UpdatePaletteList();
     }
     }
 
 
     private bool IsPaletteFavourite(string name)
     private bool IsPaletteFavourite(string name)
     {
     {
-        var favouritePalettes = IPreferences.Current.GetLocalPreference(PreferencesConstants.FavouritePalettes, new List<string>());
+        var favouritePalettes = PixiEditorSettings.Palettes.FavouritePalettes.AsList();
         return favouritePalettes.Contains(name);
         return favouritePalettes.Contains(name);
     }
     }
 
 
@@ -392,12 +392,11 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
 
 
     private static void RemoveFavouritePalette(Palette palette)
     private static void RemoveFavouritePalette(Palette palette)
     {
     {
-        var favouritePalettes =
-            IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
-        if (favouritePalettes != null && favouritePalettes.Contains(palette.Name))
+        var favouritePalettes = PixiEditorSettings.Palettes.FavouritePalettes.AsList();
+        if (favouritePalettes.Contains(palette.Name))
         {
         {
             favouritePalettes.Remove(palette.Name);
             favouritePalettes.Remove(palette.Name);
-            IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favouritePalettes);
+            PixiEditorSettings.Palettes.FavouritePalettes.Value = favouritePalettes;
         }
         }
     }
     }
 
 
@@ -631,9 +630,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
 
 
     private static void UpdateRenamedFavourite(string old, string newName)
     private static void UpdateRenamedFavourite(string old, string newName)
     {
     {
-        var favourites = IPreferences.Current.GetLocalPreference(
-            PreferencesConstants.FavouritePalettes,
-            new List<string>());
+        var favourites = PixiEditorSettings.Palettes.FavouritePalettes.AsList();
 
 
         if (favourites.Contains(old))
         if (favourites.Contains(old))
         {
         {
@@ -641,7 +638,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
             favourites.Add(newName);
             favourites.Add(newName);
         }
         }
 
 
-        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favourites);
+        PixiEditorSettings.Palettes.FavouritePalettes.Value = favourites;
     }
     }
 
 
     private void BrowseOnLospec_OnClick(object sender, RoutedEventArgs e)
     private void BrowseOnLospec_OnClick(object sender, RoutedEventArgs e)
@@ -683,6 +680,6 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
     protected override void OnClosing(WindowClosingEventArgs e)
     protected override void OnClosing(WindowClosingEventArgs e)
     {
     {
         base.OnClosing(e);
         base.OnClosing(e);
-        IPreferences.Current.RemoveCallback(PreferencesConstants.FavouritePalettes, OnFavouritePalettesChanged);
+        PixiEditorSettings.Palettes.FavouritePalettes.ValueChanged -= OnFavouritePalettesChanged;
     }
     }
 }
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Windows/Settings/SettingsWindow.axaml

@@ -13,7 +13,7 @@
     xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
     xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
     xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
     xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
     xmlns:markupExtensions="clr-namespace:PixiEditor.AvaloniaUI.Helpers.MarkupExtensions"
     xmlns:markupExtensions="clr-namespace:PixiEditor.AvaloniaUI.Helpers.MarkupExtensions"
-    xmlns:preferences="clr-namespace:PixiEditor.AvaloniaUI.Models.Preferences"
+    xmlns:preferences="clr-namespace:PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;assembly=PixiEditor.Extensions.CommonApi"
     xmlns:dialogs="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
     xmlns:dialogs="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
     xmlns:settings="clr-namespace:PixiEditor.AvaloniaUI.Views.Windows.Settings"
     xmlns:settings="clr-namespace:PixiEditor.AvaloniaUI.Views.Windows.Settings"
     mc:Ignorable="d"
     mc:Ignorable="d"

+ 11 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/DiagnosticConstants.cs

@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics;
+
+internal static class DiagnosticConstants
+{
+    public const string Category = "PixiEditor.CommonAPI";
+    
+    public const string SettingNamespace = "PixiEditor.Extensions.CommonApi.UserPreferences.Settings";
+    public static List<string> settingNames = ["SyncedSetting", "LocalSetting"];
+}

+ 22 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/DiagnosticHelpers.cs

@@ -0,0 +1,22 @@
+using Microsoft.CodeAnalysis;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics;
+
+internal static class DiagnosticHelpers
+{
+    public static bool IsSettingType(TypeInfo info) => info.Type?.ContainingNamespace.ToString() == DiagnosticConstants.SettingNamespace && DiagnosticConstants.settingNames.Contains(info.Type.Name);
+
+    public static string? GetPrefix(string name)
+    {
+        int colonPosition = name.IndexOf(':');
+
+        return colonPosition == -1 ? null : name.Substring(0, colonPosition);
+    }
+    
+    public static string? GetKey(string name)
+    {
+        int colonPosition = name.IndexOf(':');
+
+        return colonPosition == -1 ? null : name.Substring(colonPosition + 1);
+    }
+}

+ 94 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseGenericEnumerableForListArrayDiagnostic.cs

@@ -0,0 +1,94 @@
+using System.Collections.Immutable;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class UseGenericEnumerableForListArrayDiagnostic : DiagnosticAnalyzer
+{
+    private const string ListNamespace = "System.Collections.Generic";
+    private const string ListName = "List";
+    
+    public const string DiagnosticId = "UseGenericEnumerableForListArray";
+    
+    public static DiagnosticDescriptor UseGenericEnumerableForListArrayDescriptor { get; } =
+        new(DiagnosticId, "Use IEnumerable<T> in Setting instead of List/Array",
+            "Use IEnumerable<{0}> instead of {1} to allow passing any IEnumerable<{0}> for the value. Use the {2} extension from PixiEditor.Extensions.CommonApi.UserPreferences.Settings to access the Setting as a {1}.",
+            "PixiEditor.CommonAPI", DiagnosticSeverity.Info, true);
+
+    public override void Initialize(AnalysisContext context)
+    {
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+        context.EnableConcurrentExecution();
+        context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.GenericName);
+    }
+
+    private void AnalyzeNode(SyntaxNodeAnalysisContext context)
+    {
+        var semanticModel = context.SemanticModel;
+        
+        var name = (GenericNameSyntax)context.Node;
+        var typeInfo = semanticModel.GetTypeInfo(name, context.CancellationToken);
+
+        if (!DiagnosticHelpers.IsSettingType(typeInfo))
+        {
+            return;
+        }
+        
+        var typeArgument = name.TypeArgumentList.Arguments.First();
+
+        var isArrayOrList = GetInfo(context.SemanticModel, typeArgument, out var targetTypeName, out var extensionMethod, context.CancellationToken);
+        
+        if (!isArrayOrList)
+        {
+            return;
+        }
+
+        var diagnostic = Diagnostic.Create(
+            UseGenericEnumerableForListArrayDescriptor,
+            name.GetLocation(),
+            targetTypeName,
+            typeArgument.ToString(),
+            extensionMethod
+        );
+        
+        context.ReportDiagnostic(diagnostic);
+    }
+
+    private static bool GetInfo(SemanticModel semanticModel, TypeSyntax typeArgument, out string? targetTypeName, out string? extensionMethod, CancellationToken cancellationToken = default)
+    {
+        bool isArrayOrList = false;
+        targetTypeName = null;
+        extensionMethod = null;
+        
+        if (typeArgument is ArrayTypeSyntax array)
+        {
+            isArrayOrList = true;
+            targetTypeName = array.ElementType.ToString();
+            extensionMethod = ".AsArray()";
+        }
+        else if (typeArgument is GenericNameSyntax genericName)
+        {
+            var argumentSymbol = semanticModel.GetTypeInfo(typeArgument, cancellationToken);
+
+            if (argumentSymbol.Type?.ContainingNamespace.ToString() != ListNamespace ||
+                argumentSymbol.Type?.Name != ListName)
+            {
+                return isArrayOrList;
+            }
+
+            extensionMethod = ".AsList()";
+            isArrayOrList = true;
+            targetTypeName = genericName.TypeArgumentList.Arguments.First().ToString();
+        }
+
+        return isArrayOrList;
+    }
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
+        [UseGenericEnumerableForListArrayDescriptor];
+}

+ 91 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseNonOwnedDiagnostic.cs

@@ -0,0 +1,91 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class UseNonOwnedDiagnostic : DiagnosticAnalyzer
+{
+    public const string DiagnosticId = "UseNonOwned";
+
+    public const string Title = "Use .NonOwned() method";
+    
+    public static readonly DiagnosticDescriptor Descriptor = new(DiagnosticId, "Use .NonOwned() method",
+        "Use {0}.NonOwned{1}() to declare a Setting using the property name that is owned by another extension", DiagnosticConstants.Category,
+        DiagnosticSeverity.Info, true);
+    
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
+        [Descriptor];
+    
+    public override void Initialize(AnalysisContext context)
+    {
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+        context.EnableConcurrentExecution();
+        context.RegisterSyntaxNodeAction(AnalyzeDeclaration, SyntaxKind.PropertyDeclaration);
+    }
+
+    private static void AnalyzeDeclaration(SyntaxNodeAnalysisContext context)
+    {
+        var semantics = context.SemanticModel;
+        var declaration = (PropertyDeclarationSyntax)context.Node;
+
+        var typeInfo = ModelExtensions.GetTypeInfo(semantics, declaration.Type, context.CancellationToken);
+
+        if (!DiagnosticHelpers.IsSettingType(typeInfo))
+        {
+            return;
+        }
+        
+        if (declaration.Initializer is not { Value: BaseObjectCreationExpressionSyntax { ArgumentList.Arguments: { Count: > 0 } arguments } initializerExpression } ||
+            DoesNotMatch(semantics, arguments, declaration))
+        {
+            return;
+        }
+        
+        var diagnostic = GetDiagnostic(arguments, declaration, semantics, initializerExpression, typeInfo);
+
+        context.ReportDiagnostic(diagnostic);
+    }
+
+    private static Diagnostic GetDiagnostic(SeparatedSyntaxList<ArgumentSyntax> arguments, PropertyDeclarationSyntax declaration,
+        SemanticModel semantics, BaseObjectCreationExpressionSyntax initializerExpression, TypeInfo typeInfo)
+    {
+        var genericType = string.Empty;
+
+        var fallbackValueArgument = arguments.Skip(1).FirstOrDefault();
+
+        var settingType = ((GenericNameSyntax)declaration.Type).TypeArgumentList.Arguments.First();
+        if (fallbackValueArgument == null || !SymbolEqualityComparer.Default.Equals(
+                ModelExtensions.GetTypeInfo(semantics, fallbackValueArgument.Expression).Type,
+                ModelExtensions.GetTypeInfo(semantics, settingType).Type))
+        {
+            genericType = $"<{settingType}>";
+        }
+
+        var diagnostic = Diagnostic.Create(Descriptor, initializerExpression.GetLocation(),
+            typeInfo.Type?.Name, // LocalSetting or Synced Setting
+            genericType);
+        return diagnostic;
+    }
+
+    private static bool DoesNotMatch(SemanticModel semantics, SeparatedSyntaxList<ArgumentSyntax> arguments,
+        PropertyDeclarationSyntax declaration)
+    {
+        var nameArgument = arguments.First();
+
+        var operation = semantics.GetOperation(nameArgument.Expression);
+
+        if (operation?.ConstantValue.Value is not string constant || DiagnosticHelpers.GetKey(constant) is not { } key)
+        {
+            return true;
+        }
+        
+        var declarationName = declaration.Identifier.ValueText;
+        
+        return key != declarationName;
+    }
+}

+ 84 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseOwnedDiagnostic.cs

@@ -0,0 +1,84 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class UseOwnedDiagnostic : DiagnosticAnalyzer
+{
+    public const string DiagnosticId = "UseOwned";
+
+    public const string Title = "Use .Owned() method";
+
+    public static readonly DiagnosticDescriptor Descriptor = new(DiagnosticId, Title,
+        "Use {0}.Owned{1}() to declare a Setting using the property name", DiagnosticConstants.Category,
+        DiagnosticSeverity.Info, true);
+    
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
+        [Descriptor];
+    
+    public override void Initialize(AnalysisContext context)
+    {
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+        context.EnableConcurrentExecution();
+        context.RegisterSyntaxNodeAction(AnalyzeDeclaration, SyntaxKind.PropertyDeclaration);
+    }
+
+    private static void AnalyzeDeclaration(SyntaxNodeAnalysisContext context)
+    {
+        var semantics = context.SemanticModel;
+        var declaration = (PropertyDeclarationSyntax)context.Node;
+
+        var typeInfo = semantics.GetTypeInfo(declaration.Type, context.CancellationToken);
+
+        if (!DiagnosticHelpers.IsSettingType(typeInfo))
+        {
+            return;
+        }
+        
+        // TODO: Also handle => new()
+        if (declaration.Initializer is not { Value: BaseObjectCreationExpressionSyntax { ArgumentList.Arguments: { Count: > 0 } arguments } initializerExpression } ||
+            DoesNotMatch(semantics, arguments, declaration))
+        {
+            return;
+        }
+
+        var diagnostic = GetDiagnostic(arguments, declaration, semantics, initializerExpression, typeInfo);
+        context.ReportDiagnostic(diagnostic);
+    }
+
+    private static Diagnostic GetDiagnostic(SeparatedSyntaxList<ArgumentSyntax> arguments, PropertyDeclarationSyntax declaration,
+        SemanticModel semantics, BaseObjectCreationExpressionSyntax initializerExpression, TypeInfo typeInfo)
+    {
+        var genericType = string.Empty;
+
+        var fallbackValueArgument = arguments.Skip(1).FirstOrDefault();
+
+        var settingType = ((GenericNameSyntax)declaration.Type).TypeArgumentList.Arguments.First();
+        if (fallbackValueArgument == null || !SymbolEqualityComparer.Default.Equals(
+                semantics.GetTypeInfo(fallbackValueArgument.Expression).Type,
+                semantics.GetTypeInfo(settingType).Type))
+        {
+            genericType = $"<{settingType}>";
+        }
+
+        var diagnostic = Diagnostic.Create(Descriptor, initializerExpression.GetLocation(),
+            typeInfo.Type?.Name, // LocalSetting or Synced Setting
+            genericType);
+        return diagnostic;
+    }
+
+    private static bool DoesNotMatch(SemanticModel semantics, SeparatedSyntaxList<ArgumentSyntax> arguments, PropertyDeclarationSyntax declaration)
+    {
+        var nameArgument = arguments.First();
+        var operation = semantics.GetOperation(nameArgument.Expression);
+        var constant = operation?.ConstantValue.Value as string;
+        var declarationName = declaration.Identifier.ValueText;
+
+        return constant != declarationName;
+    }
+}

+ 83 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseGenericEnumerableForListArrayFix.cs

@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Fixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseGenericEnumerableForListArrayFix))]
+[Shared]
+public class UseGenericEnumerableForListArrayFix : CodeFixProvider
+{
+    public override ImmutableArray<string> FixableDiagnosticIds { get; } =
+        [UseGenericEnumerableForListArrayDiagnostic.DiagnosticId];
+    
+    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+    {
+        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+        var diagnostic = context.Diagnostics.First();
+        var diagnosticSpan = diagnostic.Location.SourceSpan;
+        
+        var syntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<GenericNameSyntax>().First();
+
+        var typeArgument = syntax.TypeArgumentList.Arguments.First();
+        
+        var title = $"Use IEnumerable<{GetTargetTypeName(typeArgument)}>";
+        // TODO: equivalenceKey only works for types with the same name. Is there some way to make this generic?
+        var action = CodeAction.Create(title, c => CreateChangedDocument(context.Document, syntax, c), title);
+        
+        context.RegisterCodeFix(action, diagnostic);
+    }
+
+    public override FixAllProvider GetFixAllProvider()
+    {
+        return WellKnownFixAllProviders.BatchFixer;
+    }
+
+    private static async Task<Document> CreateChangedDocument(Document document, GenericNameSyntax syntax, CancellationToken token)
+    {
+        var typeArgument = syntax.TypeArgumentList.Arguments.First();
+        var genericType = (TypeSyntax)GetNewGenericType(typeArgument);
+
+        var typeList = SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList([genericType]));
+        var replacement = SyntaxFactory.GenericName(syntax.Identifier, typeList);
+        
+        var root = await document.GetSyntaxRootAsync(token);
+
+        if (root == null)
+        {
+            throw new Exception("Document root was null. No code fix for you sadly :(");
+        }
+
+        var newRoot = root.ReplaceNode(syntax, replacement);
+        
+        return document.WithSyntaxRoot(newRoot);
+    }
+    private static GenericNameSyntax GetNewGenericType(TypeSyntax typeSyntax)
+    {
+        var targetTypeName = GetTargetTypeName(typeSyntax);
+        
+        var identifierToken = SyntaxFactory.Identifier("IEnumerable");
+        var separatedList = SyntaxFactory.SeparatedList([SyntaxFactory.ParseTypeName(targetTypeName)]);
+        var typeList = SyntaxFactory.TypeArgumentList(separatedList);
+        
+        return SyntaxFactory.GenericName(identifierToken, typeList);
+    }
+
+    private static string GetTargetTypeName(TypeSyntax typeSyntax) => typeSyntax switch
+        {
+            ArrayTypeSyntax array => array.ElementType.ToString(),
+            GenericNameSyntax genericName => genericName.TypeArgumentList.Arguments.First().ToString(),
+            _ => throw new ArgumentException(
+                $"{nameof(typeSyntax)} must either be a ArrayTypeSyntax or GenericNameSyntax")
+        };
+}

+ 95 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseNonOwnedFix.cs

@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Fixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseNonOwnedFix))]
+[Shared]
+public class UseNonOwnedFix : CodeFixProvider
+{
+    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+    {
+        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+        var diagnostic = context.Diagnostics.First();
+        var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+        var syntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
+
+        var action = CodeAction.Create(UseNonOwnedDiagnostic.Title, c => CreateChangedDocument(context.Document, syntax, c), UseNonOwnedDiagnostic.Title);
+
+        context.RegisterCodeFix(action, diagnostic);
+    }
+
+    private static async Task<Document> CreateChangedDocument(Document document, PropertyDeclarationSyntax declaration,
+        CancellationToken token)
+    {
+        var invocationExpression = await GetNewInvocation(document, declaration, token);
+        var root = await document.GetSyntaxRootAsync(token);
+
+        var newRoot = root!.ReplaceNode(declaration.Initializer!.Value, invocationExpression);
+
+        // TODO: The initializer part does not have it's generic type replaced
+        return document.WithSyntaxRoot(newRoot);
+    }
+
+    private static async Task<InvocationExpressionSyntax> GetNewInvocation(Document document, PropertyDeclarationSyntax declaration,
+        CancellationToken token)
+    {
+        var settingType = (GenericNameSyntax)declaration.Type;
+        var originalInvocation = (BaseObjectCreationExpressionSyntax)declaration.Initializer!.Value;
+
+        var classIdentifier = SyntaxFactory.IdentifierName(settingType.Identifier); // Removes the <> part
+        var ownedIdentifier = SyntaxFactory.GenericName(SyntaxFactory.Identifier("NonOwned"), settingType.TypeArgumentList);
+
+        var accessExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, classIdentifier, ownedIdentifier);
+
+        var prefixArgument = await GetPrefixArgument(document, token, originalInvocation);
+
+        var arguments = SkipArgumentAndAdd(originalInvocation.ArgumentList!, prefixArgument);
+
+        var invocationExpression = SyntaxFactory.InvocationExpression(accessExpression, arguments);
+        return invocationExpression;
+    }
+
+    private static async ValueTask<ArgumentSyntax> GetPrefixArgument(Document document, CancellationToken token,
+        BaseObjectCreationExpressionSyntax originalInvocation)
+    {
+        var semantics = await document.GetSemanticModelAsync(token);
+        var originalFirstArgument = originalInvocation.ArgumentList!.Arguments.First();
+        var originalName = (string?)semantics!.GetOperation(originalFirstArgument.Expression)?.ConstantValue.Value;
+
+        if (originalName is null)
+        {
+            throw new NullReferenceException($"Could not determine original name. First argument {originalFirstArgument.ToString()}");
+        }
+        
+        var prefix = DiagnosticHelpers.GetPrefix(originalName)!;
+        var prefixLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(prefix));
+        var prefixArgument = SyntaxFactory.Argument(prefixLiteral);
+
+        return prefixArgument;
+    }
+
+    private static ArgumentListSyntax SkipArgumentAndAdd(ArgumentListSyntax original, ArgumentSyntax toPrepend)
+    {
+        var list = new SeparatedSyntaxList<ArgumentSyntax>();
+
+        list = list.Add(toPrepend);
+        list = original.Arguments.Skip(1).Aggregate(list, (current, argument) => current.Add(argument));
+
+        return SyntaxFactory.ArgumentList(list);
+    }
+
+    public override ImmutableArray<string> FixableDiagnosticIds { get; } = [UseNonOwnedDiagnostic.DiagnosticId];
+}

+ 67 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseOwnedFix.cs

@@ -0,0 +1,67 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Fixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseOwnedFix))]
+[Shared]
+public class UseOwnedFix : CodeFixProvider
+{
+    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+    {
+        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+        var diagnostic = context.Diagnostics.First();
+        var diagnosticSpan = diagnostic.Location.SourceSpan;
+        
+        var syntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
+
+        var action = CodeAction.Create(UseOwnedDiagnostic.Title, c => CreateChangedDocument(context.Document, syntax, c), UseOwnedDiagnostic.Title);
+        
+        context.RegisterCodeFix(action, diagnostic);
+    }
+
+    private static async Task<Document> CreateChangedDocument(Document document, PropertyDeclarationSyntax declaration, CancellationToken token)
+    {
+        var invocationExpression = GetNewInvocation(declaration);
+        var root = await document.GetSyntaxRootAsync(token);
+
+        var newRoot = root!.ReplaceNode(declaration.Initializer.Value, invocationExpression);
+
+        // TODO: The initializer part does not have it's generic type replaced
+        return document.WithSyntaxRoot(newRoot);
+    }
+
+    private static InvocationExpressionSyntax GetNewInvocation(PropertyDeclarationSyntax declaration)
+    {
+        var settingType = (GenericNameSyntax)declaration.Type;
+        var originalInvocation = (BaseObjectCreationExpressionSyntax)declaration.Initializer!.Value;
+        
+        var classIdentifier = SyntaxFactory.IdentifierName(settingType.Identifier); // Removes the <> part
+        var ownedIdentifier = SyntaxFactory.GenericName(SyntaxFactory.Identifier("Owned"), settingType.TypeArgumentList);
+        
+        var accessExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, classIdentifier, ownedIdentifier);
+        var invocationExpression = SyntaxFactory.InvocationExpression(accessExpression, SkipArgument(originalInvocation.ArgumentList!));
+        return invocationExpression;
+    }
+
+    private static ArgumentListSyntax SkipArgument(ArgumentListSyntax original)
+    {
+        var list = new SeparatedSyntaxList<ArgumentSyntax>();
+
+        list = original.Arguments.Skip(1).Aggregate(list, (current, argument) => current.Add(argument));
+
+        return SyntaxFactory.ArgumentList(list);
+    }
+
+    public override ImmutableArray<string> FixableDiagnosticIds { get; } = [UseOwnedDiagnostic.DiagnosticId];
+}

+ 17 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/PixiEditor.Extensions.CommonApi.Diagnostics.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <LangVersion>latest</LangVersion>
+        <Nullable>enable</Nullable>
+<!--        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>-->
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
+      <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.9.2" />
+    </ItemGroup>
+    <ItemGroup>
+      <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+    </ItemGroup>
+</Project>

+ 46 - 0
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -53,4 +53,50 @@
 
 
   </Target>
   </Target>
 
 
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Extensions.CommonApi.Diagnostics\PixiEditor.Extensions.CommonApi.Diagnostics.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+    </ItemGroup>
+  
+    <ItemGroup>
+      <PackageReference Include="protobuf-net" Version="3.2.30" />
+    </ItemGroup>
+  
+  <PropertyGroup>
+    <ProtogenVersion>3.2.42</ProtogenVersion>
+  </PropertyGroup>
+  
+  <Target Name="ProtogenCheck" BeforeTargets="GenerateProtoContracts">
+    <PropertyGroup>
+      <ProtogenExists>false</ProtogenExists>
+    </PropertyGroup>
+    <Exec Command="dotnet tool run protogen --version" IgnoreExitCode="true">
+      <Output TaskParameter="ExitCode" PropertyName="ProtogenExitCode"/>
+    </Exec>
+    <PropertyGroup>
+      <ProtogenExists Condition="'$(ProtogenExitCode)' == '0'">true</ProtogenExists>
+    </PropertyGroup>
+  </Target>
+  
+  <Target Name="InstallProtogen" BeforeTargets="GenerateProtoContracts"
+          Condition="'$(ProtogenExists)' != 'true'">
+    <Message Text="Downloading protogen v$(ProtogenVersion)..." Importance="high"/>
+    <Exec Command="dotnet tool install --local protobuf-net.Protogen --version $(ProtogenVersion)"/>
+    <PropertyGroup>
+      <ProtogenExists>true</ProtogenExists>
+    </PropertyGroup>
+    
+    <Message Text="protogen installed successfully." Importance="high"/>
+  </Target>
+    
+  
+  <Target Name="GenerateProtoContracts" BeforeTargets="BeforeCompile"
+          Inputs="$(MSBuildProjectDirectory)\DataContracts\*.proto"
+          Outputs="$(MSBuildProjectDirectory)\ProtoAutogen\*.cs">
+    <Exec Command="dotnet tool run protogen --csharp_out=ProtoAutogen --proto_path=DataContracts +listset=yes *.proto"/>
+
+    <ItemGroup>
+      <Compile Include="ProtoAutogen\*.cs" KeepDuplicates="false"/>
+    </ItemGroup>
+
+  </Target>
 </Project>
 </Project>

+ 29 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/LocalSetting.cs

@@ -0,0 +1,29 @@
+using System.Runtime.CompilerServices;
+
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+
+/// <summary>
+/// A static class for creating a LocalSetting from the property name
+/// </summary>
+public static class LocalSetting
+{
+    public static LocalSetting<T> Owned<T>(T? fallbackValue = default, [CallerMemberName] string name = "") =>
+        new(name, fallbackValue);
+    
+    public static LocalSetting<T> NonOwned<T>(string prefix, T? fallbackValue = default, [CallerMemberName] string name = "") =>
+        new($"{prefix}:{name}", fallbackValue);
+}
+
+/// <summary>
+/// A preference which will only be available on the current device
+/// </summary>
+/// <param name="name">The name of the preference</param>
+/// <param name="fallbackValue">A optional fallback value which will be used if the setting has not been set before</param>
+public class LocalSetting<T>(string name, T? fallbackValue = default) : Setting<T>(name, fallbackValue)
+{
+    protected override TAny? GetValue<TAny>(IPreferences preferences, TAny fallbackValue) where TAny : default =>
+        preferences.GetLocalPreference(Name, fallbackValue);
+
+    protected override void SetValue(IPreferences preferences, T value) =>
+        preferences.UpdateLocalPreference(Name, value);
+}

+ 67 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs

@@ -0,0 +1,67 @@
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
+
+public static class PixiEditorSettings
+{
+    private const string PixiEditor = "PixiEditor";
+    
+    public static class Palettes
+    {
+        public static LocalSetting<IEnumerable<string>> FavouritePalettes { get; } = LocalSetting.NonOwned<IEnumerable<string>>(PixiEditor);
+    }
+
+    public static class Update
+    {
+        public static SyncedSetting<bool> CheckUpdatesOnStartup { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+
+        public static SyncedSetting<string> UpdateChannel { get; } = SyncedSetting.NonOwned<string>(PixiEditor);
+    }
+
+    public static class Debug
+    {
+        public static SyncedSetting<bool> IsDebugModeEnabled { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
+
+        public static LocalSetting<string> PoEditorApiKey { get; } = new($"{PixiEditor}:POEditor_API_Key");
+    }
+    
+    public static class Tools
+    {
+        public static SyncedSetting<bool> EnableSharedToolbar { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
+
+        public static SyncedSetting<RightClickMode> RightClickMode { get; } = SyncedSetting.NonOwned<RightClickMode>(PixiEditor);
+        
+        public static SyncedSetting<bool> IsPenModeEnabled { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
+    }
+
+    public static class File
+    {
+        public static SyncedSetting<int> DefaultNewFileWidth { get; } = SyncedSetting.NonOwned(PixiEditor, 64);
+
+        public static SyncedSetting<int> DefaultNewFileHeight { get; } = SyncedSetting.NonOwned(PixiEditor, 64);
+        
+        public static LocalSetting<IEnumerable<string>> RecentlyOpened { get; } = LocalSetting.NonOwned<IEnumerable<string>>(PixiEditor);
+    
+        public static SyncedSetting<int> MaxOpenedRecently { get; } = SyncedSetting.NonOwned(PixiEditor, 8);
+    }
+    
+    public static class StartupWindow
+    {
+        public static SyncedSetting<bool> ShowStartupWindow { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+
+        public static SyncedSetting<bool> DisableNewsPanel { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
+
+        public static SyncedSetting<bool> NewsPanelCollapsed { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
+
+        public static SyncedSetting<IEnumerable<int>> LastCheckedNewsIds { get; } = SyncedSetting.NonOwned<IEnumerable<int>>(PixiEditor);
+    }
+    
+    public static class Discord
+    {
+        public static SyncedSetting<bool> EnableRichPresence { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+
+        public static SyncedSetting<bool> ShowDocumentName { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
+
+        public static SyncedSetting<bool> ShowDocumentSize { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+
+        public static SyncedSetting<bool> ShowLayerCount { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+    }
+}

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Preferences/RightClickMode.cs → src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/RightClickMode.cs

@@ -1,6 +1,6 @@
 using System.ComponentModel;
 using System.ComponentModel;
 
 
-namespace PixiEditor.AvaloniaUI.Models.Preferences;
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 
 
 public enum RightClickMode
 public enum RightClickMode
 {
 {

+ 116 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/Setting.cs

@@ -0,0 +1,116 @@
+using System.ComponentModel;
+using System.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+
+[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
+public abstract class Setting<T> : INotifyPropertyChanged
+{
+    private readonly IPreferences preferences;
+    private event PropertyChangedEventHandler PropertyChanged;
+
+    /// <summary>
+    /// The name of the preference
+    /// </summary>
+    public string Name { get; }
+
+    /// <summary>
+    /// The value of the preference
+    /// </summary>
+    public T? Value
+    {
+        get => GetValue(preferences, FallbackValue);
+        set => SetValue(preferences, value);
+    }
+
+    /// <summary>
+    /// The value used if the preference has not been set before
+    /// </summary>
+    public T? FallbackValue { get; }
+
+    /// <summary>
+    /// Called when the value of the preference has changed
+    /// </summary>
+    public event SettingChangedHandler<T> ValueChanged; 
+
+    /// <param name="name">The name of the preference</param>
+    /// <param name="fallbackValue">The value used if the preference has not been set before</param>
+    protected Setting(string name, T? fallbackValue = default)
+    {
+        SettingHelper.ThrowIfEmptySettingName(name);
+        
+        Name = name;
+        FallbackValue = fallbackValue;
+        
+        preferences = IPreferences.Current;
+        preferences.AddCallback<T>(Name, SettingChangeCallback);
+    }
+
+    /// <summary>
+    /// Gets the value of the preference or the <see cref="fallbackValue"/> if the preference has not been set before. Note: This will ignore the <see cref="FallbackValue"/> set in the setting constructor
+    /// </summary>
+    /// <param name="fallbackValue">The value used if the preference has not been set before</param>
+    /// <returns>Either the value of the preference or <see cref="fallbackValue"/></returns>
+    public T GetValueOrDefault(T fallbackValue) => GetValue(preferences, fallbackValue);
+
+    /// <summary>
+    /// Gets the value of the preference as <typeparamref name="T"/> instead of the type defined by the setting.
+    /// </summary>
+    /// <param name="fallbackValue">The value used if the preference has not been set before</param>
+    /// <returns>Either the value of the preference as <typeparamref name="T"/> or <see cref="fallbackValue"/></returns>
+    public TAny? As<TAny>(TAny? fallbackValue = default) => GetValue(preferences, fallbackValue);
+
+    protected abstract TAny? GetValue<TAny>(IPreferences preferences, TAny fallbackValue);
+
+    protected abstract void SetValue(IPreferences preferences, T? value);
+
+    private void SettingChangeCallback(string name, T newValue)
+    {
+        ValueChanged?.Invoke(this, newValue);
+        PropertyChanged?.Invoke(this, PropertyChangedConstants.ValueChangedPropertyArgs);
+    }
+
+    private string GetDebuggerDisplay()
+    {
+        string value;
+
+        try
+        {
+            value = Value.ToString();
+        }
+        catch (Exception e)
+        {
+            value = $"<{e.GetType()}: {e.Message}>";
+        }
+
+        if (typeof(T) == typeof(string))
+        {
+            value = $"""
+                     "{value}"
+                     """;
+        }
+
+        var type = typeof(T).ToString();
+        
+        string preferenceType = this switch
+        {
+            LocalSetting<T> => "local",
+            SyncedSetting<T> => "synced",
+            _ => "<undefined>"
+        };
+
+        return $"{preferenceType} {Name}: {type} = {value}";
+    }
+    
+    event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+    {
+        add => PropertyChanged += value;
+        remove => PropertyChanged -= value;
+    }
+}
+
+// Generic types would create an instance for every type combination.
+file static class PropertyChangedConstants
+{
+    public static readonly PropertyChangedEventArgs ValueChangedPropertyArgs = new("Value");
+}

+ 3 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/SettingChangedHandler.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+
+public delegate void SettingChangedHandler<T>(Setting<T> setting, T? newValue);

+ 40 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/SettingHelpers.cs

@@ -0,0 +1,40 @@
+using System.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+
+public static class SettingHelper
+{
+    public static List<T> AsList<T>(this Setting<IEnumerable<T>> setting) =>
+        setting.As(new List<T>());
+    
+    public static T[] AsArray<T>(this Setting<IEnumerable<T>> setting) =>
+        setting.As(Array.Empty<T>());
+
+    public static void AddListCallback<T>(this Setting<IEnumerable<T>> setting, Action<List<T>> callback) =>
+        setting.ValueChanged += (_, value) => callback(value.ToList());
+
+    public static void AddArrayCallback<T>(this Setting<IEnumerable<T>> setting, Action<T[]> callback) =>
+        setting.ValueChanged += (_, value) => callback(value.ToArray());
+    
+    [StackTraceHidden]
+    public static void ThrowIfEmptySettingName(string name)
+    {
+        if (string.IsNullOrEmpty(name)) {
+            throw new ArgumentException($"name was empty", nameof(name));
+        }
+    
+        var colon = name.IndexOf(':');
+    
+        if (colon == 0)
+        {
+            // ":<any key>" does not have a valid prefix
+            throw new ArgumentException($"The prefix in the name '{name}' was empty", nameof(name));
+        }
+
+        if (colon == name.Length - 1)
+        {
+            // "<any prefix>:" does not have a valid key
+            throw new ArgumentException($"The key in the name '{name}' was empty", nameof(name));
+        }
+    }
+}

+ 29 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/SyncedSetting.cs

@@ -0,0 +1,29 @@
+using System.Runtime.CompilerServices;
+
+namespace PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
+
+/// <summary>
+/// A static class for creating a LocalSetting from the property name
+/// </summary>
+public static class SyncedSetting
+{
+    public static SyncedSetting<T> Owned<T>(T? fallbackValue = default, [CallerMemberName] string name = "") =>
+        new(name, fallbackValue);
+    
+    public static SyncedSetting<T> NonOwned<T>(string prefix, T? fallbackValue = default, [CallerMemberName] string name = "") =>
+        new($"{prefix}:{name}", fallbackValue);
+}
+
+/// <summary>
+/// A preference which may be synced across multiple devices
+/// </summary>
+/// <param name="name">The name of the preference</param>
+/// <param name="fallbackValue">A optional fallback value which will be used if the setting has not been set before set before</param>
+public class SyncedSetting<T>(string name, T? fallbackValue = default) : Setting<T>(name, fallbackValue)
+{
+    protected override TAny? GetValue<TAny>(IPreferences preferences, TAny fallbackValue) where TAny : default =>
+        preferences.GetPreference(Name, fallbackValue);
+
+    protected override void SetValue(IPreferences preferences, T value) =>
+        preferences.UpdatePreference(Name, value);
+}

+ 31 - 3
src/PixiEditor.sln

@@ -94,7 +94,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.MacOs", "PixiEdi
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.MSPackageBuilder", "PixiEditor.Extensions.MSPackageBuilder\PixiEditor.Extensions.MSPackageBuilder.csproj", "{AE200ADC-9E85-4275-A373-E975CD6D518C}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.MSPackageBuilder", "PixiEditor.Extensions.MSPackageBuilder\PixiEditor.Extensions.MSPackageBuilder.csproj", "{AE200ADC-9E85-4275-A373-E975CD6D518C}"
 EndProject
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiDocks.Core", "..\..\PixiDocks\src\PixiDocks.Core\PixiDocks.Core.csproj", "{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Extensions.CommonApi.Diagnostics", "PixiEditor.Extensions.CommonApi.Diagnostics\PixiEditor.Extensions.CommonApi.Diagnostics.csproj", "{D72E70F3-BF37-432F-B78B-5B247C873852}"
 EndProject
 EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -1410,8 +1410,34 @@ Global
 		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|Any CPU.Build.0 = Debug|Any CPU
 		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|Any CPU.Build.0 = Debug|Any CPU
 		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|x64.ActiveCfg = Debug|Any CPU
 		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|x64.ActiveCfg = Debug|Any CPU
 		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|x64.Build.0 = Debug|Any CPU
 		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|x64.Build.0 = Debug|Any CPU
-		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|x86.ActiveCfg = Debug|Any CPU
-		{4FA7F74B-8E55-4F85-A346-4A44878D5D3F}.Steam|x86.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|x64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.MSIX|x64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Release|ARM64.Build.0 = Release|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Release|x64.ActiveCfg = Release|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Release|x64.Build.0 = Release|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|ARM64.Build.0 = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|x64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -1434,6 +1460,7 @@ Global
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689} = {9A81B795-66AB-4743-9284-90565941343D}
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689} = {9A81B795-66AB-4743-9284-90565941343D}
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6} = {9A81B795-66AB-4743-9284-90565941343D}
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6} = {9A81B795-66AB-4743-9284-90565941343D}
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE} = {9A81B795-66AB-4743-9284-90565941343D}
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE} = {9A81B795-66AB-4743-9284-90565941343D}
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 		{10BF4001-214C-4869-8F78-2B6BDBDC7E7D} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 		{FA98BFA6-2E83-41C6-9102-76875B261F51} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{FA98BFA6-2E83-41C6-9102-76875B261F51} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
@@ -1453,6 +1480,7 @@ Global
 		{DA3AF3CC-43B2-4871-BDEC-CBE9222A8269} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{DA3AF3CC-43B2-4871-BDEC-CBE9222A8269} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{AE200ADC-9E85-4275-A373-E975CD6D518C} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{AE200ADC-9E85-4275-A373-E975CD6D518C} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
+		{D72E70F3-BF37-432F-B78B-5B247C873852} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}