D2X-XL Worklog

Notes on features and problems from the development of D2X-XL, newest first.

A Message Box For Linux

Since D2X-XL now prints diagnostic messages before being able to open a program window or render text, I was in need of a function that would work like a Windows message box for Linux too. This sounded easier than it was - way easier. Actually there is no simple way to programmatically have the operating system display a message box for an application on Linux. There are several window manager libraries (Motif, Gtk, Qt4, wxWidgets) but all of these are centered around building an application with a main window and a message loop, which D2X-XL didn't have at the stage where I needed the message box (or actually anywhere later, at least not in the form required for these GUI libraries.)

In the end I decided to use Motif for the task as it is a very common GUI API and most Linux window managers can handle it. Creating a message box proved to be rather easy, but I wanted more than that: For D2X-XL diagnostic output I needed a bigger, scrollable text viewing area. An additional problem was posed by Motif's behavior to only display my message box when I started its event loop and terminated D2X-XL when the message box was closed by the user; something I definitely had to work around and allow D2X-XL to continue its operation when it was only displaying warnings and not critical errors. Finally, I didn't want the message box to have minimize, maximize and close window controls, but simply a 'close' button below the message text.

After days of poking around in the internet I finally found some code in the Motif programmer's manual that I only had to adapt a little to get what I want.

First of all the required include files and global data:

#include <Xm/Xm.h>
#include <Xm/MwmUtil.h>
#include <Xm/MainW.h>
#include <Xm/CascadeB.h>
#include <Xm/MessageB.h>
#include <Xm/RowColumn.h>
#include <Xm/Form.h>
#include <Xm/PushBG.h>
#include <Xm/LabelG.h>
#include <Xm/PanedW.h>
#include <Xm/Text.h>
#include <Xm/DialogS.h>
#include <Xm/Command.h>

static XtAppContext appShell;

MsgSize determines the number of lines contained in the message as well as the length of the longest line.

static int MsgSize (char* pszMsg, int& nCols)
{
if (!(pszMsg && *pszMsg))
   return 0;
int nRows = 1;
nCols = 0;
for (char* p = pszMsg; *p && (pszMsg = strchr (p, '\n')); nRows++, p = ++pszMsg) {
   if (nCols < pszMsg - p)
      nCols = pszMsg - p;
   }
return nRows;
}

DestroyShell is the callback function for the message dialog's cancel button. Since this is not a predefined Motif dialog, the 'widget' parameter is not the dialog itself. That is only done by Motif dialog callbacks. Here in the real world, the callback routine is called directly by the widget that was invoked. Thus, we must pass the dialog as the client data to get its handle. (We could get it using GetTopShell() but this way is quicker, since it's immediately available.)

void DestroyShell (Widget widget, XtPointer clientData, XtPointer callData)
{
Widget shell = (Widget) clientData;
XtDestroyWidget (shell);
XtAppSetExitFlag (appShell);
}

XmMessageDialog builds a dialog containing a scrollable, non editable text widget and a close button. It will also disable the dialog's close, minimize and maximize functions. The popup window consists of a dialog shell which is derived from a vendor shell and hence allows window decoration manipulation. The delete window protocol response is set to XmDESTROY to make sure that the window goes away appropriately. Otherwise, it's XmUNMAP which means it'd be lost forever, since we're not storing the widget globally or statically to this function.

