Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

[AHK_L] PUM - owner drawn object based popup menu


  • Please log in to reply
24 replies to this topic
Deo
  • Members
  • 199 posts
  • Last active: Jan 31 2014 03:19 PM
  • Joined: 16 May 2010
Hello
This is owner draw object based & fully customizable ( as much as it is now ) popup menu. I've tried to make this module easy to use, and, possibly, change for anyone.

it currently works with AHK_L Unicode x32/64

Posted Image

Download ( .zip ) - just run test.ahk to check how it works
Documentation

I feel myself bad in any docs writing or explaining the things, so if you don't understand any feature, just let me know, i'll explain it better.

PUM module was partially inspired by majkentor's MMenu, so thanks to him too.

Here is the content of test.ahk:
#NoEnv
#Include PUM_API.ahk
#Include PUM.ahk

; parameters of the PUM object, the manager of the menus
pumParams := { "SelMethod" : "fill"            ;item selection method, may be frame,fill
                ;~ ,"selTColor" : -1         ;selection text color
                ;~ ,"selBGColor" : -1              ;selection background color, -1 means invert current color
                ,"oninit"      : "PUM_out"      ;function which will be called when any menu going to be opened
                ,"onuninit"    : "PUM_out"     ;function which will be called when any menu going to be closing
                ,"onselect"    : "PUM_out"     ;;function which will be called when any item selected with mouse (hovered)
                ,"onrbutton"   : "PUM_out"   ;function which will be called when any item right clicked
                ,"onmbutton"   : "PUM_out"  ;function which will be called when any item clicked with middle mouse button
                ,"onrun"       : "PUM_out"      ;function which will be called when any item clicked with left mouse button
                , "onshow" : "PUM_out"      ;function which will be called before any menu shown using Show method
                , "onclose" : "Pum_out"        ;function called just before quitting from Show method
                ,mnemonicCMD : "select"}
                
;PUM_Menu parameters
menuParams1 := { "bgcolor" : 0x36311f   ;background color of the menu
            , "iconssize" : 32          ;size of icons in the menu
            , "tcolor" : 0xafc9d3 }     ;text color of the menu items
menuParams2 := { "bgcolor" : 0x1C3150
            , "iconssize" : 16
            , "tcolor" : 0xFFFFFF }

;create an instance of PUM object, it is best to have only one of such in the program
pm := new PUM( pumParams )

;creating popup menu, represented by PUM_Menu object with given parameters
menu := pm.CreateMenu( menuParams1 )

;create a three othe menus
newmenu1 := pm.CreateMenu( menuParams2 )
newmenu2 := pm.CreateMenu( menuParams2 )
newmenu3 := pm.CreateMenu( menuParams2 )

;adding submenu items to the first menu, item SubItem1 will open "newmenu1" menu, and SubItem2 will open "newmenu2"
menu.add( { "name" : "SubItem1", "submenu" : newmenu1, "icon" : "shell32.dll:8"  } )
menu.add( { "name" : "SubItem2", "submenu" : newmenu2, "icon" : "shell32.dll:8" } )
menu.Add()  ;adding separator
;adding a submenu item "SubItem3" to the "newmenu2" menu, which opens "newmenu3" menu
newmenu2.add( { "name" : "SubItem3", "icon" : "shell32.dll:8", "submenu" : newmenu3 } )

;adding five items to the first menu
loop, 5
  menu.Add( { "name" : "i&tem" A_Index
            , "bold" : 1
            , "icon" : "shell32.dll:" A_index+20 } )
;adding five items to the newmenu1
loop,5
	newmenu1.add( { "name" : "item" A_index, "icon" : "shell32.dll:3" A_index } )
;adding five items to the newmenu2
loop,5
  newmenu2.add( { "name" : "item" A_index, "icon" : "shell32.dll:4" A_index } )
;adding five items to the newmenu3
loop,5
  newmenu3.add( { "name" : "item" A_index, "icon" : "shell32.dll:2" A_index, disabled : 1 } )

