Browse Source

Merge pull request #585 from PixiEditor/fix-bug-from-fixes

Hopefully fix MouseUpdateController for good
Krzysztof Krysiński 1 year ago
parent
commit
583a626036

+ 98 - 21
src/PixiEditor/Models/Controllers/MouseUpdateController.cs

@@ -1,49 +1,126 @@
-using System.Timers;
+using System.Diagnostics;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Threading;
 
 namespace PixiEditor.Models.Controllers;
 
+#nullable enable
 public class MouseUpdateController : IDisposable
 {
-    private const int MouseUpdateIntervalMs = 7;  // 7ms ~= 142 Hz
-    
-    private readonly System.Timers.Timer _timer;
-    
-    private UIElement element;
-    
-    private MouseEventHandler mouseMoveHandler;
+    private const double MouseUpdateIntervalMs = 1000 / 142.0; //142 Hz
+
+    private Thread timerThread;
+    private readonly AutoResetEvent resetEvent = new(false);
+    private readonly object lockObj = new();
+    private bool isAborted = false;
+
+    private readonly FrameworkElement element;
+    private readonly MouseEventHandler mouseMoveHandler;
     
-    public MouseUpdateController(UIElement uiElement, MouseEventHandler onMouseMove)
+    public MouseUpdateController(FrameworkElement uiElement, MouseEventHandler onMouseMove)
     {
         mouseMoveHandler = onMouseMove;
         element = uiElement;
-        
-        _timer = new System.Timers.Timer(MouseUpdateIntervalMs);
-        _timer.Elapsed += TimerOnElapsed;
-        
         element.MouseMove += OnMouseMove;
+
+        bool wasThreadCreated = !element.IsLoaded;
+        element.Loaded += (_, _) =>
+        {
+            wasThreadCreated = true;
+            CreateTimerThread();
+        };
+
+        if (!wasThreadCreated)
+            CreateTimerThread();
+
+        element.Unloaded += (_, _) =>
+        {
+            isAborted = true;
+        };
+    }
+
+    private void CreateTimerThread()
+    {
+        timerThread = new Thread(TimerThread);
+        timerThread.Name = "MouseUpdateController thread";
+        timerThread.Start();
+        isAborted = false;
     }
 
-    private void TimerOnElapsed(object sender, ElapsedEventArgs e)
+    private bool IsThreadShouldStop()
+    {
+        return isAborted || timerThread != Thread.CurrentThread || Application.Current is null;
+    }
+    
+    private void TimerThread()
     {
-        Application.Current?.Dispatcher.Invoke(() =>
+        try
         {
-            element.MouseMove += OnMouseMove;
-        });
+            long lastThreadIter = Stopwatch.GetTimestamp();
+            
+            // abort if a new thread was created
+            while (!IsThreadShouldStop())
+            {
+                // call waitOne periodically instead of waiting infinitely to make sure we crash or exit when resetEvent is disposed
+                if (!resetEvent.WaitOne(300))
+                {
+                    lastThreadIter = Stopwatch.GetTimestamp();
+                    continue;
+                }
+
+                lock (lockObj)
+                {
+                    double sleepDur = Math.Clamp(MouseUpdateIntervalMs - Stopwatch.GetElapsedTime(lastThreadIter).TotalMilliseconds, 0, MouseUpdateIntervalMs);
+                    lastThreadIter += (long)(MouseUpdateIntervalMs * Stopwatch.Frequency / 1000);
+                    if (sleepDur > 0)
+                        Thread.Sleep((int)Math.Round(sleepDur));
+                    
+                    if (IsThreadShouldStop())
+                        return;
+                    Application.Current?.Dispatcher.Invoke(() =>
+                    {
+                        element.MouseMove += OnMouseMove;
+                    });
+                    
+                }
+            }
+        }
+        catch (ObjectDisposedException)
+        {
+            return;
+        }
+        catch (Exception e)
+        {
+            Application.Current?.Dispatcher.BeginInvoke(() => throw new AggregateException("Input handling thread died", e), DispatcherPriority.SystemIdle);
+            throw;
+        }
     }
 
     private void OnMouseMove(object sender, MouseEventArgs e)
     {
-        element.MouseMove -= OnMouseMove;
-        _timer.Start();
-        mouseMoveHandler(sender, e);
+        bool lockWasTaken = false;
+        try
+        {
+            Monitor.TryEnter(lockObj, ref lockWasTaken);
+            if (lockWasTaken)
+            {
+                resetEvent.Set();
+                element.MouseMove -= OnMouseMove;
+                mouseMoveHandler(sender, e);
+            }
+        }
+        finally
+        {
+            if (lockWasTaken)
+                Monitor.Exit(lockObj);
+        }
     }
 
     public void Dispose()
     {
         element.MouseMove -= OnMouseMove;
-        _timer.Dispose();
+        isAborted = true;
+        resetEvent.Dispose();
     }
 }

+ 3 - 3
src/PixiEditor/Views/UserControls/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -23,7 +23,7 @@ internal class BrushShapeOverlay : Control
             new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsRender));
 
     public static readonly DependencyProperty MouseEventSourceProperty =
-        DependencyProperty.Register(nameof(MouseEventSource), typeof(UIElement), typeof(BrushShapeOverlay), new(null));
+        DependencyProperty.Register(nameof(MouseEventSource), typeof(FrameworkElement), typeof(BrushShapeOverlay), new(null));
 
     public static readonly DependencyProperty MouseReferenceProperty =
         DependencyProperty.Register(nameof(MouseReference), typeof(UIElement), typeof(BrushShapeOverlay), new(null));
@@ -44,9 +44,9 @@ internal class BrushShapeOverlay : Control
         set => SetValue(MouseReferenceProperty, value);
     }
 
-    public UIElement? MouseEventSource
+    public FrameworkElement? MouseEventSource
     {
-        get => (UIElement?)GetValue(MouseEventSourceProperty);
+        get => (FrameworkElement?)GetValue(MouseEventSourceProperty);
         set => SetValue(MouseEventSourceProperty, value);
     }