Researching Mouse "Drag Threshold" issues - Thoughts?

Okay, so I decided to do a little research into the whole issue of why the “drag threshold” in Vassal is so hair-trigger, causing one to constantly “lose clicks” (have them interpreted as 1-pixel drags) especially when trying to double-click stacks.

One thing I learned is that, a lot more recently than you’d think, there was no way to set java’s drag threshold (for the drag gesture threshold) AT ALL! So then this bug: bugs.java.com/bugdatabase/view_ … id=4415175 got filed and in due course a new system property was born: awt.dnd.drag.threshold

And so with a mere…
System.setProperty(“awt.dnd.drag.threshold”, “<SOME INTEGER HIGHER THAN FREAKING 1 FOR INSTANCE>”)

…this problem could theoretically go away! But then I actually “tried it”, and at first nothing seemed to happen. So I set it to higher and higher values e.g. surely I couldn’t possibly miss it if it were 80. But it still seemed to do nothing!

So I looked on Stack Overflow for any reference, and I found a single article (stackoverflow.com/questions/223 … -detection) where it was referenced in the second answer, with the interesting aspect that several commenters said that they to had tried the exact thing and it hadn’t worked at all. So that’s a little odd.

So then being “a weirdo” I ran the debugger deep into Java system code (see code snippet below) and determined
(a) The system property does actually get set (e.g. to 80 in my example)
(b) The system property does actually get “checked” at the appropriate time when a drag starts.
(c) BUT… the data for the X and Y differential seemed to be complete garbage. Like I’d move it about 1 pixel and I’d get “origin” of 1312,360 and “current” of 578,455. Made no sense. Almost like… the two points were “in two totally different scales”.
(d) So THEN I said, well what if I zoom the map all the way out to 1-to-1 (100%) scale factor.
(e) LO AND BEHOLD! Suddenly my manually set drag threshold of 80 works perfectly – I have to drag something 80 pixels before it turns into a drag cursor, etc! I suddenly realize that when this is “actually working right”, the normal java default of “5” is probably even about right or at least not-totally-sucky, it’s just that it’s getting garbage data and triggering at even ONE pixel, not even checking the default 5.

SOOOOOOOOoooooooo… after more debugging (I’ve included the key code snippet below), I have determined that at ANY scale other than 100% zoom, the threshold part of the gesture recognizer ends up “comparing apples to oranges” and essentially firing on even a 1 pixel move creating the behavior we all know and don’t love.

So my considered question to the other engineers is: is there anywhere in this chain that VASSAL itself is responsible for populating one or more of these MouseEvents that are getting sent in here? Is it possible that WE are sending improperly scaled data down into the bowels of the gesture recognizer, and therefore screwing up its ability to “recognize drag gestures”, as it were? Because if so, it’s possible that this is an “easy” bug to fix. (The other possibility would be that this has somehow been a bug in Java for over 17 years and nobody has figured it out or reported it – I find this a bit implausible of course).

So if anybody has any thoughts, I would love to hear them.

Yes I’m a bit of a fanatic about mouse interface code. Sorry not sorry :slight_smile:

Brian

//WMouseDragGestureRecognizer
    public void mouseDragged(MouseEvent e) {
        if (!events.isEmpty()) { // gesture pending
            int dop = mapDragOperationFromModifiers(e);

            if (dop == DnDConstants.ACTION_NONE) {
                return;
            }

            MouseEvent trigger = (MouseEvent)events.get(0);

            Point      origin  = trigger.getPoint();
            Point      current = e.getPoint();

            int        dx      = Math.abs(origin.x - current.x);
            int        dy      = Math.abs(origin.y - current.y);

            if (dx > motionThreshold || dy > motionThreshold) {
                fireDragGestureRecognized(dop, ((MouseEvent)getTriggerEvent()).getPoint());
            } else
                appendEvent(e);
        }
    }

Wow, well done!

I don’t know the answer but what I found very weird when I saw how map positions are handled is that Vassal does not have a position model of it’s own and simply uses java.awt.Point.

So it may well be that Vassal feeds the map pixels into the drag threshold logic, where at 100% scale factor 1 map pixel translates to 1 screen pixel, but at other scale factors 1 screen pixel is >1 map pixels and the drag threshold logic gets values that are higher than actual screen pixels.

