; docformat = 'rst'
;
; NAME:
;   cgCmdWindow
;
; PURPOSE:
;   Creates a "command" window for IDL traditional commands (Plot, Contour, 
;   Surface, etc. or for Coyote Graphics routines, cgPlot, cgContour, cgSurf, etc.). 
;   In addition, the window contents can be saved as PostScript files or as raster image 
;   files. If ImageMagick is installed on your machine, the raster image files can be 
;   created in very high quality from PostScript files. This program can be used in 
;   place of a normal draw widget or as a stand-alone program.
;
;******************************************************************************************;
;                                                                                          ;
;  Copyright (c) 2011, by Fanning Software Consulting, Inc. All rights reserved.           ;
;                                                                                          ;
;  Redistribution and use in source and binary forms, with or without                      ;
;  modification, are permitted provided that the following conditions are met:             ;
;                                                                                          ;
;      * Redistributions of source code must retain the above copyright                    ;
;        notice, this list of conditions and the following disclaimer.                     ;
;      * Redistributions in binary form must reproduce the above copyright                 ;
;        notice, this list of conditions and the following disclaimer in the               ;
;        documentation and/or other materials provided with the distribution.              ;
;      * Neither the name of Fanning Software Consulting, Inc. nor the names of its        ;
;        contributors may be used to endorse or promote products derived from this         ;
;        software without specific prior written permission.                               ;
;                                                                                          ;
;  THIS SOFTWARE IS PROVIDED BY FANNING SOFTWARE CONSULTING, INC. ''AS IS'' AND ANY        ;
;  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES    ;
;  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT     ;
;  SHALL FANNING SOFTWARE CONSULTING, INC. BE LIABLE FOR ANY DIRECT, INDIRECT,             ;
;  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED    ;
;  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;         ;
;  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND             ;
;  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT              ;
;  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS           ;
;  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.                            ;
;******************************************************************************************;
;+
;  Creates a "command" window for IDL traditional commands (Plot, Contour, 
;  Surface, etc. or for Coyote Graphics routines, cgPlot, cgContour, cgSurf, etc.). 
;  In addition, the window contents can be saved as PostScript files or as raster image 
;  files. If ImageMagick is installed on your machine, the raster image files can be 
;  created in very high quality from PostScript files. This program can be used in 
;  place of a normal draw widget or as a stand-alone program.
;
;  The program is designed to work with any IDL traditional graphics routine
;  that is a procedure and includes no more than four positional parameters.
;  Any number of keywords can be used to specify properties of the graphical
;  output. Any number of graphics commands can be "added" the the window.
;  Simply use the AddCommand method to add commands.
;  
;  If your program does not load its own color tables, the color tables in
;  effect when this program is initiated are used to display the graphics
;  commands.
;    
;  To create PostScript output from within the program, your graphics program
;  has to be written in such a way that it can work properly in the PostScript
;  device. This means there are no Window commands, WSet commands, and the like
;  that are not allowed in the PostScript device. Such commands are allowed in 
;  programs, of course, if they are "protected". Usually such protection looks 
;  like this::
;  
;     IF (!D.Flags AND 256) NE 0 THEN Window, ...
;     
;  The Coyote Graphics program `cgDisplay` is a good program for opening graphics 
;  "windows", because such PostScript protection is built into the program. In a PostScript 
;  device, cgDisplay produces a "window" with the same aspect ratio as the current
;  display graphics window, which is an aid in producing PostScript output that
;  looks like the same output in the display window.
;   
;  Much better looking raster files can be created from the graphics window contents,
;  if the raster files are created by converting PostScript files to the raster 
;  file. If the ImageMagick "convert" command can be found on your machine, you
;  will have the option to create raster files using this method. I *highly*
;  recommend doing so, as fonts and other plot annotation will be of much higher
;  quality using this method.
;   
;  This program has been designed to work with other Coyote Graphics routines: `cgPlot`,
;  `cgContour`, `cgSurf`, and so on, although I expect it to work with any IDL
;  traditional graphics routine, if the routine is well written.
;        
; :Categories:
;    Graphics
;    
; :Author:
;    FANNING SOFTWARE CONSULTING::
;       David W. Fanning 
;       1645 Sheely Drive
;       Fort Collins, CO 80526 USA
;       Phone: 970-221-0438
;       E-mail: david@idlcoyote.com
;       Coyote's Guide to IDL Programming: http://www.idlcoyote.com
;
; :Copyright:
;     Copyright (c) 2011, Fanning Software Consulting, Inc.
;     
; :History:
;     Change History::
;        Separated from old cgWindow program 22 Jan 2012, by David W. Fanning.
;        Fixed a typo in PackageCommand method that prevented extra keywords from
;           being collectd. 26 Jan 2012. DWF.
;        Fixed a problem with MULTI keyword. 31 Jan 2012. DWF.
;        Added a COPY method. 7 Feb 2012. DWF.
;        Fixed a problem with input filenames in the AutoRasterFile method. 22 Feb 2012. DWF.
;        Modified Draw_Widget creation routine to account for events that were added in 
;           different versions of IDL. 23 Feb 2012. DWF.
;        Added ability to use IM_WIDTH keyword to set the width of raster file output
;           created with ImageMagick. 3 April 2012. DWF.
;        Forgot to specify the GROUP_LEADER when calling cgPS_Open. Caused cgPS_Config to
;           run though its block when cgWindow was called from a blocking widget program.
;           5 June 2012. DWF.
;        Added the ability to save the file name and directory of the last output file, so
;            that subsequent file saves can use that last name and directory as a starting
;            default filename. 11 July 2012. DWF.
;        In decompling cgWindow from cgCmdWindow, I accidentally named the WASPECT keyword ASPECT. Restored
;             original name in this version. 13 July 2012. DWF.
;        I added a new method, ReplaceEscapeSequences, that can evaluate escape sequences in
;             string parameters and keywords to call the appropriate cgSymbol value at run-time.
;             This eliminates the need for alternate keywords. 27 July 2012. DWF.
;        Added WDestroyObjects keyword to destroy objects parameters, if needed. 11 November 2012. DWF.
;        Not adding IM_WIDTH parameter from cgWindow_GetDefs. 19 November 2012. DWF.
;        Modified ReplaceEscapeSequence method to use cgCheckForSymbols. 24 November 2012. DWF.
;        Modified to allow keywords to turn off messages from cgPS_Open and cgPS_Close with keywords. 27 November 2012. DWF.
;        The output filename was not specified correctly when making PDF file automatically. Fixed. 2 Dec 2012. DWF.
;        Renamed the MULTI keyword to WMULTI, as it was suppose to be. 11 Feb 2013. DWF.
;        Misspelled keyword in PackageCommand method. 21 February 2013. DWF.
;        Modified GUI so there is only one Raster Image File choice, depending on if ImageMagick is
;           installed. 21 May 2013. DWF.
;        I made the IM_WIDTH field in the object a pointer, so it could remain an undefined variable, 
;            if necessary. 20 FEB 2014. DWF.
;        Typo in the spelling of DESTROYOBJECTS when restoring a graphic visualization. Fixed. 20 Feb 2015. DWF.
;-


