Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / X11

Programming Xlib with Mono Develop - Part 3: Motif widgets (proof of concept)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
17 Sep 2013CPOL18 min read 15.3K   251   2  
How to call native Xm API from Mono Develop C# ending up in a very little Motif widget application.

 Download complete project for 32 bit (zip) 

 Download complete project for 64 bit (zip)

Introduction 

This article shows how to access Xm/Motif widgets API from C# using Mono Develop. A lot of API calls are ready-to-use defined and tested and some challenges are mastered, e.g., using windows manager protocol signals, transparent color image, menu callbacks and creating new custom dialogs. Moreover a third party widgets is incorporated and a multifont string is demonstrated.

This article is aimed to verify that programming Xm/Motif widgets with C# can be easyly achieved (prove of concept). It provides a sample application's complete project for 32 bit and 64 bit.

Background

This is the third article in a series of articles examining X* API calls from C# using Mono Develop.
The first article was Programming Xlib with Mono Develop - Part 1: Low-level (proof of concept) and dealed with Xlib/X11. The second article was Programming Xlib with Mono Develop - Part 2: Athena widgets (proof of concept) and dealed with Xt/Athena. The current article goes ahead to the Xm/Motif widgets and shows how to use them from a modern language/IDE like C#/Mono. 

This article illustrates how easy programming Xm/Motif using Xm API calls from C# is to achieve. Since Open Motif made Motif available with source code and under a public license, almost every XWindow system can deal with the Motif widgets royalty free. Moreover the effort to create a GUI with Motif is a fraction compared to Xlib/X11 and even to Xt/Athena. These might be two strong reasons to go in for Motif.

Using the code 

The sample application was written with Mono Develop 2.4.1 for Mono 2.8.1 on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop. Neither the port to any older nor to any newer version should be a problem. However the software packsges openmotif, openmotif-libs and openmotif-devel (versions 2.3.2-2.8 and 2.3.4-25.1 are tested) must be installt. The sample application's solution consists of five projects (the complete sources are provided for download):

  • X11Wrapper defines the function prototypes, structures and types for Xlib/X11 calls  to the libX11.so  (already known from the first article of this series)
  • XtWrapper defines the function prototypes, structures and types for Xt calls to the libXt.so (already known from the second article of this series)
  • XmNative contains a very little amount of native C code to access Motif methods and one sample that shows how to incorporate third party Xm widgets (Microline Progress widget)  - /usr/lib/libXm.so or /usr/lib64/libXm.so must be explicitly referenced to compile the project
  • XmWrapper defines the function prototypes, structures and types for Xm calls to the libXm.so and the function prototypes for Motif methods calls to the libXmNative.so plus Microline Progress widget calls
  • Xm contains the sample application based on Motif widgets and one exemplary third party Xm widget
The sample application is also tested with Mono Develop 3.0.6 for Mono 3.0.4 on OPEN SUSE 12.3 Linux 64 bit DE and GNOME desktop, IceWM, TWM und Xfce. The only difference between the 32 bit and the 64 bit solution is the definition of some types, as already described in the first article of this series.

The application itself has 1163 lines of code only. Some generic GUI code is released to several Xm* classes - not to wrap Motif widgets with C#, but to organize re-usable code. The function prototypes of the Xm/Motif and some necessary structures / enumerations are defined in the Xmlib class. The application shows:

  1. a menu bar with 4 buttons, where the first and third button have a dropdown menu and the second and fourth button have callbacks,
  2. a dropdown menu with multiple entries at the first and third menu button,
  3. a XmFileSelectionDialog at the first menu button's dropdown menu, 
  4. an extended Motif message dialog at the second menu button,
  5. a hand made dialog based on XmRowColumn, a hand made dialog based on XmPanedWindow and a hand made dialog based on XmForm at the third menu button's dropdown menu,
  6. a drawing area and three buttons to draw random lines, ellipses or rectangles,
  7. an application icon with transparency (not all up to date windows managers display it, but Xfce does),
  8. a progress bar based on third party Motif widget Microline Progress, 
  9. a status bar with bitmap and label and
  10. a XmQuestionDialog with bitmap, that can cancel application quit.

  

Look & feel on GNOME desktop 2.30.0 (32 bit) and on Xfce 4.10 (64 bit)

Manage the WM_DELETE_WINDOW signal 

Motif provides several convenience interfaces for the VendorShell to access the windows manager protocol and to minimize the number of code lines for callback registration. XmAddWMProtocolCallback() and XmAddProtocolCallback() add client callbacks for a windows manager protocol and hide the handling of action records and translation tables. Although XmAddWMProtocolCallback() is a internally calling XmAddProtocolCallback(), i couldn't get it to work. Counterwise XmAddProtocolCallback() does its job with atoms created by XmInternAtom() as well by XInternAtom(). The next code sampe illustrates how XmAddProtocolCallback() can be used to register a callback for the WM_DELETE_WINDOW signal.

C#
/* Hook the closing event from windows manager. */
// Turn off default delete response.
Arg[] shellArgs	= { new Arg(XmNames.XmNdeleteResponse,	(XtArgVal)DeleteResponse.XmDO_NOTHING) };
Xtlib.XtSetValues (_windowShell, shellArgs, (XCardinal) 1);
 
// Install delete callback.
IntPtr display = Xtlib.XtDisplay (_windowShell);
IntPtr wmDeleteMessage = Xmlib.XmInternAtom (display, "WM_DELETE_WINDOW", false);
IntPtr callbackUnmanagedPointer = CallBackMarshaler.Add (_windowShell, this.WmDeleteCB);
// This toesn't work.
// Xmlib.XmAddWMProtocolCallback(_windowShell, wmDeleteMessage, callbackUnmanagedPointer, IntPtr.Zero);
// Use this instead.
IntPtr wmProperty = Xmlib.XmInternAtom (display, "WM_PROTOCOLS", false);
Xmlib.XmAddProtocolCallback(_windowShell, wmProperty, wmDeleteMessage,
                            callbackUnmanagedPointer, IntPtr.Zero);  

