DesktopPopUpMenu - hierarchical sub-menus?

I would like to use the DesktopPopUpMenu with an already created hierarchical desktopMenuItem.

However if I try to add the menu like:

Var m As DesktopMenuItem = mUnits.menu.Clone
For i As Integer = 0 To m.count - 1
  Me.AddRow m.MenuAt(i)
Next

It only takes the first element in the hierarchy and doesn’t add the submenus that are already embedded in m.

Is there a way to achieve whilst keeping the look and feel of the standard control?

Bonjour,

In ConstructContextualMenu, I do that :

Var popMenu As DeskTopMenuItem
popMenu = MenuAction.Clone
base = popMenu.PopUp

and I have the same menu as a popup with sub-menus…

Eric
I had tried that before, expecting it to work, but it appears ContextualMenus don’t work on the Mac. Extract from the manual: “ContextualMenus do not work with PopupMenu on macOS due to a limitation of the underlying macOS control that is used.”

So currently I am using a desktopMenuItem.popup in the MouseUp event - and having to backfill the result into the text of a blank first row so that it looks as if it has been selected. Not as pretty.

I don’t understand.
With the code that I posted before, I have that :


Sorry it is in French.

I don’t understand why we have a difference.

  1. On my Mac (Ventura 13.2, Xojo 2022r4) - ConstructContextualMenu does not fire when clicking the control.

  2. In your code, base is the root menuItem for the popup control, but you have assigned to it the result of your popMenu (base = popMenu.popUp). I’m not sure that would work as you indicate.

ConstructContextualMenu fires on a right click.

Ah, I think we are discussing different things. I am trying to use the DesktopPopupMenu - not right click on canvas/window/container etc.

Oups, sorry!
I have some DesktopPopupMenu but without submenus.

There’s probably a feedback somewhere for doing this, as the underlying macOS control will happily allow it, it’s just Xojo’s framework that sits atop won’t.

What you can do is to build a hierarchical menu of menu items, Then you grab [NSMenuitem submenu] from the base item, then use [NSPopupButton setMenu:] to set that menu onto the PopupMenu.

When I used it, I dynamically built the menu at open and used addHandler to capture the action event of each item. However I was using the PopupMenu in a “PullDown” form [NSPopupButton setPullsDown:], whereby selecting an item from the menu didn’t change the displayed label of the PopupMenu.

The declares for doing this is part of my App Kit, but are NOT compatible with DesktopControls. I’m sure the MVPs can provide you with a version or plugin that works with DesktopControls.

Thanks Sam - I’ll use MBS’s stuff to get this working.

Whoa wait. The AddRow method of DesktopPopupMenu will also take a DesktopMenuItem. Why can’t you use that?

https://documentation.xojo.com/api/user_interface/desktop/desktoppopupmenu.html#desktoppopupmenu-addrow

Continuing this thread…

As Greg points out DesktopPopupMenu has method:
.addRow(item as desktopMenuItem)
and Event
.selectionChanged item as desktopMenuItem

However if I want to extract the associated menuItems from any row (e.g. to see if it has underlying hierarchy) I can’t. There appears to be no .menuAt method. All the methods return only a text value or a tag variant.

Why? So that I can do .selectRowWithTag for hierarchical menus.

Is there an obvious workaround or do I have to subclass to manage an array of menuItems to duplicate what is already there?

I keep a map of the hierarchical structure in an SQLite database and build the corresponding menu structure from that. If I want the “associated menuItems from any row”, then I get that direct from the database.

The same map also drives a listbox with the same structure. The user can drag rows around to re-order the listbox, which gets saved to the database so that, when the menu is opened again, it reflects the listbox structure.

Thanks Tim

SelectRowWithTag is relatively new (and limited) in Xojo. It’s not suitable for menus that “hijack” the native popup menu, I’d say.

One way is to make your own loop, along with recursion. For example, to find which menu item is checked in a given menu:

Public Function GetCheckedMenuItem(InMenu As DesktopMenuItem) As DesktopMenuItem
if InMenu=nil then Return nil

for i as Integer=0 to InMenu.LastRowIndex
Var mi as DesktopMenuItem=InMenu.MenuAt(i)

if mi<>nil then
  if mi.HasCheckMark then Return mi
  
  mi=GetCheckedMenuItem(mi)
  if mi<>nil then Return mi
end if

next
End Function

And to select an item with a given tag, you’d do similarly. Pass the tag as a parameter.

Hi,

Here is how I did it.
First you’ll need 2 functions

  • One to initially load the top level values of your hierarchy (LoadMenu)
  • One to update when the popupmenu value is changed (UpdateMenu)

Setup

  • One table (menu_table) with the menu structure: menu_id, menu_name, parent_id.
  • PopupMenu field is called popup_menu
  • parent_ids is a window property
  • menu_ids is a window property
  • db is the application database object

Function LoadMenu

dim parent_ids as new dictionnary
dim rs as rowset = db.SelectSQL("select * from menu_table")
while not rs.afterlastrow
   parent_ids(rs.column("parent_id").stringvalue) = "0)
   menu_ids(rs.column("menu_id").StringValue) = rs.column("menu_name").StringValue
   rs.movetonextrow
wend
rs.movetofirstrow
dim i as integer
while not rs.AfterLastRow
  if menu_id.HasKey(rs.Column("parent_id").StringValue)=false then
    popup_menu.AddRow(rs.Column("menu_name").StringValue+"-->")
    popup_menu.RowTagAt(i) = rs.Column("menu_id").IntegerValue
    i=i+1
  end if
  rs.MoveToNextRow
wend
rs.close

Function UpdateMenu

// This function is called by an event handler on the popup_menu object on selection change. The input is "id as string" The call in popup_menu is:
// if Me.SelectedRowIndex <> -1 then
//    UpdateMenu(Me.RowTagAt(Me.SelectedRowIndex))
// end if

dim selected_id as string = id
dim reset_cat as Boolean = false
'Check if requested a level up
if left(id,2) = "-2" then
  dim previous_id as string = mid(id,3)
  dim s_rs as RowSet = db.SelectSQL("select * from menu_table where menu_id=?", previous_id)
  if menu_ids.HasKey(s_rs.Column("parent_id").StringValue) = true then
    selected_id = s_rs.Column("parent_id").StringValue
  else
    'Requested top level
    reset_cat = true
  end if
end if

if reset_cat then
  LoadMenu
else
  'Check if new selection has children. If so, rebuild menu
  if parent_ids.HasKey(selected_id)= true then
    ' Add the level up selection
    popup_menu.RemoveAllRows
    popup_menu.AddRow("--Return one level up")
    popup_menu.RowTagAt(0) = "-2" + selected_id
    
    ' Add the current selection 
    popup_menu.AddRow(menu_ids.Value(selected_id).StringValue)
    popup_menu.RowTagAt(1) = selected_id
    
    ' Add the children
    dim rs as RowSet = App.empDB.SelectSQL("select * from menu_table where parent_id=?",selected_id)
    dim j as Integer = 2
    while not rs.AfterLastRow
      if parent_ids.HasKey(rs.Column("menu_id").StringValue)=true then
        popup_menu.AddRow("---"+rs.Column("category_name").StringValue+"-->")
      else
        popup_menu.AddRow("---"+rs.Column("category_name").StringValue)
      end if
      popup_menu.RowTagAt(j) = rs.Column("ebay_id").IntegerValue
      j=j+1
      rs.MoveToNextRow
    wend
    rs.close
  end if
  popup_menu.SelectRowWithTag(selected_id)
end if

Try it on!