Browse Source

Merge pull request #55 from hackf5/global-pen-up-issue-53

Resolved issue #53
Krzysztof Krysiński 4 years ago
parent
commit
b3e7cfeb0f

+ 131 - 0
PixiEditor/Helpers/GlobalMouseHook.cs

@@ -0,0 +1,131 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+namespace PixiEditor.Helpers
+{
+    // see https://stackoverflow.com/questions/22659925/how-to-capture-mouseup-event-outside-the-wpf-window
+    [ExcludeFromCodeCoverage]
+    public static class GlobalMouseHook
+    {
+        private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
+        private static int _mouseHookHandle;
+        private static HookProc _mouseDelegate;
+
+        private static event MouseUpEventHandler MouseUp;
+        public static event MouseUpEventHandler OnMouseUp
+        {
+            add
+            {
+                Subscribe();
+                MouseUp += value;
+            }
+            remove
+            {
+                MouseUp -= value;
+                Unsubscribe();
+            }
+        }
+
+        public static void RaiseMouseUp()
+        {
+            MouseUp?.Invoke(default, default);
+        }
+
+        private static void Unsubscribe()
+        {
+            if (_mouseHookHandle != 0)
+            {
+                int result = UnhookWindowsHookEx(_mouseHookHandle);
+                _mouseHookHandle = 0;
+                _mouseDelegate = null;
+                if (result == 0)
+                {
+                    int errorCode = Marshal.GetLastWin32Error();
+                    throw new Win32Exception(errorCode);
+                }
+            }
+        }
+
+        private static void Subscribe()
+        {
+            if (_mouseHookHandle == 0)
+            {
+                _mouseDelegate = MouseHookProc;
+                _mouseHookHandle = SetWindowsHookEx(
+                    WH_MOUSE_LL,
+                    _mouseDelegate,
+                    GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
+                    0);
+                if (_mouseHookHandle == 0)
+                {
+                    int errorCode = Marshal.GetLastWin32Error();
+                    throw new Win32Exception(errorCode);
+                }
+            }
+        }
+
+        private static int MouseHookProc(int nCode, int wParam, IntPtr lParam)
+        {
+            if (nCode >= 0)
+            {
+                MSLLHOOKSTRUCT mouseHookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
+                if (wParam == WM_LBUTTONUP)
+                {
+                    if (MouseUp != null)
+                    {
+                        MouseUp.Invoke(null, new Point(mouseHookStruct.pt.x, mouseHookStruct.pt.y));
+                    }
+                }
+            }
+            return CallNextHookEx(_mouseHookHandle, nCode, wParam, lParam);
+        }
+
+        private const int WH_MOUSE_LL = 14;
+        private const int WM_LBUTTONUP = 0x0202;
+
+        [StructLayout(LayoutKind.Sequential)]
+        private struct POINT
+        {
+            public int x;
+            public int y;
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        private struct MSLLHOOKSTRUCT
+        {
+            public POINT pt;
+            public uint mouseData;
+            public uint flags;
+            public uint time;
+            public IntPtr dwExtraInfo;
+        }
+
+        [DllImport("user32.dll", 
+            CharSet = CharSet.Auto,
+            CallingConvention = CallingConvention.StdCall, 
+            SetLastError = true)]
+        private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
+
+        [DllImport(
+            "user32.dll",
+            CharSet = CharSet.Auto,
+            CallingConvention = CallingConvention.StdCall,
+            SetLastError = true)]
+        private static extern int UnhookWindowsHookEx(int idHook);
+
+        [DllImport(
+            "user32.dll", 
+            CharSet = CharSet.Auto,
+            CallingConvention = CallingConvention.StdCall)]
+        private static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);
+
+        [DllImport("kernel32.dll")]
+        private static extern IntPtr GetModuleHandle(string name);
+    }
+
+    public delegate void MouseUpEventHandler(object sender, Point p);
+}

+ 12 - 11
PixiEditor/ViewModels/ViewModelMain.cs

@@ -54,7 +54,6 @@ namespace PixiEditor.ViewModels
         public RelayCommand ExportFileCommand { get; set; } //Command that is used to save file
         public RelayCommand UndoCommand { get; set; }
         public RelayCommand RedoCommand { get; set; }
-        public RelayCommand MouseUpCommand { get; set; }
         public RelayCommand OpenFileCommand { get; set; }
         public RelayCommand SetActiveLayerCommand { get; set; }
         public RelayCommand NewLayerCommand { get; set; }
@@ -211,7 +210,6 @@ namespace PixiEditor.ViewModels
             ExportFileCommand = new RelayCommand(ExportFile, CanSave);
             UndoCommand = new RelayCommand(Undo, CanUndo);
             RedoCommand = new RelayCommand(Redo, CanRedo);
-            MouseUpCommand = new RelayCommand(MouseUp);
             OpenFileCommand = new RelayCommand(Open);
             SetActiveLayerCommand = new RelayCommand(SetActiveLayer);
             NewLayerCommand = new RelayCommand(NewLayer, CanCreateNewLayer);
@@ -659,15 +657,6 @@ namespace PixiEditor.ViewModels
                 ToolCursor = Cursors.Arrow;
         }
 
-        /// <summary>
-        ///     When mouse is up stops recording changes.
-        /// </summary>
-        /// <param name="parameter"></param>
-        private void MouseUp(object parameter)
-        {
-            BitmapManager.MouseController.StopRecordingMouseMovementChanges();
-        }
-
         private void MouseDown(object parameter)
         {
             if (BitmapManager.ActiveDocument.Layers.Count == 0) return;
@@ -681,6 +670,18 @@ namespace PixiEditor.ViewModels
                     BitmapManager.MouseController.RecordMouseMovementChange(MousePositionConverter.CurrentCoordinates);
                 }
             }
+
+            // Mouse down is guaranteed to only be raised from within this application, so by subscribing here we
+            // only listen for mouse up events that occurred as a result of a mouse down within this application.
+            // This seems better than maintaining a global listener indefinitely.
+            GlobalMouseHook.OnMouseUp += MouseHook_OnMouseUp;
+        }
+
+        // this is public for testing.
+        public void MouseHook_OnMouseUp(object sender, Point p)
+        {
+            GlobalMouseHook.OnMouseUp -= MouseHook_OnMouseUp;
+            BitmapManager.MouseController.StopRecordingMouseMovementChanges();
         }
 
         /// <summary>

+ 2 - 1
PixiEditorTests/ViewModelsTests/ViewModelMainTests.cs

@@ -1,5 +1,6 @@
 using System.IO;
 using System.Windows.Media;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
@@ -87,7 +88,7 @@ namespace PixiEditorTests.ViewModelsTests
 
             Assert.True(viewModel.BitmapManager.MouseController.IsRecordingChanges);
 
-            viewModel.MouseUpCommand.Execute(null);
+            viewModel.MouseHook_OnMouseUp(default, default);
 
             Assert.False(viewModel.BitmapManager.MouseController.IsRecordingChanges);
         }