The convenience class CallBackMarshaler method Add() provides an unmanaged callback pointer - already known from the second article of this series.

MessageWindow (of XmMainWindow) refinements

A Mofti XmMainWindow consists of four areas (from top to bottom) MenuBar, CommandWindow, WorkRegion an MessageWindow. The MessageWindow has assigned a vertical oriented XmRowColumn widget to manage its children.

The first child is a XmLabel widget displaying the multiple font text "This string has 3 fonts." , that only demonstrates the multi font capabilities of Motif.

The second child ist a XmLProgress widget, that shows how to incorporate a third party widget.

The third child is a status bar, that consists of two XmLabel widgets, one to display an icon and one to display the status message. Both label widgets are organized by a horizontal oriented XmRowColumn widget. To use the available space of the status bar effectively and to enable growth/shrinkage of the status message text length, the dynamic layout for the XmRowColumn widget has to be switched on explicitly - using the XmNresizeHeight and XmNresizeWidth resources.

C#
Arg[] statrowArgs = { new Arg(XmNames.XmNorientation, (XtArgVal)XmOrientation.XmHORIZONTAL),
                      new Arg(XmNames.XmNpacking,     (XtArgVal)XmPacking.XmPACK_NONE),
                      // Don't refuse size requests from children.
                      new Arg(XmNames.XmNresizeHeight,(XtArgVal)0),
                      new Arg(XmNames.XmNresizeWidth, (XtArgVal)0) };
IntPtr _statusRow = Xmlib.XmCreateRowColumn (_msgBar, StatusRowWidgetName, statrowArgs,
                                             (XCardinal)4);
Xtlib.XtManageChild(_statusRow);

Now the XmLabel widgets  for icon  and status message can force resize using the XmNrecomputeSize resource.

C#
Arg[] statusIconArgs = { new Arg(XmNames.XmNlabelPixmap,   _statusIconPixMap),
                         new Arg(XmNames.XmNlabelType,     (XtArgVal)XmLabelType.XmPIXMAP),
                         new Arg(XmNames.XmNrecomputeSize, (XtArgVal)1)};
Xtlib.XtSetValues (_statusIcon, statusIconArgs, (XCardinal)3);
 
...
 
Arg[] statusLabelArgs = { new Arg(XmNames.XmNlabelString,   xmStatusLabel),
                          new Arg(XmNames.XmNrecomputeSize, (XtArgVal)1)};
Xtlib.XtSetValues (_statusLabel, statusLabelArgs, (XCardinal)2);

To update the status message text, the globally accessible static method Log() of the static class Logger can be used.

Create a menu and handle the callbacks

There are two general ways to create a menu bar and it's (cascade) pulldown menus.

The first one provides full control over all menu creation steps like menu bar and cascade button creation, menu shell and menu button or menu separator creation as well as callback registration. It uses XmCreateMenuBar() to create an (empty) menu bar, XmCreatePullbownMenu() to create a pulldown menu shell, XmCreateCascadeButtonGadget() to create a menu bar item or pulldown menu item and to connect it with a pulldown menu shell, XtCreateManagedWidget() to create menu items like a PushButtonGadget or a SeparatorGadget and XtAddCallback() to register callbacks. This technique is adequate to write menu construction helper functions.

The second one provides high convenience and uses XmCreateSimpleMenuBar() to create a menu bar including it's cascade buttons or push buttons and XmCreateSimplePulldownMenu()  to create a pulldown menu shell including it's cascade buttons, push buttons or separators. This technique is adequate to write very compact code.

The next code sample illustrates the creation of a menu bar.  Since XmCreateSimpleMenuBar() creats all XmNbuttonCount four XmNbuttons and assigns the XmNsimpleCallback to them, all menu buttons share the same callback function and the callback function must already be prepared before the menu bar widget creation. This is where fakeWidgetID comes into play. After menu bar widget creation the callback function will be reassigned from fakeWidgetID to _menuBar

It is possible to assign XmNbuttonMnemonics to invoke the menu's cascade buttons via keyboard using [Alt]+[<mnemonic>] key combination. The mnemonics are case sensitive. The menuBtnNameList and menuMnemonicList parameter must have the same length, that ist identical to XmNbuttonCount. The next code sample illustrates the hight concenience menu bar creation.