Sorry to post a reply when I can’t help but I wanted to say thank you for noticing this and looking into it so deeply. It is something I noticed as I have been working on a vassal module but have only just been coming round to the idea that the irritating behaviour might be a bug and not me mis-clicking. I wonder if this might also relate to the random ease with which decks and pieces seem prone to be selected accidentally, to some users at least?

It seems like somehow we’re getting one of the coordinates in “map coordinates” and the other in “component coordinates”. Obviously we want all-component (or all-screen). Now I have to figure out how on earth the map coordinates are getting in there – somehow the DragGesture engine may be registering its mouse gestures into an interface that makes it subject to one of the VASSAL.build.module.Map.java methods that do a componentToMap() translation.

I haven’t worked out yet which of the coordinates (origin or current) is wrong, but it looks more likely that origin might be wrong because I see a mouseReleased() method in Map that does a translation and seems to be forwarding things to mouse listeners, but I don’t see a similar routine for a “mouse moved” situation. Of course since Map is apparently doing coordinate translations for ALL mouse listeners, but we need the DragSource stuff to get pure component, that could create an interesting challenge to somehow figure out which one is the DragSource listener and send it something different.

Thus spake Cattlesquat:

It seems like somehow we’re getting one of the coordinates in “map
coordinates” and the other in “component coordinates”. Obviously we want
all-component (or all-screen). Now I have to figure out how on earth the
map coordinates are getting in there – somehow the DragGesture engine
may be registering its mouse gestures into an interface that makes it
subject to one of the VASSAL.build.module.Map.java methods that do a
componentToMap() translation.

I haven’t worked out yet which of the coordinates (origin or current) is
wrong, but it looks more likely that origin might be wrong because I see
a mouseReleased() method in Map that does a translation and seems to be
forwarding things to mouse listeners, but I don’t see a similar routine
for a “mouse moved” situation. Of course since Map is apparently doing
coordinate translations for ALL mouse listeners, but we need the
DragSource stuff to get pure component, that could create an interesting
challenge to somehow figure out which one is the DragSource listener and
send it something different.

You’re getting into territory here which I passed through for the HiDPI
changes. I can probably answer these questions later this weekend.


J.

Could this be related to the issue I see sometimes, where the mouse pointer is an inch or so away from where it appears to be?

Cool that would be super-useful Joel.

Okay, I think I’m getting really close here, maybe actually done even. Followed a mouse pressed event (on a draggable counter) “through its life cycle”:

First, it shows up in Map, in the mousePressed(e) method, and ends up in this little code fragment:

    else if (multicaster != null) {
      final Point p = componentToMap(e.getPoint());
      e.translatePoint(p.x - e.getX(), p.y - e.getY());
      multicaster.mousePressed(e);
    }

At the time it arrives here, the event(e) is in component coordinates. This method then “translatePoint”'s it, to get it ready to pass to the multicaster (for our internal Vassal event). BUT, since classes are passed by reference in Java, this is now modifying the “original” event as far as whatever deep inner part of Java just sent it to us.

So THEN, my next breakpoint is inside the bowels of Java, in WMouseDragGestureRecognizer:

    public void mousePressed(MouseEvent e) {
        events.clear();

        if (mapDragOperationFromModifiers(e) != DnDConstants.ACTION_NONE) {
            try {
                motionThreshold = DragSource.getDragThreshold();
            } catch (Exception exc) {
                motionThreshold = 5;
            }
            appendEvent(e);
        }
    }

And it’s getting its motionThreshold all picked out and stuff, but guess what… it just received the exact same event (same ID and everything) and NOW it has been translated into “map coordinates”. Of course this part of Java has no idea what to do with map coordinates, and it keeps treating it as if it’s in component coordinates.

Later on, when the mouse moves, WMouseDragGestureRecognizer receives another mouse event for the move, only this time (since Map doesn’t go translating mouseMoved events into map coordinates) it receives the unadulterated component form… and then tries to compare it to the “origin” event (the one from the mousePressed, above) which did get adulterated. Result: oops!

So I think that if in these Map mouse event handlers we simply make a temporary copy of the event, and pass THAT to the internal Java methods instead of changing the one we were passed, that everything will probably “just work”.

I don’t think that anything downstream in Vassal needs to be able to write back into the “original java mouse event”.

So definitely any of you veterans who know this code may have opinions on whether that will work or if there are hidden gotchas involved. But certainly it seems prima facie bad to me that the Java internals are being asked to chew on an event into which we’ve written coordinates that it can’t understand.