;+
; This method initializes the object that is at the heart of the Coyote Graphics system.
; It allows you to both configure the draw widget for events and to load a command into
; the command window.
; 
; :Params:
;    parent: in, optional
;       The parent base widget for this draw widget object. If not defined,
;       the program will create its own top-level base widget.
;       
; :Keywords:
;    altps_Keywords: in, optional, type=string
;       A structure containing alternative keyword names (as tags) and values for
;       those keywords to be used when the current device is the PostScript device.
;       See http://www.idlcoyote.com/cg_tips/kwexpressions.php and the examples
;       below for details on how to use this keyword.
;    altps_Params: in, optional, type=IntArr(3)
;       A structure containing alternative parameter values to be used when 
;       the current device is the PostScript device. Structure names are restricted
;       to the names "P1", "P2", "P3" and "P4"to correspond to the equivalent positional
;       parameter. See http://www.idlcoyote.com/cg_tips/kwexpressions.php and the 
;       examples below for details on how to use this keyword.
;    background: in, optional, type=varies, default=!P.Background
;       The background color of the window. Specifying a background color 
;       automatically sets the WErase keyword.
;    button_events: in, optional, type=boolean
;       Set this keyword to turn button events on for the draw widget.
;    command: in, required, type=string
;       The graphics procedure command to be executed. This parameter
;       must be a string and the the command must be a procedure. Examples
;       are 'Surface', 'Contour', 'Plot', 'cgPlot', cgContour, etc.
;    cmddelay: in, optional, type=float
;       If this keyword is set to a value other than zero, there will be a 
;       delay of this many seconds between command execution. This will permit
;       "movies" of command sequences to be displayed.
;    drop_events: in, optional, type=boolean
;       Set this keyword to turn drop events on for the draw widget.
;    eraseit: in, optional, type=boolean, default=0
;       Set this keyword to cause the window to be erased before graphics commands 
;       are drawn. This may need to be set, for example, to display images.
;    event_handler: in, optional, type=string
;        The name of an event handler procedure to accept events from
;        the draw widget. Write the event handler procedure with two
;        positional parameters. The first will be the event structure
;        returned from the draw widget. The second will be the cgCmdWindow
;        object reference (i.e., self) to allow you to manipulate the
;        command window.
;    group_leader: in, optional
;       The identifier of a widget to serve as a group leader for this program.
;       If the group leader is destroyed, this program is also destroyed. Used
;       when calling this program from another widget program.
;    keyboard_events: in, optional, type=boolean
;       Set this keyword to turn keyboard events on for the draw widget.
;    method: in, optional, type=boolean, default=0
;       Set this keyword if the command is an object method call rather than a 
;       procedure call. If this keyword is set, the first positional parameter, p1,
;       must be present and must be a valid object reference.
;    motion_events: in, optional, type=boolean
;       Set this keyword to turn motion events on for the draw widget.
;    oxmargin: in, optional, type=float
;       A two-element array indicating the left and right X outside margins for the
;       graphical display. Used only when doing multiple plots with `WMulti`.
;    oymargin: in, optional, type=float
;       A two-element array indicating the bottom and top Y outside margins for the
;       graphical display. Used only when doing multiple plots with `WMulti`.
;    p1: in, optional, type=any
;       The first positional parameter appropriate for the graphics command.
;    p2: in, optional, type=any
;       The second positional parameter appropriate for the graphics command.
;    p3: in, optional, type=any
;       The third positional parameter appropriate for the graphics command.
;    p4: in, optional, type=any
;       The fourth positional parameter appropriate for the graphics command.
;    replacecmd: in, optional, type=boolean, default=0
;       Set this keyword to replace a graphics command from an cgWindow.
;       If CmdIndex is undefined, *all* commands in the window are replaced. Use 
;       WinID to identify the cgWindow you are interested in. If WinID is 
;       undefined, the last cgWindow created is used for the replacement.
;    storage: in, optional, type=any
;       Any user-defined IDL variable will be stored in the object storage location.
;       Defined here for convenience. Same as `Storage` keyword for the SetProperty method.
;    tracking_events: in, optional, type=boolean
;       Set this keyword to turn tracking events on for the draw widget.
;    waspect: in, optional, type=float, default=normal
;       Set this keyword to the aspect ratio you would like the window to have.
;       The aspect ratio is calculated as (ysize/xsize). Must be a float value.
;       If this keyword is set, the window will maintain this aspect ratio,
;       even when it is resized.
;    wdestroyobjects: in, optional, type=boolean, default=0
;       If this keyword is set, and any of the input parameters p1-p4 is an object,
;       the object parameter will be destroyed when the window is destroyed.
;    wheel_events: in, optional, type=boolean
;       Set this keyword to turn wheel events on for the draw widget.
;    wmulti: in, optional, type=intarr(5)
;        Set this keyword in exactly the same way you would set the !P.Multi keyword.
;        It will allow you to display multi-plots in the cgWindow graphics window.
;    wtitle: in, optional, type=string, default='Resizeable Graphics Window'
;       The title of the graphics window if the program creates its own top-level
;       base widget. A window index number is appended to the title so multiple cgWindow 
;       programs can be selected. Also used to register the widget program and as the
;       title of the object when it is stored.
;    wxpos: in, optional, type=integer, default=5
;       The x offset in device coordinates of the window from the upper-left corner of 
;       the display, if the program creates its own top-level base. Otherwise, this keyword
;       is ignored.
;    wypos: in, optional, type=integer, default=5
;       The y offset in device coordinates of the window from the upper-left corner of 
;       the display, if the program creates its own top-level base. Otherwise, this keyword
;       is ignored.
;    wxsize: in, optional, type=integer, default=640
;       The x size in device coordinates of the graphics window.
;    wysize: in, optional, type=integer, default=512
;       The y size in device coordinates of the the graphics window.
;    _extra: in, optional
;       The "extra" keywords for the command that is being added to the window.
;-
FUNCTION cgCmdWindow::Init, parent, $
   AltPS_Keywords=altps_Keywords, $ ; A structure of PostScript alternative keywords and values.
   AltPS_Params=altps_Params, $     ; A structure of PostScript alternative parameters and values. 
   Background = background, $       ; The background color. Set to !P.Background by default.
   Button_Events=button_events, $   ; Set this keyword to allow button events in the draw widget.
   CmdDelay=cmdDelay, $             ; Set this keyword to a value to "wait" before executing the next command.
   Command=command, $               ; The graphics "command" to execute.
   Drop_Events=drop_events, $       ; Set this keyword to allow drop events in the draw widget.
   EraseIt = eraseit, $             ; Set this keyword to erase the display before executing the command.
   Event_Handler=event_handler, $   ; Set this keyword to the name of an event handler procedure to handle draw widget events.
   Group_Leader = group_leader, $   ; The group leader of the cgWindow program.
   Keyboard_Events=keyboard_events, $ ; Set this keyword to allow keyboard events in the draw widget.
   Method=method, $                 ; If set, will use CALL_METHOD instead of CALL_PROCEDURE to execute command.
   Motion_Events=motion_events, $   ; Set this keyword to allow motion events in the draw widget.
   OXMargin = xomargin, $           ; Set the !X.OMargin. A two element array.
   OYMargin = yomargin, $           ; Set the !Y.OMargin. A two element array
   P1=p1, $                         ; The first postitional parameter in a graphics command loaded for display.
   P2=p2, $                         ; The sedond postitional parameter in a graphics command loaded for display.
   P3=p3, $                         ; The third postitional parameter in a graphics command loaded for display.
   P4=p4, $                         ; The fourth postitional parameter in a graphics command loaded for display. 
   ReplaceCmd=replacecmd, $         ; Replace the current command and execute in the current window.
   Storage=storage, $               ; A storage pointer location. Used like a user value in a widget.
   Tracking_Events=tracking_events, $ ; Set this keyword to allow tracking events in the draw widget.
   WAspect = waspect, $             ; Set the window aspect ratio to this value.
   WDestroyObjects=wdestroyobjects, $ ; Set this keyword to destroy object parameters upon exit.
   Wheel_Events=wheel_events, $     ; Set this keyword to allow wheel events in the draw widget.
   WMulti = wmulti, $               ; Set this in the same way !P.Multi is used.
   WTitle = title, $                ; The window title.
   WXPos = xpos, $                  ; The X offset of the window on the display. The window is centered if not set.
   WXSize = xsize, $                ; The X size of the cgWindow graphics window in pixels. By default: 400.
   WYPos = ypos, $                  ; The Y offset of the window on the display. The window is centered if not set.
   WYSize = ysize, $                ; The Y size of the cgWindow graphics window in pixels. By default: 400.
   _Extra = extra                   ; Any extra keywords. Usually the "command" keywords.

    Compile_Opt idl2
    
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, 0
    ENDIF
   
    ; Check keywords.
    method = Keyword_Set(method)
    
    ; Get the global defaults.
    cgWindow_GetDefs, $
       AdjustSize = d_adjustsize, $                      ; Adjust charsize to window size.
       Background = d_background, $                      ; The background color. 
       Delay = d_delay, $                                ; The amount of delay between command execution.
       EraseIt = d_eraseit, $                            ; Set this keyword to erase the display before executing the commands.
       Multi = d_multi, $                                ; Set this in the same way !P.Multi is used.   
       XOMargin = d_xomargin, $                          ; Set the !X.OMargin. A two element array.
       YOMargin = d_yomargin, $                          ; Set the !Y.OMargin. A two element array
       XSize = d_xsize, $                                ; The X size of the cgWindow graphics window.
       YSize = d_ysize, $                                ; The Y size of the cgWindow graphics window.
       Title = d_title, $                                ; The window title.
       XPos = d_xpos, $                                  ; The X offset of the window on the display.
       YPos = d_ypos, $                                  ; The Y offset of the window on the display. 
       Aspect = d_aspect, $                              ; The aspect ratio of the window.
       Palette = d_palette, $                            ; The color table palette to use for the window.
       
       ; PDF properties.
       PDF_Unix_Convert_Cmd = d_pdf_unix_convert_cmd, $  ; Command to convert PS to PDF.
       PDF_Path = d_pdf_path, $                          ; The path to the Ghostscript conversion command.
   
       ; ImageMagick Properties.
       IM_Transparent = d_im_transparent, $              ; Sets the "alpha" keyword on ImageMagick convert command.
       IM_Density = d_im_density, $                      ; Sets the density parameter on ImageMagick convert command.
       IM_Raster = d_im_raster, $                        ; Create raster files via ImageMagick.
       IM_Resize = d_im_resize, $                        ; Sets the resize parameter on ImageMagick convert command.
       IM_Options = d_im_options, $                      ; Sets extra ImageMagick options on the ImageMagick convert command.
       IM_Width = d_im_width, $                          ; The width of the raster file output.
        
       ; PostScript properties.
       PS_Decomposed = d_ps_decomposed, $                ; Sets the PostScript color mode.
       PS_Delete = d_ps_delete, $                        ; Delete PS file when making IM raster.
       PS_Metric = d_ps_metric, $                        ; Select metric measurements in PostScript output.
       PS_Encapsulated = d_ps_encapsulated, $            ; Create Encapsulated PostScript output.    
       PS_FONT = d_ps_font, $                            ; Select the font for PostScript output.
       PS_CHARSIZE = d_ps_charsize, $                    ; Select the character size for PostScript output.
       PS_QUIET = d_ps_quiet, $                          ; Select the QUIET keyword for cgPS_Open.
       PS_SCALE_FACTOR = d_ps_scale_factor, $            ; Select the scale factor for PostScript output.
       PS_TT_FONT = d_ps_tt_font                         ; Select the true-type font to use for PostScript output.
        
    ; If method is set, the first positional parameter must be present,
    ; and it must be a valid object reference.
    IF method THEN BEGIN
        IF N_Elements(p1) EQ 0 THEN $
            Message, 'The parameter P1 must be present to make a method call.'
        IF ~Obj_Valid(p1) THEN $
            Message, 'The parameter P1 must be a valid object reference when making method calls.'
    ENDIF
    SetDefaultValue, waspect, d_aspect 
    SetDefaultValue, xsize, d_xsize 
    SetDefaultValue, ysize, d_ysize 
    SetDefaultValue, xpos, d_xpos 
    SetDefaultValue, ypos, d_ypos 
    SetDefaultValue, button_events, 0, /Boolean
    SetDefaultValue, drop_events, 0, /Boolean
    SetDefaultValue, keyboard_events, 0, /Boolean
    SetDefaultValue, motion_events, 0, /Boolean
    SetDefaultValue, tracking_events, 0, /Boolean
    SetDefaultValue, wheel_events, 0, /Boolean
    IF N_Elements(storage) NE 0 THEN self.storage = Ptr_New(storage)
    IF N_Elements(background) EQ 0 THEN BEGIN
        background = d_background
        IF N_Elements(command) EQ 0 THEN eraseit = 1
        IF (N_Elements(command) NE 0) && cgCoyoteGraphic(command) THEN eraseit = 1
    ENDIF ELSE BEGIN
        eraseit = 1
    ENDELSE
    IF N_Elements(eraseIt) EQ 0 THEN eraseIt = d_eraseit ELSE eraseIt = Keyword_Set(eraseIt)

    ; The commands will be placed in a linked list for execution.
    self.cmds = Obj_New('LinkedList')
    IF Obj_Valid(self.cmds) EQ 0 THEN Message, 'Failed to make the LinkedList for the commands.'
    
    ; Add a command, if you have one. Otherwise, just make the window.
    IF (N_Elements(command) NE 0) THEN BEGIN
        thisCommand = Obj_New('cgWindow_Command', COMMAND=command, $
                P1=p1, P2=p2, P3=p3, P4=p4, KEYWORDS=extra, TYPE=method, $
                ALTPS_KEYWORDS=altps_Keywords, ALTPS_PARAMS=altps_Params, $
                DESTROYOBJECTS=Keyword_Set(wdestroyobjects))
        IF Obj_Valid(thisCommand) THEN self.cmds -> Add, thisCommand ELSE Message, 'Failed to make command object.'
    ENDIF 
    
    ; If there is a palette, use it. Otherwise, use the current color table vectors.
    IF Total(d_palette) NE 0 THEN BEGIN
        IF Size(d_palette, /N_DIMENSIONS) NE 2 THEN Message, 'Color palette is not a 3xN array.'
        dims = Size(d_palette, /DIMENSIONS)
        threeIndex = Where(dims EQ 3)
        IF ((threeIndex)[0] LT 0) THEN Message, 'Color palette is not a 3xN array.'
        IF threeIndex[0] EQ 0 THEN d_palette = Transpose(d_palette)
        self.r = Ptr_New(d_palette[*,0])
        self.g = Ptr_New(d_palette[*,1])
        self.b = Ptr_New(d_palette[*,2])
    ENDIF ELSE BEGIN
        TVLCT, rr, gg, bb, /Get
        self.r = Ptr_New(rr)
        self.g = Ptr_New(gg)
        self.b = Ptr_New(bb)
    ENDELSE
    
    ; Create the widgets for the program.
    IF N_Elements(parent) NE 0 THEN BEGIN
       self.tlb = parent
       createParent = 0
    ENDIF ELSE BEGIN    
       self.tlb = Widget_Base(/TLB_SIZE_EVENTS, MBar=menuID, UNAME='TLB_RESIZE')
       createParent = 1
    ENDELSE
    
    ; Create buttons if you created the parent widget.
    IF createParent THEN BEGIN
    
        fileID = Widget_Button(menuID, Value='File')
        saveID = Widget_Button(fileID, Value='Save Window As...', /MENU)
        button = Widget_Button(saveID, Value='PostScript File', UNAME='POSTSCRIPT')
        button = Widget_Button(saveID, Value='PDF File', UNAME='PDF')
        
        ; If you can find ImageMagick on this machine, you can convert to better
        ; looking raster files.
        IF cgHasImageMagick() EQ 1 THEN BEGIN
            imraster = Widget_Button(saveID, Value='Raster Image File', /MENU)
            button = Widget_Button(imraster, Value='BMP', UNAME='IMAGEMAGICK_BMP')
            button = Widget_Button(imraster, Value='GIF', UNAME='IMAGEMAGICK_GIF')
            button = Widget_Button(imraster, Value='JPEG', UNAME='IMAGEMAGICK_JPEG')
            button = Widget_Button(imraster, Value='PNG', UNAME='IMAGEMAGICK_PNG')
            button = Widget_Button(imraster, Value='TIFF', UNAME='IMAGEMAGICK_TIFF')
        ENDIF ELSE BEGIN
            raster = Widget_Button(saveID, Value='Raster Image File', /MENU)
            button = Widget_Button(raster, Value='BMP', UNAME='RASTER_BMP')
            button = Widget_Button(raster, Value='GIF', UNAME='RASTER_GIF')
            button = Widget_Button(raster, Value='JPEG', UNAME='RASTER_JPEG')
            button = Widget_Button(raster, Value='PNG', UNAME='RASTER_PNG')
            button = Widget_Button(raster, Value='TIFF', UNAME='RASTER_TIFF')
        ENDELSE
        
        button = Widget_Button(fileID, Value='Save Current Visualization', /Separator, UNAME='SAVECOMMANDS')
        button = Widget_Button(fileID, Value='Restore Visualization', UNAME='RESTORECOMMANDS')
        button = Widget_Button(fileID, Value='Quit', /Separator, UNAME='QUIT')
    
    ENDIF
    
    ; Check to see if you have to create a window with a particular aspect ratio.
    ; If so, your xsize and ysize values will need to be adjusted.
    IF waspect NE 0 THEN BEGIN
         IF waspect GE 1 THEN BEGIN
            xsize = Round(ysize / waspect)
         ENDIF ELSE BEGIN
            ysize = Round(xsize * waspect)
         ENDELSE
     ENDIF
    
    ; Create draw widget. UNIX versions of IDL have a bug in which creating
    ; a draw widget as the very first window in an IDL session causes both
    ; !P.Background and !P.Color to be set to white. I know, it's odd. But
    ; doing this little trick fixes the problem.
    tempBackground = !P.Background
    tempColor = !P.Color
    retain = (StrUpCase(!Version.OS_Family) EQ 'UNIX') ? 2 : 1
    IF ~createParent THEN BEGIN
        notify_realize = 'cgCmdWindowDrawRealize'
        kill_notify = 'cgCmdWindowKillNotify'
    ENDIF
    self.drawID = Widget_Draw(self.tlb, XSIZE=xsize, YSize=ysize, RETAIN=retain, $
       UVALUE=self, Notify_Realize=notify_realize, $
       UNAME='DRAW_WIDGET', Kill_Notify=kill_notify, $
       BUTTON_EVENTS=button_events, $
       KEYBOARD_EVENTS=keyboard_events, $
       MOTION_EVENTS=motion_events, $
       TRACKING_EVENTS=tracking_events) 
    release = Float(!Version.Release) 
    IF (N_Elements(wheel_events) NE 0) && (release GE 6.2) THEN $
       Widget_Control, self.drawID, DRAW_WHEEL_EVENTS=wheel_events
    IF (N_Elements(drop_events) NE 0) && (release GE 6.3) THEN $
       Widget_Control, self.drawID, SET_DROP_EVENTS=drop_events
       
    
    !P.Background = Temporary(tempBackground)
    !P.Color = Temporary(tempColor)
    
    ; Do we need to center the widget? Only do this if you created the parent.
    IF (xpos EQ -1) && (ypos EQ -1) && createParent THEN BEGIN
        DefSysV, '!FSC_WINDOW_LIST', EXISTS=exists
        IF ~exists THEN BEGIN
           xpos = 5
           ypos = 5
        ENDIF ELSE BEGIN
            IF Obj_Valid(!FSC_WINDOW_LIST) THEN BEGIN
                count = !FSC_WINDOW_LIST -> Get_Count()
                xpos = 5 + (30*count)
                ypos = 5 + (25*count)
            ENDIF ELSE BEGIN
                xpos = 5
                ypos = 5           
            ENDELSE
        ENDELSE
        cgCenterTLB, self.tlb, xpos, ypos, /NOCENTER, /DEVICE
    ENDIF ELSE BEGIN
        IF createParent THEN cgCenterTLB, self.tlb, xpos, ypos, /NOCENTER, /DEVICE
    ENDELSE
    
    ; Display the widget and get window index number.
    currentWindow = !D.Window
    IF createParent THEN BEGIN 
        Widget_Control, self.tlb, /Realize
        Widget_Control, self.drawID, Get_Value=wid
        self.wid = wid
        WSet, wid
        IF N_Elements(title) EQ 0 THEN BEGIN
            title = d_title
            title = title + ' (' + StrTrim(wid,2) + ')'
        ENDIF
        Widget_Control, self.tlb, TLB_Set_Title=title
    ENDIF ELSE BEGIN
        IF N_Elements(title) EQ 0 THEN title = "cgCmdWindow"
    ENDELSE
    self.title = title
    

    ; Load object properties.
    self.background = Ptr_New(background)
    IF N_Elements(cmdDelay) NE 0 THEN self.delay = cmdDelay ELSE self.delay = d_delay
    self.eraseIt = eraseIt
    IF N_Elements(wmulti) NE 0 THEN BEGIN
       FOR j=0,N_Elements(wmulti)-1 DO self.pmulti[j] = wmulti[j]
    ENDIF ELSE self.pmulti = d_multi
    IF N_Elements(xomargin) NE 0 THEN self.xomargin = xomargin ELSE self.xomargin = d_xomargin
    IF N_Elements(yomargin) NE 0 THEN self.yomargin = yomargin ELSE self.yomargin = d_yomargin
    self.adjustsize = d_adjustsize
    self.createParent = createParent
    self.destroyObjects = Keyword_Set(wdestroyobjects)
    self.im_transparent = d_im_transparent
    self.im_density = d_im_density
    self.im_options = d_im_options
    self.im_raster = d_im_raster
    self.im_resize = d_im_resize
    self.im_width = Ptr_New(d_im_width)
    self.msysvar = Ptr_New(/Allocate_Heap)
    self.pdf_unix_convert_cmd = d_pdf_unix_convert_cmd
    self.pdf_path = d_pdf_path
    self.ps_decomposed = d_ps_decomposed
    self.ps_delete = d_ps_delete
    self.ps_encapsulated = d_ps_encapsulated
    self.ps_metric = d_ps_metric
    self.ps_charsize = d_ps_charsize
    self.ps_font = d_ps_font
    self.ps_quiet = d_ps_quiet
    self.ps_scale_factor = d_ps_scale_factor
    self.ps_tt_font = d_ps_tt_font
    self.waspect = waspect

    IF createParent THEN BEGIN
    
        ; Get it running.
        WIDGET_CONTROL, /MANAGED, self.tlb
        XManager, 'cgwindow', self.tlb, /No_Block, $
            Event_Handler='cgCmdWindow_Dispatch_Events', $
            Cleanup = 'cgCmdWindow_Cleanup', $
            Group_Leader=group_leader
            
        ; Store the self object in UVALUE of TLB. This is so it can be cleaned up properly.
        Widget_Control, self.tlb, SET_UValue=self
        
        ; Store the object reference on the external linked list.
        self -> StoreObjectReference
        
        ; Execute the commands
        self -> ExecuteCommands
        
    ENDIF
    
    ; Restore the current graphics window, if you can.
    IF (currentWindow GE 0) && WindowAvailable(currentWindow) THEN BEGIN
       WSet, currentWindow
    ENDIF ELSE WSet, -1
    RETURN, 1

END ;----------------------------------------------------------------------------------------------------------------

;+
; This program is called when the draw widget is realized. The purpose is to find
; and set the window index number in the program, store the object reference on
; an external linked list so it can found again later, and to execute the loaded
; commands.
; 
; :Params:
;     drawID: in, required
;         The widget identifier of the draw widget.
;-
PRO cgCmdWindowDrawRealize, drawID

    Widget_Control, drawID, Get_UVALUE=theObject, Get_VALUE=wid
    theObject -> SetProperty, WID=wid
    
    ; Store the object reference on the external linked list.
    theObject -> StoreObjectReference
    
    ; Execute the commands
    theObject -> ExecuteCommands
    
END



;+
; This program is called when the draw widget is destroyed. The purpose
; is to clean up the object.
; 
; :Params:
;     drawID: in, required
;         The widget identifier of the draw widget.
;-
PRO cgCmdWindowKillNotify, drawID

    Widget_Control, drawID, GET_UVALUE=theObject
    Obj_Destroy, theObject
    
END

