A Place Where No Dreams Come True...

Current Project: A very simple X11 menu implementation with no external dependencies. It currently supports icons, separators, enabled and disabled fields.

What Does This Code Do...

Implements X11 Popup-Menu class window. It does this by redirecting the appropriate window manager cursor messages to the menu window. This requires that the popup properly release the cursor 'lock' when it is dismissed. Additionally there is support for icons, separators, enable, disable and hover effects. When an enabled item is selected (mouse-button-1) a message is sent to the initiating window with a message relayed from menu creation parameters.

What Doesn't This Code Do...

I have not tried to create cascade menus and I don't believe the code to be capable yet. Although dynamically resettable, the initial theme is hardcoded. Eventually I Hope to create a set of Window-Manager Atom definitions to handle themeing. Finally some of the constants in the drawing routines need to be made runtime-selectable and probably some additional features should be added.

//-----------------------------------------------------------------------------
// menu.c
//
//  X11 popup-menu implementation.
//
// Copyright (c) 2013 - No Fun Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This software is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//  GNU General Public License for more details.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//
//-----------------------------------------------------------------------------

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdlib.h>
#include <string.h>
#include <X11/xpm.h>

#include <report.h>

#include "menu.h"
#include "xwinatom.h"
#include "xwinhlpr.h"

#define defmnuSizeBorder 2
#define defmnuItemBorder 1
#define defmnuBorder 0x00000000
#define defmnuFace 0xFFB0B080
#define defmnuAlert 0xFFC0C000
#define defmnuHilite 0xFFE0E0E0
#define defmnuShadow 0xFF505050
#define defmnuText 0x00000000
#define defmnuTextHilite 0xFF909090
#define defmnuTextShadow 0xFF505050
#define defmnuBackground 0x00000000
#define defmnuIconSize 16

//-----------------------------------------------------------------------------

struct _menu {
  Display      *display;
  Window        parent;
  Window        window;
  int           screen;
  GC            gc;
  Pixmap        bgnd;
  Font          font;
  XRectangle    map;
  Atom          message;
  pMenuDesc     desc;
  unsigned      clrBorder;
  unsigned      clrFace;
  unsigned      clrAlert;
  unsigned      clrHilite;
  unsigned      clrShadow;
  unsigned      clrText;
  unsigned      clrTextHilite;
  unsigned      clrTextShadow;
  unsigned      clrBackground;
  int           bmenu;
  int           selected;
  int           mnuborder;
  int           iconsize;
  int           itmheight;
  int           itmoffset;
  int           itmborder;
  int           strheight;
  int           strwidth;
};

//-----------------------------------------------------------------------------

Window menu_get_window(pMenu menu)
{
  Window rtn = None;

  if(menu)
  {
    rtn = menu->window;
  }
  return rtn;
}

//-----------------------------------------------------------------------------

static void menu_set_gc_mask(pMenu menu, Pixmap mask, int x , int y)
{
  XGCValues vals;
  vals.clip_x_origin = x;
  vals.clip_y_origin = y;
  vals.clip_mask = mask;
  XChangeGC(menu->display, menu->gc, GCClipMask | GCClipXOrigin | GCClipYOrigin, &vals);
}

//-----------------------------------------------------------------------------

