Unintrusive & robust generic window (graph) hook

So, we know that there is no generic window hook which one could tap into to receive window events from a random window of interest. So, I try to create something as close as possible by setting up a hook for target windows only when necessary. The whole thing should be as non-intrusive as possible, i.e., not create a mess in the experiment or with the window itself. Also, and this is important, the hook should also not throw errors left and right when no additional procedures are loaded, i.e., sharing the experiment with others should be supported. The best idea I came up with is to inject a generic hook function into the main procedure window such as the one below, and then install the hook into the window whenever needed. Hooks are all removed when I don't need them anymore.

For now I only want to catch key presses, hence the eventCode condition. The return is handled with one of the internal variables, which is hopefully completely unused otherwise. I could add a global variable, but this would add even more overhead to the function and the experiment file. I couldn't figure out a neat way to pass more than a single item from the structure through the Execute command, so only the keycode is passed for now.

function GT_GenericHook(STRUCT WMWinHookStruct &s)
    if (strlen(FunctionInfo("MyHookProcessingFunc")) && s.eventCode == 11)
        K10 = 0; Execute/Q/Z "K10 = MyHookProcessingFunc("+num2str(s.keycode)+")"
        return K10
    endif
end

Any comments or better ideas are welcome. Meanwhile, I will play around with this idea a bit more.

Are you simply saying that you are playing around to create a robust way to fix the issues caused by folks who develop packages where we embed hook functions in windows yet do not make sure that we clean up behind those hook functions so that others who use our packages can share their experiments generated within those packages with folks who don't install our packages in the first place?

No way!!!

:-)

No!!! The WM staff will be out of work in no time with all the support emails complaining about Igor-unrelated errors drying up! :-)

But on a more serious note, I don't see that all package hooks could or should be rerouted like this just to make them error-safe. I specifically want this to attach hooks to normal graphs, which otherwise have no obvious differences to other graphs, to avoid that suddenly errors are thrown around just because the some random procedures are not loaded. If a panel stops working because the user forgot to close it before sending the experiment file out into the world, then at least it is kind of obvious that something is missing.

I really wish that there was either a '/Z flag' for hooks or a generic, built-in window hook firing from the topmost window or something along these lines. For now, I try to work with want we have. Specifically, with above example, I wonder if there is a drawback in using the global variable and whether there is a better way than using the Execute workaround.

if you have a generic hook that is applied to all windows of a given type using AfterWindowCreatedHook (I do), you could use IgorBeforeQuitHook to clean up and IgorStartOrNewHook to reset (i don't). This make me realize that that I should never attempt to share an Igor Pro experiment.

Edit: AfterFileOpenHook is the appropriate startup hook, not IgorStartOrNewHook.

> This make[s] me realize that that I should never attempt to share an Igor Pro experiment.

Oh Lord no please Tony. Your packages (and those from @chozo) are of far too much value that we should be denied the honor of having them shared. Especially also for those of us who love to "borrow" trade secrets.

The trick as I suspect (and that we likely know) is to create a stand alone experiment to share, trusting that WM has taken care to make the "self-containing" process truly a way to create a robust, self-contained experiment in its own right. I've made use of this approach as part of my DemoSetup Tools package, and it has worked rather well across implementations for many of my internally posted as well as externally posted interactive, educational demonstrations.

So, rather than writing a hook function that should check behind to clean up from those of us who are wise yet not quite wise enough to know how to clean up properly on such things, perhaps the better approach would be to create a package with a function called something akin to "ShareExperiment" and have this new function become a (standard) selection option from the File menu.

As a follow up, two additional ideas strike me as possible.

* Create a general purpose ClearAllHooks([string winnm]) function that a) parses the WinRec of window winnm (or the front most window), b) assembles all hook(...)= strings, c) generates hook(...)=$"" strings for each one, and d) runs them.

* Ask package developers to create functions with prefix names ClearAllHooks_SUFFIX that will clear any hooks that they create, allowing you to create a BeforeQuit hook in your own package that a) searches for all such (hook clearing) functions and b) runs them.

 

Yes, this is one option I would deem necessary. I was event thinking to create a self-clearing function upon the deactivate event, since I personally would only need the hook to work when the user is interacting with a graph.

Comments to your ideas:

