flabbet 1 rok temu
rodzic
commit
cba3567ab3
23 zmienionych plików z 350 dodań i 55 usunięć
  1. 2 2
      samples/Sample3_Preferences/PreferencesSampleExtension.cs
  2. 1 1
      samples/Sample3_Preferences/extension.json
  3. 1 1
      src/PixiEditor.AvaloniaUI/Models/Palettes/LocalPalettesFetcher.cs
  4. 10 10
      src/PixiEditor.AvaloniaUI/Models/Preferences/PreferencesSettings.cs
  5. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SettingsWindowViewModel.cs
  6. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DebugViewModel.cs
  7. 4 4
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DiscordViewModel.cs
  8. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs
  9. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/UpdateViewModel.cs
  10. 1 1
      src/PixiEditor.AvaloniaUI/Views/Windows/PalettesBrowser.axaml.cs
  11. 4 4
      src/PixiEditor.Extensions.CommonApi/UserPreferences/IPreferences.cs
  12. 52 0
      src/PixiEditor.Extensions.CommonApi/Utilities/PrefixedNameUtility.cs
  13. 9 9
      src/PixiEditor.Extensions.Wasm/Api/UserPreferences/Preferences.cs
  14. 64 0
      src/PixiEditor.Extensions.Wasm/Bridge/Interop.Preferences.cs
  15. 46 0
      src/PixiEditor.Extensions.Wasm/Bridge/Native.Preferences.cs
  16. 3 0
      src/PixiEditor.Extensions.Wasm/Bridge/Native.cs
  17. 10 0
      src/PixiEditor.Extensions.WasmRuntime/Api/MetadataApi.cs
  18. 6 0
      src/PixiEditor.Extensions.WasmRuntime/Api/Modules/ApiModule.cs
  19. 91 0
      src/PixiEditor.Extensions.WasmRuntime/Api/Modules/PreferencesModule.cs
  20. 15 1
      src/PixiEditor.Extensions.WasmRuntime/Api/PreferencesApi.cs
  21. 3 14
      src/PixiEditor.Extensions.WasmRuntime/Utilities/PreferencesUtility.cs
  22. 15 3
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  23. 8 0
      src/PixiEditor.Extensions.WasmRuntime/WasmMemoryUtility.cs

+ 2 - 2
samples/Sample3_Preferences/PreferencesSampleExtension.cs

