Differentiate between script and real "selchange" events in custom dialogs

I'm trying to have an "undo selection" button for a multi-select listview
So on every selchange event I'm saving the current selection in some global vector
On button click I restore lv.value = old_sel of this listview to some previously saved state.
But I want these "artificial" selection changes to be ignored and not saved. I've tried setting a global nosave variable before setting the value and after it, but it seems to have no effect as, I guess, this artificial selection happens asynchronously

So how can I differentiate between these two types of selchange events?

Using a variable should work to know when the script is reacting to events it has triggered itself.

So you're saying that sel_save within a hotkey event should be set to true only AFTER all selchange events from .value= are processed? Because all the selchange events print sel_save as true all the time even for these events

sel_save = false; // prevent this event from being recorded
dbg("undid selection, set sel_save to false");
lv.value=sel_ids;
dbg("reset sel_save to true");
sel_save = true; // prevent this event from being recorded

(will try to prepare a simplified example later)

In this example I never get a red error print activated if sel_save is false even though I set it to false before manually changing selection

Test.dcf (4.7 KB)

I see what you mean. Not what I expected either. Maybe we can improve things, with a way to suppress the notifications or something.

For now, you can work around the problem using a timer:

var lv_name = '‹tab✗';

function guitest(icon)
{
	var Dlg = DOpus.Dlg; Dlg.template = "✗Tabs";
	var DC=DOpus.Create;
	Dlg.Create(); // create detached
	var lv=Dlg.Control(lv_name);

	var j = 5; while (j--) {
		var i = lv.AddItem("a"+j);
		lv.GetItemAt(i).subitems(0) = "lbl"+j;
		lv.GetItemAt(i).subitems(1) = "path"+j;
	}

	var sel_save = true;
	while (true) {
	    var Msg = Dlg.GetMsg();
	    if (!Msg.result) break;

	    if (Msg.event === "selchange") {
			if (sel_save) {
				dbgv("selchange true");
			} else {
				err("selchange false");
			}
		}

		if (Msg.event === "click") {
			if (Msg.Control === "btnU") {
				p("Undo start");
				sel_save = false;
				var lv=Dlg.Control(lv_name);
				lv.value = DC.Vector(0,2); // select 1st and 3rd items
				Dlg.SetTimer(1,"sel_save");
				p("Undo end");
			}
		}

		if (Msg.event === "timer") {
			if (Msg.Control ==="sel_save") {
				Dlg.KillTimer("sel_save");
				sel_save = true;
				dbgv("Timer");
			}
		}
	}
	retVal = Dlg.result;
}

function OnClick(cmdD) {
 var aaa = guitest();
}

function T(t) {return DOpus.TypeOf(t)}
function p(text) {DOpus.Output(text     );}
function dbg(text) {DOpus.Output(text     );}
function dbgv(text) {DOpus.Output(text     );}
function err(text) {DOpus.Output(text,true     );}

Thanks, have considered timers, but haven't tested them whether they'd differentiate between a key hold/multiselects and artificial send yet

When you improve this, please also add a way to "group" one batch of events together as otherwise I don't know how to do undo properly since the number of events can differ depending on the type of user action, so I don't know by how many events to go back to to restore a single action

I'm not sure we can group the events in any meaningful way. They come from the operating system as individual events, and we just forward them to the script.

I see, are you aware of any apps caring enough about user selection to preserve it :slight_smile: (hate it when you carefuly multi-select something and then 1 wrong click ruins everything)?

What do they do?

I guess one option would be to carefully track key/mouse events to divine start/end of 1 user action, but then scripts don't have access to those, only its hotkeys

You could use another timer, started in the selchange events, so you only save the state once when the timer fires, after things settle down.

Maybe will try that or just do undos by 1 (de)sel event, after all, you could just hold the undo button to make it go faster

1 Like

This example is straigt forward but will only work for event driven/asynchronous languages. So you change the var, then the selection and then the var again, but the event for the selection change will be evaluated in the same loop later, so this approach does not work. What will work is when you know the number of elements that changed and ignore selchange events that often. I prepared an example for you here

var ItemsChangedFromSoftware = 0;
var ListView;
function OnClick(clickData)
{
    var dlg = DOpus.Dlg;
	dlg.template = "dialog";
	dlg.Create();	

	ListView = dlg.Control("listview1");
	 
	for(var i = 1; i < 5; i++)
	{
		var index = ListView.AddItem("item " + i);
		ListView.GetItemAt(index).subitems(0) = "lbl"+i;
		ListView.GetItemAt(index).subitems(1) = "path"+i;
	}

	
  	while (true) 
	{
    	var Msg = dlg.GetMsg();
    	if (!Msg.result)
			break;
			
    	if (Msg.event === "selchange") 
			OnSelectionChanged();

		if (Msg.event === "click" && Msg.Control === "button1") 
			ChangeSelectionFromSoftware(DOpus.Create.Vector(0,2));// select 1st and 3rd items
	}
}

function OnSelectionChanged()
{
	if(ItemsChangedFromSoftware > 0) //Ignore as many events as items changed
	{
		Log("Software change");
		ItemsChangedFromSoftware--;
		return;
	}
	Log("Now user selection event");
}

function ChangeSelectionFromSoftware(selection)
{
	ListView.value = selection; 
	ItemsChangedFromSoftware = selection.count;
}

function Log(msg, e)
{
	DOpus.Output(String(msg), e || false);
}

Selection Changed.dcf (4.1 KB)

1 Like

I had some sort of issue with this in a dialog where some change in a combo box would modify many controls state (value, enabled, ...).
After the first callback ended, most of the modifications triggered events for the changes that I did not want to consider as user changes.
I used the rather new Dialog.FlushMsg() function in my first callback to avoid that. Since its a user action and first callback triggers quickly, the chance for another action from the user dropped by this FlushMsg seems very low.

Sounds like an even better solution, didnt know about that and just posted a solution I came up with some years ago since I struggled with that as well.

Worst case could be an incoming response from a http request being dropped. Yet that is an edge case.

Another option is to go for the global variable flag but to set it not in the callback but in another callback that you trigger through custom messages you can now send in the message loop :

// sel_save = false; // prevent this event from being recorded
DOpus.SendCustomMsg("SetSaveStatus", -1);
dbg("undid selection, set sel_save to false");
lv.value=sel_ids;
dbg("reset sel_save to true");
// sel_save = true; // prevent this event from being recorded
DOpus.SendCustomMsg("SetSaveStatus", 0);

// You will have to code the callback to this new custom event, setting the global var according to the second parameter.
1 Like