* I would think clearing ALL hooks might be overkill and possibly destroys other packages (such as Multipeak fitting). The package creator should know the named hooks he/she has created. So it would be enough to run hook(myPackageHook)=$"" over all windows.

* I would use BeforeExperimentSaveHook, which is the relevant time you don't want the hooks to disappear. The only complication is that the hooks must be reinstalled if the user keeps interacting with the windows. Maybe have a [Kill hooks => Save => immediately reinstall hooks] kind of function. A bit of a mess though.

I am currently steering away a bit from the idea though, since I keep finding gotchas which makes this a headache to manage. :-(

// newly created graph windows have a hook set
static function AfterWindowCreatedHook(string win, variable type)
    if (type == 1)
        SetWindow $win hook(hMyHook)=MyIndispensableGraphHookFunction
    endif
    return 0
end

// hook is removed from every graph window before quitting
static function IgorBeforeQuitHook(variable unsavedExp, variable unsavedNotebooks, variable unsavedProcedures )
    SetHooks(0)
    if (unsavedExp == 0) // was saved, so no save dialog
        SaveExperiment // save with modifications
    endif
    return 0
end

// hook is set in every graph window after opening an experiment
static function AfterFileOpenHook(variable refNum, string fileNameStr, string pathNameStr, string fileTypeStr, string fileCreatorStr, variable fileKind)
    if (filekind == 1 || filekind == 2) // opened an experiment
        SetHooks(1)
    endif
    return 0
end

static function SetHooks(int activate)
    wave/T wGraphs = ListToTextWave(WinList("*",";","WIN:1"), ";")
    for (string strWin : wGraphs)
        if (activate)
            SetWindow $strWin hook(hMyHook)=MyIndispensableGraphHookFunction
        else
            SetWindow $strWin hook(hMyHook)=$""
        endif
    endfor 
end

 

Edit: the main 'gotcha' is when a user elects not to save changes before quitting.

Edit 2: the code above is meant to be used in a package that adds a window hook without restraint to every graph window in the experiment...

 

Thanks Tony. As I understand it, the hooks will still persist if the user saves the experiment and then closes Igor afterwards, right?

If I have understood the sequence of events correctly, this is what should happen:

If the experiment has been saved just before quitting, the cleanup hook removes all window hooks and re-saves the experiment before it closes.

If the experiment has not been saved just before quitting, the cleanup hook removes all window hooks and hopes that the user will elect to save the experiment.

So the only way that hooks persist is when 1. the user saves the experiment and 2. the experiment is subsequently modified and 3. the user elects not to save the experiment in the "Do you want to save changes" dialog.

 

 

 

OK thanks. I think you can produce the loop-hole by saving the experiment (not a trigger) and then simply closing the Igor experiment, right? I think there is no mechanism to remove the hooks in the saved file then.

So to be absolutely safe that nothing breaks (and things WILL break in the end... you know how users are), there should be a) a fallback for the hook in case the procedures are gone (like the one in my introductory post) or b) some means to force the save-upon-close dialog and make the user to click it or c) hook removal and re-installation upon every save. Hmmm ... or d) use experiment initialization commands to kill any leftover hooks. But I haven't used them so far and I don't know if they are executed before one of the hooks has a chance to trigger (and throw an error).

In reply to by chozo

chozo wrote:

OK thanks. I think you can produce the loop-hole by saving the experiment (not a trigger) and then simply closing the Igor experiment, right? I think there is no mechanism to remove the hooks in the saved file then.

No, IgorBeforeQuitHook will still fire when you quit, so hooks are removed... but the hook-free experiment won't be saved unless the user allows it. Forcing a save is not an option, because the user may not want that. Forcing a save is OK if the experiment was in a saved state going into the hook.

I don't see any errors if I add a named hook with a non-existent function name to a graph window:

setwindow Graph0 hook(hNothing)=nothing

So what exactly is the problem caused by missing hook functions?

System Information:
IGORVERS:9.06
BUILD:56648
COMMIT:2d79803e52

OS:macOS
OSVERSION:14.5.0
LOCALE:US
IGORFILEVERSION:9.06B01

 

Interesting! I never actually tested this. I was assuming it behaves like control procedures. I wonder if we can rely on this behavior or if this is a "bug". This makes things somewhat easier. I will try to implement graph hooks then...