@@ -17,11 +17,11 @@ public class PreferencesSampleExtension : WasmExtension
     /// </summary>
     public override void OnInitialized()
     {
+        Api.Preferences.AddCallback<int>("HelloCount", (name, value) => Api.Logger.Log($"Hello count changed to {value}!"));
+        
         // Internally this preference will have name "yourCompany.Samples.Preferences:HelloCount".
         int helloCount = Api.Preferences.GetPreference<int>("HelloCount");
 
-        Api.Logger.Log($"Hello count: {helloCount}");
-
         Api.Preferences.UpdatePreference("HelloCount", helloCount + 1);
 
         // This will overwrite built-in PixiEditor preference. Extension must have WriteNonOwnedPreferences permission.

+ 1 - 1
samples/Sample3_Preferences/extension.json

@@ -1,7 +1,7 @@
 {
   "displayName": "Sample Extension - Preferences",
   "uniqueName": "yourCompany.Samples.Preferences",
-  "description": "Sample localization extension for PixiEditor",
+  "description": "Sample preferences extension for PixiEditor",
   "version": "1.0.0",
   "author": {
     "name": "PixiEditor",

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Palettes/LocalPalettesFetcher.cs

@@ -41,7 +41,7 @@ internal class LocalPalettesFetcher : PaletteListDataSource
         watcher.EnableRaisingEvents = true;
         cachedFavoritePalettes = IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
 
-        IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, updated =>
+        IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, (_, updated) =>
         {
             cachedFavoritePalettes = (List<string>)updated;
             cachedPalettes.ForEach(x => x.IsFavourite = cachedFavoritePalettes.Contains(x.Name));

+ 10 - 10
src/PixiEditor.AvaloniaUI/Models/Preferences/PreferencesSettings.cs

@@ -53,7 +53,7 @@ internal class PreferencesSettings : IPreferences
         {
             foreach (var action in callback)
             {
-                action.Invoke(value);
+                action.Invoke(name, value);
             }
         }
 
@@ -73,7 +73,7 @@ internal class PreferencesSettings : IPreferences
         {
             foreach (var action in callback)
             {
-                action.Invoke(value);
+                action.Invoke(name, value);
             }
         }
 
@@ -91,9 +91,9 @@ internal class PreferencesSettings : IPreferences
         File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
     }
 
-    public Dictionary<string, List<Action<object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<object>>>();
+    public Dictionary<string, List<Action<string, object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<string, object>>>();
 
-    public void AddCallback(string name, Action<object> action)
+    public void AddCallback(string name, Action<string, object> action)
     {
         if (action == null)
         {
@@ -106,20 +106,20 @@ internal class PreferencesSettings : IPreferences
             return;
         }
 
-        Callbacks.Add(name, new List<Action<object>>() { action });
+        Callbacks.Add(name, new List<Action<string, object>>() { action });
     }
 
-    public void AddCallback<T>(string name, Action<T> action)
+    public void AddCallback<T>(string name, Action<string, T> action)
     {
         if (action == null)
         {
             throw new ArgumentNullException(nameof(action));
         }
 
-        AddCallback(name, new Action<object>(o => action((T)o)));
+        AddCallback(name, new Action<string, object>((n, o) => action(n, (T)o)));
     }
 
-    public void RemoveCallback(string name, Action<object> action)
+    public void RemoveCallback(string name, Action<string, object> action)
     {
         if (action == null)
         {
@@ -132,14 +132,14 @@ internal class PreferencesSettings : IPreferences
         }
     }
 
-    public void RemoveCallback<T>(string name, Action<T> action)
+    public void RemoveCallback<T>(string name, Action<string, T> action)
     {
         if (action == null)
         {
             throw new ArgumentNullException(nameof(action));
         }
 
-        RemoveCallback(name, new Action<object>(o => action((T)o)));
+        RemoveCallback(name, new Action<string, object>((n, o) => action(n, (T)o)));
     }
 
 #nullable enable

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SettingsWindowViewModel.cs

@@ -276,7 +276,7 @@ internal partial class SettingsWindowViewModel : ViewModelBase
         Commands = new(CommandController.Current.CommandGroups.Select(x => new GroupSearchResult(x)));
         UpdateSearchResults();
         SettingsSubViewModel = new SettingsViewModel(this);
-        ViewModelMain.Current.Preferences.AddCallback("IsDebugModeEnabled", _ => UpdateSearchResults());
+        ViewModelMain.Current.Preferences.AddCallback("IsDebugModeEnabled", (_, _) => UpdateSearchResults());
         VisibleGroups = Commands.Count(x => x.IsVisible);
     }
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DebugViewModel.cs

@@ -72,7 +72,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     {
         SetDebug();
         preferences.AddCallback<bool>("IsDebugModeEnabled", UpdateDebugMode);
-        UpdateDebugMode(preferences.GetPreference<bool>("IsDebugModeEnabled"));
+        UpdateDebugMode("IsDebugModeEnabled", preferences.GetPreference<bool>("IsDebugModeEnabled"));
     }
 
     public static void OpenFolder(string path)
@@ -315,7 +315,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     [Conditional("DEBUG")]
     private static void SetDebug() => IsDebugBuild = true;
 
-    private void UpdateDebugMode(bool setting)
+    private void UpdateDebugMode(string name, bool setting)
     {
         IsDebugModeEnabled = setting;
         UseDebug = IsDebugBuild || IsDebugModeEnabled;

+ 4 - 4
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/DiscordViewModel.cs

@@ -84,10 +84,10 @@ internal class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
         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);
+        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);
         AppDomain.CurrentDomain.ProcessExit += (_, _) => Enabled = false;
     }
 

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs

@@ -399,7 +399,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    private void UpdateMaxRecentlyOpened(object parameter)
+    private void UpdateMaxRecentlyOpened(string name, object parameter)
     {
         int newAmount = (int)parameter;
 

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -55,7 +55,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         : base(owner)
     {
         Owner.OnStartupEvent += Owner_OnStartupEvent;
-        IPreferences.Current.AddCallback<string>("UpdateChannel", val =>
+        IPreferences.Current.AddCallback<string>("UpdateChannel", (_, val) =>
         {
             string prevChannel = UpdateChecker.Channel.ApiUrl;
             UpdateChecker.Channel = GetUpdateChannel(val);

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

@@ -246,7 +246,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
         return palette != null && palette.Source.GetType() == typeof(LocalPalettesFetcher);
     }
 
-    private void OnFavouritePalettesChanged(object obj)
+    private void OnFavouritePalettesChanged(string preferenceName, object value)
     {
         Filtering.Favourites =
             IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);

+ 4 - 4
src/PixiEditor.Extensions.CommonApi/UserPreferences/IPreferences.cs

@@ -14,7 +14,7 @@ public interface IPreferences
     /// </summary>
     /// <param name="name">The name of the setting</param>
     /// <param name="action">The action that will be executed when the setting changes</param>
-    public void AddCallback(string name, Action<object> action);
+    public void AddCallback(string name, Action<string, object> action);
 
     /// <summary>
     /// Adds a callback that will be executed when the setting called <paramref name="name"/> changes.
@@ -22,10 +22,10 @@ public interface IPreferences
     /// <typeparam name="T">The <see cref="Type"/> of the setting</typeparam>
     /// <param name="name">The name of the setting</param>
     /// <param name="action">The action that will be executed when the setting changes</param>
-    public void AddCallback<T>(string name, Action<T> action);
+    public void AddCallback<T>(string name, Action<string, T> action);
 
-    public void RemoveCallback(string name, Action<object> action);
-    public void RemoveCallback<T>(string name, Action<T> action);
+    public void RemoveCallback(string name, Action<string, object> action);
+    public void RemoveCallback<T>(string name, Action<string, T> action);
 
     /// <summary>
     /// Initializes the preferences.

+ 52 - 0
src/PixiEditor.Extensions.CommonApi/Utilities/PrefixedNameUtility.cs

@@ -0,0 +1,52 @@
+namespace PixiEditor.Extensions.CommonApi.Utilities;
+
+public static class PrefixedNameUtility
+{
+    /// <summary>
+    ///     Converts a preference name to a full name with the extension unique name. It is relative to PixiEditor, so
+    /// any preference without a prefix is a PixiEditor preference.
+    /// </summary>
+    /// <param name="uniqueName">Unique name of the extension.</param>
+    /// <param name="name">Name of the preference.</param>
+    /// <returns>Full name of the preference.</returns>
+    public static string ToPixiEditorRelativePreferenceName(string uniqueName, string name)
+    {
+        string[] splitted = name.Split(":");
+        
+        string finalName = $"{uniqueName}:{name}";
+        
+        if (splitted.Length == 2)
+        {
+            finalName = name;
+            
+            if(splitted[0].Equals("pixieditor", StringComparison.CurrentCultureIgnoreCase)) 
+            {
+                finalName = splitted[1];
+            }
+        }
+
+        return finalName;
+    }
+
+    /// <summary>
+    ///    It is a reverse of <see cref="ToPixiEditorRelativePreferenceName"/>. It converts a full preference name to a relative name.
+    /// Preferences owned by the extension will not have any prefix, while PixiEditor preferences will have "pixieditor:" prefix.
+    /// </summary>
+    /// <param name="extensionUniqueName">Unique name of the extension.</param>
+    /// <param name="preferenceName">Full name of the preference.</param>
+    /// <returns>Relative name of the preference.</returns>
+    public static string ToExtensionRelativeName(string extensionUniqueName, string preferenceName)
+    {
+        if (preferenceName.StartsWith(extensionUniqueName))
+        {
+            return preferenceName[(extensionUniqueName.Length + 1)..];
+        }
+
+        if(preferenceName.Split(":").Length == 1)
+        {
+            return $"pixieditor:{preferenceName}";
+        }
+
+        return preferenceName;
+    }
+}

+ 9 - 9
src/PixiEditor.Extensions.Wasm/Api/UserPreferences/Preferences.cs

@@ -59,7 +59,7 @@ public class Preferences : IPreferences
     /// <returns>Preference value.</returns>
     public T GetPreference<T>(string name, T fallbackValue)
     {
-        return Interop.GetPreference<T>(name, fallbackValue);
+        return Interop.GetPreference(name, fallbackValue);
     }
 
     /// <summary>
@@ -85,24 +85,24 @@ public class Preferences : IPreferences
         return Interop.GetLocalPreference(name, fallbackValue);
     }
     
-    public void AddCallback(string name, Action<object> action)
+    public void AddCallback(string name, Action<string, object> action)
     {
-        
+        Interop.AddPreferenceCallback(name, action);
     }
 
-    public void AddCallback<T>(string name, Action<T> action)
+    public void AddCallback<T>(string name, Action<string, T> action)
     {
-        throw new NotImplementedException();
+        Interop.AddPreferenceCallback(name, action);
     }
 
-    public void RemoveCallback(string name, Action<object> action)
+    public void RemoveCallback(string name, Action<string, object> action)
     {
-        throw new NotImplementedException();
+        Interop.RemovePreferenceCallback(name, action);
     }
 
-    public void RemoveCallback<T>(string name, Action<T> action)
+    public void RemoveCallback<T>(string name, Action<string, T> action)
     {
-        throw new NotImplementedException();
+        Interop.RemovePreferenceCallback(name, action);
     }
 
     void IPreferences.Init() { }

+ 64 - 0
src/PixiEditor.Extensions.Wasm/Bridge/Interop.Preferences.cs

@@ -0,0 +1,64 @@
+namespace PixiEditor.Extensions.Wasm.Bridge;
+
+internal static partial class Interop
+{
+    private static Dictionary<string, List<Action<string, object>>> _callbacks = new();
+    private static string uniqueName;
+
+    static Interop()
+    {
+        uniqueName = Native.get_extension_unique_name();
+        Native.PreferenceUpdated += NativeOnPreferenceUpdated;
+    }
+
+    private static void NativeOnPreferenceUpdated(string name, object value)
+    {
+        if (_callbacks.TryGetValue(name, out var actions))
+        {
+            foreach (var action in actions)
+            {
+                action(name, value);
+            }
+        }
+        else if (_callbacks.TryGetValue(name.Replace($"{uniqueName}:", ""), out var uniqueActions))
+        {
+            foreach (var action in uniqueActions)
+            {
+                action(name, value);
+            }
+        }
+    }
+
+    public static void AddPreferenceCallback(string name, Action<string, object> action)
+    {
+        if (_callbacks.TryAdd(name, new List<Action<string, object>>()))
+        {
+            Native.add_preference_callback(name);
+        }
+        
+        _callbacks[name].Add(action);
+    }
+    
+    public static void AddPreferenceCallback<T>(string name, Action<string, T> action)
+    {
+        AddPreferenceCallback(name, (preferenceName, value) => action(preferenceName, (T)value));
+    }
+
+    public static void RemovePreferenceCallback(string name, Action<string, object> action)
+    {
+        if (_callbacks.TryGetValue(name, out var actions))
+        {
+            actions.Remove(action);
+            if (actions.Count == 0)
+            {
+                _callbacks.Remove(name);
+                Native.remove_preference_callback(name);
+            }
+        }
+    }
+    
+    public static void RemovePreferenceCallback<T>(string name, Action<string, T> action)
+    {
+        RemovePreferenceCallback(name, (prefName, value) => action(prefName, (T)value));
+    }
+}

+ 46 - 0
src/PixiEditor.Extensions.Wasm/Bridge/Native.Preferences.cs

@@ -1,9 +1,11 @@
 using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 
 namespace PixiEditor.Extensions.Wasm.Bridge;
 
 internal static partial class Native
 {
+    public static event Action<string, object> PreferenceUpdated;
     
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void save_preferences();
@@ -67,4 +69,48 @@ internal static partial class Native
     
     [MethodImpl(MethodImplOptions.InternalCall)]
     public static extern double get_local_preference_double(string name, double fallbackDouble);
+    
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void add_preference_callback(string name);
+    
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void remove_preference_callback(string name);
+    
+    [ApiExport("string_preference_updated")]
+    public static void OnStringPreferenceUpdated(string name, string value)
+    {
+        PreferenceUpdated?.Invoke(name, value);
+    }
+    
+    [ApiExport("int32_preference_updated")]
+    public static void OnIntPreferenceUpdated(string name, int value)
+    {
+        PreferenceUpdated?.Invoke(name, value);
+    }
+    
+    [ApiExport("bool_preference_updated")]
+    public static void OnBoolPreferenceUpdated(string name, bool value)
+    {
+        PreferenceUpdated?.Invoke(name, value);
+    }
+    
+    [ApiExport("float_preference_updated")]
+    public static void OnFloatPreferenceUpdated(string name, float value)
+    {
+        PreferenceUpdated?.Invoke(name, value);
+    }
+    
+    [ApiExport("double_preference_updated")]
+    public static void OnDoublePreferenceUpdated(string name, double value)
+    {
+        PreferenceUpdated?.Invoke(name, value);
+    }
+    
+    [ApiExport("byte_array_preference_updated")]
+    public static void OnByteArrayPreferenceUpdated(string name, IntPtr ptr, int length)
+    {
+        var bytes = new byte[length];
+        Marshal.Copy(ptr, bytes, 0, length);
+        PreferenceUpdated?.Invoke(name, bytes);
+    }
 }

+ 3 - 0
src/PixiEditor.Extensions.Wasm/Bridge/Native.cs

@@ -23,6 +23,9 @@ internal static partial class Native
 
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void state_changed(int uniqueId, IntPtr data, int length);
+    
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern string get_extension_unique_name();
 
 
     // No need for [ApiExport] since this is a part of built-in C interop file.

+ 10 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/MetadataApi.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.Extensions.WasmRuntime.Api;
+
+internal class MetadataApi : ApiGroupHandler
+{
+    [ApiFunction("get_extension_unique_name")]
+    public string GetExtensionUniqueName()
+    {
+        return Extension.Metadata.UniqueName;
+    }
+}

+ 6 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/Modules/ApiModule.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Extensions.WasmRuntime.Api.Modules;
+
+public class ApiModule(WasmExtensionInstance extension)
+{
+    public WasmExtensionInstance Extension { get; } = extension;
+}

+ 91 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/Modules/PreferencesModule.cs

@@ -0,0 +1,91 @@
+using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.Utilities;
+using PixiEditor.Extensions.WasmRuntime.Utilities;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api.Modules;
+
+internal class PreferencesModule : ApiModule
+{
+    private IPreferences Preferences { get; }
+    public PreferencesModule(WasmExtensionInstance extension, IPreferences preferences) : base(extension)
+    {
+        Preferences = preferences;
+    }
+
+    public void AddPreferenceCallback(string name)
+    {
+        string prefixedName = PrefixedNameUtility.ToPixiEditorRelativePreferenceName(Extension.Metadata.UniqueName, name);
+        Preferences.AddCallback(prefixedName, InvokeExtensionCallback);
+    }
+    
+    public void RemovePreferenceCallback(string name)
+    {
+        string prefixedName = PrefixedNameUtility.ToPixiEditorRelativePreferenceName(Extension.Metadata.UniqueName, name);
+        Preferences.RemoveCallback(prefixedName, InvokeExtensionCallback);
+    }
+
+    private void InvokeExtensionCallback(string preferenceName, object value)
+    {
+        string returnName = PrefixedNameUtility.ToExtensionRelativeName(Extension.Metadata.UniqueName, preferenceName);
+        if (value.GetType().Name.Equals("string", StringComparison.InvariantCultureIgnoreCase))
+        {
+            InvokeStringCallback(preferenceName, value);
+        }
+        else if (value.GetType().Name.Equals("byte[]", StringComparison.InvariantCultureIgnoreCase))
+        {
+            InvokeByteArrayCallback(preferenceName, value);
+        }
+        else
+        {
+            InvokeNonLengthCallback(preferenceName, value);
+        }
+    }
+
+    private void InvokeStringCallback(string preferenceName, object value)
+    {
+        string stringValue = (string)value;
+        var callbackAction = Extension.Instance.GetAction<int, int>("string_preference_updated");
+        int valuePtr = Extension.WasmMemoryUtility.WriteString(stringValue);
+        int namePtr = Extension.WasmMemoryUtility.WriteString(preferenceName);
+            
+        callbackAction.Invoke(namePtr, valuePtr);
+    }
+    
+    private void InvokeByteArrayCallback(string preferenceName, object value)
+    {
+        byte[] byteArrayValue = (byte[])value;
+        var callbackAction = Extension.Instance.GetAction<int, int, int>("byte_array_preference_updated");
+        int valuePtr = Extension.WasmMemoryUtility.WriteSpan(byteArrayValue);
+        int namePtr = Extension.WasmMemoryUtility.WriteString(preferenceName);
+            
+        callbackAction.Invoke(namePtr, valuePtr, byteArrayValue.Length);
+    }
+    
+    private void InvokeNonLengthCallback(string preferenceName, object value)
+    {
+        bool isValid = value is int or bool or float or double;
+        if (!isValid)
+        {
+            throw new ArgumentException("Unsupported preference value type.");
+        }
+        
+        string typeName = value.GetType().Name.ToLower();
+        var callbackAction = Extension.Instance.GetAction<int, int>($"{typeName}_preference_updated");
+        int namePtr = Extension.WasmMemoryUtility.WriteString(preferenceName);
+        int valuePtr = WriteValue(value);
+        
+        callbackAction.Invoke(namePtr, valuePtr);
+    }
+
+    private int WriteValue(object value)
+    {
+        return value switch
+        {
+            int intValue => intValue,
+            bool boolValue => Extension.WasmMemoryUtility.WriteBoolean(boolValue),
+            float floatValue => Extension.WasmMemoryUtility.WriteSingle(floatValue),
+            double doubleValue => Extension.WasmMemoryUtility.WriteDouble(doubleValue),
+            _ => throw new ArgumentException("Unsupported preference value type.")
+        };
+    }
+}

+ 15 - 1
src/PixiEditor.Extensions.WasmRuntime/Api/PreferencesApi.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Extensions.Metadata;
+using PixiEditor.Extensions.CommonApi.Utilities;
+using PixiEditor.Extensions.Metadata;
+using PixiEditor.Extensions.WasmRuntime.Api.Modules;
 using PixiEditor.Extensions.WasmRuntime.Utilities;
 
 namespace PixiEditor.Extensions.WasmRuntime.Api;
@@ -140,4 +142,16 @@ internal class PreferencesApi : ApiGroupHandler
         var result = PreferencesUtility.GetPreference(Extension, name, fallbackValue, true);
         return result;
     }
+    
+    [ApiFunction("add_preference_callback")]
+    public void AddPreferenceCallback(string name)
+    {
+        Extension.GetModule<PreferencesModule>().AddPreferenceCallback(name);
+    }
+    
+    [ApiFunction("remove_preference_callback")]
+    public void RemovePreferenceCallback(string name)
+    {
+        Extension.GetModule<PreferencesModule>().RemovePreferenceCallback(name);
+    }
 }

+ 3 - 14
src/PixiEditor.Extensions.WasmRuntime/Utilities/PreferencesUtility.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Extensions.Metadata;
+using PixiEditor.Extensions.CommonApi.Utilities;
+using PixiEditor.Extensions.Metadata;
 
 namespace PixiEditor.Extensions.WasmRuntime.Utilities;
 
@@ -55,19 +56,7 @@ internal static class PreferencesUtility
             throw new ArgumentNullException(nameof(name));
         }
         
