ソースを参照

hopefully fix MouseUpdateController for good

Equbuxu 1 年間 前
コミット
14acc5b267

+ 84 - 23
src/PixiEditor/Models/Controllers/MouseUpdateController.cs

@@ -1,49 +1,110 @@
-using System.Timers;
-using System.Windows;
+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;
-    
-    public MouseUpdateController(UIElement uiElement, MouseEventHandler onMouseMove)
+    private const int MouseUpdateIntervalMs = 7; // 7ms ~= 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(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 TimerOnElapsed(object sender, ElapsedEventArgs e)
+    private void CreateTimerThread()
     {
-        Application.Current?.Dispatcher.Invoke(() =>
+        timerThread = new Thread(TimerThread);
+        timerThread.Name = "MouseUpdateController thread";
+        timerThread.Start();
+        isAborted = false;
+    }
+
+    private void TimerThread()
+    {
+        try
+        {
+            // abort if a new thread was created
+            while (!isAborted && timerThread == Thread.CurrentThread)
+            {
+                // call waitOne periodically instead of waiting infinitely to make sure we crash or exit when resetEvent is disposed
+                if (!resetEvent.WaitOne(1000))
+                    continue;
+                
+                lock (lockObj)
+                {
+                    Thread.Sleep(MouseUpdateIntervalMs);
+                    if (isAborted || timerThread != Thread.CurrentThread)
+                        return;
+                    Application.Current?.Dispatcher.Invoke(() =>
+                    {
+                        element.MouseMove += OnMouseMove;
+                    });
+                }
+            }
+        }
+        catch (ObjectDisposedException)
         {
-            element.MouseMove += OnMouseMove;
-        });
+            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);
     }