;
;
;+
; The cleanup method for the cgCmdWindow object.
;-
PRO cgCmdWindow::Cleanup

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF
    
    ; Free any pointers.
    Ptr_Free, self.background
    Ptr_Free, self.r
    Ptr_Free, self.g
    Ptr_Free, self.b
    Ptr_Free, self.msysvar
    Ptr_Free, self.storage
    Ptr_Free, self.im_width
    
    ; Destroy the command objects.
    IF Obj_Valid(self.cmds) THEN BEGIN
        count = self.cmds -> Get_Count()
        FOR j=0,count-1 DO Obj_Destroy, self.cmds -> Get_Item(j, /DEREFERENCE)
        Obj_Destroy, self.cmds
    ENDIF
    
    ; You have to remove yourself from the list of valid cgWindows.
    theList = !FSC_WINDOW_LIST
    IF Obj_Valid(theList) THEN BEGIN
        structs = theList -> Get_Item(/ALL, /DEREFERENCE)
        index = Where(structs.windowObj[*] EQ self, count)
        IF count GT 0 THEN theList -> Delete, index[0]
    ENDIF 
    
    ; If the list doesn't have any more cgWindows objects in it,
    ; delete the list so it doesn't waste memory. 
    IF ((theList -> Get_Count()) EQ 0) THEN Obj_Destroy, theList
    
    ; If your widget ID is valid, destroy the widget program.
    IF self.createParent && Widget_Info(self.tlb, /VALID_ID) THEN Widget_Control, self.tlb, /Destroy
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; This procedure builds a command structure and saves the structure
; to the main IDL level. Its purpose is to allow the user to work
; with restored commands independently of the window interface.
;
; :Params:
;     structname: in, optional, type=string, default="cmd"
;         The name of the command structure to be saved at the 
;         main IDL level.
;
; :Keywords:
;     quiet: in, optional, type=boolean
;         If set, the message indicated where the variable was saved
;         and what its name is is not printed in the IDL command window.
;-
PRO cgWindow_Command::CreateCommandStruct, structName, Quiet=quiet

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Struct variable name
    IF N_Elements(structName) EQ 0 THEN structName='cmd'

    cmdString = self.command
    cmdStruct = Create_Struct('Command', cmdString, 'nparams', self.nparams, 'type', self.type)
    CASE self.nparams OF
        0:
        1: cmdStruct = Create_Struct(cmdStruct, 'p1', *self.p1)
        2: cmdStruct = Create_Struct(cmdStruct, 'p1', *self.p1, 'p2', *self.p2)
        3: cmdStruct = Create_Struct(cmdStruct, 'p1', *self.p1, 'p2', *self.p2, 'p3', *self.p3)
        4: cmdStruct = Create_Struct(cmdStruct, 'p1', *self.p1, 'p2', *self.p2, 'p3', *self.p3, 'p4', *self.p4)
    ENDCASE
    IF Ptr_Valid(self.keywords) THEN BEGIN
        cmdStruct = Create_Struct(cmdStruct, 'keywords', *self.keywords)
    ENDIF

    ; Copy the variable to the MAIN level
    (Scope_VarFetch(structName, /Enter, Level=1)) = Temporary(cmdStruct)
    IF NOT Keyword_Set(quiet) THEN $
        PRINT, 'Created command struct variable ', structName, ' in IDL $MAIN level.'

END ;----------------------------------------------------------------------------------------------------------------


;+
; This procedure makes and returns a copy of a command object.
;-
FUNCTION cgWindow_Command::Copy

    IF Ptr_Valid(self.keywords) THEN BEGIN
    
        CASE self.nparams OF
           0: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, $
                KEYWORDS=*self.keywords, TYPE=self.type, $
                DESTROYOBJECTS=self.destroyObjects)
           1: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, $
                KEYWORDS=*self.keywords, TYPE=self.type, $
                DESTROYOBJECTS=self.destroyObjects)
           2: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, P2=*self.p2, $
                KEYWORDS=*self.keywords, TYPE=self.type, $
                DESTROYOBJECTS=self.destroyObjects)
           3: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, P2=*self.p2, $
                P3=*self.p3, KEYWORDS=*self.keywords, TYPE=self.type, $
                DESTROYOBJECTS=self.destroyObjects)
           4: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, P2=*self.p2, $
                P3=*self.p3, P4=*self.p4, KEYWORDS=*self.keywords, TYPE=self.type, $
                DESTROYOBJECTS=self.destroyObjects)
        ENDCASE
        
    ENDIF ELSE BEGIN
    
        CASE self.nparams OF
           0: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, $
                TYPE=self.type, DESTROYOBJECTS=self.destroyObjects)
           1: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, $
                 TYPE=self.type, DESTROYOBJECTS=self.destroyObjects)
           2: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, P2=*self.p2, $
                 TYPE=self.type, DESTROYOBJECTS=self.destroyObjects)
           3: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, p2=*self.p2, P3=*self.p3, $
                 TYPE=self.type, DESTROYOBJECTS=self.destroyObjects)
           4: copyObj = Obj_New('cgWindow_Command', COMMAND=self.command, P1=*self.p1, p2=*self.p2, P3=*self.p3, P4=*self.p4, $
                 TYPE=self.type, DESTROYOBJECTS=self.destroyObjects)
        ENDCASE
    
    ENDELSE
    
    RETURN, copyObj
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; This procedure returns the keyword in a command object, if there
; are any keywords.
;
; :Keywords:
;     has_keywords: out, optional, type=boolean
;         If set to 1, the function returned keywords. If set to 0,
;         there were no keywords in the command to return.
;-
FUNCTION cgWindow_Command::Get_Keywords, HAS_KEYWORDS=has_keywords
    IF Ptr_Valid(self.keywords) THEN BEGIN
        has_keywords = 1
        RETURN, *self.keywords 
    ENDIF ELSE BEGIN
        has_keywords = 0
        RETURN, Ptr_New()
    ENDELSE
END ;----------------------------------------------------------------------------------------------------------------


PRO cgWindow_Command::Draw, SUCCESS=success, KEYWORDS=keywords

    Compile_Opt idl2
    
    ; Can't really catch CALL_PROCEDURE errors, so I'm not sure what this is doing here.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        success = 0
        RETURN
    ENDIF
    
    ; It seems it is not possible to catch errors generated by CALL_PROCEDURE,
    ; so I have to fake it out. I reset the !ERROR_STATE structure at the beginning
    ; and assume failure. After I get through the code, I check to see if the
    ; !ERROR_STATE.MSG field is still a null string. If it is, I assume we successfully
    ; executed the command and change the success flag.
    success = 0
    Message, /RESET
    
    ; Do we have alternative keywords?
    IF Ptr_Valid(self.altps_Keywords) THEN BEGIN
       keywords = self -> EvaluateKeywords(*self.keywords, SUCCESS=success)
       IF success EQ 0 THEN Ptr_Free, self.keywords
    ENDIF ELSE BEGIN
        IF Ptr_Valid(self.keywords) THEN BEGIN
            keywords = *self.keywords
            keywords = self -> ReplaceEscapeSequences(keywords)
        ENDIF
    ENDELSE
    
    ; Do we have alternative parameters?
    IF Ptr_Valid(self.altps_params) THEN BEGIN
    
        altTags = Tag_Names(*self.altps_params)
        IF (Where(altTags EQ 'P1') NE -1) THEN BEGIN
            IF (!D.Name EQ 'PS') $
                THEN p1 = (*self.altps_params).p1 $
                ELSE IF Ptr_Valid(self.p1) THEN p1 = *self.p1
        ENDIF ELSE IF Ptr_Valid(self.p1) THEN p1 = *self.p1
        IF (Where(altTags EQ 'P2') NE -1) THEN BEGIN
            IF (!D.Name EQ 'PS') $
                THEN p2 = (*self.altps_params).p2 $
                ELSE IF Ptr_Valid(self.p2) THEN p2 = *self.p2
        ENDIF ELSE IF Ptr_Valid(self.p2) THEN p2 = *self.p2
        IF (Where(altTags EQ 'P3') NE -1) THEN BEGIN
            IF (!D.Name EQ 'PS') $
                THEN p3 = (*self.altps_params).p3 $
                ELSE IF Ptr_Valid(self.p3) THEN p3 = *self.p3
        ENDIF ELSE IF Ptr_Valid(self.p3) THEN p3 = *self.p3
        IF (Where(altTags EQ 'P4') NE -1) THEN BEGIN
            IF (!D.Name EQ 'PS') $
                THEN p4 = (*self.altps_params).p4 $
                ELSE IF Ptr_Valid(self.p4) THEN p4 = *self.p4
        ENDIF ELSE IF Ptr_Valid(self.p4) THEN p4 = *self.p4
    
    ENDIF ELSE BEGIN
        IF Ptr_Valid(self.p1) THEN p1 = *self.p1
        IF Ptr_Valid(self.p2) THEN p2 = *self.p2
        IF Ptr_Valid(self.p3) THEN p3 = *self.p3
        IF Ptr_Valid(self.p4) THEN p4 = *self.p4
    ENDELSE
    
    ; Remove escape sequences, if you need to.
    IF Size(p1, /TNAME) EQ 'STRING' THEN p1 = self -> ReplaceEscapeSequences(p1)
    IF Size(p2, /TNAME) EQ 'STRING' THEN p2 = self -> ReplaceEscapeSequences(p2)
    IF Size(p3, /TNAME) EQ 'STRING' THEN p3 = self -> ReplaceEscapeSequences(p3)
    IF Size(p4, /TNAME) EQ 'STRING' THEN p4 = self -> ReplaceEscapeSequences(p4)

    ; What kind of command is this?
    CASE self.type OF 
    
        ; Command calls a procedure.
        0: BEGIN
        
             IF Ptr_Valid(self.keywords) THEN BEGIN
                 CASE self.nparams OF
                     0: Call_Procedure, self.command, _Extra=keywords
                     1: Call_Procedure, self.command, p1, _Extra=keywords
                     2: Call_Procedure, self.command, p1, p2, _Extra=keywords
                     3: Call_Procedure, self.command, p1, p2, p3, _Extra=keywords
                     4: Call_Procedure, self.command, p1, p2, p3, p4, _Extra=keywords
                 ENDCASE
             ENDIF ELSE BEGIN
                 CASE self.nparams OF
                     0: Call_Procedure, self.command
                     1: Call_Procedure, self.command, p1
                     2: Call_Procedure, self.command, p1, p2
                     3: Call_Procedure, self.command, p1, p2, p3
                     4: Call_Procedure, self.command, p1, p2, p3, p4
                 ENDCASE
             ENDELSE
             
             END
             
        ; Command calls a method.
        1: BEGIN

             IF Ptr_Valid(self.keywords) THEN BEGIN
                 CASE self.nparams OF
                     0: Message, 'Call_Method requires at least one positional parameter.'
                     1: Call_Method, self.command, p1, _Extra=keywords
                     2: Call_Method, self.command, p1, p2, _Extra=keywords
                     3: Call_Method, self.command, p1, p2, p3, _Extra=keywords
                     4: Call_Method, self.command, p1, p2, p3, p4, _Extra=keywords
                 ENDCASE
             ENDIF ELSE BEGIN
                 CASE self.nparams OF
                     0: Message, 'Call_Method requires at least one positional parameter.'
                     1: Call_Method, self.command, p1
                     2: Call_Method, self.command, p1, p2
                     3: Call_Method, self.command, p1, p2, p3
                     4: Call_Method, self.command, p1, p2, p3, p4
                 ENDCASE
             ENDELSE
             
           END
    
    ENDCASE
    
    ; If nothing has been put into the message field, we much have executed the command
    ; successfully.
    IF !Error_State.MSG EQ "" THEN success = 1 

    ; For some reason, CALL_PROCEDURE does not flush the graphics buffer on UNIX machines.
    ; We have to do it ourself to get the program to resize correctly on UNIX machines.
    EMPTY
    
END ;----------------------------------------------------------------------------------------------------------------



;+
; This method replaces stored keyword values with alternative keyword values, if they
; are available. The return variable is a list of the keywords with the
; alternative values substituted for the stored values.
;     
; :Params:
;    keywords: in, required, type=structure
;       The list of input keywords.
;       
; :Keywords:
;     success: out, optional, type=boolean
;         If the command executed successfully, return a 1. Otherwise
;         return a 0.
;-
FUNCTION cgWindow_Command::EvaluateKeywords, keywords, SUCCESS=success

  ; Error handling.
  Catch, theError
  IF theError NE 0 THEN BEGIN
     Catch, /CANCEL
     success = 0
     void = cgErrorMsg()
     IF N_Elements(keywords) NE 0 THEN RETURN, keywords ELSE RETURN, 0
  ENDIF
  
  Compile_Opt idl2
  
  ; Assume success.
  success = 1
  
  ; Need to have keywords.
  IF N_Elements(keywords) EQ 0 THEN $
      Message, 'A keyword structure must be passed to the method.'

  ; Get the keyword names.
  tags = Tag_Names(keywords)
  
  ; Find the keywords to be evaluated in this keyword structure.
  evalTags = Tag_Names(*self.altps_keywords)
  theseTags = Ptr_New(/Allocate_Heap)
  FOR j=0,N_Elements(evalTags)-1 DO BEGIN
     index = Where(tags EQ evalTags[j], count)
     IF (count EQ 1) THEN BEGIN
        IF N_Elements(*theseTags) NE 0 THEN *theseTags = [*theseTags, index] ELSE *theseTags = [index] 
     ENDIF ELSE IF count GT 1 THEN Message, 'Ambiguous keywords to evaluate.'
  ENDFOR
  
  ; Evaluate the keywords and, if necessary, replace the keyword value
  ; with the evaluated value. The evaluated keyword value must initially be a 
  ; STRING that can be evaluated with the EXECUTE command.
  FOR j=0,N_Elements(tags)-1 DO BEGIN
    index = Where(*theseTags EQ j, count)
    IF count EQ 0 THEN BEGIN
      IF N_Elements(mkeys) EQ 0 THEN BEGIN
        mkeys = Create_Struct(tags[j], keywords.(j)) 
      ENDIF ELSE BEGIN
        mkeys = Create_Struct(mkeys, tags[j], keywords.(j))
      ENDELSE
    ENDIF ELSE BEGIN
    
      ; If this is PostScript, use the alternative keyword value.
      tagValue = (!D.Name EQ 'PS') ? (*self.altps_keywords).(index) : keywords.(j)
      IF N_Elements(mkeys) EQ 0 THEN BEGIN
        mkeys = Create_Struct(tags[j], tagValue)
      ENDIF ELSE BEGIN
        mkeys = Create_Struct(mkeys, tags[j], tagValue)
      ENDELSE
    ENDELSE
    
  ENDFOR
  
  ; Evaluate string keywords for escape sequences (e.g, $\mu$).
  ;mkeys = cgWindow_Command -> ReplaceEscapeSequences(mkeys)
 
  Ptr_Free, theseTags
  RETURN, mkeys
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method lists the current command by printing a representation
; of the command in the command log window.
;     
; :Params:
;    prefix: in, optional, type=string
;       A prefix for the printed command (e.g., an index number, etc.).
;       
;-
PRO cgWindow_Command::List, prefix

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    cmdString = self.command
    CASE self.nparams OF
        0:
        1: cmdString = cmdString + ', p1'
        2: cmdString = cmdString + ', p1, p2'
        3: cmdString = cmdString + ', p1, p2, p3'
        4: cmdString = cmdString + ', p1, p2, p3, p4'
    ENDCASE
    IF Ptr_Valid(self.keywords) THEN BEGIN
        tags = Tag_Names(*self.keywords)
        FOR j=0,N_Elements(tags)-1 DO BEGIN
            cmdString = cmdString + ', ' + tags[j] + '=value'
        ENDFOR
    ENDIF
    
    IF N_Elements(prefix) NE 0 THEN prefix = '   ' + prefix + ' ' ELSE prefix = '   '
    
    Print, prefix + cmdString
END ;----------------------------------------------------------------------------------------------------------------