I think I can probably code up a potential fix now. Also, now that I’ve figured out that this is actually a bug rather than “an unfortunate dragThreshold choice”, I should probably open a bug :slight_smile:

Yay progress.

Brian

Good work Brian, interesting to see how it behaves when you make the change.

Not my strongest area of expertise, but I think modifying the point inside a system generated event is definitely a mistake.

Is the WMouseDragGestureRecognizer getting the event from the Vassal Multicaster, or is it being passed the same event from the JRE through a different pathway?

If it is coming through the Multicaster, then WMouseDragGestureRecognizer will still not understand the co-oords in your modfified coy of the ebent and you will have the same problem. This will probably need extract code in WMouseDragGestureRecognizer to translate the co-ords.

If it is coming direct from the JRE, then you probably don’t need to do anything else.

As far as I can tell, it’s coming straight from the JRE when it comes into the WMouseDragGestureRecognizer – just getting sent later and on a different pathway. (Unless our multicaster is generating a delayed pathway where it then just doesn’t “appear on the stack”)

I wonder if I can just create a local “MouseEvent” instance right there on the stack, copy the stuff over, do the translation etc on that and forward it on to our stuff.

Should be able to do something like

MouseEvent e2 = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiersEx(), newX, newY,e.getClickCount(), e.isPopupTrigger());

Don’t consume the original event or the DragGestureListener will never see it. Just ignore it and pass your copy of the event to the multicaster.

Okay so THIS new code in mousePressed:

     MouseEvent mapEvent = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiersEx(), e.getX(), e.getY(), e.getXOnScreen(), e.getYOnScreen(), e.getClickCount(), e.isPopupTrigger(), e.getButton());
    if (!mouseListenerStack.isEmpty()) {
      final Point p = componentToMap(mapEvent.getPoint());
      mapEvent.translatePoint(p.x - mapEvent.getX(), p.y - mapEvent.getY());
      mouseListenerStack.get(mouseListenerStack.size()-1).mousePressed(mapEvent);
    }
    else if (multicaster != null) {
      final Point p = componentToMap(mapEvent.getPoint());
      mapEvent.translatePoint(p.x - mapEvent.getX(), p.y - mapEvent.getY());
      multicaster.mousePressed(mapEvent);
    }

(All the subsequent references to “mapEvent” in there used to be references to “e” in original code)

Correctly processes the dragThreshold now, BUT when the drag cursor DOES appear, it appears “in an offset position”, and I can drag it around, and the (offset-looking) place I drop it off ends up being where the piece actually gets dropped on the map.

I don’t see anything screwed up in that code fragment (anybody?) so there could be an additional layer of shenanigans that’s going to have to get figured out.

(But it’s nice to see that the not-overwriting-it theory is at least partially panning out)

Brian

MouseClicked and MouseReleased have the same code and probably need to be changed as well.

I did actually have them changed too w/ the same method.

Oh. I’ll bet now that we’re not scrudging up the event that gets through to DragGestureRecognizer, it’s now sending a drag “origin” in component coordinates that we’re treating as if it’s in map coordinates (because it always used to be!). Should be relatively straightforward to fix if so.

Yep! Is now working. Will scrape a clean PR together and have it up shortly.

Got it. Along with a user preference to increase dragThreshold, because the default 5 is still IMHO awfully tight.

PR#45 is up! Enjoy… github.com/vassalengine/vassal/pull/45

Also, to respond to a couple things people asked/said during the conversation earlier:

Actually that’s a different bug and I’ve checked in a separate PR that should fix it too.

Also: yay! Glad to know I’m not the only one this was bothering!

That’s an interesting problem I’d like to hear more about, especially if it’s “reproducible”. Because in one sense it’s quite possibly related but it’s also probably not exactly this bug. Can you describe the thing you’re doing when that happens? (Are you dragging a piece around? Band-selecting a group of pieces? Just clicking on the map somewhere?) And when you say it’s an inch away from “where it appears to be”, do you mean that when you click something happens an inch away from the pointer instead of where the pointer is?

Same code three times, prime candidate for refactoring. I’ll make a mental note to look into this once the PR gets accepted.

Wait till you see it, it’s sort of like six times :slight_smile:

Yea I’ve already seen the PR, its three methods with three exact same code snippets, and the code in between the snippets looks exact same in two of the methods and slightly different in the third. Looking forward to unify this! :smiley: