maptool icon indicating copy to clipboard operation
maptool copied to clipboard

[Bug]: onChangeSelection event in HTML5 can get triggered twice unnecessarily

Open Baaaaaz opened this issue 1 year ago • 10 comments

Describe the Bug

If you change token selection to a single new unselected token by clicking on the token using the mouse left-hand button, the onChangeSelection event gets triggered twice: once on mouse left-hand button down and again on either mouse left-hand button up or mouse drag.

To Reproduce

  1. Create a blank campaign.
  2. Create some tokens, one being a lib token.
  3. Add the following macro to the lib token and run it:
[h, if(getSelected()==""): broadcast("NO TOKENS SELECTED") ; broadcast(getSelected())]
[h: oCS = macroLinkText(getMacroName() + "@" + getMacroLocation(), "none")]
[frame5("TEST"), code: {
	<head>
		<link rel='onChangeSelection' type='macro' href='[r: oCS]'>
	</head>
	<body>
		[r: getSelected()]
	</body>
}]
  1. Try selecting tokens as described in the bug description.
  2. Observe the triggering twice issue where the message is broadcasted on mouse down, and again on mouse up or mouse drag.

Expected Behaviour

onChangeSelection should only get fired the once in this scenario.

Screenshots

image

MapTool Info

1.15.2

Desktop

Windows 11

Additional Context

The name of the onChangeSelection event is a bit misleading, however the wiki does state that the event is fired when a token is selected (rather than when the token selection is actually changed!).

e.g. if you left mouse click on a single token that is also the sole currently selected token, onChangeSelection will also be triggered on left mouse up (i.e. unnecessarily as the selection has not effectively changed). Right mouse click on an already selected token does not trigger it. Similarly if no tokens are selected. and you left click somewhere else on the map (that is not a token), onChangeSelection is triggered but again the token selection has not effectively changed.

Baaaaaz avatar Dec 01 '24 10:12 Baaaaaz

This is a documented issue with the onChangeSelection event, which may be called more than once on a change of selection and macro, so this needs to be taken into account. Or at least it used to be documented, but sometimes these warnings get removed from the Wiki. This is due to some internal tomfoolery that happens with how selection works and is never likely to be fixed for the current events

cwisniew avatar Dec 01 '24 12:12 cwisniew

Is there a known work around for this? Or do I have to figure it out for myself? ;)

Tdue21 avatar Mar 10 '25 21:03 Tdue21

Is there a known work around for this? Or do I have to figure it out for myself? ;)

All onChangeSelection event handlers unfortionaly have to be written in a way that takes this into account

cwisniew avatar Mar 10 '25 22:03 cwisniew

A bit of testing shows that the time between the two instances of the event varies somewhat, but is mostly within 100 ms. So I would think checking for a delta of say 200 ms, and ignoring calls within this should suffice.

Tdue21 avatar Mar 11 '25 12:03 Tdue21

A bit of testing shows that the time between the two instances of the event varies somewhat, but is mostly within 100 ms. So I would think checking for a delta of say 200 ms, and ignoring calls within this should suffice.

On your machine... it could vary wildly, but if your macros are only going to be running on the one machine then I guess its ok to use this logic in them

cwisniew avatar Mar 11 '25 13:03 cwisniew

I would love if onChangeSelection could be replaced by a "real" event instead of a frame event. But that is probably a complete different discussion ;)

Tdue21 avatar Mar 11 '25 13:03 Tdue21

I would love if onChangeSelection could be replaced by a "real" event instead of a frame event. But that is probably a complete different discussion ;)

see #5274

cwisniew avatar Mar 11 '25 13:03 cwisniew

A bit of testing shows that the time between the two instances of the event varies somewhat, but is mostly within 100 ms. So I would think checking for a delta of say 200 ms, and ignoring calls within this should suffice.

Care to share how are you are setting this up in your oCS?

FullBleed avatar Mar 11 '25 13:03 FullBleed

Well, it relies on a method Skullman3194 posted in a thread (by me) on the Discord: https://discord.com/channels/296230822262865920/1303376781985841312

But basically it is a macro taking a (url encoded) json object which defines the parameters for the RunJSFunction function. So:

    <link rel="onChangeSelection" type="macro" href="macro://SendNotifyCallback@Lib:dovesoft.trinity-continuum/all/Impersonated?%7B%22name%22%3A%22TC%3A%20%C3%86on%20-%20Initiative%22%2C%22type%22%3A%22frame%22%2C%22func%22%3A%22refreshApp%22%2C%22thisArg%22%3A%22null%22%7D" />

The json object is actually:

{
    "name": "TC: Æon - Initiative",
    "type": "frame",
    "func": "refreshApp",
    "thisArg":"null"
}

The macro:

[h:name = json.get(macro.args, "name")]
[h:type = json.get(macro.args, "type")]
[h:func = json.get(macro.args, "func")]
[h:this = json.get(macro.args, "thisArg")]
[h:args = json.get(macro.args, "funcArgs")]

[h:funcArgs = if(json.type(args) == "ARRAY" , args, "[]")]
[h:frameVisible = (type == "frame" && isFrameVisible(name))]
[h:dialogVisible = (type == "dialog" && isDialogVisible(name))]

[h,if(frameVisible || dialogVisible): runJSFunction(name, type, func, this, funcArgs)]

And the js function in the frame (in this particular case):

    const root = app.mount('#app');
    let lastRefresh = Date.now();

    async function refreshApp() {
        try {
            let thisRefresh = Date.now();
            if (thisRefresh - lastRefresh > 200) {
                lastRefresh = Date.now();
                await root.process("refresh");
            }
        } catch (error) {
            handleException("refreshApp", error);
        }
    }

app is the object representing a Vue application.

Tdue21 avatar Mar 11 '25 16:03 Tdue21

The particular issue in the OP is because we split selection logic between mouse press and mouse release. If we changed that to only modify selections in one place, it would avoid this issue. I would vote for only changing selection on mouse release.

More generally, we can fix redundant events by checking whether anything has changed before firing the event. There is a little bit of this done already, but we can make it universal by leveraging the selection history.

kwvanderlinde avatar Mar 11 '25 19:03 kwvanderlinde