;+
; This method searches for escape sequences in text keywords and replaces
; them if necessary with the particular token from cgSymbol. To create an
; micrometer symbol, for example, you might specify an XTITLE keyword like
; this: XTITLE='Length ($\mu$M)'. The symbol name should be proceeded by
; a "$\" and ended with a "$".
; 
; :Params:
;     aString: in, required
;        Either a scalar or array of strings, or a structure of keyword parameters. 
;        If a structure, then the ReplaceEscapeSequences method is called recursively.
;-
FUNCTION cgWindow_Command::ReplaceEscapeSequences, aString

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        IF N_Elements(aString) EQ 0 THEN RETURN, "" ELSE RETURN, aString
    ENDIF
    
    ; Must have a parameter.
    IF N_Elements(aString) EQ 0 THEN Message, 'Must pass a string or structure of keywords.'
    
    ; What kind of thing is the parameter?
    type = Size(aString, /TNAME)
    
    ; If this is a structure of keyword parameters, sort though and call all string
    ; parameters recursively.
    IF type EQ "STRUCT" THEN BEGIN
    
        tags = Tag_Names(aString)
        FOR j=0,N_Elements(tags)-1 DO BEGIN
           type = Size(aString.(j), /TNAME)
           IF (type EQ 'STRING') || (type EQ 'STRUCT') || (type EQ 'POINTER') THEN BEGIN
              aString.(j) = self -> ReplaceEscapeSequences(aString.(j))
           ENDIF
        ENDFOR
        RETURN, aString
        
    ENDIF
    
    ; Is this a pointer type?
    IF type EQ 'POINTER' THEN BEGIN
    
       IF Size(*aString, /TNAME) EQ 'STRING' THEN *aString = self -> ReplaceEscapeSequences(*aString)
    
    ENDIF
    
    ; If this is a string, then do your thing.
    IF type EQ 'STRING' THEN BEGIN
    
        FOR j=0,N_Elements(aString)-1 DO BEGIN
            thisString = aString[j]
            thisString = cgCheckForSymbols(thisString)
            aString[j] = thisString
        ENDFOR
        
        RETURN, aString
        
    ENDIF

END ;----------------------------------------------------------------------------------------------------------------



;+
; The clean-up routine for the command object.
;-
PRO cgWindow_Command::Cleanup

    ; Need to destroy parameter objects?
    IF self.destroyobjects THEN BEGIN
       IF Ptr_Valid(self.p1) && Obj_Valid(*self.p1) THEN Obj_Destroy, *self.p1
       IF Ptr_Valid(self.p2) && Obj_Valid(*self.p2) THEN Obj_Destroy, *self.p2
       IF Ptr_Valid(self.p3) && Obj_Valid(*self.p3) THEN Obj_Destroy, *self.p3
       IF Ptr_Valid(self.p4) && Obj_Valid(*self.p4) THEN Obj_Destroy, *self.p4
    ENDIF
    Ptr_Free, self.p1
    Ptr_Free, self.p2
    Ptr_Free, self.p3
    Ptr_Free, self.p4
    Ptr_Free, self.keywords
    Ptr_Free, self.altps_keywords
    Ptr_Free, self.altps_params
END ;----------------------------------------------------------------------------------------------------------------


;+
; The initialization routine for the command object.
; 
; :Keywords:
;   altps_keywords: in, optional, type=structure
;       A structure of alternative keyword names and values to be used
;       when the PostScript device is the current graphics device.
;   altps_params: in, optional, type=structure
;       A structure of alternative parameters and values to be used
;       when the PostScript device is the current graphics device.
;   command: in, required, type=string
;       The command that is being stored in the command object.
;   destroyobjects: in, optional, type=boolean, default=0
;       If this keyword is set, and any of the input parameters p1-p4 is an object,
;       the object parameter will be destroyed when the window is destroyed.
;   keywords: in, optional, type=structure
;       A structure containing keyword:value pairs to be executed
;       with the command.
;   P1: in, optional, type=varies
;       The first positional parameter of the command being stored
;       in the structure.
;   P2: in, optional, type=varies
;       The second positional parameter of the command being stored
;       in the structure.
;   P3: in, optional, type=varies
;       The third positional parameter of the command being stored
;       in the structure.
;   P4: in, optional, type=varies
;       The fourth positional parameter of the command being stored
;       in the structure.
;   type: in, optional, type=integer, default=0
;       The type of command. 0 indicates a procedure. 1 indicates an object method.
;-
FUNCTION cgWindow_Command::INIT, $
    ALTPS_KEYWORDS=altps_keywords, $
    ALTPS_PARAMS=altps_params, $
    COMMAND=command, $
    DESTROYOBJECTS=destroyObjects, $
    KEYWORDS=keywords, $
    P1=p1, P2=p2, P3=p3, P4=p4, $
    TYPE=type
    
    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, 0
    ENDIF

    self.command = command
    IF N_Elements(p1) NE 0 THEN self.p1 = Ptr_New(p1)
    IF N_Elements(p2) NE 0 THEN self.p2 = Ptr_New(p2)
    IF N_Elements(p3) NE 0 THEN self.p3 = Ptr_New(p3)
    IF N_Elements(p4) NE 0 THEN self.p4 = Ptr_New(p4)
    IF N_Elements(keywords) NE 0 THEN self.keywords = Ptr_New(keywords)
    IF N_Elements(altps_keywords) NE 0 THEN self.altps_keywords = Ptr_New(altps_keywords)
    IF N_Elements(altps_params) NE 0 THEN self.altps_params = Ptr_New(altps_params)
    self.destroyobjects = Keyword_Set(destroyObjects)
    self.type = Keyword_Set(type)
    self.nparams = (N_Elements(p1) NE 0) + (N_Elements(p2) NE 0) + (N_Elements(p3) NE 0) + (N_Elements(p4) NE 0)
    RETURN, 1
    
END ;----------------------------------------------------------------------------------------------------------------

;+
; The structure definition module for structure that is stored
; on the window manager list.
;-
PRO cgWindow_ID__Define

   struct = { cgWINDOW_ID, $
                 tlb: 0L, $
                 wid: 0L, $
                 title: "", $
                 windowObj: Obj_New() $
             }
END ;----------------------------------------------------------------------------------------------------------------

;+
; The clean-up method for the cgCmdWindow object.
;-
PRO cgCmdWindow_Cleanup, tlb
    Widget_Control, tlb, Get_UValue=self
    IF Obj_Valid(self) THEN Obj_Destroy, self
END ;----------------------------------------------------------------------------------------------------------------


;+
; The class definition module for the cgWindow_Command object.
;-
PRO cgWindow_Command__Define

   ; The definition of the command object.
   class = { cgWindow_Command, $
              command: "", $             ; The command to execute.
              p1: Ptr_New(), $           ; The first parameter.
              p2: Ptr_New(), $           ; The second parameter.
              p3: Ptr_New(), $           ; The third parameter.
              p4: Ptr_New(), $           ; The fourth parameter.
              nparams: 0, $              ; The number of parameters.
              keywords: Ptr_New(), $     ; The command keywords.
              destroyobjects: 0B, $      ; A flag to destroy parameter objects upon exit.
              altps_keywords: Ptr_New(), $ ; Structure of keywords to evaluate at run-time.
              altps_params: Ptr_New(), $  ; Structure of parameters to evaluate at run-time.
              type: 0 $                  ; =0 call_procedure =1 call_method
            }
END ;----------------------------------------------------------------------------------------------------------------

;+
; Adds a command object of class cgWINDOW_COMMAND to the command list 
; maintained by the window.
;
; :Params:
;     command: in, required, type=object
;         A command object of class cgWINDOW_COMMAND.
;         
; :Keywords:
;     index: in, optional, type=integer
;         The index number of where the command should be added in the command list.
;         The command is added to the end of the command list by default.
;-
PRO cgCmdWindow::AddCommand, command,  INDEX=index

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; If the command is a valid object, add it to the command list.
    IF Obj_Valid(command) THEN self.cmds -> Add, command, index, /Before

END ;----------------------------------------------------------------------------------------------------------------


;+
; Provides a programmatic way to create a PostScript file from the window.
; Call by setting the CREATE_PS keyword with cgControl.
;
; :Params:
;     filename:  in, required, type=string
;         The name of the PostScript file to generate.
;-
PRO cgCmdWindow::AutoPostScriptFile, filename

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        
        ; Close the PostScript file.
        cgPS_Close, /NoFix     

        ; Set the window index number back.
        IF N_Elements(currentWindow) NE 0 THEN BEGIN
            IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1
        ENDIF
        
        RETURN
    ENDIF
    
    ; Make this window the current graphics windows.
    currentWindow = !D.Window
    WSet, self.wid

    IF N_Elements(filename) EQ 0 THEN filename='cgwindow.ps'

    ; Allow the user to configure the PostScript file.
    cgPS_Open, GUI=0, $
        FILENAME=filename, $
        DECOMPOSED=self.ps_decomposed, $
        EUROPEAN=self.ps_metric, $
        ENCAPSULATED=self.ps_encapsulated, $
        GROUP_LEADER=self.tlb, $
        SCALE_FACTOR=self.ps_scale_factor, $
        CHARSIZE=self.ps_charsize, $
        FONT=self.ps_font, $
        QUIET=self.ps_quiet, $
        TT_FONT=self.ps_tt_font
    
    ; Execute the graphics commands.
    self -> ExecuteCommands
    
    ; Clean up.
    cgPS_Close, NOMESSAGE=self.ps_quiet

    ; Set the window index number back.
    IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1

END ;----------------------------------------------------------------------------------------------------------------


;+
; Provides a programmatic way to create a raster file from the window.
; Call by setting the create_png, etc. keyword with cgControl.
;
; :Params:
;     filetype:  in, required, type=string
;         The type of raster file (e.g., PNG, JPEG, etc.).
;     filename:  in, required, type=string
;         The name of the output file.
;-
PRO cgCmdWindow::AutoRasterFile, filetype, filename

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        
        ; Close the PostScript file.
        cgPS_Close, /NoFix     

        ; Set the window index number back.
        IF N_Elements(currentWindow) NE 0 THEN BEGIN
            IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1
        ENDIF
        
        RETURN
    ENDIF
    
    ; Make this window the current graphics windows.
    currentWindow = !D.Window
    WSet, self.wid

    IF N_Elements(filetype) EQ 0 then filetype = 'PNG'
    IF N_Elements(filename) EQ 0 THEN filename = 'cgwindow.' + StrLowCase(filetype)
    IF StrUpCase(filetype) EQ 'PDF' THEN rastertype = -1 ELSE rastertype = self.im_raster
    
    ; Strip the extension off the filename.
    outname = cgRootName(filename, DIRECTORY=dirName)
    
    ; Put it back together without an extension.
    outputFilename = Filepath(ROOT_DIR=dirName, outname)
    
    ; What kind of raster file.
    CASE rasterType OF
    
        ; PDF File.
       -1: BEGIN
       
           thisname = outputFilename + '.ps'
           outname = outputFilename + '.pdf'
           cgPS_Open, $
                DECOMPOSED=self.ps_decomposed, $
                FILENAME=thisname, $
                GROUP_LEADER=self.tlb, $
                METRIC=self.ps_metric, $
                KEYWORDS=keywords, $ ; Returned cgPS_Config keywords.
                SCALE_FACTOR=self.ps_scale_factor, $
                CHARSIZE=self.ps_charsize, $
                FONT=self.ps_font, $
                QUIET=1, $
                TT_FONT=self.ps_tt_font
                           
           ; Draw the graphics.
           self -> ExecuteCommands
           
           ; Close the file and make a PDF file.
           cgPS_Close
           cgPS2PDF, thisname, outname, DELETE_PS=self.ps_delete, /SILENT, SUCCESS=success, $
              UNIX_CONVERT_CMD=self.pdf_unix_convert_cmd, GS_PATH=self.pdf_path
           IF ~success THEN BEGIN
              Message, 'Unable to create PDF file. See cgPS2PDF documentation.'
           ENDIF ELSE BEGIN
              IF ~self.ps_quiet THEN Print, 'PDF output will be created here: ' + outname
           ENDELSE
           END
    
        ; Normal raster.
        0: BEGIN
           void = cgSnapshot(TYPE=fileType, FILENAME=outputFilename, /NODIALOG)
           Print, 'Output file is located here: ' + outputFilename 
           END
           
        ; Raster via ImageMagick.
        1: BEGIN
        
           ; Create a PostScript file first.
           thisname = outputFilename + '.ps'
           cgPS_Open, $
                DECOMPOSED=self.ps_decomposed, $
                FILENAME=thisname, $
                GROUP_LEADER=self.tlb, $
                METRIC=self.ps_metric, $
                KEYWORDS=keywords, $ ; Returned cgPS_Config keywords.
                SCALE_FACTOR=self.ps_scale_factor, $
                CHARSIZE=self.ps_charsize, $
                FONT=self.ps_font, $
                QUIET=1, $
                TT_FONT=self.ps_tt_font
                
           ; Cannot successfully convert encapsulated landscape file to raster.
           ; Limitation of ImageMagick, and specifically, GhostScript, which does
           ; the conversion.
           IF keywords.encapsulated && keywords.landscape THEN BEGIN
                Message, 'ImageMagick cannot successfully convert an encapsulated ' + $
                         'PostScript file in landscape mode to a raster file. Returning...'
           ENDIF
           
           ; Draw the graphics.
           self -> ExecuteCommands

           ; Close the file and convert to proper file type.
            CASE filetype OF
                'BMP':  cgPS_Close, /BMP, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width, NOMESSAGE=self.ps_quiet
                'GIF':  cgPS_Close, /GIF, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width, NOMESSAGE=self.ps_quiet
                'JPEG': cgPS_Close, /JPEG, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width, NOMESSAGE=self.ps_quiet
                'PNG':  cgPS_Close, /PNG,  DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width, NOMESSAGE=self.ps_quiet
                'TIFF': cgPS_Close, /TIFF, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width, NOMESSAGE=self.ps_quiet
           ENDCASE
           ;IF ~self.ps_quiet THEN Print, 'Output file is located here: ' + outfilename
           END
    
    ENDCASE

    ; Set the window index number back.
    IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method copies the contents of the draw widget to the current graphics window using
; the DEVICE COPY method. The DEVICE COPY command looks like this::
;
;    DEVICE, COPY=[origin[0], origin[1], extent[0], extent[1], destination[0], destination[1], self.wid]
;
; If the IMAGE keyword is used, the window contents are stored in a band-interleaved image variable
; and the window contents are not copied into the current graphics window.
;
; :Keywords:
;    destination: in, optional, type=intarr(2)
;        A two-element array specifying the device coordinates of the lower-left
;        corner of the copied region in the destination window. By default, [0,0].
;    extent: in, optional, type=intarr(2)
;       A two-element array specifying the number of columns and rows to copy.
;       If missing, the entire draw widget window is copied. By default, [!D.X_Size, !D.Y_Size].
;    image: out, optional, type=byte
;       Set this keyword to a named IDL variable that returns a copy of the draw
;       widget contents as a band interleaved (MxNx3) image. If this keyword is set
;       nothing is copied from the window.
;    origin: in, optional, type=intarr(2)
;       A two-element array specifying the device coordinates of the lower-left
;       corner of region in the draw widget window to be copied. By default, [0,0].
;-
PRO cgCmdWindow::Copy, $
   DESTINATION=dest, $
   EXTENT=extent,             $
   IMAGE=image,               $
   ORIGIN=origin

   Compile_Opt idl2
    
   ; Error handling.
   Catch, theError
   IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
   ENDIF
    
   ; Get the window ID of the window we are copying to.
   destination = !D.Window
   
   ; Make the draw widget the current graphics window.
   WSet, self.wid

   ; Check the input parameters
   IF (N_Elements(origin) NE 2) THEN origin = [0, 0]
   IF (N_Elements(extent) NE 2) THEN extent = [!D.X_Size, !D.Y_Size]
   IF (N_Elements(dest  ) NE 2) THEN dest   = [0, 0]

   ; If we're using an output variable, capture the current window
   IF Arg_Present(image) THEN BEGIN
      image = cgSnapshot(origin[0], origin[1], extent[0], extent[1], TRUE=3)
      IF (destination NE -1) THEN WSet, destination
   ENDIF ELSE BEGIN
      IF (destination NE -1) THEN WSet, destination ELSE Message, 'No current window to copy into.'
      DEVICE, COPY=[origin[0], origin[1], extent[0], extent[1], dest[0], dest[1], self.wid]
   ENDELSE
   
   
END


