Back in classic days, @lexikos discovered and described a simple way to hook mouse events on the script's tray icon. Since then I have
used this AHK_NOTIFYICON method in some of my scripts, but every time I had to figure it out on how to customize it for specific needs. AHK_NOTIFYICON mesage handler will receive 9 click notifications, 3 each for Left, Middle and Right mouse buttons and how nice it would be
if we had labels, one for each? Something like individual GuiContextMenu label where we handle right-click event for every GUI.
Most surely it will not happen!.
I give you the second best solution. NotifyTrayClick() : A function based implementation which provides individual labels for all 9 click events.
How to use it:
1) Copy/paste my function in to your script.
2) Handle events by using any/some/all of the following labels. (You may use functions instead of labels... or a mix of both)
NotifyTrayClick_201: ; Left click (Button down)
NotifyTrayClick_202: ; Left click (Button up)
NotifyTrayClick_203: ; Left double click
NotifyTrayClick_204: ; Right click (Button down)
NotifyTrayClick_205: ; Right click (Button up)
NotifyTrayClick_206: ; Right double click
NotifyTrayClick_207: ; Middle click (Button down)
NotifyTrayClick_208: ; Middle click (Button up)
NotifyTrayClick_209: ; Middle double click
Notes:
NotifyTrayClick_201 label (Left click down) is not of much use. You will receive the event only when you release the mouse left button.
I suggest using only Button up/Double click event for one mouse button.
If you attempt to all handle three events i.e., Button down/Button up/Double click for a button, then Double click event will be followed
by a Button down event. It is easy to work-around though. A demo is available in examples.
The mere existence of a label will suppress its original functionality. If you have NotifyTrayClick_205: (Right click up) in your script,
then right click menu wouldn't work and script can't be exited. Bind ExitApp to a hotkey when your script is in testing stage.
The cursor has to be over tray icon to receive a "button up" event. For eg. If you click on the tray icon - move the cursor away from the icon - release
the button, then script will never receive the event.
NotifyTrayClick(DoubleClickTime)
By default, the function will auto-initialize with DoubleClickTime of 250 ms. This time duration is used by the function to differentiate single
from double clicks. To override it with a custom value, say 300 ms, call the function from auto-execute section, like: NotifyTrayClick(300).
Or, if you want to apply the system default value, pass API function GetDoubleClickTime() as parameter. NotifyTrayClick( DllCall("GetDoubleClickTime") )
Note: DoubleClickTime will be applied to single click of a button, only if its counter-part double click handler is present in script.
#SingleInstance, Force
Return ; end of auto-execute section
NotifyTrayClick_202: ; Left click (Button up)
Menu, Tray, Show
Return
NotifyTrayClick_205: ; Right click (Button up)
Return ; Supress right click menu
; Copy paste NotifyTrayClick() below
Complex example
Demonstrates how to handle all 3 events for mouse right button.
The script shows disk space in a GUI.
DiskStats.png (4.28 KiB) Viewed 3798 times
Usage:
Run the script.
Right click on the tray icon and hold it to take a quick peek at the UI.
Release button to hide it.
For regular Gui show, use Right double click on the tray icon.
Access Tray menu with Left click.
The script: You need to copy/paste NotifyTrayClick()
WOW! This is amazing! This should be included with the next update of ahk! I often want to see what's on my clipboard, I think I'll utilize this to display a gui of my clipboard.
I've been playing with your function and I noticed a couple of things. One was that it sets the host script to Critical ON because the initial call is not a new thread as is the OnMessage call. Moving the 'Critical' statement to after the initialization fixed that. Also, each value of NM, i.e. function name, generates a separate distinct timer. So, while the script appears to use one timer, it can be many. I verified this using Lexiko's GetTimer() function. This came to light when I had a left click defined but no left double click... a double would trigger the single whereas I thought it should ignore it. I needed an array to keep track of pending timers because there could be one for right click down and up pending while waiting for a double right click. I also gave the user a choice to neuter the native actions of a type of click or not. I took a crack at rewriting the script for my needs. In my version I set it up to accept a call table for each type of action... not sure if I am satisfied with that yet. It is not as terse as your style but someone may find it useful.
/*
---------------------------------------------------------------------------------------
Function : onTrayClick(P*)
Description : Sets up a way to respond to mouse clicks on a tray icon using OnMessage
: 0x404 ="WM_NOTIFYICON". P[2] or LParam contains the click state.
:
Parameters : P needs to be variadic so that it can collect info from OnMessage.
: Call this function once with P as an array containing:
: key = 1 => new delay for double click sensing, default = 250 mSec.
: key = L => vector for left click
: key = L2 => vector for double left click
: key = Rd => vector for right click DOWN
: key = Ru => vector for right click UP
: key = R2 => vector for double right click
: key = Md => vector for middle click DOWN
: key = Mu => vector for middle click UP
: key = M2 => vector for double middle click
: Add a '+' to the key (prefix or suffix) to enable native action in addition to vector action.
:
Returns : True if action defined with a vector without '+'... this neuters native actions.
: "" if not defined with a vector... this allows native actions.
:
Remarks : Left-down (513 or 0x201) only received after left-up triggered so is basically useless.
: Note that every mouse movement on hover triggers code 512 (0x200); this is ignored.
:
: WM_NOTIFYICON sends the following on double click:
: down, up, double click, up
:
:
:
---------------------------------------------------------------------------------------
*/
onTrayClick(P*)
{
static NM, T := 0, Clk := 1, vector := [], pending := []
if (T = 0)
{
for k, v in P[1]
{
switch StrReplace(k, "+")
{
case 1 : vector[1] := v, tmp := True
case "L" : vector[514] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "L2": vector[515] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "Rd": vector[516] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "Ru": vector[517] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "R2": vector[518] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "Md": vector[519] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "Mu": vector[520] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
case "M2": vector[521] := [v, InStr(k, "+")], tmp := (isFunc(v) || isLabel(v))
}
if (!tmp)
{
Msgbox % A_ThisFunc . ":`nCallback """ . v . """ is not a function or label!"
T := 0 ;in case of multiple failed calls
Return False
}
}
if (vector.Length() < 512)
{
Msgbox % A_ThisFunc . ":`nNo callback function(s) or label(s) defined."
T := 0 ;in case of multiple failed calls
Return False
}
OnMessage(0x404, Func(A_ThisFunc), -1)
Return ( T := Max(-5000, 0 - (vector[1] ? Abs(vector[1]) : 250)) )
}
Critical ;will be turned off as OnMessage thread ends at 'return'
if (tmp := (P[2] = 515 || P[2] = 518 || P[2] = 521))
while % q := pending.Pop()
SetTimer, %q%, % ("Off", Clk := 2)
if ( ( P[2] < 513 || P[2] > 521 ) or !( IsFunc(NM := vector[P[2], 1]) || Islabel(NM) ) )
Return ;this return will allow native actions not redefined here to take place normally
SetTimer, %NM%, % tmp ? (-1, Clk := 2) : ( Clk = 2 ? ("Off", Clk := 1) : (T, pending.Push(NM)) )
Return vector[P[2], 2] ? "" : True
}
when I had a left click defined but no left double click... a double would trigger the single whereas I thought it should ignore it.
That is the intended effect.
For eg., if the user wants to Run, notepad.exe on left single click, it should run as many times the user left clicks the tray icon.
If the user wants to suppress the script's main window from showing, then the script has to "handle" that event.
In case the user does want the native functionality for left double click, then it can be worked around as follows:
#SingleInstance, Force
NotifyTrayClick(250)
Return ; end of auto-execute section
NotifyTrayClick_202: ; Left click (Button up)
If ( LeftDblClick and not (LeftDblClick:=0) )
Return
Menu, Tray, Show
Return
NotifyTrayClick_203: ; Left double click
LeftDblClick:=1
DllCall("PostMessage", "Ptr",A_ScriptHwnd, "Int",0x111, "Ptr",65300, "Ptr",0, "Ptr") ; Open = 65300
Return
; Copy paste NotifyTrayClick() below
AutoHotkey v2 allows the standard items to be deleted or modified by name or position, the same as any other items; e.g. A_TrayMenu.Delete("&Open"). It is possible to assign icons or override the function of an item without recreating all of the other standard items.
RemoveMenu also works, but you'd use hMenu := A_TrayMenu.Handle instead of MenuGetHandle.
If I use Middle Click and Double Middle Click, Double Middle Click sometimes does not work but run Single Click twice.
I have tried increasing the Inbetween Time to system defaukt of 500ms.
Also sometimes middle clicking on the icon, opens the context menu from right-mouse click. Even when I am really careful not to press the right button.
I'd like to use NotifyTrayClick_205 to perform some pre-processing when any right-click on the tray icon occurs, but then I want my custom context menu to appear, as it does now before using NotifyTrayClick_205. In other words, after catching the right-click on the tray icon with NotifyTrayClick_205, I'd like to, in essence, pass through a right-click and continue the script as if a right-click had occurred. Is this possible? I saw this in your doc and am wondering if there's a way around it:
SKAN wrote:The mere existence of a label will suppress its original functionality. If you have NotifyTrayClick_205: (Right click up) in your script, then right click menu wouldn't work ...
The way I'm planning on doing it now (before I discovered your function) is to call my pre-processing code at every tray menu and submenu label, which will be very painful, as I have well over 100 menu/submenu picks in the program. Thanks much, Joe
Edit on 14-Sep-2022: For anyone following this thread, I posted a new question on this issue. More details are at that thread, but the quick answer, thanks to lexikos, is that you can show the context menu at the NotifyTrayClick_205: label via a Menu,Tray,Show statement...works a charm! Regards, Joe