-        string[] splitted = name.Split(":");
-        
-        string finalName = $"{extension.Metadata.UniqueName}:{name}";
-        
-        if (splitted.Length == 2)
-        {
-            finalName = name;
-            
-            if(splitted[0].Equals("pixieditor", StringComparison.CurrentCultureIgnoreCase)) 
-            {
-                finalName = splitted[1];
-            }
-        }
+        string finalName = PrefixedNameUtility.ToPixiEditorRelativePreferenceName(extension.Metadata.UniqueName, name);
 
         if (getLocalPreference)
         {

+ 15 - 3
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -5,6 +5,7 @@ using Avalonia.Threading;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.FlyUI;
 using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.WasmRuntime.Api.Modules;
 using PixiEditor.Extensions.WasmRuntime.Management;
 using PixiEditor.Extensions.Windowing;
 using Wasmtime;
@@ -19,8 +20,7 @@ public partial class WasmExtensionInstance : Extension
     private Linker Linker { get; }
     private Store Store { get; }
     private Module Module { get; }
-
-    private Memory memory = null!;
+    
     private LayoutBuilder LayoutBuilder { get; set; }
     internal ObjectManager NativeObjectManager { get; set; }
     internal AsyncCallsManager AsyncHandleManager { get; set; }
@@ -28,6 +28,7 @@ public partial class WasmExtensionInstance : Extension
     private WasmExtensionInstance Extension => this; // api group handler needs this property
 
     private string modulePath;
+    private List<ApiModule> modules = new();
     
     public override string Location => modulePath;
 
@@ -53,7 +54,6 @@ public partial class WasmExtensionInstance : Extension
 
         Instance = Linker.Instantiate(Store, Module);
         WasmMemoryUtility = new WasmMemoryUtility(Instance);
-        memory = Instance.GetMemory("memory");
     }
 
     protected override void OnLoaded()
@@ -64,6 +64,7 @@ public partial class WasmExtensionInstance : Extension
 
     protected override void OnInitialized()
     {
+        modules.Add(new PreferencesModule(this, Api.Preferences));
         LayoutBuilder = new LayoutBuilder((ElementMap)Api.Services.GetService(typeof(ElementMap)));
 
         //SetElementMap();
@@ -90,4 +91,15 @@ public partial class WasmExtensionInstance : Extension
 
         WasmMemoryUtility.Free(ptr);
     }
+
+    public T? GetModule<T>() where T : ApiModule
+    {
+        var module = modules.FirstOrDefault(x => x.GetType() == typeof(T));
+        if (module == null)
+        {
+            return default;
+        }
+
+        return (T)module;
+    }
 }

+ 8 - 0
src/PixiEditor.Extensions.WasmRuntime/WasmMemoryUtility.cs

@@ -98,6 +98,14 @@ public class WasmMemoryUtility
         return memory.ReadSingle(offset);
     }
     
+    public int WriteSingle(float value)
+    {
+        const int length = 4;
+        var ptr = malloc.Invoke(length);
+        memory.WriteSingle(ptr, value);
+        return ptr;
+    }
+    
     public int WriteBoolean(bool value)
     {
         const int length = 1;