'cancelmouseover' hotspot event not sent when the mouse leaves the output window

Posted by Twisol on Tue 05 Jan 2010 08:41 AM — 12 posts, 44,656 views.

USA #0
Because most of my testing is done with my widgets anchored to the top-left corner, I happened to notice that if I move the cursor up and out of the output window, no cancelmouseover is sent to the window until I mouse back over the output window (which could be anywhere else).

Version: 4.45

Example plugin:
<!DOCTYPE muclient>

<muclient>
<plugin
   name="test"
   author="Twisol"
   id="0053f8ba3ff6422de5c96169"
   language="Lua"
   purpose="Test"
   date_written="2010-01-02"
   requires="4.45"
   version="1.0"
   >
</plugin>

<script><![CDATA[

function cancelmouseover()
  print("Mouse left the area")
end

function OnPluginInstall()
  local win = GetPluginID() .. "_test"
  
  WindowCreate(win, 0, 0, 50, 50, 4, 0, 0xFFFFFF)
  WindowAddHotspot(win, "test", 0, 0, 50, 50, nil, "cancelmouseover", nil, nil, nil, nil, 0, 0)
  WindowShow(win, true)
end

]]></script>

</muclient>
Amended on Tue 05 Jan 2010 08:43 AM by Twisol
Australia Forum Administrator #1
This isn't particularly easy to fix. The problem is that the cancelmouseover notification happens when a mouse movement is detected, the mouse movement is over the client area of the window, and it is appropriate (eg. there was a previous mouseover).

Now, outside the client window mouse movements return NCmousemove (non-client moves), which the MFC framework does not send to the CMUSHview class at all. So, an attempt to detect NC moves (which I have tried) has no result.

Also, it would be possible to quickly flick the mouse to another application window, if the window happened to be near the edge (which is probable) of the MUSHclient frame window. In that case, mouse movements (ie. the Windows event messages) now belong to the other application, not MUSHclient.

I would try to work around this as much as possible. Personally I regard mouse-over events as "lower importance" than mouse clicks. That is, you generally might make a button change colour, or display some mouse-over info.

One possible thing to do would be to make a timeout on the mouseover action. That is, if you mouseover something it shows some information, until a cancelmouseover, or 10 seconds elapses, something like that.

Alternatively, you could live with it. Say you mouseover an inventory item, and an information window pops up. Now you mouse out of the client window, take a sort of U-shaped trip, and come back in somewhere else. While the mouse is moving, perhaps the information window is still there, but I think this would be something that wouldn't happen that often that it is annoying.
USA #2
I did a little searching and I came up with the TrackMouseEvent() Win32 method [1], which seems to allow you to register the given window for certain extra events, like WM_MOUSELEAVE [2]. You need do to implement a WindowProc callback (in MFC it's an overridable) in order to catch and process the message [3].

It just seems like it would be really strange and stupid (in terms of MFC, not you) if it was -that- hard to track when the mouse leaves the area, so I had to look for myself. It seems like a simple enough addition, although I could be wrong: add a TrackMouseEvent() call to ::OnMouseMove, and implement a ::WindowProc filter to catch the mouseleave when it occurs.


[1] http://msdn.microsoft.com/en-us/library/ms646265(VS.85).aspx

[2] http://msdn.microsoft.com/en-us/library/ms645533(VS.85).aspx
Quote:
WM_MOUSELEAVE
The WM_MOUSELEAVE message is posted to a window when the cursor leaves the client area of the window specified in a prior call to TrackMouseEvent.

A window receives this message through its WindowProc function.


[3] http://msdn.microsoft.com/en-us/library/8k57wfbs(VS.80).aspx
USA #3
If you're interested, I've composed a little patch for this issue, which I've tested and it seems to be working perfectly fine. Unfortunately, TrackMouseEvent() was introduced in Win98, which means Win95 users (which the MUSHclient front page claims to support) would be left in the dust...
Australia Forum Administrator #4
I see you sent me the patch. :)

This was along the lines of what I was working on. I have amended it slightly to allow for not attempting to do it under Windows 95, and also it is only done if you have a cancel mouse-over function. The TrackMouseEvent is also only called on a mouse-over, to save whatever extra overhead is caused by this additional function call, and the tracking, if the user is not interested in finding when the mouse moves away from this hotspot.

I don't think we need to track on a mouse-down, because you can legitimately keep the mouse down while it wanders out of a window, and then move it back over it again.
USA #5
Ahh, cool. I'm glad it helped! :) Is there any chance I could see the diff for comparison? I'm intrigued.

(I just realized I said TrackMouseOver instead of TrackMouseEvent in the email. Well, you know what I meant.)
Australia Forum Administrator #6
What you had, except this:


***************
*** 6524,6529 ****
--- 6524,6545 ----
            m_nLastToolTipColumn = 0;
              }   // end of having tooltip window

+
+         // capture mouse movements out of the miniwindow (version 4.46)
+         // see: http://www.gammon.com.au/forum/?id=9980
+
+         if (!m_mousedover && !bWin95 && !pHotspot->m_sCancelMouseOver.empty ())
+           {
+           TRACKMOUSEEVENT tme;
+           ZeroMemory (&tme, sizeof tme);
+           tme.cbSize = sizeof(TRACKMOUSEEVENT);
+           tme.dwFlags = TME_LEAVE;
+           tme.hwndTrack = m_hWnd;
+           _TrackMouseEvent(&tme);
+
+               m_mousedover = true;
+          }
+
          }
        else
          {  // same hotspot (new in version 4.45)


Also my CPoint doesn't see to have a SetPoint function (must be newer) so handling the event looked like:


LRESULT CMUSHView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
 switch (message)
   {
    case WM_MOUSELEAVE:
    {
      CMUSHclientDoc* pDoc = GetDocument();
      ASSERT_VALID(pDoc);
      CPoint point (-1, -1);

      m_mousedover = false;

      Mouse_Move_MiniWindow (pDoc, point);

      return 0;
    }  // end of WM_MOUSELEAVE

   }  // end of switch
  
  return CView::WindowProc(message, wParam, lParam);
}



(And the default case wasn't really required).
Amended on Thu 07 Jan 2010 08:09 PM by Nick Gammon
USA #7
Ahh, makes sense. Thanks!

EDIT: I only used the default case out of personal preference, I like it better if all of the "events" are handled and returned from in the same place. Clearly it doesn't matter, but I happened to like the the indentation and location better.
Amended on Thu 07 Jan 2010 08:14 PM by Twisol
Australia Forum Administrator #8
You are probably right, in which case you could argue the final return is not necessary as it can never be reached. How about:


LRESULT CMUSHView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
  switch (message)
   {
    case WM_MOUSELEAVE:
    {
      CMUSHclientDoc* pDoc = GetDocument();
      ASSERT_VALID(pDoc);
      CPoint point (-1, -1);

      m_mousedover = false;

      Mouse_Move_MiniWindow (pDoc, point);

      return 0;
    }  // end of WM_MOUSELEAVE

    default: 
      return CView::WindowProc(message, wParam, lParam);

   }  // end of switch
  
 
  // cannot get here

}

USA #9
Makes sense to me! Actually, if you wanted to handle a message but still pass it upwards, your way would be better: just omit the return 0 from the case block. With mine, you'd need to add a CView::WindowProc() call in the case block instead.

How about a best of both worlds approach?

LRESULT CMUSHView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
  switch (message)
   {
    case WM_MOUSELEAVE:
    {
      CMUSHclientDoc* pDoc = GetDocument();
      ASSERT_VALID(pDoc);
      CPoint point (-1, -1);

      m_mousedover = false;

      Mouse_Move_MiniWindow (pDoc, point);

      return 0;
    }  // end of WM_MOUSELEAVE

    default: 
      return CView::WindowProc(message, wParam, lParam);

   }  // end of switch
  
 
  return CView::WindowProc(message, wParam, lParam);
}
USA #10
You might find that redundant, but I think it's a more accurate representation of my thought process in this function. The switch statement is for explicit handling of events. 'default' just happens to mean all events not mentioned. The second WindowProc() call is just there to pass upwards for any events that have been handled, basically saying "This is by default a -filter- method. To stop a message from propogating, return explicitly when handling it."

(That said, yes, it's redundant. But I like it!)
Amended on Thu 07 Jan 2010 08:42 PM by Twisol
USA #11
Generally it's considered "good practice" to avoid putting in code that looks like it gets executed but actually never will. It's kind of like:


if (a) {
  return 1;
}
else if (b) {
  return 2;
}
else {
  return 3;
}

return 4;


If the function ends with /* not reached */ or something like that, it makes it completely obvious that the switch statement is handling all possible cases. If it ends with a return statement, you need to go figure out what the switch is doing.

Furthermore, and this is an argument that's not just style but actually helpful in practice :-), if at some point you remove the default case from the switch, then having no return statement will cause the compiler to complain at you (not all control paths return for non-void function, or something like that). If you have a return statement there, you might not catch the fact that you have changed your switch statement but need to handle things it doesn't cover anymore.



Basically, as a general principle, redundancy should be avoided when reasonable as it confuses things often without much (if any) gain.