;showing the first menu at the center of screen (~)
if ( item := menu.Show( A_ScreenWidth/3, A_ScreenHeight/3 ) )
	msgbox % "Choosen item: " item.name

;Destroying all menus/items created
;Use Destroy() method for the PUM_Menu object if you want to destroy specific menu
pm.Destroy()
ExitApp
return

PUM_out( msg, obj )
{
  if ( msg = "onselect" )
  {
    rect := obj.GetRECT()
    CoordMode, ToolTip, Screen
    tooltip,% "Selected: " obj.name,% rect.right,% rect.top
  }
  if ( msg ~= "oninit|onuninit|onshow|onclose" )
    tooltip % "menu " msg ": " obj.handle
  if ( msg = "onrbutton" )
    tooltip % "Right clicked: " obj.name
  if ( msg = "onmbutton" )
    tooltip % "Middle clicked: " obj.name
  if ( msg = "onrun" )
    tooltip % "Item runned: " obj.name
}

Update 31/01/2012
All PUM functions moved to separate class, so no need to care about similar functions ( but include PUM_API.ahk first )
-performance improved
new handlers for PUM object added:
onshow - called when menu's Show() method used before menu shown itself
onclose - called when menu's Show() method used after menu closed
new methods for PUM_Item and PUM_menu:
GetTColor()
GetBGColor()
new methods for PUM_Item:
GetIconHandle()
new parameter for PUM_Item:
"iconUseHandle" - to avoid deleting of iconHandle passed as "icon" parameter on item destroy

Update 12/20/2011
Fixed bug when changed through SetParams() item had lost it's attached menu

Update 12/14/2011
-Added GetItemByID() method to PUM object
-Added "textMargin" option for menus
-Default colors changed to system's
-Added "disabled" item option
-Added GetPos() item method
-Added GetRect() item method
-Added Update() item method
-Added EndMenu() menu method
-Added IsMenu() menu method
-PUM_Menu.Show() method now save & restore active window after menu was closed
-Changed Add() method of PUM_Menu object to allow adding items at specific position
-To check how is new features work, refer to documentation



capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010
I like this - but I cannot get it to work

I have
AHK_L Unicode 32 bit v1.1.03.00
Windows XP Sp3


and when I run test.ahk
I get the error "Could not create new menu!"

which comes from
menu := pm.CreateMenu( menuParams1 ) 
the error (as far as I can tell) comes from
DllCall( "CreatePopupMenu", "Ptr" )
returning a blank value - I do not know enough to know why this may be.

Is it possible that you can help me to get this to run?

Deo
  • Members
  • 199 posts
  • Last active: Jan 31 2014 03:19 PM
  • Joined: 16 May 2010
hi, capitalH
just tried it in XP SP3, and it seems works fine for me with last version of ahk - v1.1.05.04
make sure you use unicode version of ahk

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Looks nice, thanks for sharing ;)

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010

hi, capitalH
just tried it in XP SP3, and it seems works fine for me with last version of ahk - v1.1.05.04
make sure you use unicode version of ahk


Thanks
Newest version fixes it (not sure why though)
Now I must run 2 versions of AHK_L as I have some scripts incompatible with the newest version.

Edit:

Although with
if A_AhkPath=C:\Program Files\AutoHotkey\AutoHotkey.exe
{
	run "%A_ScriptDir%\AutoHotkey.exe" "%A_ScriptFullPath%"
	exitapp
}

At the top and AHK in the script directory - it works!

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010
Thank you for this useful script

It is very easy to use (though I do not have the skill to change the class itself - the example is easy to follow)


Some observations:
1. When a menu that is shown, regardless if a selection is made or not, the active window is not restored correctly. In the test example, since exitapp is called right after, the active window is restored, but if the script remains active, it is not. I used a workaround in my script below (just save ID of active window and restore)

2. There does not seem to be a way to disable a menu item (refer to the AHK standard menu). This is useful for adding headings to a menu (which I would like to do), or to keep the menu consistent. Am I missing something?

