Dynamic menus (Windows / Linux)

Share your advanced PureBasic knowledge/code with the community.
Justin
Addict
Addict
Posts: 829
Joined: Sat Apr 26, 2003 2:49 pm

Dynamic menus (Windows / Linux)

Post by Justin »

Sadly PB still does not support adding / removing menu items dynamically so i did this module.
You can add / remove items and submenus.
Items can be accesed by id or position.
Regular pb functions work to check, disable, bind, etc.
Tested on Win7, Win10, Linux Mint17. 64
Not tested on 32 bit but should work.
Maybe some mac guru can fill the gaps for mac os.

Code: Select all

;Dynamic menus.
;PB 5.62.
;Windows / Linux.

;- DeclareModule
DeclareModule guiMenu
	CompilerIf #PB_Compiler_OS = #PB_OS_Windows
		#MIM_MENUDATA = $00000008
		
		;- MENUINFO
		Structure MENUINFO
			cbSize.l
			fMask.l
			dwStyle.l
			cyMax.i
			hbrBack.i
			dwContextHelpID.l
			dwMenuData.i
		EndStructure
		
	CompilerElseIf #PB_Compiler_OS = #PB_OS_Linux

	CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS

	CompilerEndIf
	
	;- ENUM ItemBy
	Enumeration
		#ItemById
		#ItemByPos
	EndEnumeration
	
	;- DECLARES
	Declare CreateMenuBar(win.i)
	Declare CreateSubmenu()
	Declare AppendMenuItem(menu.i, text.s, id.i, submenu.i = 0)
	Declare RemoveMenuItem(menu.i, item.i, flags.i = #ItemById)
	Declare InsertMenuItem(men.i, item.i, text.s, id.i, submenu.i = 0, flags.i = #ItemById)
	Declare InsertSeparator(men.i, pos.i, id.i = -1, flags.i = #ItemById)
	Declare AppendSeparator(men.i, id.i = -1)
	Declare SetSubmenu(men.i, item.i, submenu.i, flags.i = #ItemById)
	Declare GetSubMenu(men.i, item.i, flags.i = #ItemById)
	Declare GetMenuItemCount(men.i)
EndDeclareModule

;- Module
Module guiMenu
	EnableExplicit
	
	CompilerIf #PB_Compiler_OS = #PB_OS_Linux
		;Tags used to set and retrieve object data.
		#TagMenuItemId = "miid"
		#TagPbMenuHandle = "pbmenu"
		
		ProcedureC linOnMenuItemActivate(menitem.i, dat.i)
			PostEvent(#PB_Event_Menu, GetActiveWindow(), g_object_get_data_(menitem, #TagMenuItemId))
		EndProcedure 
		
		;Gets the menuitem object from a menuitem id.
		Procedure linGetMenuItemById(hmenu.i, id.i)			
			Define.GList *childs
			Define.i menItem
			
			*childs = gtk_container_get_children_(hmenu)
			If *childs
				*childs = g_list_first_(*childs)
				While *childs
					If g_object_get_data_(*childs\data, #TagMenuItemId) = id
						menItem = *childs\data
						Break 
					EndIf 
					
					*childs = *childs\next
				Wend
				
				g_list_free_(*childs)
			EndIf 
			
			ProcedureReturn menItem
		EndProcedure
		
		;Gets the menuitem object from a menuitem position.
		Procedure linGetMenuItemByPos(hmenu.i, pos.i)
			Define.GList *childs
			Define.i menItem
			
			*childs = gtk_container_get_children_(hmenu)
			If *childs
				menItem = g_list_nth_data_(*childs, pos)
				g_list_free_(*childs)
			EndIf 
			
			ProcedureReturn menItem
		EndProcedure
		
		;Gets the menuitem object from item pos or id depending on flags.
		Procedure linGetMenuItem(hmenu.i, item.i, flags.i)
			If flags = #ItemById
				ProcedureReturn linGetMenuItemById(hmenu, item)
				
			ElseIf flags = #ItemByPos
				ProcedureReturn linGetMenuItemByPos(hmenu, item)
			EndIf 
		EndProcedure
		
		;Gets the menuitem position from id.
		Procedure linGetMenuItemPosById(hmenu.i, id.i)
			Define.GList *childs
			Define.i pos
			Define.b found
			
			found = #False 
			pos = 0
			*childs = gtk_container_get_children_(hmenu)
			If *childs
				*childs = g_list_first_(*childs)
				While *childs
					If g_object_get_data_(*childs\data, #TagMenuItemId) = id
						found = #True 
						Break 
					EndIf 
					
					*childs = *childs\next
					pos = pos + 1
				Wend
				
				g_list_free_(*childs)
			EndIf 
			
			If found
				ProcedureReturn pos
				
			Else
				ProcedureReturn -1
			EndIf 
		EndProcedure
		
		Procedure linInsertMenuItem(hmen.i, menItem.i, item.i, id.i, pbsubmenu.i, flags.i)
			g_object_set_data_(menItem, #TagMenuItemId, id)

			If item = -1 And flags = #ItemByPos ;Append
				gtk_menu_shell_append_(hmen, menItem)
				
			Else ;Insert
				If flags = #ItemById
					item = linGetMenuItemPosById(hmen, item)
				EndIf 
			
				gtk_menu_shell_insert_(hmen, menItem, item)
			EndIf 
			
			g_signal_connect_(menItem, "activate", @linOnMenuItemActivate(), 0)
			gtk_widget_show_(menItem)
			
			If IsMenu(pbsubmenu)
				gtk_menu_item_set_submenu_(menItem, MenuID(pbsubmenu))
			EndIf 
		EndProcedure
	CompilerEndIf
	
	Procedure CreateMenuBar(win.i)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.i menu
				Define.MENUINFO mi
				
				menu = CreateMenu(#PB_Any, WindowID(win))
				If menu
					SetMenu_(WindowID(win), MenuID(menu))
					
					;Save pb window handle in menu data, used to redraw de menubar.
					mi\cbSize = SizeOf(MENUINFO)
					mi\fMask = #MIM_MENUDATA
					mi\dwMenuData = win
					SetMenuInfo_(MenuID(menu), mi)
					
					ProcedureReturn menu
				EndIf
				
			CompilerCase #PB_OS_Linux 				
				ProcedureReturn CreateMenu(#PB_Any, WindowID(win))
				
			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure CreateSubmenu()
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.i pbMenu
				Define.MENUINFO mi
				
				pbMenu = CreatePopupMenu(#PB_Any)
				If pbMenu
					;Save pbMenu handle so we can retrieve it later.
					mi\cbSize = SizeOf(MENUINFO)
					mi\fMask = #MIM_MENUDATA
					mi\dwMenuData = pbMenu
					SetMenuInfo_(MenuID(pbMenu), @mi)
					
					ProcedureReturn pbMenu
				EndIf 

			CompilerCase #PB_OS_Linux 
				Define.i menu
				
				menu = CreatePopupMenu(#PB_Any)
				If menu 
					;Save pbMenu handle so we can retrieve it later.
					g_object_set_data_(MenuID(menu), #TagPbMenuHandle, menu)
					
					ProcedureReturn menu
				EndIf 

			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure AppendMenuItem(menu.i, text.s, id.i, submenu.i = 0)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.MENUINFO mi
				
				If InsertMenuItem(menu, GetMenuItemCount_(MenuID(menu)), text, id, submenu, #ItemByPos)
					;Get menu window and redraw menubar, only happens when menubar items are added.
					mi\cbSize = SizeOf(MENUINFO)
					mi\fMask = #MIM_MENUDATA
					If GetMenuInfo_(MenuID(menu), @mi)
						If IsWindow(mi\dwMenuData) ;Is a menubar.
							DrawMenuBar_(WindowID(mi\dwMenuData))
						EndIf 
					EndIf 
				EndIf 
				
			CompilerCase #PB_OS_Linux 
				linInsertMenuItem(MenuID(menu), gtk_menu_item_new_with_mnemonic_(text), -1, id, submenu, #ItemByPos)
	
			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure RemoveMenuItem(menu.i, item.i, flags.i = #ItemById)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.i winflags
				
				If flags = #ItemById
					winflags = #MF_BYCOMMAND
					
				ElseIf flags = #ItemByPos
					winflags = #MF_BYPOSITION
					
				Else
					winflags = #MF_BYCOMMAND
				EndIf 
				
				RemoveMenu_(MenuID(menu), item, winflags)
				
			CompilerCase #PB_OS_Linux 
				Define.i menItem
				
				menItem = linGetMenuItem(MenuID(menu), item, flags)
				If menItem
					gtk_widget_destroy_(menItem)
				EndIf
			
			CompilerCase #PB_OS_MacOS
				
		CompilerEndSelect
	EndProcedure
	
	Procedure InsertSeparator(men.i, pos.i, id.i = -1, flags.i = #ItemById)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.i winflags
				
				If flags = #ItemById
					winflags = winflags | #MF_BYCOMMAND
					
				Else
					winflags = winflags | #MF_BYPOSITION
				EndIf 
				
				ProcedureReturn InsertMenu_(MenuID(men), pos, winflags | #MF_SEPARATOR, id, 0)
				
			CompilerCase #PB_OS_Linux 
				linInsertMenuItem(MenuID(men), gtk_separator_menu_item_new_(), pos, id, 0, flags)
				
			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure AppendSeparator(men.i, id.i = -1)
		ProcedureReturn InsertSeparator(men, -1, id, #ItemByPos)
	EndProcedure 
	
	Procedure InsertMenuItem(men.i, item.i, text.s, id.i, submenu.i = 0, flags.i = #ItemById)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.MENUITEMINFO mii
				Define.b byPos
				
				If flags = #ItemByPos
					byPos = #True
					
				Else
					byPos = #False
				EndIf 
				
				mii\cbSize = SizeOf(MENUITEMINFO)
				mii\fMask = #MIIM_ID | #MIIM_STRING | #MIIM_SUBMENU
				mii\wID = id
				mii\dwTypeData = @text
				If IsMenu(submenu)
					mii\hSubMenu = MenuID(submenu)
				EndIf 
				
				ProcedureReturn InsertMenuItem_(MenuID(men), item, byPos, @mii)
				
			CompilerCase #PB_OS_Linux
				linInsertMenuItem(men, gtk_menu_item_new_with_mnemonic_(text), item, id, submenu, flags)
				
			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure SetSubmenu(men.i, item.i, submenu.i, flags.i = #ItemById)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.MENUITEMINFO mii
				Define.b byPos
				
				If flags = #ItemByPos
					byPos = #True
					
				Else
					byPos = #False
				EndIf 
				
				mii\cbSize = SizeOf(MENUITEMINFO)
				mii\fMask = #MIIM_SUBMENU
				If IsMenu(submenu)
					mii\hSubMenu = MenuID(submenu)
				EndIf
				
				ProcedureReturn SetMenuItemInfo_(MenuID(men), item, byPos, @mii)
				
			CompilerCase #PB_OS_Linux
				Define.i menItem
				
				menItem = linGetMenuItem(MenuID(men), item, flags)
				If menItem
					If submenu = 0
						gtk_menu_item_set_submenu_(menItem, 0) ;Removes submenu
	
					ElseIf IsMenu(submenu)
						gtk_menu_item_set_submenu_(menItem, MenuID(submenu))
					EndIf 
				EndIf 
				
			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure GetSubMenu(men.i, item.i, flags.i = #ItemById)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				Define.MENUITEMINFO mii
				Define.MENUINFO mi
				Define.b byPos
				
				If flags = #ItemByPos
					byPos = #True
					
				Else
					byPos = #False
				EndIf 
				
				mii\cbSize = SizeOf(MENUITEMINFO)
				mii\fMask = #MIIM_SUBMENU
				;Get submenu
				If GetMenuItemInfo_(MenuID(men), item, byPos, @mii)
					;Get submenu pbHandle
					mi\cbSize = SizeOf(MENUINFO)
					mi\fMask = #MIM_MENUDATA
					If GetMenuInfo_(mii\hSubMenu, @mi)
						ProcedureReturn mi\dwMenuData
					EndIf 
				EndIf 

			CompilerCase #PB_OS_Linux 
				Define.i menItem, submenu
				
				menItem = linGetMenuItem(MenuID(men), item, flags)
				If menItem
					submenu = gtk_menu_item_get_submenu_(menItem)
					If submenu
						ProcedureReturn g_object_get_data_(submenu, #TagPbMenuHandle)
					EndIf 
				EndIf 
				
			CompilerCase #PB_OS_MacOS
					
		CompilerEndSelect
	EndProcedure
	
	Procedure GetMenuItemCount(men.i)
		CompilerSelect #PB_Compiler_OS
			CompilerCase #PB_OS_Windows
				ProcedureReturn GetMenuItemCount_(MenuID(men))
				
			CompilerCase #PB_OS_Linux 
				Define.GList *childs
				
				*childs = gtk_container_get_children_(MenuID(men))
				If *childs
					ProcedureReturn g_list_length_(*childs)
				EndIf
				
			CompilerCase #PB_OS_MacOS	
				
		CompilerEndSelect
	EndProcedure
EndModule

;- TEST
CompilerIf #PB_Compiler_IsMainFile
EnableExplicit


Procedure menCallback()
	Debug "mc"
EndProcedure

Enumeration 1
	#ID_COPY
	#ID_PASTE
	#ID_OPEN
	#ID_CLOSE
	#ID_ADD_FILE_ITEM
	#ID_ADD_TITLE_ITEM
	#ID_REMOVE_COPY_ITEM
	#ID_PASTE1
	#ID_PASTE2
	#ID_REMOVE_PASTE_MENU
	#ID_CHECK_OPEN
EndEnumeration

Define.i ev, win, fileMenu, editMenu, topMenu, testMenu, pasteMenu


win = OpenWindow(#PB_Any, 100, 100, 400, 300, "Dynamic Menu", #PB_Window_SystemMenu)
topMenu = guiMenu::CreateMenuBar(win)

;Paste submenu
pasteMenu = guiMenu::CreateSubmenu()
guiMenu::AppendMenuItem(pasteMenu, "Paste1", #ID_PASTE1)
guiMenu::AppendMenuItem(pasteMenu, "Paste2", #ID_PASTE2)

;Edit submenu
editMenu = guiMenu::CreateSubmenu()
guiMenu::AppendMenuItem(editMenu, "Copy", #ID_COPY)
guiMenu::AppendMenuItem(editMenu, "Paste", #ID_PASTE, pasteMenu)

;File submenu
fileMenu = guiMenu::CreateSubmenu()
guiMenu::AppendMenuItem(fileMenu, "Open", #ID_OPEN)
guiMenu::AppendMenuItem(fileMenu, "Close", #ID_CLOSE)

;Test submenu
testMenu = guiMenu::CreateSubmenu()
guiMenu::AppendMenuItem(testMenu, "Add file item", #ID_ADD_FILE_ITEM)
guiMenu::AppendMenuItem(testMenu, "Add title item", #ID_ADD_TITLE_ITEM)
guiMenu::AppendMenuItem(testMenu, "Remove copy item", #ID_REMOVE_COPY_ITEM)
guiMenu::AppendMenuItem(testMenu, "Remove paste menu", #ID_REMOVE_PASTE_MENU)
guiMenu::AppendMenuItem(testMenu, "Check open", #ID_CHECK_OPEN)

;Top menu
guiMenu::AppendMenuItem(topMenu, "File", -1, fileMenu)
guiMenu::AppendMenuItem(topMenu, "Edit", -1, editMenu)
guiMenu::AppendMenuItem(topMenu, "Test", -1, testMenu)


BindMenuEvent(topMenu, #ID_OPEN, @mencallback())
Define.w cmd, id

Repeat
	ev = WaitWindowEvent()
	Select ev
		Case #PB_Event_Menu
			cmd = EventMenu()
			Select cmd
				Case #ID_ADD_FILE_ITEM
					id = guiMenu::GetMenuItemCount(fileMenu) + 1
					guiMenu::AppendMenuItem(fileMenu, "Item " + Str(id), id)
					
				Case #ID_ADD_TITLE_ITEM
					id = guiMenu::GetMenuItemCount(topMenu) + 1
					guiMenu::AppendMenuItem(topMenu, "Item " + Str(id), -1)
					
				Case #ID_REMOVE_COPY_ITEM
					guiMenu::RemoveMenuItem(editMenu, #ID_COPY)
					
				Case #ID_REMOVE_PASTE_MENU
					guiMenu::SetSubmenu(editMenu, #ID_PASTE, 0)
					
				Case #ID_CHECK_OPEN
					SetMenuItemState(fileMenu, #ID_OPEN, #True)
			EndSelect
		
	EndSelect
			
Until ev = #PB_Event_CloseWindow
CompilerEndIf
Justin
Addict
Addict
Posts: 829
Joined: Sat Apr 26, 2003 2:49 pm

Re: Dynamic menus (Windows / Linux)

Post by Justin »

Just noticed that functions like SetMenuItemState() don't work in Linux, this will require some work to fix.

Fred, is there any plan to support this? i would like to know before spending time on it.
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5342
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: Dynamic menus (Windows / Linux)

Post by Kwai chang caine »

Thanks for sharing 8)
ImageThe happiness is a road...
Not a destination
User avatar
RSBasic
Moderator
Moderator
Posts: 1218
Joined: Thu Dec 31, 2009 11:05 pm
Location: Gernsbach (Germany)
Contact:

Re: Dynamic menus (Windows / Linux)

Post by RSBasic »

Thanks for sharing. Image
Image
Image
User avatar
Shardik
Addict
Addict
Posts: 1989
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Dynamic menus (Windows / Linux)

Post by Shardik »

Justin wrote:Just noticed that functions like SetMenuItemState() don't work in Linux, this will require some work to fix.
It's a limitation of GTK. But I have already demonstrated a cross-platform workaround in this thread.
Justin
Addict
Addict
Posts: 829
Joined: Sat Apr 26, 2003 2:49 pm

Re: Dynamic menus (Windows / Linux)

Post by Justin »

Hi Shardik, thanks for the link.
Mixing / overlaying checkmarks with images is a bit weird, what applications do is display 2 images corresponding to the checked / unchecked state, like if it were a toolbar toggle button like the debuger button in the PB Ide for example, or maybe just altering the image background color.
So you need a function like SetMenuItemImages(menu, item, imagechecked, imageuncheked), and the images are automatically set when the checked state changes.
Not very difficult in Windows and Linux, no idea on mac.
acreis
Enthusiast
Enthusiast
Posts: 182
Joined: Fri Jun 01, 2012 12:20 am

Re: Dynamic menus (Windows / Linux)

Post by acreis »

Thanks!

Excellent work!!!
User avatar
Shardik
Addict
Addict
Posts: 1989
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Dynamic menus (Windows / Linux)

Post by Shardik »

Justin wrote:Mixing / overlaying checkmarks with images is a bit weird, what applications do is display 2 images corresponding to the checked / unchecked state, like if it were a toolbar toggle button like the debuger button in the PB Ide for example, or maybe just altering the image background color.
Sorry, but I don't think that this is a bit weird. It's just the default way Windows is working when using an image. If you take a look into Keya's example you will see that she creates two images and uses

Code: Select all

MenuItem(#MenuItem, MenuItemText$, ImageID(Image))
to display a menu item with a leading image. And

Code: Select all

SetMenuItemState(#MenuItem, MenuItemID, State)
just toggles an overlaying checkmark on and off. This is the default working on Windows in a menu item using an image and I had just written code for Linux and MacOS to simulate that behaviour so that it's identical cross-platform...
Justin
Addict
Addict
Posts: 829
Joined: Sat Apr 26, 2003 2:49 pm

Re: Dynamic menus (Windows / Linux)

Post by Justin »

What you see in Keya's example is not the default OS behaviour in Windows nor Linux, it's what PB does to handle this case, it's probably an ownerdraw menu.

Windows draws a border around the image to represent the checked state. You also have the option to supply checked / unchecked images.

In linux there is not a native menuitem that handles checked images, you either draw your own menu item or pack a GtkImage inside a GtkCheckMenuItem, and the image is displayed next to the checkbox. Also GtkCheckMenuItem acts as an autocheck item, wich is diferent from windows.

I will try to support this somehow.
Post Reply