Browse Source

Hopefully now fix mouse controller for good

Equbuxu 1 year ago
parent
commit
c79ae5950b

+ 38 - 95
src/PixiEditor/Models/Controllers/MouseUpdateController.cs

@@ -1,126 +1,69 @@
-using System.Diagnostics;
-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 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 bool isDisposed = false;
 
     private readonly FrameworkElement element;
     private readonly MouseEventHandler mouseMoveHandler;
+    private MouseUpdateControllerSession? session;
     
     public MouseUpdateController(FrameworkElement uiElement, MouseEventHandler onMouseMove)
     {
         mouseMoveHandler = onMouseMove;
         element = uiElement;
-        element.MouseMove += OnMouseMove;
-
-        bool wasThreadCreated = !element.IsLoaded;
-        element.Loaded += (_, _) =>
-        {
-            wasThreadCreated = true;
-            CreateTimerThread();
-        };
-
-        if (!wasThreadCreated)
-            CreateTimerThread();
-
-        element.Unloaded += (_, _) =>
-        {
-            isAborted = true;
-        };
+        
+        element.Loaded += OnElementLoaded;
+        element.Unloaded += OnElementUnloaded;
+        
+        session ??= new MouseUpdateControllerSession(StartListening, StopListening, mouseMoveHandler); 
+        
+        element.MouseMove += CallMouseMoveInput;
     }
-
-    private void CreateTimerThread()
-    {
-        timerThread = new Thread(TimerThread);
-        timerThread.Name = "MouseUpdateController thread";
-        timerThread.Start();
-        isAborted = false;
-    }
-
-    private bool IsThreadShouldStop()
+    
+    void OnElementLoaded(object o, RoutedEventArgs routedEventArgs)
     {
-        return isAborted || timerThread != Thread.CurrentThread || Application.Current is null;
+        session ??= new MouseUpdateControllerSession(StartListening, StopListening, mouseMoveHandler);
     }
     
-    private void TimerThread()
+    private void OnElementUnloaded(object o, RoutedEventArgs routedEventArgs)
     {
-        try
-        {
-            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;
-                }
+        session.Dispose();
+        session = null;
+    }
 
-                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)
-        {
+    private void StartListening()
+    {
+        if (isDisposed)
             return;
-        }
-        catch (Exception e)
-        {
-            Application.Current?.Dispatcher.BeginInvoke(() => throw new AggregateException("Input handling thread died", e), DispatcherPriority.SystemIdle);
-            throw;
-        }
+        element.MouseMove -= CallMouseMoveInput;
+        element.MouseMove += CallMouseMoveInput;
     }
 
-    private void OnMouseMove(object sender, MouseEventArgs e)
+    private void CallMouseMoveInput(object sender, MouseEventArgs e)
+    {
+        if (isDisposed)
+            return;
+        session?.MouseMoveInput(sender, e);
+    }
+    
+    private void StopListening()
     {
-        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);
-        }
+        if (isDisposed)
+            return;
+        element.MouseMove -= CallMouseMoveInput;
     }
 
     public void Dispose()
     {
-        element.MouseMove -= OnMouseMove;
-        isAborted = true;
-        resetEvent.Dispose();
+        element.MouseMove -= CallMouseMoveInput;
+        element.Loaded -= OnElementLoaded;
+        element.Unloaded -= OnElementUnloaded;
+        session?.Dispose();
+        isDisposed = true;
     }
 }

+ 114 - 0
src/PixiEditor/Models/Controllers/MouseUpdateControllerSession.cs