void menu_draw(pMenu menu)
{
  if(menu)
  {
    // Start with blank canvas.
    XSetForeground(menu->display, menu->gc, menu->clrFace);
    XFillRectangle(menu->display, menu->bgnd, menu->gc,
		   0,
		   0,
		   menu->map.width,
		   menu->map.height);
    // Menu border.
    XSetForeground(menu->display, menu->gc, menu->clrHilite);
    XDrawLine(menu->display, menu->bgnd, menu->gc, 0, menu->map.height - 2, 0, 0);
    XDrawLine(menu->display, menu->bgnd, menu->gc, 0, 0, menu->map.width - 2, 0);
    XSetForeground(menu->display, menu->gc, menu->clrShadow);
    XDrawLine(menu->display, menu->bgnd, menu->gc, menu->map.width - 1, 0, menu->map.width - 1, menu->map.height - 1);
    XDrawLine(menu->display, menu->bgnd, menu->gc, menu->map.width - 1, menu->map.height - 1, 0, menu->map.height - 1);
    if(menu->selected)
    {
      // Highlight selected item.
      XSetForeground(menu->display, menu->gc, menu->clrTextHilite);
      XFillRectangle(menu->display, menu->bgnd, menu->gc,
		     menu->mnuborder,
		     ((menu->selected - 1) * menu->itmheight) + menu->mnuborder,
		     menu->map.width - (menu->mnuborder * 2),
		     menu->itmheight);
    }
    if(menu->desc)
    {
      int i;
      int x;
      int y;

      for(i = 0; i < menu->desc->items; i++)
      {
	if((menu->desc->item[i].flags & mf_separator) == mf_separator)
	{
	  // Separator item.
	  y = (i * menu->itmheight) + (menu->itmheight / 2);
	  XSetForeground(menu->display, menu->gc, menu->clrShadow);
	  XDrawLine(menu->display, menu->bgnd, menu->gc, 8, y, menu->map.width - 8, y);
	  XSetForeground(menu->display, menu->gc, menu->clrHilite);
	  XDrawLine(menu->display, menu->bgnd, menu->gc, 8 + 1, y + 1, menu->map.width - (8 - 1), y + 1);
	}
	else
	{
	  // Action item.
	  if(menu->desc->item[i].icon)
	  {
	    // Draw icon.
	    x = menu->itmborder + menu->mnuborder;
	    y = (i * menu->itmheight) + menu->itmborder + menu->mnuborder;
	    if(menu->desc->item[i].mask)
	    {
	      menu_set_gc_mask(menu, (Pixmap)menu->desc->item[i].mask, x, y);
	    }
	    XCopyArea(menu->display, (Pixmap)menu->desc->item[i].icon, menu->bgnd, menu->gc,
		      0, 0, menu->iconsize, menu->iconsize, x, y);
	    if(menu->desc->item[i].mask)
	    {
	      menu_set_gc_mask(menu, None, 0, 0);
	    }
	  }
	  // Draw title (label).
	  x = menu->iconsize + (menu->itmborder * 3) + menu->mnuborder;
	  y = ((((i + 1) * menu->itmheight) - menu->itmborder) - (menu->itmoffset + 1)) + menu->mnuborder;
	  if((menu->desc->item[i].state & ms_disabled) == ms_disabled)
	  {
	    // Disabled (draw chisled).
	    XSetForeground(menu->display, menu->gc, menu->clrHilite);
	    XDrawString(menu->display, menu->bgnd, menu->gc,
			x + 1,
			y + 1,
			menu->desc->item[i].title, strlen(menu->desc->item[i].title));
	    XSetForeground(menu->display, menu->gc, menu->clrShadow);
	    XDrawString(menu->display, menu->bgnd, menu->gc,
			x,
			y,
			menu->desc->item[i].title, strlen(menu->desc->item[i].title));
	  }
	  else
	  {
	    // Normal.
	    XSetForeground(menu->display, menu->gc, menu->clrText);
	    XDrawString(menu->display, menu->bgnd, menu->gc,
			x,
			y,
			menu->desc->item[i].title, strlen(menu->desc->item[i].title));
	  }
	}
      }
    }
    // Update window.
    XCopyArea(menu->display, menu->bgnd, menu->window, menu->gc,
    	      0, 0, menu->map.width, menu->map.height, 0, 0);
  }
}

//-----------------------------------------------------------------------------
// Increase reference count.

int menu_enable_menu(pMenu menu)
{
  int ref = -1;
  if(menu)
  {
    ref = menu->bmenu;
    menu->bmenu += 1;
    report(dbglevl_debug, "menu_enable_menu(%i).\n", menu->bmenu);
  }
  return ref;
}

//-----------------------------------------------------------------------------
// Decrease reference count.

int menu_disable_menu(pMenu menu)
{
  int ref = -1;
  if(menu)
  {
    ref = menu->bmenu;
    // FIXME: should catch -1 but not yet.
    menu->bmenu -= 1;
    report(dbglevl_debug, "menu_disable_menu(%i).\n", menu->bmenu);
  }
  return ref;
}

//-----------------------------------------------------------------------------

