Fanning Software Consulting

Writing Pop-Up Dialog Widgets

QUESTION: I want to gather some information from the user with a widget program, but I can't figure out how to return the information to my program. Can you help?

ANSWER: What you are looking for is what I call a modal dialog widget. Sometimes these are also called pop-up dialog widgets. Their purpose is to collect information from the user and return it to the caller of the program. These programs are typically written as modal widget programs, which mean they block all other program execution until the user interface is destroyed.

What I am going to describe here is a simple modal dialog widget named TextBox. The purpose of this program is to collect one piece of infomation from the user in the form of something the user can type into a text widget. Whatever the user types will be returned to the user in the form of a string variable. For example, to ask the user for the name of a variable to store a processed image at the main IDL level, you might call TextBox like this:

   varname = TextBox(Title='Provide Main-Level Variable Name...',, $
      Label='Variable Name: ', Cancel=cancelled, XSize=200, Value='stretched_image')
   IF NOT cancelled THEN BEGIN
      displayImage = BytScl(*info.image,,  $
         Max=info.maxThresh, Min=info.minThresh)
      dummy = Routine_Names(varname, displayImage, Store=1)

The user would see the following dialog on the display.

The TextBox Program.

Modal versus Blocking Widgets

The most important aspect of a pop-up dialog widget is that it waits for the user to fill out the dialog. All other program activity has to cease until the dialog is destroyed. There are two ways to cause this to happen. You can either block the IDL command line or you can create a modal widget. As it happens, you have to make trade-offs to do this correctly when you write a general purpose dialog widget. You can find out more about this by reading the article Modal and Blocking Widgets.

The important point to remember is that only one widget can block the IDL command line, and that widget is the first one to get there. All subsequent widgets that try to block the IDL command line run through their blocks. This will cause your pop-up dialog widgets to fail miserably. In practice this means that blocking pop-up dialog widgets cannot be called from within a blocking widget program, although they work perfectly well when called from the IDL command line.

You are on much more solid ground if you make your pop-up dialog widgets modal widgets instead of blocking widgets. But, unfortunately, since IDL 5.0 came out, RSI has required you to furnish a group leader when creating a modal widget. This, of course, is not practical when you are using your pop-up dialog from the IDL command line. (Sigh...)

You will see various things done to deal with this problem of wanting to use your pop-up dialog both at the IDL command line and from within other widget programs. I tend to write stern warnings in the program documentation about the necessity to use the GROUP_LEADER keyword if you are calling the pop-up dialog from a widget program. I reason that the chance of someone using the program in a blocking widget program these days is small and, anyway, it's not my fault if they can't RTFM. Of course, that approach seldom works.

The fine folks at RSI like a technique whereby they create an invisible top-level base widget for you to serve as the the group leader of the program. At least in this way, they know the program is going to stop for you! The downside of this is that there is no program icon in the icon tray at the bottom of the display to indicate this program is around, so if the users inadvertently move a window in front of the pop-up dialog before they dismiss it, it can be hell to find again. You can't figure out why your computer suddenly goes on strike and stops responding to you. (Has this computer been talking to my wife, for God's sake!)

But, since the advantages of getting the damn thing to actually work far outweighs the disadvantage of having to hunt for it under a pile of windows, I'll show you the RSI technique in the TextBox program.

The first part of the program just defines the keywords and sets up a Catch error handler. You can read about the keywords in the program documentation. The important keywords to pay attention to currently are the GROUP_LEADER keyword and the CANCEL keyword. I've mentioned the GROUP_LEADER keyword and I am just about to talk about it in more detail. The CANCEL keyword is an output keyword that will indicate to me whether the user cancelled out of the dialog rather than accepting it. The difference is critical.

   FUNCTION TextBox, Title=title, Label=label, Cancel=cancel, $
      Group_Leader=groupleader, XSize=xsize, Value=value

      ; Return to caller if there is an error. Set the cancel
      ; flag and destroy the group leader if it was created.

   Catch, theError
   IF theError NE 0 THEN BEGIN
      Catch, /Cancel
      ok = Dialog_Message(!Error_State.Msg)
      IF destoy_groupleader THEN Widget_Control, groupleader, /Destroy
      cancel = 1
      RETURN, ""

      ; Check parameters and keywords.

   IF N_Elements(title) EQ 0 THEN title = 'Provide Input:'
   IF N_Elements(label) EQ 0 THEN label = ""
   IF N_Elements(value) EQ 0 THEN value = ""
   IF N_Elements(xsize) EQ 0 THEN xsize = 200

Notice that the program is a function. The information collected in the dialog will be returned to the user as the result of the function. In this simple case, the return value will be a string, but in more complicated dialogs the return value is typically a structure variable with fields containing user-supplied information.

Making a Widget Modal Without a Group Leader

The important thing about creating a group leader widget if one is not supplied as an argument is that it must be realized to be a group leader. But you don't want it to be visible to the user. Thus, you realize it, but you unmap it.

You also want to destroy the group leader widget if you created it, so you will need a flag to indicate whether or not you created the group leader. (You certainly don't want to destroy a group leader someone else provided.) The code will look like this:

   IF N_Elements(groupleader) EQ 0 THEN BEGIN
      groupleader = Widget_Base(Map=0)
      Widget_Control, groupleader, /Realize
      destroy_groupleader = 1
   ENDIF ELSE destroy_groupleader = 0

      ; Create modal base widget.

   tlb = Widget_Base(Title=title, Column=1, /Modal, $
      /Base_Align_Center, Group_Leader=groupleader)

Creating the rest of the widgets is straightforward.

   labelbase = Widget_Base(tlb, Row=1)
   IF label NE "" THEN label = Widget_Label(labelbase, Value=label)
   textID = Widget_Text(labelbase, /Editable, Scr_XSize=xsize, Value=value)
   buttonBase = Widget_Base(tlb, Row=1)
   cancelID = Widget_Button(buttonBase, Value='Cancel')
   acceptID = Widget_Button(buttonBase, Value='Accept')

      ; Center the widgets on display.

   Device, Get_Screen_Size=screenSize
   IF screenSize[0] GT 2000 THEN screenSize[0] = screenSize[0]/2 ; Dual monitors.
   xCenter = screenSize(0) / 2
   yCenter = screenSize(1) / 2
   geom = Widget_Info(tlb, /Geometry)
   xHalfSize = geom.Scr_XSize / 2
   yHalfSize = geom.Scr_YSize / 2
   Widget_Control, tlb, XOffset = xCenter-xHalfSize, YOffset = yCenter-yHalfSize

      ; Realize the widget hierarchy.

   Widget_Control, tlb, /Realize

Creating a Storage Pointer

The next important point about pop-up dialog widgets is that the information you collect from the user cannot be stored in the widget program itself. If you do store it there, it will be destroyed when the widget is destroyed and you won't be able to collect it and return it to the user. It has to be stored "off-site", if you please. In practical terms, it has to be stored in a pointer location.

Typically, the pointer points to a structure containing the current or default values of the information you want to collect from the user. This is because if the user kills the dialog with the mouse (rather than selecting either the Cancel or Accept buttons) you want the information returned to the user to simulate what would happen if the user selected the Cancel button.

In our case, if the Cancel button is selected, the CANCEL keyword will be set to one and the return value of the function will be a null string. Thus, I set the pointer up like this:

   ptr = Ptr_New({text:"", cancel:1})

Next, the pointer and any other information required to run the program is placed in the info structure, and the info structure is placed in the user value of the top-level base.

   info = {ptr:ptr, textID:textID, cancelID:cancelID}
   Widget_Control, tlb, Set_UValue=info, /No_Copy

Finally, XManager is called in blocking mode. (Because we have supplied the group leader, this is not really necessary, but it doesn't do any harm and it is what I would do if I did not create the group leader widget to force a modal widget.) The point is, this program unit stops program execution at this stage until the widget is destroyed. As soon as that happens, program execution picks up again at the very next line after the XManager line. Note, that any events that occur in the program will be handled by the event handler module, which we have not written or talked about yet.

   XManager, 'textbox', tlb

Returning the Dialog Information

The final step is to gather the information that has been stored in the pointer location, destroy the pointer itself (so there is no leaking memory), and return the information to the user as the result of the function. In this case, the return value is a scalar string. In more elaborate pop-up dialogs it is more typically an IDL structure variable. Note that I take special care to set the CANCEL keyword value correctly, too.

   theText = (*ptr).text
   cancel = (*ptr).cancel
   Ptr_Free, ptr
   IF destroy_groupleader THEN Widget_Control, groupleader, /Destroy

   RETURN, theText

Note that care was taken to also destroy the group leader widget if we had created it. If this is not done, this is another source of leaking memory.

Writing the Event Handler Module

The purpose of the event handler module (or modules) is simply to gather the required information from the widget program and make sure it gets stored at the pointer storage location properly. I typically ignore all events that happen in my pop-up dialog until the user hits the Accept button. Then, I gather information from my widget interface, and store it properly. The last thing I do (no matter what button was selected) is destroy my widget interface.

Here is the entire event handler code for this program.

   PRO TextBox_Event, event

      ; This event handler responds to all events. Widget
      ; is always destoyed. The text is recorded if ACCEPT
      ; button is selected or user hits CR in text widget.

   Widget_Control,, Get_UValue=info
   CASE event.ID OF
      info.cancelID: Widget_Control,, /Destroy

            ; Get the text and store it in the pointer location.

         Widget_Control, info.textID, Get_Value=theText
         (*info.ptr).text = theText[0]
         (*info.ptr).cancel = 0 ; The user hit ACCEPT.
         Widget_Control,, /Destroy

Web Coyote's Guide to IDL Programming