C#
// The menu bar.
IntPtr   fakeWidgetID       = (IntPtr)9999;
IntPtr[] menuBtnNameList    = { Xmlib.XmStringCreateDefaultCharsetLtoR ("File"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Message box"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Dialog"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Close application") };
TUint[]  menuMnemonicList   = { (TUint)'F', (TUint)'M', (TUint)'D', (TUint)'C' };
IntPtr   menuCbUnmanagedPtr = CallBackMarshaler.Add (fakeWidgetID, this.MenuBarCB);
Arg[]    menuBarArgs        = { new Arg(XmNames.XmNscrolledWindowChildType,
                                                (XtArgVal)XmScrolledWindowChildType.XmMENU_BAR),
                                new Arg(XmNames.XmNbuttonCount,             (XtArgVal)4),
                                new Arg(XmNames.XmNbuttons,                 menuBtnNameList),
                                new Arg(XmNames.XmNbuttonMnemonics,         menuMnemonicList),
                                new Arg(XmNames.XmNsimpleCallback,          menuCbUnmanagedPtr),
                                // Don't extend the last column to the right edge of RowColumn.
                                new Arg(XmNames.XmNadjustLast,              (XtArgVal)0) };
_menuBar = Xmlib.XmCreateSimpleMenuBar (_mainWindow, MenuBarWidgetName, menuBarArgs, (XCardinal)6);
 
// ### Callback to class context assignment: Part 1
// A "new Arg(XmNames.XmNsimpleCallback, ...)" must be set at a moment, neither menu bar widget ID nor
// menu item IDs are known. Therefore the callback function can't be assigned to a widget ID in advance.
// Since callbacks loose their class context at execution time, they must be re-associated with a class
// context at execution time to achieve object oriented behaviour. To realize reassignment of a callback
// function to it's class context, the CallBackMarshaler must associate the propper widget ID.
CallBackMarshaler.Reassign (fakeWidgetID, _menuBar);
 
// Clean up dynamic unmanaged resources.
for (int countMenuBtnNames = 0; countMenuBtnNames < menuBtnNameList.Length; countMenuBtnNames)
{
    Xmlib.XmStringFree (menuBtnNameList[countMenuBtnNames]);
}
Xtlib.XtManageChild(_menuBar);

Although all menu bar cascade buttons share the same callback, the menu entries can be distinguished by clientData callback parameter, that contains the menu entry index. The next code sample illustrates such a callback, shared by all menu bar items.

C#
/// <summary> The menu bar callback procedure. </summary>
/// <param name="widget"> The widget, that initiated the callback procedure.
/// <see cref="System.IntPtr"/> </param>
/// <param name="clientData"> Additional callback data from the client.
/// <see cref="System.IntPtr"/> </param>
/// <param name="callData"> Additional data defined for the call. <see cref="System.IntPtr"/> </param>
public void MenuBarCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
    int menuItemIndex = (int) clientData;
    switch (menuItemIndex)
    {
        // File
        case 0:
            break;
        // Message box
        case 1:
            XmExtendedMessageDialog dlg = new XmExtendedMessageDialog (_windowShell,
                ExtendedMessageDialogName, "Enter string", "Enter data string:");
            dlg.Start ();
            break;
        // Dialog
        case 2:
            break;
        // Close application
        case 3:
            WmDeleteCB (widget, clientData, callData);
            break;
    }
}
An alternative to the menu bar's automatical callback function assignment via XmNsimpleCallback is to assign the callback function manually for each menu bar's child separately after menu bar widget creation. Although this would prevent juggling with fakeWidgetID, the code will be less compact and elegent.
C#
for (TInt countChildren = Xtlib.XtCountChildren (_menuBar) - 1;  countChildren >= 0; countChildren--)
{
    IntPtr child = Xtlib.XtGetChild (_menuBar, countChildren);
    // Either we assign a different callback to each child.
    // Or we do it compatible to Motif and pass the child index.
    switch ((int)countChildren)
    {
        // File
        // Dialog
        // Message box
        // Close application
        default:
            Xtlib.XtAddCallback (child, XmNames.XmNactivateCallback,
            CallBackMarshaler.Add (child, this.MenuBarCB), (IntPtr)((int)countChildren));
            break;
    }
}

The next code sample illustrates the creation of a pulldown menu.  Since XmCreateSimplePulldownMenu() creats all XmNbuttonCount five XmNbuttons and assigns the XmNsimpleCallback to them, all push buttons share the same callback function and the callback function must already be prepared before the pulldown menu widget creation. This is again where fakeWidgetID comes into play. After pulldown menu widget creation the callback function will be reassigned from fakeWidgetID to _fileMenu

The XmNpostFromButton defines the menu bar's cascade button index, this pulldown menu has to be associated to. The XmNbuttonAccelerators registers the accelrotor shortcuts to the pulldown menu push buttons and the XmNbuttonAcceleratorText defines the display of accellerator shortcuts to the pulldown menu push buttons. 

As for a menu bar it is also possible for a pulldown menu to assign XmNbuttonMnemonics to invoke the menu's cascade buttons via keyboard using [<mnemonic>] key (now without the combination of [Alt] key). The mnemonics are case sensitive. The fileBtnNameList, fileBtnAccelList, fileMnemonicList and fileBtnTypeList parameter must have the same length, that ist identical to XmNbuttonCount.

C#
// The file pull down menu.
IntPtr[] fileBtnNameList    = { Xmlib.XmStringCreateDefaultCharsetLtoR ("Open..."),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Save"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Save as ..."),
                                Xmlib.XmStringCreateDefaultCharsetLtoR (""),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Exit") };
TUint[]  fileMnemonicList   = { (TUint)'O', (TUint)'S', (TUint)'a', (TUint)' ', (TUint)'x' };
IntPtr[] fileBtnAccelList   = { Marshal.StringToHGlobalAuto ("Ctrl<Key>o\0"),
                                Marshal.StringToHGlobalAuto ("Ctrl<Key>s\0"),
                                Marshal.StringToHGlobalAuto ("Ctrl<Key>a\0"),
                                Marshal.StringToHGlobalAuto ("\0"),
                                Marshal.StringToHGlobalAuto ("Ctrl<Key>x\0") };
IntPtr[] fileBtnAccelTexts  = { Xmlib.XmStringCreateDefaultCharsetLtoR ("Ctrl-O"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Ctrl-S"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR ("Ctrl-A"),
                                Xmlib.XmStringCreateDefaultCharsetLtoR (""),
TChar[]  fileBtnTypeList    = { (TChar)XmButtonType.XmPUSHBUTTON,
                                (TChar)XmButtonType.XmPUSHBUTTON,
                                (TChar)XmButtonType.XmPUSHBUTTON,
                                (TChar)XmButtonType.XmSEPARATOR,
                                (TChar)XmButtonType.XmPUSHBUTTON};
IntPtr fileCbUnmanagedPtr   = CallBackMarshaler.Add (fakeWidgetID, this.FileMenuCB);
                               // File menu button index (to start this pulldown for) is 0.
Arg[]  fileMenuArgs         = { new Arg(XmNames.XmNpostFromButton,           (XtArgVal)0),
                                new Arg(XmNames.XmNbuttonCount,              (XtArgVal)5) ,
                                new Arg(XmNames.XmNbuttons,                  fileBtnNameList),
                                new Arg(XmNames.XmNbuttonAccelerators,       fileBtnAccelList),
                                new Arg(XmNames.XmNbuttonAcceleratorText,    fileBtnAccelTexts),
                                new Arg(XmNames.XmNbuttonMnemonics,          fileMnemonicList),
                                new Arg(XmNames.XmNbuttonType,               fileBtnTypeList),
                                new Arg(XmNames.XmNsimpleCallback,           fileCbUnmanagedPtr)};
_fileMenu = Xmlib.XmCreateSimplePulldownMenu (_menuBar, FileMenuWidgetName, fileMenuArgs, (XCardinal)7);
// ### See comments "Callback to class context assignment" part 1 & 2 on simple menu bar creation.
CallBackMarshaler.Reassign (fakeWidgetID, _fileMenu);
 
// Clean up dynamic unmanaged resources.
for (int countButtons = 0; countButtons < 5; countButtons++)
{
    Xmlib.XmStringFree  (fileBtnNameList[countButtons]);
    Marshal.FreeHGlobal (fileBtnAccelList[countButtons]);
    Xmlib.XmStringFree  (fileBtnAccelTexts[countButtons]);
}
Again all pulldown menu push buttons share the same callback and the menu entries can be distinguished by clientData callback parameter, that contains the menu entry index.
C#
/// <summary> The file menu callback procedure. </summary>
/// <param name="widget"> The widget, that initiated the callback procedure.
/// <see cref="System.IntPtr"/> </param>
/// <param name="clientData"> Additional callback data from the client.
/// <see cref="System.IntPtr"/> </param>
/// <param name="callData"> Additional data defined for the call.
/// <see cref="System.IntPtr"/> </param>
private void FileMenuCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
    int menuItemIndex = (int) clientData;
    switch (menuItemIndex)
    {
      // Open
      case 0:
        XmFileSelectionDialog dlg = new XmFileSelectionDialog (_windowShell, FileSelectionWidgetName,
                                                               "Open file ...");
        dlg.Start ();
        break;
      // Save
      case 1:
        IntPtr saveMessage = Xmlib.XmStringCreateDefaultCharset ("'Safe' menu item not implemented yet.");
        Arg[] saveDlgArgs  = { new Arg (XmNames.XmNtitle,            "Safe menu item"),
                                new Arg (XmNames.XmNmessageString,    saveMessage) };
        IntPtr saveDlg = Xmlib.XmCreateInformationDialog (_windowShell, ExtendedMessageDialogName,
                                                          saveDlgArgs, (XCardinal)2);
        Xmlib.XmStringFree (saveMessage);
        Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveDlg, "Cancel"), (TBoolean)0);
        Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveDlg, "Help"), (TBoolean)0);
        Xtlib.XtManageChild(saveDlg);
        Xtlib.XtPopup (Xtlib.XtParent (saveDlg), XtGrabKind.XtGrabNone);
        break;
      // Save As
      case 2:
        IntPtr saveasMessage = Xmlib.XmStringCreateDefaultCharset ("'Safe As' menu item not implemented" +
                                                                   " yet.");
        Arg[] saveasDlgArgs  = { new Arg (XmNames.XmNtitle,            "Safe As menu item"),
                                  new Arg (XmNames.XmNmessageString,    saveasMessage) };
        IntPtr saveasDlg = Xmlib.XmCreateInformationDialog (_windowShell, ExtendedMessageDialogName,
                                                            saveasDlgArgs, (XCardinal)2);
        Xmlib.XmStringFree (saveasMessage);
        Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveasDlg, "Cancel"), (TBoolean)0);
        Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveasDlg, "Help"), (TBoolean)0);
        Xtlib.XtManageChild(saveasDlg);
        Xtlib.XtPopup (Xtlib.XtParent (saveasDlg), XtGrabKind.XtGrabNone);
        break;
      // Exit
      case 3: // (Separators don't count!)
        WmDeleteCB (widget, clientData, callData);
        break;
    }
}  