void menu_show(pMenu menu, XEvent *event)
{
  if(menu)
  {
    report(dbglevl_debug, " menu popup w=%i h=%i.\n", menu->map.width, menu->map.height);
    XMoveResizeWindow(menu->display, menu->window,
		      event->xbutton.x_root, event->xbutton.y_root, menu->map.width, menu->map.height);
    XMapWindow(menu->display, menu->window);
    XGrabPointer(menu->display, menu->window, False,
		 ButtonPressMask | PointerMotionMask,
		 GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
  }
}

//-----------------------------------------------------------------------------

void menu_hide(pMenu menu)
{
  if(menu)
  {
    XUnmapWindow(menu->display, menu->window);
    report(dbglevl_debug, " menu hide.\n");
    XUngrabPointer(menu->display, CurrentTime);
    menu->desc = NULL;
  }
}

//-----------------------------------------------------------------------------

static void menu_notify_parent(pMenu menu)
{
  XEvent event;

  memset(&event, 0, sizeof(event));

  event.xclient.type         = ClientMessage;
  event.xclient.display      = menu->display;
  event.xclient.window       = menu->window;
  event.xclient.message_type = menu->message;
  event.xclient.format       = 32;
  event.xclient.data.l[0]    = menu->window;
  event.xclient.data.l[1]    = menu->desc->item[menu->selected - 1].itemid;
  event.xclient.data.l[2]    = menu->desc->item[menu->selected - 1].flags;
  event.xclient.data.l[3]    = menu->desc->item[menu->selected - 1].state;
  XSendEvent(menu->display, menu->parent, 0, 0, &event);
}

//-----------------------------------------------------------------------------

void menu_select(pMenu menu, XEvent *event)
{
  if(menu)
  {
    if(event->xbutton.button != Button1)
    {
      // Reject all; Accept Button1.
      menu->selected = 0;
    }
    if(menu->selected)
    {
      report(dbglevl_debug, " menu selected item=%i id=%i label=%s.\n", menu->selected,
	     menu->selected ? menu->desc->item[menu->selected - 1].itemid : 0,
	     menu->selected ? menu->desc->item[menu->selected - 1].title : "None");
      menu_notify_parent(menu);
    }
    menu_hide(menu);
  }
}

//-----------------------------------------------------------------------------

void menu_track(pMenu menu, XEvent *event)
{
  if(menu)
  {
    unsigned int select = ((event->xmotion.y - menu->mnuborder) / menu->itmheight) + 1;
    if((event->xmotion.x > (menu->map.x + menu->mnuborder)) &&
       (event->xmotion.x < (menu->map.width - menu->mnuborder)) &&
       (event->xmotion.y > (menu->map.y + menu->mnuborder)) &&
       (event->xmotion.y < (menu->map.height - menu->mnuborder)) &&
       ((menu->desc->item[select - 1].flags & mf_separator) == mf_text) &&
       ((menu->desc->item[select - 1].state & ms_disabled) != ms_disabled))
    {
      if(menu->selected != select)
      {
	menu->selected = select;
	report(dbglevl_debug, " menu track - point.x=%i point.y=%i highlight=%i.\n",
	       event->xmotion.x, event->xmotion.y, menu->selected);
	menu_draw(menu);
      }
    }
    else
    {
      if(menu->selected)
      {
	menu->selected = 0;
	report(dbglevl_debug, " menu track - point.x=%i point.y=%i highlight=%i.\n",
	       event->xmotion.x, event->xmotion.y, menu->selected);
	menu_draw(menu);
      }
    }
  }
}

//-----------------------------------------------------------------------------

pMenuDesc menu_create_popup(int count, MenuItem items[])
{
  pMenuDesc desc = malloc(sizeof(unsigned int) + (sizeof(MenuItem) * (count + 1)));
  if(desc)
  {
    desc->items = count;
    memcpy(&desc->item, items, sizeof(MenuItem) * count);
  }
  return desc;
}

//-----------------------------------------------------------------------------

void menu_init_popup(pMenu menu, pMenuDesc desc)
{
  if(menu)
  {
    int i;
    int w;
    int direction;
    int ascent;
    int descent;
    XCharStruct xchar;

    report(dbglevl_debug, " menu_init_popup() - ");
    menu->desc = desc;
    menu->map.height = (menu->itmheight * desc->items) + (menu->mnuborder * 2);
    menu->selected = 0;
    for(i = 0; i < desc->items; i++)
    {
      XQueryTextExtents(menu->display, XGContextFromGC(menu->gc),
			menu->desc->item[i].title, strlen(menu->desc->item[i].title),
			&direction, &ascent, &descent, &xchar);
      w = xchar.width + (menu->iconsize + (menu->itmborder * 4)) + (menu->mnuborder * 2);
      if(w > menu->map.width)
      {
	menu->map.width = w;
      }
    }
    report(dbglevl_debug, "items=%i max width=%i item height=%i.\n", desc->items, w, menu->itmheight);
  }
}

//-----------------------------------------------------------------------------

void menu_set_state(pMenu menu, int id, MenuState state)
{
  int i;
  if(menu)
  {
    if(menu->desc)
    {
      for(i = 0; i < menu->desc->items; i++)
      {
	if(menu->desc->item[i].itemid == id)
	{
	  menu->desc->item[i].state = state;
	}
      }
    }
  }
}

//-----------------------------------------------------------------------------

void menu_set_font(pMenu menu, char *sfont)
{
  if(menu)
  {
    int direction;
    int ascent;
    int descent;
    XCharStruct xchar;

    report(dbglevl_debug, " menu set font %s.\n", sfont);
    menu->font = XLoadFont(menu->display, sfont);
    XSetFont(menu->display, menu->gc, menu->font);
    XQueryTextExtents(menu->display, XGContextFromGC(menu->gc),
		      "0", 1,
		      &direction, &ascent, &descent,
		      &xchar);
    menu->strheight = ascent + descent;
    menu->strwidth  = xchar.width;
    menu->itmheight = ascent + descent + ((menu->itmborder + 0) * 2);
    menu->itmoffset = descent;
    if(menu->itmheight < (menu->iconsize + (menu->itmborder * 2)))
    {
      // Force minimum size.
      menu->itmheight = menu->iconsize + (menu->itmborder * 2);
    }
  }
}

//-----------------------------------------------------------------------------

void menu_popup_get_icons(pMenu menu, pMenuDesc desc)
{
  if(menu)
  {
    int i;
    Pixmap pix;
    Pixmap pixm;
    char **map;

    for(i = 0; i < desc->items; i++)
    {
      if(desc->item[i].icon)
      {
	map = desc->item[i].icon;
	XpmCreatePixmapFromData(menu->display, menu->window, map, &pix, &pixm, NULL);
	if(pix)
	{
	  report(dbglevl_debug, "  decoded xpm icon=%i mask=%i label=%s.\n", pix, pixm, desc->item[i].title);
	}
	desc->item[i].icon = (void*)pix;
	desc->item[i].mask = (void*)pixm;
      }
    }
  }
}

//-----------------------------------------------------------------------------

void menu_popup_destroy_icons(pMenu menu, pMenuDesc desc)
{
  if(menu)
  {
    int i;

    for(i = 0; i < desc->items; i++)
    {
      if(desc->item[i].icon)
      {
	report(dbglevl_debug, " releasing menu icon=%i mask=%i title=%s.\n",
	       desc->item[i].icon, desc->item[i].mask, desc->item[i].title);
	XFreePixmap(menu->display, (Pixmap)desc->item[i].icon);
	desc->item[i].icon = 0;
      }
      if(desc->item[i].mask)
      {
	XFreePixmap(menu->display, (Pixmap)desc->item[i].mask);
	desc->item[i].mask = 0;
      }
    }
  }
}

//-----------------------------------------------------------------------------

void menu_destroy(pMenu *menu)
{
  if(menu)
  {
    pMenu mnu = *menu;
    if(mnu)
    {
      report(dbglevl_debug, " Destroying popup-menu : %08X window=%08X.\n", mnu, mnu->window);
      if(mnu->bgnd)
      {
        report(dbglevl_debug, " Freeing menu background pixmap.\n");
	XFreePixmap(mnu->display, mnu->bgnd);
      }
      XFreeGC(mnu->display, mnu->gc);
      free(mnu);
    }
    *menu = NULL;
  }
}

//-----------------------------------------------------------------------------

pMenu menu_create(Display *display, Window parent, int width, int height, Atom message)
{
  XSetWindowAttributes attrs;
  pMenu menu = malloc(sizeof(Menu));
  if(menu)
  {
    memset(menu, 0, sizeof(Menu));
    menu->display        = display;
    menu->parent         = parent;
    menu->screen         = DefaultScreen(display);
    menu->map.x          = 0;
    menu->map.y          = 0;
    menu->map.width      = width;
    menu->map.height     = height;
    menu->clrBorder      = defmnuBorder;
    menu->clrFace        = defmnuFace;
    menu->clrAlert       = defmnuAlert;
    menu->clrHilite      = defmnuHilite;
    menu->clrShadow      = defmnuShadow;
    menu->clrText        = defmnuText;
    menu->clrTextHilite  = defmnuTextHilite;
    menu->clrTextShadow  = defmnuTextShadow;
    menu->clrBackground  = defmnuBackground;
    menu->iconsize       = defmnuIconSize;
    menu->mnuborder      = defmnuSizeBorder;
    menu->itmborder      = defmnuItemBorder;
    menu->message        = message;
    menu->window  = XCreateSimpleWindow(display,
					DefaultRootWindow(display), //parent,
					menu->map.x,
					menu->map.y,
					menu->map.width,
					menu->map.height,
					0,
					WhitePixel(display, 0),
					menu->clrBackground);
    XSelectInput(menu->display, menu->window, ExposureMask | ButtonPressMask);
    menu->gc = XCreateGC(menu->display, menu->window, 0, NULL);
    menu_set_font(menu, "9x15bold");//"7x14");
    menu->bgnd = XCreatePixmap(menu->display, menu->window, 640, 1920, 24);
    attrs.background_pixmap = menu->bgnd;
    attrs.override_redirect = True;
    attrs.save_under        = True;
    XChangeWindowAttributes(menu->display, menu->window, CWBackPixmap | CWOverrideRedirect | CWSaveUnder, &attrs);
    xwin_set_window_property_atoms(menu->display, menu->window, _NET_WM_WINDOW_TYPE,
				   (unsigned char*)&_NET_WM_WINDOW_TYPE_MENU, 1);
    report(dbglevl_debug, " Created popup-menu : %08X window=%08X.\n", menu, menu->window);
  }
  return menu;
}

//-----------------------------------------------------------------------------
// end: menu.c
//

  

And The Companion Header...

//-----------------------------------------------------------------------------
// menu.h
//
//  X11 popup-menu definition.
//
// Copyright (c) 2013 - No Fun Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This software is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//  GNU General Public License for more details.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//
//-----------------------------------------------------------------------------

typedef enum _emenuflags { mf_text = 0,
			   mf_separator = 1,
			   mf_check = 2,
			   mf_radio = 4
} MenuFlags;

typedef enum _emenustate { ms_normal = 0,
			   ms_highlighted = 1,
			   ms_selected = 2,
			   ms_disabled = 4,
			   ms_checked = 8
} MenuState;

typedef struct _menuitem {
  unsigned int  itemid;    // Unique Identifier for upstream processing.
  void         *icon;      // Item graphic.
  void         *mask;      // Icon mask.
  char          title[80]; // Item display.
  const char   *hint;      // Item hint.
  unsigned int  flags;     // Item flags (style,etc...).
  unsigned int  state;     // Item state.
} MenuItem, *pMenuItem;

typedef struct _menudesc {
  unsigned int  items;
  MenuItem      item[];
} MenuDesc, *pMenuDesc;

typedef struct _menu Menu, *pMenu;

//-----------------------------------------------------------------------------

extern Window menu_get_window(pMenu);
extern void menu_draw(pMenu);
extern int menu_enable_menu(pMenu);
extern int menu_disable_menu(pMenu);
extern void menu_show(pMenu, XEvent*);
extern void menu_hide(pMenu);
extern void menu_select(pMenu, XEvent*);
extern void menu_track(pMenu, XEvent*);
extern pMenuDesc menu_create_popup(int, MenuItem[]);
extern void menu_init_popup(pMenu, pMenuDesc);
extern void menu_set_state(pMenu, int, MenuState);
extern void menu_popup_get_icons(pMenu, pMenuDesc);
extern void menu_popup_destroy_icons(pMenu, pMenuDesc);
extern void menu_destroy(pMenu*);
extern pMenu menu_create(Display*, Window, int, int, Atom);

//-----------------------------------------------------------------------------
// end: menu.h
//

  

The Code Demonstrates The Following Programming Techniques...

Conclusion...

It is known that this code has many 'Freshman' mistakes in it. It is not claimed to be good code. Hopefully though; it demonstrates some of the less obvious interface mechanisms when communicating with both a Window-Manager and a Client program. A definition of how to use the code is also missing. Not purposely, an interface spec just can't be fit into my schedule. For now. Besides... This code is not really meant to be used. Although I use this code it is presented here purely for educational purposes.

6991