3. The tooltips is extremely useful (and the reason why I chose your class above the built in menu), but when using the keyboard to navigate selection, is displayed in the wrong location. Is it possible to add a parameter to PUM_out (or a new Key+Value for obj) that provides the XY co-ordinates of the menu item, to display the tooltip correctly? (to demonstrate the problem, run my script, copy something, then move the mouse away from the middle of the screen and press down)

4. Is it possible to match the windows style for menus (without manually setting colours etc)
I tried:
pumParams := { "SelMethod" : "fill"            ;item selection method, may be frame,fill
                ,"oninit"      : "PUM_out"      ;function which will be called when any of this events happen, se it below
                ,"onuninit"    : "PUM_out"
                ,"onselect"    : "PUM_out"
                ,"onrbutton"   : "PUM_out"
                ,"onmbutton"   : "PUM_out"
                ,"onrun"       : "PUM_out" }
menuParams1 :=  {"iconssize" : 0}          ;size of icons in the menu
but the menu background is different from the standard menu

The full code
#NoEnv
#Include  %A_ScriptDir%
#Include PUM.ahk
#Include PUM_foos.ahk
#singleinstance force
StartTicks:=A_TickCount
ClipBoardHistory:=Object()
ClipBoardEnabled=1
previousClip=%clipboard%

if A_AhkPath=C:\Program Files\AutoHotkey\AutoHotkey.exe
{
	run "%A_ScriptDir%\AutoHotkey.exe" "%A_ScriptFullPath%"
	exitapp
}

; parameters of the PUM object, the manager of the menus
pumParams := { "SelMethod" : "fill"            ;item selection method, may be frame,fill
                ,"selTColor" : 0x000000         ;selection text color
                ,"selBGColor" : -1              ;selection background color, -1 means invert current color
                ,"oninit"      : "PUM_out"      ;function which will be called when any of this events happen, se it below
                ,"onuninit"    : "PUM_out"
                ,"onselect"    : "PUM_out"
                ,"onrbutton"   : "PUM_out"
                ,"onmbutton"   : "PUM_out"
                ,"onrun"       : "PUM_out" }
                
;PUM_Menu parameters
menuParams1 := { "bgcolor" : 0x36311f   ;background color of the menu R
            , "iconssize" : 0          ;size of icons in the menu
            , "tcolor" : 0xafc9d3 }     ;text color of the menu items

;create an instance of PUM object, it is best to have only one of such in the program
pm := new PUM( pumParams )

PUM_out( msg, obj )
{
  if ( msg = "onselect" )
    tooltip %  obj.tooltip 
;  if ( msg = "oninit" )
;    tooltip % "menu init: " obj.handle
;  if ( msg = "onuninit" )
;    tooltip % "menu uninit: " obj.handle
;  if ( msg = "onrbutton" )
;    tooltip % "Right clicked: " obj.name
;  if ( msg = "onmbutton" )
;    tooltip % "Middle clicked: " obj.name
;  if ( msg = "onrun" )
;    tooltip % "Item runned: " obj.name
}

OnClipboardChange:
	if (ClipBoardEnabled=1) and ((A_TickCount-StartTicks)>1)
	{
	currentClip=%clipboard% 
	currentClipAll=%clipboardall% 
	If (A_EventInfo=1) and (currentClip <> previousClip)
	{
		previousClip=%clipboard% 
		ClipSave(ClipBoardHistory,currentclip, currentclipall,5)	
	}
	}
	
Return

ClipSave(ClipBoardHistory,clip, clipall,maxclips=20)
{
	ClipBoardHistory.insert(Object())
	index:=ClipBoardHistory.MaxIndex()
	ClipBoardHistory[index]["plain"]:=clip
	ClipBoardHistory[index]["All"]:=clipall
	if (index>maxclips)
		ClipBoardHistory.Remove(ClipBoardHistory.MinIndex())
}

^!v::
menu := pm.CreateMenu( menuParams1 )
index:=ClipBoardHistory.maxIndex()
if index=
	return

