Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

UnityEditor: global mouse & key events?

Discussion in 'Scripting' started by _geo__, Jul 21, 2022.

  1. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    A short story and a question.

    So, I was making some editor tools for myself and it just happened that I wanted (needed really) a way to globally react to input. I had to do it without the help of an EditorWindow or any other UI. A static [InitializeOnLoad] class was all I had. It turned out this was rather tricky to achieve.

    Keyboard Events:
    For Keyboard events I found this thread here whith a solution which uses reflection. Not exactly the ideal solution, but okay, it works.

    Mouse Events:
    I needed some mouse events too, and oh boy, was I out of luck. I could not find a simple way to achieve that. In the end my quest for global mouse events lead me to the win32 api (user32.dll). So, now I am completely bypassing any Unity API. I hook into the native message queue (SetWindowsHookEx) and get my mouse events from there. The big caveat, it only works on Windows and requires specific implementations for other platforms (mac, linux).

    My question: Did I just not find the right UnityEditor API? Is there something like global IO for the Editor which I can hook into? I wouldn't mind reflections (anything is better than native apis).

    Thank you.
     
    Bunny83 likes this.
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,611
    The Event class also works in the editor.
     
    _geo__ and Bunny83 like this.
  3. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    Yes, but Event.current is null if not called from a proper context (aka GUI), right? I have no context to call it from. I tried EditorApplication.update but within that Event.current is always null.
     
    Bunny83 likes this.
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    Well, technically there is no global input even in the OS itself. The OS directs input to the active application, always. That's a key feature of the OS. Yes, the OS provides hooks to hook into the input that is actually directed to another application but that's essentially "a hack". It's a necessary hack when we talk about things like global hotkeys and things like that, though those are quite rare cases and in no way a common thing. Mouse input is usually even more context specific. Note that the Untiy editor can be split up into several native windows (ContainerWindow) which are already treated seperately by the OS depending on which of those windows has the focus. The responsability of the propergation of events inside the application / native window is down to the application itself. Unity handles this through the Event class as spiney mentioned. However as you correctly said, the Event class is only populated if Unity actually handles an event and the corresponding callback. In generic callbacks like EditorApplication.update you can't handle events.

    Native applications generally are event controlled. The application's main thread usually pumps the main event loop that the OS provides. If no event is pending the thread is essentially sleeping and waiting for an event to be added to it's event queue.

    Since we can't really control how the OS or how the Unity Editor handles / distributes the events the only real option is to use a low level hook. Note hooks can be quite dangerous and can easily cause crashes if you do something wrong. Also there are various types of hooks, some require the hook to be inside a seperate DLL (which actually gots injected into every native GUI application), others work only within your own application space. The newer "*_LL" hooks actually work without a seperate DLL as windows actually uses the send message system to direct the information to the target thread. Note that Microsoft does not recommend to use hooks for anything else than debugging purposes because they can affect the system as a whole. Though they are often (mis-) used for such things :)

    After all the Unity editor is not a game but also just an ordinary user application. AFAIK Unity does not provide any direct access to raw input it receives from the OS.

    Besides the dedicated mouse and keyboard hooks you could also implement an GET_MESSAGE hook like I did over here to receive file drag and drop messages (WM_DROP) from windows.

    Your usage or requirement is quite vague. "Globally" can have many different meanings. System global or application global? Can you share any details about the usecase?
     
    _geo__ likes this.
  5. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    Thanks for taking the time to reply in such detail :)

    The concrete use case is that I have implemented support for shortcuts for the 4th and 5th mouse button. I am storing a history of the last few selections and then use the buttons on my mouse to move up and down in the history (very handy). It works just like moving back and forth in a webbrowser (that's where I got the idea from).

    By "global" I mean independent of which Unity window (EditorWindow, not native window) is currently focused and without any custom GUI. I don't need any info from outside Unity or if the Editor is not focused.
    I was actually shocked that with WH_MOUSE_LL I got EVERYTHING. I am amazed Windows is even working with such tools available. I mean, just one bad program (mine) can make the mouse cursor lag like crazy :eek:.

    Oh, right. I'll consider this an edge case to deal with later. For now it's one window only.

    Yes, that's why I am in search of an alternative. I've got it working but I really don't like this. My very first attempt was using WH_MOUSE_LL. But, woha, those *_LL cause a LOT of side effects. The upside: I learned a lot about win32 API event queue :D

    Yes, I stumbled across that. I have been using WH_MOUSE with a threadId specified, so not extra DLL is needed. I want to keep it as local as possible (still scared from what WH_MOUSE_LL did to my cursor). I then had to settle for WH_GETMESSAGE because the wParam of WH_MOUSE just did not have the info (which XBUTTON) I needed. But to be honest, I don't even like the idea of listening to all those messages when there would be a dedicated mouse filter available.

    Thanks. I have only glanced over it but from what I see you are using the same message (WH_GETMESSAGE) as I do. Gotta dig into it later.


    To give you and idea. Here is the code I am currently using:
    Code (CSharp):
    1. #if UNITY_EDITOR_WIN
    2. using System;
    3. using System.Diagnostics;
    4. using System.Runtime.InteropServices;
    5.  
    6. namespace Kamgam.NativeMouseHookLib
    7. {
    8.     public static partial class NativeMouseHook
    9.     {
    10.         public static bool IsSupported = true;
    11.  
    12.         private static HookMessageDelegate callback = MessageHookCallback;
    13.         private static IntPtr hookID = IntPtr.Zero;
    14.  
    15.         public static void Install()
    16.         {
    17.             hookID = SetHook(callback);
    18.         }
    19.  
    20.         public static void Uninstall()
    21.         {
    22.             UnhookWindowsHookEx(hookID);
    23.         }
    24.  
    25.         private static IntPtr SetHook(HookMessageDelegate callback)
    26.         {
    27.             using (Process curProcess = Process.GetCurrentProcess())
    28.             {
    29.                 uint threadId = GetCurrentThreadId();
    30.  
    31.                 // See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa
    32.                 return SetWindowsHookEx(
    33.                     WH_GETMESSAGE, // Thou shall not use WH_MOUSE_LL ;), but WH_MOUSE is missing 4th an 5th button infos, thus WH_GETMESSAGE
    34.                     callback,      // The callback procedure is within the same process.
    35.                     IntPtr.Zero,   // The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread
    36.                                    // created by the current process and if the hook procedure is within the code associated
    37.                                    // with the current process.
    38.                     threadId       // We do specify a native thread id.
    39.                     );
    40.             }
    41.         }
    42.  
    43.         /// <summary>
    44.         /// Message proc callback.<br />
    45.         /// see: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644981(v=vs.85)
    46.         /// </summary>
    47.         /// <param name="nCode">Specifies whether the hook procedure must process the message.<br />
    48.         /// If code is HC_ACTION, the hook procedure must process the message. If code is less than zero,
    49.         /// the hook procedure must pass the message to the CallNextHookEx function without further processing and
    50.         /// should return the value returned by CallNextHookEx.<br /><br />
    51.         ///
    52.         /// HC_ACTION 0 = The wParam and lParam parameters contain information about a mouse message.<br />
    53.         /// HC_NOREMOVE = 3 The wParam and lParam parameters contain information about a mouse message, and the mouse message has not been removed from the message queue.
    54.         /// </param>
    55.         /// <param name="wParam">Specifies whether the message has been removed from the queue. This parameter can be one of the following values.</param>
    56.         /// <param name="lParam">A pointer to a MSG struct, see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg?redirectedfrom=MSDN</param>
    57.         /// <returns></returns>
    58.         private delegate IntPtr HookMessageDelegate(int nCode, IntPtr wParam, IntPtr lParam);
    59.  
    60.         private static IntPtr MessageHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    61.         {
    62.             if (nCode >= 0)
    63.             {
    64.                 Msg msg = Marshal.PtrToStructure<Msg>(lParam);
    65.                 MessageIdentifier msgId = (MessageIdentifier) msg.message;
    66.                 switch (msgId)
    67.                 {
    68.                     case MessageIdentifier.WM_LBUTTONDOWN:
    69.                         MouseEvent.Invoke(NativeMouseEvent.LeftDown);
    70.                         break;
    71.  
    72.                     case MessageIdentifier.WM_LBUTTONUP:
    73.                         MouseEvent.Invoke(NativeMouseEvent.LeftUp);
    74.                         break;
    75.  
    76.                     // Performance hog, not needed.
    77.                     //case MessageIdentifier.WM_MOUSEMOVE:
    78.                     //    MouseEvent.Invoke(MouseButtonEvent.LeftUp);
    79.                     //    break;
    80.  
    81.                     case MessageIdentifier.WM_MOUSEWHEEL:
    82.                         MouseEvent.Invoke(NativeMouseEvent.WheelTurned);
    83.                         break;
    84.  
    85.                     case MessageIdentifier.WM_MBUTTONDOWN:
    86.                         MouseEvent.Invoke(NativeMouseEvent.MiddleDown);
    87.                         break;
    88.  
    89.                     case MessageIdentifier.WM_MBUTTONUP:
    90.                         MouseEvent.Invoke(NativeMouseEvent.MiddleUp);
    91.                         break;
    92.  
    93.                     case MessageIdentifier.WM_RBUTTONDOWN:
    94.                         MouseEvent.Invoke(NativeMouseEvent.RightDown);
    95.                         break;
    96.  
    97.                     case MessageIdentifier.WM_RBUTTONUP:
    98.                         MouseEvent.Invoke(NativeMouseEvent.RightUp);
    99.                         break;
    100.  
    101.                     case MessageIdentifier.WM_XBUTTONDOWN:
    102.                         var downBtn = HighWord((uint)msg.wParam);
    103.                         if (downBtn == XBUTTON1)
    104.                             MouseEvent.Invoke(NativeMouseEvent.FourthDown);
    105.                         else if (downBtn == XBUTTON2)
    106.                             MouseEvent.Invoke(NativeMouseEvent.FifthDown);
    107.                         break;
    108.  
    109.                     case MessageIdentifier.WM_XBUTTONUP:
    110.                         var upBtn = HighWord((uint)msg.wParam);
    111.                         if (upBtn == XBUTTON1)
    112.                             MouseEvent.Invoke(NativeMouseEvent.FourthUp);
    113.                         else if (upBtn == XBUTTON2)
    114.                             MouseEvent.Invoke(NativeMouseEvent.FifthUp);
    115.                         break;
    116.                 }
    117.             }
    118.  
    119.             return CallNextHookEx(hookID, nCode, wParam, lParam);
    120.         }
    121.  
    122.         public static ushort LowWord(uint val)
    123.         {
    124.             return (ushort)val;
    125.         }
    126.  
    127.         public static ushort HighWord(uint val)
    128.         {
    129.             return (ushort)(val >> 16);
    130.         }
    131.  
    132.         /// <summary>
    133.         /// High level mouse events (for one application or thread).<br />
    134.         /// Was used initially but it seems the information to differentiate
    135.         /// between WM_XBUTTONDOWN buttons was missing. Thus now WH_GETMESSAGE
    136.         /// is used because the MSG struct contains that info.
    137.         /// </summary>
    138.         private const int WH_MOUSE = 7;
    139.  
    140.         // Message events for the window.
    141.         private const int WH_GETMESSAGE = 3;
    142.  
    143.         /// <summary>
    144.         /// Mouse Input Notifications<br />
    145.         /// see: https://docs.microsoft.com/en-us/windows/win32/inputdev/mouse-input-notifications
    146.         /// </summary>
    147.         private enum MessageIdentifier
    148.         {
    149.             WM_LBUTTONDOWN = 0x0201,
    150.             WM_LBUTTONUP = 0x0202,
    151.             WM_MOUSEMOVE = 0x0200,
    152.             WM_MOUSEWHEEL = 0x020A,
    153.             WM_MBUTTONDOWN = 0x0207,
    154.             WM_MBUTTONUP = 0x0208,
    155.             WM_RBUTTONDOWN = 0x0204,
    156.             WM_RBUTTONUP = 0x0205,
    157.             WM_XBUTTONDOWN = 0x020B,
    158.             WM_XBUTTONUP = 0x020C
    159.         }
    160.  
    161.         /// <summary>
    162.         /// See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-get_xbutton_wparam<br />
    163.         /// Actually the documentation there is WRONG. The GET_XBUTTON_WPARAM = HIWORD = returns a WORD (unsigned int 16). Specifically the high word ot he given wParam.<br />
    164.         /// </summary>
    165.         private const short XBUTTON1 = 0x0001;
    166.         private const short XBUTTON2 = 0x0002;
    167.  
    168.         /// <summary>
    169.         /// The MSG structure which lParam points to.<br />
    170.         /// See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mousehookstruct
    171.         /// </summary>
    172.         [StructLayout(LayoutKind.Sequential)]
    173.         private struct Msg
    174.         {
    175.             public IntPtr hwnd;
    176.             public uint message;
    177.             public IntPtr wParam;
    178.             public IntPtr lParam;
    179.             public short time;
    180.             public Point pt;
    181.             public short lPrivate;
    182.         }
    183.  
    184.         [StructLayout(LayoutKind.Sequential)]
    185.         private struct Point
    186.         {
    187.             public int x;
    188.             public int y;
    189.         }
    190.  
    191.         /// <summary>
    192.         /// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa
    193.         /// </summary>
    194.         [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    195.         private static extern IntPtr SetWindowsHookEx(int idHook,
    196.           HookMessageDelegate lpfn, IntPtr hMod, uint dwThreadId);
    197.  
    198.         [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    199.         [return: MarshalAs(UnmanagedType.Bool)]
    200.         private static extern bool UnhookWindowsHookEx(IntPtr hhk);
    201.  
    202.         [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    203.         private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    204.  
    205.         [DllImport("kernel32.dll")]
    206.         private static extern uint GetCurrentThreadId();
    207.     }
    208. }
    209. #endif
    What I do not understand is why Unity won't give us some hook into the events they receive. They already do all the "find out which mouse button was pressed" stuff. Now I have to do it all by myself (reinventing the wheel).
     
    Last edited: Jul 21, 2022
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,611
    The only way I know to make custom 'global' shortcuts is to use a MenuItem, which lets you define a keyboard shortcut.

    Are your extra mouse buttons are tied to actual keyboard buttons? That's the case with my fancy mouse and it's extra six buttons. You could just make a few MenuItem's, tied to these buttons, and Bob's your uncle!
     
    Last edited: Jul 22, 2022
    _geo__ likes this.
  7. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    As far as I have read in the win api docs only the sixth, seventh, .. mouse buttons are reported as keyboard keys. I am after the fourth and fifth and these are still considered mouse buttons.

    If have checked the MenuItem manual to see if it is possible to maybe add some combination of pressing a key + a mouse button but there does not seem to be a shortcut syntax for mouse buttons. Was a good idea though, thanks :)
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,611
    Do you have any software with your mouse - or is available for your mouse - that lets you customise what the buttons do? Most mice have some OTT software for customising what everything does.
     
    _geo__ likes this.
  9. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    I see what your are going for (configure the mouse to report as keys or make it trigger some keys to easily catch them in unity). Though, my goal is to make it work without any custom setup (and multi platform if possible) as it's not only for me. I know I am a picky b.....d ^^
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,611
    Ah! Totally understandable. I do agree and wonder why we can't globally listen to these inputs in a context independent of any particular window. We can't be the first users to wonder this.
     
    _geo__ likes this.
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    Well, Unity is a native application and Unity probably has to go though all sorts of steps to get a unified input regardless of platform (windows, MacOS, Linux). On Windows every application has a main message loop and it belongs to Unity. Installing the GetMessage hook is probably the best you can do. Every application has this message loop and has to process all messages that are directed towards this thread / application. You just hook into it, essentially extending the functionality. I doubt there's currently a better way. You probably won't find any application that directly provides a way to hook into the message loop natively. Even if it does it wouldn't be much more different than using a GetMessage hook.

    An alternative could be to use RegisterHotKey. It registers a system global hot key. Programs like AutoHotKey use this as well (besides other things like hooks). Though not all virtual keys are supported. So even AHK does fall back to using a hook.

    Personally I always have other functions on my two XButtons ^^. Usually discord mute and some other things (mostly autohotkey stuff :) )
     
    _geo__ likes this.
  12. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    That's exactly what I was wondering about too. Why don't they just forward us the unified IO which I am sure some Untiy Engineer has worked hard to get right.

    Haha, then you are not my target audience :rolleyes:

    I (unreasonably) expect these mouse buttons to do something similar in every application. Which is to go back and forth in "history" just like my browser does. Fells like the established norm to me (maybe/probably I am off).

    I have been using AutoHotKey in the past too. Whenever I use these tools I kinda stop using them after a while because most of them are not portable (Win/Mac/Linux) and when I switch workstations all my hotkeys are suddenly gone. I then get tired of reconfiguring them. But I digress.

    I've found this little method Event.GetEventCount(). A count usually indicates some sort of internal collection (event queue). So I imagine maybe I could query that with reflections. But then, when is the right time to look at it? Is it even accessible in managed c# land? Does it contain the infos I need? In the end I gave up on that. The hook is good enough for me atm.

    Thanks to both of you for your time.
    Case closed :)
     
  13. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    This method was added with the "new(er)" uGUI system when it replaced the IMGUI system. It belongs to Event.PopEvent. This is used by uGUI's Input fields internally to get the currently queued keyboard events. Though there's a catch: Once you call PopEvent, the event is removed from the queue. So there can only be one consumer of this.

    I was kinda surprised that they made this part of the API public as it's really only used internally and using it manually can easily screw up the uGUI system, depending on how you use that API. I'm not even sure PopEvent works in the editor, at edit time. Since there is no "PeekEvent" (as there usually is when you have a queue), it's a bit difficult to work with this as you can not check the event without popping it. A bit of quantum mechanics flavour I guess ^^
     
    _geo__ likes this.
  14. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,298
    And so this idea goes down the drain. Glad I did not pursue it any further. Thank you for the intel!
     
  15. Fengist

    Fengist

    Joined:
    Oct 3, 2017
    Posts:
    5
    Dunno if this will help anyone but since this is the top result for Unity global mouse events, here goes.

    So, I wanted a context menu to close (active false) when anything else was clicked on. I tried just about everything I could find including custom StandaloneInputModules to detect a global mouse event on my 2d project. I even tried attaching a script with IPointerUpHandler & IPointerDownHandler to the canvas and that worked to a point. The problem with that was if any other gameobject (panels) had a script with IPointerDownHandler, the canvas script would just get ignored. If I removed the script from the gameobject, the canvas script would then detect the mouse event. Even tried just the IPointerUpHandler on the canvas script. That ran into the bug where you HAVE to have the IPointerDownHandler on otherwise the up handler doesn't work at all, which caused the mouse up events to be ignored.

    Finally, this really dumb and simple solution, and yea this will eat a few extra cpu cycles because it's constantly checking but... it works.

    I put this on the canvas.

    Code (CSharp):
    1.  
    2. void Update()
    3.     {
    4.         if (Input.GetMouseButtonDown(0))
    5.                 //do stuff when the mouse button is down
    6.     }
    7.  
    With that, I was able to raycast under the mouse position and get the PointerEventData(EventSystem.current) list, drill down in the hierarchy and find out if they were clicking on any part of the context menu and if not, close it.
     
    Last edited: Nov 27, 2022