package nxm.sys.prim;

import java.awt.Component;
import java.awt.Point;
import java.util.Date;
import java.util.Vector;

import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.Installation;
import nxm.sys.inc.ListFile;
import nxm.sys.inc.ListInterface;
import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.CsvFile;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.RamResource.RamFile;
import nxm.sys.lib.Registry;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.libg.GMenu;
import nxm.sys.libg.GPrimitive;
import nxm.sys.libg.GPrompt;
import nxm.sys.libg.GraphicsUtil;
import nxm.sys.libg.GridSheet;
import nxm.sys.libg.MJList;
import nxm.sys.libg.MJListConfig;
import nxm.sys.libg.MJPopup;
import nxm.sys.libg.MWindow;

/** Displays Midas data in the form of textual lists. This is a replacement for {@link list}.
    @author  J. Czechowski IV
    @since   NeXtMidas 1.9.5
*/
public class list2 extends GPrimitive implements Chainable {
  /** The list of Pointer Message Options (value = {@value}). */
  public static final String pointerMessageList = plot.pointerMessageList;
  /** Pointer Message Option: Send mouse clicks. */
  public static final int ONCLICK  = plot.ONCLICK;
  /** Pointer Message Option: Send mouse moves.  */
  public static final int ONMOTION = plot.ONMOTION;
  /** Pointer Message Option: Send mouse drags.  */
  public static final int ONDRAG   = plot.ONDRAG;

  /** The list of Exit Options (value = {@value}). */
  public static final String exitList  = "RETURN,MENU,MESSAGE,WINDOW,AUTO";

  /** RETURN Exit Option  */ public static final int EXIT_RETURN  =  0x1;
  /** MENU Exit Option    */ public static final int EXIT_MENU    =  0x2;
  /** MESSAGE Exit Option */ public static final int EXIT_MESSAGE =  0x4;
  /** WINDOW Exit Option  */ public static final int EXIT_WINDOW  =  0x8;
  /** ALL Exit Option     */ public static final int EXIT_ALL     =  0xF;
  /** AUTO Exit Option    */ public static final int EXIT_AUTO    = 0x10;

  /** List of click action options (value = {@value}). @since NeXtMidas 2.5.0 */
  public static final String buttonActionList = "None,Menu,Message,ClearSelection";
  private static final int BTN_NONE      = 1; // Button Action: none
  private static final int BTN_MENU      = 2; // Button Action: show/hide menu
  private static final int BTN_MESSAGE   = 3; // Button Action: show/hide menu
  private static final int BTN_CLEAR     = 4; // Button Action: clear selection

  /** Default sort limit */
  private static final int DEF_SORT_LIMIT = 8192;
  /** Default base index */
  private static final int DEF_BASE_INDEX = 1;

  private MJList        mjlist;                  // The underlying MJList.
  private ListInterface listIF;                  // Thread-safe instance of mjlist.
  private Table         template    = null;      // Template to use for the file
  private boolean       done        = false;     // Indicates if the primitive is done.
  private boolean       useRamFile  = false;     // Indicates if the file should be copied to RAM.
  private FileName      fileName    = null;      // The name of the last file opened.
  private FileName      ramFileName = null;      // The name of the RAM file being used.
  private int           controls    = ONCLICK;   // Controls mask (see plot).
  private double        pollInterval= 0.0;       // The number of seconds between polls (0=disable).
  private double        lastPoll    = 0.0;       // The time of the last poll.
  private boolean       tryOpen     = false;     // Keep trying to open the file?
  private boolean       autoSelect  = true;      // Is auto-select on?
  private boolean       unsafe      = false;     // Allow access to MJList that is not thread-safe?
  private boolean       prevAndCurr = false;     // Show previous and current in selection with /LEGACY
  private int           lmbButton   = BTN_NONE;  // Action for LMB click
  private int           mmbButton   = BTN_MENU;  // Action for MMB click
  private int           rmbButton   = BTN_CLEAR; // Action for RMB click
  private int           exit        = -1;
  private boolean       isTable     = false;
  private String        wtitle      = null;        // window title specified via /wtitle= (since 3.1.0)
  private int           maxRamFileSize = 0; // keep size within reasonable limits (since 3.1.2 - Bug 2585)

  private LegacyInterface gsInterface = null; // The legacy interface to support LIST messages.
  private Table clickActionTable = new Table("{CELL=SELECT,ROW=SELECT,COL=SORT}"); // The behavior for LM clicks

  /** default constructor */
  public list2 () {}

  public int open () {
    autoSelect       = MA.getState("AUTOSELECT",autoSelect);
    clickActionTable = MA.getTable("CLICKACTION",clickActionTable);
    unsafe           = MA.getState("/UNSAFE",unsafe);
    prevAndCurr      = MA.getState("/PREVANDCURR",true); // Only used with /LEGACY

//    BaseFile baseFile    = (MA.find("FILE"))? MA.getFile("FILE"): null;   // prior to 3.1.0, changed for BUG 2392
    BaseFile baseFile = null;
    Table tableFile = (MA.find("FILE"))? MA.getTable("FILE",null, Args.NO_ERROR): null;  // since 3.1.0 for BUG 2392
    if (tableFile == null) {  // if not a table, look for a File
      baseFile    = (MA.find("FILE"))? MA.getFile("FILE"): null;
    } else {
      isTable = true;
    }

    int      maxSortSize = MA.getL("/SORTLIMIT", DEF_SORT_LIMIT);
    int      baseIndex   = MA.getL("/BASE", DEF_BASE_INDEX);

    // check for null added to allow for addRow(..) creating/ updating template (BUG 2058)
    if (template==null) template = MA.getTable("TEMPLATE", null, Args.NO_ERROR); // do NOT error when given a XDATALIST template filename (handled below)

    if (M.pipeMode==Midas.PINIT) exit=0; else exit=EXIT_ALL;
    exit = MA.getOptionMask("/EXIT",exitList,exit);

    setButtonAction(MA.getTable("BUTTONACTION"));
    if (MA.find("/POLL")) {
      M.warning("LIST2: Use of /POLL with LIST2 is deprecated unless you are intending to adjust processing interval. " +
                "For LIST2 file data polling, use /POLLDATA.");
      setPollInterval(MA.getD("/POLL", pollInterval));
    }
    // /POLLDATA support added in 3.3.1 for Bug 2701
    if (MA.find("/POLLDATA")) {
      setPollInterval(MA.getD("/POLLDATA"));
    }

    useRamFile = !MA.getState("/MOD", (pollInterval != 0.0));
    useRamFile = MA.getState("/USERAMFILE", useRamFile);        // direct setting of useramfile (added in 3.1.2 - Bug 2168/2585)
    if (MA.isPresent("/USERAMFILEMAXSIZE")) {
      setRamFileMaxSize(MA.getL("/USERAMFILEMAXSIZE"));  // prevent outOfMemory error creating ramfile (added in 3.1.2 - Bug 2168/2585)
    } else {
      setRamFileMaxSize(RamFile.MAX_INITIAL);
    }

    if (MA.find("/COPY")) {
      M.deprecate("LIST2: Use /MOD switch instead of /COPY");
      useRamFile  = MA.getState("/COPY", useRamFile);
    }

    if (unsafe) {
      M.deprecate("LIST2: Use of /UNSAFE=TRUE is deprecated in LIST2.");
      M.warning("LIST2: Running in /UNSAFE mode. Calls to the underlying library are NOT thread-safe.");
    }

    if (useRamFile && (pollInterval != 0.0)) {
      M.error("LIST2: Poll time of "+pollInterval+" conflicts with /MOD=FALSE");
    }

    setControls(MA.getS("CNT"));

    // BEGIN DEPRECATED SECTION ////////////////////////////////////////////////////////////////////
    if ((template == null) && MA.isPresent("TEMPLATE")) {
      BaseFile templ = MA.getFile("TEMPLATE");

      if (templ instanceof DataFile) {
        M.deprecate("LIST2: Use of X-Midas style templates is deprecated, use CONVERT to convert "
                  + "existing X-Midas templates to the current format.");
        template = convert.convertTemplate2Table((DataFile)templ);
      }
    }

    if (MA.getState("/LEGACY")) {
      M.deprecate("LIST2: The /LEGACY interface to LIST2 is deprecated.");

      gsInterface = new LegacyInterface();
      useRamFile  = !MA.getZ("/MOD");
    }
    // END DEPRECATED SECTION //////////////////////////////////////////////////////////////////////

    if ((template != null) && (baseFile == null)) {
      baseFile = setFileFromTemplate(template);
    }

    // If using a Table and no template, get the template from the first row in the table
    if (template==null && baseFile==null && isTable) {
      baseFile = setFileFromTemplate(getDefaultTemplate());  // but we need a default temple to expand on
    }

    MW = new MWindow("LIST2",this);
    MW.open();
    MW.setSize(800,500);
    MW.addTo(this);

    init(MW, baseFile, template, baseIndex, maxSortSize, autoSelect);

    wtitle = MA.getS("/WTITLE", null); // since 3.1.0 for BUG 2496
    MW.setTitle(getTitle());
    if (isTable) addRowsFromTable(tableFile);  // since 3.1.0, process a given Table (BUG 2392)

    return NORMAL;
  }