@@ -0,0 +1,114 @@
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace PixiEditor.Models.Controllers;
+
+#nullable enable
+internal class MouseUpdateControllerSession : IDisposable
+{
+    private const double IntervalMs = 1000 / 142.0; //142 Hz
+
+    private readonly Action onStartListening;
+    private readonly Action onStopListening;
+    private readonly MouseEventHandler onMouseMove;
+    
+    private readonly AutoResetEvent resetEvent = new(false);
+    private readonly object lockObj = new();
+
+    /// <summary>
+    /// <see cref="MouseUpdateControllerSession"/> doesn't rely on attaching and detaching mouse move handler,
+    /// it just ignores mouse move events when not listening. <br/>
+    /// Yet it still calls <see cref="onStartListening"/> and <see cref="onStopListening"/> which can be used to attach and detach event handler elsewhere.
+    /// </summary>
+    private bool isListening = true;
+    private bool isDisposed = false;
+
+    public MouseUpdateControllerSession(Action onStartListening, Action onStopListening, MouseEventHandler onMouseMove)
+    {
+        this.onStartListening = onStartListening;
+        this.onStopListening = onStopListening;
+        this.onMouseMove = onMouseMove;
+
+        Thread timerThread = new(TimerLoop);
+        timerThread.Name = "MouseUpdateController thread";
+        timerThread.Start();
+
+        onStartListening();
+    }
+
+    public void MouseMoveInput(object sender, MouseEventArgs e)
+    {
+        if (!isListening || isDisposed)
+            return;
+        
+        bool lockWasTaken = false;
+        try
+        {
+            Monitor.TryEnter(lockObj, ref lockWasTaken);
+            if (lockWasTaken)
+            {
+                isListening = false;
+                onStopListening();
+                onMouseMove(sender, e);
+                resetEvent.Set();
+            }
+        }
+        finally
+        {
+            if (lockWasTaken)
+                Monitor.Exit(lockObj);
+        }
+    }
+    
+    public void Dispose()
+    {
+        isDisposed = true;
+        resetEvent.Dispose();
+    }
+
+    private void TimerLoop()
+    {
+        try
+        {
+            long lastThreadIter = Stopwatch.GetTimestamp();
+            while (!isDisposed)
+            {
+                // 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(IntervalMs - Stopwatch.GetElapsedTime(lastThreadIter).TotalMilliseconds, 0, IntervalMs);
+                    lastThreadIter += (long)(IntervalMs * Stopwatch.Frequency / 1000);
+                    if (sleepDur > 0)
+                        Thread.Sleep((int)Math.Round(sleepDur));
+                    
+                    if (isDisposed)
+                        return;
+
+                    isListening = true;
+                    Application.Current?.Dispatcher.Invoke(() =>
+                    {
+                        if (!isDisposed)
+                            onStartListening();
+                    });
+                }
+            }
+        }
+        catch (ObjectDisposedException)
+        {
+            return;
+        }
+        catch (Exception e)
+        {
+            Application.Current?.Dispatcher.BeginInvoke(() => throw new AggregateException("Input handling thread died", e), DispatcherPriority.SystemIdle);
+            throw;
+        }
+    }
+}

+ 7 - 1
src/PixiEditor/Views/UserControls/Layers/FolderControl.xaml.cs

@@ -32,13 +32,19 @@ internal partial class FolderControl : UserControl
 
     private readonly Brush? highlightColor;
     
-    private MouseUpdateController mouseUpdateController;
+    private MouseUpdateController? mouseUpdateController;
 
     public FolderControl()
     {
         InitializeComponent();
         highlightColor = (Brush?)App.Current.Resources["SoftSelectedLayerColor"];
         Loaded += OnLoaded;
+        Unloaded += OnUnloaded;
+    }
+
+    private void OnUnloaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController?.Dispose();
     }
 
     private void OnLoaded(object sender, RoutedEventArgs e)

+ 7 - 1
src/PixiEditor/Views/UserControls/Layers/LayerControl.xaml.cs

@@ -62,15 +62,21 @@ internal partial class LayerControl : UserControl
         nameof(MoveToFrontCommand), typeof(RelayCommand), typeof(LayerControl), new PropertyMetadata(default(RelayCommand)));
 
 
-    private MouseUpdateController mouseUpdateController;
+    private MouseUpdateController? mouseUpdateController;
     
     public LayerControl()
     {
         InitializeComponent();
         Loaded += LayerControl_Loaded;
+        Unloaded += LayerControl_Unloaded;
         highlightColor = (Brush?)App.Current.Resources["SoftSelectedLayerColor"];
     }
 