while Index>=ClipBoardHistory.minIndex()
{
	Description:=ClipBoardHistory[Index]["plain"]
	FullDescription:=Description
	if(StrLen(Description)>35) 
	{ 
		toCut:=StrLen(Description)-18 
		StringMid, itemTemp1, Description, 1, 12 
		StringTrimLeft, itemTemp2, Description, %toCut% 
		Description=%itemTemp1%...%itemTemp2% 
	} 
	if index<10
	MenuString:="&" . (ClipBoardHistory.maxindex()-Index+1) . "-" . Description
	else
	MenuString:= (ClipBoardHistory.maxindex()-Index+1) . "-" . Description
  menu.Add( { "name" : MenuString
            , "bold" : 1
			, "index" : Index
			, "tooltip" : ClipBoardHistory[Index]["plain"]})
			
	index--
}
WinGet, ActiveWindow , ID, A
 item := menu.Show( A_ScreenWidth/3, A_ScreenHeight/3 ) 
tooltip
WinActivate , ahk_id %ActiveWindow%
WinWaitActive, ahk_id %ActiveWindow%
if item
{
ClipBoardEnabled=0
test:=item.index

tempClip:=clipboard
clipboard:=ClipBoardHistory[test]["plain"]
send, ^v
clipboard:=tempClip
ClipBoardEnabled=1
}

Menu.destroy()
return


Deo
  • Members
  • 199 posts
  • Last active: Jan 31 2014 03:19 PM
  • Joined: 16 May 2010

1. When a menu that is shown, regardless if a selection is made or not, the active window is not restored correctly. In the test example, since exitapp is called right after, the active window is restored, but if the script remains active, it is not. I used a workaround in my script below (just save ID of active window and restore)

It seems right to save & restore active window after menu was closed, i'll implement it and we'll check if it works fine

2. There does not seem to be a way to disable a menu item (refer to the AHK standard menu). This is useful for adding headings to a menu (which I would like to do), or to keep the menu consistent. Am I missing something?

i have this implemented in my local sources, give me some time to publish it

3. The tooltips is extremely useful (and the reason why I chose your class above the built in menu), but when using the keyboard to navigate selection, is displayed in the wrong location. Is it possible to add a parameter to PUM_out (or a new Key+Value for obj) that provides the XY co-ordinates of the menu item, to display the tooltip correctly? (to demonstrate the problem, run my script, copy something, then move the mouse away from the middle of the screen and press down)

good idea, i'll add this

4. Is it possible to match the windows style for menus (without manually setting colours etc)

i'll see what i can do

The full code

looks like clipboard manager :)

ClipBoardHistory[index]["All"]:=clipall

as far as i know you cannot save clipboardall data directly to the object's field ( check this ) or did you bypassed this somehow?

menuParams1 := {"iconssize" : 0}

it's better to use "noicons" : 1 instead, to avoid needless resources creation

Deo
  • Members
  • 199 posts
  • Last active: Jan 31 2014 03:19 PM
  • Joined: 16 May 2010
this module have been updated, check first post & documentation about changes

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010

It seems right to save & restore active window after menu was closed, i'll implement it and we'll check if it works fine

Thanks - not a train smash - so no hurry, I have a workaround

i have this implemented in my local sources, give me some time to publish it

Thanks

i'll see what i can do

Hope it is possible. For this current script I do not need it, but it would look silly in some other apps (say for a context menu) if the menu is in a different style from everything else

looks like clipboard manager

Indeed.

