// 
// HP7470A plotter emulator
//
// 28-Jan-02 john@miles.io
//
// 28-Nov-13 Rev 2.000 - Mark S. Sims  
//    Added full HPGL/partial PCL renderer.
//
//    The new plot renderer supports pretty much the full HPGL language
//    including text sizing and direction.  Also patterned lines and image 
//    rotation.  
//
//    It also can also handle simple .PCL files that contain raster graphics.
//    (Note: PCL images cannot be rotated).
//
//    Added support for background color inversion and image rotation. These
//    modes and the alternate color scheme settings are remembered for each 
//    plot when switching between plots.  Black background look best for 
//    screen viewing,  White backgrounds look best for printing.
//
//    Added 1280x960 image size.
//
//    The '+' and '-' browse commands now wrap around at the ends of the 
//    image list.
//
//
// 05 Dec-13 Rev 2.001
//    Enabled PM/EP/FP polygon mode commands (FP filled polygons are not 
//    drawn filled, though... we hatching and fill types are not supported)
//
//    Fixed arc endpoint problem.  Cursor was not being left at the proper place.
//
//    Fixed x axis tick mark problem (if ticks were only poitive or negative).
//
//    Fixed line type scaling size (segments were too short).
//
//    Fixed DV up and DV down commands (directions were swapped).
//
//    Fixed CP command when DV was not left to right
//
//    Added support for chord tolerance mode and arc/circle chord angles.
//    Note that our default chord angle is 1 degree (not 5 degrees).
//
//    Added support for SA/SS (alternate/standard font) and DL (download character)
//    commands. Alternate font must be active for DL chars to print.
//
//    Added support for scaling mode 2 (point factor scaling) and scaling mode
//    1 (isotropic scaling)
//
//    Added pu (plotter units) command to the .INI file to allow setting the
//    plotter units range of the plotter being emulated.  Small format plotters
//    use postive values with the origin in the lower left corner of the plot.
//    Large format (newer) plotters use positive and negative units with the 
//    origin in the middle of the plot.
//
// 22 Dec-14 Rev 2.002
//    Disabled bogus "IW" clipping window generated by some Wiltron 8350B

#define OLD_WINCON
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <commdlg.h>
#include <mmsystem.h>
#include <shellapi.h>
#include <shlobj.h>

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <io.h>
#include <fcntl.h>
#include <assert.h>
#include <limits.h>
#include <direct.h>
#include <float.h>

#include "typedefs.h"
#include "7470res.h"
#include "w32sal.h"
#include "winvfx.h"
#include "gpiblib.h"
#include "specan.h"
#include "8566plt.h"
#include "49xplt.h"

#include "appfile.cpp"
#include "gfxfile.cpp"

#define VERSION "Version 2.08"

C8 szAppName[] = "HP7470A Plotter Emulator - "VERSION" ";

//
// Window handle created by SAL
//

HWND hWnd;
HINSTANCE hInstance;

//
// App's target window resolution and image margin
//

#define HPGL_CODE
#define PCL_CODE

#define s08 signed char
#define u08 unsigned char
#define u16 unsigned int
#define u32 unsigned long
#define COORD int
#define selected_pen gl_pen

void parse_hpgl(u08 c);
void parse_pcl(u08 c);
void render_init();
int scale_x(double x);
int scale_y(double y);
double unscale_x(COORD x);
double unscale_y(COORD y);

u08 in_hpgl = 1;               // 1=hpgl, 0=pcl
u08 force_lb_term;

#define BACKGROUND_PEN 1

int user_rotate;                     // user specified image rotation value
int background = BACKGROUND_PEN;     // screen background pen
int INI_background = BACKGROUND_PEN;
u08 gl_user_scaling = 0;       // flag set if user scale factors selected


//#define BIG_PLOT
#ifdef BIG_PLOT  //gggg
double GL_XMAX=(+8760.0);//(5140.0*1.400) // maximum page coordinate extents (plus a little margin for error)
double GL_YMAX=(+6760.0); //8200.0  //7479.0
double GL_XMIN=(-8760.0);//(-5140.0*1.400) // maximum page coordinate extents (plus a little margin for error)
double GL_YMIN=(-10000.0); //8200.0  //7479.0
u08 plotter_mode = 0;
#else
double GL_XMAX=(10250.0*1.040);// maximum page coordinate extents (plus a little margin for error)
double GL_YMAX=(7700.0*1.040); //8200.0  //7479.0
double GL_XMIN=(0.0);// maximum page coordinate extents (plus a little margin for error)
double GL_YMIN=(0.0); //8200.0  //7479.0
u08 plotter_mode = 0;
#endif

double gl_length, gl_width;  // plot size

#define MM_PER_GRID (0.025)
#define GL_DIAG_MM  (sqrt(((GL_XMAX-GL_XMIN)*(GL_XMAX-GL_XMIN))+((GL_YMAX-GL_YMIN)*(GL_YMAX-GL_YMIN)))*MM_PER_GRID)

double gl_ip_xmin, gl_ip_xmax;  // input point scale factors
double gl_ip_ymin, gl_ip_ymax;

double gl_sc_xmin, gl_sc_xmax;  // user scale factors
double gl_sc_ymin, gl_sc_ymax;

double gl_clip_x1, gl_clip_y1;  // input clipping window
double gl_clip_x2, gl_clip_y2;
u08 gl_clip;


S32 RES_X = 640;
S32 RES_Y = 480;

S32 menu_height = 0;

S32 requested_RES_X = 640;
S32 requested_RES_Y = 480;

C8   alt_colors = FALSE;
C8   antialias  = FALSE;
bool force_generic = FALSE;

S32 default_X_margin = 0;
S32 default_Y_margin = 0;

#define N_PENS       8

#define MAX_CAPTION_LEN 128
#define CAPTION_LOCATOR ";;; ;; ;;;"    // This string separates the original plot data from an added caption

//
// Max # of data sources we can overlay and/or browse
//

#define MAX_DATA_SOURCES 10000

//
// Windows/panes used by app
//

VFX_WINDOW *stage_window = NULL;
PANE       *stage = NULL;
PANE       *image = NULL;

VFX_WINDOW *screen_window = NULL;
PANE       *screen = NULL;

//
// 7470.ini
//

enum
{
   M_BROWSE = 0,     // Browse multiple sources one at a time 
   M_OVERLAY = 1     // Overlay multiple sources in list
};

S32 INI_mode = M_BROWSE;
S32 INI_res_x = 800;
S32 INI_res_y = 600;
S32 INI_pos_x = 99999;
S32 INI_pos_y = 99999;
S32 INI_x_margin = 8;
S32 INI_y_margin = 8;
C8  INI_color_scheme = 0;
C8  INI_antialias    = 0;
S32 INI_async_timeout = 3500;
S32 INI_serial_read_dropout = 500;
S32 INI_SP0_detect = 1;
S32 INI_min_plot_bytes = 128;
S32 INI_process_OS = 1;
S32 INI_default_board_addr = 5;
S32 INI_async_xfer_size = 500;
S32 INI_async_GPIB_timeout_ms = 1000;
S32 INI_auto_print_mode = 0;
S32 INI_use_default_printer = 0;
S32 INI_beep = 0;
bool INI_release_sys_control = FALSE;
bool INI_ignore_write_aborts = FALSE;

C8 INI_auto_save[MAX_PATH] = "";
C8 INI_beep_wav_filename[MAX_PATH] = "";
C8 INI_traffic_diag_filename[MAX_PATH] = "";
C8 INI_HPGL_preface[512] = "";
C8 temp_name[MAX_PATH];

C8 INI_caption_text[MAX_CAPTION_LEN] = "";

C8 INI_OE_reply[256] = "";
C8 INI_OH_reply[256] = "";
C8 INI_OI_reply[256] = "";
C8 INI_OP_reply[256] = "";
C8 INI_OO_reply[256] = "";
C8 INI_OF_reply[256] = "";
C8 INI_OA_reply[256] = "";
C8 INI_OC_reply[256] = "";

//
// 7470user.ini
//

C8 user_INI_pathname[MAX_PATH];

//
// Misc. globals and working buffers
//

APPDIRS global_dirs;

C8  DLG_box_caption[256];

S32 background_invalid;
S32 force_redraw;

S32 GPIB_polling = 0;
S32 GPIB_abort = 0;

S32 refresh_cnt = 0;

C8 synthesis_buffer[4194304];
C8 trace_buffer    [4194304];

S32 mouse_x = 0;
S32 mouse_y = 0;
S32 left_button = 0;
S32 right_button = 0;
S32 last_left_button = 0;
S32 last_right_button = 0;

const SINGLE degs_to_rads = 0.017453292F;

S32 sc_enabled = FALSE;

//
// GPIB library initialized if 1
//

S32 GPIB_init_status = 0;

//
// Array of data sources to overlay and/or browse
//

C8  data_source_file[MAX_DATA_SOURCES][MAX_PATH];
S32 data_source_GPIB[MAX_DATA_SOURCES];
S32 data_source_shortcut[MAX_DATA_SOURCES];
C8  data_source_rotate[MAX_DATA_SOURCES];
C8  data_source_background[MAX_DATA_SOURCES];
C8  data_source_alt[MAX_DATA_SOURCES];
C8  data_source_ant[MAX_DATA_SOURCES];
S32 n_data_sources;
S32 current_data_source;
S32 caption_target = -1;

//
// Optional array of named instruments used to populate Acquire menu
//

#define MAX_NAMED_INSTRUMENTS 20

struct ACQINST
{
   C8     name[256];
   S32    addr;
   WPARAM function_key;
};

ACQINST named_instruments[MAX_NAMED_INSTRUMENTS];
S32 n_named_instruments;

//
// UI state
//

HMENU hmenu;
S32 board_address = 5;

//
// We just #include the parser routines for now, since they're shared
// with the parse.cpp utility
//

#include "7470lex.cpp"

//
// Pen colors
//

S32 plot_order_colors[] = {
                   RGB_TRIPLET(0,0,255),         // Blue
                   RGB_TRIPLET(255,0,255),       // Magenta
                   RGB_TRIPLET(0,128,0),         // Dark green
                   RGB_TRIPLET(255,0,0),         // Red
                   RGB_TRIPLET(255,192,0),       // Dark yellow
                   RGB_TRIPLET(0,192,255),       // Dark cyan
                   };

#define N_PLOT_ORDER_COLORS 6

const S32 default_standard_pen_colors[N_PENS] =
   { RGB_TRIPLET(255,255,255),
     RGB_TRIPLET(0,0,0),
     RGB_TRIPLET(0,0,255),
     RGB_TRIPLET(0,192,0),
     RGB_TRIPLET(128,128,0),
     RGB_TRIPLET(255,0,0),
     RGB_TRIPLET(255,0,255),
     RGB_TRIPLET(0,255,255) };

S32 standard_pen_colors[N_PENS] =
   { RGB_TRIPLET(255,255,255),
     RGB_TRIPLET(0,0,0),
     RGB_TRIPLET(0,0,255),
     RGB_TRIPLET(0,192,0),
     RGB_TRIPLET(128,128,0),
     RGB_TRIPLET(255,0,0),
     RGB_TRIPLET(255,0,255),
     RGB_TRIPLET(0,255,255) };

S32 n_specified_pen_colors;
S32 using_default_pen_colors;

S32 pen_colors[N_PENS];

//
// Utility macros
//

#define LACS 4       // ibsta value corresponding to Listener ACceSs bit

#define FTOI(x) (S32((x)+0.5F))

//
// File menu
//

#define IDM_LOAD_HPGL    1
#define IDM_SAVE         2

#define IDM_PRINT_IMAGE  3
#define IDM_CLOSE_ALL    4
#define IDM_CLOSE        5
#define IDM_DELETE       6

#define IDM_QUIT         7

//
// View menu
//

#define IDM_REFRESH      100

#define IDM_BROWSE       101
#define IDM_OVERLAY      102

#define IDM_NEXT         103
#define IDM_PREV         104

//
// Display menu
//

#define IDM_512           200
#define IDM_400           201
#define IDM_640           202
#define IDM_800           203
#define IDM_1024          204
#define IDM_1280          205
#define IDM_ALT_COLORS    212
#define IDM_ANTIALIAS     213
#define IDM_ROTATE        214
#define IDM_SWAP          215

//
// Caption menu
//

#define IDM_EDIT_CAPTION  250
#define IDM_ERASE_CAPTION 251

//
// Acquire menu
//

#define IDM_LISTEN       300
#define IDM_LISTEN_PRINT 301
#define IDM_LISTEN_BEEP  302
#define IDM_REQUESTS     310
#define IDM_ACQUIRE_HW   350
#define IDM_SHORTCUTS    360

//
// GPIB menu
//

#define IDM_PLOTTER_ADDR 400

//
// Help menu
//

#define IDM_ABOUT        500
#define IDM_USER_GUIDE   501

//
// Prototypes
//

bool CMD_listen(bool   auto_print,
                S32    plotter_address,
                C8    *explicit_filename,
                S32    device_address = -1,
                C8    *device_command = NULL,
                bool   loop           = TRUE,
                C8    *preface        = NULL,
                bool   reset_to_local = TRUE);

bool read_492(S32 address, C8 *filename);

//
// Renderer
//

#include "renderer.cpp"

//****************************************************************************
//*                                                                          *
//*  Initialize main menu                                                    *
//*                                                                          *
//****************************************************************************

void main_menu_init(void)
{
   S32 i;

   if (hmenu != NULL)
      {
      SetMenu(hWnd,NULL);
      DestroyMenu(hmenu);
      }

   hmenu = CreateMenu();

   HMENU pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_LOAD_HPGL,    "&Load HP-GL/2 data... \t l");
   AppendMenu(pop, MF_STRING, IDM_SAVE,         "&Save image or HP-GL/2 data... \t s");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_PRINT_IMAGE,  "&Print image \t p");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_CLOSE_ALL,    "Close all visible plots \t Home");
   AppendMenu(pop, MF_STRING, IDM_CLOSE,        "Close current plot \t Del");
   AppendMenu(pop, MF_STRING, IDM_DELETE,       "Delete current plot file\t Ctrl-Del");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_QUIT,         "Quit \t q");

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&File");

   pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_REFRESH,      "Refresh current display\t Space or F5");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_BROWSE,       "Browse sources one a time\t b");
   AppendMenu(pop, MF_STRING, IDM_OVERLAY,      "Overlay all sources\t o");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_NEXT,         "Next source \t +");
   AppendMenu(pop, MF_STRING, IDM_PREV,         "Previous source \t -");

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&View");

   pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_512,          "512 x 384 \t 5");
   AppendMenu(pop, MF_STRING, IDM_400,          "640 x 400");
   AppendMenu(pop, MF_STRING, IDM_640,          "640 x 480 \t 6");
   AppendMenu(pop, MF_STRING, IDM_800,          "800 x 600 \t 8");
   AppendMenu(pop, MF_STRING, IDM_1024,         "1024 x 768 \t 0");
   AppendMenu(pop, MF_STRING, IDM_1280,         "1280 x 960 \t 9");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_ANTIALIAS,    "Antialiasing\t a");
   AppendMenu(pop, MF_STRING, IDM_ALT_COLORS,   "Alternate colors\t i");
   AppendMenu(pop, MF_STRING, IDM_SWAP,         "Background\t k");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_ROTATE,       "Rotate\t r");

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&Display");

   pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_EDIT_CAPTION, "Edit caption text... \t c"); 
   AppendMenu(pop, MF_STRING, IDM_ERASE_CAPTION,"Erase most-recently-added caption \t e");

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&Caption");

   pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_LISTEN,       "Wait for device-initiated plot \t w");
   AppendMenu(pop, MF_STRING, IDM_LISTEN_PRINT, "Wait for device-initiated plot, then print \t W");
   AppendMenu(pop, MF_STRING, IDM_LISTEN_BEEP,  "Beep when plot received");

   AppendMenu(pop, MF_SEPARATOR, 0, NULL);

   bool need_sep = FALSE;

   for (i=0; i < n_named_instruments ;i++)
      {
      AppendMenu(pop, MF_STRING, i+IDM_SHORTCUTS, named_instruments[i].name);
      need_sep = TRUE;
      }

   if (need_sep)
      {
      AppendMenu(pop, MF_SEPARATOR, 0, NULL);
      need_sep = FALSE;
      }

   for (i=1; i <= 30 ;i++)
      {
      C8 val[500];

      sprintf(val,"Request plot from supported device at address %d",i);

      AppendMenu(pop, MF_STRING, i+IDM_REQUESTS, val);
      }

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&Acquire");

   pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_PLOTTER_ADDR,  "No assigned plotter address (listen-only)");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);

   for (i=0; i <= 30 ;i++)
      {
      C8 val[500];

      sprintf(val,"Plotter addressable at %d",i);

      AppendMenu(pop, MF_STRING, i+IDM_PLOTTER_ADDR+1, val);
      }

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&GPIB");

   pop = CreateMenu();

   AppendMenu(pop, MF_STRING, IDM_USER_GUIDE,   "&User guide \t F1");
   AppendMenu(pop, MF_SEPARATOR, 0, NULL);
   AppendMenu(pop, MF_STRING, IDM_ABOUT,        "&About 7470");

   AppendMenu(hmenu, MF_POPUP, (UINT) pop,      "&Help");

   SetMenu(hWnd,hmenu);
   DrawMenuBar(hWnd);

   for (i=-1; i <= 30; i++)
      {
      if (i == board_address)
         {
         CheckMenuItem(hmenu, IDM_PLOTTER_ADDR+1+i, MF_CHECKED);
         }
      else
         {
         CheckMenuItem(hmenu, IDM_PLOTTER_ADDR+1+i, MF_UNCHECKED);
         }
      }

   CheckMenuItem(hmenu, IDM_LISTEN_BEEP, INI_beep ? MF_CHECKED : MF_UNCHECKED);
}

//****************************************************************************
//
// Launch HTML page from program directory
//
//****************************************************************************

void WINAPI launch_page(C8 *filename)
{
   C8 path[MAX_PATH];

   GetModuleFileName(NULL, path, sizeof(path)-1);

   _strlwr(path);

   C8 *exe = strstr(path,"7470.exe");

   if (exe != NULL)
      {
      strcpy(exe, filename);
      }
   else
      {
      strcpy(path, filename);
      }

   ShellExecute(NULL,    // hwnd
               "open",   // verb
                path,    // filename
                NULL,    // parms
                NULL,    // dir
                SW_SHOWNORMAL);
}

//****************************************************************************
//
// Return GPIB address if valid (0-32), else -1
//
//****************************************************************************

S32 valid_GPIB_address(C8 *string)
{
   if (strlen(string) > 3)
      {
      //
      // Long strings can't be a GPIB address unless they're of form "n ("
      //

      C8 *spc = strstr(string," (");

      if (spc == NULL)
         {
         return -1;
         }
      }

   //
   // Valid GPIB addresses are 0-31
   //

   if (!isdigit(string[0]))
      {
      return -1;
      }

   S32 addr = atoi(string);

   if ((addr <= 32) && (addr >= 0))
      {
      return addr;
      }

   return -1;
}

//****************************************************************************
//
// GPIB error handler
//
//****************************************************************************

void WINAPI GPIB_error(C8 *msg, S32 ibsta, S32 iberr, S32 ibcntl)
{
   SAL_alert_box("GPIB Error",msg);
   exit(1);
}

//****************************************************************************
//
// Send formatted string to GPIB (with optional diagnostic logging)
//
//****************************************************************************

void __cdecl GPIB_print(C8 *fmt, ...)
{
   static C8 work_string[16384];

   va_list ap;

   va_start(ap,
            fmt);

   vsprintf(work_string,
            fmt,
            ap);

   va_end(ap);

   //
   // Remove any trailing whitespace
   //

   S32 l = strlen(work_string);

   while (l > 0)
      {
      --l;

      if (!isspace((U8) work_string[l]))
         {
         break;
         }

      work_string[l] = 0;
      }

   if (INI_traffic_diag_filename[0])
      {
      FILE *temp = fopen(INI_traffic_diag_filename,"a+t");
      fprintf(temp,"Xmit: [%s]\n",work_string);
      fclose(temp);
      }

   GPIB_write(work_string,
             -1,
              FALSE,
              INI_ignore_write_aborts);
}

//****************************************************************************
//
// Send formatted string to GPIB, then wait for CR/LF to be transmitted
// as an acknowledgement
//
// Tek 49x commands (other than queries) need to go through here -- otherwise,
// they'll remain unacknowledged if a Prologix adapter is in use.  An
// unacknowledged command prevents subsequent commands from executing.
//
//****************************************************************************

void __cdecl GPIB_cmd_printf(C8 *fmt, ...)
{
   static C8 work_string[16384];

   va_list ap;

   va_start(ap,
            fmt);

   vsprintf(work_string,
            fmt,
            ap);

   va_end(ap);

   //
   // Remove any trailing whitespace
   //

   S32 l = strlen(work_string);

   while (l > 0)
      {
      --l;

      if (!isspace((U8) work_string[l]))
         {
         break;
         }

      work_string[l] = 0;
      }

   GPIB_print("%s",work_string);

   GPIB_read_ASC();
}

//****************************************************************************
//
// Fetch plot from HP 8566B or compatible spectrum analyzer
//
//****************************************************************************

bool read_856x(S32 address, //)
               C8 *filename)
{
   alt_colors = TRUE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                   "PLOT 550,279,9750,7479;",      // (Chosen to display unaliased trace in 
                   FALSE))                         // both 512x384 and 640x480 modes)
      {
      return FALSE;
      }

   return TRUE;
}


//****************************************************************************
//
// Fetch plot from HP 3577A/B network analyzer
//
//****************************************************************************

bool read_3577(S32 address, //)
               C8 *filename)
{
   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                   "PLA",                    
                   FALSE))                  
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Fetch plot from HP 8510 or 8753-series spectrum analyzer
// (contributed by A. Rosati)
//
//****************************************************************************