;+
; This method allows you to turn draw widget events on and off and to set
; the name of an event handler procedure to accept these widget events.
; An event handler procedure must be defined to get the events processed
; correctly. 
; 
; :Keywords:
;     button_events: in, optional, type=boolean
;         Set this keyword to turn button events on for the draw widget.
;     drop_events: in, optional, type=boolean
;         Set this keyword to turn drop events on for the draw widget.
;     event_handler: in, optional, type=string
;         The name of an event handler procedure to accept events from
;         the draw widget. Write the event handler procedure with two
;         positional parameters. The first will be the event structure
;         returned from the draw widget. The second will be the cgCmdWindow
;         object reference (i.e., self) to allow you to manipulate the
;         command window.
;     keyboard_events: in, optional, type=boolean
;         Set this keyword to turn keyboard events on for the draw widget.
;     motion_events: in, optional, type=boolean
;         Set this keyword to turn motion events on for the draw widget.
;     storage: in, optional, type=any
;         Any user-defined IDL variable will be stored in the object storage location.
;         Defined here for convenience. Same as `Storage` keyword for the SetProperty method.
;     tracking_events: in, optional, type=boolean
;         Set this keyword to turn tracking events on for the draw widget.
;     wheel_events: in, optional, type=boolean
;         Set this keyword to turn wheel events on for the draw widget.
;-
PRO cgCmdWindow::DrawWidgetConfig, $
   Button_Events=button_events, $
   Drop_Events=drop_events, $
   Event_Handler=event_handler, $
   Keyboard_Events=keyboard_events, $
   Motion_Events=motion_events, $
   Storage=storage, $
   Tracking_Events=tracking_events, $
   Wheel_Events=wheel_events

   IF N_Elements(button_events) NE 0 THEN $
      Widget_Control, self.drawID, Draw_Button_Events=Keyword_Set(button_events)
   IF N_Elements(drop_events) NE 0 THEN $
      Widget_Control, self.drawID, Draw_Drop_Events=Keyword_Set(drop_events)
   IF N_Elements(event_handler) NE 0 THEN self.event_handler = event_handler
   IF N_Elements(keyboard_events) NE 0 THEN $
      Widget_Control, self.drawID, Draw_Keyboard_Events=Keyword_Set(keyboard_events)
   IF N_Elements(motion_events) NE 0 THEN $
      Widget_Control, self.drawID, Draw_Motion_Events=Keyword_Set(motion_events)
   IF N_Elements(storage) NE 0 THEN self -> SetProperty, Storage=storage
   IF N_Elements(tracking_events) NE 0 THEN $
      Widget_Control, self.drawID, Draw_Tracking_Events=Keyword_Set(tracking_events)
   IF N_Elements(wheel_events) NE 0 THEN $
      Widget_Control, self.drawID, Draw_Wheel_Events=Keyword_Set(wheel_events)
   
END



;+
; This event handler processes draw widget events and sends the events
; to a user-defined event handler, if such and event handler exists.
; The event handler must be defined to accept two positional parameters.
; The first is the event structure returned by the draw widget, and the
; second is the cgCmdWindow object reference.
; 
; :Params:
;     event: in, required, type=structure
;         An event structure.
;-
PRO cgCmdWindow::DrawWidgetEvents, event

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF
    
    ; Make the draw widget the current graphics window.
    WSet, self.wid

    ; Pass the event on to the event handler, if there is one.
    IF self.event_handler NE "" THEN BEGIN
       Call_Procedure, self.event_handler, event
    ENDIF ELSE Message, 'No event handler has been specified for draw widget events from cgCmdWindow.'
    
END



;+
; Sends the window commands to a PostScript file.
;
; :Params:
;     event: in, required, type=structure
;         An event structure.
;-
PRO cgCmdWindow::CreatePostScriptFile, event

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        
        ; Close the PostScript file.
        cgPS_Close, /NoFix     

        ; Set the window index number back.
        IF N_Elements(currentWindow) NE 0 THEN BEGIN
            IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1
        ENDIF
        
        RETURN
    ENDIF
    
    ; Make this window the current graphics windows
    currentWindow = !D.Window
    WSet, self.wid
    
    ; Construct a file name, if you have one.
    ext = self.ps_encapsulated ? '.eps' : '.ps'
    IF self.lastWriteFile NE "" THEN BEGIN
        filename = Filepath(ROOT_DIR=self.lastWriteDir, self.lastWriteFile + ext)
    ENDIF ELSE BEGIN
         CD, CURRENT=thisDir
         filename = Filepath(ROOT_DIR=thisDir, 'cgwindow' + ext)
    ENDELSE

    ; Allow the user to configure the PostScript file.
    cgPS_Open, /GUI, $
        CANCEL=cancelled, $
        CHARSIZE=self.ps_charsize, $
        DECOMPOSED=self.ps_decomposed, $
        EUROPEAN=self.ps_metric, $
        ENCAPSULATED=self.ps_encapsulated, $
        FILENAME=filename, $
        FONT=self.ps_font, $
        GROUP_LEADER=self.tlb, $
        KEYWORDS=keywords, $
        SCALE_FACTOR=self.ps_scale_factor, $
        QUIET=self.ps_quiet, $
        TT_FONT=self.ps_tt_font
    IF cancelled THEN RETURN
    
    ; Save the name of the last output file.
    self.lastWriteDir = File_DirName(keywords.filename)
    self.lastWriteFile = cgRootName(keywords.filename)
    
    ; Execute the graphics commands.
    self -> ExecuteCommands
    
    ; Clean up.
    cgPS_Close
    
    ; Set the window index number back.
    IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1

END ;----------------------------------------------------------------------------------------------------------------

;+
; Deletes a command object from the command list maintained by the window.
;
; :Params:
;     cmdIndex: in, optional, type=integer
;         The zero-based index of the command to delete. If undefined, the
;         index of the last command in the window is used.
;
; :Keywords:
;     all: in, optional, type=boolean
;         If set, all the commands in the command list are deleted.
;-
PRO cgCmdWindow::DeleteCommand, cmdIndex, ALL=all

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Get the command count.
    count = self.cmds -> Get_Count()
    
    ; Delete all the commands? Is so, delete and RETURN.
    IF Keyword_Set(all) THEN BEGIN
        self.cmds -> Delete_Nodes, /DESTROY
        self -> ExecuteCommands
        RETURN
    ENDIF
    
    ; Need a command index?
    IF N_Elements(cmdIndex) EQ 0 THEN BEGIN
        cmdIndex = count - 1
    ENDIF 
    
    ; Do we have a command with this command number?
    IF cmdIndex GT (count-1) THEN Message, 'A command with index ' + StrTrim(cmdIndex,2) + ' does not exist.'
    
    IF cmdIndex GE 0 THEN BEGIN
        self.cmds -> Delete, cmdIndex, /Destroy
        self -> ExecuteCommands
    ENDIF ELSE Message, 'A negative command index number is not allowed.'
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; All widget events come here and are dispatched to the proper object method.
;
; :Params:
;     event: in, required, type=structure
;         An event structure.
;-
PRO cgCmdWindow_Dispatch_Events, event

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Get the window object.
    Widget_Control, event.top, GET_UVALUE=self
    
    ; Get the event type. If the type is an object, then
    ; this must be a resize event from the TLB.
    eventType = Widget_Info(event.id, /UNAME)
    
    ; Dispatch the events to the proper method.
    CASE eventType OF   
        'DRAW_WIDGET': self -> DrawWidgetEvents, event
        'IMAGEMAGICK_BMP': self -> SaveAsRaster, event
        'IMAGEMAGICK_GIF': self -> SaveAsRaster, event
        'IMAGEMAGICK_JPEG': self -> SaveAsRaster, event
        'IMAGEMAGICK_PNG': self -> SaveAsRaster, event
        'IMAGEMAGICK_TIFF': self -> SaveAsRaster, event
        'POSTSCRIPT': self -> CreatePostscriptFile, event
        'PDF': self -> SaveAsRaster, event
        'RASTER_BMP': self -> SaveAsRaster, event
        'RASTER_GIF': self -> SaveAsRaster, event
        'RASTER_JPEG': self -> SaveAsRaster, event
        'RASTER_PNG': self -> SaveAsRaster, event
        'RASTER_TIFF': self -> SaveAsRaster, event
        'RESTORECOMMANDS': self -> RestoreCommands
        'SAVECOMMANDS': self -> SaveCommands
        'TLB_RESIZE': self -> Resize, event.x, event.y
        'QUIT': Obj_Destroy, self
        ELSE: Message, 'Unknown type of event: ' + eventtype + '. Cannot dispatch properly.'
    ENDCASE
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method executes the commands on the command list.
;-
PRO cgCmdWindow::ExecuteCommands

    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        !P.Multi = thisMulti
        !X.OMargin = thisXOmargin
        !Y.OMargin = thisYOmargin
        IF N_Elements(rr) NE 0 THEN TVLCT, rr, gg, bb
        IF (!D.Flags AND 256) NE 0 THEN WSet, -1
        RETURN
    ENDIF
    
    ; Make sure you are suppose to be executing these commands.
    IF Keyword_Set(self.noExecuteCommands) THEN RETURN
    
    ; Store the current !P.MULTI state.
    thisMulti = !P.Multi
    thisXOmargin = !X.OMargin
    thisYOmargin = !Y.OMargin

    ; Make this window the current graphics window.
    IF (!D.Flags AND 256) NE 0 THEN BEGIN
        currentWindow = !D.Window
        WSet, self.wid
    ENDIF
    
    ; Get the current color table vectors so they can be restored.
    TVLCT, rr, gg, bb, /GET
    
    ; Load the color vectors.
    TVLCT, *self.r, *self.g, *self.b
    
    ; Erase the window.
    IF self.eraseit THEN cgErase, *self.background 
    
    ; Are we doing multiple commands?
    IF Total(self.pmulti) NE 0 THEN !P.Multi = self.pmulti
    IF Total(self.xomargin) NE 0 THEN !X.OMargin = self.xomargin
    IF Total(self.yomargin) NE 0 THEN !Y.OMargin = self.yomargin

    ; How many commands are there?
    n_cmds = self.cmds -> Get_Count()
    
    ; Execute the commands.
    FOR j=0,n_cmds-1 DO BEGIN
        thisCmdObj = self.cmds -> Get_Item(j, /DEREFERENCE)
        
        ; Execute the command. 
        thisCmdObj -> Draw, SUCCESS=success
        
        ; Did you successfully draw this command?
        IF ~success THEN BEGIN
        
            thisCmdObj -> List
            answer = Dialog_Message('Problem executing the command shown ' + $
                                    'in the console output. Delete command?', /QUESTION)
            IF StrUpCase(answer) EQ 'YES' THEN BEGIN
                self -> DeleteCommand, j
                !P.Multi = thisMulti
                !X.OMargin = thisXOmargin
                !Y.OMargin = thisYOmargin
                IF N_Elements(rr) NE 0 THEN TVLCT, rr, gg, bb
                IF (!D.Flags AND 256) NE 0 THEN WSet, -1
                RETURN
            ENDIF
        
        ENDIF
        
        ; Need to delay?
        IF self.delay NE 0 THEN Wait, self.delay
    ENDFOR
    
    ; Save the current data coordinate system.
    self -> SaveDataCoords
    
    ; If there are no commands, then just erase the window.
    IF n_cmds EQ 0 THEN cgErase, *self.background 
    
    ; Restore the colors in effect when we entered.
    TVLCT, rr, gg, bb
    
    ; Set the !P.Multi and outside margin system variables back to its original values.
    !P.Multi = thisMulti
    !X.OMargin = thisXOmargin
    !Y.OMargin = thisYOmargin
    
    ; Reset the current graphics window, if possible.
    IF (!D.Flags AND 256) NE 0 THEN BEGIN
       IF (currentWindow GE 0) && WindowAvailable(currentWindow) THEN BEGIN
            WSet, currentWindow
       ENDIF ELSE WSet, -1
    ENDIF

END ;----------------------------------------------------------------------------------------------------------------