+    private void LayerControl_Unloaded(object sender, RoutedEventArgs e)
+    { 
+        mouseUpdateController?.Dispose();
+    }
+
     private void LayerControl_Loaded(object sender, RoutedEventArgs e)
     {
         mouseUpdateController = new MouseUpdateController(this, Manager.LayerControl_MouseMove);

+ 2 - 5
src/PixiEditor/Views/UserControls/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -65,7 +65,7 @@ internal class BrushShapeOverlay : Control
     private Pen whitePen = new Pen(Brushes.LightGray, 1);
     private Point lastMousePos = new();
 
-    private MouseUpdateController mouseUpdateController;
+    private MouseUpdateController? mouseUpdateController;
 
     public BrushShapeOverlay()
     {
@@ -75,10 +75,7 @@ internal class BrushShapeOverlay : Control
 
     private void ControlUnloaded(object sender, RoutedEventArgs e)
     {
-        if (MouseEventSource is null)
-            return;
-        
-        mouseUpdateController.Dispose();
+        mouseUpdateController?.Dispose();
     }
 
     private void ControlLoaded(object sender, RoutedEventArgs e)

+ 6 - 0
src/PixiEditor/Views/UserControls/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -72,12 +72,18 @@ internal class LineToolOverlay : Control
     {
         Cursor = Cursors.Arrow;
         Loaded += OnLoaded;
+        Unloaded += OnUnloaded;
     }
 
     private void OnLoaded(object sender, RoutedEventArgs e)
     {
         mouseUpdateController = new MouseUpdateController(this, MouseMoved);
     }
+    
+    private void OnUnloaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController?.Dispose();
+    }
 
     private static void OnZoomboxScaleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
     {

+ 7 - 1
src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -119,11 +119,17 @@ internal class SymmetryOverlay : Control
     private double horizontalAxisY;
     private double verticalAxisX;
 
-    private MouseUpdateController mouseUpdateController;
+    private MouseUpdateController? mouseUpdateController;
 
     public SymmetryOverlay()
     {
         Loaded += OnLoaded;
+        Unloaded += OnUnloaded;
+    }
+
+    private void OnUnloaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController?.Dispose();
     }
 
     private void OnLoaded(object sender, RoutedEventArgs e)

+ 13 - 2
src/PixiEditor/Views/UserControls/PreviewWindow.xaml.cs

@@ -54,11 +54,22 @@ internal partial class PreviewWindow : UserControl
     {
         InitializeComponent();
         
-        mouseUpdateController = new MouseUpdateController(imageGrid, ImageGrid_MouseMove);
-        
         imageGrid.MouseRightButtonDown += ImageGrid_MouseRightButtonDown;
         imageGrid.MouseEnter += ImageGrid_MouseEnter;
         imageGrid.MouseLeave += ImageGrid_MouseLeave;
+        
+        imageGrid.Loaded += OnGridLoaded;
+        imageGrid.Unloaded += OnGridUnloaded;
+    }
+
+    private void OnGridUnloaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController?.Dispose();
+    }
+
+    private void OnGridLoaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController = new MouseUpdateController(imageGrid, ImageGrid_MouseMove);
     }
 
     private void ImageGrid_MouseLeave(object sender, MouseEventArgs e)

+ 3 - 3
src/PixiEditor/Views/UserControls/Viewport.xaml.cs

@@ -283,7 +283,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     public Guid GuidValue { get; } = Guid.NewGuid();
 
-    private MouseUpdateController mouseUpdateController;
+    private MouseUpdateController? mouseUpdateController;
 
     public Viewport()
     {
@@ -295,8 +295,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         MainImage!.Loaded += OnImageLoaded;
         Loaded += OnLoad;
         Unloaded += OnUnload;
-        
-        mouseUpdateController = new MouseUpdateController(this, Image_MouseMove);
     }
 
     public Image? MainImage => (Image?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[1];
@@ -310,11 +308,13 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     private void OnUnload(object sender, RoutedEventArgs e)
     {
         Document?.Operations.RemoveViewport(GuidValue);
+        mouseUpdateController?.Dispose();
     }
 
     private void OnLoad(object sender, RoutedEventArgs e)
     {
         Document?.Operations.AddOrUpdateViewport(GetLocation());
+        mouseUpdateController = new MouseUpdateController(this, Image_MouseMove);
     }
 
     private static void OnDocumentChange(DependencyObject viewportObj, DependencyPropertyChangedEventArgs args)