Widget XmMessageDialog (const char* pszMsg, int nRows, int nCols, bool bError)
{
   Widget   msgBox, pane, msgText, form, widget;
   void       DestroyShell(Widget, XtPointer, XtPointer);
   Arg        args [10];
   int         n = 0;
   int         i;
   const char* szCaption [] = {"Warning", "Error"};

Widget topWid = XtVaAppInitialize (&appShell, "D2X-XL", NULL, 0, &argc, argv, NULL, NULL);
xtSetArg (args [0], XmNdeleteResponse, XmDESTROY);
msgBox = XmCreateDialogShell (topWid, const_cast<char*>(szCaption [bError]), args, 1);
XtVaSetValues (msgBox, XmNmwmDecorations,
                     MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE, NULL);
XtVaGetValues (msgBox, XmNmwmFunctions, &i, NULL);
i &= ~(MWM_FUNC_ALL | MWM_FUNC_MINIMIZE | MWM_FUNC_MAXIMIZE | MWM_FUNC_CLOSE);
XtVaSetValues (msgBox, XmNmwmFunctions, i, NULL);
// Create a PanedWindow to manage the stuff in this dialog. PanedWindow won't let us set these to 0!
XtSetArg (args [0], XmNsashWidth, 1);
// Make small so user doesn't try to resize
XtSetArg (args [1], XmNsashHeight, 1);
pane = XmCreatePanedWindow (msgBox, const_cast<char*>("pane"), args, 2);
// Create a RowColumn in the form for Label and Text widgets. This is the control area.
form = XmCreateForm (pane, const_cast<char*>("form1"), NULL, 0);
// prepare the text for display in the ScrolledText object we are about to create.
n = 0;
XtSetArg (args [n], XmNscrollVertical, True); n++;
XtSetArg (args [n], XmNscrollHorizontal, False); n++;
XtSetArg (args [n], XmNeditMode, XmMULTI_LINE_EDIT); n++;
XtSetArg (args [n], XmNeditable, False); n++;
XtSetArg (args [n], XmNcursorPositionVisible, False); n++;
XtSetArg (args [n], XmNwordWrap, True); n++;
XtSetArg (args [n], XmNvalue, pszMsg); n++;
XtSetArg (args [n], XmNrows, min (nRows, 30)); n++;
XtSetArg (args [n], XmNcolumns, min (nCols, 120)); n++;
msgText = XmCreateScrolledText (form, const_cast<char*>("help_text"), args, n);
// Attachment values must be set on the Text widget's parent, the ScrolledWindow.
// This is the object that is positioned.
XtVaSetValues (XtParent (msgText),
                      XmNleftAttachment, XmATTACH_FORM,
                      XmNtopAttachment, XmATTACH_FORM,
                      XmNrightAttachment, XmATTACH_FORM,
                      XmNbottomAttachment, XmATTACH_FORM,
                      NULL);
XtManageChild (msgText);
XtManageChild (form);
// Create another form to act as the action area for the dialog
XtSetArg (args [0], XmNfractionBase, 5);
form = XmCreateForm (pane, const_cast<char*>("form2"), args, 1);
// The OK button is under the pane's separator and is centered in the form. It spreads from position
// 1 to 2 along the bottom (the form is split into 5 separate grids via XmNfractionBase upon creation).
widget = XmCreatePushButtonGadget (form, const_cast<char*>("Close"), NULL, 0);
XtVaSetValues (widget,
                  &nbsp   XmNtopAttachment, XmATTACH_FORM,
                  &nbsp   XmNbottomAttachment, XmATTACH_FORM,
                  &nbsp   XmNleftAttachment, XmATTACH_POSITION,
                  &nbsp   XmNleftPosition, 2,
                  &nbsp   XmNrightAttachment, XmATTACH_POSITION,
                  &nbsp   XmNrightPosition, 3,
                  &nbsp   XmNshowAsDefault, True,
                  &nbsp   XmNdefaultButtonShadowThickness, 1,
                  &nbsp   NULL);
XtManageChild (widget);
XtAddCallback (widget, XmNactivateCallback, DestroyShell, (XtPointer) msgBox);
// Fix the action area pane to its current height -- never let it resize
XtManageChild (form);
XtVaGetValues (widget, XmNheight, &h, NULL);
XtVaSetValues (form, XmNpaneMaximum, h, XmNpaneMinimum, h, NULL);
// This also pops up the dialog, as it is the child of a DialogShell
XtManageChild (pane);
return topWid;
}

The purpose of the following callback is to tell the application event loop to quit without terminating the entire program. This callback is invoked when the message box's close button is clicked.

void XmCloseMsgBox (Widget w, XtPointer clientData, XtPointer callData)
{
XtAppSetExitFlag (appShell);
}

XmMessageBox decides whether the message to be displayed is small enough to warrant a simple message box or should rather be displayed in a bigger, scrolling text display.

void XmMessageBox (const char* pszMsg, bool bError)
{
   Widget   topWid;
   int         nRows, nCols;

nRows = MsgSize (const_cast<char*>(pszMsg), nCols);
if ((nRows > 3) || (nCols > 360))
   topWid = XmMessageDialog (pszMsg, nRows, nCols, bError);
else { // use the built-in message box
   topWid = XtVaAppInitialize (&appShell, "D2X-XL", NULL, 0, &argc, argv, NULL, NULL);
   // setup message box text
   Arg args [1];
   XmString xmString = XmStringCreateLocalized (const_cast<char*>(pszMsg));
   XtSetArg (args [0], XmNmessageString, xmString);
   // create and label message box
   Widget xMsgBox = bError
                            ? XmCreateErrorDialog (topWid, const_cast<char*>("Error"), args, 1)
                            : XmCreateWarningDialog (topWid, const_cast<char*>("Warning"), args, 1);
     // remove text resource
   XmStringFree (xmString);
   // remove help and cancel buttons
   XtUnmanageChild (XmMessageBoxGetChild (xMsgBox, XmDIALOG_CANCEL_BUTTON));
   XtUnmanageChild (XmMessageBoxGetChild (xMsgBox, XmDIALOG_HELP_BUTTON));
   // add callback to the "close" button that signals closing of the message box
   XtAddCallback (xMsgBox, XmNokCallback, XmCloseMsgBox, NULL);
   XtManageChild (xMsgBox);
   XtRealizeWidget (topWid);
   }
XtAppMainLoop (appShell);
XtUnrealizeWidget (topWid);
XtDestroyApplicationContext (appShell);
}