My plan is to merge this with the clipboard manipulation of Mango (http://www.autohotke...pic.php?t=44727) to have a lightweight clipboard manager.


it's better to use "noicons" : 1 instead, to avoid needless resources creation


Noted. I tried just leaving the iconsize out but it left a big empty space.

as far as i know you cannot save clipboardall data directly to the object's field ( check this ) or did you bypassed this somehow?


No - sorry. I saved this value and my plan was to (later) have a way of either pasting as plaintext or the original clipboard from the history.

There seems to be a workaround http://www.autohotke...p=468369#468369

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010

It seems right to save & restore active window after menu was closed, i'll implement it and we'll check if it works fine

Thanks - not a train smash - so no hurry, I have a workaround

i have this implemented in my local sources, give me some time to publish it

Thanks

i'll see what i can do

Hope it is possible. For this current script I do not need it, but it would look silly in some other apps (say for a context menu) if the menu is in a different style from everything else

looks like clipboard manager

Indeed.

My plan is to merge this with the clipboard manipulation of Mango (http://www.autohotke...pic.php?t=44727) to have a lightweight clipboard manager.


it's better to use "noicons" : 1 instead, to avoid needless resources creation


Noted. I tried just leaving the iconsize out but it left a big empty space.

as far as i know you cannot save clipboardall data directly to the object's field ( check this ) or did you bypassed this somehow?


No - sorry. I saved this value and my plan was to (later) have a way of either pasting as plaintext or the original clipboard from the history.

There seems to be a workaround http://www.autohotke...p=468369#468369

Edit: my text seemed to have been lost

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010
Some additional observations

PUM_Out is called with "onselect" when an accelerator key is pressed to choose a menu item, surely "onrun" is the expected behaviour?

Update 12/14/2011
-Added GetItemByID() method to PUM object
-Added "textMargin" option for menus
-Default colors changed to system's
-Added "disabled" item option
-Added GetPos() item method
-Added GetRect() item method
-Added Update() item method
-Added EndMenu() menu method
-Added IsMenu() menu method
-PUM_Menu.Show() method now save & restore active window after menu was closed
-Changed Add() method of PUM_Menu object to allow adding items at specific position
-To check how is new features work, refer to documentation


I cannot find some of the updates in the documentation (for example GetPos() and GetItemRect()

Sorry for all my questions - hope I am not getting annoying

Deo
  • Members
  • 199 posts
  • Last active: Jan 31 2014 03:19 PM
  • Joined: 16 May 2010

PUM_Out is called with "onselect" when an accelerator key is pressed to choose a menu item, surely "onrun" is the expected behaviour?

It depends on the value of "mnemonicCmd" option for PUM manager, it can be "run" and "select". In first case target item should be run, in second it will be selected only. Though "run" is the default value. Just checked it and it seems work as expected. Let me know if it wrong for you

I cannot find some of the updates in the documentation (for example GetPos() and GetItemRect()

try this link ( click on PUM_Item ). The page also could be cached in your browser, thats why you see old one.

Sorry for all my questions - hope I am not getting annoying

glad you have interest in this module. I planning to use it a lot in my Quick Cliq proj, so it will be maintained as long as possible.

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010

PUM_Out is called with "onselect" when an accelerator key is pressed to choose a menu item, surely "onrun" is the expected behaviour?

It depends on the value of "mnemonicCmd" option for PUM manager, it can be "run" and "select". In first case target item should be run, in second it will be selected only. Though "run" is the default value. Just checked it and it seems work as expected. Let me know if it wrong for you


Thanks - but I think my message was unclear. The problem is not what action is instituted by your program - it is more the message that is sent to PUM_Out. And the default is indeed "run".

To elaborate


What happens:

"mnemonicCmd" : "select"
Press accelerator key
Item gets selected, but is not run
Pum_Out gets called with onselect <- This is what I expect

"mnemonicCmd" : "run"
Press accelerator key
Item gets run
Pum_Out gets called with onselect <- This is not what I expect


I cannot find some of the updates in the documentation (for example GetPos() and GetItemRect()

try this link ( click on PUM_Item ). The page also could be cached in your browser, thats why you see old one.


Sorry, should have though about that.

The good news is now, I fixed my tooltip location problem - see code below.


PUM_out( msg, obj )
{
  if ( msg = "onselect" )
    {
		Rectangle:=obj.getRect()
		x:=Rectangle.right
		y:=Rectangle.top
		CoordMode, ToolTip, Screen
		Tooltip:=obj.tooltip
		tooltip %ToolTip%,%X%,%Y%
	}
}
[/code]

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010
Edit: I managed to find another solution: cannot clone submenus - but good enough for my needs.

;does not clone UID or Submenu items 
CloneMenuItems(pm,menu,newmenu)
{
for i, item in menu.GetItems()
	newmenu.add({"name" : item.name, "bold" : item.bold, "issep" : item.issep, "icon" : item.icon , "break": item.break, "tcolor": item.tcolor, "bgcolor": item.bgcolor, "noPrefix" : item.noPrefix, "disabled" : item.disabled})
}

RAlt::
	If AltMenu
		AltMenu.Destroy
	Altmenu := pm.CreateMenu( menuParams1 )
	cm2 := pm.CreateMenu( menuParams1 )
	
	CloneMenuitems(pm,clipmenu,cm2)
	
	Altmenu.add( { "name" : "Paste original text", "submenu" : ClipMenu} )
	Altmenu.add( { "name" : "Paste plain text", "submenu" : Cm2} ) 			Does not work
	AltItem := AltMenu.Show( A_ScreenWidth/3, A_ScreenHeight/3 ) 
	tooltip
		test:=altitem.menu.owner.name											;works to determine which menu the item was selected from
Return

-----------------------------
I am trying (without success) to have create two submenus (with different names) referring to the same menu (and then using altitem.menu.owner.name to determine which item it referred to). I have also tried cloning/deepcloning the menu object. Now I can make a workaround (essentially maintaining two menus), but before I do that - is there a way to do it without this overhead and messy code?

The code (extract - will not run):

DeepClone(Obj)
{
	newobj:=object()
	for key,val in obj
	{
		if isobject(val)
			newobj[key]:=DeepClone(Val)
		else
			newobj[key]:=Val
	}
	return newobj
}

RAlt::
	If AltMenu
		AltMenu.Destroy
	Altmenu := pm.CreateMenu( menuParams1 )
	Altmenu.add( { "name" : "Paste original text", "submenu" : ClipMenu} )
	cm2:=ClipMenu.clone()
	cm:=deepclone(clipMenu)
	Altmenu.add( { "name" : "Paste plain text", "submenu" : Cm} )				;Does not work
	;Altmenu.add( { "name" : "Paste plain text", "submenu" : Cm2} ) 			Does not work
	;Altmenu.add( { "name" : "Paste plain text", "submenu" : ClipMenu} )		Does not work
	AltItem := AltMenu.Show( A_ScreenWidth/3, A_ScreenHeight/3 ) 
	tooltip
		test:=altitem.menu.owner.name											;works to determine which menu the item was selected from
Return


Deo
  • Members
  • 199 posts
  • Last active: Jan 31 2014 03:19 PM
  • Joined: 16 May 2010

CloneMenuItems(pm,menu,newmenu)
{
for i, item in menu.GetItems()
newmenu.add({"name" : item.name, "bold" : item.bold, "issep" : item.issep, "icon" : item.icon , "break": item.break, "tcolor": item.tcolor, "bgcolor": item.bgcolor, "noPrefix" : item.noPrefix, "disabled" : item.disabled})
}

this is the good solution, because i have disabled possibility to add same menu as submenu to the several items since it caused some bad problems. I think i'll better make similar method to clone menus instead...

Thanks - but I think my message was unclear. The problem is not what action is instituted by your program - it is more the message that is sent to PUM_Out. And the default is indeed "run".

now i understand, but if i got two messages, i should transfer them all both to the event handling function without filtering, right? That depends on how you will make handling of them. For example you can call "tooltip" whenever you getting "onrun" event to remove shown tooltip (since "onselect" message received first ), or you can make tooltips showing through timers and make few checks there, for example GetRect() method will return nothing if specified item is not shown on the screen at the moment
Though i can add extra parameter to return with "onselect" message, which can tell you whether item selected with mouse or not, is it highlited, is it disabled. But as i tried it doesn't filter events when you choose item with cursor or by pressing button.