SetMenuItemState destroys menu icons

Linux specific forum
User avatar
Keya
Addict
Addict
Posts: 1891
Joined: Thu Jun 04, 2015 7:10 am

SetMenuItemState destroys menu icons

Post by Keya »

SetMenuItemState() (ie. un/setting a checkmark on a menu) appears to destroy the menu icon in Linux. Windows and Mac are ok.

I don't know of a workaround for showing a checked menu on Linux other than to not use icons; as far as I can tell there is no way to set an icon apart from during its creation, so I can't just refresh the image like with SetGadgetState(#image, ImageId(hImg)) for example -- a SetMenuItemImage() function would be a welcome addition.

Simple demo of the problem follows, just click the menu items to toggle check on/off which destroys the icons:
Image

Code: Select all

hImg1 = CreateImage(#PB_Any,16,16,24,RGB(0,0,255))  ;Icon 16x16 plain Blue
hImg2 = CreateImage(#PB_Any,16,16,24,RGB(0,255,0))  ;Icon 16x16 plain Green
;Both images are created and remain resident, never FreeImage'd so thats not the problem
#MenuItem1=1
#MenuItem2=2

If OpenWindow(0, 200, 200, 300, 100, "Image menu Example", #PB_Window_ScreenCentered | #PB_Window_SystemMenu)
  If CreateImageMenu(0, WindowID(0))
    MenuTitle("File")
    MenuItem(#MenuItem1, "Icon1 (click to toggle)", ImageID(hImg1))
    MenuItem(#MenuItem2, "Icon2 (click to toggle)", ImageID(hImg2))
  EndIf  
  Repeat
    evt = WaitWindowEvent() 
    If evt = #PB_Event_Menu
      curmenu = EventMenu()
      SetMenuItemState(0, lastmenu, 0)
      SetMenuItemState(0, curmenu, 1) 
      lastmenu = curmenu
    EndIf
  Until evt = #PB_Event_CloseWindow
EndIf
User avatar
Shardik
Addict
Addict
Posts: 1989
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: SetMenuItemState destroys menu icons

Post by Shardik »

Keya wrote:SetMenuItemState() (ie. un/setting a checkmark on a menu) appears to destroy the menu icon in Linux. Windows and Mac are ok.
Unfortunately this doesn't seem to be a PureBasic bug but a limitation of GTK. The Eclipse documentation states:
Eclipse documentation for setImage wrote:Furthermore, some platforms (such as GTK), cannot display both a check box and an image at the same time. Instead, they hide the image and display the check box.
User avatar
Shardik
Addict
Addict
Posts: 1989
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: SetMenuItemState destroys menu icons

Post by Shardik »

Keya has demonstrated above that GTK on Linux (and hence PureBasic) is not able to display both an icon and a checkmark in a menu entry.

MacOS displays both a checkmark and an icon in a menu entry:

Image

Windows 7 displays an icon overlayed with a checkmark:

Image

So it's a mess when trying to program a cross-platform solution with menu items containing both an icon and a checkmark which should look as uniform as possible on all 3 operating systems (and it's currently not possible at all in Linux... :?)

Therefore I rolled up my own solution which uses the PureBasic native way on Windows and separate API solutions for Linux and MacOS (by using a checkmark generated by vector graphics commands which looks very similar to the Windows checkmark and which is overlayed over the icon). So now the Linux and MacOS menu entries look as similar as possible to the Windows ones:

Linux:

Image

MacOS:

Image

The MacOS solution even removes the now unnecessary separate checkmark column with an API command which otherwise would contain a strange looking leading empty space.

I have tested my code example successfully on these operating systems:
- Linux Mint 18 'Sarah' x64 Cinnamon with GTK 3 and PB 5.60 x64
- MacOS 10.6.8 'Snow Leopard' with PB 5.60 x86
- MacOS 10.12.4 'Sierra' with PB 5.60 x86 and x64
- Windows 7 SP1 x64 with PB 5.60 x86 and x64

My example is different from Keya's because you are able to toggle the checkmark in each menu entry (in Keya's example the checkmark is toggled between the two different entries).

Code: Select all

EnableExplicit

Define ImageIDGreen.I
Define ImageIDPink.I
Define SelectedMenuItem.I

CompilerIf #PB_Compiler_OS <> #PB_OS_Windows
  Structure MenuItemDataEntry
    MenuItemID.I
    IsChecked.I
    ImageIDChecked.I
    ImageIDUnchecked.I
    ItemText.S
  EndStructure

  Define i.I
  Define ImageID.I
  Define ImageIDCheckmark.I
  Define MenuItem.I

  Dim MenuItemData.MenuItemDataEntry(0)

  Procedure.I CreateVectorCheckmark(ImageID.I)
    Protected Result.I
    
    Result = CreateImage(ImageID, 16, 16, 32, #PB_Image_Transparent)
    
    If Result
      If ImageID = #PB_Any
        ImageID = Result
      EndIf
      
      If StartVectorDrawing(ImageVectorOutput(ImageID, #PB_Unit_Pixel)) = 0
        FreeImage(ImageID)
        ProcedureReturn 0
      EndIf
      
      VectorSourceColor($FF000000)
      MovePathCursor(5, 7)
      AddPathLine(7, 10)
      AddPathLine(12, 5)
      StrokePath(1.5)
      StopVectorDrawing()
      
      ProcedureReturn ImageID
    EndIf
  EndProcedure

  Procedure.I GetMenuItem(Menu.I, ItemText.S)
    Protected i.I
    
    CompilerSelect #PB_Compiler_OS
      CompilerCase #PB_OS_Linux ; ---------------------------------------------
        Protected Child.I
        Protected ChildrenCount.I
        Protected ChildrenList.I
        Protected MenuItem.I
        Protected WidgetName.S
        
        ChildrenList = gtk_container_get_children_(Menu)
        ChildrenCount = g_list_length_(ChildrenList)
        
        If ChildrenCount > 0
          For i = 0 To ChildrenCount - 1
            Child = g_list_nth_data_(ChildrenList, i)
            WidgetName = PeekS(gtk_widget_get_name_(Child), -1, #PB_UTF8)
            
            Select WidgetName
              Case "GtkMenuItem", "GtkImageMenuItem"
                MenuItem = GetMenuItem(Child, ItemText)
                
                If MenuItem <> 0
                  Break
                Else
                  Menu = gtk_menu_item_get_submenu_(Child)
                  
                  If Menu
                    MenuItem = GetMenuItem(Menu, ItemText)
                    
                    If MenuItem <> 0
                      Break
                    EndIf
                  EndIf
                EndIf
              Case "GtkAccelLabel"
                If ItemText = PeekS(gtk_label_get_text_(Child), -1, #PB_UTF8)
                  MenuItem = gtk_widget_get_parent_(Child)
                  Break
                EndIf
            EndSelect
          Next i
          
          g_list_free_(ChildrenList)
        EndIf
        
        ProcedureReturn MenuItem
      CompilerCase #PB_OS_MacOS ; ---------------------------------------------
        Static IterationCount.I
        Static MenuItem.I
        
        Protected ItemCount.I
        
        If PeekS(CocoaMessage(0, CocoaMessage(0, Menu, "className"),
          "UTF8String"), -1, #PB_UTF8) = "NSMenu"
          ItemCount = CocoaMessage(0, Menu, "numberOfItems")
        Else
          ItemCount = CocoaMessage(0, Menu, "count")
        EndIf
        
        For i = 0 To ItemCount - 1
          If IterationCount = 0
            MenuItem = CocoaMessage(0, Menu, "objectAtIndex:", i)
          Else
            MenuItem = CocoaMessage(0, Menu, "itemAtIndex:", i)
          EndIf
          
          If ItemText = PeekS(CocoaMessage(0,
            CocoaMessage(0, MenuItem, "title"), "UTF8String"), -1, #PB_UTF8)
            ProcedureReturn
          EndIf
          
          If IterationCount = 0
            IterationCount + 1
            GetMenuItem(MenuItem, ItemText)
          Else
            If CocoaMessage(0, MenuItem, "isSeparatorItem") = #False
              If CocoaMessage(0, MenuItem, "hasSubmenu")    
                IterationCount + 1
                GetMenuItem(CocoaMessage(0, MenuItem, "submenu"), ItemText)
              EndIf
            EndIf
          EndIf
        Next i
        
        IterationCount - 1
        
        ProcedureReturn MenuItem
    CompilerEndSelect
  EndProcedure
  
  ImageIDCheckmark = CreateVectorCheckmark(#PB_Any)
CompilerEndIf

Procedure MenuItemEx(MenuItemID.I, ItemText.S, ImageID.I)
  CompilerIf #PB_Compiler_OS = #PB_OS_Windows
    MenuItem(MenuItemID, ItemText, ImageID(ImageID))
  CompilerElse
    Shared MenuItemData.MenuItemDataEntry()
    Shared ImageIDCheckmark.I
    Static IsArrayInitialized.I
    Protected MenuItemCount.I
    
    MenuItem(MenuItemID, ItemText, ImageID(ImageID))
    MenuItemCount = ArraySize(MenuItemData())
    
    If IsArrayInitialized = #True
      MenuItemCount + 1
      ReDim MenuItemData(MenuItemCount)
    Else
      IsArrayInitialized = #True
    EndIf
    
    MenuItemData(MenuItemCount)\MenuItemID = MenuItemID
    MenuItemData(MenuItemCount)\ItemText = ItemText
    MenuItemData(MenuItemCount)\ImageIDUnchecked = ImageID
    MenuItemData(MenuItemCount)\ImageIDChecked = CopyImage(ImageID, #PB_Any)
    
    If StartDrawing(ImageOutput(MenuItemData(MenuItemCount)\ImageIDChecked))
      DrawAlphaImage(ImageID(ImageIDCheckmark), 0, 0, 255)
      StopDrawing()
    EndIf
  CompilerEndIf
EndProcedure

ImageIDPink  = CreateImage(#PB_Any, 16, 16, 24, $FF00FF) ; Icon 16x16 Pink
ImageIDGreen = CreateImage(#PB_Any, 16, 16, 24, $00FF00) ; Icon 16x16 Green

OpenWindow(0, 270, 100, 350, 100, "Menu with icon and overlayed checkmark")

If CreateImageMenu(0, WindowID(0))
  MenuTitle("Checkmark-Demo")
  MenuItemEx(0, "Option 1 (Click to toggle checkmark)", ImageIDPink)
  MenuItemEx(1, "Option 2 (Click to toggle checkmark)", ImageIDGreen)
EndIf 

CompilerIf #PB_Compiler_OS = #PB_OS_MacOS
  ; ----- Remove separate checkmark column
  Define MenuTitle.I
  MenuTitle = CocoaMessage(0, MenuID(0), "objectAtIndex:", 0)
  CocoaMessage(0, MenuTitle, "setShowsStateColumn:", #NO)
CompilerEndIf

Repeat
  Select WaitWindowEvent()
    Case #PB_Event_CloseWindow
      Break
    Case #PB_Event_Menu
      SelectedMenuItem = EventMenu()

      CompilerIf #PB_Compiler_OS = #PB_OS_Windows
        SetMenuItemState(0, SelectedMenuItem,
          GetMenuItemState(0, SelectedMenuItem) ! 1)
      CompilerElse
        For i = 0 To ArraySize(MenuItemData())
          If MenuItemData(i)\MenuItemID = SelectedMenuItem
            If MenuItemData(i)\IsChecked
              MenuItemData(i)\IsChecked = #False
              ImageID = MenuItemData(i)\ImageIDUnchecked
            Else
              MenuItemData(i)\IsChecked = #True
              ImageID = MenuItemData(i)\ImageIDChecked
            EndIf

            MenuItem = GetMenuItem(MenuID(0), MenuItemData(i)\ItemText)

            CompilerSelect #PB_Compiler_OS
              CompilerCase #PB_OS_Linux
                gtk_image_menu_item_set_image_(MenuItem,
                  gtk_image_new_from_pixbuf_(ImageID(ImageID)))
              CompilerCase #PB_OS_MacOS
                CocoaMessage(0, MenuItem, "setImage:", ImageID(ImageID))
            CompilerEndSelect

            Break
          EndIf
        Next i
    CompilerEndIf
  EndSelect
ForEver
Post Reply