SetCloseCallBack intercepts the window manager's close signal to a window. It can be used to prevent the entire application being terminated when the main is closed.

#include <Xm/Protocols.h>

Boolean SetCloseCallBack (Widget shell, void (*callback) (Widget, XtPointer, XtPointer))
{
extern Atom XmInternAtom (Display *, char *, Boolean);
if (!shell)
   return False;
Display* disp = XtDisplay (shell);
if (!disp)
   return False;
// Retrieve Window Manager Protocol Property
Atom prop = XmInternAtom (disp, const_cast<char*>("WM_PROTOCOLS"), False);
if (!prop)
   return False;
// Retrieve Window Manager Delete Window Property
Atom prot = XmInternAtom (disp, const_cast<char*>("WM_DELETE_WINDOW"), True);
if (!prot)
   return False;
// Ensure that Shell has the Delete Window Property
// Necessary since some Window managers are not fully XWM Compilant (olwm for instance is not)
XmAddProtocols (shell, prop, &prot, 1);
// Now add our callback into the Protocol Callback List
XmAddProtocolCallback (shell, prop, prot, callback, NULL);
return True;
}

To be able to compile and link this code you will need to install e.g. the OpenMotif and OpenMotif-devel packages and use the Xm, Xt, and X11 libraries (-lXm -lXt -lX11).


News

A new D2X-XL update fixing broken screen resolution persistence is available

There is yet another D2X-XL update is available (the new Pumo Mines broke a few things ...)

Believe it or not: There's a new D2X-XL version online!

Uploaded updated D2X-XL and library source code that compiles with Visual Studio 19

After a really long time, another new DLE version is available

A new DLE version is available (hear, hear!) :D

A new Max OS X version of D2X-XL is available!

D2X-XL now offers a cartoon style render mode!

New D2X-XL and DLE versions are available

Blarget has got his own area in the level spotlight

New D2X-XL and DLE versions are available

A couple of levels have been added to the level spotlight

Hooray! We're finally having a new Mac OS X version of D2X-XL!

D2X-XL now features an observer mode for multiplayer games

A new D2X-XL version with improved multiplayer synchronization is available

A new D2X-XL version fixing out of sync problems in multiplayer games is available

A new D2X-XL version with many multiplayer bug fixes is available

Added high quality (ogg) Descent 1 and 2 music to the downloads area

A new D2X-XL version with important bug fixes is available

D2X-XL compiles and runs on Linux again!

Descent 1 and 2 high res textures are now available as complete downloads

A new DLE version with a ton of bug fixes and improvements is online

A new D2X-XL version is online

A new D2X-XL version with a much needed bug fix is online

A new DLE version is online

Published another DLE version with more bug fixes and improvements by Sirius

Published a new D2X-XL version

Published a new DLE version with tons of bug fixes and improvements by Sirius

D2X-XL now supports Oculus Rift 3D headsets!

Updated DLE and D2X-XL to support new level features requested by Pumo

Added an article about DLE's new tunnel generator to the worklog

DLE's new tunnel generator is finished

Added a new article to the worklog

Published a new DLE version

Added a new article to the worklog

Added two new articles to the worklog

D2X-XL and DLE-XP now support triangular segment sides!

DLE-XP has an OpenGL render and 1st person view!

New DLE-XP version available

Wrote a new worklog article

Check out DarkFlameWolf's level spotlight section!

New DLE-XP and D2X-XL versions are available

A new DLE-XP version is available

Added a worklog article about the way point feature

Added way point support to DLE-XP and D2X-XL

Posted a bunch of new screenshots

Posted a bunch of new screenshots

A New version of DLE-XP is available

New OS X version of D2X-XL is available!

Added article about lightmaps to worklog.

New article and video added to worklog.