bool read_8510(S32 address, //)
               C8 *filename)
{
   alt_colors = FALSE;
     
   GPIB_connect(address,               // connect to GPIB instrument without
                GPIB_error,            // resetting it
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   C8 *ID = GPIB_query("OUTPERRO");    // ask for string to clear error state
   Sleep(250);

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "OUTPPLOT;",
                   FALSE,
                  "SC0,5750,0,4212;")) // (some versions don't provide this automatically;
      {                                // adding it here makes the plot fill the page)
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Fetch plot from HP 8753A or compatible spectrum analyzer
//
// (no longer used due to inconsistent plot sizing on various models; replaced 
// by 8510 routine above)
//
//****************************************************************************

bool read_8753(S32 address, //)
               C8 *filename)
{
   alt_colors = TRUE;
     
   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "OUTPPLOT",
                   FALSE,
                   NULL,
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Fetch plot from Advantest R3762AH or compatible spectrum analyzer
//
//****************************************************************************

bool read_3762AH(S32 address, //)
                 C8 *filename)
{
   alt_colors = TRUE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "PLTEXEC;",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Fetch plot from HP 3561A dynamic signal analyzer
//
//****************************************************************************

bool read_3561A(S32 address, //)
                C8 *filename)
{
   bool result = TRUE;

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   //
   // Query current grid state
   //

   GPIB_print("%s","SET?");

   S32 actual_len = 0;
   C8 *preamble = GPIB_read_BIN(4,TRUE,FALSE,&actual_len);

   assert(actual_len == 4);
   assert(preamble[0] == '#');
   assert(preamble[1] == 'A');
   
   S32 expect_bytes = ((S32) preamble[2]) * 256 + (S32) preamble[3];

   C8 *state = GPIB_read_BIN(expect_bytes, TRUE, FALSE, &actual_len);
   assert(actual_len == expect_bytes);

   S32 grid_state = !state[313];

   if (!grid_state)
      {
      GPIB_print("%s","GRID ON;");           // turn grid on if necessary
      }

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "PLT;",
                   FALSE))
      {
      result = FALSE;
      }

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   if (!grid_state)
      {
      GPIB_print("%s","GRID OFF;");          // turn grid back off
      }

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   return result;
}

//****************************************************************************
//
// Fetch plot from Tek 49xP/275xP spectrum analyzer
//
//****************************************************************************

bool read_49x(C8 *ID,    //)
              S32 address, 
              C8 *filename)
{
   //
   // If this is an original 492P or 496P, we can't get an HPGL plot 
   // from it -- synthesize one from a binary trace read instead
   //

   if ((!_strnicmp(ID,"ID TEK/492P",11)) ||      // TODO: ID for 492/6?
       (!_strnicmp(ID,"ID TEK/496P",11)))
      {
      return read_492(address, filename);
      }

   bool result = TRUE;

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   C8 grat_state[256];

   strcpy(grat_state,
         GPIB_query("GRAT?"));         // save graticule illumination state

   GPIB_cmd_printf("PTYPE HP7470");    // select HP7470 plotter compatibility
   GPIB_cmd_printf("GRAT ON");         // turn on graticule illumination

   //
   // Large synchronous reads in previous version are possible
   // only with correspondingly-large GPIB_read() buffers.  Buffers > 4K
   // are incompatible with GPIB-232, so we now do simulated-async reads
   // even on instruments that don't need them (like the Tek 49x and 24x0
   // series).
   //

#if 0

   FILE *out = fopen(filename,"w+t");  // open session file

   fprintf(out,"%s\n",
      GPIB_query("plot?"));            // send result of PLOT? to file

   if (ferror(out))
      {
      SAL_alert_box("Error","Could not write to %s", filename);
      }

   fclose(out);                        // close session file

#else

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "plot?",
                   FALSE))
      {
      result = FALSE;
      }

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   GPIB_init_status = 1;

#endif

   GPIB_cmd_printf("%s",grat_state);   // restore former grat illum state

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   return result;
}

//****************************************************************************
//
// Fetch plot from Tek SCD-series transient digitizer
//
//****************************************************************************

bool read_SCD(S32 address, //)
                C8 *filename)
{
   bool result = TRUE;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "plot?",
                   FALSE))
      {
      result = FALSE;
      }

   return result;
}

//****************************************************************************
//
// Fetch plot from Tek 271x spectrum analyzer
//
//****************************************************************************

bool read_271x(S32 address, //)
                 C8 *filename)
{
   bool result = TRUE;

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   C8 grat_state[256];

   strcpy(grat_state,
          GPIB_query("GRAT?"));        // save graticule illumination state

   GPIB_print("%s","PTYPE HPGL4");           // select 4-pin HPGL plotter compatibility
   GPIB_print("%s","GRAT ON");               // turn on graticule illumination

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "plot?",
                   FALSE))
      {
      result = FALSE;
      }

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                100000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   GPIB_print("%s",grat_state);              // restore former grat illum state

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   return result;
}

//****************************************************************************
//
// Fetch plot from Tek 2430A DSO
//
//****************************************************************************

bool read_2430(S32 address, //)
                 C8 *filename)
{
   bool result = TRUE;

   GPIB_connect(address,               // connect to GPIB instrument
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   GPIB_print("%s","device type:hpgl");
   GPIB_print("%s","device settings:on,grat:on,text:on,wavfrm:on");

   //
   // Large synchronous reads in previous version are possible
   // only with correspondingly-large GPIB_read() buffers.  Buffers > 4K
   // are incompatible with GPIB-232, so we now do simulated-async reads
   // even on instruments that don't need them (like the Tek 49x and 24x0
   // series).
   //

#if 0

   FILE *out = fopen(filename,"w+t");  // open session file

   fprintf(out,"%s\n",
      GPIB_query("print"));            // send result of PRINT to file

   if (ferror(out))
      {
      SAL_alert_box("Error","Could not write to %s", filename);
      }

   fclose(out);                        // close session file

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

#else

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "print",
                   FALSE))
      {
      result = FALSE;
      }

#endif

   return result;
}

//****************************************************************************
//
// Fetch plot from Tek 223X DSO
//
//****************************************************************************

bool read_223X(S32 address, //)
                 C8 *filename)
{
   bool result = TRUE;

   GPIB_connect(address, 
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   GPIB_print("%s","PLOT GRAT:ON,FORMAT:HPGL,SPEED:10,AUTO:OFF");

   GPIB_disconnect();
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "PLOT START",
                   FALSE))
      {
      result = FALSE;
      }

   return result;
}


//****************************************************************************
//
// Fetch plot from Tek TDS-series DSO
//
//****************************************************************************