;+
; This method retrieves the value of a keyword from a particular command,
; if it can.
;     
; :Params:
;     keyword: in, required, type=string
;        The name of the keyword you would like to retrieve from the command.
;     cmdindex: in, required, type=integer
;        The index number of the command you wish to retrieve the keyword from.
;        
; :Keywords:
;     success: out, optional, type=boolean
;         If the program can successfully get the keyword, this variable will
;         contain the value 1. Otherwise, it will contain the value 0.
;-
FUNCTION cgCmdWindow::GetCommandKeyword, keyword, cmdIndex, SUCCESS=success

    Compile_Opt idl2

    Catch, theError
    IF theError NE 0 THEN BEGIN
        void = cgErrorMsg()
        success = 0
        RETURN, success
    ENDIF
    
    ; Assume failure.
    success = 0

    IF N_Params() NE 2 THEN BEGIN
        Print, 'Useage: keywordValue = obj -> GetCommandKeyword(keywordName, commandIndex)'
        RETURN, ""
    ENDIF
    IF Size(keyword, /TNAME) NE 'STRING' THEN Message, 'Keyword name must be a string variable.'
    
    ; Make sure command index is in the range.
    cmdIndex = 0 > cmdIndex < ((self.cmds -> Get_Count()) - 1)
    
    ; Get the keywords for the proper command.
    cmdObject = self.cmds -> Get_Item(cmdIndex, /DEREFERENCE)
    keywordStruct = cmdObject -> Get_Keywords(HAS_KEYWORDS=has_keywords)
    
    ; Does this command have keywords?
    retValue = ""
    IF has_keywords THEN BEGIN
        tagNames = Tag_Names(keywordStruct)
        keyIndex = Where(tagNames EQ StrUpCase(keyword), nameCount)
        IF nameCount GT 0 THEN BEGIN
            retvalue = keywordStruct.(nameCount)
            success = 1
        ENDIF ELSE Message, 'This command has no keyword named: ' + keyword
    ENDIF ELSE Message, 'This command has no keywords.'

    RETURN, retValue
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method retrieves properties from the object.
; 
; :Keywords:
;     adjustsize: out, optional, type=boolean
;         Set this keyword to adjust default character size to the display window size.
;     background: out, optional, type=string
;         The background color of the window. Only use if the ERASEIT property is also set.
;     commands: out, optional
;         A list of the commands stored in the window.
;     delay: out, optional, type=float
;         Set this keyword to the amount of "delay" you want between commands in the command list.
;     dimensions: out, optional, type=intarr(2)
;         Set this keyword to a two-element array giving the xsize and ysize
;         of the draw widget.
;     eraseit: out, optional, type=boolean
;         If this property is set, the cgWindow erases with the background color before
;         displaying the commands in the window's command list.
;     im_density: out, optional, type=integer, default=300
;         Set this keyword to the sampling density when ImageMagick creates raster image
;         file from PostScript outout.
;     im_options: out, optional, type=string, default=""
;         Set this keyword to any ImageMagick options you would like to pass along to the
;         ImageMagick convert command when creating raster image files from PostScript output.
;     im_resize: out, optional, type=integer, default=25
;         Set this keyword to percentage that the raster image file created my ImageMagick
;         from PostScript output should be resized.
;     im_raster: out, optional, type=boolean, default=1
;         Set this keyword to zero to create raster files using the create_png etc. keywords
;         directly, instead of via ImageMagick.
;     im_transparent: out, optional, type=boolean, default=0
;         Set this keyword to allow ImageMagick to create transparent backgrounds when it
;         makes raster image files from PostScript output.
;     multi: out, optional, type=Intarr(5)
;         Set this keyword to the !P.MULTI setting you want to use for the window.
;         !P.MULTI is set to this setting before command execution, and set back to
;         it's default value when the commands are finished executing.
;     noexecutecommands: out, optional, type=boolean, default=0
;         Set this keyword to 1 to prevent the window from executing the commands and to
;         0 if you want the window to execute the commands.
;     palette: out, optional, type=byte
;         Use this keyword to pass in an N-by-3 (or 3-by-N) byte array containing the
;         R, G, and B vectors of a color table. It is probably easier to use cgLoadCT or
;         XCOLORS to load color tables for the window, but this is provided as another option.
;     pdf_path: out, optional, type=string
;         Set this keyword to the name of the path to the Ghostscript command for converting PS to PDF.
;     pdf_unix_convert_cmd: out, optional, type=string
;         Set this keyword to the name of an alternative UNIX command to convert PostScript to PDF.
;     ps_charsize: out, optional, type=float
;         The PostScript character size.
;     ps_decomposed: out, optional, type=boolean, default=0
;         Set this keyword to zero to set the PostScript color mode to indexed color and to
;         one to set the PostScript color mode to decomposed color.
;     ps_delete: out, optional, type=boolean, default=1
;         Set this keyword to zero if you want to keep the PostScript output ImageMagick creates
;         when making raster file output.
;     ps_encapsulated: out, optional, type=boolean, default=0
;         Set this keyword to configure cgPS_Config to produce encapsulated PostScript output by default.
;     ps_font: out, optional, type=integer
;         Set this keyword to the type of font you want to use in PostScript output. It sets the 
;         FONT keyword on the cgPS_Config command. Normally, 0 (hardware fonts) or 1 (true-type fonts).
;     ps_metric: out, optional, type=boolean, default=0
;         Set this keyword to configure cgPS_Config to use metric values and A4 page size in its interface.
;     ps_quiet: out, optional, type=boolean, default=0
;         Set this keyword to set the QUIET keyword on cgPS_Open.
;     ps_scale_factor: out, optional, type=float
;         Set his keyword to the PostScript scale factor you wish to use in creating PostScript output.
;     ps_tt_font: out, optional, type=string
;         Set this keyword to the name of a true-type font to use in creating PostScript output.
;     storage: out, optional
;         An IDL variable that was defined and stored here by the program's user .
;     tlb: out, optional, type=long
;         The widget identifier of the top-level base widget.
;     wid: out, optional, type=integer
;         The window index number of the draw widget.
;     xomargin: out, optional, type=intarr(2)
;         Sets the !X.OMargin system variable when multiple plots are displayed in the window.
;     yomargin: out, optional, type=intarr(2)
;         Sets the !Y.OMargin system variable when multiple plots are displayed in the window.
;-
PRO cgCmdWindow::GetProperty, $
    ADJUSTSIZE=adjustsize, $
    BACKGROUND=background, $
    COMMANDS=commands, $
    DELAY=delay, $
    DIMENSIONS=dimensions, $
    ERASEIT=eraseit, $
    IM_DENSITY=im_density, $                      ; Sets the density parameter on ImageMagick convert command.
    IM_RESIZE=im_resize, $                        ; Sets the resize parameter on ImageMagick convert command.
    IM_OPTIONS=im_options, $                      ; Sets extra ImageMagick options on the ImageMagick convert command.
    IM_RASTER=im_raster, $                        ; Sets whether to generate raster files via ImageMagick
    IM_TRANSPARENT=im_transparent, $  ; Sets the "alpha" keyword on ImageMagick convert command.
    NOEXECUTECOMMANDS=noExecuteCommands, $ ; Set if you want commands to execute commands.
    MULTI=multi, $
    PALETTE=palette, $
    PDF_PATH=pdf_path, $                          ; The path to the Ghostscript conversion command.
    PDF_UNIX_CONVERT_CMD=pdf_unix_convert_cmd, $  ; Command to convert PS to PDF.
    PS_CHARSIZE=ps_charsize, $                    ; Select the character size for PostScript output.
    PS_DECOMPOSED=ps_decomposed, $
    PS_DELETE=ps_delete, $
    PS_ENCAPSULATED=ps_encapsulated, $
    PS_FONT=ps_font, $                            ; Select the font for PostScript output.
    PS_METRIC=ps_metric, $
    PS_SCALE_FACTOR=ps_scale_factor, $            ; Select the scale factor for PostScript output.
    PS_QUIET=ps_quiet, $
    PS_TT_FONT=ps_tt_font, $                      ; Select the true-type font to use for PostScript output.
    TLB=tlb, $
    STORAGE=storage, $
    WID=wid, $
    XOMARGIN=xomargin, $
    YOMARGIN=yomargin, $
    _EXTRA=extra
    
    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Window properties.
    IF Arg_Present(adjustsize) THEN adjustsize = self.adjustsize
    IF Arg_Present(background) THEN background = *self.background
    IF Arg_Present(palette) THEN BEGIN
        len = N_Elements(*self.r)
        palette = BytArr(len,3)
        palette[*,0] = *self.r
        palette[*,1] = *self.g
        palette[*,2] = *self.b
    ENDIF
    IF Arg_Present(commands) THEN commands = self.cmds
    IF Arg_Present(dimensions) THEN BEGIN
        thisWindow = !D.Window
        WSet, self.wid
        dimensions = [!D.X_Size, !D.Y_Size]
        IF thisWindow GE 0 THEN WSet, thisWindow
    ENDIF
    IF Arg_Present(delay) THEN delay = self.delay
    IF Arg_Present(eraseit) THEN eraseit = self.eraseit
    IF Arg_Present(multi) THEN multi = self.pmulti
    IF Arg_Present(tlb) THEN tlb = self.tlb
    IF Arg_Present(storage) THEN IF Ptr_Valid(self.storage) THEN storage = *self.storage
    IF Arg_Present(wid) THEN wid = self.wid
    IF Arg_Present(xomargin) THEN xomargin = self.xomargin
    IF Arg_Present(yomargin) THEN yomargin = self.yomargin
    
     ; PDF properties.
     pdf_unix_convert_cmd = self.pdf_unix_convert_cmd
     pdf_path = self.pdf_path
    
     ; PostScript properties.
     ps_charsize = self.ps_charsize
     ps_decomposed = self.ps_decomposed
     ps_delete = self.ps_delete
     ps_encapsulated = self.ps_encapsulated
     ps_metric = self.ps_metric
     ps_font = self.ps_font
     ps_quiet = self.ps_quiet
     ps_scale_factor = self.ps_scale_factor
     ps_tt_font = self.ps_tt_font
     
     ; ImageMagick properties.
     im_transparent = self.im_transparent
     im_density = self.im_density
     im_options = self.im_options
     im_resize = self.im_resize
     im_raster = self.im_raster
     
     noExecuteCommands = self.noExecuteCommands
     
    ; You may have gotten keywords you don't know what to deal with. Inform
    ; the user there.
    IF N_Elements(extra) NE 0 THEN BEGIN
       tags = Tag_Names(extra)
       FOR j=0,N_Elements(tags)-1 DO BEGIN
          Message, 'The following property could NOT be obtained in the GetProperty method: ' + tags[j], /Informational
       ENDFOR
    ENDIF
END ;----------------------------------------------------------------------------------------------------------------


;+
; :Private:
;     This method invalidates a widget ID. It is used for restored
;     visualizations, so that the current window doesn't get inadvertenly
;     destroyed by a widget identifier from an old program.
;-
PRO cgCmdWindow::InvalidateWidgetID
    self.tlb = -1
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method lists the command indicated by the command index. In the
; absence of the command index, all commands are listed.
;
; :Params:
;      cmdindex: in, optional, type=index
;         The index number of the command you wish to list. If not supplied,
;         all the commands in the window are listed.
; :Keywords:
;       createcommandstruct: out, optional, type=structure
;          Set to a named variable to return a command structure of this command.
;-
PRO cgCmdWindow::ListCommand, cmdIndex, CREATECOMMANDSTRUCT=createCommandStruct

    Compile_Opt idl2

    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Need a command structure?
    createCommandStruct = Keyword_Set(createCommandStruct)

    ; How many commands are there?
    count = self.cmds -> Get_Count()

    IF N_Elements(cmdIndex) EQ 0 THEN BEGIN
        FOR j = 0, count-1 DO BEGIN
            thisCmdObj = self.cmds -> Get_Item(j, /DEREFERENCE)

            ; Preface the commands with their index number.
            thisCmdObj -> List, StrTrim(j,2) + '.'

            ; Create the command struct
            IF createCommandStruct THEN thisCmdObj -> CreateCommandStruct, 'cmd' + StrTrim(j,2)
        ENDFOR
    ENDIF ELSE BEGIN
        IF cmdIndex LT (count-1) THEN BEGIN
            thisCmdObj = self.cmds -> Get_Item(cmdIndex, /DEREFERENCE)

            ; Preface the commands with their index number.
            thisCmdObj -> List, StrTrim(cmdIndex,2) + '.'

            ; Create the command struct
            IF createCommandStruct THEN thisCmdObj -> CreateCommandStruct, 'cmd' + StrTrim(cmdIndex,2)
        ENDIF ELSE Message, 'The command index is out of range of the number of commands.'
    ENDELSE

END ;----------------------------------------------------------------------------------------------------------------


;+
; This method loads color table vectors into the program.
; The XCOLORS_DATA keyword is required to get color vector
; information from XCOLORS.
;  
; :Params:
;     r: in, optional, type=bytarr(256)
;        The red color vector.
;     g: in, optional, type=bytarr(256)
;        The green color vector.
;     b: in, optional, type=bytarr(256)
;        The blue color vector.
;        
; :Keywords:
;      xcolors_data: in, optional, type=structure
;         The information XColors sends to an object when colors are changed
;         in XColors.
; 
;-
PRO cgCmdWindow::LoadColors, r, g, b, XCOLORS_DATA=colorData

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Load the vectors
    IF N_Elements(r) NE 0 THEN *self.r = r
    IF N_Elements(g) NE 0 THEN *self.g = g
    IF N_Elements(b) NE 0 THEN *self.b = b
    
    IF N_Elements(colorData) NE 0 THEN BEGIN
       *self.r = colorData.r
       *self.g = colorData.g
       *self.b = colorData.b
    ENDIF
    
    ; Execute the commands.
    self -> ExecuteCommands
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method creates PostScript, PDF, BMP, GIF, JPEG, PNG, and TIFF file output
; from the pixmap window contents. The method assumes ImageMagick and Ghostscript
; are installed correctly.
;
; :Params:
;    filename: in, optional, type=string, default='idl.ps'
;        The name of the output file. The type of file is determined from the
;        file name extension.
;-
PRO cgCmdWindow::Output, filename

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF
    
    ; Need a filename?
    IF N_Elements(filename) EQ 0 THEN filename = 'idl.ps'
    
    ; The type of file is determined by the filename extension.
    rootname = cgRootName(filename, DIRECTORY=dir, EXTENSION=ext)
    CASE StrUpCase(ext) OF
       'PS':   self -> AutoPostScriptFile, filename
       'EPS':  self -> AutoPostScriptFile, filename
       'PDF':  self -> AutoRasterFile, 'PDF', filename
       'BMP':  self -> AutoRasterFile, 'BMP', filename
       'GIF':  self -> AutoRasterFile, 'GIF', filename
       'JPG':  self -> AutoRasterFile, 'JPEG', filename
       'JPEG': self -> AutoRasterFile, 'JPEG', filename
       'PNG':  self -> AutoRasterFile, 'PNG', filename
       'TIF':  self -> AutoRasterFile, 'TIFF', filename
       'TIFF': self -> AutoRasterFile, 'TIFF', filename
       ELSE: Message, 'Unknown file type: ' + StrUpCase(ext) + '.'
    ENDCASE
    
 END



;+
; Packages the command up into a command object that can be added to the window
; or used to replace commands that are already in the window.
; 
; :Returns:
;    The command, packaged as a cgWindow_Command object, is returned.
; 
; :Params:
;    command: in, required, type=string
;       The graphics procedure command to be executed. This parameter
;       must be a string and the the command must be a procedure. Examples
;       are 'Surface', 'Contour', 'Plot', 'cgPlot', cgContour, etc.
;    p1: in, optional, type=any
;       The first positional parameter appropriate for the graphics command.
;    p2: in, optional, type=any
;       The second positional parameter appropriate for the graphics command.
;    p3: in, optional, type=any
;       The third positional parameter appropriate for the graphics command.
;    p4: in, optional, type=any
;       The fourth positional parameter appropriate for the graphics command.
;       
; :Keywords:
;    addcmd: in, optional, type=boolean, default=0
;       Set this keyword to add the packaged command to the command list.
;    altps_Keywords: in, optional, type=string
;       A structure containing alternative keyword names (as tags) and values for
;       those keywords to be used when the current device is the PostScript device.
;       See http://www.idlcoyote.com/cg_tips/kwexpressions.php and the examples
;       below for details on how to use this keyword.
;    altps_Params: in, optional, type=IntArr(3)
;       A structure containing alternative parameter values to be used when 
;       the current device is the PostScript device. Structure names are restricted
;       to the names "P1", "P2", "P3" and "P4" to correspond to the equivalent positional
;       parameter. See http://www.idlcoyote.com/cg_tips/kwexpressions.php and the 
;       examples below for details on how to use this keyword.
;    cmdindex: in, optional, type=integer
;       The command list index. This is used when adding or replacing a command. If the
;       command index is undefined when replacing a command, all the commands in the command
;       list are replaced with the command that is being added. If the command index is 
;       undefined when adding a command, the command is added to the end of the command list.
;    execute: in, optional, type=boolean, default=0
;       Set this keyword is you want to execute the commands in the command list as soon as
;       the current command is added to the list. Otherwise, no command execution takes place.
;    method: in, optional, type=boolean, default=0
;       Set this keyword if the command is an object method call rather than a 
;       procedure call. If this keyword is set, the first positional parameter, p1,
;       must be present and must be a valid object reference.
;    multi: in, optional, type=intarr(5)
;       A replacement value to assign to the self.pmulti value. It is
;       a way to reset multiple command plotting to single command plotting
;       for a new single command. Like calling: object -> SetPropery, MULTI=multi. Used only if
;       `ReplaceCmd` is set.
;    replacecmd: in, optional, type=boolean, default=0
;       Set this keyword to replace a graphics command with this packaged command on the command list
;       If `CmdIndex` is undefined, *all* the commands on the command list are replaced. 
;    _extra: in, optional
;       Any extra keywords are collected by keyword inheritance for the command structure.
;- 
FUNCTION cgCmdWindow::PackageCommand, command, p1, p2, p3, p4, $
   AddCmd=addCmd, $                 ; Add the command to the command list
   AltPS_Keywords=altps_Keywords, $ ; A structure of PostScript alternative keywords and values.
   AltPS_Params=altps_Params, $     ; A structure of PostScript alternative parameters and values. 
   CmdIndex=cmdIndex, $             ; The location of the command in the command list.
   DestroyObjects=destroyobjects, $ ; Set this keyword to destroy parameter objects on exit.
   Execute=execute, $               ; Execute the commands in the window, if this keyword set.
   Method=method, $                 ; A flag that indicates a method call.
   Multi=multi, $                   ; If you are replacing all commands, you may want to change the way they are displayed.
   ReplaceCmd=replaceCmd, $         ; Set this keyword to replace one or all commands in the command list.
    _Extra=extra                    ; Extra keywords to the command.
    
    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN, Obj_New()
    ENDIF

   newCommand = Obj_New('cgWindow_Command', COMMAND=command, $
      P1=p1, P2=p2, P3=p3, P4=p4, KEYWORDS=extra, AltPS_Keywords=altps_Keywords, $
      AltPS_Params=altps_Params, TYPE=Keyword_Set(method), DESTROYOBJECTS=Keyword_Set(destroyObjects))
                        
    ; Replace command? If the cmdIndex is undefined, ALL commands are replaced.
    IF Keyword_Set(replaceCmd) THEN self -> ReplaceCommand, newCommand, cmdIndex, Multi=multi
      
    ; Need to add a command?
    IF Keyword_Set(addCmd) THEN self -> AddCommand, newCommand, INDEX=cmdIndex
   
    ; Execute the commands?
    IF Keyword_Set(execute) THEN self -> ExecuteCommands
    
    ; Return the packaged command.
    RETURN, newCommand
    
END