Modify standard Motif dialogs

Two sample modifications to a Motif standard MessageDialog should be shown next, both exemplary implemented in the XmExtendedMessageDialog class.

  1. The display of a custom color symbol with transparency instead of the default symbol.
  2. The integration of an additional TextField.

The first thing to do, if unmanaged resources are involved in the madifications, is to provide a way to displose those resources. The best point in time doing this is the dialog's destruction. There are two ways to hook up the dialog's destruction, that both end up in the same behaviour and usage of the same signals.

Either XmAddProtocolCallback() can be used to register a signal handler for WM_DELETE_WINDOW at WM_PROTOCOLS. This takes about 5 lines of code and is demonstrated in the XmWindows.Run() method of the sample application, already discussed in chapter Manage the WM_DELETE_WINDOW signal.

Or the dialog's XmNdeleteResponse attribute is changed from XmUNMAP (which is the default behaviour) to XmDESTROY and a XmNdestroyCallback is registered. Because the dialog's attributes are set anyway and the callback method is already prepared, only two additional lines of code are required.
C#
Arg[] messageDlgArgs = {
   ...
   // Define action on delete on WM_DESTROY.
   new Arg(XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDESTROY) }; 
 
...
 
// Send notification on WM_DESTROY do dialog's callback.
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNdestroyCallback,	callbackUnmanagedPointer, (IntPtr)99);

It is always a good approach to use one callback method for all dialog messages and to distinguish the action to take via the clientData or callData callback parameter.  The dialog's callback method can be re-used and also control the displosal of unmanaged resources. 

A custom color symbol with transparency

There are four steps necessary to provide a standard Motif dialog with a custom color symbol including transparency. 

  1. Determination of the background color. 
  2. Creation of the transparency mask pixmap.
  3. Creation of the symbol pixmap with background color pixel at transparent image pixel.
  4. Assignment of the symbol pixmap to the dialog.