  public int process () {
    int status = NOOP;

    if (done) {
      status = FINISH;
    } else if (tryOpen) {
      if (!doOpenFile(fileName, false)) {
        Time.sleep(0.5);
      }
      status = NORMAL;
    } else {
      double numRead = 0.0;

      if (pollInterval > 0.0) {
        if (Time.current() >= (pollInterval + lastPoll)) {
          numRead = pollImmediately(true);
        } else {
          // Do not make reads between poll intervals.
        }
      } else {
        numRead = pollImmediately(false);
      }
      status = (numRead <= 0.0)? NOOP: NORMAL;
    }

    return status;
  }

  public int close () {
    if (MW != null) { MW.close(); }
    done = true;
    deleteRamFile();
    return NORMAL;
  }


  /** Create a file based on a template. This method uses code extracted from {@link #open()}
      to assist in making a new file from a new or updated template.
      @param templ The template to use
      @return The file created from the template
      @since NeXtMidas 3.1.0
   */
  private BaseFile setFileFromTemplate (Table templ) {   // method created for BUG 2058
    if (templ == null) templ = template;
    StringBuffer str = new StringBuffer();
    String[] keys = templ.getKeys();
    DataFile df   = new DataFile(M, getRamFileName());

    df.setType(3000);
    for (int i = 0; i < keys.length; i++) {
      keys[i] = keys[i].toUpperCase().trim();
      if (!(keys[i].startsWith("_") && keys[i].endsWith("_"))) {
        df.addSubRec(keys[i], "XA");
        if (str.length() > 0) { str.append(","); }
        str.append(keys[i]).append("/XA");
      }
    }
    df.open(DataFile.OUTPUT);

    if (isTable) M.info("LIST2: Creating ramFile based on given Table.");
    else         M.info("LIST2: No input file found, creating Type 3000 RamFile to match template ("+str+").");
    return df;
  }

  /** Whether or not the list is a Table (including .tbl files)
      @return true if the list is a Table
      @since NeXtMidas 3.1.0
   */
  public boolean isTable () { return isTable; }

  /** Creates a template if one s not defined. Based off the default template made in
      MJList.doApplyTemplate(). This is used to create a dummy file at startup when
      listing a Table
      @since NeXtMidas 3.1.0
   */
  private Table getDefaultTemplate () {
    Table tbl = new Table();
    Table globalTbl = new Table();
    globalTbl.put("TYPE", "LIST2");
    globalTbl.put("AUTOHIDE","TRUE");
    globalTbl.put("AUTOORDER", "TRUE");

    tbl.put("_GLOBAL_", globalTbl);

    return tbl;
  }

  /** This cannot use {@link MJList#addRows(Table)} because there may not be a
      templated MJList reference yet (if given just a Table to list). This method calls {@link #addRow(Table)},
      which will make a new template if necessary
      @param tbl Table containing the rows.
   */
  private void addRowsFromTable (Table tbl) {

    for (Table.Iterator ti = tbl.iterator(); ti.getNext(); ) {
      Object obj = ti.value;
      if (obj instanceof Table) {
        addRow(Convert.o2t(obj));
      }
    }

  }

  /** Initializes everything but use legacy interface.
      @param midas      The midas context to use.
      @param baseFile   The file to show (null to leave unset).
      @param template   The template to use (null to leave unset).
      @deprecated This method has always been deprecated, use init(midas,baseFile,template).
   */
  @Deprecated
  public void initLegacy (Midas midas, BaseFile baseFile, Table template) {
    gsInterface = new LegacyInterface();
    useRamFile  = true;
    init(midas, baseFile, template);
  }

  /** Initializes everything with default sort limit and base index.
      This method is intended for use by external primitives.
      @param midas      The midas context to use.
      @param baseFile   The file to show (null to leave unset).
      @param template   The template to use (null to leave unset).
   */
  public void init (Midas midas, BaseFile baseFile, Table template) {
    M = midas;
    init(null, baseFile, template, DEF_BASE_INDEX, DEF_SORT_LIMIT, true);
    mjlist.addMouseKeyListeners(new MWindow("LIST2",this)); // Add the basic listeners.
  }