;+
; This method replaces a command in the command list. If cmdIndex is missing,
; all the commands in the command list are replaced by this command.
;
; :Params:
;    command: in, required, type=object
;       The new command object. 
;    cmdindex: in, optional, type=integer
;       The index number of the "command" to replace. If absent, then
;       all the current commands are replaces with this new command.
; :Keywords:
;     multi: in, optional, type=intarr(5)
;        A replacement value to assign to the self.pmulti value. It is
;        a way to reset multiple command plotting to single command plotting
;        for a new single command. Like calling: object -> SetPropery, MULTI=multi.
;-
PRO cgCmdWindow::ReplaceCommand, command, cmdIndex, MULTI=multi

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; If cmdIndex is missing, remove all the current commands with this one.
    IF N_Elements(cmdIndex) EQ 0 THEN BEGIN
        self.cmds -> Delete, /ALL, /Destroy
        IF N_Elements(multi) NE 0 THEN BEGIN
            self.pmulti = multi ; Reset !P.Multi to new value.
        ENDIF ELSE BEGIN
            self.pmulti = Intarr(5) ; Reset !P.Multi to zero.
        ENDELSE
        self.cmds -> Add, command
    ENDIF ELSE BEGIN
    
        ; Get the old command first, so you can destroy it properly.
        oldcmd = self.cmds -> Get_Item(cmdIndex, /DEREFERENCE)
        self.cmds -> Replace_Item, command, cmdIndex
        Obj_Destroy, oldcmd
    ENDELSE
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method resizes the graphics window and executes the commands again.
; 
; :Params:
;     x: in, required, type=integer
;        The new X size of the draw widget in device coordinates.
;     y: in, required, type=integer
;        The new Y size of the draw widget in device coordinates.
;-
PRO cgCmdWindow::Resize, x, y

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF
    
    ; Make sure we have both sizes.
    IF N_Params() NE 2 THEN Message, 'Resize method requires both an X and Y size be specified.'
    
    ; Do you need to maintain the aspect ratio of the window?
    IF self.waspect NE 0 THEN BEGIN
         IF self.waspect GE 1 THEN BEGIN
             ysize = y
             xsize = Round(y / self.waspect)
         ENDIF ELSE BEGIN
             xsize = x
             ysize = Round(x * self.waspect)         
         ENDELSE
    ENDIF ELSE BEGIN
        xsize = x
        ysize = y
    ENDELSE

    Widget_Control, self.drawID, DRAW_XSIZE=xsize, DRAW_YSIZE=ysize
    self -> ExecuteCommands
    
END ;----------------------------------------------------------------------------------------------------------------


;+
; This event handler method saves the graphics window as a raster image file.
; PDF files also pass through here.
; 
; :Params:
;     event: in, required, type=structure
;        The event structure.
;-
PRO cgCmdWindow::SaveAsRaster, event

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        
        ; Close the PostScript file.
        cgPS_Close, /NoFix     

        ; Set the window index number back.
        IF N_Elements(currentWindow) NE 0 THEN BEGIN
            IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1
        ENDIF
        
        RETURN
    ENDIF

    ; Only going in here for down event.
    IF event.select NE 1 THEN RETURN
    
    buttonValue = Widget_Info(event.id, /UNAME)
    
    ; Is this a PDF file or a raster file?
    IF buttonValue EQ 'PDF' THEN BEGIN
       rasterType = -1
       filetype = 'PDF'
    ENDIF ELSE BEGIN
    
        ; Determine if this is normal raster (0) or ImageMagick raster (1).
        IF StrMid(buttonValue, 0, 6) EQ 'RASTER' THEN BEGIN
            fileType = StrMid(buttonValue, 7)
            rasterType = 0 
        ENDIF ELSE BEGIN
            filetype = StrMid(buttonValue, 12)
            rasterType = 1
        ENDELSE
        
    ENDELSE
    
    ; Make this window the current graphics windows.
    currentWindow = !D.Window
    WSet, self.wid
    
    ; Construct a file name, if you have one.
    CASE filetype OF
       'BMP':  ext = '.bmp'
       'GIF':  ext = '.gif'
       'JPEG': ext = '.jpg'
       'PDF':  ext = '.pdf'
       'PNG':  ext = '.png'
       'TIFF': ext = '.tif'
    ENDCASE
    IF self.lastWriteFile NE "" THEN BEGIN
        filename = Filepath(ROOT_DIR=self.lastWriteDir, self.lastWriteFile + ext)
    ENDIF ELSE BEGIN
         CD, CURRENT=thisDir
         filename = Filepath(ROOT_DIR=thisDir, 'cgwindow' + ext)
    ENDELSE
    
    ; Get a filename from the user.
    CASE filetype OF
       'BMP':  filename = cgPickfile(FILE=filename, /WRITE, TITLE='Select an Output File...')
       'GIF':  filename = cgPickfile(FILE=filename, /WRITE, TITLE='Select an Output File...')
       'JPEG': filename = cgPickfile(FILE=filename, /WRITE, TITLE='Select an Output File...')
       'PDF':  filename = cgPickfile(FILE=filename, /WRITE, TITLE='Select an Output File...')
       'PNG':  filename = cgPickfile(FILE=filename, /WRITE, TITLE='Select an Output File...')
       'TIFF': filename = cgPickfile(FILE=filename, /WRITE, TITLE='Select an Output File...')
    ENDCASE
    IF filename EQ "" THEN RETURN
    
    ; Parset the name.
    root_name = cgRootName(filename, DIRECTORY=dirName)
    outname = Filepath(ROOT_DIR=dirname, root_name)
    
    ; Save this name.
    self.lastWriteFile = root_name
    self.lastWriteDir = dirName
    
    ; What kind of raster file.
    CASE rasterType OF
    
        ; PDF File.
       -1: BEGIN
       
           thisname = outname + '.ps'
           outname = outname + '.pdf'
           cgPS_Open, $
                DECOMPOSED=self.ps_decomposed, $
                FILENAME=thisname, $
                GROUP_LEADER=self.tlb, $
                METRIC=self.ps_metric, $
                KEYWORDS=keywords, $ ; Returned cgPS_Config keywords.
                SCALE_FACTOR=self.ps_scale_factor, $
                CHARSIZE=self.ps_charsize, $
                FONT=self.ps_font, $
                QUIET=self.ps_quiet, $
                TT_FONT=self.ps_tt_font
                           
           ; Draw the graphics.
           self -> ExecuteCommands
           
           ; Close the file and make a PDF file.
           cgPS_Close
           cgPS2PDF, thisname, outname, DELETE_PS=self.ps_delete, /SILENT, SUCCESS=success, $
              UNIX_CONVERT_CMD=self.pdf_unix_convert_cmd, GS_PATH=self.pdf_path
           IF ~success THEN BEGIN
              Message, 'Unable to create PDF file. See cgPS2PDF documentation.'
           ENDIF ELSE BEGIN
              IF ~self.ps_quiet THEN Print, 'PDF output will be created here: ' + outname
           ENDELSE
           END
    
        ; Normal raster.
        0: BEGIN
           void = cgSnapshot(TYPE=fileType, FILENAME=outname, /NODIALOG)
           IF ~self.ps_quiet THEN Print, 'Output file located here: ' + outname 
           END
           
        ; Raster via ImageMagick.
        1: BEGIN
        
           ; Create a PostScript file first.
           thisname = outname + '.ps'
           cgPS_Open, $
                DECOMPOSED=self.ps_decomposed, $
                FILENAME=thisname, $
                GROUP_LEADER=self.tlb, $
                METRIC=self.ps_metric, $
                KEYWORDS=keywords, $ ; Returned cgPS_Config keywords.
                SCALE_FACTOR=self.ps_scale_factor, $
                CHARSIZE=self.ps_charsize, $
                FONT=self.ps_font, $
                QUIET=1, $
                TT_FONT=self.ps_tt_font
                
           ; Cannot successfully convert encapsulated landscape file to raster.
           ; Limitation of ImageMagick, and specifically, GhostScript, which does
           ; the conversion.
           IF keywords.encapsulated && keywords.landscape THEN BEGIN
                Message, 'ImageMagick cannot successfully convert an encapsulated ' + $
                         'PostScript file in landscape mode to a raster file. Returning...'
           ENDIF
           
           ; Draw the graphics.
           self -> ExecuteCommands
           
           ; Close the file and convert to proper file type.
           CASE filetype OF
                'BMP':  cgPS_Close, /BMP, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width
                'GIF':  cgPS_Close, /GIF, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width
                'JPEG': cgPS_Close, /JPEG, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width
                'PNG':  cgPS_Close, /PNG,  DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width
                'TIFF': cgPS_Close, /TIFF, DELETE_PS=self.ps_delete, $
                            ALLOW_TRANSPARENT=self.im_transparent, $
                            DENSITY=self.im_density, RESIZE=self.im_resize, $
                            IM_OPTIONS=self.im_options, OUTFILENAME=outfilename, $
                            WIDTH=*self.im_width
           ENDCASE
           IF ~self.ps_quiet THEN Print, 'Output will be created here: ' + outfilename
           END
    
    ENDCASE
    
    ; Set the window index number back.
    IF WindowAvailable(currentWindow) THEN WSet, currentWindow ELSE WSet, -1
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method restores the commands from a specified file and loads them
; into the window.
;
; :Params:
;    filename: in, required, type=string
;       The name of the file containing the cgWindow commands that were previously saved.
;-
PRO cgCmdWindow::RestoreCommands, filename

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF

    ; Need a file name?
    IF N_Elements(filename) EQ 0 THEN BEGIN
        filename = cgPickfile(Title='Restore Coyote Graphics Commands...', $
            FILTER='*.cgs')
        IF filename EQ "" THEN RETURN
    ENDIF
    
    ; Does the file exist?
    fileTest = File_Test(filename, /READ)
    IF ~fileTest THEN BEGIN
        text = 'The file (' + filename + ') cannot be located. Returning.'
        void = Dialog_Message(text)
        RETURN
    ENDIF
    
    ; Restore the file. The variables "cg_commands" and "cg_window" are restored.
    Restore, FILE=filename, /Relaxed_Structure_Assignment
    
    ; Make this object the current object.
    cgSet, self.wid
    
    ; Erase the current commands in the window. If this is not
    ; done memory will leak.
    cgErase, self.wid, /Window
    
    ; Copy the commands from the restored command list to this command list.
    oldCommands = cg_commands -> Get_Item(/DEREFERENCE, /ALL)
    self.cmds -> Delete, /ALL
    FOR j=0,N_Elements(oldCommands)-1 DO BEGIN
        thisCmd = oldCommands[j] -> Copy()
        self.cmds -> Add, thisCmd
    ENDFOR
        
    ; Copy properties from old window object to new.
    cg_window -> GetProperty, $
        BACKGROUND=background, $       ; The background color of the window.
        DELAY=delay, $                 ; The delay between command execution.
        ERASEIT=eraseit, $             ; Set the erase flag for the window
        PALETTE=palette, $             ; Change window color table vectors.
        MULTI=multi, $                 ; Change the !P.MULTI setting for the window.
        XOMARGIN=xomargin, $           ; Change the !X.OMargin setting for the winow.
        YOMARGIN=yomargin, $           ; Change the !Y.OMargin setting for the window.
        IM_TRANSPARENT=im_transparent, $              ; Sets the "alpha" keyword on ImageMagick convert command.
        IM_DENSITY=im_density, $                      ; Sets the density parameter on ImageMagick convert command.
        IM_RESIZE=im_resize, $                        ; Sets the resize parameter on ImageMagick convert command.
        IM_OPTIONS=im_options, $                      ; Sets extra ImageMagick options on the ImageMagick convert command.
        IM_RASTER=im_raster, $                        ; Sets whether to use ImageMagick to create raster files.
        PS_DELETE=ps_delete, $                        ; Delete the PostScript file when making IM raster files.
        PS_METRIC=ps_metric, $                        ; Select metric measurements in PostScript output.
        PS_ENCAPSULATED=ps_encapsulated, $            ; Select encapusulated PostScript output.
        PS_FONT=ps_font, $                            ; Select the font for PostScript output.
        PS_CHARSIZE=ps_charsize, $                    ; Select the character size for PostScript output.
        PS_SCALE_FACTOR=ps_scale_factor, $            ; Select the scale factor for PostScript output.
        PS_TT_FONT=ps_tt_font                         ; Select the true-type font to use for PostScript output.
        
    self -> SetProperty, $
        BACKGROUND=background, $       ; The background color of the window.
        DELAY=delay, $                 ; The delay between command execution.
        ERASEIT=eraseit, $             ; Set the erase flag for the window
        PALETTE=palette, $             ; Change window color table vectors.
        MULTI=multi, $                 ; Change the !P.MULTI setting for the window.
        XOMARGIN=xomargin, $           ; Change the !X.OMargin setting for the winow.
        YOMARGIN=yomargin, $           ; Change the !Y.OMargin setting for the window.
        IM_TRANSPARENT=im_transparent, $              ; Sets the "alpha" keyword on ImageMagick convert command.
        IM_DENSITY=im_density, $                      ; Sets the density parameter on ImageMagick convert command.
        IM_RESIZE=im_resize, $                        ; Sets the resize parameter on ImageMagick convert command.
        IM_OPTIONS=im_options, $                      ; Sets extra ImageMagick options on the ImageMagick convert command.
        IM_RASTER=im_raster, $                        ; Sets whether to use ImageMagick to create raster files.
        PS_DELETE=ps_delete, $                        ; Delete the PostScript file when making IM raster files.
        PS_METRIC=ps_metric, $                        ; Select metric measurements in PostScript output.
        PS_ENCAPSULATED=ps_encapsulated, $            ; Select encapusulated PostScript output.
        PS_FONT=ps_font, $                            ; Select the font for PostScript output.
        PS_CHARSIZE=ps_charsize, $                    ; Select the character size for PostScript output.
        PS_SCALE_FACTOR=ps_scale_factor, $            ; Select the scale factor for PostScript output.
        PS_TT_FONT=ps_tt_font                         ; Select the true-type font to use for PostScript output.
        
        
    ; The widget ID of the restored object must be invalidated,
    ; or the current object (with that widget ID) will be destroyed.
    cg_window -> InvalidateWidgetID
    Obj_Destroy, cg_window
    
    ; Execute the new commands.
    self -> ExecuteCommands

END ;----------------------------------------------------------------------------------------------------------------


;+
; This method restores the current data coordinate system by restoring the system variable
; state from the last command execution.
;-
PRO cgCmdWindow::RestoreDataCoords

   !MAP = *self.msysvar
   !P = self.psysvar
   !X = self.xsysvar
   !Y = self.ysysvar
   !Z = self.zsysvar
   
END


;+
; This method saves the commands from a cgWindow into a form that can be
; e-mailed to colleagues or restored to a cgWindow at some later time.
;
; :Params:
;    filename: in, optional, type=string, default='commands.cgs'
;       The name of the file where the cgWindow commands are to be saved.
;-
PRO cgCmdWindow::SaveCommands, filename

    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF
    
    ; Need a file name.
    IF N_Elements(filename) EQ 0 THEN BEGIN
       filename = cgPickfile(FILE='commands.cgs', $
            TITLE='Save Coyote Graphics Commands...', /Write)
       IF filename EQ "" THEN RETURN
    ENDIF
    
    ; Copy the commands.
    cg_commands = self.cmds
    cg_window = self
    
    ; Save them in the file.
    Save, cg_commands, cg_window, FILE=filename
   
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method saves the current data coordinate system by saving all the plotting and mapping
; system variables after the commands have been executed in the program.
;-
PRO cgCmdWindow::SaveDataCoords

   *self.msysvar = !Map
   self.psysvar = !P
   self.xsysvar = !X
   self.ysysvar = !Y
   self.zsysvar = !Z
   
END