C#
// Change icon.
// - Determine background color.
TPixel backgroundColorPixel = Xtlib.XtGetValueOfPixel (_dialogWidget, XmNames.XmNbackground);
// - Prepare and set symbol.
_display = Xtlib.XtDisplay (parentShell);
IntPtr window  = Xtlib.XtWindow  (parentShell);
TInt   screen  = Xtlib.XDefaultScreen (_display);
using (X11Graphic dialogIcon = new X11Graphic (_display, screen, IconPath))
{
   _maskPixmap = dialogIcon.CreateIndependentMaskPixmap (_display, window);
   _iconPixMap = dialogIcon.CreateIndependentPixmap (_display, window, backgroundColorPixel, _maskPixmap);
   if (_iconPixMap != IntPtr.Zero)
   {
      Arg[] symbolDlgArgs = { new Arg(XmNames.XmNsymbolPixmap, _iconPixMap) };
      Xtlib.XtSetValues (_dialogWidget, symbolDlgArgs, (XCardinal)1);
   }
} 

To determine the background color pixel, the XtGetValueOfPixel() convenience method can be used. It hides the complicated color pixel value determination, that requires marhaling of a reference to a TPixel variable and the fact, that TPixel must be recognized with ReadInt32() for 32 bit or ReadInt64() for 64 bit OS. 

The most challenging step is the creation of the symbol pixmap with background color pixel at transparent image pixel. To achive this, the X11Graphic class, already known from the first article Programming Xlib with Mono Develop - Part 1: Low-level (proof of concept), has been extended by the new method CreateIndependentPixmap(). This method requires the pixmap handle maskPixmap (as parameter) and returns a pixmap handle. A pixmap handle referes to a server side pixmap resource.