  /** Initializes everything.
      @param mw          The MWindow to use (can be null).
      @param baseFile    The file to show (null to leave unset).
      @param template    The template to use (null to leave unset).
      @param baseIndex   Set this to 1 for 1-based indexing, 0 for 0-based indexing.
      @param maxSortSize The maximum sortable table size (-1 to disable sorting).
      @param autoSelect  Enable auto select?
   */
  private void init (final MWindow mw, final BaseFile baseFile, final Table template,
                     final int baseIndex, final int maxSortSize, final boolean autoSelect) {
    try {
      if (!Shell.isHeadless()) { // Since NeXtMidas 3.5.0. Skip if in Headless state. BUG 2817
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            init0(mw, baseFile, template, baseIndex, maxSortSize, autoSelect);
          }
        });
      }
    } catch (Exception e) {
      Shell.printStackTrace(e);
    }
  }

  /** Called by init(..) when inside the AWT/Swing drawing thread. */
  private void init0 (final MWindow mw, final BaseFile baseFile, final Table template,
                      final int baseIndex, final int maxSortSize, final boolean autoSelect) {
    mjlist = new MJList(M, mw, maxSortSize, baseIndex);
    listIF = mjlist.getThreadSafeInstance();

    if (baseFile != null) { openFile(baseFile); }
    if (template != null) { mjlist.setTemplate(template); }

    Table settings = new Table();  // added in 3.1.0 for Bug 1970
    settings.setKey("AUTOSELECT",autoSelect);
    settings.setKey("PREVANDCURR",prevAndCurr);

    if (gsInterface == null) {
      //tabularDisp.setTableSelectionModel(new ListSelectionModel(tabularDisp, autoSelect, this));
      // Added line below in 3.1.0 (BUG 1970) to support getting messages without the /legacy switch
      mjlist.setTableSelectionModel(new MJList.NMTableSelectionModel(mjlist,this, settings));
    } else {
      //mjlist.setTableSelectionModel(new LegacySelectionModel(mjlist, autoSelect, this, prevAndCurr));  // changed to line below in 3.1.0

      // Added lines below in 3.1.0 (BUG 1970) to support getting messages without the /legacy switch
      settings.setKey("LEGACY","true");
      mjlist.setTableSelectionModel(new MJList.NMTableSelectionModel(mjlist,this, settings));
    }
    mjlist.setMessageHandler(this);
    mjlist.setCellSelectionAllowed(autoSelect);
    mjlist.setCellClickAction(clickActionTable.getS("CELL"));
    mjlist.setRowClickAction (clickActionTable.getS("ROW"));
    mjlist.setColClickAction (clickActionTable.getS("COL"));
    mjlist.setEditable(MA.getState("/EDITABLE", true));
    mjlist.setDragAndDropEnabled(MA.getState("/DRAGDROP"));  // since 3.3.0 (Bug 2120)
  }

  /** Gets the name of the file that was loaded. In cases where a copy of the file is being used
      (i.e. /MOD=FALSE) this returns the name of the file that was initially read in and not the
      copy.
      @return the name of the file that was loaded
      @since NeXtMidas 2.5.0
   */
  public FileName getFileName () {
    return fileName;
  }

  /** Gets the temp file used when /MOD=FALSE is set.
      @return The file name or null if /MOD=TRUE.
      @since NeXtMidas 2.5.0
   */
  public FileName getTempFile () {
    return (useRamFile) ? ramFileName : null;
  }

  /** Sets the poll time.
      @param seconds The number of seconds between polls. Set this to 0 to disable polling.
   */
  public void setPollInterval (double seconds) {
    pollInterval = Math.max(0.0, seconds);
  }

  /** Gets the poll time in seconds. This is 0 if polling is disabled.
   *  @return the poll time in seconds
   */
  public double getPollInterval () {
    return pollInterval;
  }

  /** Gets a copy of the buttonAction table currently in use.
      @return copy of the buttonAction table currently in use
      @since NeXtMidas 2.5.0
   */
  public Table getButtonAction () {
    Table tbl = new Table();
    tbl.put("LMB", Parser.get(buttonActionList, lmbButton));
    tbl.put("MMB", Parser.get(buttonActionList, mmbButton));
    tbl.put("RMB", Parser.get(buttonActionList, rmbButton));
    return tbl;
  }

  /** Sets the click actions to use. The table should be in the form:
      <pre>
        {LMB=[action],MMB=[action]RMB=[action]}
      </pre>
      Where each <code>[action]</code> is one of the items on the {@link #buttonActionList} or
      <code>"DEFAULT"</code> to use the LIST2 default.
      @param tbl The click action table.
      @since NeXtMidas 2.5.0
   */
  public void setButtonAction (Table tbl) {
    if (tbl != null) {
      lmbButton = findButtonAction(tbl.getS("LMB"), lmbButton, BTN_NONE);
      mmbButton = findButtonAction(tbl.getS("MMB"), mmbButton, BTN_MENU);
      rmbButton = findButtonAction(tbl.getS("RMB"), rmbButton, BTN_CLEAR);
    }
  }

  /** Only used by setButtonAction(..). */
  private int findButtonAction(String val, int current, int def) {
    int action = current;
    if (val != null) {
      if (val.equalsIgnoreCase("DEFAULT")) {
        action = def;

      } else {
        action = Parser.find(buttonActionList, val, def);
        if (action < 0) {
          throw new MidasException("LIST2: Invalid buttonAction '"+val+"', valid options are '"
                                  +buttonActionList+"'.");
        }
      }
    }
    return action;
  }

  /** Sets the active template from an Object that can be converted to a Table. The
      Object must be convertible to a Table, (e.g. .tbl file, .xml file, Table, etc) or else it will be treated as null.
      Any previous template settings will be discarded.  <br><br>
      @see nxm.sys.lib.Convert#o2t(Object, Object)
      @param template The template.
      @since NeXtMidas 3.1.2
   */
  public void setTemplate (Object template) {
    // This method is basically a pass-thru to the interface to support
    // Objects that can be converted to Tables  (Bug 2602)
    if (getList()!=null) {
      getList().setTemplate(Convert.o2t(template,this));
    }
  }
  /** Updates the current template by adding additional (or overriding) entries. Any previous
      template settings will be kept (if not overridden). The object given must be able to
      be converted a Table, (e.g. .tbl file, .xml file, Table, etc) or else it will be treated as null.
      @see nxm.sys.lib.Convert#o2t(Object, Object)
      @param template The template.
      @since NeXtMidas 3.1.2
  */
  public void updateTemplate (Object template) {
    // This method is basically a pass-thru to the interface to support
    // Objects that can be converted to Tables  (Bug 2602)
    if (getList()!=null) {
      getList().updateTemplate(Convert.o2t(template,this));
    }
  }

  /** Sets the integer mask value for the controls. Use {@link #setControls(String)} to set via
      a string.
      @param cnt The controls mask {@link plot#pointerMessageList}.
   */
  public void setControls(String cnt) {
    setControlsMask(Parser.mask(pointerMessageList, cnt, controls));
  }

  /** Gets the mask value for the controls (see {@link plot#pointerMessageList}).
   *  @return mask value for the controls in String form
   */
  public String getControls() {
    return Parser.mask2s(pointerMessageList, getControlsMask());
  }

  /** Sets the integer mask value for the controls. Use {@link #setControls(String)} to set via
      a string.
      @param cnt The controls mask {@link plot#pointerMessageList}.
   */
  public void setControlsMask(int cnt) {
    controls = cnt;
  }

  /** Gets the integer mask value for the controls (see {@link plot#pointerMessageList}).
   *  @return integer mask value for the controls
   */
  public int getControlsMask() {
    return controls;
  }

  /** Refresh Midas Window */
  public void refresh () {
    if (MW != null) MW.refresh();
  }

  /** Poll the file immediately for any new/updated data. This can be called even if polling is
      turned off. If polling is turned on, this will poll the file immediately and will reset the
      polling timer so it will not poll again until <code>now+pollInterval</code>. Unlike opening
      a new file, the template is not reinitialized; it is assumed that the structure of the file
      has not been changed.<br><br>
      Note: When used with a cached RAM file (which is the default), this effectively does nothing.
      Use /MOD, /POLLDATA, or /UseRamFile=false to circumvent the use of the RAM file.
      @return The number of rows read.
   */
  public double pollImmediately() {
    return pollImmediately(true);
  }

  /** Polls for new/updated data.
      @param poll If true checks for new data (added to end of file) and changed data in the file.
                  If false checks only for new data.
      @return The number of rows read.
   */
  private synchronized double pollImmediately(boolean poll) {
    double numRead = 0.0;
    if (getList() != null) {
      numRead  = getList().pollData(poll);
      lastPoll = Time.current();
    }
    return numRead;
  }

  /** Opens a file.
      @param fileName The file to open.
   */
  public void openFile(String fileName) {
    doOpenFile(fileName, true);
  }

  /** Opens a file.
      @param fileName The file to open.
      @since NeXtMidas 2.5.0
   */
  public void openFile(FileName fileName) {
    doOpenFile(fileName, true);
  }

  /** Opens a file.
      @param baseFile The file to open.
   */
  public void openFile(BaseFile baseFile) {
    doOpenFile(baseFile, true);
  }

  /** Opens a file.
      @param listFile The file to open.
   */
  public void openFile(ListFile listFile) {
    doOpenFile(listFile, true);
  }

  /** Opens a file.
      @param file The file to open.
      @param warn if true, display warning if unable to find file
   */
  private boolean doOpenFile(Object file, boolean warn) {
    ListFile listFile = lmbButton(file);
    boolean  readOnly = (pollInterval > 0)  || !useRamFile; // Added check for UseRamFile in 3.3.1 for Bug 2701.
    boolean  exists   = true;
    boolean  opened   = false;
    boolean  ramSafe  = true;  // File is small enough to copy to RAM and not detached (since 3.1.2 - Bug 2168/2585/2553)

    fileName = (listFile == null)? null: listFile.getName();
    tryOpen  = false; // Reset to false

    if (listFile instanceof BaseFile) {
      exists = ((BaseFile)listFile).exists();

      if (exists) {
        // open the file so we can check the size and if it is detached
        listFile.open(BaseFile.INPUT);

        ramSafe = ((BaseFile) listFile).io.getLength() <= maxRamFileSize;

        // Do not issue INFO if user has set to 0
        if (!ramSafe && maxRamFileSize>0) {
          M.info("File name=["+listFile.getName()+"] size=["+((BaseFile)listFile).io.getLength()+
              "] exceeds /MaxRAMFileSize=" +maxRamFileSize+". Accessing file directly.");
        }

        if (listFile instanceof DataFile) {
          if (((DataFile)listFile).getDetached()!=0) {
            ramSafe = false;   // do not attempt to copy detached file into RAM (since 3.1.2 - Bug 2553)
          }
        }
      }

    }

    if (!exists) {
      if (warn) { M.warning("LIST2: Could not find file "+fileName); }
      getList().setListFile(null,true); // Clear out any old file data.
      tryOpen = true;
    } else {
      // NTN: if ramfile is same as file to open don't create mirror ramfile as they are the same
      if (ramFileName==null || !ramFileName.equals(fileName)) {
        deleteRamFile();
        if (((listFile == null) || (useRamFile && !readOnly)) && ramSafe ) {
          listFile = createRamFile(listFile);
        }
      }
      getList().setListFile(listFile, readOnly);
      opened = true;

    }
    return opened;
  }

  /** Set the Max allowable size of a RAM file (in bytes)
      @param size Max allowable size of a RAM file (in bytes)
      @since NeXtMidas 3.1.2
   */
  public void setRamFileMaxSize (int size) {
    maxRamFileSize = size;   // Added for Bug 2585, 2168
  }

  /** Get the Max allowable size of a RAM file (in bytes)
      @return Max allowable size of a RAM file (in bytes)
      @since NeXtMidas 3.1.2
  */
  public int getRamFileMaxSize () {
    return maxRamFileSize;   // Added for Bug 2585, 2168
  }

  /** Set whether or not to copy the file being listed to RAM prior to use.<br><br>
      Note: This will not reset the current list, but will apply to future files being
      listed.
      @param val true if should copy the file being listed to RAM prior to use
      @since NeXtMidas 3.1.2
   */
  public void setUseRamFile (boolean val) { useRamFile = val; }   // Added for Bug 2585, 2168

  /** Get whether or not LIST2 will copy the file being listed into RAM
      @return true if LIST2 will copy the file being listed into RAM
      @since NeXtMidas 3.1.2
   */
  public boolean getUseRamFile () { return useRamFile; }          // Added for Bug 2585, 2168

  /** Converts a BaseFile to a ListFile. */
  private ListFile lmbButton(Object file) {
    ListFile listFile = null;

    if (file == null) {
      listFile = null;
    } else if (file instanceof ListFile) {
      listFile = (ListFile)file;
    } else if (file instanceof BaseFile) {
      throw new MidasException("LIST2 only supports ListFiles, given "+file);
    } else if ((file instanceof String) && (file.toString().trim().length() == 0)) {
      listFile = null;
    } else {
      FileName fileName = FileName.toFileName(file);
      listFile = lmbButton(BaseFile.getInstanceFor(M, fileName));
    }

    return listFile;
  }

  /** Closes the current file. Since LIST2 requires a file to be open at all times this simply
      closes the current file and opens a null one.
   */
  private void closeFile() {
    openFile((ListFile)null);
  }

  /** Closes the current file. Since LIST2 requires a file to be open at all times this simply
      closes the current file and opens a null one.
   */
  private void deleteRamFile() {
    if (ramFileName != null) {
      BaseFile bf = new BaseFile(M, ramFileName);   // create instance just to erase ramFileName
      bf.erase(true);                               // changed for BUG 1529.
      ramFileName = null;
    }
  }

  /** Gets a thread-safe handle to the {@link ListInterface} being used as the graphical display.
      @return the list interface bsing used as the graphical display
      @since NeXtMidas 2.5.0
   */
  public ListInterface getList() {
    return listIF;
  }

  /** @return instance of MJList that is not thread-safe
      @deprecated Since NeXtMidas 2.5.0: This method returns an instance of {@link MJList} that is
                  not thread-safe. Use {@link #getList()} to get a thread-safe instance.
   */
  @Deprecated
  public MJList getTabularDisplay() {
    M.deprecate("LIST2: Since NeXtMidas 2.5.0: Use getTabularDisplay() is deprecated since it "
              + "returns a handle to MJList that is NOT thread-safe. Use getList() to get a "
              + "thread-safe instance.");
    return mjlist;
  }

  /** Gets the underlying {@link LegacyInterface} (to support LIST messages) that manages the table.
      @return the underlying legacy interface
      @deprecated Since NextMidas 1.9.5: Use {@link #getList()}.
   */
  @Deprecated
  public Object getGridSheet() {
    if (gsInterface == null) {
      M.warning("LIST2: Use of LIST2.GS.<NAME> is not supported, use LIST2.<NAME>.");
    }
    return gsInterface;
  }

  /** Creates a unique name to use in ram.
      @return The URL.
   */
  private FileName getRamFileName() {
    if (ramFileName == null) {
      ramFileName = new FileName(M.pipes.getUniqueKey("list2_temp_00")+"{AUX=RAM}");
    }
    return ramFileName;
  }

  /** Creates a simple DataFile in ram that is a copy of the given file.
      @param file The file to copy into ram.
      @return A handle to the file.
   */
  private ListFile createRamFile(ListFile file) {
    deleteRamFile(); // Remove any pre-existing RAM file.

    FileName fileName   = getRamFileName();                  // Name of new RamFile
    String   fileGroup  = null;                              // HACK: FileGroup to propagate
    Table    quals      = null;                              // Qualifiers to propagate
    Table    ramQuals   = fileName.getQualifierTable();      // Qualifiers for ram file
    FileName name       = null;                              // File name with qualifiers (if avail)

    if (file instanceof BaseFile) {
      quals = ((BaseFile)file).getQualifiers();
      name  = ((BaseFile)file).getName();
    } else {
      name = (file==null)? null: file.getName();
    }

    // JC4: This next section is a hack work-around to account for files in RAM having no FileGroup
    if (file != null) {
      Table    handlers  = Registry.handlers.getTable("FILE");
      String   className = file.getClass().getName();
      String[] exts      = handlers.getKeys();

      for (int i = 0; (i < exts.length) && (fileGroup == null); i++) {
        String clazz = handlers.getS(exts[i]);
        if (clazz.equals(className)) { fileGroup = exts[i]; }
      }
    }

    if ((file != null) && (name != null)) {
      // Copy file to ram (do a full byte-for-byte copy)
      FileUtil.copyBaseFile(M, name, fileName);
    } else {
      // Creates an empty file in ram
      BaseFile ramFile = new BaseFile(M, fileName);
      ramFile.open(DataFile.OUTPUT);
      ramFile.close();
    }

    if (quals != null) {
      ramQuals.merge(quals);
    }
    ramQuals.put("AUX","RAM");    // Force AUX to be RAM
    ramQuals.put("FG",fileGroup); // HACK: Add file group

    if (file==null) return null;  // prevent stack trace if file not set yet - Bug 2639
    return (ListFile)BaseFile.getInstanceFor(M, fileName);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implement Chainable
  //////////////////////////////////////////////////////////////////////////////////////////////////
  public Object getPrevLink() { return null; }
  public Object getNextLink() {
    if (gsInterface != null) {
      return gsInterface;
    } else if (unsafe) {
      return mjlist; // <-- This is not thread-safe (warning issued in open() method)
    } else {
      return getList();
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implement MessageHandler
  //////////////////////////////////////////////////////////////////////////////////////////////////
  public int processMessage(Message msg) {
//if (!msg.name.equals("POINTER")) System.out.println("msg --> name='"+msg.name+"' info="+msg.info+" data="+msg.data);
    int status = NORMAL;

    // translate hot-key equivalents
    if (msg.name.equals("KEYPRESS")) {
      String text = (String)msg.data;
      if (text.equalsIgnoreCase("Enter")) {
        if ((exit&EXIT_RETURN)!=0) done=true;
        return NOOP;
      }
    }

    if (msg.name.equals("WINDOW")) {
           if (msg.data.equals("CLOSING")) { if ((exit&EXIT_WINDOW)!=0) done = true; }
      else if (msg.data.equals("TOGGLE" )) { MW.pop(-1);  }
      else if (msg.data.equals("PUSH"   )) { MW.pop(0);   }
      else if (msg.data.equals("POP"    )) { MW.pop(1);   }

    } else if (msg.name.equals("EXIT")) {
      if ((exit&EXIT_MESSAGE)!=0) done = true;

    } else if (msg.name.equals("OPENFILE")) {
      // check and support for empty data field added in 3.3.1 for Bug 2701
      if (msg.data != null && !msg.data.equals("")) {
        fileName = new FileName((String)msg.data);
      } else {
        M.info("LIST2: Got OPENFILE Message with no FileName. Re-opening current file: "+fileName);
      }

      //openFile (fileName);  changed to line below in 3.3.0 for Bug 2639
      openFileOnEDT();

    } else if (msg.name.equals("OPENTEMPLATE") ) {

       // Since only one file can be open at a time, the open will make
       // sure previous file is closed
       //if (hcb.isOpen) hcb.close();
        //openFile(fileName);  changed to line below in 3.3.0 for Bug 2639
        openFileOnEDT();
        getList().setTemplate(Convert.o2t(msg.data,this));  // allow use of .tbl files

//  } else if (msg.name.equals("POP") && msg.data.equals("CLOSING")) { // ORIG prior to 3.3.0 (NPE when doing ctrl-P)
    } else if (msg.name.equals("POP")) {
      if ("CLOSING".equals(msg.data)) { // fixes NPE for BUG 2691
        MW.pop(0); // pop out
      } else {
        MW.pop(msg.info); // matches's MWindow.processMessage() which normally has info=-1 to toggle for BUG 2691
      }
      // TODO: The above ~5 lines of code should move to a common place in GPrimitive (or MWindow)
      //       It also matches code in MWindow.processMessage(Message)
      //       MAYBE should should call super.processMessage(..) later on instead of processing the "POP" message here?

      mjlist.setSize(MW.pos.w, MW.pos.h);

    } else if (msg.name.equals("BUTTON")) {
      switch (msg.info) {
        case 1:  doButtonAction("LMB", lmbButton); break;
        case 2:  doButtonAction("MMB", mmbButton); break;
        case 3:  doButtonAction("RMB", rmbButton); break;
        default: sendMouseMessage(msg, ONCLICK);  break;
      }
    } else if (msg.name.equals("SELECT") || msg.name.equals("SELECTROW")
                                         || msg.name.equals("DESELECT")) {
//System.out.println("LIST2 got msg SELECT. AutoSelect= " +autoSelect+ " from=" +msg.from );
          sendMessage(msg);

    } else if (msg.name.equals("POINTER")) {
      sendMouseMessage(msg, ONMOTION);

    } else if (msg.name.equals("MENU")) {
      processMenuMessage(msg);

    } else if (msg.name.equals("REFRESH")) {
      refresh();

    } else if (msg.name.equals("DROPDATA")) {  // since 3.3.0 (Bug 2120)
      sendMessage(msg);  // just send back to Macro to handle

    } else if (gsInterface != null) { // THIS else if MUST COME LAST!!!
      status = gsInterface.processMessage(msg);

    } else {
      //M.warning("LIST2: Unhandled msg --> "+msg.name+"="+msg.data+" (info="+msg.info+")");
    }

    return status;
  }

  private void openFileOnEDT () {
    try {
      SwingUtilities.invokeAndWait(new Runnable() {
        public void run() {
          openFile (fileName);
        }
      });
    } catch (Exception e) {
      Shell.printStackTrace(e);
    }
  }

  /** Performs the proper click action. */
  private void doButtonAction(String name, int action) {
    switch (action) {
      case BTN_NONE:
        // do nothing
        break;
      case BTN_MENU:
        mjlist.setMenuVisible(!mjlist.isMenuVisible());
        break;
      case BTN_MESSAGE:
        Point pos = new Point(MW.px, MW.py);
        int   rap = mjlist.rowAtPoint(pos);
        int   cap = mjlist.columnAtPoint(pos);
        long  row = (rap < 0)? rap : mjlist.getMacroRowIndex(mjlist.getRealRowIndex(mjlist.getModelRowIndex(rap)));
        long  col = (cap < 0)? cap : mjlist.getMacroRowIndex(mjlist.getRealColIndex(mjlist.getModelColIndex(cap)));
        Table tbl = new Table();
        tbl.put("ROW", row);
        tbl.put("COL", col);
        sendMessage(new Message(name, 0, tbl));
        break;
      case BTN_CLEAR:
        if (autoSelect) mjlist.getTableSelectionModel().clearSelection(true);
        break;
      default:
        throw new MidasException("Illegal buttonAction="+action);
    }
  }

  /** Warns about an unhandled message. */
  private void unhandledMessage(Message msg) {
    M.warning("LIST2: Unhandled message name="+msg.getName()+" info="+msg.getInfo()
             +" data="+msg.getData()+" from="+msg.getFrom());
  }

  /** Processes a menu message. */
  private void processMenuMessage(Message msg) {
    final int PLAIN_MSG = JOptionPane.PLAIN_MESSAGE;
    final int WARN_MSG  = JOptionPane.WARNING_MESSAGE;

    String act = msg.getData().toString();
    Component parent = mjlist.getParent();

    if (act.startsWith("FILE.")) {
      if (act.equals("FILE.LOAD")) {
        String fname = MJPopup.showFile(parent,getTitle()+": Load File",PLAIN_MSG,
                                        "Load file...",getFileName());
        if (fname != null) {
          openFile(fname);
          getList().setTemplate(null);
        }
      } else if (act.equals("FILE.REREAD")) {
        openFile(fileName);
      } else if (act.equals("FILE.SAVE_COPY")) {
        String fname = MJPopup.showFile(parent,getTitle()+": Save File",PLAIN_MSG,
                                        "Save file copy as...","myfile.csv");
        if (fname != null) {
          FileName srcFile    = (getTempFile() == null)? getFileName(): getTempFile();
          FileName destFile   = FileName.toFileName(fname);
          String[] allCols    = getList().getColumnNames();
          String   allColsStr = StringUtil.join(allCols,",");

          Object[] cols = MJPopup.showMaskArray(parent,getTitle()+": Save File",PLAIN_MSG,
                                                "Include Columns (click 'Cancel' to use file quals)",
                                                allCols,allColsStr);

          ListFile listfile = getList().getListFile();
          listfile.update(); // since 3.1.1: flush headers to disk to fix BUG 1992

          FileUtil.copyListFile(M, srcFile, destFile, cols);
        }
      } else if (act.equals("FILE.EXIT")) {
        if ((exit&EXIT_MENU)!=0) done = true;
      } else {
        unhandledMessage(msg);
      }
    } else if (act.startsWith("EDIT.")) {
      if (act.equals("EDIT.COPY")) {
        Table  defs = getList().getListFile().getRecordDefs();
        Table  data = getList().getAllSelectedRows();
        String csv  = CsvFile.tableToCsv(defs,data);
        GraphicsUtil.toSystemClipboard(csv);
        GraphicsUtil.toSystemSelection(csv);
      } else if (act.equals("EDIT.PASTE")) {
        String name = "Paste Values";
        String text = "Paste values into row";
        Long   line = getLineNumber(name,text,null);
        if (line != null) {
          Table[] rows = getClipRows(name);
          if (rows != null) {
            for (int i = 0; i < rows.length; i++) {
              getList().setRow(line.longValue()+i,rows[i]);
            }
          }
        }
      } else if (act.equals("EDIT.INSERT_VAL")) {
        String name = "Insert Values";
        String text = "Insert row with values at (-1 to add to end)";
        Long   line = getLineNumber(name,text,null);
        if (line != null) {
          Table[] rows = getClipRows(name);
          if (rows != null) {
            getList().addRows(line.longValue(),rows);
          }
        }
      } else if (act.equals("EDIT.INSERT_ROW")) {
        String name = "Insert Row";
        String text = "Insert blank row at (-1 to add to end)";
        Long   line = getLineNumber(name,text,null);

        if (line != null) {
          getList().addRow(line.longValue(),new Table());
        }
      } else if (act.equals("EDIT.DELETE")) {
        Long line = getLineNumber("Delete Row", "Delete row at", null);
        if (line != null) getList().removeRow(line.longValue());
      } else if (act.equals("EDIT.PREF")) {
        M.warning("LIST2: processMenuMessage: "+act+" not implemented yet");
      } else {
        unhandledMessage(msg);
      }
    } else if (act.startsWith("TEMP.")) {
      if (act.equals("TEMP.EDIT")) {
        MJListConfig config = new MJListConfig(M,this,mjlist);
        // BUG 1494 - Fix for graphical template editing.  Button does nothing.
        String [] choices = {"DISMISS"};
        MJPopup popup = MJPopup.createButtons(parent,getTitle()+": Edit Template",PLAIN_MSG,config,choices);
        popup.display(MW,0,MJPopup.POSITION_CENTER);
      } else if (act.equals("TEMP.COPY")) {
        String templ = getList().getTemplate().toString();
        GraphicsUtil.toSystemClipboard(templ);
        GraphicsUtil.toSystemSelection(templ);
      } else if (act.equals("TEMP.DEF")) {
        getList().setTemplate(null);
      } else if (act.equals("TEMP.PASTE")) {
        String clip = GraphicsUtil.fromSystemClipboard();
        if (clip != null) {
          getList().setTemplate(Convert.o2t(clip));
        }
      } else if (act.equals("TEMP.LOAD")) {
        String fname = MJPopup.showFile(parent,getTitle()+": Load Template",PLAIN_MSG,
                                        "Load template...","mytemplate.tbl","*.tbl",null,null);
        if (fname != null) {
          getList().setTemplate(Convert.o2t(fname,M));
        }
      } else if (act.equals("TEMP.SAVE_COPY")) {
        String fname = MJPopup.showFile(parent,getTitle()+": Save Template",PLAIN_MSG,
                                        "Save template as...","mytemplate.tbl","*.tbl",null,null);
        if (fname != null) {
          Table templ = getList().getTemplate();
          FileName fn = FileName.toFileName(fname);
          TextFile tf = new TextFile(M,fn);
          templ.toTextFile(tf);
        }
      } else {
        M.warning("Unknown menu action "+act);
      }
    } else if (act.startsWith("NAVI.")) {
      if (act.equals("NAVI.GOTO")) {
        Long line = getLineNumber("Go To Row", "Go To Row", Long.toString(getList().getRowOffset()));
        if (line != null) getList().showCell(line.longValue(),0);
      } else {
        unhandledMessage(msg);
      }
    } else if (act.startsWith("HELP.")) {
      if (act.equals("HELP.ABOUT")) {
        MJPopup.showOk(parent,getTitle()+": About",PLAIN_MSG,
                       "LIST2\nNeXtMidas "+Installation.NM_VERSION);
      } else if (act.equals("HELP.KEYS")) {
        M.warning("LIST2: processMenuMessage: "+act+" not implemented yet");
      } else {
        unhandledMessage(msg);
      }
    } else {
      unhandledMessage(msg);
    }
  }

  private Table[] getClipRows(String name) {
    String  clip = GraphicsUtil.fromSystemClipboard();
    Table   defs = getList().getListFile().getRecordDefs();
    Table[] rows = CsvFile.csvToTables(defs,clip,1);

    if ((rows == null) || (rows.length == 0)) {
      M.warning("list2.getClipRows warning: "+getTitle()+": No CSV data available on clipboard.");
      MJPopup.showOk(mjlist.getParent(),getTitle()+": "+name,JOptionPane.WARNING_MESSAGE,
                     "No CSV data available on clipboard.");
      rows = null;
    } else if (rows.length != 1) {
      String status = MJPopup.showYesNo(mjlist.getParent(),getTitle()+": "+name,
                                        JOptionPane.WARNING_MESSAGE,
                                        ""+rows.length+" rows found on clipboard, continue?");
      M.warning("list2.getClipRows rows > 1 warning:"+ getTitle()+": "+rows.length+"  rows found on clipboard, continue? "+status);
      if ((status == null) || StringUtil.isFalse(status)) {
        rows = null;
      }
    }
    return rows;
  }

  /** Graphically prompt for a line number. */
  private Long getLineNumber(String title, String msg, String def) {
    if (def == null) {
      long row = mjlist.getTableSelectionModel().getFirstRowSelected();
      if (row < 0) {
        def = "";
      } else {
        def = Long.toString(mjlist.getMacroRowIndex(row));
      }
    }

    String val = MJPopup.showText(mjlist.getParent(), getTitle()+": "+title,
                                  JOptionPane.PLAIN_MESSAGE, msg, def);
    Long line = null;

    if ((val != null) && (val.length() > 0)) {
      if (StringUtil.isInteger(val)) {
        line = Long.valueOf(Convert.s2xl(val));
      } else {
        MJPopup.showOk(mjlist.getParent(), getTitle()+": "+title, JOptionPane.WARNING_MESSAGE,
                       "ERROR: '"+val+"' is not a valid line number.");
      }
    }
    return line;
  }

  /** Gets the title to display. */
  private String getTitle() {
    // since 3.1.0 prepend wtitle (if specified) instead of ID for BUG 2496
    return ((wtitle!=null)? wtitle : getID()) + ": " + fileName;
  }

  /** Add a row of data from a given Table. This method is basically a pass-through method
      which calls {@link MJList#addRow(Table)} but with the added support of creating a
      new Table definition if one does not exist, as detailed below.<br>
      Note: If there is no current file (i.e. the user called list2 with no data or template),
      then this method will create a new Template based on the given table. If there is
      a current template, the it will be updated to include the new data columns.
      Finally, {@link #open()} will be called with the new or updated template. <br>
      @param data The data for the row
      @since NeXtMidas 2.9.2
   */
  public void addRow (Table data) {
  //System.out.println("List2 in addRow mjlist null?="+(mjlist==null)+" lf null?="+(mjlist.getListFile()==null)+" recdefCt="+mjlist.getListFile().getRecordDefCount());
    if (mjlist!=null && mjlist.getListFile()!=null &&
        mjlist.getListFile().getRecordDefCount() == 0 ) {      // undefined file
      //System.out.println("List2 in addRow mjlist null?="+(mjlist==null)+" lf null?="+(mjlist.getListFile()==null));
      Table template = MA.getTable("TEMPLATE");
      Table values   = new Table();
      String type = "STRING";
      if (template == null) template = new Table();
      for (String key : data.getKeys()) {
        Object val = data.get(key);
        if      (val instanceof Number)  type = "NUMBER";
        else if (val instanceof Date)    type = "DATE";
        else if (val instanceof Enum)    type = "ENUM";
        else if (val instanceof Boolean) type = "TOGGLE";
        else type = "STRING";  // default to String
        values.setKey("TYPE", type);
        template.setKey(key, values);
      }
      this.template = template;
      BaseFile bf = setFileFromTemplate(template);
      ListFile lf = lmbButton(bf);
      mjlist.setTemplate(template);
      mjlist.setListFile(lf);
    }
    getList().addRow(data);
  }

  /** Pass-through method for {@link MJList#getTableSelectionModel()}. As such, it requires
      the use of MJList
      @return the current selection model
      @since NeXtMidas 3.1.0
   */
  public nxm.sys.libg.LargeJTable.LargeTableSelectionModel getTableSelectionModel () {
    // Added for Bug 2428
    if (mjlist != null) return mjlist.getTableSelectionModel();
    else return null;
  }

  /** Show the configuration menu for the list. (Note: the name of this method missors that used
      by the layers in PLOT).
      @since NeXtMidas 2.5.0
   */
  public void configure() {
    mjlist.setMenuVisible(true);
  }

  /** Sends a message (usually to the macro). This handles special cases such as no message handler
      being set and prevents sending the message back to the point of origin.
      @param msg The message.
   */
  private void sendMouseMessage(Message msg, int type) {
    if ((controls & type) != 0) {
      switch(type) {
        case ONCLICK:  msg.name = "MARK"; msg.data = null; break;
        case ONMOTION:                    msg.data = null; break;
      }
      sendMessage(msg);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implement MessageHandler
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** This is the legacy interface that enables LIST2 to be a drop-in replacement for LIST. This
      interface is deprecated and will be removed in future releases.
   */
  public final class LegacyInterface implements Chainable, MessageHandler {
    public Object getNextLink() {
      if (unsafe) {
        return mjlist; // <-- This is not thread-safe (warning issued in open() method)
      } else {
        return getList();
      }
    }

    public Object getPrevLink() {
      return null;
    }

    private int exit     = -1;
    private int settings =  0;
    private int outtype  =  list.T_TABLE;
    private int hilite   =  1;
    private int curmode  =  0;
    private int poll     =  0;
    private int multiSelMsgMode = GridSheet.MULTISEL_OFF;
    private MessageHandler msgid;

    /** Prevent outside instantiation. */
    private LegacyInterface() {
      if (M.pipeMode==Midas.PINIT) exit=0; else exit=-1;
      exit = MA.getOptionMask("/EXIT",list.exitList,exit);
    }

    public int processMessage (Message msg) {
 //System.out.println("List2 leg got msg="+msg+" data="+msg.data);
      int status = NORMAL;
      if (msg.name.equals("KEYPRESS")) {
        String text = (String)msg.data;
        if (text.equalsIgnoreCase("M")) msg.name = "MENU";
        if (text.equalsIgnoreCase("Enter") && (exit&list.EXIT_RETURN)!=0) msg.name = "EXIT";
      }
      if (msg.name.equals("BUTTON") && msg.info==2) {
        msg.name = "MENU";
      }

      if (msg.name.equals("BUTTON") && msg.info==3) {
        mjlist.getTableSelectionModel().clearSelection(true);
      }

      if (msg.name.equals("MENU")) {
        new GMenu (MW,"List","Files...,Template...,Settings...,"+
                  "Control...,Window...,Options...,Other...,Exit",0,0,this);
      } else if (msg.name.equals("DROPDATA")) {  // since 3.3.0 (Bug 2120)
        sendMessage(msg);   // just send back to macro to handle
      }
      else if (msg.name.equals("LIST")) {
        String text = (String)msg.data;
        if (text.equals("FILES...")) {
          new GMenu (MW,"Files","Open,Close,Reread",0,0,this);
        }
        else if (text.equals("TEMPLATE...")) {
          new GMenu (MW,"Template","Open,Default",0,0,this);
        }
        else if (text.equals("CONTROL...")) {
          new GMenu (MW,"Controls","Off,Click,Continuous,Assign...",controls+1,0,this);
        }
        else if (text.equals("WINDOW...")) {
          new GMenu (MW,"Window","Toggle,Push,Pop",0,0,this);
        }
        else if (text.equals("OTHER...")) {
          new GMenu (MW,"Other","Theme",0,0,this);
        }
        else if (text.equals("OPTIONS...")) {
          new GMenu (MW,"Options","Add Column,Cursor Granularity...,Highlight...,OutputType...",0,0,this);
        }
        else if (text.equals("SETTINGS...")) {
          new GMenu (MW,"Settings","Grid,Column Labels,Row Labels",settings,GMenu.TOGGLE,this);
        }
        else if (text.equals("EXIT")) {
          msg.name = "EXIT";
          if ((exit&list.EXIT_MENU)!=0) MQ.put(msg);
        }
      }
      else if (msg.name.equals("FILES")) {
        String text = (String)msg.data;
        if (text.equals("OPEN")) {
          new GPrompt (MW,"OpenFile","",0,this);
        }
        else if (text.equals("CLOSE")) {
          closeFile();
        }
        else if (text.equals("REREAD")) {
          M.warning("LIST2: REREAD not supported.");
        }
      }
      else if (msg.name.equals("TEMPLATE")) {
        String text = (String)msg.data;
        if (text.equals("OPEN")) {
          new GPrompt (MW,"OPENTEMPLATE","",0,this);
        }
        else if (text.equals("DEFAULT")) {
          M.warning("LIST2: Can not open old style template.");
        }
      }
      else if (msg.name.equals("SETTINGS")) {
        M.warning("LIST2: Can not set 'SETTINGS' at this time.");
      }
      else if (msg.name.equals("SETROW")) {
        setOutputType(outtype);
        getList().setRow(msg.info, Convert.o2t(msg.data));
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("GETROW")) {
        msg.data = getRow(msg.info);
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("GETROWVECTOR")) {
        msg.data = getRowVector(msg.info);
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("GETROWTABLE")) {
        msg.data = getRowTable(msg.info);
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("GETCOL")) {
        msg.data = getColumn(msg.info);
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("GETCOLUMNVECTOR")) {
        msg.data = getColumnVector(msg.info);
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("GETCOLUMNTABLE")) {
        msg.data = getColumnTable(msg.info);
        MQ.put(msg,msg.from,this);
      }
      else if (msg.name.equals("CONTROLS")) {
        if (msg.info == 4);
        else controls = msg.info-1;
      }
      else if (msg.name.equals("WINDOW")) {
        M.warning("LIST2: PUSH/POP messages should be processed by normal LIST2 methods.");
      }
      else if (msg.name.equals("OTHER")) {
        String text = (String)msg.data;
        if (text.equals("THEME")) {
          new GMenu (MW,"THEME",MWindow.themeList,0,0,this);
        }
      }
      else if (msg.name.equals("OPTIONS")) {
        String text = (String)msg.data;
        if (text.equals("ADD COLUMN"))
          new GPrompt (MW,"AddColumn","",0,this);
        else if (text.equals("CURSOR GRANULARITY..."))
          new GMenu (MW,"Cursor","Cell,Row,Column",curmode,0,this);
        else if (text.equals("HIGHLIGHT..."))
          new GMenu (MW,"HIGHLIGHT","Background,Foreground,Color",hilite,0,this);
        else if (text.equals("OUTPUTTYPE..."))
          new GMenu (MW,"OUTPUTTYPE","Auto,Vector,Table",outtype,0,this);
      }

      /* if initializing or this Thread is not me - queue for me */
      else if (MW==null || MW.status==0 || !thisIsMe()) {
        if (msg.name.equals("POINTER")) MQ.remove(msg.name);
        if (msg.name.equals("REFRESH") || msg.name.equals("RESIZE")
                  || msg.name.startsWith("PAN")) {
          MQ.remove(msg.name);
        }
        MQ.put(msg);
        return (NOOP);
      }

      else if (msg.name.equals("SELECT")) {
        //M.info("Sending select "+msg.data);
        sendMessage (msg);
      }
      else if (msg.name.equals("SELECTROW")) {
        M.warning("LIST2: Can not select row at this time.");
//        getList().selectRow(Convert.o2l(msg.data));
//        sendMessage(msg);
      }
      else if (msg.name.equals("DESELECT")) {
        //M.info("Sending deselect "+msg.data);
        sendMessage (msg);
      }
      /* the rest draw so main thread gets them from the queue */
      else if (msg.name.equals("POINTER")) {
        if (controls==list.ONMOTION) {
          msg.data = null;
          sendMessage (msg);
        }
      }
      else if (msg.name.equals("BUTTON") && msg.info == -1) {
        if (controls==list.ONCLICK) {
          msg.name = "MARK";
          msg.data = null;
          sendMessage (msg);
        }
      }
      else if (msg.name.equals("REFRESH")) {
        MW.refresh();
      }
      else if (msg.name.equals("RESIZE")) {
        MW.resize(0);
      }
      else if (msg.name.equals("OPENFILE")) {
//        tabularDisp.setTemplate(null); // Matches functionality in LIST
        openFile((String)msg.data);
      }
      else if (msg.name.equals("OPENTEMPLATE")) {
        DataFile df = (DataFile)BaseFile.getInstanceFor(M, msg.data.toString());

        getList().setTemplate(convert.convertTemplate2Table(df));
      }
      else if (msg.name.equals("APPLYTEMPLATE")) {
        DataFile df = (DataFile)BaseFile.getInstanceFor(M, msg.data.toString());

        getList().updateTemplate(convert.convertTemplate2Table(df));
      }
      else if (msg.name.equals("CLOSEFILE")) {
        closeFile ();
      }
      else if (msg.name.equals("THEME")) {
        M.warning("LIST2: Theme must be set using LOOKANDFEEL command.");
      }
      else if (msg.name.equals("ADDCOLUMN")) {
M.warning("LIST2: Can not add column at this time.");
//        String text = (String)msg.data;
//        if (text.length() > 0) gs.addColumns(text);
      }
      else if (msg.name.equals("OUTPUTTYPE")) {
        setOutputType(msg.info);
      }
      else if (msg.name.equals("HIGHLIGHT")) {
        if (msg.info == 3) {
          new GMenu (MW,"Color",list.colorChoices,0,0,this);
        } else {
          hilite = msg.info;
          M.warning("LIST2: Must use templates to set color.");
        }
      }
      else if (msg.name.equals("CURSOR")) {
        setCursorMode(msg.info);
      }
      else if (msg.name.equals("COLOR")) {
        M.warning("LIST2: Must use templates to set color.");
      }
      else if (msg.name.equals("EXIT")) {
        M.warning("LIST2: EXIT messages should be processed by normal LIST2 methods.");
        status = FINISH;
      }
      else if (msg.name.equals("POP")) {
        M.warning("LIST2: POP messages should be processed by normal LIST2 methods.");
      }

      return status;
    }

    /** Send Message via MessageHandler
     *  @param msg the Message to be sent
     */
    public void sendMessage (Message msg) {
      if (msgid == msg.from) return;    // don't send it back
      msg.to = msgid; msg.from = this;
      msgid.processMessage(msg);
    }

    /** Get a row from the List
     *  @param index the row index
     *  @param type  the form in which to return the row (i.e. Table, Vector, etc.)
     *  @return the row from the List in the form specified by the type
     */
    public Object getRow(int index, int type) {
      return (type == list.T_TABLE)? (Object)getRowTable(index): (Object)getRowVector(index);
    }

    /** Get a row from the List as a Vector
     *  @param index the row index
     *  @return the row from the List as a Midas Table
     */
    public Vector<Object> getRowVector(int index) {
      Table    tbl  = getRowTable(index);
      Vector<Object>   vect = new Vector<>(); // Data most of the time
      ListFile file = getList().getListFile();

      for (int i = 0; i < file.getRecordDefCount(); i++) {
        vect.add(tbl.get(file.getRecordDef(i).getS("NAME")));
      }

      return vect;
    }

    /** Calls {@link nxm.sys.inc.ListInterface#getRow(long)} to get a row from the List as a Midas Table
     *  @param index the row index
     *  @return the row from the List as a Midas Table
     */
    public Table getRowTable(int index) {
      return getList().getRow((long)index);
    }

    /** Calls {@link #getRow(int, int)} to get a row from the List in the form specified by {@link #outtype}
     *  @param index the row index
     *  @return the row from the List in the form specified by {@link #outtype}
     */
    public Object getRow(int index) {
      return getRow(index, outtype);
    }

    /** Set the output type
     *  @param type the output type
     */
    public void setOutputType(int type) {
      if (type == list.T_ONTYPE) {
        M.warning("LIST2: Setting of OUTPUTTYPE=T_ONTYPE not supported, using T_TABLE");
        type = list.T_TABLE;
      }

      outtype = type;
    }

    /** Get a column from the List
     *  @param col  the index of the column as an int
     *  @param type the form in which to return the column (i.e. Table, Vector, etc.)
     *  @return the column in the form specified by the type
     */
    public Object getColumn(int col, int type) {
      return (type == list.T_TABLE)? getColumnTable(col): getColumnVector(col);
    }

    /** Get a column from the List
     *  @param name  the name of the column as a String
     *  @param type the form in which to return the column (i.e. Table, Vector, etc.)
     *  @return the column in the form specified by the type
     */
    public Object getColumn(String name, int type) {
      return (type == list.T_TABLE)? getColumnTable(name): getColumnVector(name);
    }

    /** Calls {@link #getColumn(int, int)} to get a column from the List in the form specified by {@link #outtype}
     *  @param colIdx the index of the column as an int
     *  @return the column from the List in the form specified by {@link #outtype}
     */
    public Object getColumn(int colIdx) {
      return getColumn(colIdx, outtype);
    }

    /** Calls {@link #getColumn(String, int)} to get a column from the List in the form specified by {@link #outtype}
     *  @param subrName the name of the column as a String
     *  @return the column from the List in the form specified by {@link #outtype}
     */
    public Object getColumn(String subrName) {
      return getColumn(subrName, outtype);
    }

    /** Calls {@link #getColumnTable(String)} to get a column from the List as a Midas Table
     *  @param colIdx the index of the column as an int
     *  @return the column from the List as a Midas Table
     */
    public Object getColumnTable(int colIdx) {
      String name = getList().getListFile().getRecordDef(colIdx-1).getS("NAME");
      return getColumnTable(name);
    }

    /** Calls {@link #getColumnVector(String)} to get a column from the List as a Vector
     *  @param colIdx the index of the column as an int
     *  @return the column from the List as a Vector
     */
    public Vector<Object> getColumnVector(int colIdx) {
      String name = getList().getListFile().getRecordDef(colIdx-1).getS("NAME");
      return getColumnVector(name);
    }

    /** Get a column from the List as a Table
     *  @param subrName the name of the column as a String
     *  @return the column from the List as a Table
     */
    public Table getColumnTable(String subrName) {
      Vector<Object> vect = getColumnVector(subrName);
      Table  tbl  = new Table();

      for (int i = 0; i < vect.size(); i++) {
        tbl.setKey(Integer.toString(i), vect.get(i));
      }

      return tbl;
    }
    /** Get a column from the List as a Vector
     *  @param subrName the name of the column as a String
     *  @return the column from the List as a Vector
     */
    public Vector<Object> getColumnVector(String subrName) {
      Vector<Object> vect = new Vector<>();
      ListFile file = getList().getListFile();

      for (double offset = 0; offset < file.getNumberOfRows(); offset++) {
        vect.add(file.getDataTable(offset).getKey(subrName));
      }
      return vect;
    }

    /** Set cursor mode
     *  @param value the cursor mode
     */
    public void setCursorMode(int value) {
      curmode = value;
      mjlist.getTableSelectionModel().clearSelection(true);
    }

    /** Get poll interval
     *  @return the poll interval
     */
    public int getPollInterval () {
      return poll;
    }

    /** Set poll interval
     *  @param pollInterval the poll interval
     */
    public void setPollInterval (int pollInterval) {
      poll = pollInterval;
    }

    /** Calls {@link nxm.sys.inc.ListInterface#getAllSelectedRows()}
     *  to get all the selected rows as a Midas Table
     *  @return all the selected rows as a Midas Table
     */
    public Table getSelRowsTable() {
      return getList().getAllSelectedRows();
    }

    /** Calls {@link #getSelRowsTable()}
     *  to get all the selected rows as a Midas Table
     *  @return all the selected rows as a Midas Table
     */
    public Table getSelectedRowData() {
      return getSelRowsTable();
    }

    /** Clears the whole selection */
    public void deselectAll() {
      mjlist.getTableSelectionModel().clearSelection(true);
    }

    /** Returns whether or not multi-select mode is enabled
     *  @return the state of multi-select mode
     */
    public int getMultiSelMsgMode () {
      return multiSelMsgMode;
    }

    /** Calls {@link #setMultiSelMsgMode(int)} to set the state of multi-select mode
     *  @param ms_mode the state of multi-select mode as a String
     */
    public void setMultiSelMsgMode (String ms_mode) {
      if (ms_mode.equals("FULL")) {
        setMultiSelMsgMode(GridSheet.MULTISEL_FULL);
      } else {
        setMultiSelMsgMode(GridSheet.MULTISEL_OFF);
      }
    }

    /** Set the state of multi-select mode
     *  @param ms_mode the state of multi-select mode as an int
     */
    public void setMultiSelMsgMode (int ms_mode) {
      if (ms_mode == GridSheet.MULTISEL_LIST) {
        M.warning("LIST2: Multi select mode MULTISEL_LIST is TBD. Setting mode OFF.");
        multiSelMsgMode = GridSheet.MULTISEL_OFF;
      } else {
        multiSelMsgMode = ms_mode;
      }
    }

    /** Select a row, maintaining other selected rows
     *  @param row the row to be selected
     */
    public void selectMRow(long row) {
      getList().selectRow(row, false);
    }

    /** Select a row by column value, maintaining other selected rows
     *  @param column     The name of the column
     *  @param value      The value to be matched
     *  @param ignoreCase Ignore case in comparison
     */
    public void selectRowByColValue (String column, String value, boolean ignoreCase) {
      getList().selectRow(column, value, ignoreCase);
    }

    /** Calls {@link #sort(String, boolean)} to sort a column in ascending order
     *  @param subrToSort the name of the column to be sorted
     */
    public void sortA(String subrToSort) {
      sort(subrToSort, false);
    }
    /** Calls {@link #sort(String, boolean)} to sort a column in descending order
     *  @param subrToSort the name of the column to be sorted
     */
    public void sortD(String subrToSort) {
      sort(subrToSort, true);
    }
    /** Calls {@link nxm.sys.inc.ListInterface#setSort(String)} to sort a column
     *  @param subrToSort the name of the column to be sorted
     *  @param sortDesc   whether or not sorting in descending order or ascending order
     */
    public void sort(String subrToSort, boolean sortDesc) {
      if (sortDesc) { subrToSort =  "-" + subrToSort; }
      else          { subrToSort =  "+" + subrToSort; }
      getList().setSort(subrToSort);
    }

    /** Clears the list by calling {@link #clear(boolean)} */
    public void clear() {
      clear(true);
    }

    /** Clears the list by removing all rows from the List
     *  @param refresh state for refreshing, not used ??
     */
    public void clear(boolean refresh) {
      getList().removeAllRows();
    }

    /** Clears the List by calling {@link #clear()}
     *  @param tmp temp parameter, not used ??
     */
    public void clear(int tmp) {
      clear();
    }
  }
}