;+
; This method sets properties of the window object. 
; 
; :Keywords:
;     adjustsize: in, optional, type=boolean
;        Set this keyword to adjust default character size to the display window size.
;     background: in, optional, type=string
;        The background color of the window. Only use if the ERASEIT property is also set.
;     delay: in, optional, type=float
;        Set this keyword to the amount of "delay" you want between commands in the command list.
;     dimensions: in, optional, type=intarr(2)
;        Set this keyword to a two-element array giving the xsize and ysize
;        of the draw widget.
;     eraseit: in, optional, type=boolean
;        If this property is set, the cgWindow erases with the background color before
;        displaying the commands in the window's command list.
;     im_density: in, optional, type=integer, default=300
;        Set this keyword to the sampling density when ImageMagick creates raster image
;        file from PostScript outout.
;     im_options: in, optional, type=string, default=""
;        Set this keyword to any ImageMagick options you would like to pass along to the
;        ImageMagick convert command when creating raster image files from PostScript output.
;     im_resize: in, optional, type=integer, default=25
;        Set this keyword to percentage that the raster image file created my ImageMagick
;        from PostScript output should be resized.
;     im_raster: in, optional, type=boolean, default=1
;        Set this keyword to zero to create raster files using the create_png etc. keywords
;        directly, instead of via ImageMagick.
;     im_transparent: in, optional, type=boolean, default=0
;        Set this keyword to allow ImageMagick to create transparent backgrounds when it
;        makes raster image files from PostScript output.
;     multi: in, optional, type=Intarr(5)
;        Set this keyword to the !P.MULTI setting you want to use for the window.
;        !P.MULTI is set to this setting before command execution, and set back to
;        it's default value when the commands are finished executing.
;     noexecutecommands: in, optional, type=boolean, default=0
;        Set this keyword to 1 to prevent the window from executing the commands and to
;        0 if you want the window to execute the commands.
;     palette: in, optional, type=byte
;        Use this keyword to pass in an N-by-3 (or 3-by-N) byte array containing the
;        R, G, and B vectors of a color table. It is probably easier to use cgLoadCT or
;        XCOLORS to load color tables for the window, but this is provided as another option.
;     pdf_path: out, optional, type=string
;        Set this keyword to the name of the path to the Ghostscript command for converting PS to PDF.
;     pdf_unix_convert_cmd: out, optional, type=string
;        Set this keyword to the name of an alternative UNIX command to convert PostScript to PDF.
;     ps_charsize: in, optional, type=float
;        The PostScript character size.
;     ps_decomposed: in, optional, type=boolean, default=0
;        Set this keyword to zero to set the PostScript color mode to indexed color and to
;        one to set the PostScript color mode to decomposed color.
;     ps_delete: in, optional, type=boolean, default=1
;        Set this keyword to zero if you want to keep the PostScript output ImageMagick creates
;        when making raster file output.
;     ps_encapsulated: in, optional, type=boolean, default=0
;        Set this keyword to configure cgPS_Config to produce encapsulated PostScript output by default.
;     ps_font: in, optional, type=integer
;        Set this keyword to the type of font you want to use in PostScript output. It sets the 
;        FONT keyword on the cgPS_Config command. Normally, 0 (hardware fonts) or 1 (true-type fonts).
;     ps_metric: in, optional, type=boolean, default=0
;        Set this keyword to configure cgPS_Config to use metric values and A4 page size in its interface.
;     ps_quiet: in, optional, type=boolean, default=0
;        Set this keyword to set the QUIET keyword on cgPS_Open.
;     ps_scale_factor: in, optional, type=float
;        Set his keyword to the PostScript scale factor you wish to use in creating PostScript output.
;     ps_tt_font: in, optional, type=string
;        Set this keyword to the name of a true-type font to use in creating PostScript output.
;     storage: in, optional
;        A storage location for any kind of IDL variable. This functions like a user value in
;        a widget program. 
;     update: in, optional, type=boolean, default=1
;        Set this keyword to zero if you do not want the updates to be done immediately
;        after the properties are changed.
;     wid: in, optional, type=integer
;        The draw window index number. Not to be used by the user. Used by the draw widget
;        Notify_Realize callback routine when the draw widget is realized.
;     xomargin: in, optional, type=intarr(2)
;        Sets the !X.OMargin system variable when multiple plots are displayed in the window.
;     yomargin: in, optional, type=intarr(2)
;        Sets the !Y.OMargin system variable when multiple plots are displayed in the window.
;          
;-
PRO cgCmdWindow::SetProperty, $
    ADJUSTSIZE=adjustsize, $       ; Adjust the default charsize to match display size.
    BACKGROUND=background, $       ; The background color of the window.
    DELAY=delay, $                 ; The delay between command execution.
    DIMENSIONS=dimensions, $       ; Set the dimensions of the draw widget.
    ERASEIT=eraseit, $             ; Set the erase flag for the window
    IM_DENSITY=im_density, $                      ; Sets the density parameter on ImageMagick convert command.
    IM_OPTIONS=im_options, $                      ; Sets extra ImageMagick options on the ImageMagick convert command.
    IM_RASTER=im_raster, $                        ; Sets whether to use ImageMagick to create raster files.
    IM_RESIZE=im_resize, $                        ; Sets the resize parameter on ImageMagick convert command.
    IM_TRANSPARENT=im_transparent, $              ; Sets the "alpha" keyword on ImageMagick convert command.
    IM_WIDTH = im_width, $                        ; Sets the final width of the raster files create with ImageMagick.
    PDF_UNIX_CONVERT_CMD=pdf_unix_convert_cmd, $  ; Command to convert PS to PDF.
    PDF_PATH=pdf_path, $                          ; The path to the Ghostscript conversion command.
    PALETTE=palette, $             ; Change window color table vectors.
    PS_CHARSIZE=ps_charsize, $                    ; Select the character size for PostScript output.
    PS_DECOMPOSED=ps_decomposed, $                ; Sets the PostScript color mode.
    PS_DELETE=ps_delete, $                        ; Delete the PostScript file when making IM raster files.
    PS_ENCAPSULATED=ps_encapsulated, $            ; Select encapusulated PostScript output.
    PS_FONT=ps_font, $                            ; Select the font for PostScript output.
    PS_METRIC=ps_metric, $                        ; Select metric measurements in PostScript output.
    PS_QUIET=ps_quiet, $                          ; Select the QUIET keyword for cgPS_Open.
    PS_SCALE_FACTOR=ps_scale_factor, $            ; Select the scale factor for PostScript output.
    PS_TT_FONT=ps_tt_font, $                      ; Select the true-type font to use for PostScript output.
    NOEXECUTECOMMANDS=noExecuteCommands, $ ; Set if you don't want the window to execute commands.
    MULTI=multi, $                 ; Change the !P.MULTI setting for the window.
    STORAGE=storage, $             ; A pointer location for storing any IDL variable, like a UVALUE.
    XOMARGIN=xomargin, $           ; Change the !X.OMargin setting for the window.
    YOMARGIN=yomargin, $           ; Change the !Y.OMargin setting for the window.
    UPDATE=update, $               ; Set if you want the commands to be updated after property change.
    WID=wid, $                     ; The window index number. Used in draw widget notify realize event.
    _EXTRA=extra
    
    Compile_Opt idl2
    
    ; Error handling.
    Catch, theError
    IF theError NE 0 THEN BEGIN
        Catch, /CANCEL
        void = cgErrorMsg()
        RETURN
    ENDIF
    
    IF N_Elements(adjustsize) NE 0 THEN self.adjustsize = Keyword_Set(adjustsize)

    IF N_Elements(background) NE 0 THEN BEGIN
        IF Ptr_Valid(self.background) $
            THEN *self.background = background $
            ELSE self.background = Ptr_New(background)
    ENDIF 
    IF N_Elements(palette) NE 0 THEN BEGIN
        IF Size(palette, /N_DIMENSIONS) NE 2 THEN Message, 'Color palette is not a 3xN array.'
        dims = Size(palette, /DIMENSIONS)
        threeIndex = Where(dims EQ 3)
        IF ((threeIndex)[0] LT 0) THEN Message, 'Color palette is not a 3xN array.'
        IF threeIndex[0] EQ 0 THEN palette = Transpose(palette)
        *self.r = palette[*,0]
        *self.g = palette[*,1]
        *self.b = palette[*,2]
    ENDIF   
    IF N_Elements(dimensions) NE 0 THEN BEGIN
       IF N_Elements(dimensions) EQ 1 THEN dimensions = [dimensions, dimensions]
       Widget_Control, self.drawID, DRAW_XSIZE=dimensions[0], DRAW_YSIZE=dimensions[1]
    ENDIF
    IF N_Elements(delay) NE 0 THEN self.delay = delay
    IF N_Elements(eraseit) NE 0 THEN self.eraseit = Keyword_Set(eraseit)
    IF N_Elements(noExecuteCommands) NE 0 THEN self.noexecutecommands = Keyword_Set(noExecuteCommands)
    IF N_Elements(multi) NE 0 THEN BEGIN
        IF (N_Elements(multi) EQ 1) && (multi EQ 0) THEN multi = IntArr(5)
        FOR j=0,N_Elements(multi)-1 DO self.pmulti[j] = multi[j]
    ENDIF
    IF N_Elements(xomargin) NE 0 THEN self.xomargin = xomargin
    IF N_Elements(yomargin) NE 0 THEN self.yomargin = yomargin
    IF N_Elements(im_transparent) NE 0 THEN self.im_transparent = im_transparent
    IF N_Elements(im_density) NE 0 THEN self.im_density = im_density
    IF N_Elements(im_resize) NE 0 THEN self.im_resize = im_resize
    IF N_Elements(im_options) NE 0 THEN self.im_options = im_options
    IF N_Elements(im_raster) NE 0 then self.im_raster = im_raster
    IF N_Elements(im_width) NE 0 then *self.im_width = im_width
    IF N_Elements(pdf_unix_convert_cmd) NE 0 THEN self.pdf_unix_convert_cmd = pdf_unix_convert_cmd
    IF N_Elements(pdf_path) NE 0 THEN self.pdf_path = pdf_path
    IF N_Elements(ps_decomposed) NE 0 THEN self.ps_decomposed = ps_decomposed
    IF N_Elements(ps_delete) NE 0 THEN self.ps_delete = ps_delete
    IF N_Elements(ps_metric) NE 0 THEN self.ps_metric = ps_metric
    IF N_Elements(ps_encapsulated) NE 0 THEN self.ps_encapsulated = ps_encapsulated
    IF N_Elements(ps_charsize) NE 0 THEN self.ps_charsize = ps_charsize
    IF N_Elements(ps_font) NE 0 THEN self.ps_font = ps_font
    IF N_Elements(ps_quiet) NE 0 THEN self.ps_quiet = ps_quiet
    IF N_Elements(ps_scale_factor) NE 0 THEN self.ps_scale_factor = ps_scale_factor
    IF N_Elements(ps_tt_font) NE 0 THEN self.ps_tt_font = ps_tt_font
    IF N_Elements(storage) NE 0 THEN BEGIN
       IF Ptr_Valid(self.storage) THEN *self.storage = storage ELSE self.storage = Ptr_New(storage)
    ENDIF
    
    ; You may have gotten keywords you don't know what to deal with. Inform
    ; the user there.
    IF N_Elements(extra) NE 0 THEN BEGIN
       tags = Tag_Names(extra)
       FOR j=0,N_Elements(tags)-1 DO BEGIN
          Message, 'The following property could NOT be set in the SetProperty method: ' + tags[j], /Informational
       ENDFOR
    ENDIF
    
    ; Update now?
    IF Keyword_Set(update) THEN self -> ExecuteCommands
    
    ; Update the draw widget index number. Should only be called from the draw widget notify realize procedure.
    IF N_Elements(wid) NE 0 THEN self.wid = wid
END ;----------------------------------------------------------------------------------------------------------------


;+
; This method saves the window object reference in a linked-list system variable.
;-
PRO cgCmdWindow::StoreObjectReference

    ; Each instance of cgWindow will store evidence of its
    ; existance in a linked list.
    DefSysV, '!FSC_WINDOW_LIST', EXISTS=exists
    IF ~exists THEN BEGIN
        fsc_window_list = Obj_New("LinkedList")
        DefSysV, '!FSC_WINDOW_LIST', fsc_window_list 
        fsc_window_list -> Add, {cgWINDOW_ID, self.tlb, self.wid, self.title, self}
    ENDIF ELSE BEGIN
        IF Obj_Valid(!FSC_WINDOW_LIST) THEN BEGIN
            !FSC_WINDOW_LIST -> Add, {cgWINDOW_ID, self.tlb, self.wid, self.title, self}
        ENDIF ELSE BEGIN
            !FSC_WINDOW_LIST = Obj_New('LinkedList')
            !FSC_WINDOW_LIST-> Add, {cgWINDOW_ID, self.tlb, self.wid, self.title, self}
        ENDELSE
    ENDELSE
    
END



;+
; The definition module for the cgCmdWindow object
; 
; :Params:
;    class: out, optional, type=struct
;        The object class structure definition. Occasionally useful.
;-
PRO cgCmdWindow__Define, class

    class = { cgCMDWINDOW, $
              tlb: 0L, $                    ; The identifier of the top-level base widget.
              cmds: Obj_New(), $            ; A linkedlist object containing the graphics commands.
              wid: 0L, $                    ; The window index number of the graphics window.
              drawid: 0L, $                 ; The identifier of the draw widget.
              destroyObjects: 0B, $         ; If set, destroy object parameters upon exit.
              event_handler: "", $          ; The name of an event handler that will receive draw widget events.
              storage: Ptr_New(), $         ; Holder for user information, like UVALUE.
              title: "", $                  ; The "title" of the object when it is stored.
              createParent: 0B, $           ; Flag that indicates whether this object created its own parent widget.
              lastWriteFile: "", $          ; The name of the last file written.
              lastWriteDir: "", $           ; The name of the last directory written to.
              
              ; cgWindow parameters
              adjustsize: 0B, $             ; Adjust character size to display window.
              background: Ptr_New(), $      ; The background color.
              delay: 0.0, $                 ; The command delay.
              eraseit: 0B, $                ; Do we need to erase the display.
              noExecuteCommands: 0B, $      ; Set to stop command execution (e.g.,for loading commands)
              pmulti: LonArr(5), $          ; Identical to !P.Multi.
              xomargin: FltArr(2), $        ; Identical to !X.OMargin
              yomargin: FltArr(2), $        ; Identical to !Y.OMargin
              waspect: 0.0, $               ; The aspect ratio of the window.
              r: Ptr_New(), $               ; The red color table vector.
              g: Ptr_New(), $               ; The green color table vector.
              b: Ptr_New(), $               ; The blue color table vector.
              
              ; Data coordinate fields.
              psysvar: !P, $                ; The !P system variable.
              xsysvar: !X, $                ; The !X system variable.
              ysysvar: !Y, $                ; The !Y system variable.
              zsysvar: !Z, $                ; The !Z system variable.
              msysvar: Ptr_New(), $         ; The !MAP system variable.
              
              ; PostScript options.
              ps_decomposed: 0L, $          ; Sets the PostScript color mode.
              ps_delete: 0L, $              ; Delete the PS file when making IM image file.
              ps_encapsulated: 0L, $        ; Encapsulated PostScript
              ps_metric: 0L, $              ; Metric measurements in PostScript.
              ps_charsize: 0.0, $           ; The character size to use for PostScript output.
              ps_font: 0, $                 ; The PostScript font to use.
              ps_quiet: 0, $                ; Select the QUIET keyword for cgPS_Open.
              ps_scale_factor: 0, $         ; The PostScript scale factor.
              ps_tt_font: "", $             ; The name of a true-type font to use for PostScript output.
              
              ; PDF options.
              pdf_unix_convert_cmd: "", $   ; The name of an alternative UNIX command to convert PS to PDF.
              pdf_path: "", $               ; The name of the path to a Ghostscript conversion command.

              ; ImageMagick output parameters.
              im_transparent: 0B, $         ; Sets the "alpha" keyword on ImageMagick convert command.
              im_density: 0L, $             ; Sets the density parameter on ImageMagick convert command.
              im_resize: 0L, $              ; Sets the resize parameter on ImageMagick convert command.
              im_options: "", $             ; Sets extra ImageMagick options on the ImageMagick convert command.
              im_raster: 0L, $              ; Create raster files via ImageMagick
              im_width: Ptr_New() $         ; Sets the width of the final raster output with ImageMagick
            }
            
END ;----------------------------------------------------------------------------------------------------------------