C#
/// <summary> Provide a bitmap, containing the transparent graphic, that can be used
/// independent from this class. </summary>
/// <param name="display"> The display pointer, that specifies the connection to the X
/// server. <see cref="System.IntPtr"/> </param>
/// <param name="window"> The target window to create the pixmap for. <see cref="IntPtr"/>
/// </param>
/// <param name="backgroundColorPixel"> The background color behind any transparent
/// pixel. <see cref="TPixel"/> </param>
/// <param name="maskPixmap"> The mask pixmap to distinguish transparent from intransparent
/// pixel. <see cref="IntPtr"/> </param>
/// <returns> The pixmap on success, or IntPtr.Zero otherwise. <see cref="IntPtr"/> </returns>
public IntPtr CreateIndependentPixmap (IntPtr display, IntPtr window, TPixel backgroundColorPixel,
                                       IntPtr maskPixmap)
{
    if (_graphicXImage == IntPtr.Zero)
        return IntPtr.Zero;
    
    IntPtr pixmap = X11lib.XCreatePixmap (display, window, (TUint)_width, (TUint)_height,
                                          (TUint)_graphicDepth);
    
    // Fill pixmap with background color.
    IntPtr bgGc    = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
    X11lib.XSetForeground (display, bgGc, backgroundColorPixel);
    X11lib.XFillRectangle (display, pixmap, bgGc, (TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
    X11lib.XFreeGC (display, bgGc);
    bgGc = IntPtr.Zero;
    
    // Overlay the image.
    IntPtr pixmapGc = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
    if (pixmapGc != IntPtr.Zero)
    {
        if (maskPixmap != IntPtr.Zero)
        {
            // Prepare the clipping graphics context.
            IntPtr graphicGc = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
            if (graphicGc != IntPtr.Zero)
            {
                X11lib.XSetClipMask (display, graphicGc, maskPixmap);
                X11lib.XSetClipOrigin (display, graphicGc, (TInt)0, (TInt)0);
                
                // Draw graphic using the clipping graphics context.
                X11lib.XPutImage (display, pixmap, graphicGc, _graphicXImage, (TInt)0, (TInt)0,
                                  (TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
                
                // Restore previous behaviour and clean up. 
                X11lib.XSetClipMask (display, graphicGc, IntPtr.Zero);
                X11lib.XSetClipOrigin (display, graphicGc, (TInt)0, (TInt)0);
                // Console.WriteLine (CLASS_NAME + "::Draw () Delete clipping image GC.");
                X11lib.XFreeGC (display, graphicGc);
                graphicGc = IntPtr.Zero;
            }
            else
            {
                Console.WriteLine (CLASS_NAME + "::Draw () ERROR: Can not create graphics " +
                                   "context for transparency application.");
            }
        }
        else
        {
            X11lib.XPutImage (display, pixmap, pixmapGc, _graphicXImage, (TInt)0, (TInt)0,
                              (TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
            // Console.WriteLine (CLASS_NAME + "::CreateIndependentGraphicPixmap () Delete " +
            //                    "graphic image GC.");
            X11lib.XFreeGC   (display, pixmapGc);
            pixmapGc = IntPtr.Zero;
        }
    }    
    else
    {
        Console.WriteLine (CLASS_NAME + "::CreateIndependentGraphicPixmap () ERROR: Can not create " +
                           "graphics context for graphic pixmap.");
    }
    return pixmap;
}

At the end of the dialog's lifetime the unmanaged resources have to be disposed. The XmExtendedMessageDialog class method Dispose() implements the disposal of both unmanaged image resources, the mask pixmap and the transparent image pixmap.

C#
/// <summary> IDisposable implementation. </summary>
public override void Dispose ()
{
    Console.WriteLine (CLASS_NAME + "::Dispose ()");
    
    this.DisposeByParent ();
}

/// <summary> Dispose by parent. </summary>
public override void DisposeByParent ()
{    
    // Dispose new ressources.
    UnregisterCallbacks ();
    
    if (_display != IntPtr.Zero)
    {    if (_iconPixMap != IntPtr.Zero)
        {
            // Dispose symbol pixmap.
            X11lib.XFreePixmap  (_display, _iconPixMap);
            _iconPixMap = IntPtr.Zero;
        }
        if (_maskPixmap != IntPtr.Zero)
        {
            // Dispose mask pixmap.
            X11lib.XFreePixmap  (_display, _maskPixmap);
            _maskPixmap = IntPtr.Zero;
        }
    }

    // Dispose base ressources.
    base.DisposeByParent ();
}

The Dispose() method will be called from the one and only callback method for all dialog messages DialogCB(), that is shown next paragraph.

An additional TextField

Adding a TextField to a Motif standard dialog is a fairly simple task.

C#
// Inject a text widget.
IntPtr textField = Xmlib.XmCreateTextField (_dialogWidget, TextFieldWidgetName, Arg.Zero, 0);
Xtlib.XtManageChild (textField);

TheTextField's text, entered by the user, shuld be readout after dialog confirmation. The best place to do this is the DialogCB() callback. It is called for any button event (help button is unmanaged) as well as for the destroy event (all callback registrations are done in the constructor).  The next code sample shows all registrations for DialogCB() callback. 

C#
/// <summary> Register all callbacks (separetely from construction.) </summary>
private void RegisterCallbacks ()
{
    IntPtr callbackUnmanagedPointer = CallBackMarshaler.Add (_dialogWidget, this.DialogCB);
    
    Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNokCallback,
	                     callbackUnmanagedPointer, (IntPtr)0);
    Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNcancelCallback,
	                     callbackUnmanagedPointer, (IntPtr)1);

    // Send notification on WM_DESTROY do dialog's callback.
    // Requires XmNames.XmNdeleteResponse set to DeleteResponse.XmDESTROY
    // to be called from windows decorations as well.
    Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNdestroyCallback,
	                     callbackUnmanagedPointer, (IntPtr)99);
}
Since the text will be determined by the XtGetValues() method and be returned as a reference to a char* variable - as well as the background color pixel above has been returned as a reference to a TPixel variable - here a marshaling of the variable reference is necessary too. Because the TextField's text isn't a Motif compound string, ReadIntPtr() can be used to return the text.
C#
/// <summary> The question dialog command button callback procedure. </summary>
/// <param name="widget"> The widget, that initiated the callback procedure.
/// <see cref="System.IntPtr"/> </param>
/// <param name="clientData"> Additional callback data from the client.
/// <see cref="System.IntPtr"/> </param>
/// <param name="callData"> Additional data defined for the call. <see cref="System.IntPtr"/>
/// </param>
private void DialogCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
    // Distinguish the action to take by callData.
    XmAnyCallbackStruct cbStruct = (callData != IntPtr.Zero ? (XmAnyCallbackStruct)
                                   Marshal.PtrToStructure (callData, typeof (XmAnyCallbackStruct)) :
                                   new XmAnyCallbackStruct ());
			
    // Distinguish the action to take by clientData.
    // int buttonIndex = (int) clientData;
 
    Xtlib.XtUnmanageChild (_dialogWidget);
    DisposeResources ();
 
    if (cbStruct.reason == (TInt)XmCallbackReason.XmCR_OK) // buttonIndex == 0)
    {
        IntPtr textField = Xtlib.XtNameToWidget (_dialogWidget, TextFieldWidgetName);
        
        IntPtr pointer       = Xtlib.XtGetValueOfPointer (textField, XmNames.XmNvalue);
        string prompt = Marshal.PtrToStringAnsi (pointer);
        
        Logger.Log (CLASS_NAME + "::DialogCB() OK ==> " + prompt);
        Console.WriteLine (CLASS_NAME + "::DialogCB() OK ==> " + prompt);
        Xtlib.XtDestroyWidget (_dialogWidget);
    }
    else if (cbStruct.reason == (TInt)XmCallbackReason.XmCR_CANCEL) // buttonIndex == 1)
    {
        Console.WriteLine (CLASS_NAME + "::DialogCB() Cancel");
        Xtlib.XtDestroyWidget (_dialogWidget);
    }
    
    if ((int) clientData == 99)
    {
        // WM_DESTROY.
        Dispose ();
    }
}

The DialogCB() is the only callback method of the whole dialog. It uses the callData parameter, converted to XmAnyCallbackStruct via PtrToStructure(), to distinguish the action to take. This is the preferred way to handle dialog messages. 

The alternative way to distinguish the action to take, using the clientData parameter, is shown as well but deactivated. 

After button event processing DialogCB() calls XtDestroyWidget() that forces the dialog to destroy, which, in turn, calls DialogCB() to dispose all unmanaged resources.

Design a new custom dialog from scratch 

Custom dialogs based on XmRowColumn

The easiest way to build up a new - very simple structured - custom dialog from scratch is using XmRowColumn widget as root manager widget and also as action area widget to layout the children of a XmDialogShell widget. There is nothing wrong with that but it should be mentioned, that XmRowColumn widget's ease of use might break compatibility to general Motif dialog handling - especially to the keyborad shortcuts.

All built-in Motif dialogs use a XmBulletinBoard, that has several default translations (see Transltns.c file for details on _XmBulletinB_defaultTranslations) - including translations for activate key (apply the dialog), cancel key (escape the dialog) and help key (call help) - as well as the XmNdefaultButton and XmNcancelButton resources to handle <Key>osfActivate, <Key>osfCancel and <Key>osfHelp keyborad shortcuts. The XmRowColumn widget has neither default translations nor resources to handle keyborad shortcuts.

To achive the same user experience for a new custom dialog based on XmRowColumn as of XmBulletinBoard, some things are important: 

  1. Extend the manager widget's translations. If multiple manager widgets are used, it might be necessary to extend more than one manager widget's translations. 
  2. Use gadgets instead of widgets, at least for the (OK, Cancel, ...) buttons, otherwise the manager widget's translations are unnoticated.
  3. Avoid XmText and XmTextBox widgets or provide them with callbacks or translations.

The XmHandMadeDialogOnRowColumn class implements a new custom dialog based on XmRowColumn

1. It is always a good approach to use "osf" keysyms to extend the manager widget's translations. Due to the implementation specifics of the ActionMarshaler class, only one action procedure can be registered per event type. That's why all translations call DlgKeyPressAction(). To distinguish the different key press events for activate, cancel and help, an action procedure parameter is passed.

C#
/// <summary> The translations to realize dialog key press response as expected. </summary>
public const string HandMadeDlgTranslations = "#override\n:<Key>osfActivate: DlgKeyPressAction(0)" +
                                                       "\n:<Key>osfCancel:   DlgKeyPressAction(1)" +
                                                       "\n:<Key>osfHelp:     DlgKeyPressAction(2)";

The action registration must be done prior the translation registration. Once the action registration  is done, the actions can be used in multiple translation tables.

C#
// Prepare generic action procedure to handle Enter, Escape and Help key press events.
// Don't forget to assign a method to a widget to enable the action procedure.
XtActionsRec[] actRec	= { new XtActionsRec (X11.X11Utils.StringToSByteArray
                                                 ("DlgKeyPressAction\0"), ActionMarshaler.ActionProc) };
Xmlib.XtAppAddActions (XmWindow.Instance.AppContext, actRec, (XCardinal)1);
The parsed translations shouldn't be applied by XtSetValues() (this would completely replace already existing widget translations) but by XtOverrideTranslations() (to merge the new translations into the existing widget translations).
C#
// Enable the action procedure for the actionarea RowColumn widget.
// Use XtOverrideTranslations() instead of XtSetValues() to prevent a complete replacement.
ActionMarshaler.Add (_actionareaWidget, XEventName.KeyPress, this.DlgKeyPressAction);
IntPtr  mgrTransl = Xtlib.XtParseTranslationTable (HandMadeDlgTranslations);
Xtlib.XtOverrideTranslations (_actionareaWidget, mgrTransl);

2. Although button widgets (not gadgets!) generally support accelerators and accelerators must not be installed on application level, it is a hard job to enable this feature. See Motif FAQ 132 "Why can't I use accelerators on buttons not in a menu?" for details. At least if the accelerators to support must be installed in a manner that they can be called from all fucusable widgets/gadgets of the dialog, this approach runs into trouble.

3. Text widgets in a dialog make translation handling complicated. Dependent on XmNeditMode resource a text widget can be XmSINGLE_LINE_EDIT or XmMULTI_LINE_EDIT. For multi line text widgets the activate key is  required to create a new line. Only for single line text widgets the activate key can be used to close the dialog.

Either activate key (for single line text widgets) and help key can be supported using the predefined resources XmNactivateCallback and XmNhelpCallback, but there is no adequate resource to support the cancel key. 

Or, because suitable actions for the manager widget(s) are already registered on application level, it is possible to re-use the actions on text widgets. Typically the text widget translations are a subset of the manager widget translations (the activate key migth be omitted for multi line text widgets).

C#
/// <summary> The translations to realize text widget key press response as expected. </summary>
public const string TextWidgetTranslations = "#override\n:<Key>osfActivate: DlgKeyPressAction(0)" +
                                                      "\n:<Key>osfCancel:   DlgKeyPressAction(1)" +
                                                      "\n:<Key>osfHelp:     DlgKeyPressAction(2)";
 
...
 
// Enable the action procedure for the text TextField widget.
// Use XtOverrideTranslations() instead of XtSetValues() to prevent a complete replacement.
ActionMarshaler.Add (textField, XEventName.KeyPress, this.DlgKeyPressAction);
IntPtr  textTransl	= Xtlib.XtParseTranslationTable (TextWidgetTranslations);
Xtlib.XtOverrideTranslations (textField, textTransl);
Estimation  

Custom dialogs based on XmRowColumn are easy to create and can be designed fully compatible to general Motif dialog handling. The biggest handicap is the almost static layout - a dialog resize can completely confuse the layout. 

To prevent a dialog resize by switching off the window decoration / window functions is doubtful, because not all window managers support all MWM flags of VendorShell's XmNmwmFunctions and XmNmwmDecorations resources.

C#
Arg[] shellArgs = { new Arg (XmNames.XmNtitle,          caption),
                    new Arg (XmNames.XmNmwmFunctions,   (XtArgVal)MWM_Functions.MWM_FUNC_ALL),
                    new Arg (XmNames.XmNmwmDecorations, (XtArgVal)MWM_Decorations.MWM_DECOR_ALL),
                    new Arg (XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDESTROY) };
_dialogShell	= Xmlib.XmCreateDialogShell (parentShell, ShellResourceName, shellArgs, (XCardinal)4);

Alernatively the size of the root manager widget can be fixed.  

C#
XDimension height = Xtlib.XtGetValueOfDimension (_dialogWidget, XmNames.XmNheight);
XDimension width  = Xtlib.XtGetValueOfDimension (_dialogWidget, XmNames.XmNwidth);
 
Arg[] cancelArgs  = { new Arg (XmNames.XmNminWidth,  (XtArgVal)width),
                      new Arg (XmNames.XmNmaxWidth,  (XtArgVal)width),
                      new Arg (XmNames.XmNminHeight, (XtArgVal)height),
                      new Arg (XmNames.XmNmaxHeight, (XtArgVal)height) };
Xtlib.XtSetValues (_dialogShell, cancelArgs, (XCardinal)4);

Custom dialogs based on XmPanedWindow

Almost all literature shows how to build up a new custom dialog from scratch using XmPanedWindow widget as root manager widget and XmForm as action area widget to layout the children of a XmDialogShell widget. Since XmForm widget is inherited from XmBulletinBoard widget, it is much easier to achive compatibility to general Motif dialog handling than with XmRowColumn widget. 

The XmHandMadeDialogOnPanedWindow class implements a new custom dialog based on XmPanedWindow as root manager widget and XmForm as action area widget.

Using the XmForm widget is very simple. This manager widget spans a grid if equally sized cells to position the widgets.  The "OK" and "Cancel" buttons of the action area shall align left and right to the dialog borders. 



OK (Position 1)
 

(Position 2)
 

(Position 3)
 

Cancel (Position 4)
 

Widgets can span multiple grid positions as well. Complex layouts can (almost only) be achieved by an extensive use of cell spans.


(Position 1)       OK       (Position 2)
 

(Position 3)
 

(Position 4)     Cancel     (Position 5)
 

The number of grid cells can be set via XmNfractionBase resource.

C#
// ==== Action area, that holds all action buttons.
Arg[] actaArgs = { new Arg (XmNames.XmNfractionBase, (XtArgVal)(ActionAreaButtons + ActionAreaBlanks)),
                   new Arg (XmNames.XmNleftOffset,   (XtArgVal)10),
                   new Arg (XmNames.XmNrightOffset,  (XtArgVal)10) };
_actionareaWidget = Xmlib.XmCreateForm (_dialogWidget, ActionAreaResourceName, actaArgs, (XCardinal)3);
The constraints of the form widget's children support attachements to the form widget boundary, neighboring widgets and grid positions.
C#
// ==== OK action button.
IntPtr okLabel = Xmlib.XmStringCreateDefaultCharset ("OK");
Arg[]  okArgs = { new Arg (XmNames.XmNleftAttachment,	(XtArgVal)XmAttachement.XmATTACH_POSITION),
new Arg (XmNames.XmNrightAttachment,                (XtArgVal)XmAttachement.XmATTACH_POSITION),
  new Arg (XmNames.XmNtopAttachment,                (XtArgVal)XmAttachement.XmATTACH_FORM),
  new Arg (XmNames.XmNbottomAttachment,             (XtArgVal)XmAttachement.XmATTACH_FORM),
  new Arg (XmNames.XmNleftPosition,                 (XtArgVal)0),
  new Arg (XmNames.XmNrightPosition,                (XtArgVal)1),
  new Arg (XmNames.XmNlabelString,                  okLabel),
  new Arg (XmNames.XmNshowAsDefault,                (XtArgVal)1),
  new Arg (XmNames.XmNdefaultButtonShadowThickness, (XtArgVal)1) };
_okbuttonWidget = Xmlib.XmCreatePushButtonGadget (_actionareaWidget, OkResourceName, okArgs, (XCardinal)9);
Xmlib.XmStringFree (okLabel);
Xtlib.XtManageChild (_okbuttonWidget);

If form widget's XmNfractionBase resource and form child widget's XmN*Position resources don't fit together, typically the last child widget balances the misfit. 

To achive the typical keyborad shortcut behaviour, XmNdefaultButton and XmNcancelButton resources of XmBulletinBoard widget and XmNhelpCallback resource of XmManager widget can be used. 

Estimation  

Custom dialogs based on  XmPanedWindow and XmForm are easy to create and can be designed almost compatible to general Motif dialog handling. Resizing is expectation-conformal. The only drawback is the "invisible" tab stop on XmPanedWindow's pane during kyboart traversal.

Custom dialogs based on XmBulletinBoard  

Because XmBulletinBoard widget has no built-in geometry management by defaul, it is a very hard job to create a custom dialog from scratch using XmBulletinBoard widget as root manager widget. Usually it's more efficient to use XmBulletinBoard's subclass XmForm.
The particular challenge using XmForm will be to define the best attachements for all widgets. The XmHandMadeDialogOnBulletinBoard class implements a simple XmBulletinBoard widget based dialog, that exactly behaves like a Motif MessageBox dialog (resizing, accelerators, ...). For more complex dialog layouts it might be unavoidable to use more manager widgets than only one root manager widget. In this case it can happen, that acellerators must be assigned multiple.

Points of Interest

Callbacks and disposal of unmanaged resources

The callback registration for menu bar and pulldown menu must use a trick (fakeWidgetID) to continue with the already introduced convenience class CallBackMarshaler.

All shells (main window, dialogs) inherit from XtShell and thus standardizes the disposal of unmanaged resources, callback registration and callback deregistration through Dispose(), DisposeByParent(), RegisterCallbacks() and UnregisterCallbacks() class methods. All shells have to change their delete response behaviour from XmUNMAP to XmDESTROY to ensure the correct WM_DESROY message processing.

General functionality 

This sample application also demonstrates the utilization of a Motif compliant third party widget. There are several commercial, open source or community projects, that provide Motif compliant third party widgets, e. g. The Xg widget set, XmHTML or XRT as well as collections of available widgets like Widget.FAQ, motifdeveloper, ftp widget project collection or ICS MotifZone. Although most of them are no longer maintained, they might help to complete a Motif application.

History

  • 18. September 2013, This is the third article about native calls from C# and Mono Develop to X11 API. It deals with the Xm/Motif widgets primarily. The first article was about the Xlib only. The second article was about the Xlib only. Further articles are planned, that dive deeper into Xlib, Xt/Athena or Xm/Motif.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Celonis SA
Germany Germany
I am currently the CEO of Symbioworld GmbH and as such responsible for personnel management, information security, data protection and certifications. Furthermore, as a senior programmer, I am responsible for the automatic layout engine, the simulation (Activity Based Costing), the automatic creation of Word/RTF reports and the data transformation in complex migration projects.

The main focus of my work as a programmer is the development of Microsoft Azure Services using C# and Visual Studio.

Privately, I am interested in C++ and Linux in addition to C#. I like the approach of open source software and like to support OSS with own contributions.

Comments and Discussions

 
-- There are no messages in this forum --