bool read_TDS(S32 address, //)
                C8 *filename)
{
   bool result = TRUE;

   GPIB_connect(address, 
                GPIB_error,
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   GPIB_print("%s","HARDC:FORM HPGL");
   GPIB_print("%s","HARDC:LAYOUT LANDSCAPE");
// GPIB_print("%s","HARDC:PORT GPIB");

   GPIB_disconnect();
   GPIB_init_status = 0;

   if (!CMD_listen(FALSE,
                  -1,
                   filename,
                   address,
                  "HARDCOPY START",
                   FALSE))
      {
      result = FALSE;
      }

   return result;
}

//****************************************************************************
//
// Synthesize HP-GL/2 plot from HP 8566A / 8568A spectrum analyzer
//
//****************************************************************************

bool read_8566A_8568A(S32 address, //)
                        C8 *filename)
{
   alt_colors = FALSE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                   "HP 856xA",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Synthesize HP-GL/2 plot from Tek 2782/2784 spectrum analyzer, using its
// GPIB control port (rather than its dedicated plotter port)
//
//****************************************************************************

bool read_278x(S32 address, //)
               C8 *filename)
{
   alt_colors = FALSE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                   "Tektronix 278x",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Synthesize HP-GL/2 plot from HP 3585A/B spectrum analyzer
//
//****************************************************************************

bool read_3585(S32 address, //)
               C8 *filename)
{
   alt_colors = FALSE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                   "HP 3585",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Synthesize HP-GL/2 plot from Tek 492P or 496P spectrum analyzer
//
//****************************************************************************

bool read_492(S32 address, //)
              C8 *filename)
{
   alt_colors = FALSE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "Tektronix 492P",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Synthesize HP-GL/2 plot from HP 70000-series analyzer
//
//****************************************************************************

bool read_70000(S32 address, //)
                C8 *filename)
{
   alt_colors = FALSE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "HP 70000",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Synthesize HP-GL/2 plot from SCPI-compatible analyzers recognized by SPECAN.DLL
//
//****************************************************************************

bool read_SCPI(S32 address, //)
                C8 *filename)
{
   alt_colors = FALSE;

   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "SCPI",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}


//****************************************************************************
//
// Fetch plot from HP 54XXX DSO
// (54201A, 54111D tested)
//
//****************************************************************************

bool read_54XXX(S32 address, //)
                C8 *filename)
{
   if (!CMD_listen(FALSE,
                   board_address,
                   filename,
                   address,
                  "PLOT;",
                   FALSE))
      {
      return FALSE;
      }

   return TRUE;
}

//****************************************************************************
//
// Attempt to identify instrument and request an HP-GL/2 plot from it
//
//****************************************************************************

bool read_instrument_data(S32      address, //)
                          S32      optional_shortcut,
                          C8      *filename)
{
   bool result = TRUE;

   HCURSOR hcurSave = SetCursor(LoadCursor(0, IDC_WAIT));

   //
   // If shortcut used, check for specific instrument names that we
   // support but can't auto-detect with ID? commands
   //

   if (optional_shortcut != -1)
      {
      ACQINST *A = &named_instruments[optional_shortcut];

      address = A->addr;

      if ((strstr(A->name,"8566A") != NULL) ||
          (strstr(A->name,"8567A") != NULL) ||
          (strstr(A->name,"8568A") != NULL))
         {
         result = read_8566A_8568A(address, filename);

         SetCursor(hcurSave);
         return result;
         }

      if (strstr(A->name,"3585") != NULL)
         {
         result = read_3585(address, filename);

         SetCursor(hcurSave);
         return result;
         }

      if (strstr(A->name,"8510") != NULL)
         {
         result = read_8510(address, filename);

         SetCursor(hcurSave);
         return result;
         }

      if (strstr(A->name,"8753") != NULL)
         {
         result = read_8753(address, filename);

         SetCursor(hcurSave);
         return result;
         }

      if (strstr(A->name,"8702") != NULL)
         {
         result = read_8753(address, filename);

         SetCursor(hcurSave);
         return result;
         }

      if (strstr(A->name,"SCPI") != NULL)
         {
         result = read_SCPI(address, filename);

         SetCursor(hcurSave);
         return result;
         }
      }

   //
   // Send ID? query to instrument
   //
   // This will identify Tek instruments such as the 49x analyzers and TDS
   // oscilloscopes, and HP instruments such as the 8566B/8568B
   //

   GPIB_connect(address,               // connect to GPIB instrument without
                GPIB_error,            // resetting it
                0,
                15000,
                -1,
                INI_release_sys_control);

   GPIB_init_status = 1;

   GPIB_set_EOS_mode(10);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   C8 *ID = GPIB_query("ID?");         // ask for ID string

   GPIB_disconnect();                  // return instrument to local control
   GPIB_init_status = 0;

   if (!_strnicmp(ID,"ID TEK/49",9))
      {
      result = read_49x(ID, address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/2AP",10))
      {
      result = read_49x(ID, address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/5AP",10))  
      {
      result = read_49x(ID, address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/275",10))
      {
      result = read_49x(ID, address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/271",10))
      {
      result = read_271x(address, filename);
      }
   else if (!_strnicmp(ID,"TEK/278",7))
      {
      result = read_278x(address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/SCD",10))
      {
      result = read_SCD(address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/24",9))
      {
      result = read_2430(address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/22",9))
      {
      result = read_223X(address, filename);
      }
   else if (!_strnicmp(ID,"ID TEK/TDS",10))
      {
      result = read_TDS(address, filename);
      }
   else if (!_strnicmp(ID,"HP3561A",7))
      {
      result = read_3561A(address, filename);
      }
   else if (!_strnicmp(ID,"HP3577",6))
      {
      result = read_3577(address, filename);
      }
   else if (!_strnicmp(ID,"HP856",5))
      {
      result = read_856x(address, filename);
      }
   else if (!_strnicmp(ID,"HP859",5))
      {
      result = read_856x(address, filename);
      }
   else if (!_strnicmp(ID,"HP8576",6))
      {
      result = read_856x(address, filename);
      }
   else if (!_strnicmp(ID,"HP8578",6))
      {
      result = read_856x(address, filename);
      }
   else if (!_strnicmp(ID,"HP70",4))
      {
      result = read_70000(address, filename);
      }
   else if (!_strnicmp(ID,"HP71",4))
      {
      result = read_70000(address, filename);
      }
   else if (!_strnicmp(ID,"\"HP54201A\"",6))    // ID string has leading and trailing quotes 
      {                                         // Should work for HP542xxx DSO              
      result = read_54XXX(address, filename);
      }
   else if (!_strnicmp(ID,"HP54111D",5))        // ID string has no quotes
      {                                         // Should work for HP541xxx DSO
      result = read_54XXX(address, filename);
      }
   else
      {
#if 0
      //
      // ID? failed; try IDNT
      // TODO, borrow this beast from Bob F. and debug it
      //

      GPIB_startup(address,               // connect to GPIB instrument without
                   GPIB_error,            // resetting it
                   0);

      GPIB_init_status = 1;

      ID = GPIB_query("IDNT");

      GPIB_shutdown();                    // return instrument to local control
      GPIB_init_status = 0;

      C8 lower_ID[512];
      memset(lower_ID, 0, sizeof(lower_ID));
      strncpy(lower_ID, ID, 511);
      _strlwr(lower_ID);

      if (strstr(lower_ID,"r3762") != NULL)
         {
         result = read_3762AH(address, filename);
         }
      else
#endif
         {
         SAL_alert_box("Error","Attempt to read from unsupported instrument model '%s'",
            ID);
         exit(1);
         }
      }

   SetCursor(hcurSave);
   return result;
}

//****************************************************************************
//
// Compose text label for frequency in Hz, rounded to integer or 3 
// decimal places
//
//****************************************************************************

enum FT_LSD_MODE
{
   FT_LSD_E3,
   FT_LSD_HZ,
   FT_LSD_MHZ,
   FT_LSD_INT
};

C8 *freq_text(DOUBLE f, FT_LSD_MODE LSD)
{
   static C8 text[256];

   DOUBLE a = abs(f);

   if (LSD == FT_LSD_INT)
      {
           if (a >= 1E9) f /= 1E9;                 
      else if (a >= 1E6) f /= 1E6;                        
      else if (a >= 1E3) f /= 1E3;                        

      S32 d = (S32) ((f >= 0) ? (f + 0.5) : (f - 0.5));

      if (a >= 1E9)
         sprintf(text, "%d GHz", d);
      else if (a >= 1E6)
         sprintf(text, "%d MHz", d);
      else if (a >= 1E3)
         sprintf(text, "%d kHz", d);
      else if ((a > 0) && (a < 1.0))
         sprintf(text,"%0.1f Hz", f);       
      else
         sprintf(text,"%d Hz", d);
      }
   else if (LSD == FT_LSD_MHZ)
      {
      if (a >= 1E9)  
         {
         f /= 1E9;                 
         sprintf(text, "%0.3lf GHz", f);   // display with LSD rounded to nearest MHz
         }
      else
         {
         f /= 1E6;                        
         S32 d = (S32) ((f >= 0) ? (f + 0.5) : (f - 0.5));
         sprintf(text, "%d MHz", d);       // display integer MHz
         }
      }
   else if (LSD == FT_LSD_E3)
      {
      if (a >= 1E9)
         sprintf(text, "%0.3lf GHz", f / 1E9);   // display with LSD rounded to nearest MHz
      else if (a >= 1E6)
         sprintf(text, "%0.3lf MHz", f / 1E6);   // display with LSD rounded to nearest kHz
      else if (a >= 1E3)
         sprintf(text, "%0.3lf kHz", f / 1E3);   // display with LSD rounded to nearest Hz
      else
         sprintf(text,"%0.lf Hz", f);            // display with LSD rounded to nearest Hz 
      }
   else if (LSD == FT_LSD_HZ)
      {
      if (a >= 1E9)
         sprintf(text, "%0.9lf GHz", f / 1E9);   // display with LSD rounded to nearest Hz
      else if (a >= 1E6)
         sprintf(text, "%0.6lf MHz", f / 1E6);   // display with LSD rounded to nearest Hz
      else if (a >= 1E3)
         sprintf(text, "%0.3lf kHz", f / 1E3);   // display with LSD rounded to nearest Hz
      else
         sprintf(text,"%0.lf Hz", f);            // display with LSD rounded to nearest Hz 
      }

   return text;
}

//****************************************************************************
//
// Establish display resolution
//
//****************************************************************************

void set_display_mode(void)
{
   menu_height = GetSystemMetrics(SM_CYMENU);

   S32 width  = RES_X;
   S32 height = RES_Y + menu_height;

   if (!VFX_set_display_mode(width,
                             height,
                             16,
                             VFX_WINDOW_MODE,
                             FALSE))
      {
      exit(1);
      }

   if ((INI_pos_x != 99999) && (INI_pos_y != 99999))
      {
      SetWindowPos(hWnd,
                   NULL, 
                   INI_pos_x,
                   INI_pos_y,
                   0,
                   0,
                   SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

      INI_pos_x = 99999;      // (only move the window once, at startup)
      INI_pos_y = 99999;
      }

   //
   // Configure output window and clipping regions
   //

   if (stage_window)
      {
      VFX_window_destroy(stage_window);
      }

   stage_window = VFX_window_construct(width, height);

   if (stage)
      {
      VFX_pane_destroy(stage);
      }

   stage = VFX_pane_construct(stage_window,
                              0,
                              0,
                              width-1,
                              height-1);

   if (image)
      {
      VFX_pane_destroy(image);
      }

   image = VFX_pane_construct(stage_window,
                              0,
                              menu_height,
                              width-1,
                              height-1);

   VFX_assign_window_buffer(stage_window,
                            NULL,
                           -1);

   if (screen_window)
      {
      VFX_window_destroy(screen_window);
      }

   screen_window = VFX_window_construct(width, height);

   if (screen)
      {
      VFX_pane_destroy(screen);
      }

   screen = VFX_pane_construct(screen_window,
                               0,
                               0,
                               width-1,
                               height-1);

   background_invalid = TRUE;

   main_menu_init();

   //
   // Set overlay trace colors as native RGB words for compatibility with
   // font renderer
   //

   for (S32 i=0; i < N_PLOT_ORDER_COLORS; i++)
      {
      VFX_RGB color = *VFX_color_to_RGB(plot_order_colors[i]);

      plot_order_colors[i] = RGB_NATIVE(color.r, color.g, color.b);
      }
}

//****************************************************************************
//
// Center with respect to another window (from KB140722)
//
// Specifying NULL for hwndParent centers hwndChild relative to the
// screen
//
//****************************************************************************

bool CenterWindow(HWND hwndChild, HWND hwndParent)
{
   RECT rcChild, rcParent;
   int  cxChild, cyChild, cxParent, cyParent;
   int  cxScreen, cyScreen, xNew, yNew;
   HDC  hdc;

   GetWindowRect(hwndChild, &rcChild);
   cxChild = rcChild.right - rcChild.left;
   cyChild = rcChild.bottom - rcChild.top;

   if (hwndParent)
      {
      GetWindowRect(hwndParent, &rcParent);
      cxParent = rcParent.right - rcParent.left;
      cyParent = rcParent.bottom - rcParent.top;
      }
   else
      {
      cxParent = GetSystemMetrics (SM_CXSCREEN);
      cyParent = GetSystemMetrics (SM_CYSCREEN);
      rcParent.left = 0;
      rcParent.top  = 0;
      rcParent.right = cxParent;
      rcParent.bottom= cyParent;
      }

   hdc = GetDC(hwndChild);
   cxScreen = GetDeviceCaps(hdc, HORZRES);
   cyScreen = GetDeviceCaps(hdc, VERTRES);
   ReleaseDC(hwndChild, hdc);

   xNew = rcParent.left + ((cxParent - cxChild) / 2);

   if (xNew < 0)
      {
      xNew = 0;
      }
   else if ((xNew + cxChild) > cxScreen)
      {
      xNew = cxScreen - cxChild;
      }

   yNew = rcParent.top  + ((cyParent - cyChild) / 2);

   if (yNew < 0)
      {
      yNew = 0;
      }
   else if ((yNew + cyChild) > cyScreen)
      {
      yNew = cyScreen - cyChild;
      }

   return SetWindowPos(hwndChild,
                       NULL,
                       xNew, yNew,
                       0, 0,
                       SWP_NOSIZE | SWP_NOZORDER) != 0;
}

//****************************************************************************
//*                                                                          *
//*  Utility hook procedure to center save-filename dialogs                  *
//*                                                                          *
//****************************************************************************

UINT CALLBACK SFNHookProc(HWND hdlg,      // handle to the dialog box window //)
                          UINT uiMsg,     // message identifier
                          WPARAM wParam,  // message parameter
                          LPARAM lParam)  // message parameter
{
   if (uiMsg == WM_NOTIFY)
      {
      LPNMHDR pnmh = (LPNMHDR) lParam;

      if (pnmh->code == CDN_INITDONE)
         {
         CenterWindow(GetParent(hdlg), hWnd);
         }
      }

   return 0;
}

//****************************************************************************
//*                                                                          *
//*  Utility hook procedure to center filename dialogs                       *
//*                                                                          *
//****************************************************************************

UINT CALLBACK OFNHookProc(HWND hdlg,      // handle to the dialog box window //)
                          UINT uiMsg,     // message identifier
                          WPARAM wParam,  // message parameter
                          LPARAM lParam)  // message parameter
{
   if (uiMsg == WM_NOTIFY)
      {
      LPNMHDR pnmh = (LPNMHDR) lParam;

      if (pnmh->code == CDN_INITDONE)
         {
         CenterWindow(GetParent(hdlg), hWnd);
         }
      }

   return 0;
}

//****************************************************************************
//*                                                                          *
//*  Return rectangle containing client-area boundaries in screenspace       *
//*                                                                          *
//****************************************************************************

RECT *client_screen_rect(HWND hWnd)
{
   static RECT rect;
   POINT       ul,lr;

   GetClientRect(hWnd, &rect);

   ul.x = rect.left;
   ul.y = rect.top;
   lr.x = rect.right;
   lr.y = rect.bottom;

   ClientToScreen(hWnd, &ul);
   ClientToScreen(hWnd, &lr);

   SetRect(&rect, ul.x, ul.y,
                  lr.x-1, lr.y-1);

   return &rect;
}

//****************************************************************************
//
// Get list of all files in directory with given name specification
//
// TODO: This is sort of ugly -- depending on where the file came from 
// (e.g., the command line), its fully qualified pathname isn't written 
// to 7470user.ini, only its filename.  The only reason we don't get 
// error messages when we restart 7470.exe from a different working
// directory is that the wildcard search in this routine silently fails
// to find the same filename in the new CWD...
//
//****************************************************************************

void add_files_to_list(C8 *dirspec, C8 *filespec)
{
   C8 dir_buffer[MAX_PATH];
   strcpy(dir_buffer,dirspec);

   if (strlen(dir_buffer))
      {
      if (dir_buffer[strlen(dir_buffer)-1] != '\\')
         {
         strcat(dir_buffer,"\\");
         }
      }

   C8 name_buffer[MAX_PATH+4];

   strcpy(name_buffer,dir_buffer);
   strcat(name_buffer,filespec);

   HANDLE          search_handle;
   WIN32_FIND_DATA found;

   search_handle = FindFirstFile(name_buffer, &found);

   if ((search_handle != NULL) && (search_handle != INVALID_HANDLE_VALUE))
      {
      do
         {
         if (found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            {
            continue;
            }

         if (n_data_sources >= MAX_DATA_SOURCES)
            {
            break;
            }

         strcpy(name_buffer, dir_buffer);
         strcat(name_buffer, found.cFileName);

         data_source_GPIB    [n_data_sources] = -1;
         data_source_shortcut[n_data_sources] = -1;

         strcpy(data_source_file[n_data_sources++], name_buffer);
         }
      while (FindNextFile(search_handle, &found));

      FindClose(search_handle);
      }
}

void add_files_to_list(C8 *pathname)
{
   C8 buff[MAX_PATH] = { 0 };
   _snprintf(buff, sizeof(buff)-1, "%s", pathname);

   C8 *dirspec  = strrchr(buff, '\\');
   C8 *filespec = buff;

   if (dirspec != NULL)
      {
      *dirspec = 0;
      filespec = dirspec+1;
      dirspec  = buff;
      }
   else
      {
      dirspec = "";
      }

   add_files_to_list(dirspec, filespec);
}

//****************************************************************************
//*                                                                          *
//* Get filename of image to save                                            *
//*                                                                          *
//****************************************************************************

bool get_save_filename(C8 *string)
{
   OPENFILENAME fn;
   C8           fn_buff[MAX_PATH];

   fn_buff[0] = 0;

   if (strlen(string))
      {
      strcpy(fn_buff, string);
      }

   memset(&fn, 0, sizeof(fn));
   fn.lStructSize       = sizeof(fn);
   fn.hwndOwner         = hWnd;
   fn.hInstance         = NULL;

   #ifdef PCL_CODE
      fn.lpstrFilter       = "\
      HP-GL/2 data files (*.PLT)\0*.PLT\0\
      HP-GL/2 data files (*.HPG)\0*.HPG\0\
      HP-GL/2 data files (*.HGL)\0*.HGL\0\
      HP-GL/2 data files (*.PGL)\0*.PGL\0\
      HP-GL/2 data files (*.PCL)\0*.PCL\0\
      Image files (*.GIF)\0*.GIF\0\
      Image files (*.TGA)\0*.TGA\0\
      Image files (*.BMP)\0*.BMP\0\
      Image files (*.PCX)\0*.PCX\0\
      ";
   #else  
      fn.lpstrFilter       = "\
      HP-GL/2 data files (*.PLT)\0*.PLT\0\
      HP-GL/2 data files (*.HPG)\0*.HPG\0\
      HP-GL/2 data files (*.HGL)\0*.HGL\0\
      HP-GL/2 data files (*.PGL)\0*.PGL\0\
      Image files (*.GIF)\0*.GIF\0\
      Image files (*.TGA)\0*.TGA\0\
      Image files (*.BMP)\0*.BMP\0\
      Image files (*.PCX)\0*.PCX\0\
      ";
   #endif

   fn.lpstrCustomFilter = NULL;
   fn.nMaxCustFilter    = 0;
   fn.nFilterIndex      = 1;
   fn.lpstrFile         = fn_buff;
   fn.nMaxFile          = sizeof(fn_buff);
   fn.lpstrFileTitle    = NULL;
   fn.nMaxFileTitle     = 0;
   fn.lpstrInitialDir   = NULL;
   fn.lpstrTitle        = "Save";
   fn.Flags             = OFN_EXPLORER |
                          OFN_LONGNAMES |
                          OFN_ENABLESIZING |
                          OFN_NOCHANGEDIR |
                          OFN_ENABLEHOOK |
                          OFN_PATHMUSTEXIST |
                          OFN_OVERWRITEPROMPT |
                          OFN_HIDEREADONLY;
   fn.nFileOffset       = 0;
   fn.nFileExtension    = 0;
   fn.lpstrDefExt       = NULL;
   fn.lCustData         = NULL;
   fn.lpfnHook          = SFNHookProc;
   fn.lpTemplateName    = NULL;

   if (!GetSaveFileName(&fn))
      {
      return FALSE;
      }

   strcpy(string, fn_buff);

   //
   // Force string to end in a valid suffix
   //

   C8 *suffixes[] =
      {
      ".plt",
      ".hpg",
      ".hgl",
      ".pgl",
#ifdef PCL_CODE
      ".pcl",
#endif
      ".gif",
      ".tga",
      ".bmp",
      ".pcx",
      };

   if (fn.nFilterIndex-1 < ARY_CNT(suffixes))
      {
      S32 l = strlen(string);

      for (S32 i=0; i < ARY_CNT(suffixes); i++)
         {
         if (l >= 4)
            {
            if (!_stricmp(&string[l-4],suffixes[i]))
               {
               return TRUE;
               }
            }
         }

      strcat(string, suffixes[fn.nFilterIndex-1]);
      }

   return TRUE;
}

//****************************************************************************
//*                                                                          *
//* Get filename of plotter file to load                                     *
//*                                                                          *
//****************************************************************************

bool get_load_filename(C8 *string)
{
   OPENFILENAME fn;
   C8           fn_buff[MAX_PATH];

   fn_buff[0] = 0;

   if (strlen(string))
      {
      strcpy(fn_buff, string);
      }

   memset(&fn, 0, sizeof(fn));
   fn.lStructSize       = sizeof(fn);
   fn.hwndOwner         = hWnd;
   fn.hInstance         = NULL;
#ifdef PCL_CODE
   fn.lpstrFilter       = "HP-GL/2 files (*.PLT)\0*.PLT\0HP-GL/2 files (*.HPG)\0*.HPG\0HP-GL/2 files (*.HGL)\0*.HGL\0HP-GL/2 files (*.PGL)\0*.PGL\0HP-GL/2 files (*.PCL)\0*.PCL\0All files (*.*)\0*.*\0";
#else
   fn.lpstrFilter       = "HP-GL/2 files (*.PLT)\0*.PLT\0HP-GL/2 files (*.HPG)\0*.HPG\0HP-GL/2 files (*.HGL)\0*.HGL\0HP-GL/2 files (*.PGL)\0*.PGL\0All files (*.*)\0*.*\0";
#endif
   fn.lpstrCustomFilter = NULL;
   fn.nMaxCustFilter    = 0;
   fn.nFilterIndex      = 1;
   fn.lpstrFile         = fn_buff;
   fn.nMaxFile          = sizeof(fn_buff);
   fn.lpstrFileTitle    = NULL;
   fn.nMaxFileTitle     = 0;
   fn.lpstrInitialDir   = NULL;
   fn.lpstrTitle        = "Load HP-GL/2 Plotter File";
   fn.Flags             = OFN_EXPLORER |
                          OFN_LONGNAMES |
                          OFN_ENABLESIZING |
                          OFN_NOCHANGEDIR |
                          OFN_ENABLEHOOK |
                          OFN_PATHMUSTEXIST |
                          OFN_HIDEREADONLY;
   fn.nFileOffset       = 0;
   fn.nFileExtension    = 0;
   fn.lpstrDefExt       = NULL;
   fn.lCustData         = NULL;
   fn.lpfnHook          = OFNHookProc;
   fn.lpTemplateName    = NULL;

   if (!GetOpenFileName(&fn))
      {
      return FALSE;
      }

   strcpy(string, fn_buff);

   //
   // Force string to end in a valid suffix
   //

   C8 *suffixes[] =
      {
      ".plt",
      ".hpg",
      ".hgl",
#ifdef PCL_CODE
      ".pgl",
      ".pcl"
#else
      ".pgl"
#endif
      };

   if (fn.nFilterIndex-1 < ARY_CNT(suffixes))
      {
      S32 l = strlen(string);

      for (S32 i=0; i < ARY_CNT(suffixes); i++)
         {
         if (l >= 4)
            {
            if (!_stricmp(&string[l-4],suffixes[i]))
               {
               return TRUE;
               }
            }
         }

      strcat(string, suffixes[fn.nFilterIndex-1]);
      }

   return TRUE;
}

//****************************************************************************
//*
//* Support routines to dump bitmap to printer
//*
//* Source: http://support.microsoft.com/default.aspx?scid=kb;EN-US;q186736
//*
//****************************************************************************

UINT_PTR CALLBACK PrintHookProc(HWND hdlg,      // handle to the dialog box window //)
                                UINT uiMsg,     // message identifier
                                WPARAM wParam,  // message parameter
                                LPARAM lParam)  // message parameter
{
   if (uiMsg == WM_INITDIALOG) 
      {
      CenterWindow(hdlg, GetParent(hdlg));
      }

   return 0;
}

// Return a HDC for the default or selected printer
HDC GetPrinterDC(void)
{
   PRINTDLG pdlg;    
   memset(&pdlg, 0, sizeof(pdlg));
   pdlg.lStructSize = sizeof( PRINTDLG );
   pdlg.nCopies = 1;

   if (!INI_use_default_printer)
      {
      pdlg.Flags = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION | PD_USEDEVMODECOPIESANDCOLLATE | PD_ENABLEPRINTHOOK | PD_ENABLESETUPHOOK;
      pdlg.lpfnPrintHook = PrintHookProc;
      pdlg.lpfnSetupHook = PrintHookProc;

      if (!PrintDlg(&pdlg))
         {
         return NULL;
         }
      }
   else
      {
      pdlg.Flags = PD_RETURNDEFAULT | PD_RETURNDC;
      PrintDlg(&pdlg);
      }

   return pdlg.hDC;
}

// Create a copy of the current system palette.
HPALETTE GetSystemPalette()
{
    HDC hDC;
    HPALETTE hPal;
    LPLOGPALETTE lpLogPal;

    // Get a DC for the desktop.
    hDC = GetDC(NULL);

    // Check to see if you are a running in a palette-based video mode.
    if (!(GetDeviceCaps(hDC, RASTERCAPS) & RC_PALETTE)) {
        ReleaseDC(NULL, hDC);
        return NULL;
    }

    // Allocate memory for the palette.
    lpLogPal = (tagLOGPALETTE *) GlobalAlloc(GPTR, sizeof(LOGPALETTE) + 256 *
                           sizeof(PALETTEENTRY));
    if (!lpLogPal)
        return NULL;

    // Initialize.
    lpLogPal->palVersion = 0x300;
    lpLogPal->palNumEntries = 256;

    // Copy the current system palette into the logical palette.
    GetSystemPaletteEntries(hDC, 0, 256,
        (LPPALETTEENTRY)(lpLogPal->palPalEntry));

    // Create the palette.
    hPal = CreatePalette(lpLogPal);

    // Clean up.
    GlobalFree(lpLogPal);
    ReleaseDC(NULL, hDC);

    return hPal;
}

// Create a 24-bit-per-pixel surface.
HBITMAP Create24BPPDIBSection(HDC hDC, int iWidth, int iHeight)
{
    BITMAPINFO bmi;
    HBITMAP hbm;

    // Initialize to 0s.
    ZeroMemory(&bmi, sizeof(bmi));

    // Initialize the header.
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = iWidth;
    bmi.bmiHeader.biHeight = iHeight;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 24;
    bmi.bmiHeader.biCompression = BI_RGB;

    // Create the surface.
    void *ptr;
    hbm = CreateDIBSection(hDC, &bmi, DIB_RGB_COLORS, &ptr, NULL, 0);

    return(hbm);
}

// Print the entire contents of the specified window to the default printer

bool PrintWindowToDC(HWND hWnd)
{
    HBITMAP hbm;
    HDC     hdcPrinter;
    HDC     hdcMemory;
    HDC     hdcWindow;
    int     iWidth;
    int     iHeight;
    DOCINFO di;
    RECT    rc,wrc,crc;
    DIBSECTION ds;
    HPALETTE   hPal;

    // Do you have a valid window?
    if (!IsWindow(hWnd))
        return FALSE;

    // Get a HDC for the default printer.
    hdcPrinter = GetPrinterDC();
    if (!hdcPrinter)
        return FALSE;

    // Get the HDC for the entire window.
    hdcWindow  = GetWindowDC(hWnd);

    // Get the rectangles bounding the window and client area.
    GetWindowRect(hWnd, &wrc);

    crc = *client_screen_rect(hWnd);

    int cw = crc.right-crc.left+1;
    int ch = crc.bottom-crc.top+1;

    rc.left   = crc.left - wrc.left;
    rc.top    = crc.top  - wrc.top;
    rc.right  = rc.left + cw - 1;
    rc.bottom = rc.top + ch - 1;

    //
    // Get the resolution of the printer device in square pixels
    //

    iWidth  = GetDeviceCaps(hdcPrinter, HORZRES);
    iHeight = GetDeviceCaps(hdcPrinter, VERTRES);

    //
    // Maintain aspect ratio of printed image
    //

    if (iHeight > iWidth)
      {
      //
      // Page is taller than it is wide (portrait mode)
      //
      // Scale the height to conform to the client area's size
      //

      iHeight = iWidth * ch / cw;
      }
   else
      {
      //
      // Page is wider than it is tall (landscape mode)
      //
      // Leave the width and height alone for a full-page printout
      //
      }

    //
    // Create the intermediate drawing surface at client resolution
    //
    // (16bpp DIB sections don't work with LaserJets for some reason, although they're
    // fine with inkjets)
    //

    S32 width  = rc.right-rc.left+1;
    S32 height = rc.bottom-rc.top+1;

    hbm = Create24BPPDIBSection(hdcWindow, width, height);

    if (!hbm) {
        DeleteDC(hdcPrinter);
        ReleaseDC(hWnd, hdcWindow);
        return FALSE;
    }

    // Prepare the surface for drawing.
    hdcMemory = CreateCompatibleDC(hdcWindow);
    SelectObject(hdcMemory, hbm);

    // Get the current system palette.
    hPal = GetSystemPalette();

    // If a palette was returned.
    if (hPal) {
        // Apply the palette to the source DC.
        SelectPalette(hdcWindow, hPal, FALSE);
        RealizePalette(hdcWindow);

        // Apply the palette to the destination DC.
        SelectPalette(hdcMemory, hPal, FALSE);
        RealizePalette(hdcMemory);
    }

    // Copy the window contents to the memory surface.
    BitBlt(hdcMemory, 0, 0, rc.right-rc.left+1, rc.bottom-rc.top+1,
        hdcWindow, rc.left, rc.top, SRCCOPY);

    // Prepare the DOCINFO.
    ZeroMemory(&di, sizeof(di));
    di.cbSize = sizeof(di);
    di.lpszDocName = "Window Contents";

    // Initialize the print job.
    if (StartDoc(hdcPrinter, &di) > 0) {

        // Prepare to send a page.
        if (StartPage(hdcPrinter) > 0) {

            // Retrieve the information describing the surface.
            GetObject(hbm, sizeof(DIBSECTION), &ds);

            // Print the contents of the surface.
            StretchDIBits(hdcPrinter,
                0, 0, iWidth, iHeight,
                0, 0, rc.right-rc.left+1, rc.bottom-rc.top+1,
                ds.dsBm.bmBits,
                (LPBITMAPINFO)&ds.dsBmih,
                DIB_RGB_COLORS,
                SRCCOPY);

            // Let the driver know the page is done.
            EndPage(hdcPrinter);
        }

        // Let the driver know the document is done.
        EndDoc(hdcPrinter);
    }

    // Clean up the objects you created.
    DeleteDC(hdcPrinter);
    DeleteDC(hdcMemory);
    ReleaseDC(hWnd, hdcWindow);
    DeleteObject(hbm);
    if (hPal)
        DeleteObject(hPal);

    return TRUE;
}

// Print the entire contents of the SAL back buffer to the selected printer

bool PrintBackBufferToDC(void)
{
    HBITMAP    hbm;
    HDC        hdcPrinter;
    HDC        hdcMemory;
    HDC        hdcWindow;
    int        iWidth;
    int        iHeight;
    S32        cw;
    S32        ch;
    DOCINFO    di;
    DIBSECTION ds;
    HPALETTE   hPal;

    // Get a HDC for the default printer.
    hdcPrinter = GetPrinterDC();
    if (!hdcPrinter)
        return FALSE;

    // Get back buffer dimensions and DC
    SAL_get_back_buffer_DC(&hdcWindow);

    SAL_client_resolution(&cw, &ch);

    //
    // Get the resolution of the printer device in square pixels
    //

    iWidth  = GetDeviceCaps(hdcPrinter, HORZRES);
    iHeight = GetDeviceCaps(hdcPrinter, VERTRES);

    //
    // Maintain aspect ratio of printed image
    //

    if (iHeight > iWidth)
      {
      //
      // Page is taller than it is wide (portrait mode)
      //
      // Scale the height to conform to the client area's size
      //

      iHeight = iWidth * ch / cw;
      }
   else
      {
      //
      // Page is wider than it is tall (landscape mode)
      //
      // Leave the width and height alone for a full-page printout
      //
      }

    //
    // Create the intermediate drawing surface at client resolution
    //
    // (16bpp DIB sections don't work with LaserJets for some reason, although they're
    // fine with inkjets)
    //

    hbm = Create24BPPDIBSection(hdcWindow, cw, ch);

    if (!hbm) {
        DeleteDC(hdcPrinter);
        SAL_release_back_buffer_DC(hdcWindow);
        return FALSE;
    }

    // Prepare the surface for drawing.
    hdcMemory = CreateCompatibleDC(hdcWindow);
    SelectObject(hdcMemory, hbm);

    // Get the current system palette.
    hPal = GetSystemPalette();

    // If a palette was returned.
    if (hPal) {
        // Apply the palette to the source DC.
        SelectPalette(hdcWindow, hPal, FALSE);
        RealizePalette(hdcWindow);

        // Apply the palette to the destination DC.
        SelectPalette(hdcMemory, hPal, FALSE);
        RealizePalette(hdcMemory);
    }

    // Copy the window contents to the memory surface.
    BitBlt(hdcMemory, 0, 0, cw, ch,
        hdcWindow, 0, 0, SRCCOPY);

    // Prepare the DOCINFO.
    ZeroMemory(&di, sizeof(di));
    di.cbSize = sizeof(di);
    di.lpszDocName = "Window Contents";

    // Initialize the print job.
    if (StartDoc(hdcPrinter, &di) > 0) {

        // Prepare to send a page.
        if (StartPage(hdcPrinter) > 0) {

            // Retrieve the information describing the surface.
            GetObject(hbm, sizeof(DIBSECTION), &ds);

            // Print the contents of the surface.
            StretchDIBits(hdcPrinter,
                0, 0, iWidth, iHeight,
                0, 0, cw,     ch,
                ds.dsBm.bmBits,
                (LPBITMAPINFO)&ds.dsBmih,
                DIB_RGB_COLORS,
                SRCCOPY);

            // Let the driver know the page is done.
            EndPage(hdcPrinter);
        }

        // Let the driver know the document is done.
        EndDoc(hdcPrinter);
    }

    // Clean up the objects you created.
    DeleteDC(hdcPrinter);
    DeleteDC(hdcMemory);
    SAL_release_back_buffer_DC(hdcWindow);
    DeleteObject(hbm);
    if (hPal)
        DeleteObject(hPal);

    return TRUE;
}

BOOL CALLBACK CapDlgProc (HWND   hDlg,  
                          UINT   message,
                          WPARAM wParam,
                          LPARAM lParam)
{
   switch (message)
      {
      case WM_INITDIALOG:
         {
         //
         // Center dialog on screen
         //

         S32 screen_w = GetSystemMetrics(SM_CXSCREEN); 
         S32 screen_h = GetSystemMetrics(SM_CYSCREEN); 

         LPNMHDR pnmh = (LPNMHDR) lParam;

         RECT r;

         GetWindowRect(hDlg, &r);

         r.right  -= r.left;
         r.bottom -= r.top;

         r.left = (screen_w - r.right)  / 2;
         r.top  = (screen_h - r.bottom) / 2;

         MoveWindow(hDlg, r.left, r.top, r.right, r.bottom, TRUE);

         //
         // Set caption for window
         //

         SetWindowText(hDlg, DLG_box_caption);

         //
         // Set initial control values
         //

         SetWindowText(GetDlgItem(hDlg,IDC_CAPTION), INI_caption_text);
         return TRUE;
         }

      case WM_COMMAND:
         {
         //
         // Update context-sensitive help text
         //

         if ((HIWORD(wParam) == EN_SETFOCUS) || (HIWORD(wParam) == 0))
            {
            S32 ID = LOWORD(wParam);

            switch (ID)
               {
               case IDC_CAPTION:
                  {
                  SetDlgItemText(hDlg, IDC_HELPGROUP, "Edit caption");
                  SetDlgItemText(hDlg, IDC_HELPBOX,   "\
Enter text to be added to the currently-viewed trace at the clicked location.\n\nTo erase the last user caption added, select \"Caption->Erase most-recently-added caption\" or press the 'e' key.");
                  break;
                  }
               }
            }

         //
         // Process input
         // 

         switch (LOWORD(wParam))
            {
            //
            // Accept/cancel buttons return to CMD_caption()
            //

            case IDOK:
               {
               //
               // Copy values from control windows back to DLG_ variables
               // prior to returning
               //

               memset(INI_caption_text,  0, sizeof(INI_caption_text));      
               GetWindowText(GetDlgItem(hDlg, IDC_CAPTION), INI_caption_text, sizeof(INI_caption_text)-1);

               EndDialog(hDlg, 1);
               return TRUE;
               }

            case IDCANCEL:
               {
               EndDialog(hDlg, 0);
               return FALSE;
               }
            }
         break;
         }
      }

   return FALSE;
}

//****************************************************************************
//*
//*  Refresh screen_window
//*
//****************************************************************************

void refresh(void)
{
   //
   // Lock the buffer and validate the VFX_WINDOW
   //

   VFX_lock_window_surface(screen_window,VFX_BACK_SURFACE);

   //
   // Copy entire staging pane to screen_window
   //

   VFX_pane_copy(stage,0,0,screen,0,0,NO_COLOR);

   //
   // Release surface and perform page flip
   //

   VFX_unlock_window_surface(screen_window, TRUE);
}

//****************************************************************************
//*
//*  Render the GPIB data file to screen
//*
//****************************************************************************

S32    char_abs_size = FALSE;
SINGLE char_scale_w = 0.0075F;
SINGLE char_scale_h = 0.015F;

S32 X_MARGIN = 0;
S32 Y_MARGIN = 0;

SINGLE display_width  = 640.0F;
SINGLE display_height = 480.0F;

bool clipped = FALSE;
S32 clip_x0;
S32 clip_y0;
S32 clip_x1;
S32 clip_y1;

C8 *add_caption = NULL;
bool erase_caption = FALSE;

const SINGLE em_width_pixels = 8.0F;
const SINGLE em_height_pixels = 8.0F;

C8 *find_last_caption(C8 *plot_data)
{
   C8 *caption = strstr(plot_data,CAPTION_LOCATOR);

   while (caption != NULL)
      {
      C8 *next_cap = strstr(&caption[strlen(CAPTION_LOCATOR)],CAPTION_LOCATOR);

      if (next_cap == NULL)
         {
         break;
         }

      caption = next_cap;
      }

   return caption;
}

void render_file(C8    *input_filename, //)
                 S32    pen1_color,
                 S32    default_pen_color,
                 S32    text_color,
                 S32    render_text,
                 S32    file_index)
{
char c;

   X_MARGIN = default_X_margin;
   Y_MARGIN = default_Y_margin;

   display_width  = SINGLE(RES_X) - (2 * X_MARGIN);
   display_height = SINGLE(RES_Y) - (2 * Y_MARGIN);

   clipped = FALSE;

   // -------------------------------
   // Load plotter data file and set
   // global vars needed by parser
   // -------------------------------

   S32 plot_len;

   C8 filename[MAX_PATH];

   if (input_filename[0] == '\"')
      {
      strcpy(filename, &input_filename[1]);
      }
   else
      {
      strcpy(filename, input_filename);
      }

   if (filename[strlen(filename)-1] == '\"')
      {
      filename[strlen(filename)-1] = 0;
      }

   plot_filename = filename;

   plot_data = (C8 *) FILE_read(filename,
                               &plot_len);
   if (plot_data == NULL)
      {
      SAL_alert_box("Error","Couldn't read %s\n",filename);
      exit(1);
      }

   //
   // Remove ?$T sequences from R&S FSM plot (Andreas P. 27-Jul-21)
   //

   C8 *seq = NULL;

   while (((seq = strstr(plot_data,"?$T")) != NULL))
      {
      memmove(seq, &seq[3], strlen(&seq[3])+1);
      }

   //
   // For the benefit of C string functions, try to zero-terminate plot if            
   // it ends in another terminator and was acquired before V1.92           
   //
   // This can still allow crashes with arbitrary plots from other sources
   // (e.g., ADC2's .hpg files), so we'll reallocate and append a zero if 
   // one is still needed
   //

   plot_end = &plot_data[plot_len];
   C8 *plot_term = &plot_data[plot_len-1];

   if ((*plot_term == 10) || (*plot_term == 26))
      {
      *plot_term = 0;      
      }                    

   if (*plot_term != 0)
      {
      plot_len++;

      C8 *temp = (C8 *) malloc(plot_len);
      assert(temp != NULL);

      memcpy(temp, plot_data, plot_len-1);
      free(plot_data);
      plot_data = NULL;

      temp[plot_len-1] = 0;

      plot_data = temp;
      plot_end  = &plot_data[plot_len];
      }
    
   //
   // Erase the last user caption in the file, if requested
   //

   bool file_modified = FALSE;

   if ((INI_mode == M_BROWSE) || (file_index == caption_target))
      {
      if (erase_caption)
         {
         C8 *caption = find_last_caption(plot_data);

         if (caption != NULL)
            {
            C8 *LB = strstr(caption,"LB");
            LB += 2;
            C8 *term = strchr(LB,3);

            if (term != NULL)
               {
               memcpy(INI_caption_text, LB, term-LB);
               INI_caption_text[term-LB] = 0;      // (to make it easy to move the caption around) 
               }

            S32 cl = strlen(caption);

            plot_len -= cl;
            plot_end -= cl;

            plot_term = caption;
            *plot_term = 0;

            file_modified = TRUE;
            }

         erase_caption = FALSE;
         caption_target = -1;
         }
                              
      //
      // Add user caption, if requested
      //

      if (add_caption != NULL)
         {
         strcpy(temp_name, filename);
         _strlwr(temp_name);
//       if ((*plot_term != 0) || (((S32) strlen(plot_data)) != plot_len-1))
         if ((*plot_term != 0) || strstr(temp_name, ".pcl") || (((S32) strlen(plot_data)) != plot_len-1))
            {
            SAL_alert_box("Error","Captioning not supported for this file");
            }
         else
            {
            plot_len += strlen(add_caption);

            C8 *temp = (C8 *) malloc(plot_len);
            assert(temp != NULL);

            strcpy(temp, plot_data);
            free(plot_data);
            plot_data = NULL;

            strcat(temp, add_caption);

            plot_data = temp;
            plot_end  = &plot_data[plot_len];

            file_modified = TRUE;
            }

         free(add_caption);
         add_caption = NULL;
         caption_target = -1;
         }
      }

   //
   // If file was modified by addition or deletion of a caption, write it back
   //

   if (file_modified)
      {
      FILE *out = fopen(filename,"wb");

      if ((out==NULL) || (fwrite(plot_data, plot_len, 1, out) != 1))
         {
         SAL_alert_box("Error","File %s could not be modified",filename);
         }

      if (out != NULL)
         {
         fclose(out);
         }
      }

   //
   // Remove CR/LF sequences (and additional junk chars) that appear at end of physical
   // lines in HP 54110D
   //
   // To avoid altering LB statements (HP 8566B, HP 3040A), do this only if at
   // least one !\r\n sequence appears in the file, and no DT\r statement appears
   //

   if ((strstr(plot_data,"DT\r")  == NULL) &&
       (strstr(plot_data,"!\r\n") != NULL))
      {
      while (1)
         {
         C8 *t = strstr(plot_data,"!\r\n ");

         if (t == NULL)
            {
            break;
            }

         memmove(t, &t[4], plot_end-t-4);

         plot_len  -= 4;
         plot_end  -= 4;
         *plot_end = 0;
         }

      while (1)
         {
         C8 *t = strstr(plot_data,"\r\n");

         if (t == NULL)
            {
            break;
            }

         memmove(t, &t[2], plot_end-t-2);

         plot_len  -= 2;
         plot_end  -= 2;
         *plot_end = 0;
         }
      }

   ptr = plot_data;

   //
   // If this plot is from a Tektronix 2782 or 2784 and pen colors have not
   // been changed from their defaults, set alternate color set
   //

   S32 alt_pen_set = alt_colors;

   if ((strstr(ptr,"Tektronix     2784")) ||
       (strstr(ptr,"Tektronix     2782")))
      {
      if (using_default_pen_colors)    // do this if we haven't specified our own pen colors in the .INI file
         {
         alt_pen_set ^= 1;
         }
      }

   //
   // Kludge to allow HP 4145B plots to occupy the full page
   //

   if (strstr(ptr,"DF;DF;IP 200, 200, 7400, 4800;SC0,2456,0,2047;"))
      {
      ptr[18] = '1'; ptr[19] = '0'; 
      ptr[25] = '7'; ptr[26] = '4';
      }

   ptr = plot_data;

   S32 font_height = VFX_font_height(VFX_default_system_font());
   S32 font_width  = VFX_character_width(VFX_default_system_font(), '_');

   // -------------------------------
   // Plotter state
   // -------------------------------

   S32 done = 0;

   #define PEN 0    // Use selected_pen for independent pen state, 0 for global state (like 7470)

//qqqq   S32    selected_pen = 1;   // HP 54110D never does an SP, so we must default to a nonzero pen

   S32    pen_state   [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   S32    pen_rel_mode[N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   S32    pen_sym_mode[N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE pen_x       [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE pen_y       [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE pen_x_lb    [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE pen_y_lb    [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE plot_x      [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE plot_y      [N_PENS]  = { 0, 0, 0, 0, 0, 0, 0, 0 };
   SINGLE pen_tick_p  [N_PENS]  = { 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 };
   SINGLE pen_tick_n  [N_PENS]  = { 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 };

   S32 char_set = 0;

   S32 graticule_X_bug_state = 0;

   S32 label_terminator = 3;

   U16 font_colors[256];

   sc_enabled = FALSE;

   char_abs_size = FALSE;
   char_scale_w = 0.0075F;
   char_scale_h = 0.015F;

   //
   // Was this plot acquired with an earlier version of 7470, using either
   // the built-in 8566B HP-GL template or a PLOT 250,250,10000,10000 operation,
   // or another instrument with nonstandard plotting limits?
   //

   S32 is_8566_8568 = FALSE;
   S32 is_8562A     = FALSE;
   S32 is_3588A     = FALSE;
   S32 is_FSM       = FALSE;

   if (!strncmp(plot_data,"SP3;",4))
      {
      C8 search_string[1000];

      S32 j = 0;
      for (S32 i=0; i < 84*16; i++)
         {
         search_string[j++] = HP8566_plot[i];
         if (HP8566_plot[i] == '\r') search_string[j++] = '\r';

         if (j >= sizeof(search_string)-2)
            {
            search_string[j] = 0;
            break;
            }
         }

      if (strstr(plot_data, search_string) != NULL)
         {
         is_8566_8568 = TRUE;
         }
      }

   if (strstr(plot_data, "PA10000,10000") != NULL)
      {
      is_8562A = TRUE;
      }

   if ((strlen(plot_data) > 14) && 
       ((!strcmp(&plot_data[strlen(plot_data)-14],"PA0,0;SP0;PG;\r")) ||
        (strstr(plot_data,"PA0,0;SP0;PG;\r" CAPTION_LOCATOR) != NULL)))
      {
      is_3588A = TRUE;
      }

   if ((strstr(plot_data,"SR0.75,2.54;SL0;PU;") != NULL) && 
       (strstr(plot_data,"Ref.Lvl")             != NULL))
      {
      is_FSM = TRUE;       // fix FSM plot from AP 27-Jul-21
      }

   if (is_8562A || is_8566_8568)
      {
      //         
      // If so, set default scale factors needed to bring the trace within 
      // DEFAULT_P1,P2 range, so it's not clipped
      //

      gl_sc_xmin = 0;      //qqq
      gl_sc_ymin= 250;
      gl_sc_xmax = 10300;
      gl_sc_ymax = 10000;
      gl_user_scaling = 1;
      sc_enabled = TRUE;
      }

   if (1 && is_3588A)
      {
      //
      // HP 3588A's coords have a very narrow range for some reason
      //

      gl_sc_xmin = 0;      //qqq
      gl_sc_ymin = 460;
      gl_sc_xmax = 0;
      gl_sc_ymax = 400;
      gl_user_scaling = 1;
      sc_enabled = TRUE;
      }

   if (is_FSM)
      {
      gl_sc_xmin = 8;   
      gl_sc_ymin = 19;
      gl_sc_xmax = 1031; 
      gl_sc_ymax = 525;
      gl_user_scaling = 1;
      sc_enabled = TRUE;
      }

   //
   // Set up default pen colors
   //


   pen_colors[0] = standard_pen_colors[0];

   if (default_pen_color != -1)
      {
      for (S32 i=1; i < 8; i++)     // we're in overlay mode, so pens 1-8 are the same color in a given
         {                          // plot
         pen_colors[i] = default_pen_color;
         }
      }
   else
      {
      default_pen_color = RGB_NATIVE(0,0,0);

      for (S32 i=1; i < 8; i++)
         {
         pen_colors[i] = standard_pen_colors[i];
         }
      }

   if (pen1_color != -1)
      {
      //
      // Force pen 1 to black in overlay mode (usually used for graticule)
      //

      pen_colors[1] = pen1_color;
      }

   user_rotate = data_source_rotate[file_index];
   background  = data_source_background[file_index];
   alt_colors  = data_source_alt[file_index];
   antialias   = data_source_ant[file_index];

//#define YELLOW RGB_TRIPLET(255,255,0)  // blue on black is hard to see... use yellow
#define BLUE   RGB_TRIPLET(0,0,255)
   #ifdef YELLOW
      if(background) pen_colors[2] = YELLOW;
      else           pen_colors[2] = BLUE;
   #endif

   if (alt_pen_set)
      {
      if(background) c = 0;
      else           c = 1;
      S32 temp = pen_colors[c];
      pen_colors[c] = pen_colors[3];
      pen_colors[3] = temp;

      temp = pen_colors[4];
      pen_colors[4] = pen_colors[5];
      pen_colors[5] = temp;

      temp = pen_colors[2];
      pen_colors[2] = pen_colors[4];
      pen_colors[4] = temp;

      temp = pen_colors[6];
      pen_colors[6] = pen_colors[7];
      pen_colors[7] = temp;
      }

   memset(font_colors, 0, sizeof(font_colors));
   font_colors[0] = RGB_TRANSPARENT;
   font_colors[1] = (U16) text_color;

   C8 *caption_at_offset = strstr(plot_data,CAPTION_LOCATOR);  

   render_init();
   render_init();  // !!!! must be called twice for some reason!
   if((ptr[0] == 0x0C) && (ptr[1] == 0x20) && (ptr[2] == 0x01) && (ptr[3] == 0x02)) {
      force_lb_term = 0x08;   // HP3585A utility klude
   }

   while(ptr <= plot_end) {
      c = *ptr++;

      if((caption_at_offset != NULL) && (ptr > caption_at_offset)) {
         render_text = TRUE;                 // in overlay mode, captions are rendered with their trace's 
         font_colors[0] = RGB_TRANSPARENT;   // assigned pen color -- else black
         font_colors[1] = (U16) ((INI_mode == M_OVERLAY) ? default_pen_color : RGB_NATIVE(0,0,0));
      }

      #ifdef HPGL_CODE
         if(in_hpgl) parse_hpgl(c);
      #endif

      #ifdef PCL_CODE
         if(in_hpgl == 0) parse_pcl(c);
      #endif
   }

   if (plot_data != NULL)
      {
      free(plot_data);
      plot_data = NULL;
      }

   return;
}

//****************************************************************************
//
// Render data from input sources (files specified on command line, and/or
// real-time GPIB input)
//
//****************************************************************************

void wipe_pane()
{
VFX_pane_wipe(stage, standard_pen_colors[background]);
return;
   if(background) {
      VFX_pane_wipe(stage, RGB_TRIPLET(0,0,0));
   }
   else {
      VFX_pane_wipe(stage, RGB_TRIPLET(255,255,255));
   }
}

void render_source_list(void)
{
   //
   // Clear background to white
   //

   wipe_pane();

   //
   // For all sources in list...
   //

   for (S32 i=0; i < n_data_sources; i++)
      {
      //
      // If we're in 'browse mode', display only the currently-browsed
      // source
      //

      if (INI_mode == M_BROWSE)
         {
         if (i != current_data_source)
            {
            continue;
            }
         }

      //
      // If plotting directly from instrument, read data into
      // temporary file if the temporary file hasn't already been created
      //

      C8 filename[MAX_PATH];
      strcpy(filename, data_source_file[i]);

      S32 GPIB_address  = data_source_GPIB      [i];
      S32 GPIB_shortcut = data_source_shortcut  [i];
      user_rotate       = data_source_rotate    [i];
      background        = data_source_background[i]; 
      alt_colors        = data_source_alt       [i]; 
      antialias         = data_source_ant       [i]; 

      if (GPIB_address != -1)
         {
         FILE *in = fopen(filename,"rt");

         if (in != NULL)
            {
            //
            // File already exists
            //

            fclose(in);
            }
         else
            {
            //
            // File does not exist -- read data from instrument and save it
            //
            // Keep track of what address the plot came from, so it can be refreshed by
            // hitting the space bar
            //

            if (!read_instrument_data(GPIB_address,
                                      GPIB_shortcut,
                                      filename)) 
               {
               //
               // Acquisition failed -- remove current source from list
               //

               S32 j;
               for (j=i+1; j < n_data_sources; j++)
                  {
                  strcpy(data_source_file[j-1], data_source_file[j]);

                  data_source_GPIB      [j-1] = data_source_GPIB      [j];
                  data_source_shortcut  [j-1] = data_source_shortcut  [j];
                  data_source_rotate    [j-1] = data_source_rotate    [j];
                  data_source_background[j-1] = data_source_background[j];
                  data_source_alt       [j-1] = data_source_alt       [j];
                  data_source_ant       [j-1] = data_source_ant       [j];
                  }

               n_data_sources--;

               //
               // Remove any other sources with the same filename
               //

               for (j=0; j < n_data_sources; j++)
                  {
                  if (!_stricmp(filename, data_source_file[j]))
                     {
                     for (S32 k=j+1; k < n_data_sources; k++)
                        {
                        strcpy(data_source_file[k-1], data_source_file[k]);

                        data_source_GPIB      [k-1] = data_source_GPIB      [k];
                        data_source_shortcut  [k-1] = data_source_shortcut  [k];
                        data_source_rotate    [k-1] = data_source_rotate    [k];
                        data_source_background[k-1] = data_source_background[k];
                        data_source_alt       [k-1] = data_source_alt       [k];
                        data_source_ant       [k-1] = data_source_ant       [k];
                        }

                     n_data_sources--;
                     }
                  }

               if (current_data_source >= n_data_sources)
                  {
                  current_data_source = n_data_sources-1;
                  user_rotate = data_source_rotate[current_data_source];
                  background  = data_source_background[current_data_source]; 
                  alt_colors  = data_source_alt[current_data_source];
                  antialias   = data_source_ant[current_data_source];
                  }

               force_redraw = TRUE;
               return;
               }
            }
         }

      //
      // Render data file
      //

      if ((INI_mode == M_OVERLAY) && (n_data_sources > 1)) 
         {
         render_file(filename,
                     RGB_TRIPLET(0,0,0),
                     plot_order_colors[i % N_PLOT_ORDER_COLORS],
                     plot_order_colors[current_data_source % N_PLOT_ORDER_COLORS],
                     (i == current_data_source),
                     i);
         }
      else
         {
         render_file(filename,
                     -1,
                     -1,
                     RGB_NATIVE(0,0,0),
                     TRUE,
                     i);
         }

      //
      // Update window title
      //

      C8 buffer[MAX_PATH+128];
      C8 current[MAX_PATH+128];

      GetWindowText(hWnd, current, MAX_PATH + 127);

      if ((INI_mode == M_OVERLAY) && (n_data_sources > 1))
         {
         sprintf(buffer,"%s - %d sources",szAppName, n_data_sources);
         }
      else
         {
         if (n_data_sources == 0)
            {
            strcpy(buffer,szAppName);
            }
         else
            {
            sprintf(buffer,"%s - %s",szAppName, filename);
            }
         }

      if (strcmp(current,buffer))
         {
         SetWindowText(hWnd, buffer);
         }
      }
}

//****************************************************************************
//
// Simulate HP-GL/2 acquisition from Tek 492P or 496P spectrum analyzer
//
//****************************************************************************

#define X_GRAT 200
#define Y_GRAT 100
#define W_GRAT 2000
#define H_GRAT 800
#define APPEND (&synthesis_buffer[strlen(synthesis_buffer)])

S32 x_to_49x_scale(SA_STATE *SA, S32 n)   // n=[0,W_GRAT)
{
   S32 x = X_GRAT + n;

   if (x >= X_GRAT + W_GRAT) x = X_GRAT + W_GRAT - 1;
   if (x <  X_GRAT)          x = X_GRAT;

   return x;
}

S32 y_to_49x_scale(SA_STATE *SA, DOUBLE dBm) 
{
   S32 y = Y_GRAT + (S32) (((dBm - SA->min_dBm) * (F32) H_GRAT) / (SA->max_dBm - SA->min_dBm));

   if (y >= Y_GRAT + H_GRAT) y = Y_GRAT + H_GRAT - 1;
   if (y <  Y_GRAT)          y = Y_GRAT;

   return y;
}

void write_49x_field(C8 *buffer, C8 *tag, C8 *src, C8 *suffix)
{
   C8 *txt = strstr(buffer, tag);
   assert(txt != NULL);

   for (S32 i=0; i < (S32) strlen(tag); i++)
      {
      txt[i] = ' ';
      }

   C8 string[256];
   strcpy(string, src);
   strcat(string, suffix);

   for (S32 i=0; i < (S32) strlen(string); i++)
      {
      if (txt[i] != ' ') break;
      txt[i] = string[i];
      }
}

void write_49x_int(C8 *buffer, C8 *tag, DOUBLE val, C8 *suffix)
{
   S32 n = 0;

   if (val >= 0.0F)
      n = (S32) (val + 0.5);
   else
      n = (S32) (val - 0.5);

   C8 src[256];
   sprintf(src,"%d",n);

   write_49x_field(buffer, tag, src, suffix);
}

C8 *synthesize_492P(S32 device_address)
{
   SA_STATE *SA = SA_startup();

   //
   // 10-second timeouts are not sufficient for slow traces 
   //

   SA_parse_command_line("-t");  

   if (!SA_connect(device_address))
      {
      SA_shutdown();
      return NULL;
      }

   //
   // If SAVEA is turned on, we have to treat the two traces in separate
   // passes
   //

   S32 n_passes = 1;

   SA_query_printf("SAVEA?;");
   C8 *text = GPIB_read_ASC();

   if (!strncmp(text,"SAVEA ON",8))
      {
      n_passes = 2;
      }

   //
   // Fetch trace, resample it to the width of the graticule, 
   // and create an HP-GL command to draw it
   //
   // Note that spline-sampling looks better than point-sampling, but it
   // tends to suppress narrow peaks, even when 1000-point acquisition is 
   // turned on
   //
   // High-res acquisition is less reliable with the Prologix interface, but 
   // it's worthwhile here since only one trace is being acquired at a time
   //

   SA->hi_speed_acq = FALSE;
   SA_fetch_trace();

   DOUBLE dest_array[2][W_GRAT];
   S32    n_points;

   if (n_passes == 1)
      {
      n_points = SA->n_trace_points;

      SA_resample_data(SA->dBm_values, n_points,
                       dest_array[0],  W_GRAT,
                       RT_POINT);
      }
   else
      {
      n_points = SA->n_trace_points / 2;
      assert(SA->n_trace_points == n_points * 2);

      for (S32 i=0; i < 2; i++)
         {
         DOUBLE src_array[2][W_GRAT];

         for (S32 j=0; j < n_points; j++)
            {
            src_array[i][j] = SA->dBm_values[(j*2)+i];
            }

         SA_resample_data(src_array[i],  n_points,
                          dest_array[i], W_GRAT,
                          RT_POINT);
         }
      }

   synthesis_buffer[0] = 0;

   for (S32 c=0; c < n_passes; c++)
      {
      S32 i = 0;
   
      strcat(synthesis_buffer,c ? "SP5;" : "SP3;");

      bool PD_written = FALSE;
      bool PA_written = FALSE;

      while (1)
         {
         if (i < W_GRAT)
            {
            //
            // If this is the first valid point, move the pen to it
            // and begin a new PA segment list
            //
            // Otherwise, add the point to the current PA command
            //
   
            if (!PD_written)
               {
               sprintf(APPEND, "\nPU;PA %d,%d;PD;",
                  x_to_49x_scale(SA, i),
                  y_to_49x_scale(SA, dest_array[c][i]));
               PD_written = TRUE;
               PA_written = FALSE;
               }
            else
               {
               if (!PA_written)
                  {
                  sprintf(APPEND,"PA ");
                  PA_written = TRUE;
                  }
   
               sprintf(APPEND, "%d,%d,",
                  x_to_49x_scale(SA, i),
                  y_to_49x_scale(SA, dest_array[c][i]));
               }
            }
         else
            {
            //
            // X-coordinate is at end of graph
            //
            // Terminate current PA segment list, then set up to begin another
            // at the next point with a valid Y-coordinate... or exit the loop if
            // entire trace has been plotted
            //
   
            S32 l = strlen(trace_buffer);
   
            if (trace_buffer[l-1] == ',')
               {
               trace_buffer[l-1] = ';';
               }
   
            if (i == W_GRAT)
               {
               break;
               }
   
            PA_written = FALSE;
            PD_written = FALSE;
            }
   
         i++;
         }
      }

   //
   // Copy Tek 49x graticule and annotation template to synthesis buffer
   //

   strcpy(trace_buffer, (C8 *) TEK49X_plotData);

   //
   // Fix up annotation strings
   //

   write_49x_field(trace_buffer,"*FRQ",freq_text(SA->CF_Hz, FT_LSD_MHZ), "");
   write_49x_field(trace_buffer,"*SPN",freq_text((SA->max_Hz - SA->min_Hz) / 10, FT_LSD_INT), "");
   write_49x_int  (trace_buffer,"*REF",SA->max_dBm, " dBm");
   write_49x_int  (trace_buffer,"*DIV",SA->dB_division, " dB/");
   write_49x_field(trace_buffer,"*RBW",freq_text(SA->RBW_Hz, FT_LSD_INT), "");

   if (SA->VBW_Hz >= 0.0)
      write_49x_field(trace_buffer,"*VBW",freq_text(SA->VBW_Hz, FT_LSD_INT), ""); 
   else
      write_49x_field(trace_buffer,"*VBW",freq_text(SA->RBW_Hz, FT_LSD_INT), "");

   C8 ID_buffer[512];
   strcpy(ID_buffer, &SA->ID_string[7]); 
   ID_buffer[4] = 0;

   write_49x_field(trace_buffer,"*MFG","TEK","");
   write_49x_field(trace_buffer,"*IDN",ID_buffer,"");

   DOUBLE val = SA->max_dBm;

   for (S32 i=0; i < 9; i++)
      {
      C8 tag[3] = "*0";
      tag[1] = (C8) (i + '0');

      write_49x_int(trace_buffer,tag,val,"");

      val -= SA->dB_division;
      }

   //
   // Append the trace command
   //

   strcat(trace_buffer, synthesis_buffer);
   strcat(trace_buffer, "PU;PA0,1000;SP0");

   SA_shutdown();
   return trace_buffer;
}

#undef X_GRAT
#undef Y_GRAT
#undef W_GRAT
#undef H_GRAT
#undef APPEND

//****************************************************************************
//
// Simulate HP-GL/2 acquisition from HP 8566A/8568A spectrum analyzer
// This is also the fastest way to get a plot from an 8566B or 8568B!
//
// We also support the Tek 2782/2784 here
//
//****************************************************************************

#define SWAP16(y) ((((y) & 0xff00) >> 8) + (((y) & 0x00ff) << 8))
#define XDEV(x)   ((SINGLE(x) * 8.415F) + 1391.0F)
#define YDEV(y)   ((SINGLE(y) * 8.426F) + 844.0F)
#define APPEND    (&trace_buffer[strlen(trace_buffer)])

C8 *synthesize_856xA(S32  device_address, //)
                     bool is_278X)
{
   S32 i,c;

   //
   // Earlier 8566A/8568A models will time out during reads if we assert EOI at the
   // end of our request commands...
   //

   GPIB_set_EOS_mode(-1, FALSE);

   //
   // Copy 8566B graticule and annotation template to synthesis buffer
   //

   strcpy(synthesis_buffer, HP8566_plot);

   //
   // Perform "OL" query to obtain instrument state
   //
   // (TODO: OL isn't supported by the Tek 278x... fill in the
   // state array with explicit queries so markers, etc. can be rendered?)
   //

   U8 state[80];
   memset(state, 0, sizeof(state));

   if (!is_278X)
      {
      GPIB_print("OL");      // No semicolon needed

      S32 actual_len = 0;
      C8 *response = GPIB_read_BIN(80,TRUE,FALSE,&actual_len);

      if (actual_len != 80)
         {
         return NULL;
         }

      memcpy(state, response, 80);
      }
   else
      {
      C8 *result = GPIB_query("TRDSP TRNOR?");
      _strupr(result);
      if (strstr(result,"OFF") != NULL) state[20] |= 4;

      result = GPIB_query("TRDSP TRA?");
      _strupr(result);
      if (strstr(result,"OFF") != NULL) state[20] |= 32;
      }

   S32 trace_blanked[2] =
      {
      state[20] & 4,    // TRUE if A blanked (TRNOR for 278x)
      state[20] & 32,   // TRUE if B blanked (TRA for 278x)
      };

   //
   // Perform "OT" query to obtain display annotation
   //

   static C8 OT_buffer[(32*64)+1];
   memset(OT_buffer, 0, sizeof(OT_buffer));

   GPIB_print("OT");

   C8 *result = GPIB_read_ASC(sizeof(OT_buffer)-1);

   if (result == NULL)
      {
      return NULL;
      }

   strcpy(OT_buffer, result);

   //
   // Copy the resulting annotations over the placeholder strings
   // in the synthesis buffer
   //

   C8 *src = OT_buffer;

   for (i=0; i < 31; i++)
      {
      //
      // Terminate this annotation string and find the next one
      // (by searching for the next CR)
      //

      C8 *s = src;

      src = strchr(src, 0x0D);

      if (src == NULL)
         {
         SAL_alert_box("Error","Unknown GPIB read error");
         return NULL;
         }

      *src = 0;
      src++;
      src++;

      //
      // See if we have a placeholder for this string # in the synthesis buffer
      //

      C8 marker[8];
      sprintf(marker,"xxxx%02d",i+1);

      C8 *tag = strstr(synthesis_buffer, marker);

      if (tag == NULL)
         {
         //
         // We don't support this string
         //

         continue;
         }

      //
      // Find string following the placeholder
      //

      C8 *next = strchr(tag, 0x0A);
      next++;

      //
      // Find 'LB' tag associated with this placeholder string
      //

      while ((tag[-1] != 'B') || (tag[-2] != 'L')) tag--;

      //
      // Overwrite placeholder with annotation text, terminating with
      // EOT character (ctrl-C)
      //
      // Replace any sequence of three or more spaces with CR/LF
      // Clean up any double spaces before HZ substrings
      // Clean up any double spaces after REF substrings
      //

      while (*s)
         {
         if ((s[0] == ' ') && (s[1] == ' ')  && (s[2] == 'H') && (s[3] == 'z'))
            {
            *tag++ = ' ';
            *tag++ = 'H';
            *tag++ = 'z';
            s += 4;
            }
         else if ((s[0] == 'R') && (s[1] == 'E')  && (s[2] == 'F') && (s[3] == ' ') && (s[4] == ' '))
            {
            *tag++ = 'R';
            *tag++ = 'E';
            *tag++ = 'F';
            *tag++ = ' ';
            s += 5;
            }
         else if ((s[0] == ' ') && (s[1] == ' ') && (s[2] == ' '))
            {
            *tag++ = 13;
            *tag++ = 10;

            while (*s == ' ') s++;
            }
         else if (s[0] == -7)
            {
            strcpy(tag,"DELTA");
            tag += 5;
            s++;
            }
         else if ((s[0] > 0x7e) || (s[0] < ' '))
            {
            *tag++ = isprint(s[0]) ? '?' : ' ';
            s++;
            }
         else
            {
            *tag++ = *s++;
            }
         }

      *tag++ = 3;

      //
      // Move rest of synthesis buffer text back to close the gap
      //

      memmove(tag, next, strlen(next)+1);
      }

   //
   // Get trace-A and trace-B curve data
   //
   // Request 16-bit binary display values
   // (Slightly slower but more precise than 8-bit binary mode)
   //

   static U16 curve[2][1001];

   for (c=0; c < 2; c++)
      {
      if (trace_blanked[c])
         {
         continue;
         }

      if (!is_278X)
         {
         GPIB_print(c ? "O2;TB" : "O2;TA");
         }
      else
         {
         GPIB_print(c ? "O2;TROUT TRA" : "O2;TROUT TRNOR");
         }

      memset(curve[c], 0, 1001 * sizeof(U16));
      memcpy(curve[c], GPIB_read_BIN(1001 * sizeof(U16),TRUE,FALSE), 1001 * sizeof(U16));

      if (is_278X)
         {
         GPIB_read_ASC();  // Swallow trailing CR/LF (generated by Tek 278x, but not by HP analyzers)
         }
      }

   //
   // Convert curve data to little-endian form, and translate trace data
   // from display coordinates (0,0 to 1000,1000) to FP device coordinates
   // at graticule corners (1391,844 to 9806,9270)
   //
   // A curve is valid if it isn't blanked and has at least one valid
   // onscreen point (0 - 1023)
   //

   static SINGLE trace[2][1001];
   S32 valid[2] = { -1, -1 };

   for (c=0; c < 2; c++)
      {
      if (trace_blanked[c])
         {
         continue;
         }

      for (i=0; i < 1001; i++)
         {
         curve[c][i] = SWAP16(curve[c][i]);

         trace[c][i] = YDEV(curve[c][i]);

         if ((curve[c][i] < 1024) && (valid[c] == -1))
            {
            valid[c] = i;
            }
         }
      }

   //
   // Add trace data to HP-GL/2 plot
   //

   trace_buffer[0] = 0;

   for (c=0; c < 2; c++)
      {
      if (valid[c] == -1)
         {
         continue;
         }

      strcat(trace_buffer,c ? "SP5;" : "SP3;");

      i = valid[c];

      bool PD_written = FALSE;
      bool PA_written = FALSE;

      while (1)
         {
         if ((i < 1001) && (curve[c][i] < 1024))
            {
            //
            // Y-coordinate is valid
            //
            // If this is the first valid point, move the pen to it
            // and begin a new PA segment list
            //
            // Otherwise, add the point to the current PA command
            //

            if (!PD_written)
               {
               sprintf(APPEND, "\nPU;PA %.0f,%.0f;PD;",XDEV(i),trace[c][i]);
               PD_written = TRUE;
               PA_written = FALSE;
               }
            else
               {
               if (!PA_written)
                  {
                  sprintf(APPEND,"PA ");
                  PA_written = TRUE;
                  }

               sprintf(APPEND, "%.0f,%.0f,",XDEV(i),trace[c][i]);
               }
            }
         else
            {
            //
            // Y-coordinate is offscreen, or X-coordinate is at end of graph
            //
            // Terminate current PA segment list, then set up to begin another
            // at the next point with a valid Y-coordinate... or exit the loop if
            // entire trace has been plotted
            //

            S32 l = strlen(trace_buffer);

            if (trace_buffer[l-1] == ',')
               {
               trace_buffer[l-1] = ';';
               }

            if (i == 1001)
               {
               break;
               }

            PA_written = FALSE;
            PD_written = FALSE;
            }

         i++;
         }

      sprintf(APPEND, "PU;\n");
      }

   //
   // If delta-marker enabled, draw reference marker
   //

   if (state[62] == 19)
      {
      S32 x = (S32) XDEV(SWAP16(*(U16 *) &state[53]));
      S32 y = (S32) YDEV(SWAP16(*(U16 *) &state[55]));

      sprintf(APPEND,
             "SP6;PA %d,%d;PD;PA %d,%d;",
              x-100,
              y+130,
              x+100,
              y-130);

      sprintf(APPEND,
             "PU;PA %d,%d;PD;PA %d,%d;",
              x+100,
              y+130,
              x-100,
              y-130);

      sprintf(APPEND,"PU;\n");
      }

   //
   // If marker state is anything other than "off," draw active marker
   //

   if (state[62] != 0)
      {
      S32 x = (S32) XDEV(SWAP16(*(U16 *) &state[57]));
      S32 y = (S32) YDEV(SWAP16(*(U16 *) &state[59]));

      sprintf(APPEND,
             "SP2;PA %d,%d;PD;PA %d,%d;",
              x-100,
              y+130,
              x+100,
              y-130);

      sprintf(APPEND,
             "PU;PA %d,%d;PD;PA %d,%d;",
              x+100,
              y+130,
              x-100,
              y-130);

      sprintf(APPEND,"PU;\n");
      }

   //
   // Trace should appear before graticule data in synthesis buffer
   //

   strcat(trace_buffer, synthesis_buffer);

   //
   // Append either Tek or HP logo
   //

   C8 Tek_logo[] = "sp1; pa250,9307; pr56,0; pd; pr0,168; pr-56,0; pr0,49; pr168,0; "
                   "pr0,-49; pr-56,0; pr0,-168; pr-56,0; pu; pr133,105; pd; pr56,0; "
                   "pr-14,21; pr-28,0; pr-14,-21; pu; pr0,-42; pd; pr112,0; pr0,63; "
                   "pr-28,35; pr-56,14; pr-63,-14; pr-21,-70; pr21,-77; pr35,-14; "
                   "pr182,0; pr0,63; pr56,-63; pr70,0; pr-77,84; pr63,84; pr-63,0; "
                   "pr-56,-56; pr0,105; pr-49,0; pr0,-175; pr-119,0; pr-7,21; pu; "
                   "pr-63,-63; pr49,0; pa; pu;";

   C8 HP_logo[]  = "PA250,9307; SR1.549,2.323; UC-99,3,-1,99,2,9,-99,-1,-4,99,3,0,-1,-5,-99;  "
                   "UC-99,0,-5,99,2,9,3,0,-1,-5,-3,0,-99;";

   sprintf(APPEND,"%s",is_278X ? Tek_logo : HP_logo);

   //
   // Display line appears last, so it's not obscured by graticule
   //

   if (state[18] & 16)
      {
      S32 y = (S32) YDEV(SWAP16(*(U16 *) &state[31]));

      for (S32 x=0; x < 1000; x += 50)
         {
         sprintf(APPEND,
                "SP2;PU;PA %d,%d;PD;PA %d,%d;",
                 (S32) XDEV(x),
                 y,
                 (S32) XDEV(x+24),
                 y);

         sprintf(APPEND,
                "SP5;PU;PA %d,%d;PD;PA %d,%d;",
                 (S32) XDEV(x+25),
                 y,
                 (S32) XDEV(x+49),
                 y);
         }

      sprintf(APPEND,"PU;\n");
      }

   return trace_buffer;
}

#undef SWAP16
#undef APPEND
#undef XDEV
#undef YDEV

//****************************************************************************
//
// Simulate HP-GL/2 acquisition from HP 3585A/B spectrum analyzer
//
//****************************************************************************

#define XDEV(x) ((SINGLE(x) * 8.415F) + 1391.0F)
#define YDEV(y) ((SINGLE(y) * 8.426F) + 844.0F)
#define APPEND  (&trace_buffer[strlen(trace_buffer)])

C8 *synthesize_3585A(S32 device_address)
{
   S32 i,j;

   //
   // Copy 8566B graticule and annotation template to synthesis buffer
   //

   strcpy(synthesis_buffer, HP8566_plot);

   //
   // Perform "D7T4" query to obtain display annotation with immediate triggering
   // (EOS is set to 10 to separate the strings)
   //

   GPIB_set_EOS_mode(10, FALSE);

   static C8 annotation[10][64];
   memset(annotation, 0, sizeof(annotation));

   static C8 original_annotation[10][64];
   memset(original_annotation, 0, sizeof(original_annotation));

   GPIB_print("D7T4");

   for (i=0; i < 10; i++)
      {
      C8 *result = GPIB_read_ASC(sizeof(annotation[i])-1);
      assert(result != NULL);

      //
      // Remove leading spaces from original strings
      //

      for (j=0; j < (S32) strlen(result); j++)
         {
         if (!isspace((U8) result[j]))
            {
            strcpy(original_annotation[i],&result[j]);
            break;
            }
         }

      //
      // Remove all spaces from modified strings so numeric values can be 
      // parsed with sscanf()
      //

      strcpy(annotation[i], result);
      _strupr(annotation[i]);

      for (j=strlen(annotation[i])-1; j >= 0; j--)    
         {
         if (isspace((U8) annotation[i][j]))
            {
            strcpy(&annotation[i][j], &annotation[i][j+1]);
            }
         }
      }

   //
   // Determine HP 3585 amplitude range and frequency span
   //
   // (Marker is rendered only in log mode, with frequency readout in 
   // start/stop or center/span mode)
   // 

   S32 marker_visible = FALSE;

   SINGLE SOURCE_min_dBm     = 10000.0F;
   SINGLE SOURCE_max_dBm     = 10000.0F;
   S32    SOURCE_dB_division = 10000;
   DOUBLE SOURCE_min_Hz      = -FLT_MAX;
   DOUBLE SOURCE_max_Hz      = -FLT_MAX;
   DOUBLE MARKER_Hz          = -FLT_MAX;
   SINGLE MARKER_dBm         = 10000.0F;

   sscanf(annotation[2], "%dDB/DIV", &SOURCE_dB_division);

   if (SOURCE_dB_division != 10000)
      {
      sscanf(annotation[0], "REF%fDBM", &SOURCE_max_dBm);
      SOURCE_min_dBm = SOURCE_max_dBm - (SOURCE_dB_division * 10);

      if ((strstr(annotation[5],"START") != NULL) && (strstr(annotation[6],"STOP") != NULL))
         {
         sscanf(annotation[5], "START%lf", &SOURCE_min_Hz);
         sscanf(annotation[6], "STOP%lf", &SOURCE_max_Hz);

              if (strstr(annotation[5], "GHZ")) SOURCE_min_Hz *= 1.0E9;
         else if (strstr(annotation[5], "MHZ")) SOURCE_min_Hz *= 1.0E6;
         else if (strstr(annotation[5], "KHZ")) SOURCE_min_Hz *= 1.0E3;
         
              if (strstr(annotation[6], "GHZ")) SOURCE_max_Hz *= 1.0E9;
         else if (strstr(annotation[6], "MHZ")) SOURCE_max_Hz *= 1.0E6;
         else if (strstr(annotation[6], "KHZ")) SOURCE_max_Hz *= 1.0E3;

         marker_visible = TRUE;
         }
      else if ((strstr(annotation[5],"CENTER") != NULL) && (strstr(annotation[6],"SPAN") != NULL)) 
         {
         DOUBLE SOURCE_center_Hz = -FLT_MAX;
         DOUBLE SOURCE_span_Hz   = -FLT_MAX;

         sscanf(annotation[5], "CENTER%lf", &SOURCE_center_Hz);
         sscanf(annotation[6], "SPAN%lf", &SOURCE_span_Hz);

              if (strstr(annotation[5], "GHZ")) SOURCE_center_Hz *= 1.0E9;
         else if (strstr(annotation[5], "MHZ")) SOURCE_center_Hz *= 1.0E6;
         else if (strstr(annotation[5], "KHZ")) SOURCE_center_Hz *= 1.0E3;
         
              if (strstr(annotation[6], "GHZ")) SOURCE_span_Hz *= 1.0E9;
         else if (strstr(annotation[6], "MHZ")) SOURCE_span_Hz *= 1.0E6;
         else if (strstr(annotation[6], "KHZ")) SOURCE_span_Hz *= 1.0E3;

         SOURCE_span_Hz /= 2.0;

         SOURCE_min_Hz = SOURCE_center_Hz - SOURCE_span_Hz;
         SOURCE_max_Hz = SOURCE_center_Hz + SOURCE_span_Hz; 

         marker_visible = TRUE;
         }

      if (marker_visible)
         {
         if (((SOURCE_max_Hz  - SOURCE_min_Hz)  < 0.1) ||  
             ((SOURCE_max_dBm - SOURCE_min_dBm) < 0.1))
            {
            marker_visible = FALSE;
            }
         }
      }

   //
   // Perform "D2T4" query to obtain marker frequency and amplitude
   //

   if (marker_visible)
      {
      C8 marker_text[64];
      memset(marker_text, 0, sizeof(marker_text));

      GPIB_print("D2T4");

      C8 *result = GPIB_read_ASC(sizeof(marker_text)-1);
      assert(result != NULL);

      strcpy(marker_text, result);         // Remove all spaces from result
      _strupr(marker_text);

      for (j=strlen(marker_text)-1; j >= 0; j--)    
         {
         if (isspace((U8) marker_text[j]))
            {
            strcpy(&marker_text[j], &marker_text[j+1]);
            }
         }

      sscanf(marker_text, "%lf,%f", 
         &MARKER_Hz, 
         &MARKER_dBm);
   
      if ((MARKER_Hz == -FLT_MAX) || (MARKER_dBm == 10000.0F))
         {
         marker_visible = FALSE;
         }
      else
         {
         if (MARKER_Hz  < SOURCE_min_Hz)  MARKER_Hz  = SOURCE_min_Hz;
         if (MARKER_Hz  > SOURCE_max_Hz)  MARKER_Hz  = SOURCE_max_Hz;
         if (MARKER_dBm < SOURCE_min_dBm) MARKER_dBm = SOURCE_min_dBm;
         if (MARKER_dBm > SOURCE_max_dBm) MARKER_dBm = SOURCE_max_dBm;
         }
      }

   //
   // Copy the resulting annotations over the placeholder strings
   // in the synthesis buffer
   //

   for (S32 OT=1; OT <= 32; OT++)    // OT strings are 1-based
      {
      //
      // See if we have a placeholder for this string # in the synthesis buffer
      //

      C8 marker[8];
      sprintf(marker,"xxxx%02d",OT);

      C8 *tag = strstr(synthesis_buffer, marker);

      if (tag == NULL)
         {
         //
         // We don't support this string
         //

         continue;
         }

      //
      // Find string following the placeholder
      //

      C8 *next = strchr(tag, 0x0A);
      next++;

      //
      // Find 'LB' tag associated with this placeholder string
      //

      while ((tag[-1] != 'B') || (tag[-2] != 'L')) tag--;

      //
      // Overwrite placeholder with annotation text, terminating with
      // EOT character (ctrl-C)
      //
      // Replace any sequence of three or more spaces with CR/LF
      //

      const S32 source_string[33] =             // 3585A string #0-9 for each 8566B string #1-32
         { -1, -1, -1,  7,  8,  9,  3,  0,
            2, -1,  5,  6, -1, -1, -1,  1,  
            4, -1, -1, -1, -1, -1, -1, -1, 
           -1, -1, -1, -1, -1, -1, -1, -1, -1 };

      S32 sn = source_string[OT];

      if (sn != -1)
         {
         C8 *s = &original_annotation[sn][0];

         while (*s)
            {
            if ((s[0] == ' ') && (s[1] == ' ') && (s[2] == ' '))
               {
               *tag++ = 13;
               *tag++ = 10;

               while (*s == ' ') s++;
               }
            else if (s[0] == -7)
               {
               strcpy(tag,"DELTA");
               tag += 5;
               s++;
               }
            else if ((s[0] > 0x7e) || (s[0] < ' '))
               {
               *tag++ = isprint(s[0]) ? '?' : ' ';
               s++;
               }
            else
               {
               *tag++ = *s++;
               }
            }
         }

      *tag++ = 3;

      //
      // Move rest of synthesis buffer text back to close the gap
      //

      memmove(tag, next, strlen(next)+1);
      }

   //
   // Copy trace A to trace B, then request 16-bit binary display values from trace B
   //

   GPIB_print("SABO");

   static U16 curve[1002]; // first 16-bit word ignored (all 1s) 

#if 0
   static SINGLE trace[1002];
   SINGLE offset =  SOURCE_min_dBm;
   SINGLE scale  = (SOURCE_max_dBm - SOURCE_min_dBm) / 1000;    // bottom=0, ref=1000, top=1023
   SINGLE grat_offset = 0.0F;
#endif
                              
   memset(curve, 0, 1002 * sizeof(S16));
   memcpy(curve, GPIB_read_BIN(1002 * sizeof(S16),TRUE,FALSE), 1002 * sizeof(S16));

   for (i=0; i < 1001; i++)
      {
      curve[i+1] = ((curve[i+1] & 0xff00) >> 8) + ((curve[i+1] & 0x0003) << 8);
#if 0
      trace[i] = ((SINGLE(curve[i+1]) + grat_offset) * scale) + offset;
#endif
      }

   GPIB_print("TB0");     // turn trace 'B' back off

   //
   // Translate trace data from display coordinates (0,0 to 1000,1000) 
   // to FP device coordinates at graticule corners (1391,844 to 9806,9270)
   //

   static SINGLE dspcoord[1001];

   for (i=0; i < 1001; i++)
      {
      dspcoord[i] = YDEV(curve[i+1]);
      }

   //
   // Add trace data to HP-GL/2 plot
   //

   trace_buffer[0] = 0;
   strcat(trace_buffer,"SP3;");

   bool PD_written = FALSE;
   bool PA_written = FALSE;

   i = 0;

   while (1)
      {
      if ((i < 1001) && (curve[i] < 1024))
         {
         //
         // Y-coordinate is valid
         //
         // If this is the first valid point, move the pen to it
         // and begin a new PA segment list
         //
         // Otherwise, add the point to the current PA command
         //

         if (!PD_written)
            {
            sprintf(APPEND, "\nPU;PA %.0f,%.0f;PD;",XDEV(i),dspcoord[i]);
            PD_written = TRUE;
            PA_written = FALSE;
            }
         else
            {
            if (!PA_written)
               {
               sprintf(APPEND,"PA ");
               PA_written = TRUE;
               }

            sprintf(APPEND, "%.0f,%.0f,",XDEV(i),dspcoord[i]);
            }
         }
      else
         {
         //
         // Y-coordinate is offscreen, or X-coordinate is at end of graph
         //
         // Terminate current PA segment list, then set up to begin another
         // at the next point with a valid Y-coordinate... or exit the loop if
         // entire trace has been plotted
         //

         S32 l = strlen(trace_buffer);

         if (trace_buffer[l-1] == ',')
            {
            trace_buffer[l-1] = ';';
            }

         if (i == 1001)
            {
            break;
            }

         PA_written = FALSE;
         PD_written = FALSE;
         }

      i++;
      }

   sprintf(APPEND, "PU;\n");

   //
   // If marker is drawable, create it
   //

   if (marker_visible)
      {
      DOUBLE dsp_mx = (MARKER_Hz  - SOURCE_min_Hz ) / (SOURCE_max_Hz  - SOURCE_min_Hz);
      SINGLE dsp_my = (MARKER_dBm - SOURCE_min_dBm) / (SOURCE_max_dBm - SOURCE_min_dBm);

      S32 x = (S32) XDEV(dsp_mx * 1000.0);
      S32 y = (S32) YDEV(dsp_my * 1000.0);

      sprintf(APPEND,
             "SP2;PA %d,%d;PD;PA %d,%d;",
              x-100,
              y+130,
              x+100,
              y-130);

      sprintf(APPEND,
             "PU;PA %d,%d;PD;PA %d,%d;",
              x+100,
              y+130,
              x-100,
              y-130);

      sprintf(APPEND,"PU;\n");
      }

   //
   // Trace should appear before graticule data in synthesis buffer
   //

   strcat(trace_buffer, synthesis_buffer);

   //
   // Append HP logo
   //

   C8 HP_logo[]  = "PA250,9307; SR1.549,2.323; UC-99,3,-1,99,2,9,-99,-1,-4,99,3,0,-1,-5,-99;  "
                   "UC-99,0,-5,99,2,9,3,0,-1,-5,-3,0,-99;";

   sprintf(APPEND,"%s", HP_logo);

   return trace_buffer;
}

#undef APPEND
#undef XDEV
#undef YDEV

//****************************************************************************
//
// Simulate HP-GL/2 acquisition from any 10x10-division analyzer 
// supported by SPECAN.DLL
//
//****************************************************************************

#define X_GRAT 1391
#define Y_GRAT 844
#define W_GRAT 8415
#define H_GRAT 8426
#define APPEND  (&trace_buffer[strlen(trace_buffer)])

S32 x_to_8566_scale(SA_STATE *SA, S32 n)   // n=[0,W_GRAT)
{
   S32 x = X_GRAT + n;

   if (x >= X_GRAT + W_GRAT) x = X_GRAT + W_GRAT - 1;
   if (x <  X_GRAT)          x = X_GRAT;

   return x;
}

S32 y_to_8566_scale(SA_STATE *SA, DOUBLE dBm) 
{
   S32 y = Y_GRAT + (S32) (((dBm - SA->min_dBm) * (F32) H_GRAT) / (SA->max_dBm - SA->min_dBm));

   if (y >= Y_GRAT + H_GRAT) y = Y_GRAT + H_GRAT - 1;
   if (y <  Y_GRAT)          y = Y_GRAT;

   return y;
}

C8 *synthesize_generic_SA(S32 device_address, bool SCPI)
{
   //
   // 10-second timeouts are not sufficient for slow traces 
   // Also avoid retriggering when possible (allows capture of averaged/max-hold trace on FSP)
   //

   SA_STATE *SA = SA_startup();

   SA_parse_command_line("-t -G");  

   if (SCPI)
      {
      SA_parse_command_line("-scpi");
      }

   if (!SA_connect(device_address))
      {
      SA_shutdown();
      return NULL;
      }

   //
   // Fetch trace and create annotation strings
   //

   SA_fetch_trace();

   static DOUBLE dest_array[W_GRAT];

   SA_resample_data(SA->dBm_values, SA->n_trace_points,
                    dest_array,     W_GRAT,
                    RT_SPLINE); 

   static C8 annotation[10][64];
   memset(annotation, 0, sizeof(annotation));

   sprintf(annotation[0],"Start %s",      freq_text(SA->min_Hz, FT_LSD_HZ));     
   sprintf(annotation[1],"Stop %s",       freq_text(SA->max_Hz, FT_LSD_HZ));      
   sprintf(annotation[2],"Ref %0.1f dBm", SA->max_dBm);                     
   sprintf(annotation[3],"%d dB/",        SA->dB_division);                        
   sprintf(annotation[5],"Center %s",     freq_text(SA->CF_Hz, FT_LSD_HZ));     

   if (SA->RBW_Hz     >= 0.0)     sprintf(annotation[4],"Res BW %s",     freq_text(SA->RBW_Hz, FT_LSD_INT));   
   if (SA->sweep_secs >= 0)       sprintf(annotation[6],"Sweep %0.3f s", SA->sweep_secs);
   if (SA->VBW_Hz     >= 0.0)     sprintf(annotation[7],"Vid BW %s",     freq_text(SA->VBW_Hz, FT_LSD_INT));   
   if (SA->RFATT_dB   > -10000.0) sprintf(annotation[8],"RF Att %d dB",  (S32) (SA->RFATT_dB + 0.5F));                     
   if (SA->vid_avgs   > 0)        sprintf(annotation[9],"Vid Avgs\n %d",  SA->vid_avgs);

   //
   // Copy the resulting annotations over the placeholder strings
   // in the synthesis buffer
   //

   strcpy(synthesis_buffer, HP8566_plot);

   for (S32 OT=1; OT <= 32; OT++)    // OT strings are 1-based
      {
      //
      // See if we have a placeholder for this string # in the synthesis buffer
      //

      C8 marker[8];
      sprintf(marker,"xxxx%02d",OT);

      C8 *tag = strstr(synthesis_buffer, marker);

      if (tag == NULL)
         {
         //
         // We don't support this string
         //

         continue;
         }

      //
      // Find string following the placeholder
      //

      C8 *next = strchr(tag, 0x0A);
      next++;

      //
      // Find 'LB' tag associated with this placeholder string
      //

      while ((tag[-1] != 'B') || (tag[-2] != 'L')) tag--;

      //
      // Overwrite placeholder with annotation text, terminating with
      // EOT character (ctrl-C)
      //

      const S32 source_string[33] =             // Our string #0-9 for each 8566B string #1-32
         { -1, -1, -1,  4,  7,  6,  8,  2,
            3, -1,  0,  1, -1, -1, -1,  5,  
           -1, -1,  9, -1, -1, -1, -1, -1, 
           -1, -1, -1, -1, -1, -1, -1, -1, -1 };
                                                
      S32 sn = source_string[OT];               
                                                
      if (sn != -1)                             
         {
         C8 *s = &annotation[sn][0];

         while (*s)
            {
            *tag++ = *s++;
            }
         }

      *tag++ = 3;

      //
      // Move rest of synthesis buffer text back to close the gap
      //

      memmove(tag, next, strlen(next)+1);
      }

   //
   // Add trace data to HP-GL/2 plot
   //

   strcpy(trace_buffer,"SP3;");

   bool PD_written = FALSE;
   bool PA_written = FALSE;

   S32 i = 0;

   while (1)
      {
      if (i < W_GRAT)
         {
         //
         // If this is the first valid point, move the pen to it
         // and begin a new PA segment list
         //
         // Otherwise, add the point to the current PA command
         //

         if (!PD_written)
            {
            sprintf(APPEND, "\nPU;PA %d,%d;PD;",
               x_to_8566_scale(SA, i),
               y_to_8566_scale(SA, dest_array[i]));
            PD_written = TRUE;
            PA_written = FALSE;
            }
         else
            {
            if (!PA_written)
               {
               sprintf(APPEND,"PA ");
               PA_written = TRUE;
               }

            sprintf(APPEND, "%d,%d,",
               x_to_8566_scale(SA, i),
               y_to_8566_scale(SA, dest_array[i]));
            }
         }
      else
         {
         //
         // X-coordinate is at end of graph
         //
         // Terminate current PA segment list, then set up to begin another
         // at the next point with a valid Y-coordinate... or exit the loop if
         // entire trace has been plotted
         //

         S32 l = strlen(trace_buffer);

         if (trace_buffer[l-1] == ',')
            {
            trace_buffer[l-1] = ';';
            }

         if (i == W_GRAT)
            {
            break;
            }

         PA_written = FALSE;
         PD_written = FALSE;
         }

      i++;
      }

   //
   // Trace should appear before graticule data in synthesis buffer
   //

   strcat(trace_buffer, synthesis_buffer);

   SA_shutdown();
   return trace_buffer;
}

#undef X_GRAT
#undef Y_GRAT
#undef W_GRAT
#undef H_GRAT
#undef APPEND

//****************************************************************************
//
// Perform simulated asynchronous read from any device on bus
//
// Set plotter_address==-1 for listen-only mode
//
//****************************************************************************

C8 *async_read(S32   plotter_address, //)
               bool  refresh_display           = TRUE,
               S32   device_address            = -1,
               C8   *device_command            = NULL,
               bool  allow_source_list_refresh = FALSE,
               bool  reset_to_local            = TRUE)
{
   //
   // Handle instruments that connect via SPECAN.DLL
   //

   if (force_generic)
      {
      return synthesize_generic_SA(device_address, FALSE);
      }

   if (device_command != NULL)
      {
      if (!_stricmp(device_command,"HP 70000"))
         {
         return synthesize_generic_SA(device_address, FALSE);
         }

      if (!_stricmp(device_command,"SCPI"))
         {
         return synthesize_generic_SA(device_address, TRUE);
         }

      if ((!_stricmp(device_command,"Tektronix 492P")) ||
          (!_stricmp(device_command,"Tektronix 496P")))
         {
         return synthesize_492P(device_address);
         }
      }

   //
   // Otherwise, establish connection and set default GPIB timeout according 
   // to value in 7470.ini
   //
   // Board-level EOS detection causes "Plotter not ready" errors
   // on Wiltron 562, per 4Z5CP's note of 2-8-07, so we turn it off here
   //

   S32 start_time = timeGetTime();

   GPIB_connect(device_address,
                GPIB_error,
                0,
                INI_async_GPIB_timeout_ms,
                plotter_address,
                INI_release_sys_control);

   GPIB_set_EOS_mode(10, TRUE, FALSE);
   GPIB_set_serial_read_dropout(INI_serial_read_dropout);

   GPIB_init_status = 1;
   GPIB_polling = 1;
   GPIB_abort   = 0;

   //
   // Reserve 4 MB for plots, which should be "enough for anyone"
   //

   bool read_complete = FALSE;

   S32 last_incoming_time = 0;
   S32 last_refresh_time  = 0;

   S32 total_len = 0;
   S32 OS_status = 24;

   static C8 contents[4194304];
   contents[0] = 0;

   if (INI_HPGL_preface[0])
      {
      strcpy(contents, INI_HPGL_preface);
      }

   S32 start_data = strlen(contents);

   //
   // Send initial device command, or use it to branch to an instrument-specific
   // handler
   //

   if (device_command != NULL)
      {
      if (!_stricmp(device_command,"HP 856xA"))
         {
         reset_to_local = FALSE;

         C8 *result = synthesize_856xA(device_address, FALSE);

         if (result != NULL)
            {
            strcat(contents, result);
            read_complete = TRUE;
            }

         goto GPIB_done;
         }
      else if (!_stricmp(device_command,"HP 3585"))
         {
         C8 *result = synthesize_3585A(device_address);

         if (result != NULL)
            {
            strcat(contents, result);
            read_complete = TRUE;
            }

         goto GPIB_done;
         }
      else if (!_stricmp(device_command,"Tektronix 278x"))
         {
         C8 *result = synthesize_856xA(device_address, TRUE);

         if (result != NULL)
            {
            strcat(contents, result);
            read_complete = TRUE;
            }

         goto GPIB_done;
         }
      else
         {
         GPIB_print("%s",device_command, INI_ignore_write_aborts);
         }
      }

   //
   // Prepare to receive new plot image
   //

GPIB_receive_plot:
   contents[start_data] = 0;
   
   last_incoming_time = 0;
   last_refresh_time  = 0;

   total_len = 0;
   OS_status = 24;

   //
   // Read incoming data to contents array
   //

GPIB_wait:
   while (1)
      {
      Sleep(5);
      S32 cur_time = timeGetTime();

      SAL_serve_message_queue();

      if (GPIB_abort)
         {
         goto GPIB_done;
         }

      //
      // Keep display refreshed
      //
      if ((refresh_display) &&
         ((last_refresh_time == 0) || (cur_time - last_refresh_time > 1000)))
         {
         if (force_redraw && allow_source_list_refresh)
            {
            force_redraw = FALSE;
            render_source_list();
            }

         C8 text[256];
         sprintf(text,"Reading data from instrument, %d bytes received . . . ",
            total_len);

         VFX_rectangle_fill(stage,
                            28,
                            28,
                            RES_X-28,
                            54,
                            LD_DRAW,
                            RGB_TRIPLET(255,0,0));

         VFX_string_draw(stage,
                         32,
                         32,
                         VFX_default_system_font(),
                         text,
                         VFX_default_font_color_table(VFC_WHITE_ON_XP));

         VFX_string_draw(stage,
                         32,
                         42,
                         VFX_default_system_font(),
                         "Press any key to return to display mode",
                         VFX_default_font_color_table(VFC_WHITE_ON_XP));

         last_refresh_time = cur_time;
         refresh();
         }

      //
      // Exit loop if overall timeout interval reached
      //

      if ((last_incoming_time != 0) &&
          (cur_time - last_incoming_time > INI_async_timeout))
         {
         read_complete = TRUE;
         goto GPIB_plot_received;
         }

      //
      // Read incoming data in small chunks, inhibiting timeout error reports
      //
      // ICS 488-USB appears to reset the entire connection state after ibrd() returns
      // with an EABO timeout condition... this causes numerous problems with device-initiated
      // plots
      //

      C8 *read_result = GPIB_read_ASC(INI_async_xfer_size, FALSE);

      if ((read_result == NULL) || (read_result[0] == 0))
         {
         //
         // Read operation timed out.  Either the entire transfer is complete, or
         // the instrument is waiting for a reply
         //
         // Either way, find last valid (non-terminator) character in last mnemonic received
         // and look for a recognized mnemonic
         //

         S32 len = strlen(contents);

         S32 end = len-1;

         while ((end >= 0) && (isspace((U8) contents[end]) || (contents[end] == ';')))
            {
            end--;
            }

         if (end >= 1)
            {
            //
            // Check for opcodes requring a reply
            //

            bool replied = FALSE;

            C8 *tail = &contents[end-1];

            if (INI_OE_reply[0] && (!_strnicmp(tail,"OE",2))) { GPIB_print("%s", INI_OE_reply); replied = TRUE; }
            if (INI_OH_reply[0] && (!_strnicmp(tail,"OH",2))) { GPIB_print("%s", INI_OH_reply); replied = TRUE; }
            if (INI_OI_reply[0] && (!_strnicmp(tail,"OI",2))) { GPIB_print("%s", INI_OI_reply); replied = TRUE; }
            if (INI_OP_reply[0] && (!_strnicmp(tail,"OP",2))) { GPIB_print("%s", INI_OP_reply); replied = TRUE; }
            if (INI_OO_reply[0] && (!_strnicmp(tail,"OO",2))) { GPIB_print("%s", INI_OO_reply); replied = TRUE; }
            if (INI_OF_reply[0] && (!_strnicmp(tail,"OF",2))) { GPIB_print("%s", INI_OF_reply); replied = TRUE; }
            if (INI_OA_reply[0] && (!_strnicmp(tail,"OA",2))) { GPIB_print("%s", INI_OA_reply); replied = TRUE; }
            if (INI_OC_reply[0] && (!_strnicmp(tail,"OC",2))) { GPIB_print("%s", INI_OC_reply); replied = TRUE; }

            //
            // If "OS" received, respond with requested status byte
            //
            // NB: HP 7470A reference says that status should go from 24->16; HP-GL/2 3rd ed. says
            // 26->18
            //

            if (INI_process_OS && (!_strnicmp(tail,"OS",2)))
               {
               GPIB_print("%d", OS_status);

               OS_status = 16;
               replied   = TRUE;
               }

            if (replied)
               {
               //
               // Remove the two-character query opcode we replied to, so we won't
               // process it again if the next read times out (e.g., the trailing OE sent
               // as the last opcode in a plot from the HP 3562A)
               //

               *tail = 0;
               len = strlen(contents);
               }

            //
            // If "SP;" or "SP0;" received within the last INI_async_xfer_size bytes after a timeout,
            // we can assume the read is complete without waiting for INI_async_timeout to expire
            //

            if ((INI_SP0_detect) && (!replied) && (len > INI_async_xfer_size))
               {
               C8 *last_chunk = &contents[len - INI_async_xfer_size];

               if ((strstr(last_chunk, "SP;")  != NULL) ||
                   (strstr(last_chunk, "SP0;") != NULL))
                  {
                  read_complete = TRUE;
                  goto GPIB_plot_received;
                  }
               }
            }

         //
         // After a timeout, go back to waiting
         //

         goto GPIB_wait;
         }

      //
      // Read operation did not time out; append new data to buffer
      //

      if (INI_traffic_diag_filename[0])
         {
         FILE *temp = fopen(INI_traffic_diag_filename,"a+t");
         fprintf(temp,"Recv: [%s]\n",read_result);
         fclose(temp);
         }

      S32 read_len = strlen(read_result);

      if (!read_len)
         {
         continue;
         }

      if (read_result[0] == -1)
         {
         //
         // Tek 2430A sends 0xFFs after end of requested data, so
         // we'll never time out if we don't catch them here
         //

         continue;
         }

      total_len += read_len;

      strcat(contents, read_result);

      if (last_incoming_time == 0)
         {
         last_refresh_time = 0;
         }

      last_incoming_time = cur_time;
      }

GPIB_plot_received:

   if (strlen(contents) < (U32) INI_min_plot_bytes)
      {
      //
      // Don't treat this as a standalone plot; it's not big enough
      // (Can't accumulate the received data, or it'll generate false .PLT images
      // if repeated preambles are transmitted by the instrument)
      //

      read_complete = FALSE;
      goto GPIB_receive_plot;
      }

GPIB_done:
   GPIB_polling = 0;

   if (!read_complete)
      {
      GPIB_flush_receive_buffers();
      }

   GPIB_disconnect(reset_to_local);
   GPIB_init_status = 0;

   S32 end_time = timeGetTime();

   DOUBLE dt = (DOUBLE) (end_time-start_time) / 1000.0;

   printf("dt = %.1lf secs, %d bytes/sec\n", dt, (S32) (((DOUBLE) total_len / dt) + 0.5));

   if (read_complete)
      {
      return contents;
      }

   return NULL;
}

//****************************************************************************
//
// Update 7470USER.INI with changed user options, where applicable
//
//****************************************************************************

void update_user_INI(void)
{
   FILE *read_options = fopen(user_INI_pathname,"rt");

   if (read_options == NULL)
      {
      return;
      }

   TEMPFILE tempfile(".ini", "wt", FALSE);

   if (!tempfile.active)
      {
      return;
      }

   const S32 N_LINES = 6;

   C8 lines[N_LINES][512] = { 0 };

   _snprintf(lines[0], sizeof(lines[0])-1, "res_x %d\n",         RES_X);
   _snprintf(lines[1], sizeof(lines[1])-1, "res_y %d\n",         RES_Y); 
   _snprintf(lines[2], sizeof(lines[2])-1, "overlay_mode %d\n",  INI_mode);
   _snprintf(lines[3], sizeof(lines[3])-1, "antialias %d\n",     antialias);
   _snprintf(lines[4], sizeof(lines[4])-1, "color_scheme %d\n",  alt_colors);
   _snprintf(lines[5], sizeof(lines[5])-1, "background %d\n",    background);

   while (1)
      {
      //
      // Read input line and make a copy
      //

      C8 linbuf[512] = { 0 };

      C8 *result = fgets(linbuf, 
                         sizeof(linbuf) - 1, 
                         read_options);

      if ((result == NULL) || (feof(read_options)))
         {
         break;
         }

      C8 original_line[512];
      strcpy(original_line, linbuf);

      //
      // Skip blank lines, and kill trailing and leading spaces
      //

      S32 l = strlen(linbuf);

      if ((!l) || (linbuf[0] == ';'))
         {
         if (fputs(original_line, tempfile.file) == EOF)
            {
            _fcloseall();
            return;
            }

         continue;
         }

      C8 *lexeme  = linbuf;
      C8 *end     = linbuf;
      S32 leading = 1;

      for (S32 i=0; i < l; i++)
         {
         if ((!isspace((U8) linbuf[i])) && (linbuf[i] != ';'))      // (Removes trailing semicolons as well as leading ones)
            {
            if (leading)
               {
               lexeme = &linbuf[i];
               leading = 0;
               }

            end = &linbuf[i];
            }
         }

      end[1] = 0;

      if ((leading) || (!strlen(lexeme)))
         {
         if (fputs(original_line, tempfile.file) == EOF)
            {
            _fcloseall();
            return;
            }

         continue;
         }

      //
      // Terminate key substring at first space
      //

      C8 *value = strchr(lexeme,' ');

      if (value == NULL)
         {
         value = strchr(lexeme,'\t');
         }

      if (value != NULL)
         {
         *value = 0;
         }

      //
      // Don't copy existing source file references
      //

      if (!_stricmp(lexeme,"source"))
         {
         continue;
         }

      //
      // Write modified values for recognized parameters, passing unrecognized 
      // or inapplicable ones to the new file unchanged
      //

      C8 *modified_line = NULL;

      for (S32 n=0; n < N_LINES; n++)
         {
         C8 tag[512] = { 0 };
         C8 *t = strchr(lines[n],' ');

         if (t != NULL)
            {
            *t = 0;
            strcpy(tag,lines[n]);
            *t = ' ';
            }

         if (!_stricmp(lexeme,tag))
            {
            modified_line = lines[n];
            break;
            }
         }

      if (modified_line != NULL)
         {
         if (fputs(modified_line, tempfile.file) == EOF)
            {
            _fcloseall();
            return;
            }

         modified_line[0] = 0;
         }
      else
         {  
         if (fputs(original_line, tempfile.file) == EOF)
            {
            _fcloseall();
            return;
            }
         }
      }

   //
   // Write any values that weren't present in the original file
   //

   for (S32 n=0; n < N_LINES; n++)
      {
      if (lines[n][0] == 0) 
         {
         continue;
         }

      if (fputs(lines[n], tempfile.file) == EOF)
         {
         _fcloseall();
         return;
         }
      }

   //
   // Append filenames of all loaded sources to 7470USER.INI so they'll be reloaded at startup time
   //

   for (S32 s=0; s < n_data_sources; s++)
      {
      if (data_source_GPIB[s] != -1)      // don't try to reload acquired tempfiles
         {
         continue;
         }

      C8 *S = &data_source_file[s][0];

      fprintf(tempfile.file, "source %s\n", S);
      }

   //
   // Overwrite original .INI file with our modified copy
   //

   _fcloseall();

   CopyFile(tempfile.name,
            user_INI_pathname,
            FALSE);
}

//****************************************************************************
//
// Exit handlers must be present in every SAL application
//
// These routines handle exits under different conditions (exit() call,
// user request via GUI, etc.)
//
//****************************************************************************

static int exit_handler_active = 0;

void WINAPI WinClean(void)
{
   if (exit_handler_active)
      {
      return;
      }

   exit_handler_active = 1;

   if (!(GetAsyncKeyState(VK_SHIFT) & 0x8000))
      {
      update_user_INI();
      }

   SAL_shutdown();

   if (GPIB_init_status)
      {
      GPIB_disconnect();
      GPIB_init_status = 0;
      }

   OutputDebugString("Final exit OK\n");
}

void WINAPI WinExit(void)
{
   OutputDebugString("WinExit called\n");

   if (!exit_handler_active)
      {
      WinClean();
      }

   exit(0);
}

void AppExit(void)
{
   OutputDebugString("AppExit() called via atexit()\n");

   if (!exit_handler_active)
      {
      WinClean();
      }

   return;
}

//****************************************************************************
//
// CMD_load_HPGL()
//
//****************************************************************************

void CMD_load_HPGL()
{
   if (n_data_sources < MAX_DATA_SOURCES)
      {
      C8 buffer[MAX_PATH];
      buffer[0] = 0;

      if (get_load_filename(buffer))
         {
         current_data_source = n_data_sources++;

         strcpy(data_source_file[current_data_source], buffer);

         data_source_GPIB      [current_data_source] = -1;     // Don't refresh a loaded file from an instrument
         data_source_shortcut  [current_data_source] = -1;
         data_source_rotate    [current_data_source] = 0;
         data_source_background[current_data_source] = INI_background;
         data_source_alt       [current_data_source] = INI_color_scheme;
         data_source_ant       [current_data_source] = INI_antialias;

         user_rotate = data_source_rotate[current_data_source];
         background  = data_source_background[current_data_source];
         alt_colors  = data_source_alt[current_data_source];
         antialias   = data_source_ant[current_data_source];

         force_redraw = TRUE;
         }
      }
}

//****************************************************************************
//
// CMD_save()
//
//****************************************************************************

void CMD_save(C8 *explicit_save_filename)
{
   C8 new_name[MAX_PATH];

   if (current_data_source >= n_data_sources)
      {
      return;
      }

   C8 *src = "overlay";

   if (!((INI_mode == M_OVERLAY) && (n_data_sources > 1)))
      {
      src = data_source_file[current_data_source];
      }

   if (src[0] == '\"')
      {
      strcpy(new_name,&src[1]);
      }
   else
      {
      strcpy(new_name,src);
      }

   C8 *suffix = strrchr(new_name,'.');

   if (suffix)
      {
      *suffix = 0;
      }

   bool new_name_valid = FALSE;

   if (explicit_save_filename != NULL)
      {
      strcpy(new_name, explicit_save_filename);
      new_name_valid = TRUE;
      }

   if (!new_name_valid)
      {
      new_name_valid = get_save_filename(new_name);
      }

   if (new_name_valid)
      {
      C8 *suffix = strrchr(new_name,'.');
      assert(suffix);

      if (!_stricmp(suffix,".tga"))
         {
         TGA_write_16bpp(image, new_name);
         }
      else if (!_stricmp(suffix,".gif"))
         {
         GIF_write_16bpp(image, new_name);
         }
      else if (!_stricmp(suffix,".bmp"))
         {
         BMP_write_16bpp(image, new_name);
         }
      else if (!_stricmp(suffix,".pcx"))
         {
         PCX_write_16bpp(image, new_name);
         }
      else if ((!_stricmp(suffix,".plt")) ||
               (!_stricmp(suffix,".hgl")) ||
               (!_stricmp(suffix,".hpg")) ||
#ifdef PCL_CODE
               (!_stricmp(suffix,".pcl")) ||
#endif
               (!_stricmp(suffix,".pgl")))

         {
         C8 *current_name = data_source_file[current_data_source];

         CopyFile(current_name,
                  new_name,
                  FALSE);

         if ((data_source_GPIB[current_data_source] != -1)        // if we just acquired this file in this session...
               &&
             (_stricmp(current_name, new_name)))                  // and we saved it under a different name...
            {
            printf("Removing %s\n", current_name);
            _unlink(current_name);                                // then delete the acquired file...

            data_source_GPIB      [current_data_source] = -1;     // and mark it as not acquired (so future saves won't delete it)...
            data_source_shortcut  [current_data_source] = -1;
//          data_source_rotate    [current_data_source] = 0;      // but don't reset its visual attributes
//          data_source_background[current_data_source] = INI_background;
//          data_source_alt       [current_data_source] = INI_color_scheme;
//          data_source_ant       [current_data_source] = INI_antialias;
            }

         strcpy(current_name, new_name);         // keep track of the file's latest name
         force_redraw = 1;
         }
      }
}

//****************************************************************************
//
// CMD_print_image()
//
//****************************************************************************

void CMD_print_image()
{
   PrintBackBufferToDC();
}

//****************************************************************************
//
// CMD_quit()
//
//****************************************************************************

void CMD_quit()
{
   exit(0);
}

//****************************************************************************
//
// CMD_refresh(void)
//
//****************************************************************************

void CMD_refresh(void)
{
   if (current_data_source >= n_data_sources)
      {
      return;
      }

   wipe_pane();
   refresh();

   S32 addr        = data_source_GPIB      [current_data_source];
   S32 s           = data_source_shortcut  [current_data_source];
   C8  user_rotate = data_source_rotate    [current_data_source];
   C8  background  = data_source_background[current_data_source];
   C8  alt_colors  = data_source_alt       [current_data_source];
   C8  antialias   = data_source_ant       [current_data_source];

   if (addr != -1)
      {
      //
      // Create new source with address-based filename, and delete any existing file under that
      // name so that it will be refreshed from the GPIB bus
      //

      current_data_source = n_data_sources++;

      data_source_GPIB      [current_data_source] = addr;
      data_source_shortcut  [current_data_source] = s;
      data_source_rotate    [current_data_source] = user_rotate;
      data_source_background[current_data_source] = background;
      data_source_alt       [current_data_source] = alt_colors;
      data_source_ant       [current_data_source] = antialias;

      sprintf(data_source_file[current_data_source],
              "GPIB_%d (%d).plt",
              addr,
            ++refresh_cnt);

      _unlink(data_source_file[current_data_source]);
      }

   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_browse(void)
//
//****************************************************************************

void CMD_browse(void)
{
   INI_mode = M_BROWSE;
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_overlay(void)
//
//****************************************************************************

void CMD_overlay(void)
{
   INI_mode = M_OVERLAY;
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_next(void)
//
//****************************************************************************

void CMD_next(void)
{
   current_data_source++;

   if (current_data_source >= n_data_sources)
      {
//    current_data_source = n_data_sources-1;
      current_data_source = 0;
      if (0 && INI_beep)
         {
         PlaySound(INI_beep_wav_filename, NULL, SND_SYNC | SND_FILENAME);
         }
      }

   user_rotate = data_source_rotate[current_data_source];
   background  = data_source_background[current_data_source];
   alt_colors  = data_source_alt[current_data_source];
   antialias   = data_source_ant[current_data_source];
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_prev(void)
//
//****************************************************************************

void CMD_prev(void)
{
   --current_data_source;

   if (current_data_source < 0)
      {
//    current_data_source = 0;
      current_data_source = n_data_sources-1;
      if(current_data_source < 0) current_data_source = 0;
      if (0 && INI_beep)
         {
         PlaySound(INI_beep_wav_filename, NULL, SND_SYNC | SND_FILENAME);
         }
      }

   user_rotate = data_source_rotate[current_data_source];
   background  = data_source_background[current_data_source];
   alt_colors  = data_source_alt[current_data_source];
   antialias   = data_source_ant[current_data_source];
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_erase_caption()
//
//****************************************************************************

void CMD_erase_caption(void)
{
   erase_caption = TRUE;
   caption_target = current_data_source;
   force_redraw  = TRUE;
}

//****************************************************************************
//
// CMD_edit_caption(void)
//
//****************************************************************************

void CMD_edit_caption(void)
{
   strcpy(DLG_box_caption, "Edit user trace caption");

   if (!DialogBox(hInstance,
                  MAKEINTRESOURCE(IDD_EDIT_CAPTION),
                  hWnd,
                  CapDlgProc))
      {
      return;
      } 
}

//****************************************************************************
//
// CMD_set_resolution(S32 width, S32 height)
//
//****************************************************************************

void CMD_set_resolution(S32 width, S32 height)
{
   requested_RES_X = width;
   requested_RES_Y = height;
}

//****************************************************************************
//
// CMD_antialias()
//
//****************************************************************************

void CMD_antialias()
{
   antialias = !antialias;
   if(current_data_source >= 0) data_source_ant[current_data_source] = antialias;
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_alt_colors()
//
//****************************************************************************

void CMD_alt_colors()
{
   alt_colors = !alt_colors;
   if(current_data_source >= 0) data_source_alt[current_data_source] = alt_colors;
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_rotate()
//
//****************************************************************************

void CMD_rotate()
{
   --user_rotate;
   user_rotate &= 0x03;
   if(current_data_source >= 0) data_source_rotate[current_data_source] = user_rotate;
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_swap()
//
//****************************************************************************

void CMD_swap()
{
   if(background) background = 0;
   else if(INI_background) background = INI_background;
   else background = 1;
   if(current_data_source >= 0) data_source_background[current_data_source] = background;
   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_close_current_source()
//
//****************************************************************************

void CMD_close_current_source(void)
{
   if (n_data_sources == 0)
      {
      return;
      }

   S32 j;

   for (j=current_data_source+1; j < n_data_sources; j++)
      {
      strcpy(data_source_file[j-1], data_source_file[j]);

      data_source_GPIB      [j-1] = data_source_GPIB      [j];
      data_source_shortcut  [j-1] = data_source_shortcut  [j];
      data_source_rotate    [j-1] = data_source_rotate    [j];
      data_source_background[j-1] = data_source_background[j];
      data_source_alt       [j-1] = data_source_alt       [j];
      data_source_ant       [j-1] = data_source_ant       [j];
      }

   n_data_sources--;

   if (current_data_source >= n_data_sources)
      {
      current_data_source = n_data_sources-1;
      }
      user_rotate = data_source_rotate[current_data_source];
      background  = data_source_background[current_data_source];
      alt_colors  = data_source_alt[current_data_source];
      antialias   = data_source_ant[current_data_source];

   if (!n_data_sources)
      {
      SetWindowText(hWnd, szAppName);
      }

   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_close_all_sources()
//
//****************************************************************************

void CMD_close_all_sources(void)
{
   if (n_data_sources == 0)
      {
      return;
      }

   n_data_sources = 0;
   current_data_source = -1;

   SetWindowText(hWnd, szAppName);

   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_delete_current_source(void)
//
//****************************************************************************

void CMD_delete_current_source(void)
{
   if (n_data_sources == 0)
      {
      return;
      }

   C8 filename[MAX_PATH];
   strcpy(filename, data_source_file[current_data_source]);

   _unlink(filename);

   //
   // Close all sources that refer to this filename
   //

   for (S32 j=0; j < n_data_sources; j++)
      {
      C8 src_filename[MAX_PATH];
      strcpy(src_filename, data_source_file[j]);

      if (!_stricmp(src_filename, filename))
         {
         current_data_source = j;
         user_rotate = data_source_rotate[current_data_source];
         background  = data_source_background[current_data_source]; 
         alt_colors  = data_source_alt[current_data_source];
         antialias   = data_source_ant[current_data_source];
         CMD_close_current_source();
         j--;
         }
      }
}

//****************************************************************************
//
// CMD_acquire(void)
//
//****************************************************************************

void CMD_acquire(S32      GPIB_address,     // 0 = nonaddressable serial adapter, 1-31 = GPIB addr
                 S32      optional_shortcut)
{
   if (n_data_sources >= MAX_DATA_SOURCES)
      {
      return;
      }

   current_data_source = n_data_sources++;

   data_source_GPIB      [current_data_source] = GPIB_address;
   data_source_shortcut  [current_data_source] = optional_shortcut;
   data_source_rotate    [current_data_source] = 0;
   data_source_background[current_data_source] = INI_background;
   data_source_alt       [current_data_source] = INI_color_scheme;
   data_source_ant       [current_data_source] = INI_antialias;

   sprintf(data_source_file[current_data_source],
      "GPIB_%d (%d).plt",
      GPIB_address,
    ++refresh_cnt);

   _unlink(data_source_file[current_data_source]);

   force_redraw = TRUE;
}

//****************************************************************************
//
// CMD_listen(void)
//
//****************************************************************************

bool CMD_listen(bool  auto_print, //)
                S32   plotter_address,
                C8   *explicit_filename,
                S32   device_address,       // Must be -1 to make device-initiated plots non-refreshable!
                C8   *device_command,
                bool  loop,
                C8   *preface,
                bool  reset_to_local)
{
   while (1)
      {
      if (n_data_sources >= MAX_DATA_SOURCES)
         {
         SAL_alert_box("Error","Too many plot files -- delete some first!");
         return FALSE;
         }

      C8 *result = async_read(plotter_address,
                              TRUE,
                              device_address,
                              device_command,
                              explicit_filename == NULL,
                              reset_to_local);

      force_redraw = TRUE;

      if (result == NULL)
         {
         return FALSE;
         }

      wipe_pane();
      refresh();

      //
      // Generate unique filename
      //

      C8 filename[MAX_PATH];

      if (explicit_filename != NULL)
         {
         strcpy(filename,explicit_filename);
         }
      else
         {
         S32 i;

         for (i=0; i < 10000; i++)
            {
            sprintf(filename, "plot%04d.plt",i);

            FILE *test = fopen(filename,"rt");

            if (test == NULL)
               {
               break;
               }

            fclose(test);
            }

         if (i == 10000)
            {
            sprintf(filename, "plot0000.plt");
            }
         }

      //
      // Write plot data to file
      //

      FILE *out = fopen(filename,"w+t");

      if (out == NULL)
         {
         SAL_alert_box("Error","Could not open %s for writing", filename);
         }
      else
         {
         if (preface != NULL)
            {
            fprintf(out,"%s\n",preface);     // (e.g., to add an initial SC command to 8510 plots)
            }

         fprintf(out,"%s\n",result);
         fputc(0,out);

         if (ferror(out))
            {
            SAL_alert_box("Error","Could not write to %s", filename);
            return FALSE;
            }

         fclose(out);
         }

      //
      // Exit if we were called to acquire a single file
      //

      if (explicit_filename != NULL)
         {
         return TRUE;
         }

      //
      // Add it to display list
      //

      current_data_source = n_data_sources++;
      strcpy(data_source_file[current_data_source], filename);

      data_source_GPIB      [current_data_source] = device_address;
      data_source_shortcut  [current_data_source] = -1;
      data_source_rotate    [current_data_source] = 0;
      data_source_background[current_data_source] = INI_background;
      data_source_alt       [current_data_source] = INI_color_scheme;
      data_source_ant       [current_data_source] = INI_antialias;

      //
      // Print if requested
      //

      render_source_list();
      refresh();

      if (INI_beep)
         {
         PlaySound(INI_beep_wav_filename, NULL, SND_SYNC | SND_FILENAME);
         }

      if (auto_print)
         {
         PrintBackBufferToDC();
         }

      //
      // Exit if we're not supposed to loop
      //

      if (!loop)
         {
         break;
         }
      }

   force_redraw = TRUE;
   return TRUE;
}

//****************************************************************************
//
// CMD_toggle_beep(void)
//
//****************************************************************************

void CMD_toggle_beep(void)
{
   INI_beep = !INI_beep;
   CheckMenuItem(hmenu, IDM_LISTEN_BEEP, INI_beep ? MF_CHECKED : MF_UNCHECKED);
}

//****************************************************************************
//
// CMD_about(void)
//
//****************************************************************************

void CMD_about(void)
{
   MessageBox(hWnd,
             "Usage: 7470 <plotter data file or GPIB address> [...]",
             "HP 7470A Plotter Emulator "VERSION" by John Miles, KE5FX (john@miles.io)",
             MB_OK | MB_ICONINFORMATION);
}

//****************************************************************************
//
// Window message receiver procedure for application
//
//****************************************************************************

long FAR PASCAL WindowProc(HWND   hWnd,   UINT   message,   //)
                           WPARAM wParam, LPARAM lParam)
{
   if (GPIB_polling)
      {
      if (message == WM_CHAR)
         {
         if (toupper(wParam) == 'I')
            {
            CMD_alt_colors();
            }
         else
            {
            GPIB_abort = 1;
            }
         }

      if (message == WM_COMMAND)
         {
         SAL_alert_box("Error","Must exit from listen mode first");
         }

      if ((message == WM_LBUTTONDOWN) || (message == WM_RBUTTONDOWN))
         {
         GPIB_abort = 1;
         }

      return DefWindowProc(hWnd, message, wParam, lParam);
      }

   switch (message)
      {
      case WM_PAINT:
         {
         refresh();
         break;
         }

      case WM_KEYDOWN:

         switch (wParam)
            {
            case VK_F1:

               launch_page("7470.htm");
               break;

            case VK_F5:
               CMD_refresh();
               break;

            case VK_DELETE:

               if (GetKeyState(VK_CONTROL) & 0x8000)
                  {
                  CMD_delete_current_source();
                  }
               else
                  {
                  CMD_close_current_source();
                  }
               break;

            case VK_HOME:
               CMD_close_all_sources();
               break;

            default:
               {
               if ((GetKeyState(VK_CONTROL) & 0x8000) && (wParam == 'G'))
                  {
                  force_generic = !force_generic;
                  }
               else
                  {
                  for (S32 i=0; i < n_named_instruments; i++)
                     {
                     ACQINST *A = &named_instruments[i];

                     if (wParam == A->function_key)
                        {
                        CMD_acquire(A->addr, i);
                        break;
                        }
                     }
                  }
               }
            }
         break;

      case WM_CHAR:

         switch (wParam)
            {
            //
            // ESC terminates app
            //

            case VK_ESCAPE:
            case 'q':
            case 'Q':

               CMD_quit();
               break;

            case ' ':

               CMD_refresh();
               break;

            case 'l':

               CMD_load_HPGL();
               break;

            case 'w':

               CMD_listen(FALSE, board_address, NULL, -1, NULL, TRUE);
               break;

            case 'W':

               CMD_listen(TRUE, board_address, NULL, -1, NULL, TRUE);
               break;

            case 's':
            case 'S':

               CMD_save(NULL);
               break;

            case 'e':
            case 'E':

               CMD_erase_caption();
               break;

            case 'c':
            case 'C':

               CMD_edit_caption();
               break;

            case 'a':
            case 'A':

               CMD_antialias();
               break;

            case 'i':
            case 'I':

               CMD_alt_colors();
               break;

            case 'p':
            case 'P':

               CMD_print_image();
               break;

            case 'b':
            case 'B':

               CMD_browse();
               break;

            case 'o':
            case 'O':

               CMD_overlay();
               break;

            case 'r':
            case 'R':

               CMD_rotate();
               break;

            case 'k':
            case 'K':

               CMD_swap();
               break;

            case '-':

               CMD_prev();
               break;

            case '+':

               CMD_next();
               break;

            case '5': CMD_set_resolution(512,384); break;
            case '6': CMD_set_resolution(640,480); break;
            case '8': CMD_set_resolution(800,600); break;
            case '0': CMD_set_resolution(1024,768); break;
            case '9': CMD_set_resolution(1280,960); break;

            case '?':
               CMD_about();
               break;
            }
         break;

      case WM_SYSKEYUP:

         if (wParam == VK_RETURN)
            {
            //
            // User has toggled fullscreen_window mode, clearing video memory...
            //

            background_invalid = SAL_get_preference(SAL_MAX_VIDEO_PAGES);
            }
         break;

      case WM_KILLFOCUS:

         //
         // Video memory has been lost; set up to reload background image
         // (if any) when control returned
         //

         background_invalid = SAL_get_preference(SAL_MAX_VIDEO_PAGES);
         break;

      case WM_COMMAND:
         {
         switch (wParam)
            {
            case IDM_LOAD_HPGL    : CMD_load_HPGL();                  break;
            case IDM_SAVE         : CMD_save(NULL);                   break;
            case IDM_PRINT_IMAGE  : CMD_print_image();                break;
            case IDM_QUIT         : CMD_quit();                       break;
            case IDM_REFRESH      : CMD_refresh();                    break;
            case IDM_BROWSE       : CMD_browse();                     break;
            case IDM_OVERLAY      : CMD_overlay();                    break;
            case IDM_NEXT         : CMD_next();                       break;
            case IDM_PREV         : CMD_prev();                       break;
            case IDM_CLOSE        : CMD_close_current_source();       break;
            case IDM_CLOSE_ALL    : CMD_close_all_sources();          break;
            case IDM_DELETE       : CMD_delete_current_source();      break;
            case IDM_ERASE_CAPTION: CMD_erase_caption();              break;
            case IDM_EDIT_CAPTION : CMD_edit_caption();               break;
            case IDM_512          : CMD_set_resolution(512, 384);     break;
            case IDM_400          : CMD_set_resolution(640, 400);     break;
            case IDM_640          : CMD_set_resolution(640, 480);     break;
            case IDM_800          : CMD_set_resolution(800, 600);     break;
            case IDM_1024         : CMD_set_resolution(1024,768);     break;
            case IDM_1280         : CMD_set_resolution(1280,960);     break;
            case IDM_ALT_COLORS   : CMD_alt_colors();                 break;
            case IDM_ANTIALIAS    : CMD_antialias();                  break;
            case IDM_ROTATE       : CMD_rotate();                     break;
            case IDM_SWAP         : CMD_swap();                       break;
            case IDM_LISTEN_BEEP  : CMD_toggle_beep();                break;
            case IDM_LISTEN       : CMD_listen(FALSE, board_address, NULL, -1, NULL, TRUE); break;
            case IDM_LISTEN_PRINT : CMD_listen(TRUE,  board_address, NULL, -1, NULL, TRUE); break;
            case IDM_ACQUIRE_HW   : CMD_acquire((S32) 0, -1);         break;
            case IDM_ABOUT        : CMD_about();                      break;

            case IDM_USER_GUIDE:
               {
               launch_page("7470.htm");
               break;
               }

            default:
               {
               S32 i;

               for (i=1; i <= 30; i++)
                  {
                  if ((S32) wParam == IDM_REQUESTS+i)
                     {
                     CMD_acquire(wParam - IDM_REQUESTS, -1);
                     break;
                     }
                  }

               for (i=0; i < n_named_instruments; i++)
                  {
                  if ((S32) wParam == IDM_SHORTCUTS+i)
                     {
                     CMD_acquire(named_instruments[i].addr, i);
                     break;
                     }
                  }

               for (i=-1; i <= 30; i++)
                  {
                  if ((S32) wParam == (IDM_PLOTTER_ADDR+1) + i)
                     {
                     board_address = i;
                     CheckMenuItem(hmenu, IDM_PLOTTER_ADDR+1+i, MF_CHECKED);
                     }
                  else
                     {
                     CheckMenuItem(hmenu, IDM_PLOTTER_ADDR+1+i, MF_UNCHECKED);
                     }
                  }

               break;
               }
            }
         }
         break;

      }

    return DefWindowProc(hWnd, message, wParam, lParam);
}

//****************************************************************************
//
// Windows main() function
//
//****************************************************************************

int PASCAL WinMain(HINSTANCE hInst, //)
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nCmdShow)
{
   global_dirs.init("KE5FX", "GPIB", FALSE); 

   //
   // Initialize system abstraction layer -- must succeed in order to continue
   //

   hInstance = hInst;

   SAL_set_preference(SAL_USE_PARAMON, NO);     // Leave parallel ports alone -- we may be using
                                                // them to control the DUT!
   if (!SAL_startup(hInstance,
                    szAppName,
                    TRUE,
                    WinExit))
      {
      return 0;
      }

   //
   // Get current working directory and make sure it's not the Windows desktop
   // (We don't want to drop our temp files there)
   //
   // If it is, try to change the CWD to the current user's My Documents folder
   // Otherwise, leave the CWD alone to permit use of the "Start In" field on a
   // desktop shortcut
   //
   // (Also do this if the current working directory contains "Program Files")
   //

   C8 docs[MAX_PATH] = "";
   C8 desktop[MAX_PATH] = "";

   SHGetSpecialFolderPath(HWND_DESKTOP,
                          desktop,
                          CSIDL_DESKTOPDIRECTORY,
                          FALSE);

   SHGetSpecialFolderPath(HWND_DESKTOP,
                          docs,
                          CSIDL_PERSONAL,
                          FALSE);

   C8 CWD[MAX_PATH] = "";

   if (GetCurrentDirectory(sizeof(CWD),
                           CWD))
      {
       _strlwr(CWD);
       _strlwr(desktop);
    
      if ((!_stricmp(CWD,desktop)) ||
            (strstr(CWD,"program files") != NULL))
         {
         SetCurrentDirectory(docs);
         strcpy(CWD, docs);
         }
      }
     
   //
   // Set default pen colors
   //

   memcpy(standard_pen_colors,
          default_standard_pen_colors,
          sizeof(standard_pen_colors));

   using_default_pen_colors = TRUE;

   //
   // Read 7470.ini from installation directory
   // (OK because 7470.ini is never written)
   //

   C8 path[MAX_PATH];
   GetModuleFileName(NULL, path, sizeof(path)-1);

   _strlwr(path);

   C8 *exe = strstr(path,"7470.exe");

   if (exe != NULL)
      {
      sprintf(exe,"7470.ini");
      }
   else
      {
      strcpy(path,"7470.ini");
      }

   FILE *options = fopen(path,"rt");

   if (options == NULL)
      {
      SAL_alert_box("Warning","Could not find 7470.INI");
      }
   else
      {
      n_specified_pen_colors = 0;

      while (1)
         {
         //
         // Read input line
         //

         C8 linbuf[512];

         memset(linbuf,
                0,
                sizeof(linbuf));

         C8 *result = fgets(linbuf,
                            sizeof(linbuf) - 1,
                            options);

         if ((result == NULL) || (feof(options)))
            {
            break;
            }

         //
         // Skip blank lines, and kill trailing and leading spaces
         //

         S32 l = strlen(linbuf);

         if ((!l) || (linbuf[0] == ';'))
            {
            continue;
            }

         C8 *lexeme  = linbuf;
         C8 *end     = linbuf;
         S32 leading = 1;

         for (S32 i=0; i < l; i++)
            {
            if (!isspace((U8) linbuf[i]))
               {
               if (leading)
                  {
                  lexeme = &linbuf[i];
                  leading = 0;
                  }

               end = &linbuf[i];
               }
            }

         end[1] = 0;

         if ((leading) || (!strlen(lexeme)))
            {
            continue;
            }

         //
         // Terminate key substring at first space
         //

         C8 *value = strchr(lexeme,' ');

         if (value == NULL)
            {
            continue;
            }

         *value = 0;
         value++;

         //
         // Look for key/value pairs
         // Skip spaces and tabs between key and value
         //

         while (1)
            {
            if ((*value == ' ') || (*value == '\t'))
               {
               ++value;
               }
            else
               {
               break;
               }
            }

         if (*value == 0)
            {
            continue;
            }

         //
         // Kill trailing comment and any spaces before it
         //

         C8 *lin_comment = strchr(value,'`');

         if (lin_comment != NULL)
            {
            *lin_comment = 0;

            while (1)
               {
               --lin_comment;

               if (*lin_comment == ' ')
                  {
                  *lin_comment = 0;
                  }
               else
                  {
                  break;
                  }
               }
            }

              if (!_stricmp(lexeme,"res_x"))                   INI_res_x                   = atoi(value);
         else if (!_stricmp(lexeme,"res_y"))                   INI_res_y                   = atoi(value);
         else if (!_stricmp(lexeme,"pos_x"))                   INI_pos_x                   = atoi(value);
         else if (!_stricmp(lexeme,"pos_y"))                   INI_pos_y                   = atoi(value);
         else if (!_stricmp(lexeme,"x_margin"))                INI_x_margin                = atoi(value);
         else if (!_stricmp(lexeme,"y_margin"))                INI_y_margin                = atoi(value);
         else if (!_stricmp(lexeme,"color_scheme"))            INI_color_scheme            = atoi(value);
         else if (!_stricmp(lexeme,"antialias"))               INI_antialias               = atoi(value);
         else if (!_stricmp(lexeme,"background"))              background = INI_background = atoi(value) & 0x07;
         else if (!_stricmp(lexeme,"rotate"))                  user_rotate                 = atoi(value) & 0x03;
         else if (!_stricmp(lexeme,"async_timeout"))           INI_async_timeout           = atoi(value);
         else if (!_stricmp(lexeme,"serial_read_dropout"))     INI_serial_read_dropout     = atoi(value);
         else if (!_stricmp(lexeme,"SP0_detect"))              INI_SP0_detect              = atoi(value);
         else if (!_stricmp(lexeme,"min_plot_bytes"))          INI_min_plot_bytes          = atoi(value);
         else if (!_stricmp(lexeme,"process_OS"))              INI_process_OS              = atoi(value);
         else if (!_stricmp(lexeme,"default_board_addr"))      INI_default_board_addr      = atoi(value);
         else if (!_stricmp(lexeme,"async_xfer_size"))         INI_async_xfer_size         = atoi(value);
         else if (!_stricmp(lexeme,"async_GPIB_timeout_ms"))   INI_async_GPIB_timeout_ms   = atoi(value);
         else if (!_stricmp(lexeme,"release_sys_control"))     INI_release_sys_control     = atoi(value) != 0;
         else if (!_stricmp(lexeme,"ignore_write_aborts"))     INI_ignore_write_aborts     = atoi(value) != 0;
         else if (!_stricmp(lexeme,"auto_print_mode"))         INI_auto_print_mode         = atoi(value);
         else if (!_stricmp(lexeme,"use_default_printer"))     INI_use_default_printer     = atoi(value);
         else if (!_stricmp(lexeme,"beep_when_plot_received")) INI_beep                    = atoi(value);
         else if (!_stricmp(lexeme,"HPGL_preface"))            strcpy(INI_HPGL_preface, value);
         else if (!_stricmp(lexeme,"traffic_diag_filename"))   strcpy(INI_traffic_diag_filename, value);
         else if (!_stricmp(lexeme,"beep_wav_filename"))       strcpy(INI_beep_wav_filename, value);
         else if (!_stricmp(lexeme,"auto_save_filename"))      strcpy(INI_auto_save, value);
         else if (!_stricmp(lexeme,"caption_text"))            strcpy(INI_caption_text, value);
         else if (!_stricmp(lexeme,"OE_reply"))                strcpy(INI_OE_reply,  value);
         else if (!_stricmp(lexeme,"OH_reply"))                strcpy(INI_OH_reply,  value);
         else if (!_stricmp(lexeme,"OI_reply"))                strcpy(INI_OI_reply,  value);
         else if (!_stricmp(lexeme,"OP_reply"))                strcpy(INI_OP_reply,  value);
         else if (!_stricmp(lexeme,"OO_reply"))                strcpy(INI_OO_reply,  value);
         else if (!_stricmp(lexeme,"OF_reply"))                strcpy(INI_OF_reply,  value);
         else if (!_stricmp(lexeme,"OA_reply"))                strcpy(INI_OA_reply,  value);
         else if (!_stricmp(lexeme,"OC_reply"))                strcpy(INI_OC_reply,  value);
         else if (!_stricmp(lexeme,"pu")) {  // specify plotter units range
            
            sscanf(value,"%lg,%lg,%lg,%lg",&GL_XMIN,&GL_YMIN,&GL_XMAX,&GL_YMAX);
         }
         else if (!_strnicmp(lexeme,"pen_",4))
            {
            S32 r=0,g=0,b=0;
            sscanf(value,"%d,%d,%d",&r,&g,&b);

            if (n_specified_pen_colors == N_PENS)
               {
               SAL_alert_box("Warning","Too many pen colors specified in 7470.ini (max=%d)",N_PENS);
               }
            else
               {
               S32 INI_color = RGB_TRIPLET(r,g,b);

               if (INI_color != standard_pen_colors[n_specified_pen_colors])
                  {
                  standard_pen_colors[n_specified_pen_colors] = INI_color;
                  using_default_pen_colors = FALSE;
                  }

               ++n_specified_pen_colors;
               }
            }
         else if (!_strnicmp(lexeme,"instrument_",11))
            {
            ACQINST *A = &named_instruments[n_named_instruments];
            bool OK = TRUE;

            C8 *sep = strstr(value," at GPIB address ");

            if (sep == NULL)
               {
               SAL_alert_box("Warning","Syntax error in 7470.ini entry (%s)",lexeme);
               OK = FALSE;
               }
            else
               {
               strcpy(A->name, value);
               A->addr = atoi(&sep[17]);
               A->function_key = -1;

               C8 *k = strstr(A->name,"\\t");

               if (k != NULL)
                  {
                  memmove(k, k+1, strlen(k));
                  *k = '\t';

                  S32 n = atoi(&k[2]);

                  A->function_key = VK_F1 + (n - 1);

                  if ((A->function_key < VK_F1) || (A->function_key > VK_F24))
                     {
                     SAL_alert_box("Warning","Invalid function key assignment in 7470.ini entry (%s)",lexeme);
                     OK = FALSE;
                     }
                  }
               }

            if (OK)
               {
               ++n_named_instruments;
               }
            }
         else
            {
            SAL_alert_box("Warning","Unknown 7470.ini entry '%s'",lexeme);
            }
         }

      fclose(options);
      }

   //
   // If 7470user.ini has not yet been created, do it now
   //
   // (Can't do this at installation time because the installer may be run
   // by a different user, and because we don't want to wipe out the session state)
   //

   _snprintf(user_INI_pathname, sizeof(user_INI_pathname)-1,"%s7470user.ini", global_dirs.LOCDATA);

   FILE *test = fopen(user_INI_pathname,"rt");

   if (test != NULL) 
      {
      fclose(test);
      }
   else  
      {
      C8 default_user_INI_pathname[MAX_PATH] = { 0 } ;
      _snprintf(default_user_INI_pathname, sizeof(default_user_INI_pathname)-1, "%sdefault_7470user.ini", global_dirs.EXE);

      printf("Copying %s to %s\n", default_user_INI_pathname, user_INI_pathname);

      CopyFile(default_user_INI_pathname, 
               user_INI_pathname,
               FALSE);
      }

   //
   // Allow 7470user.ini to override 7470.ini settings and provide initial set of files to load
   //
   // (If current working directory contains a 7470user.ini file, use it in preference to the user-global one)
   //

   C8 INI_CWD_pathname[MAX_PATH] = { 0 };
   _snprintf(INI_CWD_pathname, sizeof(INI_CWD_pathname)-1, "%s7470user.ini", global_dirs.ICW);

   test = fopen(INI_CWD_pathname, "rt");
   
   if (test != NULL)
      {
      strcpy(user_INI_pathname, INI_CWD_pathname);
      fclose(test);
      }

   printf("User INI pathname: %s\n", user_INI_pathname);

   n_data_sources = 0;

   FILE *user_options = fopen(user_INI_pathname,"rt");

   if (user_options != NULL)
      {
      while (1)
         {
         //
         // Read input line
         //

         C8 linbuf[512] = { 0 };

         C8 *result = fgets(linbuf, 
                            sizeof(linbuf) - 1, 
                            options);

         if ((result == NULL) || (feof(options)))
            {
            break;
            }

         //
         // Skip blank lines, and kill trailing and leading spaces
         //

         S32 l = strlen(linbuf);

         if ((!l) || (linbuf[0] == ';'))
            {
            continue;
            }

         C8 *lexeme  = linbuf;
         C8 *end     = linbuf;
         S32 leading = 1;

         for (S32 i=0; i < l; i++)
            {
            if ((!isspace((U8) linbuf[i])) && (linbuf[i] != ';'))      // (Removes trailing semicolons as well as leading ones)
               {
               if (leading)
                  {
                  lexeme = &linbuf[i];
                  leading = 0;
                  }

               end = &linbuf[i];
               }
            }

         end[1] = 0;

         if ((leading) || (!strlen(lexeme)))
            {
            continue;
            }

         //
         // Terminate key substring at first space
         //

         C8 *value = strchr(lexeme,' ');

         if (value == NULL)
            {
            value = strchr(lexeme,'\t');
            }

         if (value == NULL)
            {
            continue;
            }

         *value = 0;
         value++;

         //
         // Look for key/value pairs
         // Skip spaces and tabs between key and value
         //
         
         while (1)
            {
            if ((*value == ' ') || (*value == '\t'))
               {
               ++value;
               }
            else
               {
               break;
               }
            }

         if (*value == 0)
            {
            continue;
            }

         if      (!_stricmp(lexeme,"res_x"))            INI_res_x                   = atoi(value);
         else if (!_stricmp(lexeme,"res_y"))            INI_res_y                   = atoi(value); 
         else if (!_stricmp(lexeme,"antialias"))        INI_antialias               = atoi(value);
         else if (!_stricmp(lexeme,"color_scheme"))     INI_color_scheme            = atoi(value);
         else if (!_stricmp(lexeme,"overlay_mode"))     INI_mode                    = atoi(value);
         else if (!_stricmp(lexeme,"background"))       background = INI_background = atoi(value) & 0x07;
         else if (!_stricmp(lexeme,"rotate"))           user_rotate                 = atoi(value) & 0x03;
         else if (!_stricmp(lexeme,"source"))
            {
            if (!strlen(lpCmdLine))
               {
               add_files_to_list(value);
               }
            }
         else 
            {
            SAL_alert_box("Warning","Unknown 7470user.ini entry '%s'",lexeme);
            }
         }

      fclose(options);
      }

   //
   // Apply .INI values
   //

   for (S32 i=0; i < MAX_DATA_SOURCES; i++) 
      {
      data_source_background[i] = INI_background;
      data_source_alt       [i] = INI_color_scheme;
      data_source_ant       [i] = INI_antialias;
      }

   board_address               = INI_default_board_addr;
   alt_colors                  = INI_color_scheme;
   RES_X                       = INI_res_x;
   RES_Y                       = INI_res_y;
   default_X_margin            = INI_x_margin;
   default_Y_margin            = INI_y_margin;
   requested_RES_X             = INI_res_x;
   requested_RES_Y             = INI_res_y;

   if (INI_traffic_diag_filename[0])
      {
      _unlink(INI_traffic_diag_filename);
      }

   //
   // Create application window
   //

   SAL_set_preference(SAL_ALLOW_WINDOW_RESIZE, NO);
   SAL_set_preference(SAL_MAXIMIZE_TO_FULLSCREEN, NO);
   SAL_set_preference(SAL_USE_DDRAW_IN_WINDOW, NO);

   SAL_set_application_icon((C8 *) IDI_ICON);

   hWnd = SAL_create_main_window();

   if (hWnd == NULL)
      {
      SAL_shutdown();
      return 0;
      }

   //
   // Register window procedure
   //

   SAL_register_WNDPROC(WindowProc);

   //
   // Register exit handler and validate command line
   //

   atexit(AppExit);

   //
   // Set RES_X * RES_Y mode at desired pixel depth
   //

   set_display_mode();

   SAL_show_system_mouse();

   //
   // Load file(s) specified on command line, if any, instead of restoring from 7470user.ini
   //

   if (strlen(lpCmdLine))
      {
      //
      // Command tail was provided -- disable auto-save option if present
      //

      INI_auto_save[0] = 0;

      //
      // Parse command line into discrete strings, adding each string (or the
      // set of files it specifies) to the input-source list
      //

      C8 src[MAX_PATH];
      strcpy(src, lpCmdLine);

      S32 n = 0;

      C8 *srcptr = src;
      S32 qp = 0;

      while (*srcptr)
         {
         C8 *input = srcptr;

         while (1)
            {
            if (!*srcptr)
               {
               break;
               }

            if (*srcptr == '\"')
               {
               qp = !qp;
               }

            if ((*srcptr == ' ') && (!qp))
               {
               *srcptr++ = 0;
               break;
               }

            ++srcptr;
            }

         if (*input == 0)
            {
            //
            // No (more) strings to process, exit outer loop
            //

            break;
            }

         //
         // If string contains * or ? wildcards, generate file list based on
         // specification
         //

         if (strchr(input,'*') || strchr(input,'?'))
            {
            add_files_to_list(input);
            continue;
            }

         //
         // Otherwise, string is a filename or GPIB address -- add it to the list
         //

         if ((n_data_sources + 1) < MAX_DATA_SOURCES)
            {
            C8 buffer[MAX_PATH];

            if (input[0] == '\"')
               {
               strcpy(buffer,&input[1]);
               }
            else
               {
               strcpy(buffer,input);
               }

            C8 *q = strchr(buffer,'\"');

            if (q != NULL)
               {
               *q = 0;
               }

            S32 addr = valid_GPIB_address(buffer);

            data_source_GPIB    [n_data_sources] = addr;
            data_source_shortcut[n_data_sources] = -1;

            for (S32 i=0; i < n_named_instruments; i++)
               {
               if (named_instruments[i].addr == addr)      // (Use the last matching entry, so 18 will
                  {                                        // correspond to the F8 option for the 856xA)
                  data_source_shortcut[n_data_sources] = i;
                  }
               }

            if (addr == -1)
               {
               strcpy(data_source_file[n_data_sources], buffer);
               }
            else
               {
               sprintf(data_source_file[n_data_sources], "GPIB_%d.plt", addr);
               }

            ++n_data_sources;
            }
         }

      //
      // Overlay multiple explicitly-specified sources by default
      //

      if (n_data_sources == 1)
         {
         INI_mode = M_BROWSE;
         }
      else
         {
         INI_mode = M_OVERLAY;
         }
      }

   //
   // Force initial acquisition of data from any GPIB addresses in source list
   //

   for (S32 i=0; i < n_data_sources; i++)
      {
      if (data_source_GPIB[i] != -1)
         {
         _unlink(data_source_file[i]);
         }
      }

   //
   // Generate initial plot
   //

   force_redraw = TRUE;
   current_data_source = 0;
   user_rotate = data_source_rotate[current_data_source];
   background  = data_source_background[current_data_source];
   alt_colors  = data_source_alt[current_data_source];
   antialias   = data_source_ant[current_data_source];

   // -------------------------------
   // Main plot loop
   // -------------------------------

   while (1)
      {
      //
      // Check Windows message queue
      //

      Sleep(10);

      SAL_serve_message_queue();

      if (INI_auto_save[0])
         {
         if (CMD_listen(FALSE, board_address, NULL, -1, NULL, FALSE))
            {
            CMD_save(INI_auto_save);
            }
         break;
         }

      if (INI_auto_print_mode)
         {
         CMD_listen(TRUE, board_address, NULL, -1, NULL, TRUE);
         INI_auto_print_mode = 0;
         }

      //
      // Read mouse
      //

      last_left_button  = left_button;
      last_right_button = right_button;

      left_button  = (GetKeyState(VK_LBUTTON) & 0x8000);
      right_button = (GetKeyState(VK_RBUTTON) & 0x8000);

      if (SAL_is_app_active())
         {
         POINT cur;
         GetCursorPos(&cur);

         SAL_WINAREA area;
         SAL_client_area(&area);

         mouse_x = cur.x - area.x;
         mouse_y = (cur.y - area.y) - menu_height;

         if ((left_button) && (!last_left_button)  // TODO: not in dialog
              && 
             ((mouse_y < RES_Y) && (mouse_y >= 0))
              &&
             ((mouse_x < RES_X) && (mouse_x >= 0))
              && 
             (caption_target == -1) 
              &&
             (current_data_source < n_data_sources))
            {
            caption_target = current_data_source;

            if (!strlen(INI_caption_text))
               {
               CMD_edit_caption();
               }

            if (strlen(INI_caption_text))
               {
               add_caption = (C8 *) malloc(MAX_CAPTION_LEN+64);
               assert(add_caption);

               sprintf(add_caption,CAPTION_LOCATOR"PA %d,%d;LB%s%c",
                  (S32) unscale_x((COORD) mouse_x),
                  (S32) unscale_y((COORD) (mouse_y + menu_height)),
                  INI_caption_text,
                  3);
               }

            force_redraw = TRUE;
            }
         }

      //
      // Refresh screen if needed
      //

      if (background_invalid)
         {
         refresh();
         background_invalid--;
         }

      //
      // Update the plot
      //

      if (force_redraw)
         {
         force_redraw = FALSE;
         render_source_list();
         background_invalid = 1;
         }

      //
      // Detect requested display resolution changes
      //

      if ((requested_RES_X != RES_X) ||
          (requested_RES_Y != RES_Y))
         {
         RES_X = requested_RES_X;
         RES_Y = requested_RES_Y;
         set_display_mode();
         force_redraw = TRUE;
         }
      }

   return 0;
}

