/* A program to mimic the windows file browser. It allows:
 *   displaying files and directories
 *   editing file names
 *   moving and copying multiple files and directories
 *   folder (tree view) or file view
 *
 * Copyright (C) 2017-20 Sidney Marshall (swm@cs.rit.edu)
 *
 * This program is free software: you can redistribute it and/or modfy
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 */

/*
 * javac -g -cp myjsch.jar:. FileBrowser.java
 * javac -g -cp myjsch.jar\;. FileBrowser.java
 * jdb -classpath myjsch.jar:. FileBrowser
 * javac -g -cp myjsch.jar:. FileBrowser1.java
 * javac -g -cp myjsch.jar\;. FileBrowser1.java
 * jdb -classpath myjsch.jar:. FileBrowser1
 *
 * cp ~/home/Projects/sftp/jsch-0.1.54/src/main/java/myjsch.jar .
 *
 * java -Djava.net.preferIPv4Stack=true -jar filebrowser.jar -ssh sidney@server
 */

/*    *** TODO ***
 *
 * format date
 * fix undo / redo
 * rename file in place - mostly done - need to stop editing when selecting in file tree
 *    TableCellEditor te = table.getCellEditor();
 *    if(te != null) te.stopCellEditing();
 * selectFromPath should do something reasonable with malformed or missing files
 * get copy links working - linux done; windows impossible
 * fix filenames with . and .. and ~
 * make relative file names on command line work
 * get scripting working (make or explicit)
 * clean out unused methods and variables
 * get .filepart working
 *   x.filepart date only compares to x
 * reopen a closed session if necessary
 * redisplay if suspect directory listing has changed
 *
 * static MyPath stringToMyPath(String s)
 *       make ~ work - hard because no sftp channel
 *       make . work (current directory)
 *       make .. work (parent directory)
 *
 * enable file transfers
 * enable drag and drop from browser to comparison combo box(es)
 *
 * check .reload() calls - change to nodesWereInserted() ???
 */

/*
 * Note: Using File objects will not work with non-UTF8 file
 * names. Always use Path objects. Also, using FileInputStream and
 * FileOutputStream causes GC slowups. Here is a handy table:
 *
 * Path Paths.get(uri);
 *
 * URI path.toUri();
 *
 * Path file.toPath()
 *
 * java.io.File (class)
 * java.nio.file.Path (interface)
 *
 * file = new File("path/to/file.txt")
 * path = Paths.get("path/to/file.txt")
 *
 * file = new File(parentFile, "file.txt")
 * path = parentPath.resolve("file.txt")
 *
 * String file.getName()
 * Path path.getFileName()
 * String path.getFileName().toString()
 *
 * File file.getParentFile()
 * Path path.getParent()
 *
 * boolean file.mkdir()
 * boolean file.mkdirs()
 * Path Files.createDirectory(path)
 * Path Files.createDirectories(path)
 *
 * long file.length()
 * long Files.size(path)
 *
 * boolean file.exists()
 * boolean Files.exists(path)
 *
 * boolean file.delete()
 * void Files.delete(path)
 * boolean Files.deleteIfExists(path)
 *
 * FileOutputStream new FileOutputStream(file)
 * OutputStream Files.newOutputStream(path)
 *
 * FileInputStream new FileInputStream(file)
 * InputStream Files.newInputStream(path)
 *
 * file.listFiles(filter)
 * Stream<Path> Files.list(path)
 * List<Path> Files.list(path).filter(filter).collect(Collectors.toList())
 */

//import com.jcraft.jsch.Channel;
//import com.jcraft.jsch.SftpStatVFS;
//import java.awt.FlowLayout;
//import java.awt.Shape;
//import java.awt.event.FocusEvent;
//import java.awt.event.FocusListener;
//import java.awt.event.WindowListener;
//import java.awt.geom.Rectangle2D;
//import java.awt.im.InputContext;
//import java.io.PipedInputStream;
//import java.io.PipedOutputStream;
//import java.io.Reader;
//import java.io.StringBufferInputStream;
//import java.io.StringReader;
//import java.io.StringWriter;
//import java.nio.file.FileVisitResult;
//import java.nio.file.InvalidPathException;
//import java.nio.file.SimpleFileVisitor;
//import java.nio.file.Watchable;
//import java.nio.file.attribute.BasicFileAttributes;
//import java.time.Instant;
//import javax.swing.Action;
//import javax.swing.BoxLayout;
//import javax.swing.CellEditor;
//import javax.swing.ComboBoxModel;
//import javax.swing.DefaultListModel;
//import javax.swing.JCheckBox;
//import javax.swing.JEditorPane;
//import javax.swing.JToggleButton;
//import javax.swing.ListModel;
//import javax.swing.ProgressMonitor;
//import javax.swing.ScrollPaneConstants;
//import javax.swing.SizeRequirements;
//import javax.swing.WindowConstants;
//import javax.swing.event.CellEditorListener;
//import javax.swing.event.ChangeEvent;
//import javax.swing.event.DocumentEvent;
//import javax.swing.event.ListDataListener;
//import javax.swing.plaf.UIResource;
//import javax.swing.plaf.basic.BasicTransferable;
//import javax.swing.plaf.metal.MetalIconFactory;
//import javax.swing.table.DefaultTableModel;
//import javax.swing.table.TableCellEditor;
//import javax.swing.text.AbstractDocument;
//import javax.swing.text.BoxView;
//import javax.swing.text.Caret;
//import javax.swing.text.CompositeView;
//import javax.swing.text.DefaultEditorKit;
//import javax.swing.text.EditorKit;
//import javax.swing.text.Element;
//import javax.swing.text.GlyphView;
//import javax.swing.text.Position;
//import javax.swing.text.StyledEditorKit;
//import javax.swing.text.View;
//import javax.swing.text.ViewFactory;
//import javax.swing.text.WrappedPlainView;
//import javax.swing.text.html.HTMLEditorKit.HTMLFactory;
//import javax.swing.text.html.HTMLEditorKit;
//import javax.swing.text.html.InlineView;
//import javax.swing.text.html.ParagraphView;
//import javax.swing.text.rtf.RTFEditorKit;
//import javax.swing.tree.DefaultMutableTreeNode;
//import javax.swing.tree.DefaultTreeCellEditor;
//import javax.swing.tree.DefaultTreeCellRenderer;
//import javax.swing.tree.DefaultTreeModel;
//import javax.swing.tree.MutableTreeNode;
//import javax.swing.tree.TreeCellEditor;
//import javax.swing.tree.TreeNode;

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.print.PrinterException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFilePermission;
import java.text.MessageFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ComboBoxEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DropMode;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.Spring;
import javax.swing.SpringLayout;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.TransferHandler;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

/*
 * ////////////////////////////////////////////////
 * Sessions and Channels are not daemon threads and must be closed for
 * the application to Shut down.
 *
 * Closing a session closes all channels associated with it.
 * //////////////////////////////////////////////////
 * 0 OVERWRITE
 * 1 RESUME
 * 2 APPEND
 *
 * 0 SSH_FX_OK
 * 1 SSH_FX_EOF
 * 2 SSH_FX_NO_SUCH_FILE
 * 3 SSH_FX_PERMISSION_DENIED
 * 4 SSH_FX_FAILURE
 * 5 SSH_FX_BAD_MESSAGE
 * 6 SSH_FX_NO_CONNECTION
 * 7 SSH_FX_CONNECTION_LOST
 * 8 SSH_FX_OP_UNSUPPORTED
 */

class FileBrowser {
  final static long serialVersionUID = 42;

  final static Color BLACK = Color.BLACK;
  final static Color LIGHT_GRAY = Color.LIGHT_GRAY;
  final static Color WHITE = Color.WHITE;
  final static Color BROWN = new Color(0xAA4400);
  final static Color RED = Color.RED;
  final static Color PINK = Color.PINK;
  final static Color ORANGE = new Color(0xFF8000); // not currently used
  final static Color YELLOW = Color.YELLOW;
  final static Color LIGHT_YELLOW = new Color(0xffffc0);
  final static Color GREEN = Color.GREEN;
  final static Color CYAN = Color.CYAN;
  final static Color BLUE = Color.BLUE;
  final static Color MAGENTA = Color.MAGENTA;

  static boolean windows = File.pathSeparatorChar == ';';

  static int windowCount = 0; // number of open windows
  static JSch jsch = new JSch();

  static Charset charSet = StandardCharsets.UTF_8;
  static FileSystem fileSystem = FileSystems.getDefault();
  static Toolkit toolkit = Toolkit.getDefaultToolkit();
  // not toolkit.getSystemClipboard()
  static Clipboard clipboard = toolkit.getSystemSelection();
  static String home = System.getProperty("user.home");
  static Path optionPath = fileSystem.getPath(home, ".filebrowserrc");
  static TreeMap<String, ArrayList<String>> options
    = new TreeMap<String, ArrayList<String>>();
  static final String passwordKey = "Passwords";
  static final String selectionsKey = "Selections";

  static Charset fileCharset = Charset.availableCharsets().get("UTF-8");

  /**
   * Removes all % escape sequences from a String. No check is made
   * that the % escape sequences are valid.
   *
   * @param uri the String to be flattened
   * @return the flattened String
   */
  static String flatten(String uri) {
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < uri.length(); ++i) {
      char c = uri.charAt(i);
      if(c == '%') {
        char c1 = uri.charAt(i + 1);
        int i1 = legalHex.indexOf(c1);
        if(i1 < 0) throw new Error("bad hex digit #1: " + c1);
        if(i1 >= 16) i1 -= 6;
        char c2 = uri.charAt(i + 2);
        int i2 = legalHex.indexOf(c2);
        if(i2 < 0) throw new Error("bad hex digit #2: " + c2);
        if(i2 >= 16) i2 -= 6;
        c = (char)(16*i1 + i2);
        i += 2; // skip over hex value following %
      }
      sb.append(c);
    }
    return sb.toString();
  } // static String flatten(String uri)

  /**
   * Convert a byte[] to an escaped String
   *
   * List of non-escaped characters determined empirically by
   * converting to Path and back to URI.
   *
   * @param bytes array of bytes to convert
   * @return the escaped String
   */
  static String bytesToString(byte[] bytes) { ////////////////////////
    StringBuilder sb = new StringBuilder();
    for(byte b : bytes) {
      char c = (char)b;
      if(c >= '0' && c <= '9') sb.append(c);
      else if(c >= 'A' && c <= 'Z') sb.append(c);
      else if(c >= 'a' && c <= 'z') sb.append(c);
      else if("!$&'()*+,-./:;=@_~".indexOf(c) >= 0) sb.append(c);
      else {
        sb.append('%');
        sb.append(hex[(b >> 4) & 0xF]);
        sb.append(hex[b & 0xF]);
      }
    }
    return sb.toString();
  } // static String bytesToString(byte[] bytes)

  /**
   * Convert a String to byte[]
   *
   * @param s the String to convert
   * @return the path with escapes converted from hex
   */
  static byte[] stringToBytes(String s) {
    int count = 0;
    for(int i = 0; i < s.length(); ++i) {
      if(s.charAt(i) == '%') ++count;
    }
    byte[] bytes = new byte[s.length() - 2*count];
    for(int i = 0, j = 0; i < s.length(); ++i, ++j) {
      char c = s.charAt(i);
      if(c == '%') {
        bytes[j] = (byte)Integer.parseInt(s.substring(i + 1, i + 3), 16);
        //System.out.println((int)bytes[j]);
        i += 2;
      }
      //////////////////// removing this causes fail
      else {
        if(c <= ' ' || c > '~') {
          System.err.println("Bad character in URI: " + c);
          new Error("Bad character in URI: " + c).printStackTrace();
        }
        bytes[j] = (byte)c; /////// need something like this
      }
      ///////////////////
    }
    return bytes;
  } // static byte[] stringToBytes(String s)

  final static char[] hex = new char[]{'0','1','2','3','4','5','6','7',
                                       '8','9','A','B','C','D','E','F'};
  final static String legalHex = "0123456789ABCDEFabcdef";

  /**
   * Convert an "escaped" string to a URI. Characters (other than a
   * %xx where xx is a legitimate hex string) are escaped with the
   * appropriate %xx escape sequence. % is assumed to introduce a hex
   * escape and is left untouched if well-formed. Otherwise the % is
   * escaped and processing continues.
   *
   * /////////////// maybe use bytes to convert UTF-8 characters ??????????
   * /////////////// need to convert &gt; 128 chars properly ????????
   *
   * @param s the string to be converted
   * @return the equivalent URI
   */
  static URI toUri(String s) {
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < s.length(); ++i) {
      char c = s.charAt(i);
      percent: if(c == '%') {
        if(i + 2 >= s.length()) break percent;
        if(legalHex.indexOf(s.charAt(i + 1)) < 0) break percent;
        if(legalHex.indexOf(s.charAt(i + 2)) < 0) break percent;
        ////////////// maybe convert to char and let the rest take care of it
        ////////////// what about UTF-8 extended char codes????????
        sb.append(s.substring(i, i + 3).toUpperCase());
        i += 2;
        continue;
      }
      if(c >= '0' && c <= '9') sb.append(c);
      else if(c >= 'A' && c <= 'Z') sb.append(c);
      else if(c >= 'a' && c <= 'z') sb.append(c);
      // String generated by converting characters to Path and then to URI
      else if("!$&'()*+,-./:;=@_~".indexOf(c) >= 0) sb.append(c);
      else {
        sb.append('%');
        sb.append(hex[(c >> 4) & 0xF]);
        sb.append(hex[c & 0xF]);
      }
    }

    try {
      //////////////////// need to escape string
      return new URI(sb.toString());
    } catch(URISyntaxException e) {
      e.printStackTrace();
      return root.uri;
    }
  } // static URI toUri(String s)

  static String rootName = "file system root(s)";
  static RootPath root = new RootPath();

  static ArrayList<RemotePath> remoteRoots = new ArrayList<RemotePath>();

  // map from user@host to Session for all sessions
  static TreeMap<String, Session> sessions = new TreeMap<String, Session>();

  // map from user@host to Stack<ChannelSftp>
  static TreeMap<String, Stack<ChannelSftp>> channels
    = new TreeMap<String, Stack<ChannelSftp>>();

  static DataFlavor remotePathArrayFlavor
    = new DataFlavor(ArrayList.class, "remotePathListFlavor");

  //Set the location of the known hosts file
  static {
    try {
      jsch.setKnownHosts("~/.ssh/known_hosts");
    } catch(JSchException e) {
      e.printStackTrace();
    }
  }

  /**
   * Open a session on the given user@host. If a session is cached for
   * this user@host, it is returned. Otherwise, if the password is
   * cached then it is used for an attempted open. If successful, the
   * opened session is cached and returned. Otherwise, a dialog is
   * opened to get the password for the open. If successful then the
   * opened session is cached and returned. If the open fails a
   * SftpOpenFailedException is thrown.
   *
   * This method is synchronized on the interned user@host.
   *
   * @param userHost the user@host string specifying where to open the session
   * @return the opened session
   * @throws SftpOpenFailedException if the open fails
   */
  static Session getSession(String userHost) throws SftpOpenFailedException {
    userHost = userHost.intern(); // to make unique for following synchronized
    synchronized(userHost) {
      try {
        Session session = sessions.get(userHost);
        if(session != null && session.isConnected()) {
          return session;
        }
        int index = userHost.indexOf('@');
        String user = userHost.substring(0, index);
        String host = userHost.substring(index + 1);
        int port = 22;
        session = jsch.getSession(user, host, port);
        // hash user/host in ~/.ssh/known_hosts file
        session.setConfig("HashKnownHosts",  "yes");
        ArrayList<String> passwords = options.get(passwordKey);
        if(passwords != null) {
          for(String entry : passwords) {
            int colon = entry.indexOf(":");
            String userHost1 = entry.substring(0, colon);
            if(colon >= 0 && userHost1.equals(userHost)) {
              session.setPassword(decrypt(userHost, entry.substring(colon+1)));
              break;
            }
          }
        }
        MyUserInfo myUserInfo = new MyUserInfo();
        session.setUserInfo(myUserInfo);
        session.connect(20*1000);
        sessions.put(userHost, session);
        String password = myUserInfo.getPassword();
        if(password != null) {
          passwords = options.get(passwordKey);
          if(passwords == null) passwords = new ArrayList<String>();
          for(String entry : passwords) {
            int colon = entry.indexOf(":");
            String userHost1 = entry.substring(0, colon);
            if(colon < 0 || userHost1.equals(userHost)) {
              passwords.remove(entry); // remove old entry(s) if exist
            }
          }
          String newItem = "//" + userHost + "/";
          String newEntry = userHost + ':' + encrypt(userHost, password);
          if(passwords.indexOf(newEntry) == -1) {
            passwords.add(newEntry);
            options.put(passwordKey, passwords);
          }
        }
        remoteRoots.add(new RemotePath(toUri("//" + user + '@' + host + "/")));
        return session;
      } catch(JSchException e) {
        System.err.println(e);
        System.err.println("Opening Session failed: " + userHost);
        //e.printStackTrace();
        throw new SftpOpenFailedException("Open failed");
        //return null;
      }
    }
  } // static Session getSession(String userHost)

  /**
   * Get a sftp channel on the given user@host. First, a session for
   * user@host is obtained. If there is a cached unused sftp channel
   * for the given user@host it is returned. Otherwise, a new sftp
   * channel is opened on the given user@host.
   *
   * Note: all channels must be returned for possible reuse by calling
   * releaseChannel()
   *
   * This method is synchronized on channels
   *
   * @param userHost the user@host to open the sftp channel
   * @return an opened sftp channel
   * @throws SftpOpenFailedException if the open fails
   */
  static ChannelSftp getSftpChannel(String userHost)
    throws SftpOpenFailedException {
    synchronized(channels) {
      try {
        Stack<ChannelSftp> channelStack = channels.get(userHost);
        if(channelStack == null) {
          channelStack = new Stack<ChannelSftp>();
          channels.put(userHost, channelStack);
        }
        getChannel: if(channelStack.size() != 0) {
          ChannelSftp channel = channelStack.pop();
          if(!channel.getSession().isConnected()) {
            channelStack.clear();
            break getChannel;
          }
          return channel;
        }
        Session session = getSession(userHost);
        if(session == null) {
          new Error().printStackTrace();
        }
        ChannelSftp channel = (ChannelSftp)session.openChannel("sftp");
        channel.connect(10*1000);
        return channel;
      } catch(JSchException e) {
        System.err.println(e);
        System.err.println("Opening Session failed: " + userHost);
        //e.printStackTrace();
        throw new SftpOpenFailedException();
      }
    }
  } // static ChannelSftp getSftpChannel(String userHost)

  /**
   * When a client is done using a channel it must be released by
   * calling this method so the channel can be used by someone
   * else. This should be done in a "finally" clause.
   *
   * This method is synchronized on channels.
   *
   * @param channel the channel to be released for future use
   */
  static void releaseChannel(ChannelSftp channel) {
    if(channel == null) {
      System.err.println("releasing null channel");
      new Error("releasing null channel").printStackTrace();
      return;
    }
    String userHost = "none";
    synchronized(channels) {
      try {
        Session session = channel.getSession();
        String user = session.getUserName();
        String host = session.getHost();
        userHost = user + '@' + host;
        Stack<ChannelSftp> channelStack = channels.get(userHost);
        channelStack.push(channel); // for possible future use
      } catch(JSchException e) {
        System.err.println(e);
        System.err.println("Returning Channel failed: " + userHost);
        e.printStackTrace();
      }
    }
  } // static void releaseChannel(ChannelSftp channel)

  static class SftpOpenFailedException extends Exception {
    final static long serialVersionUID = 42;
    SftpOpenFailedException() {}
    SftpOpenFailedException(String message) {
      super(message);
    }
  } // static class SftpOpenFailedException extends Exception

  /**
   * Normalize file specifier
   *   replace /./ with /
   *   remove final /.
   *   replace /x/../ with /
   *   remove final x/..
   *   user@host/~ to user@host/[home-directory]
   *
   * Canonicalizes a path string:
   *   replace // with /
   *   remove "." element from path
   *   remove single trailing "/" not part of name@server
   *
   * Note that //x@y/. -&gt; //x@y
   *
   * @param s a String representing a path in a file system
   * @return a String representing the same target with redundancies removed
   */

  /**
   * Creates an appropriate subclass instance of MyPath from a
   * string. LocalPaths start with a single "/" and RemotePaths start
   * with "//" with the format "//user@host/".
   *
   * @param s String to convert
   * @return an instance of an approprate subclass of MyPath
   */
  static MyPath stringToMyPath(String s) { //////////// stringToUri
    x: {
      if(s.equals("")) break x;
      if(s.equals(".")) {
        s = "";
        break x;
      }
      if(s.equals("/")) break x;
      if(s.indexOf(':') == 1) {
        s = Character.toUpperCase(s.charAt(0)) + s.substring(1);
      }
      if(s.indexOf(':') == 2 && s.charAt(0) == '/') {
        s = "" + s.charAt(0) + Character.toUpperCase(s.charAt(1))
          + s.substring(2);
      }
      int index = 0;
      while((index = s.indexOf("//", 2)) > 0) {
        s = s.substring(0, index) + s.substring(index + 1);
      }
      int length = s.length();
      if(s.charAt(length - 1) == '/') {
        if(!s.substring(0, 2).equals("//")) break x;
        else if(s.indexOf('/', 2) < length - 1) s = s.substring(0, length - 1);
      }

    }
    if(s.length() == 0) {
      return root;
    }
    if(s.length() > 1 && s.substring(0,2).equals("//")) {
      int at = s.indexOf('@');
      int start = s.indexOf('/', 2);
      if(start < 0) {
        start = s.length();
      }
      if(at < 0 || at > start)
        return new RemotePath(toUri("//unknown@unknown" + s.substring(2)));
      String user = s.substring(2, at);
      String host = s.substring(at + 1, start);
      String fileName = s.substring(start);
      ChannelSftp channel = null;
      if(fileName.length() == 0) {
        try {
          channel = getSftpChannel(user + '@' + host);
          fileName = channel.getHome(); ////// getHomeUri() ???
        } catch(SftpOpenFailedException e) {
          System.err.println("Can't find home: " + user + '@' + host);
        } catch(SftpException e) {
          System.err.println("Error finding home: " + user + '@' + host);
        } finally {
          releaseChannel(channel);
        }
      }
      return new RemotePath(toUri("//" + user + '@' + host + fileName));
    } else {
      //////////////// make relative to home
      if(s.charAt(0) != '/') s = '/' + s; ///difference between windows and unix
      return new LocalPath(toUri("file://" + s)); ///////// toAbsolutePath
    }
  } // static MyPath stringToMyPath(String s)

  /**
   * Returns the current root Paths. The current root Paths are the
   * file system root paths plus the roots of all open sftp
   * channels. Paths are sorted ignoring case with directories
   * before files.
   *
   * @param dotFiles true if dot files are to be included
   * @return an ArrayList&lt;Path&gt; created from all of the roots
   */
  static TreeSet<TableData> getRootPaths(boolean dotFiles) {
    TreeSet<TableData> list = new TreeSet<TableData>(tableDataComparator);
    Iterable<Path> iter = fileSystem.getRootDirectories();
    for(Path p : iter) {
      Path pp = p.getFileName();
      if(pp == null) pp = p.getRoot(); /////////// ????????
      else System.out.println("not null");
      String name = pp.toString();
      if(dotFiles || !(name.length() > 0 && name.charAt(0) == '.')) {
        list.add(new TableData(new LocalPath(p.toUri())));
      }
    }
    // add remote roots
    for(RemotePath path : remoteRoots) {
      list.add(new TableData(path));
    }
    return list;
  } // static TreeSet<TableData> getRootPaths(boolean dotFiles)

  /**
   * Takes a String that is presumed to be an integer and inserts
   * commas every third digit.
   *
   * @param s String containing an integer
   * @return a String with commas inserted every third digit
   */
  static String addCommas(String s) {
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < s.length(); ++i) {
      if(i != 0 && (s.length() - i) % 3 == 0) sb.append(',');
      sb.append(s.charAt(i));
    }
    return sb.toString();
  } // static String addCommas(String s)

  static PosixFilePermission[] permissions = {
    PosixFilePermission.OTHERS_EXECUTE,
    PosixFilePermission.OTHERS_WRITE,
    PosixFilePermission.OTHERS_READ,
    PosixFilePermission.GROUP_EXECUTE,
    PosixFilePermission.GROUP_WRITE,
    PosixFilePermission.GROUP_READ,
    PosixFilePermission.OWNER_EXECUTE,
    PosixFilePermission.OWNER_WRITE,
    PosixFilePermission.OWNER_READ
  };

  /**
   * Get an integer file permissions from a set of POSIX file permissions.
   *
   * @param perms set of POSIX file permissions
   * @return integer file permissions
   */
  static int octalFilePermissionsx(Set<PosixFilePermission> perms) {
    int octal = 0;
    for(PosixFilePermission perm : perms) {
      octal |= 1 << (8 - perm.ordinal());
    }
    return octal;
  } // static int octalFilePermissions(Set<PosixFilePermission> perms)

  /**
   * Get a set of POSIX file permissions from an octal file permissions.
   *
   * @param octal integer file permissions
   * @return set of POSIX file permissions
   */
  static Set<PosixFilePermission> javaFilePermissionsx(int octal) {
    Set<PosixFilePermission> set = new TreeSet<PosixFilePermission>();
    for(int i = 0; i < 9; ++i) {
      if((octal & (1 << i)) != 0) {
        System.out.println(i + " " + permissions[i]);
        set.add(permissions[i]);
      }
    }
    return set;
  } // static Set<PosixFilePermission> javaFilePermissions(int octal)

  // backwards
  // not used ////////////////
  static PosixFilePermission[] stuff = PosixFilePermission.values();

  /**
   * Gets the home directory using the authority of a URI. If the
   * authority is null, gets the home directory on the local file
   * system.
   *
   * @param authority the authority of a URI
   * @return the MyPath of the requested home directory.
   */
  static MyPath getHomeDirectoryx(String authority) {
    if(authority == null) {
      return new LocalPath(fileSystem.getPath(home).toUri());
    } else {
      ChannelSftp channel = null;
      try {
        channel = getSftpChannel(authority);
        return new RemotePath(channel.getHomeUri());
      } catch(SftpException | SftpOpenFailedException e) {
        return root;
      } finally {
        releaseChannel(channel);
      }
    }
  } // static MyPath getHomeDirectory(String authority)

  /**
   * Finds String in another String ignoring case if ignoreCase is
   * true. If s1 is null, returns -1. Search starts at fromIndex. To
   * search the entire string set fromIndex to 0.
   *
   * @param ignoreCase true if case is to be ignored
   * @param s1 String to search
   * @param s2 String to search for
   * @param fromIndex location to start search
   * @return index of s2 in s1 or -1 if unsuccessful
   */
  static int findIgnoreCase(boolean ignoreCase, String s1, String s2, int fromIndex) {
    if(s2.length() == 0) return -1;
    if(s1 == null) return -1;
    if(ignoreCase) {
      int length = s2.length();
      for(int i = fromIndex; i <= s1.length() - length; ++i) {
        if(s1.substring(i, i + length).equalsIgnoreCase(s2)) {
          return i;
        }
      }
      return -1;
    }
    return s1.indexOf(s2, fromIndex);
  }

  /**
   * MyProgMon is an implementation of a file transfer progress
   * monitor. It implements a progress window showing file transfer
   * progress to or from remote files. The interface first calls init
   * when a transfer starts, then calls count for each block of data
   * transferred, and finally calls end after the last block of data
   * is transferred.
   *
   * Calls to setVisible(true) also grab focus so calls to repaint()
   * are used to prevent this.
   */
  static class MyProgMon extends JFrame implements SftpProgressMonitor {
    final static long serialVersionUID = 42;

    long count=0;
    long max=0;
    long percent = -1;
    String title = null;
    JLabel label = null;
    Timer timer = null;

    MyProgMon() {
      label = new JLabel("                              ");
      setTitle("File Transfer Progress Monitor");
      pack();
      addWindowListener(new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            setVisible(false);
            dispose();
          }
        });
    }

    /**
     * Initialize a MyProgMonitor to start monitoring progress of a
     * file transfer.
     *
     * @param op I think this is PUT=0, GET=1 - not used by me
     * @param src source name of the file transfer
     * @param dest destination name of the file transfer
     * @param max length of the file transfer
     */
    @Override
    public void init(int op, String src, String dest, long max) {
      title = src + " -> " + dest;
      this.max=max;
      count=0;
      percent = -1;
      SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            add(label);
            pack();
            if(isVisible()) {
              repaint();
            } else {
              setVisible(true);
            }
            if(timer != null) timer.stop();
          }
        });
    }

    /**
     * Size of the current block of the transfer. The counts must be
     * summed to get the number of bytes currently transferred.
     *
     * @param count number of bytes transferred in current block
     * @return true to continue, false leaves a mess
     */
    @Override
    public boolean count(long count) {
      this.count += count;
      long newPercent = max == 0 ? count : this.count*100/max;
      if(percent != newPercent) {
        percent = newPercent;
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
              label.setText(percent + "% " + title);
              pack();
              repaint();
            }
          });
      }
      return true;
    }

    /**
     * Called when the transfer is completed
     */
    @Override
    public void end() {
      SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            if(timer == null) {
              timer = new Timer(5000, new ActionListener() {
                  public void actionPerformed(ActionEvent e) {
                    myProgMon.dispose();
                  }
                });
              timer.setRepeats(false);
              timer.start();
            } else {
              timer.restart();
            }
          }});
    }
  } // static class MyProgMon extends JFrame implements SftpProgressMonitor

  static MyProgMon myProgMon = new MyProgMon();

  /**
   * This class is the superclass of all path-type objects. The
   * current implementing subclasses are LocalPath and RemotePath. All
   * subclass constructors and serialized reads must set the parent
   * field.
   */
  static abstract class MyPath implements Serializable {
    final static long serialVersionUID = 42;

    URI uri;

    // just child directories
    protected transient ArrayList<MyPath> treeChildren = null; // cached

    /**
     * Returns the appropriate transfer DataFlavor for this MyPath.
     *
     * @return DataFlavor for this MyPath
     */
    abstract DataFlavor getFlavor();

    /**
     * Checks if the path refers to an existing file or directory.
     *
     * @return true if the path refers to an existing file or directory
     */
    abstract boolean exists(); // use stat comand

    /**
     * Checks if the path refers to a directory.
     *
     * @return true if path refers to a directory
     */
    abstract boolean isDirectory(); // use stat command and isDir()

    /**
     * Checks if the path refers to an ordinary file.
     *
     * @return true if the path refers to an ordinary file
     */
    abstract boolean isFile(); // use stat command and isFile()

    /**
     * Checks if the path refers to a link.
     *
     * @return true if the path refers to a link
     */
    abstract boolean isLink(); // use stat command and isLink()

    /**
     * returns a path to the parent of this path or null if there is
     * no parent.
     *
     * @return parent path
     */
    abstract MyPath getParent();

    /**
     * Gets the exposed directory children of this path. Only gets
     * directories.
     *
     * @return exposed children of this path
     */
    abstract ArrayList<MyPath> getTreeChildren(); // ls(path)

    /**
     * Enumerates all children (files and directories) of this path.
     *
     * @param dotFiles true if dot files are to be included
     * @return a list of all children of this path
     */
    abstract TreeSet<TableData> getChildren(boolean dotFiles);

    /**
     * Gets the index of child in children or -1 if not found.
     *
     * @param child the child to search for
     * @return the index of the child in children
     */
    int getIndex(MyPath child) {
      if(treeChildren == null) return -1;
      for(int i = 0; i < treeChildren.size(); ++i) {
        if(treeChildren.get(i).equals(child)) return i;
      }
      return -1;
    }

    /**
     * Appends other to the current path and returns it. "other" is a
     * simple file name.
     *
     * @param other the name to append to this path
     * @return the augmented path
     */
    abstract MyPath resolve(String other);

    /**
     * Appends other to the current path and returns it. "other" is a
     * simple file name.
     *
     * @param other the name to append to this path
     * @return the augmented path
     */
    abstract MyPath resolve(Path other);

    /**
     * Returns the length of the file in bytes refered to by this
     * path.
     *
     * @return the length of the file in bytes
     */
    abstract long size();

    /**
     * Returns the modification date of this file as the number of
     * milliseconds after 00:00:00 GMT, January 1, 1970.
     *
     * @return number of milliseconds after 00:00:00 GMT, January 1, 1970
     */
    abstract long getMTime();

    /**
     * Sets the modification date of this file as the number of
     * milliseconds after 00:00:00 GMT, January 1, 1970.
     *
     * @param time Number of milliseconds after 00:00:00 GMT, January 1, 1970
     * @return true if success
     */
    abstract boolean setMTime(long time);

    /**
     * creates an empty directory at the location specified by this.
     *
     * @return true if successful
     */
    abstract boolean makeDirectory();

    /**
     * Renames this file or directory, keeping it in the same parent
     * directory.
     *
     * @param newName the new name of the file or directory
     * @return true is successful
     */
    abstract boolean renameFile(String newName);

    /**
     * Will fill data with read information.
     *
     * @param data buffer to read data into
     */
    abstract void readFile(byte[] data);

    /**
     * Will write data to file starting at offset.
     *
     * @param data buffer to write data from
     */
    abstract void writeFile(byte[] data);

    /**
     * Reads lines from this file and returns them as a List&lt;String&gt;
     *
     * @return the lines from the file
     */
    abstract List<String> readAllLines();

    /**
     * Reads the link value (not it's target)
     *
     * @return the value of the link
     */
    abstract byte[] readLink();

    /**
     * Creates a symbolic link from this to target.
     *
     * @param target the target of the link
     * @return the value of the link
     */
    abstract boolean makeLinkTo(byte[] target);

    /**
     * Will touch file or directory.
     */
    abstract void touch();

    /**
     * Moves a file or directory from one directory to
     * another.
     *
     * @param file the file to be moved to this
     */
    abstract void moveFileFrom(MyPath file);

    /**
     * Move this to other.
     *
     * @param other directory to move this to
     */
    abstract void moveFileTo(LocalPath other);

    /**
     *  Move this to other.
     *
     * @param other directory to move this to
     */
    abstract void moveFileTo(RemotePath other);

    /**
     * Copies a file to this.
     *
     * @param file the file to be copied
     * @return true is successful
     */
    abstract boolean copyFileFrom(MyPath file);

    /**
     *  Copy this to toFile.
     *
     * @param toFile LocalPath to copy this to
     * @return true is successful
     */
    abstract boolean copyFileTo(LocalPath toFile);

    /**
     *  Copy this to toFile.
     *
     * @param toFile RemotePath to copy this to
     * @return true is successful
     */
    abstract boolean copyFileTo(RemotePath toFile);

    /**
     * Deletes a file or a symbolic link
     *
     * @return true is successful
     */
    abstract boolean delete();

    /**
     * Returns the base name of this path
     *
     * @return the base name of this path
     */
    @Override
    public abstract String toString();

    /**
     * Returns the full path of this path
     *
     * @return full path name
     */
    abstract String fullName();

    @Override
    abstract public boolean equals(Object other); // needed for JTree

    @Override
    abstract public int hashCode(); // needed for JTree
  } // static abstract class MyPath implements Serializable

  /**
   * A singleton class representing the root node of the tree.
   */
  static class RootPath extends MyPath {
    final static long serialVersionUID = 42;

    @Override
    DataFlavor getFlavor() {
      throw new Error("root getFlavor");
    };

    @Override
    boolean exists() {
      return true;
      //return Files.exists(path);
    }

    @Override
    boolean isDirectory() {
      return true;
    }

    @Override
    boolean isFile() {
      return false;
    }

    @Override
    boolean isLink() {
      return false;
    }

    @Override
    LocalPath getParent() {
      return null;
    }

    @Override
    TreeSet<TableData> getChildren(boolean dotFiles) {
      return getRootPaths(dotFiles); //////????????????????
    }

    /**
     * Only gets directories
     */
    @Override
    ArrayList<MyPath> getTreeChildren() {
      return treeChildren;
    }

    @Override
    LocalPath resolve(String other) {
      throw new Error("root resolve");
    }

    @Override
    LocalPath resolve(Path other) {
      throw new Error("root resolve");
    }

    @Override
    long size() {
      return 0;
    }

    @Override
    long getMTime() {
      return 0;
    }

    @Override
    boolean setMTime(long time) {
      return true;
    }

    @Override
    boolean makeDirectory() {
      return true;
    }

    @Override
    boolean renameFile(String newName) {
      return false;
    }

    @Override
    void readFile(byte[] data) {
      throw new Error("root readFile");
    }

    @Override
    void writeFile(byte[] data) {
      throw new Error("root writeFile");
    }

    @Override
    List<String> readAllLines() {
      throw new Error("root readAllLines");
    }

    @Override
    byte[] readLink() {
      throw new Error("root readLink");
    }

    @Override
    boolean makeLinkTo(byte[] target) {
      throw new Error("root makeLinkTo");
    }

    @Override
    void touch() {
      throw new Error("root touch");
    }

    @Override
    void moveFileFrom(MyPath file) {
      throw new Error("root moveFileFrom");
    }

    @Override
    // other is a directory
    void moveFileTo(LocalPath other) {
      throw new Error("root moveFileTo");
    }

    @Override
    // other is a directory
    void moveFileTo(RemotePath other) {
      throw new Error("root moveFileTo");
    }

    @Override
    boolean copyFileFrom(MyPath file) {
      throw new Error("root copyFileFrom");
    }

    @Override
    boolean copyFileTo(LocalPath toFile) {
      throw new Error("root copyFileTo");
    }

    @Override
    boolean copyFileTo(RemotePath toFile) {
      throw new Error("root copyFileTo");
    }

    @Override
    boolean delete() {
      throw new Error("root delete");
    }

    @Override
    public String toString() {
      return rootName;
    }

    @Override
    String fullName() {
      return rootName;
    }

    @Override
    public boolean equals(Object other) {
      if(other instanceof RootPath) return true;
      return false;
    }

    @Override
    public int hashCode() {
      return 7176405;
    }
  } // static class RootPath extends MyPath

  /**
   * Implements a path (MyPath) in the local file system. Illegal
   * characters for a linux file name are: '/' and null. Illegal
   * characters for a windows file name are /\:*?"&lt;&gt;|. Transferring a
   * file from a linux machine to a windows machine can be a problem
   * if one of the illegal windows file name characters is in the
   * linux file name. I don't know what to do about this.
   */
  static class LocalPath extends MyPath {
    final static long serialVersionUID = 42;

    WatchKey key; // the watch key for a visible directory entry in tree
    Path path; // cached Path for URI
    /**
     * For linux: root directory ends in '/', no other LocalPath does
     * For windows: top-level drives /x:/ ends in '/', no other LocalPath does
     *
     * NB: InvalidPathException.getIndex() returns an index to the
     * unescaped URI - all %xx count as one character. It also appears
     * to use -1 based indexing, i.e., need to add +1 to returned
     * index to get address of bad character (byte). (Need to check
     * this out on linux.) We don't use the index value in this code.
     *
     * 1. expand URI removing all % quoted characters.
     * 2. quote with % all characters not on approved list.
     * 3. convert back to uri
     * 4. convert URI to Path
     * 5. catch any exceptions and quote forbidden character
     * 6. when no more - set URI and cache Path in class variable
     *
     * Attempts to make a valid Path for the local file system.
     *
     * The strategy is to clear all %'s and add %'s for known
     * characters requiring escaping. The resulting String is
     * converted to a URI and then to a Path. If this fails then the
     * second part of the algorithm is tried.
     *
     * The URI is truncated and tried again. If this works another
     * character is added to the URI. If this fails then the character
     * is escaped. If it is already escaped then the % is
     * escaped. Must check that forward progress is made or it is a
     * failure. When the end of the String is reached then we are
     * done.
     *
     * The starting point depends on whether this is a windows or a
     * linux machine.
     *
     * @param uri the URI to be converted to a Path
     */
    LocalPath(URI uri) {
      String s = uri.getRawPath();
      if(s.charAt(s.length() - 1) == '/') s = s.substring(0, s.length() - 1);
      if(windows) {
        if(s.length() == 3 && s.charAt(0) == '/' && s.charAt(2) == ':') {
          s = s + '/';
        }
      } else {
        if(s.length() == 0) {
          s = "/";
        }
      }
      try {
        this.uri = new URI("file://" + s);
      } catch(URISyntaxException e) {
        System.out.println("s: " + s);
        e.printStackTrace();
        System.exit(1);
      }
      path = Paths.get(this.uri);
      return;
    }

    @Override
    DataFlavor getFlavor() {
      return DataFlavor.javaFileListFlavor;
    };

    @Override
    boolean exists() {
      return Files.exists(path, LinkOption.NOFOLLOW_LINKS);
    }

    @Override
    boolean isDirectory() {
      return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS);
    }

    @Override
    boolean isFile() {
      return Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);
    }

    @Override
    boolean isLink() {
      return Files.isSymbolicLink(path);
    }

    @Override
    MyPath getParent() {
      Path parent = path.getParent();
      if(parent == null) return root; // required by DefaultMutableTreeNode
      return new LocalPath(parent.toUri());
    }

    @Override
    TreeSet<TableData> getChildren(boolean dotFiles) {
      TreeSet<TableData> list = new TreeSet<TableData>(tableDataComparator);
      DirectoryStream<Path> iter = null;
      try {
        iter = Files.newDirectoryStream(path);
        for(Path p : iter) {
          String rawPath = p.toUri().getRawPath();
          String name = p.getFileName().toString();
          if(dotFiles || !(name.length() > 0 && name.charAt(0) == '.')) {
            list.add(new TableData(new LocalPath(p.toUri())));
          }
        }
      } catch(NoSuchFileException e) {
        System.err.println("NoSuchFileException caught");
        e.printStackTrace();
      } catch(IOException e) {
        System.err.println("IOException caught");
        e.printStackTrace();
        System.err.println(list);
      } finally {
        try {
          if(iter != null) iter.close();
        } catch(IOException ex) {
          System.err.println("Error closing iter");
          ex.printStackTrace();
        }
      }
      return list;
    }

    /**
     * Only gets directories
     */
    @Override
    ArrayList<MyPath> getTreeChildren() {
      if(treeChildren != null) return treeChildren; //////////////////
      treeChildren = new ArrayList<MyPath>();
      DirectoryStream<Path> iter = null;
      try {
        iter = Files.newDirectoryStream(path);
        for(Path p : iter) {
          LocalPath localPath = new LocalPath(p.toUri());
          //// links are followed - not
          if(localPath.isDirectory()) {
            treeChildren.add(localPath);
          }
        }
      } catch(IOException e) {
        System.err.println("Error listing children: " + uri);
        e.printStackTrace();
      } finally {
        try {
          iter.close();
        } catch(IOException ex) {
          ex.printStackTrace();
        }
      }
      Collections.sort(treeChildren, pathComparator);
      return treeChildren;
    }

    @Override
    LocalPath resolve(String other) {
      String name = uri.toString();
      if(name.charAt(name.length() - 1) != '/') name = name + '/';
      try {
        return new LocalPath(new URI(name + other));
      } catch(URISyntaxException e) {
        e.printStackTrace();
        return this;
      }
    }

    @Override
    LocalPath resolve(Path other) {
      Path p = Paths.get(uri).resolve(other);
      return(new LocalPath(p.toUri()));
    }

    @Override
    long size() {
      try {
        return Files.size(path);
      } catch(IOException e) {
        System.err.println("size() failed: " + uri);
        return 0;
      }
    }

    @Override
    long getMTime() {
      try {
        FileTime time
          = (FileTime)Files.getAttribute(path,
                                         "lastModifiedTime",
                                         LinkOption.NOFOLLOW_LINKS);
        return time.toMillis();
      } catch(IOException e) {
        System.err.println("getMTime() failed: " + uri);
        return 0;
      }
    }

    @Override
    boolean setMTime(long time) {
      try {
        Files.setLastModifiedTime(path, FileTime.fromMillis(time));
      } catch(IOException e) {
        e.printStackTrace();
        return false;
      }
      return true;
    }

    @Override
    boolean makeDirectory() {
      try {
        Files.createDirectory(path);
      } catch(IOException e) {
        System.err.println("makeDirectory() failed: " + uri);
        return false;
      }
      return true;
    }

    @Override
    boolean renameFile(String newName) {
      // cannot change case of local directories ///////////////////
      Path newPath = path.resolveSibling(newName);
      boolean force = false;
      try {
        if(force) {
          Files.move(path, newPath,
                     java.nio.file.StandardCopyOption.REPLACE_EXISTING,
                     LinkOption.NOFOLLOW_LINKS);
        } else {
          Files.move(path, newPath, LinkOption.NOFOLLOW_LINKS);
        }
        return true;
      } catch(AccessDeniedException e) {
        System.err.println(e);
        System.err.println(e.getReason());
        System.err.println("Failed renaming file: " + fullName());
        return false;
      } catch(IOException e) {
        System.err.println(e);
        System.err.println("Failed renaming file: " + fullName());
        return false;
      }
    }

    @Override
    void readFile(byte[] data) {
      InputStream is = null;
      try {
        is = Files.newInputStream(path);
        int index = 0;
        while(index < data.length) {
          int count = is.read(data, index, data.length - index);
          if(count <= 0) throw new Error("readFile did not fill buffer");
          index += count;
        }
      } catch(IOException e) {
        System.err.println("Failed reading file: " + fullName());
        e.printStackTrace();
      } finally {
        try {
          if(is != null) is.close();
        } catch(IOException e) {
          System.err.println(e);
          System.err.println("Failed closing file: " + fullName());
        }
      }
    }

    @Override
    void writeFile(byte[] data) {
      OutputStream os = null;
      try {
        os = Files.newOutputStream(path, LinkOption.NOFOLLOW_LINKS);
        os.write(data);
      } catch(IOException e) {
        System.err.println(e);
        System.err.println("Failed writing file: " + fullName());
      } finally {
        try {
          if(os != null) os.close();
        } catch(IOException e) {
          System.err.println(e);
          System.err.println("Failed closing file: " + fullName());
        }
      }
    }

    @Override
    List<String> readAllLines() {
      try {
        return Files.readAllLines(path, Charset.forName("ISO-8859-1"));
      } catch(IOException e) {
        e.printStackTrace();
        return new ArrayList<String>();
      }
    }

    @Override
    byte[] readLink() {
      try {
        ///////////// not quite correct
        return stringToBytes(Files.readSymbolicLink(path).toString());
      } catch(IOException e) {
        e.printStackTrace();
        return null;
      }
    }

    @Override
    boolean makeLinkTo(byte[] target) {
      try {
        //URI uri = toUri(bytesToString(target));
        //Path targetPath = Paths.get(toUri(bytesToString(target)));
        //Path targetPath = Paths.get(uri);
        Path targetPath = Paths.get(bytesToString(target));
        Files.createSymbolicLink(path, targetPath);
      } catch(IOException e) {
        e.printStackTrace();
        return false;
      }
      return true;
    }

    @Override
    void touch() {
      try {
        try {
          Files.createFile(path);
        } catch(FileAlreadyExistsException e) {
          Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
        }
      } catch(IOException e) {
        e.printStackTrace();
      }
    }

    @Override
    void moveFileFrom(MyPath file) {
      file.moveFileTo(this);
    }

    @Override
    // other is a directory
    void moveFileTo(LocalPath other) {
      boolean force = false;
      try {
        if(force) {
          Files.move(path, other.path,
                     java.nio.file.StandardCopyOption.REPLACE_EXISTING,
                     LinkOption.NOFOLLOW_LINKS);
        } else {
          Files.move(path, other.path, LinkOption.NOFOLLOW_LINKS);
        }
      } catch(IOException e) {
        System.err.println("Failed moving file: " + fullName());
      }
    }

    @Override
    void moveFileTo(RemotePath other) {
      ////////////////////////////////
    }

    @Override
    boolean copyFileFrom(MyPath file) {
      return file.copyFileTo(this);
    }

    @Override
    boolean copyFileTo(LocalPath toFile) {
      boolean force = true;
      try {
        Files.copy(path, toFile.path,
                   java.nio.file.StandardCopyOption.REPLACE_EXISTING,
                   java.nio.file.StandardCopyOption.COPY_ATTRIBUTES,
                   LinkOption.NOFOLLOW_LINKS);
      } catch(IOException e) {
        e.printStackTrace();
        System.err.println("Failed copying file: " + fullName());
        return false;
      }
      return true;
    }

    @Override
    boolean copyFileTo(RemotePath toFile) {
      ChannelSftp channel = null;
      try {
        channel = getSftpChannel(toFile.uri.getRawAuthority());
        channel.putUri(uri, toFile.uri, myProgMon);
        toFile.setMTime(getMTime());
        ////////// set permissions
        try {
          //System.out.println(Files.getPosixFilePermissions(path));
          //Set<PosixFilePermission> set = Files.getPosixFilePermissions(path);
          /*
          int octal = 0;
          for(PosixFilePermission perm : set) {
            System.out.println(perm.ordinal());
            octal |= 1 << (8 - perm.ordinal());
          }
          System.out.println(Integer.toOctalString(octal));
          System.out.println(Integer.toOctalString(octalFilePermissions(set)));
          */
        } catch(UnsupportedOperationException e) {
          System.err.println("Could not copy permissions on file: " + fullName());
        }
      } catch(SftpException | SftpOpenFailedException e) {
        e.printStackTrace();
        return false;
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    boolean delete() {
      try {
        Files.delete(path); // does not follow links
      } catch(IOException e) {
        e.printStackTrace();
        return false;
      }
      return true;
    }

    @Override
    public String toString() {
      String localFileName = uri.getRawPath();
      int lastSlash = localFileName.lastIndexOf('/');
      if(lastSlash == localFileName.length() - 1) return fullName();
      return localFileName.substring(lastSlash + 1);
    }

    @Override
    String fullName() {
      return uri.getRawPath();
    }

    @Override
    public boolean equals(Object other) {
      if(other == null || !(other instanceof LocalPath)) return false;
      return path.equals(((LocalPath)other).path);
    }

    @Override
    public int hashCode() {
      return path.hashCode();
    }
  } // static class LocalPath extends MyPath

  /**
   * An object that may be used to locate a file in a remote file
   * system.
   *
   * A RemotePath represents a path that is hierarchical and composed
   * of a sequence of directory and file name elements separated by a
   * special separator or delimiter. A root component, that identifies
   * a file system hierarchy, must be present. The name element that
   * is farthest from the root of the directory hierarchy is the name
   * of a file or directory. The other name elements are directory
   * names. A RemotePath can represent a root or a root and a sequence
   * of names, i.e., all RemotePaths are absolute. RemotePath defines
   * the getFileName(), getParent() getRoot(), and methods to access
   * the path components or a subsequence of its name elements.
   *
   * [In addition to accessing the components of a path, a Path also
   * defines the resolve and resolveSibling(Path) methods to combine
   * paths. (The relativize method that can be used to construct a
   * relative path between two paths. Paths can be compared, and
   * tested against each other using the startsWith and endsWith
   * methods.]
   *
   * RemotePaths may be used to operate on files, directories, and
   * other types of files.
   *
   * Implementations of this interface are immutable and safe for use
   * by multiple concurrent threads.
   */
  static class RemotePath extends MyPath {
    final static long serialVersionUID = 42;

    transient SftpATTRS linkAttributes = null;

    RemotePath(URI uri) {
      if(uri.getRawAuthority() == null) new Error("NULL constructor uri: " + uri).printStackTrace();
      this.uri = uri;
    }

    RemotePath(URI uri, SftpATTRS linkAttributes) {
      if(uri.getRawAuthority() == null) new Error("NULL constructor uri: " + uri).printStackTrace();
      this.uri = uri;
      this.linkAttributes = linkAttributes;
    }

    SftpATTRS getLinkAttributes() {
      if(linkAttributes != null) return linkAttributes;
      ChannelSftp channel = null;
      try {
        channel = getSftpChannel(uri.getRawAuthority());
        return linkAttributes = channel.lstatUri(uri);
      } catch(SftpException | NullPointerException | SftpOpenFailedException e) {
        return null;
      } finally {
        releaseChannel(channel);
      }
    }

    @Override
    DataFlavor getFlavor() {
      return remotePathArrayFlavor;
    }

    @Override
    boolean exists() {
      SftpATTRS attrs = getLinkAttributes();
      return attrs != null;
    }

    @Override
    boolean isDirectory() {
      if(getLinkAttributes() == null) return false;
      return getLinkAttributes().isDir();
    }

    @Override
    boolean isFile() {
      if(getLinkAttributes() == null) return false;
      return getLinkAttributes().isReg();
    }

    @Override
    boolean isLink() {
      if(getLinkAttributes() == null) return false;
      return getLinkAttributes().isLink();
    }

    @Override
    MyPath getParent() {
      String remoteFileName = uri.getRawPath();
      String userHost = uri.getRawAuthority();
      int lastSlash = remoteFileName.lastIndexOf('/');
      if(lastSlash < 0 || remoteFileName.length() == 1) {
        return root;
      }
      if(lastSlash == 0) return new RemotePath(toUri("//" + userHost + "/"));
      return new RemotePath(toUri("//" + userHost + remoteFileName.substring(0, lastSlash)));
    }

    /**
     * Fix failures to return empty ArrayList&lt;TableData&gt; /////////////
     */
    @Override
    TreeSet<TableData> getChildren(boolean dotFiles) {
      TreeSet<TableData> list = new TreeSet<TableData>(tableDataComparator);
      java.util.Vector<ChannelSftp.LsEntryUri> vv = null;
      ChannelSftp channel = null;
      String userHost = uri.getRawAuthority();
      String remoteFileName = uri.getRawPath();
      try {
        channel = getSftpChannel(userHost);
        vv = channel.lsUri(uri);
      } catch(SftpException e) {
        System.err.println("Can't get Paths: " + uri.toString()); /////////////
        e.printStackTrace();
      } catch(SftpOpenFailedException e) {
        System.err.println("Couldn't open " + userHost + "  " + remoteFileName);
        return root.getChildren(dotFiles);
      } finally {
        releaseChannel(channel);
      }
      if(vv!=null) {
        for(int ii = 0; ii < vv.size(); ++ii) {
          ChannelSftp.LsEntryUri obj = vv.get(ii);
          ChannelSftp.LsEntryUri entry = obj;
          SftpATTRS attrs = entry.getAttrs();
          String name = entry.getUri().getRawPath(); ////////////////
          int lastSlash = name.lastIndexOf('/');
          if(name.length() != 1) {
            name = name.substring(lastSlash + 1);
          }
          if(name.equals(".") || name.equals("..")) continue;
          if(!dotFiles && name.length() > 0 && name.charAt(0) == '.') continue;
          list.add(new TableData(new RemotePath(entry.getUri(), attrs)));
        }
      }
      return list;
    }

    /**
     * Only gets directories
     */
    @Override
    ArrayList<MyPath> getTreeChildren() {
      if(treeChildren != null) return treeChildren; // needed to prevent expansion of upper directories
      treeChildren = new ArrayList<MyPath>();
      // get remote directory listing
      java.util.Vector<ChannelSftp.LsEntryUri> vv =null;
      ChannelSftp channel = null;
      String userHost = uri.getRawAuthority();
      String remoteFileName = uri.getRawPath();
      try {
        channel = getSftpChannel(userHost);
        vv = channel.lsUri(uri);
      } catch(SftpException e) {
        System.err.println("SftpException: " + "   " + userHost + "  " + remoteFileName);
        e.printStackTrace();
      } catch(SftpOpenFailedException e) {
        System.err.println("getTreeChildren() failed " + "   " + userHost + "  " + remoteFileName);
        e.printStackTrace();
      } finally {
        releaseChannel(channel);
      }
      if(vv!=null) {
        for(int ii = 0; ii < vv.size(); ++ii) {
          ChannelSftp.LsEntryUri entry = vv.get(ii);
          SftpATTRS attrs = entry.getAttrs();
          String name = entry.getUri().getRawPath();
          int lastSlash = name.lastIndexOf('/');
          if(name.length() != 1) {
            name = name.substring(lastSlash + 1);
          }
          if(name.equals(".") || name.equals("..")) continue;
          if(attrs.isDir()) {
            treeChildren.add(new RemotePath(entry.getUri()));
          }
        }
      }
      Collections.sort(treeChildren, pathComparator);
      return treeChildren;
    }

    @Override
    RemotePath resolve(String other) {
      String name = uri.toString();
      if(name.charAt(name.length() - 1) != '/') name = name + '/';
      try {
        return new RemotePath(new URI(name + other));
      } catch(URISyntaxException e) {
        e.printStackTrace();
        return this;
      }
    }

    @Override
    RemotePath resolve(Path other) {
      return(new RemotePath(uri.resolve(other.toUri())));
    }

    @Override
    long size() {
      return getLinkAttributes().getSize();
    }

    @Override
    long getMTime() {
      return getLinkAttributes().getMTime()*1000L; // only second resolution
    }

    @Override
    boolean setMTime(long time) {
      ChannelSftp channel = null;
      String userHost = uri.getRawAuthority();
      try {
        channel = getSftpChannel(userHost);
        channel.setMtimeUri(uri, (int)(time/1000));
      } catch(SftpOpenFailedException e) {
        System.err.println(e);
        System.err.println("Opening Session failed: " + userHost);
        e.printStackTrace();
        return false;
      } catch(Exception e) {
        e.printStackTrace();
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    boolean makeDirectory() {
      ChannelSftp channel = null;
      String userHost = uri.getRawAuthority();
      try {
        channel = getSftpChannel(userHost);
        channel.mkdirUri(uri);
      } catch(SftpException e) {
        System.err.println("makeDirectory failed: " + uri.toString());
        return false;
      } catch(SftpOpenFailedException e) {
        e.printStackTrace();
        return false;
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    boolean renameFile(String newName) {
      String fullName = fullName();
      int firstSlash = fullName.indexOf('/', 2);
      int lastSlash = fullName.lastIndexOf('/');
      String targetFullName
        = fullName.substring(firstSlash, lastSlash + 1) + newName;
      ChannelSftp channel = null;
      try {
        channel = getSftpChannel(uri.getAuthority());
        channel.rename(uri.getPath(), targetFullName); ///////////////uri
      } catch(SftpException e) {
        System.err.println("makeDirectory failed: " + uri.getPath());
        return false;
      } catch(SftpOpenFailedException e) {
        e.printStackTrace();
        return false;
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    void readFile(byte[] data) {
      ChannelSftp fromChannel = null;
      InputStream is = null;
      try {
        fromChannel = getSftpChannel(uri.getRawAuthority());
        is = fromChannel.getUri(uri, null, 0L);
        int index = 0;
        while(index < data.length) {
          int count = is.read(data, index, data.length - index);
          if(count <= 0) throw new Error("readFile did not fill buffer");
          index += count;
        }
      } catch(Exception e) {
        System.err.println("Failed reading file: " + fullName());
        e.printStackTrace();
      } finally {
        try {
          if(is != null) is.close();
        } catch(IOException e) {
          System.err.println(e);
          System.err.println("Failed closing file: " + fullName());
        }
        releaseChannel(fromChannel);
      }
    }

    @Override
    void writeFile(byte[] data) {
      ChannelSftp toChannel = null;
      OutputStream os = null;
      try {
        toChannel = getSftpChannel(uri.getRawAuthority());
        os = toChannel.putUri(uri, null, ChannelSftp.OVERWRITE, 0L);
        os.write(data);
      } catch(Exception e) {
        e.printStackTrace();
      } finally {
        try {
          if(os != null) os.close();
        } catch(IOException e) {
          System.err.println(e);
          System.err.println("Failed closing file: " + fullName());
        }
        releaseChannel(toChannel);
      }
    }

    @Override
    List<String> readAllLines() {
      byte[] data = new byte[(int)size()]; // possible overflow
      readFile(data);
      ArrayList<String> lines = new ArrayList<String>();
      int index = 0;
      int i = 0;
      for(i = index; i < data.length; ++i) {
        if(data[i] == '\n') {
          lines.add(new String(data, index, i - index)); ////// set charset????
          if(i+1 < data.length && data[i+1] == '\r') ++i;
          index = i + 1; // past \n or \r
        } else if(i < data.length && data[i] == '\r') {
          lines.add(new String(data, index, i - index)); ////// set charset????
          index = i + 1; // past \r
        }
      }
      if(index < data.length && i < data.length) {
        lines.add(new String(data, index, i - index));
      }
      return lines;
    }

    @Override
    byte[] readLink() {
      ChannelSftp channel = null;
      String userHost = uri.getRawAuthority();
      try {
        channel = getSftpChannel(userHost);
        return channel.readlinkUri(uri);
        //return bytesToString(bytes);
      } catch(SftpException e) {
        System.err.println("readLink failed: " + uri.toString());
        return null;
      } catch(SftpOpenFailedException e) {
        e.printStackTrace();
        return null;
      } finally {
        releaseChannel(channel);
      }
    }

    @Override
    boolean makeLinkTo(byte[] target) {
      ChannelSftp channel = null;
      String userHost = uri.getRawAuthority();
      try {
        channel = getSftpChannel(userHost);
        channel.symlinkUri(target, uri);
      } catch(SftpException e) {
        System.err.println("makeLinkTo failed: " + uri.toString());
        return false;
      } catch(SftpOpenFailedException e) {
        e.printStackTrace();
        return false;
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    void touch() {
      if(exists()) {
        setMTime(System.currentTimeMillis());
      } else {
        writeFile(new byte[0]);
      }
    }

    @Override
    void moveFileFrom(MyPath file) {
      file.moveFileTo(this);
    }

    @Override
    void moveFileTo(LocalPath file) {
      throw new Error("Not Implemented Yet");
      ///////////////////////////////
    }

    @Override
    void moveFileTo(RemotePath file) {
      throw new Error("Not Implemented Yet");
      ///////////////////////////////
    }

    @Override
    boolean copyFileFrom(MyPath file) {
      return file.copyFileTo(this);
    }

    @Override
    boolean copyFileTo(LocalPath toPath) {
      ChannelSftp channel = null;
      try {
        channel = getSftpChannel(uri.getRawAuthority());
        channel.getUri(uri, toPath.uri, myProgMon);
        toPath.setMTime(getMTime());
        ////////// set permissions
      } catch(SftpException | SftpOpenFailedException e) {
        System.err.println(e.getMessage());
        e.printStackTrace();
        return false;
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    boolean copyFileTo(RemotePath toPath) {
      ChannelSftp fromChannel = null;
      ChannelSftp toChannel = null;
      InputStream is = null;
      OutputStream os = null;
      try {
        fromChannel = getSftpChannel(uri.getRawAuthority());
        toChannel = getSftpChannel(toPath.uri.getRawAuthority());
        is = fromChannel.getUri(uri, null, 0L);
        os = toChannel.putUri(toPath.uri, myProgMon, ChannelSftp.OVERWRITE, 0L);
        byte[] buf = new byte[4096];
        int len;
        while((len = is.read(buf)) > 0) {
          os.write(buf, 0, len);
        }
        os.flush();
        toPath.setMTime(getMTime());
      } catch(Exception e) {
        e.printStackTrace();
        return false;
      } finally {
        try {
          if(is != null) is.close();
        } catch(IOException e) {}
        try {
          if(os != null) os.close();
        } catch(IOException e) {}
        releaseChannel(toChannel);
        releaseChannel(fromChannel);
      }
      return true;
    }

    @Override
    boolean delete() {
      ChannelSftp channel = null;
      try {
        channel = getSftpChannel(uri.getRawAuthority());
        if(isDirectory()) {
          channel.rmdirUri(uri);
        } else if(isFile() || isLink()) {
          channel.rmUri(uri);
        } else {
          System.err.println("Unknown file type: " + fullName());
          return false;
        }
      } catch(SftpException | SftpOpenFailedException e) {
        e.printStackTrace();
        return false;
      } finally {
        releaseChannel(channel);
      }
      return true;
    }

    @Override
    public String toString() {
      String remoteFileName = uri.getRawPath();
      int lastSlash = remoteFileName.lastIndexOf('/');
      if(remoteFileName.charAt(remoteFileName.length() - 1) == '/') return fullName();
      return remoteFileName.substring(lastSlash + 1);
    }

    @Override
    String fullName() {
      return "//" + uri.getRawAuthority() + uri.getRawPath();
    }

    @Override
    public boolean equals(Object other) {
      if(other == null || !(other instanceof RemotePath)) return false;
      RemotePath p = (RemotePath)other;
      return uri.equals(p.uri);
    }

    @Override
    public int hashCode() {
      return uri.hashCode();
    }
  } // static class RemotePath extends MyPath

  final static int replaceFile = 0x1; // allow replacing a file with a file
  final static int replaceFileWithDirectory = 0x2; // allow replacing file with directory
  final static int replaceDirectoryWithFile = 0x4; // allow replacing directory with file

  /**
   * Copies a file or a complete directory tree.
   *
   * ////////////// need to copy file permissions (chmod)
   *
   * @param from the file or directory to copy
   * @param to the file or directory to copy to
   * @param flags bit map of what to allow
   * @param feedback a JLabel where to display current file/directory
   * @return true if successful
   */
  static boolean copyTree(MyPath from, MyPath to, int flags, JLabel feedback) {
    if(feedback != null) {
      feedback.setText("copying: " + from.fullName() + " -> " + to.fullName());
    }
    if(!from.exists()) {
      System.err.println("Copying from nonexistant path: " + from.fullName());
      return false;
    }
    if(from.isFile()) {
      if(to.exists()) {
        if((flags & (replaceFile | replaceDirectoryWithFile)) == 0) {
          System.err.println("flag1 fail: " + flags + "   to: " + to.uri);
          return false; //////////// dialog here
        }
        deleteTree(to, feedback);
      }
      return to.copyFileFrom(from);
    } else if(from.isLink()) {
      if(to.exists()) {
        if((flags & (replaceFile | replaceDirectoryWithFile)) == 0) {
          System.err.println("flag1 fail: " + flags + "   to: " + to.uri);
          return false; //////////// dialog here
        }
        deleteTree(to, feedback);
      }
      byte[] target = from.readLink();
      if(target == null) return false; ///////// error message
      return from.makeLinkTo(target);
    } else if(from.isDirectory()) {
      if(to.exists()) {
        if((flags & replaceFileWithDirectory) == 0) {
          System.err.println("flag2 fail: " + flags + "   to: " + to.uri);
          return false; //////////// dialog here
        }
        deleteTree(to, feedback);
      }
      to.makeDirectory();
      for(TableData child : from.getChildren(true)) {
        try {
          String name = to.uri.toString();
          URI toUri = new URI(name + '/');
          URI suffix = from.uri.relativize(child.path.uri);
          toUri = toUri.resolve(suffix);
          if(toUri.getRawAuthority() == null) {
            copyTree(child.path, new LocalPath(toUri), flags, feedback);
          } else {
            copyTree(child.path, new RemotePath(toUri), flags, feedback);
          }
        } catch(Throwable e) {
          e.printStackTrace();
          return false;
        }
      }
      return true;
    } else {
      System.err.println("unknown file type: " + from.fullName());
      return false;
    }
  } // static boolean copyTree(from, to, flags, feedback)

  /**
   * Moves a file or directory. Does a copy followed by a delete of
   * the old file or directory. For testing the delete is not
   * performed.
   *
   * @param from the file or tree to move
   * @param to the directory to move the file or directory to
   * @return true if successful
   */
  static boolean moveTree(MyPath from, MyPath to) {
    if(copyTree(from, to, 0, null)) {
      // return deleteTree(from); //////// add this statement when confident
      return true;
    } else return false;
  } // static boolean moveTree(MyPath from, MyPath to)

  /**
   * Deletes a file or an entire directory tree
   *
   * @param path the file or directory to delete
   * @param feedback A JLabel for posting progress feedback
   * @return true if successful
   */
  static boolean deleteTree(MyPath path, JLabel feedback) {
    if(path.isDirectory()) {
      for(TableData p : path.getChildren(true)) {
        if(!deleteTree(p.path, feedback)) return false;
      }
    }
    return path.delete();
  } // static boolean deleteTree(MyPath path, JLabel feedback)

  /**
   * Called to shut down everything at the end of running the program.
   */
  static void finish() {
    for(Map.Entry<String, Session> pair : sessions.entrySet()) {
      pair.getValue().disconnect();
    }
    if(myProgMon != null) myProgMon.dispose();
    writeOptions();
  } // static void finish()

  /**
   * This class provides a search and replace function for a file edit
   * window.
   *
   * Layout:
   *
   * (((Table |XXX| Field |XXX| _rowid_ |XXX|)))
   *
   * |   Find   | |XXXXX|   |Replace/Find| |XXXXX|
   * |  Replace | |XXXXX|   | Find Lines | |XXXXX|
   *
   * Find: select next occurance after current selection
   * Replace: replace current selection with replacement string
   * Replace/Find: do a Replace followed by a Find
   * Find Lines: find lines by number,number specified in second window
   */
  static class FindReplace extends JFrame {
    final static long serialVersionUID = 42;

    JTextArea textArea;
    JPanel statusLine = new JPanel(new BorderLayout());
    JLabel status = new JLabel("Status area");
    JTextArea findField = new UndoableTextArea(4, 15);
    JTextArea replaceField = new UndoableTextArea(4, 15);

    /**
     * Make a find/replace window operating on the given EditWindow.
     *
     * @param editWindow the EditWindow to operate on
     */
    FindReplace(EditWindow editWindow) {
      setTitle("Find - Replace");
      addWindowListener(new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            if(--windowCount == 0) {
              finish();
            }
            dispose();
          }
        });
      ++windowCount;
      this.textArea = editWindow.textArea;
      Rectangle r = editWindow.getBounds();
      setLocation(r.x + r.width, r.y);
      setTitle("Find - Replace");
      JPanel args2 = new JPanel();
      JPanel btns1 = new JPanel(new GridLayout(2, 1));
      JCheckBox checkBox = new JCheckBox("Ignore Case", true);
      btns1.add(new JButton("Find:") {
          final static long serialVersionUID = 42;

          {
            addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                  status.setText("");
                  String text = textArea.getText();
                  String find = findField.getText();
                  int end = textArea.getSelectionEnd();
                  int location = findIgnoreCase(checkBox.isSelected(), text, find, end);
                  if(location < 0) {
                    status.setText("not found");
                    textArea.select(0, 0);
                  } else {
                    textArea.select(location, location + find.length());
                    editWindow.toFront();
                  }
                }
              });
          }
        });
      btns1.add(new JButton("Replace:") {
          final static long serialVersionUID = 42;

          {
            addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                  status.setText("");
                  String replace = replaceField.getText();
                  textArea.replaceSelection(replace);
                  int end = textArea.getSelectionEnd();
                  textArea.select(end - replace.length(), end);
                }
              });
          }
        });
      args2.add(btns1);
      JScrollPane findScrollPane = new JScrollPane(findField);
      args2.add(findScrollPane);
      args2.add(new JLabel("      "));
      JPanel btns2 = new JPanel(new GridLayout(2, 1));
      args2.add(btns2);
      btns2.add(new JButton("Replace/Find:") {
          final static long serialVersionUID = 42;

          {
            addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                  status.setText("");
                  String replace = replaceField.getText();
                  textArea.replaceSelection(replace);
                  int end = textArea.getSelectionEnd();
                  textArea.select(end - replace.length(), end);
                  String text = textArea.getText();
                  String find = findField.getText();
                  int location = findIgnoreCase(checkBox.isSelected(), text, find, end);
                  if(location < 0) {
                    status.setText("not found");
                    textArea.select(0, 0);
                  } else {
                    textArea.select(location, location + find.length());
                    editWindow.toFront();
                  }
                }
              });
          }
        });
      btns2.add(new JButton("Find Lines") {
          final static long serialVersionUID = 42;

          {
            addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                  status.setText("");
                  try {
                    String lineSelection = replaceField.getText();
                    int comma = lineSelection.indexOf(',');
                    if(comma < 0) comma = lineSelection.length();
                    int startLine = Integer.parseUnsignedInt(lineSelection.substring(0, comma));
                    int endLine = startLine;
                    if(comma != lineSelection.length()) {
                      endLine = Integer.parseUnsignedInt(lineSelection.substring(comma + 1));
                    }
                    int start = textArea.getLineStartOffset(startLine - 1);
                    int end = textArea.getLineEndOffset(endLine - 1);
                    textArea.select(start, end);
                  } catch(NumberFormatException | BadLocationException ex) {
                    status.setText("Bad line designation");
                  }
                  editWindow.toFront();
                }
              });
          }
        });
      args2.add(btns2);
      JScrollPane replaceScrollPane = new JScrollPane(replaceField);
      args2.add(replaceScrollPane);
      getContentPane().add(args2, BorderLayout.CENTER);
      statusLine.add(status, BorderLayout.CENTER);
      statusLine.add(checkBox, BorderLayout.EAST);
      getContentPane().add(statusLine, BorderLayout.SOUTH);
    } // FindReplace(EditWindow editWindow)
  } // static class FindReplace extends JFrame

  static class UndoableTextArea extends JTextArea {
    final static long serialVersionUID = 42;

    UndoableTextArea() {
      super();
    }

    UndoableTextArea(Document doc) {
      super(doc);
    }

    UndoableTextArea(Document doc, String text, int rows, int columns) {
      super(doc, text, rows, columns);
    }

    UndoableTextArea(int rows, int columns) {
      super(rows, columns);
    }

    UndoableTextArea(String text) {
      super(text);
    }

    UndoableTextArea(String text, int rows, int columns) {
      super(text, rows, columns);
    }

    {
      UndoManager undoManager = new UndoManager();
      undoManager.setLimit(-1); // unlimited undos
      Document doc = getDocument();
      doc.addUndoableEditListener(new UndoableEditListener() {
          @Override
          public void undoableEditHappened(UndoableEditEvent e) {
            undoManager.addEdit(e.getEdit());
          }
        });

      InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
      ActionMap am = getActionMap();

      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
                                    toolkit.getMenuShortcutKeyMask()),
             "Undo");
      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
                                    toolkit.getMenuShortcutKeyMask()),
             "Redo");

      am.put("Undo", new AbstractAction() {
          final static long serialVersionUID = 42;
          @Override
          public void actionPerformed(ActionEvent e) {
            try {
              if(undoManager.canUndo()) {
                undoManager.undo();
              }
            } catch(CannotUndoException exp) {
              exp.printStackTrace();
            }
          }
        });
      am.put("Redo", new AbstractAction() {
          final static long serialVersionUID = 42;
          @Override
          public void actionPerformed(ActionEvent e) {
            try {
              if(undoManager.canRedo()) {
                undoManager.redo();
              }
            } catch(CannotUndoException exp) {
              exp.printStackTrace();
            }
          }
        });
    }
  } // static class UndoableTextArea extends JTextArea

  /**
   * An undoable text edit window
   *
   * This is the base class for all of the text windows. Since the
   * layout is BorderLayout (the default) clients can add buttons or
   * other things around the text window. The variable textArea can be
   * used to add listeners to the text area. The number of undos or
   * redos is unlimited.
   */
  static class EditWindow extends JFrame {
    final static long serialVersionUID = 42;

    ArrayList<JFrame> dependents = new ArrayList<JFrame>();
    JTextArea textArea;

    /**
     * Make a text edit window with given title and
     * contents. Dependent windows will be closed. The caller is
     * responsible for displaying the window and adding any
     * appropriate listeners.
     *
     * @param title the title of the window
     * @param contents the initial contents of the window
     */
    EditWindow(String title, String contents) {
      super(title);
      addWindowListener(new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            if(--windowCount == 0) {
              finish();
            }
            dispose();
          }
        });
      ++windowCount;
      textArea = new UndoableTextArea(contents);
      textArea.setLineWrap(true);
      textArea.setWrapStyleWord(true);
      textArea.addMouseListener(new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            if(e.getButton() == 3) {
              JFrame findReplace = new FindReplace(EditWindow.this);
              EditWindow.this.dependents.add(findReplace);
              findReplace.pack();
              findReplace.setVisible(true);
            }
          }
        });
      JScrollPane areaScrollPane = new JScrollPane(textArea);
      setPreferredSize(new Dimension(600, 300));
      getContentPane().add(areaScrollPane);
    } // EditWindow(String title, String contents)

    /**
     * Wrap lines that are too long. Insert prefix before broken
     * lines. Delete (some) trailing whitespace
     *
     * @param text the text to wrap lines in
     * @param maxLineLength the maximum line length desired
     * @param prefix the prefix for lines that had to be split
     * @return a String with all lines wrapped
     */
    String wrapLines(String text, int maxLineLength, String prefix) {
      int maxLength = maxLineLength;
      int shortLength = maxLineLength - prefix.length();
      StringBuilder newText = new StringBuilder();
      int textLength = text.length();
      while(textLength > 0 && (text.charAt(textLength - 1) == '\n' ||
                               text.charAt(textLength - 1) == ' ')) {
        --textLength;
      }
      text = text.substring(0, textLength) + '\n';
      int end;

      for(int start = 0 ; start < textLength ; start = end) {
        end = text.indexOf('\n', start) + 1;
        if(end == -1) end = textLength;
        int lineLength = end - start - 1;
        if(lineLength <= maxLength) {
          newText.append(text.substring(start, end));
          maxLength = maxLineLength;
        } else {
          end = text.lastIndexOf(' ', start + maxLength);
          if(end <= start) end = start + maxLength;
          newText.append(text.substring(start, end));
          newText.append('\n');
          newText.append(prefix);
          maxLength = shortLength;
        }
      }
      return newText.toString();
    } // String wrapLines(String text, int maxLineLength, String prefix)

    /**
     * Print the contents of the EditWindow.
     * //////////// should be in separate thread.
     *
     * @param title The title on each page
     */
    void print(String title) {
      MessageFormat footer = new MessageFormat("Page - {0}");
      try {
        JTextArea ta = new JTextArea(wrapLines(textArea.getText(), 80, ">>>>") + "\n "); // needed to print last line
        ta.print(new MessageFormat(title), footer);
      } catch(PrinterException e) {
        System.err.println(e);
      }
    } // void print(String title)
  } // static class EditWindow extends JFrame

  /**
   * Read the file at MyPath and return it as a String.
   *
   * @param path the path to the file to be read
   * @return the contents of the file as a String
   */
  static String readPath(MyPath path) {
    long length = path.size();
    if(length > 1000000) return ""; //////// or null ?????????
    byte[] buffer = new byte[(int)length];
    path.readFile(buffer);
    String s = new String(buffer, fileCharset);
    return s;
  } // static String readPath(MyPath path)

  /**
   * Write the contents of a String to the file at MyPath.
   *
   * @param path the path to the file to be written
   * @param s the String to be written out
   */
  static void writePath(MyPath path, String s) {
    byte[] buffer = s.getBytes(fileCharset);
    path.writeFile(buffer);
  } // static void writePath(MyPath path, String s)

  /**
   * Make a file edit window for editing files.
   *
   * The Read File button rereads the file and replaces the window
   * contents with the contents of the file. This operation is
   * undoable.
   *
   * The Write File button writes the window contents back to the
   * file.
   *
   * The Print button prints the edit window to a printer.
   *
   * @param path the Path to the file to be edited
   */
  void makeFileEditWindow(MyPath path) {
    String contents = readPath(path);
    EditWindow editWindow = new EditWindow(path.fullName(), contents);
    JTextArea textArea = editWindow.textArea;
    JPanel inner = new JPanel(new BorderLayout());
    JPanel btns = new JPanel(new GridLayout(1,3));
    JLabel status = new JLabel("(status line)");
    btns.add(new JButton("Read File") {
        final static long serialVersionUID = 42;

        {
          addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                String s = readPath(path);
                if(s != null) {
                  textArea.setText(s);
                  textArea.setCaretPosition(0);
                  textArea.grabFocus();
                }
              }
            });
        }
      });
    btns.add(new JButton("Write File") {
        final static long serialVersionUID = 42;

        {
          addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                writePath(path, textArea.getText());
                textArea.grabFocus();
              }
            });
        }
      });
    btns.add(new JButton("Print") {
        final static long serialVersionUID = 42;

        {
          addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                editWindow.print(path.fullName());
              }
            });
        }
      });
    textArea.addCaretListener(new CaretListener() {
        @Override
        public void caretUpdate(CaretEvent e) {
          try {
            int lwb = textArea.getLineOfOffset(textArea.getSelectionStart());
            int upb = textArea.getLineOfOffset(textArea.getSelectionEnd());
            String s;
            if(lwb++ == upb++) {
              status.setText("line:  " + lwb);
            } else {
              status.setText("lines: " + lwb + "-" + upb);
            }
          } catch(BadLocationException ex) {
            ex.printStackTrace();
          }
        }
      });
    inner.add(btns, BorderLayout.NORTH);
    inner.add(status, BorderLayout.SOUTH);
    editWindow.getContentPane().add(inner, BorderLayout.SOUTH);
    editWindow.setLocationByPlatform(true);
    editWindow.pack();
    editWindow.setVisible(true);
  } // makeFileEditWindow(MyPath path)

  static class MyComboBox extends JComboBox<MyPath> {
    final static long serialVersionUID = 42;

    MyPath selectedPath = root; /////////////////////////

    class MyComboBoxModel extends DefaultComboBoxModel<MyPath> {
      final static long serialVersionUID = 42;

      ArrayList<MyPath> comboBoxArray = new ArrayList<MyPath>();

      /*
       *  Create an empty model that will use the specified Comparator
       */
      MyComboBoxModel(MyPath path) {
        super();
        comboBoxArray.add(path); /////////////////////////
      }

      @Override
      public void addElement(MyPath element) {
        insertElementAt(element, comboBoxArray.size());
      }

      @Override
      public MyPath getElementAt(int index) {
        return comboBoxArray.get(index);
      }

      @Override
      public int getIndexOf(Object anObject) {
        return comboBoxArray.indexOf(anObject);
      }

      @Override
      public Object getSelectedItem() {
        return selectedPath;
      }

      @Override
      public int getSize() {
        return comboBoxArray.size();
      }

      @Override
      public void insertElementAt(MyPath element, int index) {
        comboBoxArray.add(index, element);
        fireIntervalAdded(this, index, index);
      }

      @Override
      public void removeAllElements() {
        if(comboBoxArray.size() > 0) {
          int firstIndex = 0;
          int lastIndex = comboBoxArray.size() - 1;
          comboBoxArray.clear();
          fireIntervalRemoved(this, firstIndex, lastIndex);
        }
      }

      @Override
      public void removeElement(Object anObject) {
        int index = comboBoxArray.indexOf(anObject);
        if(index != -1) {
          removeElementAt(index);
          fireIntervalRemoved(this, index, index);
        }
      }

      @Override
      public void removeElementAt(int index) {
        comboBoxArray.remove(index);
        fireIntervalRemoved(this, index, index);
      }

      @Override
      public void setSelectedItem(Object anObject) {
        if((selectedPath != null && !selectedPath.equals(anObject)) ||
            selectedPath == null && anObject != null) {
          selectedPath = (MyPath)anObject;
          fireContentsChanged(this, -1, -1);
        }
      }
    } // class MyComboBoxModel extends DefaultComboBoxModel<MyPath>
    MyComboBoxModel myComboBoxModel = new MyComboBoxModel(selectedPath);

    class MyComboBoxEditor implements ComboBoxEditor {
      final static long serialVersionUID = 42;

      JTextField editor;

      MyComboBoxEditor() {
        setOpaque(true);
        editor = new JTextField(selectedPath.fullName());
        editor.setFocusTraversalKeysEnabled(false); // allow VK_TAB events
        editor.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
              // canonize file name and set selectedPath
              try {
                selectedPath = stringToMyPath(editor.getText());
                while(!selectedPath.exists() || !selectedPath.isDirectory()) {
                  selectedPath = selectedPath.getParent();
                  if(selectedPath == null) {
                    selectedPath = root;
                    break;
                  }
                }
              } catch(NullPointerException ex) {
                System.err.println("caught null pointer"); /////////
                ex.printStackTrace();
                selectedPath = root;
              }
              String name = selectedPath.fullName();
              editor.setText(name);
              //setSelectedItem(selectedPath);
              //selectFromPath((MyPath)selectionBox.getSelectedItem(), true);
            }
          });
        editor.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent event) {
              if(event.getKeyChar() == KeyEvent.VK_TAB) {
                String s = editor.getText();
                if(s.length() == 0) {
                  selectedPath = root;
                  editor.setText(selectedPath.fullName());
                  return;
                }
                String candidate = s;
                MyPath parent;
                ArrayList<MyPath> childPaths;
                if(s.charAt(s.length() - 1) == '/') {
                  parent = stringToMyPath(s);
                  if(!parent.exists()) return;
                  childPaths = parent.getTreeChildren();
                } else {
                  parent = stringToMyPath(s).getParent();
                  if(!parent.exists()) return;
                  childPaths = parent.getTreeChildren();
                }
                Iterator<MyPath> iterator = childPaths.iterator();
                while(iterator.hasNext()) {
                  MyPath directoryPath = iterator.next();
                  String child = directoryPath.fullName();
                  if(child.length() < s.length()) continue;
                  if(!s.equals(child.substring(0, s.length()))) continue;
                  candidate = child + '/';
                  break;
                }
                int parentLength = parent.fullName().length();
                w: while(iterator.hasNext()) {
                  MyPath directoryPath = iterator.next();
                  String child = directoryPath.fullName() + '/';
                  if(child.length() < s.length()) continue;
                  int min = Math.min(child.length(), candidate.length());
                  for(int i = parentLength; i < min; ++i) {
                    if(child.charAt(i) != candidate.charAt(i)) {
                      if(i < s.length()) continue w;
                      candidate = candidate.substring(0, i);
                      break;
                    }
                  }
                }
                editor.setText(candidate);
                selectedPath = stringToMyPath(candidate);
                event.consume(); //////// do I need this?
              } else {
                //super.keyTyped(event); /////////// ???????????????
              }
            }
          });
      } // MyComboBoxEditor()

      @Override
      public void addActionListener(ActionListener l) {
        listenerList.add(ActionListener.class, l);
      }

      @Override
      public Component getEditorComponent() {
        return editor;
      }

      @Override
      public Object getItem() {
        return selectedPath;
      }

      @Override
      public void removeActionListener(ActionListener l) {
        listenerList.remove(ActionListener.class, l);
      }

      @Override
      public void selectAll() {
      }

      @Override
      public void setItem(Object newValue) {
        MyPath path = selectedPath = (MyPath) newValue;
        if(newValue != null) {
          editor.setText(path.fullName());
          selectedPath = path;
        }
      }
    } // class MyComboBoxEditor implements ComboBoxEditor
    MyComboBoxEditor myComboBoxEditor = new MyComboBoxEditor();

    class MyComboBoxRenderer extends JLabel
      implements ListCellRenderer<MyPath> {
      final static long serialVersionUID = 42;

      MyComboBoxRenderer() {
        setOpaque(true);
        //setHorizontalAlignment(CENTER);
        //setVerticalAlignment(CENTER);
      }
      /*
       * This method finds the image and text corresponding
       * to the selected value and returns the label, set up
       * to display the text and image.
       */
      @Override
      public Component
        getListCellRendererComponent(JList<? extends MyPath> list,
                                     MyPath path,
                                     int index,
                                     boolean isSelected,
                                     boolean cellHasFocus) {
        // the index is -1 when rendering the ConboBox itself
        if(isSelected) {
          setBackground(list.getSelectionBackground());
          setForeground(list.getSelectionForeground());
          if(index >= 0) {
            list.setToolTipText(path.fullName());
          }
        } else {
          setBackground(list.getBackground());
          setForeground(list.getForeground());
        }
        if(index < 0) {
          setText(path.fullName());
        } else {
          setText(path.fullName());
        }
        return this;
      }
    } // class MyComboBoxRenderer extends JLabel
    MyComboBoxRenderer myComboBoxRenderer = new MyComboBoxRenderer();

    MyComboBox(MyPath path) {
      setMaximumRowCount(10);
      selectedPath = path;
      setModel(myComboBoxModel);
      setEditor(myComboBoxEditor);
      setRenderer(myComboBoxRenderer);
      setEditable(true);

      // Just for testing - should be added by client ????????????
      addPopupMenuListener(new PopupMenuListener() {
          @Override
          public void popupMenuCanceled(PopupMenuEvent e) {
          }

          @Override
          public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
          }

          @Override
          public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            JTextField textEditor = myComboBoxEditor.editor;
            String text = textEditor.getText();
            if(textEditor.getText().length() == 0
               || selectedPath == root && text.equals(rootName)) {
              textEditor.setText(rootName);
              myComboBoxModel.removeAllElements();
              TreeSet<TableData> fileRoots = getRootPaths(true);
              ArrayList<MyPath> roots = new ArrayList<MyPath>();
              for(TableData p : fileRoots) {
                roots.add(p.path); //////////
              }
              selectedPath.treeChildren = roots;
              for(MyPath child : roots) {
                myComboBoxModel.addElement(child);
              }
              for(String entry : options.get(selectionsKey)) {
                addItem(stringToMyPath(entry));
              }
            } else {
              try {
                selectedPath = stringToMyPath(text);
                while(!selectedPath.exists() || !selectedPath.isDirectory()) {
                  selectedPath = selectedPath.getParent();
                  if(selectedPath == null || selectedPath == root) {
                    selectedPath = root;
                    textEditor.setText(rootName);
                    break;
                  }
                }
              } catch(NullPointerException ex) {
                System.err.println("caught null pointer"); /////////
                selectedPath = root;
              }
              textEditor.setText(selectedPath.fullName());
              myComboBoxModel.removeAllElements();
              if(selectedPath != null) {
                if(selectedPath == root) {
                  TreeSet<TableData> fileRoots = getRootPaths(true);
                  ArrayList<MyPath> roots = new ArrayList<MyPath>();
                  for(TableData p : fileRoots) {
                    roots.add(p.path); //////////
                  }
                  selectedPath.treeChildren = roots;
                  for(MyPath child : roots) {
                    myComboBoxModel.addElement(child);
                  }
                  return;
                }
                for(MyPath child : selectedPath.getTreeChildren()) {
                  myComboBoxModel.addElement(child);
                }
              }
            }
          }
        });
    } // MyComboBox(MyPath path)
  } // static class MyComboBox extends JComboBox<MyPath>

  /**
   * This class is just a data structure with the table information
   */
  static class TableData /* implements Comparable<TableData> */ {
    MyPath path; // the file path / name
    long size; // the size of the file
    long mtime; // the modification date / time

    /**
     * initialize a TableData
     *
     * @param path the full path
     */
    TableData(MyPath path) {
      this.path = path;
      if(path.isLink()) {
        size = -1;
      } else {
        size = path.size();
      }
      mtime = path.getMTime();
    } // TableData(MyPath path)

    @Override
    public String toString() {
      return path.fullName();
    }
  } // static class TableData

  static class TableDataComparator implements Comparator<TableData> {
    /**
     * This comparator puts directories first and sorts names
     * differing only in case together.
     */
    @Override
    public int compare(TableData d1, TableData d2) {
      if(d1.path.isDirectory()) {
        if(d2.path.isDirectory()) {
          String s1 = flatten(d1.path.uri.getRawPath());
          String s2 = flatten(d2.path.uri.getRawPath());
          int c = s1.compareToIgnoreCase(s2);
          if(c != 0) return c;
          return s1.compareTo(s2);
        } else {
          return -1;
        }
      } else {
        if(d2.path.isDirectory()) {
          return 1;
        }
      }
      String s1 = flatten(d1.path.uri.getRawPath());
      String s2 = flatten(d2.path.uri.getRawPath());
      int c = s1.compareToIgnoreCase(s2);
      if(c != 0) return c;
      return s1.compareTo(s2);
    }
  } // static class TableDataComparator implements Comparator<TableData>
  static TableDataComparator tableDataComparator = new TableDataComparator();

  static class DataNameComparator implements Comparator<TableData> {
    /**
     * This comparator respects case and does not treat directories
     * specially.
     */
    @Override
    public int compare(TableData d1, TableData d2) {
      String s1 = flatten(d1.path.toString());
      String s2 = flatten(d2.path.toString());
      int c = s1.compareToIgnoreCase(s2);
      if(c != 0) return c;
      return s1.compareTo(s2);
    }
  } // static class DataNameComparator implements Comparator<TableData>
  static DataNameComparator dataNameComparator = new DataNameComparator();

  /**
   * This comparator of Paths puts directories first, then sorts
   * ignoring case, then considering case.
   */
  static class PathComparator implements Comparator<MyPath> {
    @Override
    public int compare(MyPath p1, MyPath p2) {
      boolean isDirectory1 = p1.isDirectory();
      boolean isDirectory2 = p2.isDirectory();
      if(isDirectory1 && !isDirectory2) return -1;
      if(!isDirectory1 && isDirectory2) return 1;
      String s1 = p1.uri.getRawAuthority();
      if(s1 == null) s1 = "";
      else s1 += '/';
      s1 = flatten(s1 + p1.uri.getRawPath());
      String s2 = p2.uri.getRawAuthority();
      if(s2 == null) s2 = "";
      else s2 += '/';
      s2 = flatten(s2 + p2.uri.getRawPath());
      int c = s1.compareToIgnoreCase(s2);
      if(c != 0) return c;
      return s1.compareTo(s2);
    }
  } // static class PathComparator implements Comparator<MyPath>

  static PathComparator pathComparator = new PathComparator();

  /**
   * This class is a pure data structure and represents a node in a
   * JTree.
   */
  static class MyTreeNode {
    MyPath myPath;
    MyTreeNode parent;
    ArrayList<MyTreeNode> children;

    MyTreeNode(MyPath myPath, MyTreeNode parent) {
      this.myPath = myPath;
      this.parent = parent;
      children = null;
    }

    public String toString() {
      return myPath.toString(); // needed for tree cell editing
    }
  } // static class MyTreeNode

  static class TreeNodeComparator implements Comparator<MyTreeNode> {
    @Override
    public int compare(MyTreeNode t1, MyTreeNode t2) {
      return pathComparator.compare(t1.myPath, t2.myPath);
    }
  } // static class TreeNodeComparator implements Comparator<MyTreeNode>

  static TreeNodeComparator treeNodeComparator = new TreeNodeComparator();

  /**
   * This class handles JTextField drag &amp; drop. Strings and files
   * can be dropped. For files only the file name without directory
   * information is retained. Dropping multiple files is not
   * supported.
   */
  static class MyTextTransferHandler extends TransferHandler {
    final static long serialVersionUID = 42;

    @Override
    public boolean canImport(TransferHandler.TransferSupport info) {
      // we only import Strings and single Files
      try {
        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
          if(((List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)).size() == 1) {
            return true;
          } else {
            return false;
          }
        }
      } catch(UnsupportedFlavorException e) {
        System.err.println("UnsupportedFlavorException");
        return false;
      } catch(IOException e) {
        e.printStackTrace();
        return false;
      }
      if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
        return true;
      }
      return false;
    }

    @Override
    public boolean importData(TransferHandler.TransferSupport info) {
      if(!canImport(info)) {
        return false;
      }
      JTextField tf = (JTextField)info.getComponent();
      String data;
      try {
        if(info.isDrop()) {
          if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
            List<?> files = (List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
            if(files.size() != 1) return false; // can handle only one file
            data = files.get(0).toString();
            tf.setText(data);
          } else if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
            data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
            if(info.isDrop()) {
              tf.setText(data);
            } else {
              tf.replaceSelection(data);
            }
          } else {
            return false;
          }
        } else {
          // paste
          if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
            List<?> files = (List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
            if(files.size() != 1) return false; // can handle only one file
            data = files.get(0).toString();
            tf.setText(data);
          } else if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
            data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
            tf.replaceSelection(data);
          }
        }
      } catch(UnsupportedFlavorException e) {
        System.err.println("UnsupportedFlavorException");
        return false;
      } catch(IOException e) {
        e.printStackTrace();
        return false;
      }
      SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            tf.grabFocus();
          }
        });
      return true;
    }

    @Override
    public int getSourceActions(JComponent c) {
      return COPY_OR_MOVE;
    }

    protected Transferable createTransferable(JComponent c) {
      JTextField source = (JTextField)c;
      int start = source.getSelectionStart();
      int end = source.getSelectionEnd();
      if(start == end) {
        return null;
      }
      String data = source.getSelectedText();
      return new StringSelection(data);
    }

    @Override
    public void exportDone(JComponent c, Transferable t, int action) {
      if(action != MOVE) {
        return;
      }
      ((JTextComponent)c).replaceSelection("");
    }
  } // class MyTextTransferHandler extends TransferHandler

  static class MyUserInfo implements UserInfo, UIKeyboardInteractive {
    @Override
    public String getPassword() { return passwd; }
    @Override
    public boolean promptYesNo(String str) {
      Object[] options = { "yes", "no" };
      int foo = JOptionPane.showOptionDialog(null,
                                             str,
                                             "Warning",
                                             JOptionPane.DEFAULT_OPTION,
                                             JOptionPane.WARNING_MESSAGE,
                                             null, options, options[0]);
      return foo == 0;
    }

    String passwd;
    JTextField passwordField = (JTextField)new JPasswordField(20);

    {
      passwordField.addHierarchyListener(new HierarchyListener() {
          @Override
          public void hierarchyChanged(HierarchyEvent e) {
            Component c = e.getComponent();
            if(c.isShowing() && (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
              Window toplevel = SwingUtilities.getWindowAncestor(c);
              toplevel.addWindowFocusListener(new WindowAdapter() {
                  @Override
                  public void windowGainedFocus(WindowEvent e) {
                    c.requestFocus();
                  }
                });
            }
          }
        });
    }

    @Override
    public String getPassphrase() {
      return null;
    }

    @Override
    public boolean promptPassphrase(String message) {
      return true;
    }

    @Override
    public boolean promptPassword(String message) {
      Object[] ob = { passwordField };
      int result = JOptionPane.showConfirmDialog(null, ob, message,
                                                 JOptionPane.OK_CANCEL_OPTION);
      if(result == JOptionPane.OK_OPTION) {
        passwd = passwordField.getText();
        return true;
      } else {
        return false;
      }
    }

    @Override
    public void showMessage(String message) {
      JOptionPane.showMessageDialog(null, message);
    }

    GridBagConstraints gbc  =
      new GridBagConstraints(0,0,1,1,1,1,
                             GridBagConstraints.NORTHWEST,
                             GridBagConstraints.NONE,
                             new Insets(0,0,0,0),0,0);
    private Container panel;
    @Override
    public String[] promptKeyboardInteractive(String destination,
                                              String name,
                                              String instruction,
                                              String[] prompt,
                                              boolean[] echo) {
      panel = new JPanel();
      panel.setLayout(new GridBagLayout());

      gbc.weightx = 1.0;
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.gridx = 0;
      panel.add(new JLabel(instruction), gbc);
      ++gbc.gridy;

      gbc.gridwidth = GridBagConstraints.RELATIVE;

      JTextField[] texts = new JTextField[prompt.length];
      for(int i = 0; i < prompt.length; ++i) {
        gbc.fill = GridBagConstraints.NONE;
        gbc.gridx = 0;
        gbc.weightx = 1;
        panel.add(new JLabel(prompt[i]),gbc);

        gbc.gridx = 1;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weighty = 1;
        if(echo[i]) {
          texts[i] = new JTextField(20);
        } else {
          texts[i] = new JPasswordField(20);
        }
        panel.add(texts[i], gbc);
        ++gbc.gridy;
      }
      texts[0].addHierarchyListener(new HierarchyListener() {
          @Override
          public void hierarchyChanged(HierarchyEvent e) {
            Component c = e.getComponent();
            if(c.isShowing() && (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
              Window toplevel = SwingUtilities.getWindowAncestor(c);
              toplevel.addWindowFocusListener(new WindowAdapter() {
                  @Override
                  public void windowGainedFocus(WindowEvent e) {
                    c.requestFocus();
                  }
                });
            }
          }
        });
      if(JOptionPane.showConfirmDialog(null, panel,
                                       destination + ": " + name,
                                       JOptionPane.OK_CANCEL_OPTION,
                                       JOptionPane.QUESTION_MESSAGE)
         == JOptionPane.OK_OPTION) {
        String[] response = new String[prompt.length];
        for(int i = 0; i < prompt.length; ++i) {
          response[i] = texts[i].getText();
        }
        passwd = response[0];
        return response;
      } else {
        return null;  // cancel
      }
    }
  } // static class MyUserInfo implements UserInfo, UIKeyboardInteractive

  /**
   * Perform a diff between two files. Show the results in an
   * EditWindow.
   *
   * @param path1 the MyPath for the first file
   * @param path2 the MyPath for the second file
   */
  static void diff(MyPath path1, MyPath path2) {
    class Diff {
      // Space optimized function to find the length of the longest
      // common subsequence of substring 'X[0:m-1]' and 'Y[0:n-1]'
      List<String> lines1 = path1.readAllLines();
      List<String> lines2 = path2.readAllLines();
      int m = lines1.size();
      int n = lines2.size();
      int[] lenLCS = new int[m+1];
      int[] locLCS = new int[m+1];
      int[] f = new int[n+1];
      int[] b = new int[n+1];
      StringBuilder sb = new StringBuilder();

      Diff() {
        int max = LCSpart(0, m + 1, 0, n);
        int left1 = 0;
        int right1 = 0;
        for(int i = 0; i <= m; ++i) {
          if(i == m || lenLCS[i] < lenLCS[i + 1]) {
            int left2 = i;
            int right2 = i != m ? locLCS[i+1]-1 : n;
            String left = (left1+1) >= left2 ? "" + left2
              : (left1+1) + "," + left2;
            String right = (right1+1) >= right2 ? "" + right2
              : (right1+1) + "," + right2;
            if(left1 != left2) {
              if(right1 != right2) {
                sb.append(left + 'c' + right + '\n');
                for(int ii = left1; ii < left2; ++ii) {
                  sb.append("< " + lines1.get(ii) + '\n');
                }
                sb.append("---" + '\n');
                for(int ii = right1; ii < right2; ++ii) {
                  sb.append("> " + lines2.get(ii) + '\n');
                }
              } else {
                sb.append(left + 'd' + right + '\n');
                for(int ii = left1; ii < left2; ++ii) {
                  sb.append("< " + lines1.get(ii) + '\n');
                }
              }
            } else {
              if(right1 != right2) {
                sb.append(left + 'a' + right + '\n');
                for(int ii = right1; ii < right2; ++ii) {
                  sb.append("> " + lines2.get(ii) + '\n');
                }
              }
            }
            left1 = left2 + 1;
            right1 = right2 + 1;
          }
        }
        EditWindow editWindow
          = new EditWindow(path1.fullName() + " <> " + path2.fullName(),
                           sb.toString());
        editWindow.pack();
        editWindow.setVisible(true);
      } // diff

      /**
       * Do a matching in a rectangle
       *
       * @param lom lower bound for m
       * @param him upper bound for m
       * @param lin lower bound for n
       * @param him upper bound for n
       */
      int LCSpart(int lom, int him, int lon, int hin) {
        int midm = (lom + him)/2;
        if(midm == lom) return -1;
        forward(lom, midm, lon, hin); // uses forward array f[]
        backward(midm, him, lon, hin); // uses backward array b[]
        int maxLCS = -1;
        int maxj = 0;
        for(int j = lon; j <= hin; ++j) {
          int len = f[j] + b[j];
          if(len > maxLCS) {
            maxLCS = len;
            maxj = j;
          }
        }
        locLCS[midm] = maxj;
        lenLCS[midm] = f[maxj] + lenLCS[lom];
        LCSpart(lom, midm, lon, maxj);
        int r = LCSpart(midm, him, maxj, hin);
        return r < 0 ? maxj : r;
      } // int LCSpart(int lom, int him, int lon, int hin)

      /**
       * Do a forward scan in a rectangle
       *
       * @param lom lower bound for m
       * @param him upper bound for m
       * @param lin lower bound for n
       * @param him upper bound for n
       */
      int forward(int lom, int him, int lon, int hin) {
        int prev;
        for (int i = lom; i <= him; ++i) {
          prev = f[lon];
          for (int j = lon; j <= hin; ++j) {
            int backup = f[j];
            if (i == lom || j == lon) {
              f[j] = 0;
            } else {
              if(lines1.get(i - 1).equals(lines2.get(j - 1))) {
                f[j] = prev + 1;
              } else {
                f[j] = Integer.max(f[j], f[j - 1]);
              }
            }
            prev = backup;
          }
        }
        // LCS will be the last entry in the lookup table
        return f[hin];
      } // int forward(int lom, int him, int lon, int hin)

      /**
       * Do a backward scan in a rectangle
       *
       * @param lom lower bound for m
       * @param him upper bound for m
       * @param lin lower bound for n
       * @param him upper bound for n
       */
      int backward(int lom, int him, int lon, int hin) {
        int prev;
        // fill the lookup table in a bottom-up manner
        for (int i = him; i >= lom; --i) {
          prev = b[hin];
          for (int j = hin; j >= lon; --j) {
            int backup = b[j];
            if (i == him || j == hin) {
              b[j] = 0;
            } else {
              if (i != m && lines1.get(i).equals(lines2.get(j))) {
                b[j] = prev + 1;
              } else {
                b[j] = Integer.max(b[j], b[j + 1]);
              }
            }
            prev = backup;
          }
        }
        // LCS will be the last entry in the lookup table
        return b[lon];
      } // int backward(int lom, int him, int lon, int hin)
    } // class Diff
    new Diff(); // do the diff
  } // public static void diff(MyPath path1, MyPath path2)

  static boolean windowsXPDSTHack = false;
  /**
   * This class reads all of the user options readfrom
   * optionPath. There are lists of remote servers (for a drop-down
   * selection list), size of initial window etc.
   *
   * The format of the ~/.filebrowserrc file is a section header in
   * square brackets [] followed by one line for each instance of the
   * option, possibly followed by a blank line (or end of file).
   *
   * The options variable is a map from option header names to an
   * ArrayList of options. This options variable may be updated during
   * the running of the program and written out by the user (in the
   * proper format).
   *
   * Current option categories are:
   * [remote hosts] // user@host or user@host:password(encrypted)
   *
   * MUST USE readAllBytes to ignore character set issues /////////////
   */
  static void readOptions() { ////fffffffffffff
    options.clear();
    try {
      List<String> lines = Files.readAllLines(optionPath, charSet);
      String type = "Heading";
      ArrayList<String> optionList = new ArrayList<String>();
      for(String line : lines) {
        if(line.length() > 0 && line.charAt(0) == '[') {
          if(optionList.size() != 0) options.put(type, optionList);
          type = line.substring(1, line.length() - 1);
          optionList = new ArrayList<String>();
        } else {
          if(line.length() != 0 && optionList.indexOf(line) == -1) {
            optionList.add(line);
          }
        }
      }
      if(optionList.size() != 0) options.put(type, optionList);
    } catch(IOException e) {
      System.err.println("Error reading options file");
      System.err.println(e);
    }
    // make sure there is an entry for selectionsKey
    if(options.get(selectionsKey) == null) {
      options.put(selectionsKey, new ArrayList<String>());
    }
    ArrayList<String> optionList = options.get("Settings");
    if(optionList != null) {
      for(String option : optionList) {
        int index = option.indexOf(':');
        if(index > 0 && index + 1 < option.length()) {
          String optionName = option.substring(0, index);
          String optionValue = option.substring(index + 1);
          //String optionValue = option.substring(index + 1).trim();
          switch(optionName) {
          case "WindowsXPDSTHack": {
            if(optionValue.equalsIgnoreCase("Yes")) windowsXPDSTHack = true;
          }
          }
        }
      }
    }
  } // static void readOptions()

  static {
    readOptions();
  }

  /**
   * This class writes all of the user options from optionPath. There
   * are lists of remote servers (for a drop-down selection list),
   * size of initial window etc.
   *
   * The format of the ~/.filebrowserrc file is a section header in
   * square brackets [] followed by one line for each instance of the
   * option, possibly followed by a blank line (or end of file).
   *
   * The options variable is a map from option header names to an
   * ArrayList of options. This options variable may be updated during
   * the running of the program and written out by the user (in the
   * proper format).
   *
   * Current option categories are: [remote hosts] // user@host or
   * user@host:password(encrypted)
   *
   * MUST USE writeAllBytes to ignore character set issues /////////////
   */
  static void writeOptions() {
    ArrayList<String> lines = new ArrayList<String>();
    for(Map.Entry<String, ArrayList<String>> pair : options.entrySet()) {
      ArrayList<String> values = pair.getValue();
      if(values.size() != 0) {
        String key = pair.getKey();
        lines.add('[' + key + ']');
        for(String value : values) {
          lines.add(value);
        }
        lines.add("");
      }
    }
    try {
      Files.write(optionPath, lines, charSet); ////ffffffffff
    } catch(IOException e) {
      System.err.println("Error writing options file");
      System.err.println(e);
    }
  } // static void writeOptions()

  static final char lo = ' ';
  static final char hi = '~';

  /**
   * Encrypts a password for storing in options file.
   *
   * @param seed a seed for encryption
   * @param data a string for encryption
   * @return the encrypted string
   */
  static String encrypt(String seed, String data) {
    char[] chars = data.toCharArray();
    for(int i = 0; i < chars.length; ++i) {
      char c = chars[i];
      if(c >= lo && c <= hi) {
        int subst = 23*i + 7
          + seed.charAt(i % seed.length())
          + 2*seed.charAt(seed.length() - 1  - (i % seed.length()));
        int code = (chars[i] - lo + subst) % (hi - lo + 1) + lo;
        if(code < lo) code += hi - lo + 1;
        chars[i] = (char)code;
      }
    }
    return new String(chars);
  } // static String encrypt(String seed, String data)

  /**
   * Decrypts a password for storing in options file.
   *
   * @param seed a seed for decryption
   * @param data a string for decryption
   * @return the decrypted string
   */
  static String decrypt(String seed, String data) {
    char[] chars = data.toCharArray();
    for(int i = 0; i < chars.length; ++i) {
      char c = chars[i];
      if(c >= lo && c <= hi) {
        int subst = 23*i + 7
          + seed.charAt(i % seed.length())
          + 2*seed.charAt(seed.length() - 1  - (i % seed.length()));
        int code = (chars[i] - lo - subst) % (hi - lo + 1) + lo;
        if(code < lo) code += hi - lo + 1;
        chars[i] = (char)code;
      }
    }
    return new String(chars);
  } // static String decrypt(String seed, String data)

  MyPath selectedPath;

  MyPath endEditing(MyComboBox comboBox) {
    MyComboBox.MyComboBoxEditor comboBoxEditor = comboBox.myComboBoxEditor;
    JTextField editor = comboBoxEditor.editor;
    MyPath path;
    try {
      path = stringToMyPath(editor.getText());
      while(!path.exists() || !path.isDirectory()) {
        path = path.getParent();
        if(path == null) {
          path = root;
          break;
        }
      }
    } catch(NullPointerException ex) {
      System.err.println("caught null pointer"); /////////
      path = root;
    }
    ////editor.setText(path.fullName()); //**********************//////
    return path;
  } // MyPath endEditing(MyComboBox comboBox)

  class Browser extends JFrame {
    final static long serialVersionUID = 42;

    JComboBox<MyPath> selectionBox = new MyComboBox(root);
    JTextField editor = (JTextField)selectionBox.getEditor().getEditorComponent();
    {
      selectionBox.setPreferredSize(new Dimension(200, 15)); //// 15 ?????
    }

    JSplitPane splitPane; // the split pane - tree on left, table on right
    int splitLocation; // the remembered location of the split

    JTree tree; // The left side of the JSplitPane

    // tree watcher watches all expanded nodes of a tree
    WatchService treeWatcher;
    HashMap<WatchKey,TreePath> treeKeys = new HashMap<WatchKey,TreePath>();
    // table watcher watches only one directory
    WatchService tableWatcher;
    WatchKey tableKey = null;
    MyPath tablePath = null;
    JTable table; // The right side of the JSplitPane
    ArrayList<TableData> tableData = new ArrayList<TableData>();
    MyTreeModel myTreeModel = new MyTreeModel();
    MyTableModel myTableModel = new MyTableModel();
    ArrayList<String> tableColumnNames = new ArrayList<String>();
    { tableColumnNames.add("Name");
      tableColumnNames.add("Size");
      tableColumnNames.add("Date Modified");
    } ///// hack until user-selectable properties - if ever

    boolean showDotFiles = false;
    boolean listFiles = true;

    JTextField currentDirectory = new JTextField();

    ArrayList<MyPath> history = new ArrayList<MyPath>();
    int historyIndex = -1; // points to current directory

    //static DataFlavor remotePathArrayFlavor
    //= new DataFlavor(ArrayList.class, "remotePathListFlavor");

    MyPath getSelectedPath(TreePath treePath) {
      return ((MyTreeNode)treePath.getLastPathComponent()).myPath;
    } // MyPath getSelectedPath(TreePath treePath)

    /**
     * This method takes a Path and selects the corresponding tree
     * node and table entry (if not a directory). If the Path does not
     * refer to an actual directory or file in the tree / table then
     * it selects the deepest Path that does exist. The selection is
     * truncated to the current visible tree.
     *
     * should return valid selection if problems, e.g., first root
     *
     * @param myPath a file path to be selected
     * @param mark true if path to be pushed on undo stack
     */
    void selectFromPath(MyPath myPath, boolean mark) {
      if(myPath == null) { ///////////////////
        return;
      }
      //if(mark) System.out.println(myPath);
      if(mark) pushDirectoryInHistory(myPath);
      MyPath file = null;
      ArrayDeque<MyPath> pathStack = new ArrayDeque<MyPath>();
      while(!myPath.exists() && (myPath != root)) myPath = myPath.getParent();
      if(myPath.exists() && !myPath.isDirectory()) {
        file = myPath;
        myPath = myPath.getParent();
      }
      while(myPath != root && myPath != null) {
        pathStack.push(myPath);
        myPath = myPath.getParent();
      }
      if(myPath == null) myPath = root;
      TreePath treePath = rootTreePath;
      while(!pathStack.isEmpty()) {
        MyTreeNode node = ((MyTreeNode)treePath.getLastPathComponent());
        myPath = pathStack.pop(); // get next MyPath to enter in tree
        MyPath childPath = myPath;
        MyTreeNode parent = node;
        if(parent.children == null) {
          parent.children = new ArrayList<MyTreeNode>();
        }
        int lo = 0;
        int hi = parent.children.size();
        int c = 1;
        int index;
        found: {
          while(lo < hi) {
            int mid = (lo + hi) >>> 1;
            c = pathComparator.compare(parent.children.get(mid).myPath,
                                       childPath);
            if(c == 0) {
              index = mid;
              break found;
            }
            if(c > 0) {
              hi = mid;
            } else {
              lo = mid + 1;
            }
          } // end while
          parent.children.add(lo, new MyTreeNode(childPath, parent));
          index = lo;
          myTreeModel.nodesWereInserted(parent, index);
        } // break found goes after here
        MyTreeNode child = node.children.get(index);
        treePath = treePath.pathByAddingChild(child); // get next TreePath
      }
      tree.setSelectionPath(treePath);
      if(file != null) {
        for(int i = 0; i < tableData.size(); ++i) {
          if(tableData.get(i).path.toString().equals(file.toString())) {
            table.setRowSelectionInterval(i, i); // scroll to this entry
            table.scrollRectToVisible(table.getCellRect(i, 0, true));
            break;
          }
        }
      }
    } // void selectFromPath(MyPath myPath, boolean mark)

    /**
     * Regenerates the table data after notification that the table data
     * has changed.
     */
    void regenerateTable() {
      if(tablePath == null) {
        tableData = new ArrayList<TableData>(getRootPaths(true));
      } else {
        tableData = new ArrayList<TableData>(tablePath.getChildren(showDotFiles));
      }
      myTableModel.fireTableChanged(new TableModelEvent(myTableModel));
    } // void regenerateTable()

    /**
     * Pushes path into the file/directory stack. There could be a
     * problem with character sets.
     *
     * @param path the Path to be pushed onto the stack.
     */
    void pushDirectoryInHistory(MyPath path) {
      if(historyIndex >= 0 && history.get(historyIndex).equals(path)) return;
      ++historyIndex;
      while(history.size() > historyIndex) history.remove(history.size() - 1);
      history.add(path);
      //System.out.println(history);
    } // void pushDirectoryInHistory(MyPath path)

    /**
     * MyTableModel gets data from and stores data into the containing
     * Table class. MyTableModel model allows drag &amp; drop and cell
     * editing.
     */
    class MyTableModel extends AbstractTableModel {
      final static long serialVersionUID = 42;

      @Override
      public int getColumnCount() {
        return tableColumnNames.size();
      }

      @Override
      public int getRowCount() {
        return tableData.size();
      }

      @Override
      public String getColumnName(int col) {
        return tableColumnNames.get(col);
      }

      /**
       * Method used to get data to display after
       * getTableCellRendererComponent converts it for displaying.
       */
      @Override
      public Object getValueAt(int row, int col) {
        TableData data = tableData.get(row);
        switch(col) {
        case 0: return data.path.toString(); ////////////
        case 1: {
          if(tableData.get(row).path.isLink()) {
            byte[] targetBytes = tableData.get(row).path.readLink();
            String target = bytesToString(targetBytes);
            return target;
          } else return data.size;
        }
        case 2:
          Calendar c = Calendar.getInstance();
          c.setTime(new Date(data.mtime));
          //c.setTimeInMillis(long millis);
          //c.get(Calendar.DST_OFFSET)
          Date date = new Date(data.mtime);
          return date.toString()/* + '|' + date.getTime()*/;
        default: throw new Error("Bad table column");
        }
      }

      /*
       * JTable uses this method to determine the default renderer/
       * editor for each cell. If we didn't implement this method then
       * the editor might not be a String editor.
       *
       * This is correct even though the cells contain Paths. It is
       * fixed in setValueAt
       */
      @Override
      public Class<?> getColumnClass(int c) {
        return String.class; // all columns rendered as strings
      }

      /**
       * Disable the default editor if not the first column or the cell
       * contains a directory (because of an apparent race
       * condition). Directories must be edited in the tree view.
       */
      @Override
      public boolean isCellEditable(int row, int col) {
        return col == 0 && !tableData.get(row).path.isDirectory()
          || col == 1 && tableData.get(row).path.isLink();
      }

      /**
       * Apparently only used if cell is edited. We are using Strings as
       * the type of the table cell so must convert to Path and check
       * for equality. Must also check for duplicating another file in
       * the same directory.
       *
       * @param value new (edited) value of cell
       * @param row row of edited cell
       * @param col column of edited cell
       */
      @Override
      public void setValueAt(Object value, int row, int col) {
        if(row < tableData.size()) {
          if(col == 0) {
            String current = tableData.get(row).path.toString();
            if(!value.equals(current)) {
              tableData.get(row).path.renameFile((String)value); ////fffffffff
              //renameFile(tableData.get(row).getPath(), (String)value);
              fireTableCellUpdated(row, col);
              // following needed to update display with new file name
              javax.swing.SwingUtilities.invokeLater(new Runnable() {
                  @Override
                  public void run() {
                    regenerateTable();
                  }
                });
            }
          } else if(col == 1) {
            byte[] targetBytes = tableData.get(row).path.readLink();
            String target = bytesToString(targetBytes);
            //System.out.println(target);
            //System.out.println(value);
            tableData.get(row).path.delete();
            tableData.get(row).path.makeLinkTo(stringToBytes((String)value)); ////fffffffff
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                  regenerateTable();
                }
              });
          }
        }
      }
    } // class MyTableModel extends AbstractTableModel

    /**
     * This class controls the text, foreground and background color for
     * table cells.
     */
    class MyTableCellRenderer extends DefaultTableCellRenderer {
      final static long serialVersionUID = 42;

      @Override
      public Component getTableCellRendererComponent(JTable table,
                                                     Object value,
                                                     boolean isSelected,
                                                     boolean hasFocus,
                                                     int row,
                                                     int viewColumn) {
        if(table == null) return this;
        int column = table.convertColumnIndexToModel(viewColumn); ////

        Color fg = table.getForeground();
        Color bg = table.getBackground();
        setToolTipText(null);
        /*
         * drop location - pink/blue
         * selected - selection colors
         * link - yellow
         * normal - background gray for directories
         */
        JTable.DropLocation dropLocation = table.getDropLocation();
        if(dropLocation != null
           && !dropLocation.isInsertRow()
           && !dropLocation.isInsertColumn()
           && dropLocation.getColumn() == column
           && dropLocation.getRow() == row) {
          fg = BLUE;
          bg = PINK;
        } else if(isSelected) {
          fg = table.getSelectionForeground();
          bg = table.getSelectionBackground();
        } else {
          fg = table.getForeground();
          if(column == 0 && tableData.get(row).path.isDirectory()) {
            bg = LIGHT_GRAY;
          } else if(tableData.get(row).path.isLink()) {
            bg = LIGHT_YELLOW;
          } else {
            bg = (table.getBackground());
          }
        }
        setForeground(fg);
        setBackground(bg);
        setFont(table.getFont());
        setBorder(new EmptyBorder(1, 1, 1, 1));
        if(column == 0) {
          setValue(value);
          setHorizontalAlignment(JLabel.LEFT);
          setToolTipText(value.toString());
        } else if(column == 1) {
          if(value == null) {
            System.err.println("null at column 1 getTableCellRendererComponent");
            return this;
          }
          String s;
          if(tableData.get(row).path.isLink()) {
            byte[] bytes = tableData.get(row).path.readLink();
            if(bytes == null) s = "?????";
            else s = bytesToString(bytes);
            setHorizontalAlignment(JLabel.LEFT);
          } else {
            s = addCommas(value.toString());
            setHorizontalAlignment(JLabel.RIGHT);
          }
          setValue(s);
        } else if(column == 2) {
          setValue(value);
          setHorizontalAlignment(JLabel.LEFT);
        }
        return this;
      }
    } // class MyTableCellRenderer extends DefaultTableCellRenderer

    /**
     * This class handles drag &amp; drop on Tables. Only files can be
     * dropped. Dropping multiple files is also supported. A drop on a
     * directory moves or copies the files to the dropped on
     * directory. A drop on a file or in a blank area of the table drops
     * to the node selected in the tree.
     */
    class MyTableTransferHandler extends TransferHandler {
      final static long serialVersionUID = 42;

      @Override
      public boolean canImport(TransferHandler.TransferSupport info) {
        // we only support drags and drops (not clipboard paste)
        if(!info.isDrop()) return false;
        info.setShowDropLocation(true);

        // we only import files //////// This is the destination Component
        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return true;
        if(info.isDataFlavorSupported(remotePathArrayFlavor)) return true;
        if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
          return true;
        }
        return false;
      }

      /**
       * Return true if data successfully imported from info
       */
      @Override
      public boolean importData(TransferHandler.TransferSupport info) {
        if(!canImport(info)) {
          return false;
        }
        Transferable transferable = info.getTransferable();
        JTable.DropLocation dl = (JTable.DropLocation)info.getDropLocation();
        int row = dl.getRow();
        MyPath targetPath;
        MyPath tp; // drop directory location

        if(row == -1) {
          // no table entry selected so use selected tree node
          targetPath = tp = getSelectedPath(tree.getSelectionPath());
        } else {
          targetPath = tp = tableData.get(row).path;
        }
        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ||
           info.isDataFlavorSupported(remotePathArrayFlavor)) {
          // get directory to move/copy file(s) into
          if(!tp.isDirectory()) tp = tp.getParent();
          ArrayList<MyPath> sourceFiles = new ArrayList<MyPath>();
          try {
            // get list of MyPath of files to move/copy
            if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
              List<?> files = (List<?>)transferable.getTransferData(DataFlavor.javaFileListFlavor);
              for(Object file : files) {
                String s = toUri(((File)file).toString()).toString(); ////ffffffff
                if(windows) s = s.replaceAll("%5C", "/");
                sourceFiles.add(new LocalPath(toUri(s))); ////fffffffff
              }
            } else if(info.isDataFlavorSupported(remotePathArrayFlavor)) {
              try {
                List<?> files = (List<?>)transferable.getTransferData(remotePathArrayFlavor);
                for(Object file : files) {
                  RemotePath remotePath = (RemotePath)file;
                  sourceFiles.add(remotePath);
                }
              } catch(Exception e) {
                System.err.println(e);
                e.printStackTrace();
                return false;
              }
            }
            // do the move/copy
            if(info.getDropAction() == COPY) {
              for(MyPath path : sourceFiles) {
                copyTree(path, tp.resolve(path.toString()), 1, null);
              }
            } else if(info.getDropAction() == MOVE) {
              for(MyPath path : sourceFiles) {
                moveTree(path, tp.resolve(path.toString()));
                //moveTree(path, tp);
              }
              //tp.moveFilesFrom(sourceFiles);
            } else return false;
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                  regenerateTable();
                }
              });
            return true; // success!
          } catch(UnsupportedFlavorException e) {
            System.err.println("Exception: UnsupportedFlavorException");
            return false;
          } catch(IOException e) {
            System.err.println("Exception: IOException");
            e.printStackTrace();
            return false;
          } catch(ClassCastException e) {
            System.err.println("Exception: ClassCastException");
            e.printStackTrace();
            return false;
          } catch(Throwable e) {
            System.err.println("Exception: Throwable");
            e.printStackTrace();
            return false;
          }

        }
        if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
          if(targetPath.isDirectory()) return false;
          String data;
          try {
            data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
          } catch(UnsupportedFlavorException e) {
            return false;
          } catch(IOException e) {
            return false;
          }
          if(info.isDrop()) {
            targetPath.renameFile(data);
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                  regenerateTable();
                }
              });
            return true;
          } else {
            return false;
          }
        }
        return false;
      }

      @Override
      public int getSourceActions(JComponent c) {
        return COPY_OR_MOVE;
      }

      @Override
      public Transferable createTransferable(JComponent c) {
        int[] rows = table.getSelectedRows();
        DataFlavor myFileFlavor = tableData.get(rows[0]).path.getFlavor();
        return new Transferable() {
          @Override
          public DataFlavor[] getTransferDataFlavors() {
            return new DataFlavor[]{myFileFlavor,
                                    DataFlavor.stringFlavor};
          }

          @Override
          public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.equals(myFileFlavor)
              || flavor.equals(DataFlavor.stringFlavor);
          }

          @Override
          public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
            if(flavor.equals(myFileFlavor)) { ////fffffffffffffff
              if(flavor.equals(DataFlavor.javaFileListFlavor)) {
                ArrayList<File> files = new ArrayList<File>();
                for(int row : rows) {
                  files.add(((LocalPath)tableData.get(row).path).path.toFile());
                }
                return files;
              }
              if(flavor.equals(remotePathArrayFlavor)) {
                ArrayList<RemotePath> files = new ArrayList<RemotePath>();
                for(int row : rows) {
                  files.add((RemotePath)tableData.get(row).path);
                }
                return files;
              }
            }
            if(flavor.equals(DataFlavor.stringFlavor)) {
              int row = table.getSelectedRows()[0];
              return tableData.get(row).path.toString();
            }
            return null;
          }
        };
      }

      @Override
      public void exportDone(JComponent c, Transferable t, int i) {
      }
    } // class MyTableTransferHandler extends TransferHandler

    MyTreeNode treeRoot = new MyTreeNode(root, null);
    TreePath rootTreePath = new TreePath(treeRoot);

    /**
     * This class controls the updating of the tree when a node is
     * renamed.
     */
    class MyTreeModel implements TreeModel {
      final static long serialVersionUID = 42;

      MyTreeModel() {
      }

      EventListenerList listenerList = new EventListenerList();
      TreeModelEvent treeModelEvent = null;

      @Override
      public void addTreeModelListener(TreeModelListener l) {
        listenerList.add(TreeModelListener.class, l);
      }

      @Override
      public void removeTreeModelListener(TreeModelListener l) {
        listenerList.remove(TreeModelListener.class, l);
      }

      void reload(TreePath treePath) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for(int i = listeners.length-2; i>=0; i-=2) {
          if(listeners[i]==TreeModelListener.class) {
            // Lazily create the event:
            treeModelEvent = new TreeModelEvent(this, treePath);
            ((TreeModelListener)listeners[i+1]).treeStructureChanged(treeModelEvent);
          }
        }
      } // void reload(TreePath treePath)

      @Override
      public int getIndexOfChild(Object parent, Object child) {
        if(((MyTreeNode)parent).children == null) return -1;
        return ((MyTreeNode)parent).children.indexOf((MyTreeNode)child);
      }

      @Override
      public int getChildCount(Object parent) {
        ArrayList<MyTreeNode> children = ((MyTreeNode)parent).children;
        if(children == null) return 0;
        return children.size();
      }

      @Override
      public MyTreeNode getChild(Object parent, int index) {
        return ((MyTreeNode)parent).children.get(index);
      }

      @Override
      public MyTreeNode getRoot() {
        return treeRoot;
      }

      @Override
      public boolean isLeaf(Object node) {
        return false;
      }
      ////ffffffffffffff
      @Override
      public void valueForPathChanged(TreePath treePath, Object newValue) {
        getSelectedPath(treePath).renameFile((String)newValue);
      }

      void nodesWereInserted(MyTreeNode node, int childIndex) {
        if(listenerList != null && node != null) {
          Object[] newChildren = new Object[]{node.children.get(childIndex)};
          fireTreeNodesInserted(this,
                                getPathToRoot(node), new int[]{childIndex},
                                newChildren);
        }
      }

      void nodesWereRemoved(MyTreeNode node, int childIndex,
                            MyTreeNode removedChild) {
        if(listenerList != null && node != null) {

          if(node != null) {
            fireTreeNodesRemoved(this, getPathToRoot(node),
                                 new int[]{childIndex},
                                 new Object[]{removedChild});
          }

          //Object[] newChildren = new Object[]{node.children.get(childIndex)};
          //fireTreeNodesDeleted;
        }
      }

      /**
       * Builds the parents of node up to and including the root node,
       * where the original node is the last element in the returned
       * array. The length of the returned array gives the node's depth in
       * the tree.
       *
       * @param aNode the TreeNode to get the path for
       * @return array of treenodes from the root to given MyTreeNode
       */
      MyTreeNode[] getPathToRoot(MyTreeNode aNode) {
        return getPathToRoot(aNode, 0);
      }

      /**
       * Builds the parents of node up to and including the root node,
       * where the original node is the last element in the returned
       * array. The length of the returned array gives the node's depth
       * in the tree.
       *
       * @param aNode  the TreeNode to get the path for
       * @param depth  an int giving the number of steps already taken towards
       *        the root (on recursive calls), used to size the returned array
       * @return an array of TreeNodes giving the path from the root to the
       *         specified node
       */
      MyTreeNode[] getPathToRoot(MyTreeNode aNode, int depth) {
        MyTreeNode[] retNodes;
        // This method recurses, traversing towards the root in order
        // size the array. On the way back, it fills in the nodes,
        // starting from the root and working back to the original node.

        /* Check for null, in case someone passed in a null node, or
           they passed in an element that isn't rooted at root. */
        if(aNode == null) {
          if(depth == 0)
            return null;
          else
            retNodes = new MyTreeNode[depth];
        }
        else {
          ++depth;
          if(aNode == treeRoot)
            retNodes = new MyTreeNode[depth];
          else
            retNodes = getPathToRoot(aNode.parent, depth);
          retNodes[retNodes.length - depth] = aNode;
        }
        return retNodes;
      }

      protected void fireTreeNodesInserted(Object source, Object[] path,
                                           int[] childIndices,
                                           Object[] children) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        TreeModelEvent e = null;
        // Process the listeners last to first, notifying those that
        // are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
          if (listeners[i]==TreeModelListener.class) {
            // Lazily create the event:
            if (e == null)
              e = new TreeModelEvent(source, path,
                                     childIndices, children);
            ((TreeModelListener)listeners[i+1]).treeNodesInserted(e);
          }
        }
      }

      protected void fireTreeNodesRemoved(Object source, Object[] path,
                                          int[] childIndices,
                                          Object[] children) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        TreeModelEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
          if (listeners[i]==TreeModelListener.class) {
            // Lazily create the event:
            if (e == null)
              e = new TreeModelEvent(source, path,
                                     childIndices, children);
            ((TreeModelListener)listeners[i+1]).treeNodesRemoved(e);
          }
        }
      }
    } // class MyTreeModel

    /**
     * MyTreeCellRenderer renders paths as root names and sets
     * appropriate colors for selections etc.
     */
    class MyTreeCellRenderer extends JLabel implements TreeCellRenderer {
      final static long serialVersionUID = 42;

      { setOpaque(true); } // needed to allow setting background color

      @Override
      public Component getTreeCellRendererComponent(JTree tree,
                                                    Object value,
                                                    boolean isSelected,
                                                    boolean expanded,
                                                    boolean leaf,
                                                    int row,
                                                    boolean hasFocus) {
        if(tree == null) return this;
        MyPath path = ((MyTreeNode)value).myPath;
        String name = path == null ? "null" : path.toString();
        setText(name);
        Color fg = tree.getForeground();
        Color bg = tree.getBackground();
        JTree.DropLocation dropLocation = tree.getDropLocation();
        if(dropLocation != null && dropLocation.getPath() != null
           && getSelectedPath(dropLocation.getPath()) == path) {
          fg = BLUE;
          bg = PINK;
          isSelected = true;
        } else if(isSelected) {
          fg = table.getSelectionForeground();
          bg = table.getSelectionBackground();
        }
        setForeground(fg);
        setBackground(bg);
        return this;
      }
    } // class MyTreeCellRenderer implements TreeCellRenderer
    MyTreeCellRenderer myTreeCellRenderer = new MyTreeCellRenderer();

    /**
     * This class handles drag &amp; drop on Trees. Files can be
     * dropped. For files only the file name without directory
     * information is retained. Dropping multiple files is not
     * supported.
     */
    class MyTreeTransferHandler extends TransferHandler {
      final static long serialVersionUID = 42;

      @Override
      public boolean canImport(TransferHandler.TransferSupport info) {
        // we only support drags and drops (not clipboard paste)
        if(!info.isDrop()) return false;
        info.setShowDropLocation(true);

        // we only import files
        if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) return true;
        if(info.isDataFlavorSupported(remotePathArrayFlavor)) return true;
        return false;
      }

      @Override
      public boolean importData(TransferHandler.TransferSupport info) {
        if(!canImport(info)) {
          return false;
        }
        Transferable transferable = info.getTransferable();
        JTree.DropLocation dl = (JTree.DropLocation)info.getDropLocation();
        TreePath treePath = dl.getPath();
        MyPath targetPath = getSelectedPath(treePath);
        ArrayList<MyPath> sourceFiles = new ArrayList<MyPath>();
        try {
          // get list of MyPath of files to move/copy
          if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
            List<?> files = (List<?>)transferable.getTransferData(DataFlavor.javaFileListFlavor);
            for(Object file : files) { ////ffffffffffffff
              String s = toUri(((File)file).toString()).toString();
              if(windows) s = s.replaceAll("%5C", "/");
              sourceFiles.add(new LocalPath(toUri(s))); /////////////////
              //sourceFiles.add(new LocalPath(toUri(((File)file).toString())));
            }
          } else if(info.isDataFlavorSupported(remotePathArrayFlavor)) {
            try {
              List<?> files = (List<?>)transferable.getTransferData(remotePathArrayFlavor);
              for(Object file : files) {
                RemotePath remotePath = (RemotePath)file;
                sourceFiles.add(remotePath);
              }
            } catch(Exception e) {
              System.err.println(e);
              e.printStackTrace();
              return false;
            }
          } else {
            System.err.println("Failed to import");
            return false;
          }

          // do the move/copy
          if(info.getDropAction() == COPY) {
            for(MyPath path : sourceFiles) {
              copyTree(path, targetPath.resolve(path.toString()), 1, null);
              //copyTree(path, targetPath);
            }
          } else if(info.getDropAction() == MOVE) {
            //targetPath.moveFilesFrom(sourceFiles);
            for(MyPath path : sourceFiles) {
              moveTree(path, targetPath.resolve(path.toString()));
              //moveTree(path, targetPath);
            }
          } else return false;
          return true; // success!
        } catch(UnsupportedFlavorException e) {
          System.err.println("Exception: UnsupportedFlavorException");
          return false;
        } catch(IOException e) {
          System.err.println("Exception: IOException");
          e.printStackTrace();
          return false;
        } catch(ClassCastException e) {
          System.err.println("Exception: ClassCastException");
          e.printStackTrace();
          return false;
        }
      }

      @Override
      public int getSourceActions(JComponent c) {
        return COPY_OR_MOVE;
      }

      @Override
      public Transferable createTransferable(JComponent c) {
        MyPath myPath = getSelectedPath(tree.getSelectionPath());
        DataFlavor myFileFlavor = myPath.getFlavor();
        return new Transferable() {
          @Override
          public DataFlavor[] getTransferDataFlavors() {
            return new DataFlavor[]{myFileFlavor, DataFlavor.stringFlavor};
          }

          @Override
          public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.equals(myFileFlavor) || flavor.equals(DataFlavor.stringFlavor);
          }

          @Override
          public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
            if(flavor.equals(myFileFlavor)) {
              if(flavor.equals(DataFlavor.javaFileListFlavor)) {
                ArrayList<File> files = new ArrayList<File>();
                files.add(((LocalPath)myPath).path.toFile());
                return files; ////ffffffffffffff
              }
              if(flavor.equals(remotePathArrayFlavor)) {
                ArrayList<RemotePath> files = new ArrayList<RemotePath>();
                files.add((RemotePath)myPath);
                return files;
              }
            }
            if(flavor.equals(DataFlavor.stringFlavor)) {
              int row = table.getSelectedRows()[0];
              return myPath.toString();
            }
            return null;
          }
        };
      }

      @Override
      public void exportDone(JComponent c, Transferable t, int i) {
      }
    } // class MyTreeTransferHandler extends TransferHandler

    /**
     * Initialize a FileBrowser to the root(s) of the file system
     */
    /**
     * This class is the main window. It contains a JSplitPane for the
     * files with surrounding ornaments.
     *
     * Displayed directories are "watched" for changes so that the
     * display accurately reflects the contents of the directory. The
     * selected tree node is watched as well as any expanded tree
     * node.
     *
     * Expanding a null node does a full expansion.
     * Expanding a non-null node does no recalculation.
     * Contracting a node sets children to null.
     * Selecting from the selection box only adds required node(s).
     * Double clicking a directory from the table only adds one directory to tree
     * Changing selected node recalculates table children.
     * Changing tree node adds or deletes child node and resets selected node.
     *
     * The file manipulation routines are:
     *
     * drag File array from tree
     *   only single directories can be dragged.
     * drag File array from table
     *   multiple directories and files can be dragged.
     * both move and copy are supported.
     *   With a move the files and directories are removed from the old
     *   directory and moved to the new directory.
     *   With a copy the files and directories are copied from the old
     *   directory to the new directory so two copies result.
     *
     * drop File array to tree
     *   The files and directories are put in the specified directory.
     * drop File array to table
     *   The files and directories are placed in the dropped-on directory
     *   or the containing directory if a file.
     * In all cases a collision of file/directory names results in a popup
     * asking if the operation should proceed.
     *
     * directory moves also check for directory move to child*
     *
     * edit File name in tree or table
     *   The renamed file or directory remains in the same containing
     *   directory. If there is a name collision the operation is aborted.
     * FIX TREE DIRECTORY RENAME - also check for directory move to child*
     *   Currently only CR causes rename otherwise editor is not closed.
     *     This might be good.
     *
     * If a directory replaces a directory of the same name then the
     * two directories are compared and the results are displayed in a
     * new Comparison window. The direction of transfer can be altered
     * or some files not transferred before the Transfer button is
     * pressed.
     *
     */
    Browser() {
      initBrowser();
      //tree.setSelectionPath(rootTreePath); ///////
      selectFromPath(root, true);
    } // Browser()

    /**
     * Initialize a FileBrowser to view the given path
     *
     * @param path the path to set the file browser to initially view
     */
    Browser(MyPath path) {
      initBrowser();
      selectFromPath(path, true); /////// should throw if unsuccessful
    } // Browser(MyPath path)

    /**
     * Initialize a 2-panel file browser (directory tree on the left
     * and file table on the right).
     */
    void initBrowser() {
      super.setLayout(new BorderLayout());
      super.setTitle("Files");

      addWindowListener(new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            // remove all file listeners before session(s) disconnect
            for(Map.Entry<WatchKey,TreePath> pair : treeKeys.entrySet()) {
              pair.getKey().cancel();
            }
            if(tableKey != null) {
              tableKey.cancel();
              tableKey = null;
              tablePath = null;
            }
            if(--windowCount == 0) {
              finish();
            }
            dispose();
          }
        });
      ++windowCount;
      try {
        treeWatcher = FileSystems.getDefault().newWatchService();
        tableWatcher = FileSystems.getDefault().newWatchService();
      } catch(IOException e) {
        e.printStackTrace();
      }
      setLayout(new BorderLayout());

      JMenuBar menuBar = new JMenuBar();
      menuBar.setLayout(new GridLayout(0, 1));

      JMenu menu1 = new JMenu();
      JMenuBar menuBar1 = new JMenuBar();
      menuBar1.add(menu1);
      menuBar.add(menuBar1);

      // back
      menuBar1.add(new JButton(" ") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  if(historyIndex < 0) return;
                  --historyIndex;
                  if(historyIndex < 0) {
                    tree.setSelectionPath(rootTreePath);
                  } else {
                    MyPath path = history.get(historyIndex);
                    currentDirectory.setText(path.fullName());
                    selectionBox.setSelectedItem(path);
                    selectFromPath(path, false);
                  }
                }
              });
            setToolTipText("Goto last directory");
          }

          protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int w = getWidth();
            int h = getHeight();
            // left facing arrow
            g.fillPolygon(new int[]{w/2+h/4, w/2-h/4, w/2+h/4},
                          new int[]{0, h/2, h}, 3);
          }
        });

      // up
      menuBar1.add(new JButton(" ") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  TreePath treePathChild = tree.getSelectionPath();
                  //System.out.println('*' + treePathChild.toString());
                  TreePath treePathParent = treePathChild.getParentPath(); ///////// null pointer exception
                  if(treePathChild != null && treePathParent != null) {
                    tree.setSelectionPath(treePathParent);
                  }
                }
              });
            setToolTipText("Goto parent directory");
          }

          protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int w = getWidth();
            int h = getHeight();
            // upward arrow
            g.fillPolygon(new int[]{w/2-h/2, w/2, w/2+h/2},
                          new int[]{h/2+h/4, h/2-h/4, h/2+h/4}, 3);
          }
        });

      // right
      menuBar1.add(new JButton(" ") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  if(history.size() > historyIndex + 1) {
                    ++historyIndex;
                    MyPath path = history.get(historyIndex);
                    currentDirectory.setText(path.fullName());
                    selectionBox.setSelectedItem(path);
                    selectFromPath(path, false);
                  }
                }
              });
            setToolTipText("Goto next directory");
          }

          protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int w = getWidth();
            int h = getHeight();
            // right facing arrow
            g.fillPolygon(new int[]{w/2-h/4, w/2+h/4, w/2-h/4},
                          new int[]{0, h/2, h}, 3);
          }
        });

      menuBar1.add(new JButton("Show dot files") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  showDotFiles = !showDotFiles;
                  if(showDotFiles) setText("Hide dot files");
                  else setText("Show dot files");
                  TreePath treePath = tree.getSelectionPath();
                  regenerateTable();
                }
              });
          }
        });

      menuBar1.add(new JButton("Tree View") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  listFiles = !listFiles;
                  if(listFiles) {
                    setText("Tree View");
                    splitLocation = splitPane.getDividerLocation();
                    splitPane.setDividerLocation(0);
                  } else {
                    setText("List View");
                    splitPane.setDividerLocation(splitLocation);
                  }
                }
              });
          }
        });

      menuBar1.add(new JButton("Clone") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  makeBrowser(getSelectedPath(tree.getSelectionPath()));
                }
              });
          }
        });

      JMenu menu2 = new JMenu();
      JMenuBar menuBar2 = new JMenuBar();
      menuBar2.add(menu2);
      menuBar.add(menuBar2);

      menuBar2.add(new JLabel("Directory: "));

      currentDirectory.setTransferHandler(new MyTextTransferHandler());

      menuBar2.add(selectionBox);

      menuBar2.add(new JButton("Go") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  try {
                    selectedPath = stringToMyPath(editor.getText());
                    while((!selectedPath.exists()
                           || !selectedPath.isDirectory())
                          && selectedPath != root) {
                      selectedPath = selectedPath.getParent();
                      if(selectedPath == null) {
                        selectedPath = root;
                        break;
                      }
                    }
                  } catch(NullPointerException ex) {
                    System.err.println("caught null pointer"); /////////
                    selectedPath = root;
                  }
                  editor.setText(selectedPath.fullName());
                  selectionBox.setSelectedItem(selectedPath);
                  selectFromPath((MyPath)selectionBox.getSelectedItem(), true);
                }
              });
          }
        });

      menuBar2.add(new JButton("Add") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  MyPath newItem = (MyPath)selectionBox.getSelectedItem();
                  lookup: {
                    for(int i = 0; i < selectionBox.getItemCount(); ++i) {
                      MyPath item = selectionBox.getItemAt(i);
                      if(item.equals(newItem)) break lookup;
                    }
                    selectionBox.addItem(newItem);
                    ArrayList<String> selections = options.get(selectionsKey);
                    selections.add(newItem.fullName());
                  }
                }
              });
          }
        });

      menuBar2.add(new JButton("Delete") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  MyPath newItem = (MyPath)selectionBox.getSelectedItem();
                  lookup: {
                    for(int i = 0; i < selectionBox.getItemCount(); ++i) {
                      MyPath item = selectionBox.getItemAt(i);
                      if(item.equals(newItem)) {
                        selectionBox.removeItemAt(i);
                        selectionBox.setSelectedItem(newItem);
                        ArrayList<String> selections = options.get(selectionsKey);
                        selections.remove(item.fullName());
                        break lookup;
                      }
                    }
                  }
                }
              });
          }
        });

      setJMenuBar(menuBar);

      JPanel spanel = new JPanel(new GridLayout(0,6));

      spanel.add(new JButton("Print Selected Path") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  int index[] = table.getSelectedRows();
                  if(index.length == 0) {
                    MyTreeNode p = (MyTreeNode)tree.getLastSelectedPathComponent();
                    if(p == null) return;
                    System.out.println(p.myPath.fullName());
                  } else {
                    for(int i : index) {
                      MyPath p = tableData.get(i).path;
                      System.out.println(p.fullName());
                    }
                  }
                }
              });
          }
        });

      spanel.add(new JButton("Print Tree") {
          final static long serialVersionUID = 42;

          void printTreeNodes(String indent, MyTreeNode node) {
            System.out.println(indent + node.myPath);
            if(node.children != null) {
              for(MyTreeNode child : node.children) {
                printTreeNodes(indent + "  ", child);
              }
            }
          }

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  System.out.println("-----------------");
                  printTreeNodes("", treeRoot);
                  System.out.println("-----------------");
                }
              });
          }
        });

      spanel.add(new JButton("Delete tree") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  int index[] = table.getSelectedRows();
                  if(index.length == 0) {
                    MyPath p = getSelectedPath(tree.getSelectionPath());
                    TreePath parentPath = tree.getSelectionPath().getParentPath();
                    tree.setSelectionPath(parentPath);
                    deleteTree(p, null);
                  } else {
                    for(int i : index) {
                      MyPath p = tableData.get(i).path;
                      deleteTree(p, null);
                    }
                  }
                  regenerateTable();
                }
              });
          }
        });

      spanel.add(new JButton("Create Link") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  selectedPath = stringToMyPath(editor.getText());
                  if(selectedPath.exists()) return;
                  selectedPath.makeLinkTo(new byte[]{'x','x','x'});
                  regenerateTable();
                }
              });
          }
        });

      spanel.add(new JButton("Touch File") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  selectedPath = stringToMyPath(editor.getText());
                  selectedPath.touch();
                  regenerateTable();
                }
              });
          }
        });

      spanel.add(new JButton("Create Directory") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  selectedPath = stringToMyPath(editor.getText());
                  selectedPath.makeDirectory();
                  regenerateTable();
                }
              });
          }
        });

      add(spanel, BorderLayout.SOUTH);

      // Initialize tree (in left side of JSplitPane)

      TreeSet<TableData> fileRoots = getRootPaths(true);
      ArrayList<MyPath> roots = new ArrayList<MyPath>();
      for(TableData data : fileRoots) {
        roots.add(data.path);
      }
      root.treeChildren = roots;
      tree = new JTree(myTreeModel);
      tree.setOpaque(true);
      tree.setCellRenderer(myTreeCellRenderer);
      tree.setDragEnabled(true);
      tree.setDropMode(DropMode.ON);
      tree.setTransferHandler(new MyTreeTransferHandler() {
          final static long serialVersionUID = 42;
        });
      tree.setEditable(true);
      tree.getSelectionModel().setSelectionMode
        (TreeSelectionModel.SINGLE_TREE_SELECTION);
      //tree.setInvokesStopCellEditing(true); //////////do we want this?
      tree.setShowsRootHandles(true);
      //tree.setRootVisible(false);
      tree.setRootVisible(true);
      //tree.setLargeModel(true);
      tree.addMouseListener(new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            if(e.getButton() == 3) {
              Point p = e.getPoint();
              TreePath tp = tree.getPathForLocation(p.x, p.y);
              if(tp == null) return;
              MyPath path = getSelectedPath(tp);
              if(path instanceof RemotePath) {
                String authority = ((RemotePath)path).uri.getRawAuthority();
                javax.swing.SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                      SSHWindow ssh = new SSHWindow(authority);
                      ssh.setLocationByPlatform(true);
                      ssh.pack();
                      ssh.setVisible(true);
                    }
                  });
              }
            }
          }
        });
      tree.addTreeWillExpandListener(new TreeWillExpandListener() {
          @Override
          public void treeWillExpand(TreeExpansionEvent event) {
            try {
              TreePath treePath = event.getPath();
              MyTreeNode parent = (MyTreeNode)treePath.getLastPathComponent();
              MyPath node = parent.myPath;
              if(parent.children == null) { // full expansion
                TreeSet<TableData> data = node.getChildren(showDotFiles);
                ArrayList<MyPath> treeChildren = new ArrayList<MyPath>();
                ArrayList<MyTreeNode> nodeChildren = new ArrayList<MyTreeNode>();
                for(TableData datum : data) {
                  if(datum.path.isDirectory()) {
                    treeChildren.add(datum.path);
                    nodeChildren.add(new MyTreeNode(datum.path, parent));
                  }
                  node.treeChildren = treeChildren;
                  parent.children = nodeChildren;
                  myTreeModel.reload(treePath);
                }
              }
              if(node instanceof LocalPath) {
                try {
                  WatchKey key = ((LocalPath)node).path.register(treeWatcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
                  treeKeys.put(key, treePath);
                } catch(IOException e) {
                  e.printStackTrace();
                }
              }
            } catch(Throwable e) {
              e.printStackTrace();
            }
          }

          @Override
          public void treeWillCollapse(TreeExpansionEvent event) {
            // remove watcher on node.getPath()
            MyTreeNode parent = ((MyTreeNode)event.getPath().getLastPathComponent());
            MyPath node = parent.myPath;
            Enumeration<TreePath> treePath = tree.getExpandedDescendants(event.getPath());
            ArrayList<TreePath> treePaths = new ArrayList<TreePath>();
            if(treePath != null) while(treePath.hasMoreElements()) {
                treePaths.add(treePath.nextElement());
              }
            Collections.sort(treePaths, new Comparator<TreePath>() {
                @Override
                public int compare(TreePath x,TreePath y) {
                  return y.getPathCount() - x.getPathCount();
                }
              });
            for(TreePath p : treePaths) {
              // remove key referring to p from treeKeys
              if(getSelectedPath(p) instanceof LocalPath) {
                WatchKey key = ((LocalPath)getSelectedPath(p)).key;
                if(key != null) key.cancel();
                treeKeys.remove(((LocalPath)getSelectedPath(p)).key);
              }
              if(tree.isExpanded(p) && !getSelectedPath(p).equals(node)) {
                tree.collapsePath(p);
                myTreeModel.reload(p);
              }
            }
            node.treeChildren = null;
            parent.children = null;
            myTreeModel.reload(event.getPath());
          }
        });
      tree.addTreeSelectionListener(new TreeSelectionListener() {
          @Override
          public void valueChanged(TreeSelectionEvent e) {
            TreePath treePath = tree.getSelectionPath();
            MyTreeNode node = (MyTreeNode)tree.getLastSelectedPathComponent();
            if(node == null) {
            } else {
              MyTreeNode myTreeNode = node;
              if(rootTreePath.equals(treePath)) {
                // remove table watcher
                if(tableKey != null) {
                  tableKey.cancel();
                  tableKey = null;
                  tablePath = null;
                }
              } else {
                // change table watcher if different
                if(!myTreeNode.equals(tablePath)) {
                  if(tableKey != null) tableKey.cancel();
                  tableKey = null;
                  tablePath = null;
                  /////// make polymorphic
                  while(!myTreeNode.myPath.exists()) {
                    myTreeNode = myTreeNode.parent;
                  }
                  if(myTreeNode.myPath instanceof LocalPath) {
                    try {
                      tableKey = ((LocalPath)myTreeNode.myPath).path.register(tableWatcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
                      tablePath = myTreeNode.myPath;
                    } catch(NoSuchFileException ex) {
                      System.err.println("No file here: " + myTreeNode.myPath);
                      ex.printStackTrace();
                      tablePath = myTreeNode.myPath;
                    } catch(IOException ex) {
                      System.err.println("Could not register path: " + myTreeNode.myPath);
                      ex.printStackTrace();
                    }
                  }
                  tablePath = myTreeNode.myPath;
                }
              }
              String s = myTreeNode.myPath.fullName();
              currentDirectory.setText(s);
              selectionBox.setSelectedItem(myTreeNode.myPath);
              //selectFromPath(myTreeNode.myPath, true);
              pushDirectoryInHistory(myTreeNode.myPath);
              regenerateTable();
            }
            tree.scrollPathToVisible(treePath);
            return;
          }
        });

      // Initialize table (on right side of JSplitPane)

      table = new JTable(myTableModel);
      table.setOpaque(true);
      table.setAutoCreateColumnsFromModel(true);
      table.setFillsViewportHeight(true);
      table.setRowSelectionAllowed(true);
      table.setColumnSelectionAllowed(false);
      table.setCellSelectionEnabled(false);
      table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
      table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
      table.setRowSelectionAllowed(true);
      table.setColumnSelectionAllowed(false);
      table.setDragEnabled(true);
      table.setDropMode(DropMode.ON);
      table.setDefaultRenderer(String.class, new MyTableCellRenderer());
      table.setTransferHandler(new MyTableTransferHandler());
      table.addMouseListener(new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            Point p = e.getPoint();
            int i = table.rowAtPoint(p);
            if(i < 0) return;
            if(e.getButton() == 1 && e.getClickCount() == 2) {
              if(tableData.get(i).path.isDirectory()) {
                MyPath myPath = tableData.get(i).path;
                selectFromPath(myPath, true);
              } else {
                // ?????? ///////////////// link?
              }
            } else if(e.getButton() == 3) {
              // check for directory
              MyPath path = tableData.get(i).path;
              if(path.isFile() || path.isLink()) {
                makeFileEditWindow(tableData.get(i).path);
              } else {
                if(path instanceof RemotePath) {
                  String authority = ((RemotePath)path).uri.getRawAuthority();
                  javax.swing.SwingUtilities.invokeLater(new Runnable() {
                      @Override
                      public void run() {
                        SSHWindow ssh = new SSHWindow(authority);
                        ssh.setLocationByPlatform(true);
                        ssh.pack();
                        ssh.setVisible(true);
                      }
                    });
                }
              }
            }
          }
        });
      //////////// set widths differently???
      for(int i = 0; i < tableColumnNames.size(); ++i) {
        TableColumn column = table.getColumnModel().getColumn(i);
        column.setPreferredWidth(100);
      }
      ((DefaultTableCellRenderer)table.getTableHeader().getDefaultRenderer())
        .setHorizontalAlignment(SwingConstants.LEFT);

      // table watcher
      new Thread() {
        {
          setDaemon(true);
        }

        @Override
        public void run() {
          for(;;) {
            WatchKey key;
            try {
              key = tableWatcher.take();
            } catch(InterruptedException x) {
              x.printStackTrace();
              return; //////////////
            }
            if(tablePath == null || !key.equals(tableKey)) {
              System.err.println("WatchKey not recognized: " + key);
              continue;
            }
            for(WatchEvent<?> event: key.pollEvents()) {
              WatchEvent.Kind<?> kind = event.kind();
              if(kind == OVERFLOW) {
                System.err.println("OVERFLOW");
                continue;
              }
              Path p = (Path)event.context();
              //System.out.println(p);
              if(kind == ENTRY_CREATE) {
                //System.out.println("table create: " + "   " + event.context()); // add path
              } else if(kind == ENTRY_DELETE) {
                //System.out.println("table delete: " + "   " + event.context()); // remove path
                // remove path from children
              } else if(kind == ENTRY_MODIFY) {
                //System.out.println("table modify: " + "   " + event.context()); // not used
                /////// ????????????????
              }
              javax.swing.SwingUtilities.invokeLater(new Runnable() {
                  @Override
                  public void run() {
                    regenerateTable();
                  }
                });
            }
            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset(); // to enable further events
            if(!valid) {
              key.cancel();
            }
          }
        }
      }.start();

      // tree watcher
      new Thread() {
        {
          setDaemon(true);
        }

        int getIndex(ArrayList<MyTreeNode> a, MyPath p) {
          for(int i = 0; i < a.size(); ++i) {
            if(a.get(i).myPath.equals(p)) return i;
          }
          return -1;
        }

        @Override
        public void run() {
          for(;;) {
            WatchKey key;
            try {
              key = treeWatcher.take();
            } catch(InterruptedException x) {
              System.err.println("InterruptedException in tree watcher");
              return;
            }
            TreePath treePath = treeKeys.get(key);
            MyTreeNode treeNode = ((MyTreeNode)treePath.getLastPathComponent());
            if(treeNode == null) {
              System.err.println("Tree WatchKey not recognized: " + key);
              continue;
            }
            for(WatchEvent<?> event: key.pollEvents()) {
              WatchEvent.Kind<?> kind = event.kind();
              if(kind == OVERFLOW) {
                System.err.println("OVERFLOW");
                continue;
              }
              Path p = (Path)event.context();
              MyPath child = treeNode.myPath.resolve(p);
              if(kind == ENTRY_CREATE) {
                javax.swing.SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                      if(getIndex(treeNode.children, child) != -1) return;
                      if(!child.isDirectory()) return;
                      if(treeNode.children == null) {
                        treeNode.children = new ArrayList<MyTreeNode>();
                      }
                      int lo = 0;
                      int hi = treeNode.children.size();
                      int c = 1;
                      int index;
                      found: {
                        while(lo < hi) {
                          int mid = (lo + hi) >>> 1;
                          c = pathComparator.compare(treeNode.children.get(mid).myPath,
                                                     child);
                          if(c == 0) {
                            index = mid;
                            break found;
                          }
                          if(c > 0) {
                            hi = mid;
                          } else {
                            lo = mid + 1;
                          }
                        } // end while
                        treeNode.children.add(lo, new MyTreeNode(child, treeNode));
                        index = lo;
                        myTreeModel.nodesWereInserted(treeNode, index);
                      } // break found goes after here
                    }
                  });
              } else if(kind == ENTRY_DELETE) {
                javax.swing.SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                      if(treeNode.children == null) return;
                      int index = getIndex(treeNode.children, child);
                      if(index < 0) return;
                      myTreeModel.nodesWereRemoved(treeNode, index, treeNode.children.get(index));
                      if(index >= 0) treeNode.children.remove(index);
                    }
                  });
              } else if(kind == ENTRY_MODIFY) {
                /////// ????????????????
              }
            }
            boolean valid = key.reset(); // to enable further events
            if(!valid) key.cancel();
          }
        }
      }.start();

      //Lay everything out.

      splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
                                 new JScrollPane(tree) {
                                   final static long serialVersionUID = 42;

                                   {
                                     setPreferredSize(new Dimension(200, 300));
                                   }
                                 },
                                 new JScrollPane(table) {
                                   final static long serialVersionUID = 42;

                                   {
                                     setPreferredSize(new Dimension(300, 300));
                                   }
                                 }) {
          final static long serialVersionUID = 42;
          {
            setOneTouchExpandable(false);
            setResizeWeight(.4);
            splitLocation = 200;
            setDividerLocation(0);
          }
        };

      add(splitPane, BorderLayout.CENTER);
    } // initBrowser
  } // class Browser extends JFrame

  /**
   * Make a FileBrowser on the indicated path.
   *
   * @param path a MyPath to make a FileBrowser on
   */
  static void makeBrowser(MyPath path) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          JFrame frame = new FileBrowser().new Browser(path);
          frame.setLocationByPlatform(true);
          //Display the window.
          frame.pack();
          frame.setVisible(true);
        }
      });
  } // static void makeBrowser(MyPath path)

  ///////////////////////////////////////////////////////////////
  // start of file comparison routines
  ///////////////////////////////////////////////////////////////

  /**
   * Gets the part of a base path after the base path.
   *
   * @param base MyPath of the base Path
   * @param path the full path whose base will be eliminated
   * @return the part of the path after the base path
   */
  static String getSuffix(MyPath base, MyPath path) {
    String b = base.fullName();
    String p = path.fullName();
    if(p.indexOf(b) != 0) throw new Error("Not prefix: " + b + "   " + p);
    if(b.charAt(b.length() - 1) == '/') return p.substring(b.length());
    return p.substring(b.length() + 1);
  } // static String getSuffix(MyPath base, MyPath path)

  /**
   * Replaces the prefix of a path with a new prefix. Removes the old
   * prefix from the path and replaces it with the new prefix.
   *
   * @param oldPrefix the prefix of the path to be removed
   * @param newPrefix the new prefix of the truncated path
   * @param path the path whose prefix is to be changed
   * @return the MyPath with the old prefix replaced by the new prefix
   */
  static MyPath changePrefix(MyPath oldPrefix, MyPath newPrefix, MyPath path) {
    String o = oldPrefix.fullName();
    String n = newPrefix.fullName();
    String s = path.fullName();
    String tail = getSuffix(oldPrefix, path);
    return newPrefix.resolve(tail);
  } // static MyPath changePrefix(MyPath oldPrefix, MyPath newPrefix, MyPath path)

  /**
   * An enum whose members indicate the possible operations between
   * pairs of files.
   */
  enum Direction {
    leftOverwriteNewer {
      Direction targetDefault() { return rightOverwriteOlder; }
      Direction targetLeft() { return leftOverwriteNewer; }
      Direction targetRight() { return rightOverwriteOlder; }
      Direction nextDirection() { return rightOverwriteOlder; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(MAGENTA);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return leftPath.copyFileFrom(rightPath);
      }
      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
        long time = rightPath.getMTime();
        if(time == 0) return false;
        return leftPath.setMTime(time);
      }
    },
    leftOverwriteOlder {
      Direction targetDefault() { return leftOverwriteOlder; }
      Direction targetLeft() { return leftOverwriteOlder; }
      Direction targetRight() { return rightOverwriteNewer; }
      Direction nextDirection() { return rightOverwriteNewer; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(RED);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return true; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return leftPath.copyFileFrom(rightPath);
      }
      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
        long time = rightPath.getMTime();
        if(time == 0) return false;
        return leftPath.setMTime(time);
      }
    },
    leftCreateFile {
      Direction targetDefault() { return leftCreateFile; }
      Direction targetLeft() { return leftCreateFile; }
      Direction targetRight() { return rightDeleteFile; }
      Direction nextDirection() { return rightDeleteFile; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(GREEN);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return true; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return leftPath.copyFileFrom(rightPath);
      }
    },
    leftCreateDirectory {
      Direction targetDefault() { return leftCreateDirectory; }
      Direction targetLeft() { return leftCreateDirectory; }
      Direction targetRight() { return rightDeleteDirectory; }
      Direction nextDirection() { return rightDeleteDirectory; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(GREEN);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -1*q+x,-1*q+x,-3*q+x,-1*q+x,-1*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
                    2*q+y,1*q+y,1*q+y},
          11);
      }
      boolean defaultCheck() { return true; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return copyTree(rightPath, leftPath, 0, message);
      }
    },
    leftDeleteDirectory {
      Direction targetDefault() { return rightCreateDirectory; }
      Direction targetLeft() { return leftDeleteDirectory; }
      Direction targetRight() { return rightCreateDirectory; }
      Direction nextDirection() { return rightCreateDirectory; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(RED);
        g.fillPolygon(new int[]{3*q+x,2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,
                                -3*q+x,-4*q+x,-3*q+x,-4*q+x,-3*q+x,
                                -2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,3*q+x},
          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
                    2*q+y,1*q+y,0*q+y,-1*q+y,-2*q+y,
                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
          17);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return deleteTree(leftPath, message);
      }
    },
    leftDeleteFile {
      Direction targetDefault() { return rightCreateFile; }
      Direction targetLeft() { return leftDeleteFile; }
      Direction targetRight() { return rightCreateFile; }
      Direction nextDirection() { return rightCreateFile; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(RED);
        g.fillPolygon(new int[]{3*q+x,2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,
                                -1*q+x,
                                -2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,3*q+x},
          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
                    0*q+y,
                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
          13);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return deleteTree(leftPath, message);
      }
    },
    rightDirectoryOverwriteLeftFile {
      Direction targetDefault() { return rightDirectoryOverwriteLeftFile; }
      Direction targetLeft() { return rightDirectoryOverwriteLeftFile; }
      Direction targetRight() { return leftFileOverwriteRightDirectory; }
      Direction nextDirection() { return leftFileOverwriteRightDirectory; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BROWN);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-2*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,2*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return copyTree(rightPath, leftPath, replaceFileWithDirectory, null);
      }
    },
    rightFileOverwriteLeftDirectory {
      Direction targetDefault() { return leftDirectoryOverwriteRightFile; }
      Direction targetLeft() { return rightFileOverwriteLeftDirectory; }
      Direction targetRight() { return leftDirectoryOverwriteRightFile; }
      Direction nextDirection() { return leftDirectoryOverwriteRightFile; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BROWN);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{0*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,0*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return copyTree(rightPath, leftPath, replaceDirectoryWithFile, null);
      }
    },
    leftCreateLink {
      Direction targetDefault() { return leftCreateLink; }
      Direction targetLeft() { return leftCreateLink; }
      Direction targetRight() { return rightDeleteLink; }
      Direction nextDirection() { return rightDeleteLink; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLUE);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        byte[] target = rightPath.readLink();
        if(target == null) return false;
        return leftPath.makeLinkTo(target);
      }
    },
    leftDeleteLink {
      Direction targetDefault() { return rightCreateLink; }
      Direction targetLeft() { return leftDeleteLink; }
      Direction targetRight() { return rightCreateLink; }
      Direction nextDirection() { return rightCreateLink; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLUE);
        g.fillPolygon(new int[]{3*q+x,2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,
                                -1*q+x,
                                -2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,3*q+x},
          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
                    0*q+y,
                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
          13);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return deleteTree(leftPath, message);
      }
    },
    rightLinkOverwriteLeft {
      Direction targetDefault() { return leftOverwriteRightLink; }
      Direction targetLeft() { return rightLinkOverwriteLeft; }
      Direction targetRight() { return leftOverwriteRightLink; }
      Direction nextDirection() { return leftOverwriteRightLink; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLACK);

        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -2*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        if(!deleteTree(leftPath, message)) return false;
        byte[] target = rightPath.readLink();
        if(target == null) return false;
        return leftPath.makeLinkTo(target);
      }
    },
    rightOverwriteLeftLink {
      Direction targetDefault() { return rightOverwriteLeftLink; }
      Direction targetLeft() { return rightOverwriteLeftLink; }
      Direction targetRight() { return leftLinkOverwriteRight; }
      Direction nextDirection() { return leftLinkOverwriteRight; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLACK);
        g.fillPolygon(new int[]{2*q+x,0*q+x,0*q+x,
                                -1*q+x,-1*q+x,-3*q+x,-1*q+x,-1*q+x,
                                0*q+x,0*q+x,2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
                    2*q+y,1*q+y,1*q+y},
          11);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        deleteTree(leftPath, message);
        return copyTree(rightPath, leftPath, 0, message);
      }
    },
    rightOverwriteNewer {
      Direction targetDefault() { return leftOverwriteOlder; }
      Direction targetLeft() { return leftOverwriteOlder; }
      Direction targetRight() { return rightOverwriteNewer; }
      Direction nextDirection() { return leftOverwriteOlder; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(MAGENTA);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return rightPath.copyFileFrom(leftPath);
      }
      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
        long time = leftPath.getMTime();
        if(time == 0) return false;
        return rightPath.setMTime(time);
      }
    },
    rightOverwriteOlder {
      Direction targetDefault() { return rightOverwriteOlder; }
      Direction targetLeft() { return leftOverwriteNewer; }
      Direction targetRight() { return rightOverwriteOlder; }
      Direction nextDirection() { return leftOverwriteNewer; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(RED);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return true; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return rightPath.copyFileFrom(leftPath);
      }
      boolean doTime(MyPath leftPath, MyPath rightPath, JLabel message) {
        long time = leftPath.getMTime();
        if(time == 0) return false;
        return rightPath.setMTime(time);
      }
    },
    rightCreateFile {
      Direction targetDefault() { return rightCreateFile; }
      Direction targetLeft() { return leftDeleteFile; }
      Direction targetRight() { return rightCreateFile; }
      Direction nextDirection() { return leftDeleteFile; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(GREEN);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return true; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return rightPath.copyFileFrom(leftPath);
      }
    },
    rightCreateDirectory {
      Direction targetDefault() { return rightCreateDirectory; }
      Direction targetLeft() { return leftDeleteDirectory; }
      Direction targetRight() { return rightCreateDirectory; }
      Direction nextDirection() { return leftDeleteDirectory; }
      void icon(Graphics g, int columnWidth, int rowHeight) {

        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(GREEN);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                1*q+x,1*q+x,3*q+x,1*q+x,1*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
                    2*q+y,1*q+y,1*q+y},
          11);
      }
      boolean defaultCheck() { return true; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return copyTree(leftPath, rightPath, 0, message);
      }
    },
    rightDeleteDirectory {
      Direction targetDefault() { return leftCreateDirectory; }
      Direction targetLeft() { return leftCreateDirectory; }
      Direction targetRight() { return rightDeleteDirectory; }
      Direction nextDirection() { return leftCreateDirectory; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(RED);
        g.fillPolygon(new int[]{-3*q+x,-2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,
                                3*q+x,4*q+x,3*q+x,4*q+x,3*q+x,
                                2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,-3*q+x},
          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
                    2*q+y,1*q+y,0*q+y,-1*q+y,-2*q+y,
                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
          17);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return deleteTree(rightPath, message);
      }
    },
    rightDeleteFile {
      Direction targetDefault() { return leftCreateFile; }
      Direction targetLeft() { return leftCreateFile; }
      Direction targetRight() { return rightDeleteFile; }
      Direction nextDirection() { return leftCreateFile; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(RED);
        g.fillPolygon(new int[]{-3*q+x,-2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,
                                1*q+x,
                                2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,-3*q+x},
          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
                    0*q+y,
                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
          13);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return deleteTree(rightPath, message);
      }
    },
    leftDirectoryOverwriteRightFile {
      Direction targetDefault() { return leftDirectoryOverwriteRightFile; }
      Direction targetLeft() { return rightFileOverwriteLeftDirectory; }
      Direction targetRight() { return leftDirectoryOverwriteRightFile; }
      Direction nextDirection() { return rightFileOverwriteLeftDirectory; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BROWN);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-2*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,2*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return copyTree(leftPath, rightPath, replaceFileWithDirectory, null);
      }
    },
    leftFileOverwriteRightDirectory {
      Direction targetDefault() { return rightDirectoryOverwriteLeftFile; }
      Direction targetLeft() { return rightDirectoryOverwriteLeftFile; }
      Direction targetRight() { return leftFileOverwriteRightDirectory; }
      Direction nextDirection() { return rightDirectoryOverwriteLeftFile; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BROWN);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{0*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,0*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return copyTree(leftPath, rightPath, replaceDirectoryWithFile, message);
      }
    },
    rightCreateLink {
      Direction targetDefault() { return rightCreateLink; }
      Direction targetLeft() { return leftDeleteLink; }
      Direction targetRight() { return rightCreateLink; }
      Direction nextDirection() { return leftDeleteLink; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLUE);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        byte[] target = leftPath.readLink();
        if(target == null) return false;
        return rightPath.makeLinkTo(target);
      }
    },
    rightDeleteLink {
      Direction targetDefault() { return leftCreateLink; }
      Direction targetLeft() { return leftCreateLink; }
      Direction targetRight() { return rightDeleteLink; }
      Direction nextDirection() { return leftCreateLink; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLUE);
        g.fillPolygon(new int[]{-3*q+x,-2*q+x,-1*q+x,0*q+x,1*q+x,2*q+x,
                                1*q+x,
                                2*q+x,1*q+x,0*q+x,-1*q+x,-2*q+x,-3*q+x},
          new int[]{1*q+y,1*q+y,2*q+y,1*q+y,2*q+y,1*q+y,
                    0*q+y,
                    -1*q+y,-2*q+y,-1*q+y,-2*q+y,-1*q+y,-1*q+y},
          13);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        return deleteTree(rightPath, message);
      }
    },
    leftLinkOverwriteRight {
      Direction targetDefault() { return rightOverwriteLeftLink; }
      Direction targetLeft() { return rightOverwriteLeftLink; }
      Direction targetRight() { return leftLinkOverwriteRight; }
      Direction nextDirection() { return rightOverwriteLeftLink; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLACK);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                2*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    0*q+y,
                    2*q+y,1*q+y,1*q+y},
          7);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        if(!deleteTree(rightPath, message)) return false;
        byte[] target = leftPath.readLink();
        if(target == null) return false;
        return rightPath.makeLinkTo(target);
      }
    },
    leftOverwriteRightLink {
      Direction targetDefault() { return leftOverwriteRightLink; }
      Direction targetLeft() { return rightLinkOverwriteLeft; }
      Direction targetRight() { return leftOverwriteRightLink; }
      Direction nextDirection() { return rightLinkOverwriteLeft; }
      void icon(Graphics g, int columnWidth, int rowHeight) {
        int w = columnWidth;
        int h = rowHeight;
        int q = h/4;
        int x = w/2;
        int y = h/2;
        g.setColor(BLACK);
        g.fillPolygon(new int[]{-2*q+x,0*q+x,0*q+x,
                                1*q+x,1*q+x,3*q+x,1*q+x,1*q+x,
                                0*q+x,0*q+x,-2*q+x},
          new int[]{-1*q+y,-1*q+y,-2*q+y,
                    -1*q+y,-2*q+y,0*q+y,2*q+y,1*q+y,
                    2*q+y,1*q+y,1*q+y},
          11);
      }
      boolean defaultCheck() { return false; }
      boolean doOperation(MyPath leftPath, MyPath rightPath, JLabel message) {
        deleteTree(rightPath, message);
        return copyTree(leftPath, rightPath, 0, message);
      }
    };

    /**
     * Returns the default Direction for this enum entry.
     *
     * @return the default Direction for this enum entry
     */
    abstract Direction targetDefault();

    /**
     * Returns the left target for this enum entry.
     *
     * @return the left target for this enum entry
     */
    abstract Direction targetLeft();

    /**
     * Returns the right target for this enum entry.
     *
     * @return the right target for this enum entry
     */
    abstract Direction targetRight();

    /**
     * Returns the opposite target for this enum entry.
     *
     * @return the opposite target for this enum entry
     */
    abstract Direction nextDirection();

    /**
     * Draws an icon indicating the operation for this enum entry.
     *
     * @param g the graphics context
     * @param columnWidth the width of the icon to draw
     * @param rowHeight the height of the icon to draw
     */
    abstract void icon(Graphics g, int columnWidth, int rowHeight);

    /**
     * Returns true if this Direction should be checked by default.
     *
     * @return true if this Direction should be checked by default
     */
    abstract boolean defaultCheck();

    /**
     * Perform the operation indicated by the icon.
     *
     * @param leftPath the left path for the operation or null
     * @param rightPath the right path for the operation or null
     * @param message a JLabel used for feedback
     * @return true if successful
     */
    abstract boolean doOperation(MyPath leftPath, MyPath rightPath,
                                 JLabel message);
    /**
     * Only update file times. Both sides must be files or nothing
     * happens and false is returned. Returns false unless overridden.
     *
     * @param leftPath the left path for the operation or null
     * @param rightPath the right path for the operation or null
     * @param message a JLabel used for feedback
     * @return true if successful
     */
    boolean doTime(MyPath leftPath, MyPath rightPath,
                   JLabel message) {
      return false;
    }
  } // enum Direction

  Direction[] d = Direction.values(); ////////// maybe not here

  /**
   * This class holds data for a line of the file comparison
   * table. It is a pure data structure.
   */
  static class CompareTableData {
    boolean check;
    TableData left;
    Direction direction; // current direction of transfer etc.
    TableData right;

    CompareTableData(TableData left, Direction direction, TableData right) {
      this.check = direction.defaultCheck();
      this.left = left;
      this.direction = direction;
      this.right = right;
    }

    @Override
    public String toString() {
      return String.valueOf(left) + " " + direction + " " + String.valueOf(right);
    }
  } // static class CompareTableData

  /**
   * Table of updates to perform. There are two ComboBoxes: the left
   * ComboBox and the right ComboBox. There is one table giving file
   * specs for both sides of transfer. Center column gives direction
   * of transfer. Far left column is check box for disabling transfer
   * or deletion.
   */
  class PathCompare extends JFrame {
    final static long serialVersionUID = 42;

    MyPath left;
    MyPath right;
    ArrayList<CompareTableData> compareData = new ArrayList<CompareTableData>();
    JTable compareTable = new JTable();
    JLabel[] countLabels = new JLabel[Direction.values().length];
    JLabel[] countTotals = new JLabel[4];
    JLabel message;
    JButton refresh;
    JButton synchronize;
    boolean localXPDSTHack;
    /**
     * Compare modification times and return true if less than 2 seconds
     * different. This is to compensate for various precisions in time
     * stamps between opreating systems. If the windowsXPDSTHack is true
     * then file times plus or minus 1 hour plus or minus 2 seconds are
     * assumed to be equal (only set this flag on windows XP (and pray).
     *
     * @param x first file to compare
     * @param y second file to compare
     * @return true if the modification times are within 2 seconds of each other
     */
    int dateCompare(TableData x, TableData y) {
      long xtime = x.mtime;
      long ytime = y.mtime;
      long difference = Math.abs(xtime - ytime);
      if(difference < 2000) return 0;
      if(localXPDSTHack) {
        long hourDifference = Math.abs(difference - 3600000);
        if(hourDifference < 2000 && x.size == y.size) return 0;
      }
      return xtime - ytime < 0 ? -1 : 1;
    } // int dateCompare(TableData x, TableData y)

    /**
     * Make table of differences between two directory trees. Caller
     * must remember roots. First two arguments must be
     * directories. The table should be empty initially and will be
     * filled in with differences. Note that file times on links are
     * unreliable.
     *
     * Must sort files/directories/links by name only ignoring
     * file/directory/link property.
     *
     * @param leftPath left-side directory
     * @param rightPath right-side directory
     * @param table resulting table of differences
     */
    void compareDirectories(MyPath leftPath, MyPath rightPath,
                            ArrayList<CompareTableData> table) {
      SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            message.setText(leftPath.fullName());
          }
        });
      // do name sort
      TreeSet<TableData> leftSet = new TreeSet<TableData>(dataNameComparator);
      leftSet.addAll(leftPath.getChildren(true));
      TreeSet<TableData> rightSet = new TreeSet<TableData>(dataNameComparator);
      rightSet.addAll(rightPath.getChildren(true));
      Iterator<TableData> leftFiles = leftSet.iterator();
      Iterator<TableData> rightFiles = rightSet.iterator();
      TableData leftTable = leftFiles.hasNext() ? leftFiles.next() : null;
      TableData rightTable = rightFiles.hasNext() ? rightFiles.next() : null;
      while(leftTable != null || rightTable != null) {
        int c = leftTable == null ? 1 : rightTable == null ? -1
          : dataNameComparator.compare(leftTable, rightTable);
        if(c < 0) {
          if(leftTable.path.isDirectory()) {
            table.add(new CompareTableData(leftTable,
                                           Direction.rightCreateDirectory,
                                           null));
          } else if(leftTable.path.isFile()) {
            table.add(new CompareTableData(leftTable,
                                           Direction.rightCreateFile, null));
          } else if(leftTable.path.isLink()) {
            table.add(new CompareTableData(leftTable,
                                           Direction.rightCreateLink,
                                           null));

          } else {
            System.err.println("Unknown file type: " + leftTable.path.uri.toString());
          }
          leftTable = leftFiles.hasNext() ? leftFiles.next() : null;
        } else if(c > 0) {
          if(rightTable.path.isDirectory()) {
            table.add(new CompareTableData(null,
                                           Direction.leftCreateDirectory,
                                           rightTable));
          } else if(rightTable.path.isFile()) {
            table.add(new CompareTableData(null,
                                           Direction.leftCreateFile,
                                           rightTable));
          } else if(rightTable.path.isLink()) {
            table.add(new CompareTableData(null,
                                           Direction.leftCreateLink,
                                           rightTable));
          } else {
            System.err.println("Unknown file type: " + rightTable.path.uri.toString());
          }
          rightTable = rightFiles.hasNext() ? rightFiles.next() : null;
        } else {
          if(leftTable.path.isFile()) {
            if(rightTable.path.isFile()) {
              int comp = dateCompare(leftTable, rightTable);
              if(comp != 0) {
                if(comp > 0) {
                  table.add(new CompareTableData(leftTable,
                                                 Direction.rightOverwriteOlder,
                                                 rightTable));
                } else {
                  table.add(new CompareTableData(leftTable,
                                                 Direction.leftOverwriteOlder,
                                                 rightTable));
                }
              }
            } else if(rightTable.path.isDirectory()) {
              table.add(new CompareTableData(leftTable,
                                             Direction.rightDirectoryOverwriteLeftFile,
                                             rightTable));
            } else if(rightTable.path.isLink()) {
              table.add(new CompareTableData(leftTable,
                                             Direction.leftOverwriteRightLink,
                                             rightTable));
            } else {
              System.err.println("Unknown file type: " + rightTable.path.uri.toString());
            }
          } else if(leftTable.path.isDirectory()) {
            if(rightTable.path.isFile()) {
              table.add(new CompareTableData(leftTable,
                                             Direction.leftDirectoryOverwriteRightFile,
                                             rightTable));
            } else if(rightTable.path.isDirectory()) {
              compareDirectories(leftTable.path,
                                 rightTable.path,
                                 table);
            } else if(rightTable.path.isLink()) {
              table.add(new CompareTableData(leftTable,
                                             Direction.leftOverwriteRightLink,
                                             rightTable));

            } else {
              System.err.println("Unknown file type: " + rightTable.path.uri.toString());
            }
          } else if(leftTable.path.isLink()) {
            if(rightTable.path.isFile()) {
              table.add(new CompareTableData(leftTable,
                                             Direction.rightOverwriteLeftLink,
                                             rightTable));
            } else if(rightTable.path.isDirectory()) {
              table.add(new CompareTableData(leftTable,
                                             Direction.rightOverwriteLeftLink,
                                             rightTable));
            } else if(rightTable.path.isLink()) {
              byte[] leftTarget = leftTable.path.readLink();
              byte[] rightTarget = rightTable.path.readLink();
              if(!Arrays.equals(leftTarget, rightTarget)) {
                int comp = dateCompare(leftTable, rightTable);
                if(comp != 0) {
                  if(comp > 0) {
                    table.add(new CompareTableData(leftTable,
                                                   Direction.rightOverwriteLeftLink,
                                                   rightTable));
                  } else {
                    table.add(new CompareTableData(leftTable,
                                                   Direction.leftOverwriteRightLink,
                                                   rightTable));
                  }
                }
              }
            } else {
              System.err.println("Unknown file type: " + rightTable.path.uri.toString());
            }
          } else {
            System.err.println("Unknown file type: " + leftTable.path.uri.toString());
          }
          leftTable = leftFiles.hasNext() ? leftFiles.next() : null;
          rightTable = rightFiles.hasNext() ? rightFiles.next() : null;
        }
      }
    } // void compareDirectories(MyPath leftPath, MyPath rightPath,
      //                         ArrayList<CompareTableData> table)

    class ComboBoxPanel extends JPanel {
      final static long serialVersionUID = 42;

      ComboBoxPanel(MyComboBox comboBox) {
        setLayout(new BorderLayout());
        add(new JButton(" ") {
            final static long serialVersionUID = 42;

            {
              addMouseListener(new MouseAdapter() {
                  @Override
                  public void mouseClicked(MouseEvent e) {
                    MyPath child = comboBox.selectedPath;
                    MyPath parent = child.getParent();
                    if(child != null && parent != null) {
                      comboBox.getModel().setSelectedItem(parent);
                    }
                  }
                });
              setToolTipText("Goto parent directory");
            }

            protected void paintComponent(Graphics g) {
              super.paintComponent(g);
              int w = getWidth();
              int h = getHeight();
              // upward arrow
              g.fillPolygon(new int[]{w/2-h/2, w/2, w/2+h/2},
                            new int[]{h/2+h/4, h/2-h/4, h/2+h/4}, 3);
            }
          }, BorderLayout.WEST);
        add(comboBox, BorderLayout.CENTER);
      } // ComboBoxPanel(MyComboBox comboBox)
    } // class ComboBoxPanel extends JPanel

    ///////////////////////////////////////////////////////////////
    /*
     * Here is where the JTable goes.
     */
    ///////////////////////////////////////////////////////////////

    static final int checkCol = 0;
    static final int directionCol = 4;

    class CompareTableModel extends AbstractTableModel {
      final static long serialVersionUID = 42;

      String tableColumnNames[] = {"x", "path", "left size", "left date", "icon", "right size", "right date"};

      @Override
      public int getColumnCount() {
        return tableColumnNames.length;
      }

      @Override
      public int getRowCount() {
        return compareData.size();
      }

      @Override
      public String getColumnName(int col) {
        return tableColumnNames[col];
      }

      /**
       * Method used to get data to display after
       * getTableCellRendererComponent converts it for displaying.
       */
      @Override
      public Object getValueAt(int row, int col) {
        CompareTableData data = compareData.get(row);
        if(col == checkCol) return data.check;
        return null;
      }

      /*
       * JTable uses this method to determine the default renderer/
       * editor for each cell. If we didn't implement this method then
       * the editor might not be a String editor.
       *
       * This is correct even though the cells contain Paths. It is
       * fixed in setValueAt
       */
      @Override
      public Class<?> getColumnClass(int c) {
        if(c == checkCol) return Boolean.class;
        if(c == directionCol) return Direction.class;
        return String.class; // all other columns rendered as strings
      }

      /**
       * Disable the default editor if not the first column or the cell
       * contains a Direction.
       */
      @Override
      public boolean isCellEditable(int row, int col) {
        return col == checkCol || col == directionCol;
      }

      /**
       * Apparently only used if cell is edited. We are using Strings as
       * the type of the table cell so must convert to Path and check
       * for equality. Must also check for duplicating another file in
       * the same directory.
       *
       * @param value new (edited) value of cell
       * @param row row of edited cell
       * @param col column of edited cell
       */
      @Override
      public void setValueAt(Object value, int row, int col) {
        if(col == checkCol) {
          boolean current = compareData.get(row).check;
          if(!value.equals(current)) {
            compareData.get(row).check = (Boolean)value;
            updateCounts();
            fireTableCellUpdated(row, col);
          }
        } else if(col == directionCol) {
          compareData.get(row).direction = (Direction)value;
          fireTableCellUpdated(row, col);
        }
      }
    } // class CompareTableModel extends AbstractTableModel

    CompareTableModel compareTableModel = new CompareTableModel();

    PathCompare(String title, MyPath left, MyPath right) {
      super(title);
      this.left = left;
      this.right = right;
      initPathCompare(left, right);
    } // PathCompare(String title, MyPath left, MyPath right)

    void updateCounts() {
      int[] checked = new int[Direction.values().length];
      int[] allValues = new int[Direction.values().length];
      int topChecked = 0;
      int bottomChecked = 0;
      int topAll = 0;
      int bottomAll = 0;
      for(CompareTableData data : compareData) {
        if(data.check) {
          ++checked[data.direction.ordinal()];
          if(data.direction.ordinal() < Direction.values().length/2) {
            ++topChecked;
          } else {
            ++bottomChecked;
          }
        }
        ++allValues[data.direction.ordinal()];
        if(data.direction.ordinal() < Direction.values().length/2) {
          ++topAll;
        } else {
          ++bottomAll;
        }
      }
      for(int i = 0; i < countLabels.length; ++i) {
        countLabels[i].setText(" " + addCommas("" + checked[i])
                               + '/' + addCommas("" + allValues[i]));
      }
      for(int i = 0; i < 4; ++i) {
        countTotals[i].setText("" + i);
      }
      countTotals[0].setText("Totals");
      countTotals[1].setText(" " + addCommas("" + topChecked)
                               + '/' + addCommas("" + topAll));
      countTotals[2].setText(" " + addCommas("" + (topChecked + bottomChecked))
                             + '/' + addCommas("" + (topAll + bottomAll)));
      countTotals[3].setText(" " + addCommas("" + bottomChecked)
                               + '/' + addCommas("" + bottomAll));
    } // void updateCounts()

    void initPathCompare(MyPath leftPath, MyPath rightPath) {
      super.setTitle(leftPath.fullName() + "   " + rightPath.fullName());

      MyComboBox leftBox = new MyComboBox(leftPath);
      MyComboBox rightBox = new MyComboBox(rightPath);
      JPanel comboPanel = new JPanel(new SpringLayout());
      ComboBoxPanel leftPanel = new ComboBoxPanel(leftBox);
      comboPanel.add(leftPanel);
      refresh = new JButton("refresh") {
          final static long serialVersionUID = 42;
          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  left = endEditing(leftBox);
                  right = endEditing(rightBox);
                  int buttonNumber = e.getButton();
                  switch(e.getButton()) {
                  case 1:
                    localXPDSTHack = windowsXPDSTHack;
                    break;
                  case 3:
                    localXPDSTHack = false;
                    break;
                  default: return;
                  }
                  compareData.clear();
                  Color background = getBackground();
                  Color synBackground = synchronize.getBackground();
                  if(background.equals(RED) ||
                     synBackground.equals(RED)) {
                    System.err.println("BUSY");
                    return;
                  }
                  setBackground(RED);
                  new Thread("refresh") {
                    @Override
                    public void run() {
                      compareDirectories(left, right, compareData);
                      SwingUtilities.invokeLater(new Runnable() {
                          @Override
                          public void run() {
                            updateCounts();
                            compareTableModel.fireTableDataChanged();
                            setBackground(background);
                          }
                        });
                    }
                  }.start();
                }
              });
          }
        };
      comboPanel.add(refresh);
      ComboBoxPanel rightPanel = new ComboBoxPanel(rightBox);
      comboPanel.add(rightPanel);
      SpringLayout layout = (SpringLayout)comboPanel.getLayout();
      SpringLayout.Constraints panelC = layout.getConstraints(comboPanel);
      SpringLayout.Constraints leftC = layout.getConstraints(leftPanel);
      SpringLayout.Constraints centerC = layout.getConstraints(refresh);
      SpringLayout.Constraints rightC = layout.getConstraints(rightPanel);
      leftC.setWidth(Spring.constant(10, 100, 1000));
      rightC.setWidth(Spring.constant(10, 100, 1000));
      Spring height = Spring.max(leftC.getHeight(), centerC.getHeight());
      height = Spring.max(height, rightC.getHeight());
      layout.putConstraint("North", leftPanel, 0, "North", refresh);
      layout.putConstraint("North", refresh, 0, "North", comboPanel);
      layout.putConstraint("North", rightPanel, 0, "North", refresh);
      layout.putConstraint("South", comboPanel, 0, "South", refresh);
      layout.putConstraint("West", leftPanel, 0, "West", comboPanel);
      layout.putConstraint("West", refresh, 0, "East", leftPanel);
      layout.putConstraint("West", rightPanel, 0, "East", refresh);
      layout.putConstraint("East", comboPanel, 0, "East", rightPanel);
      add(comboPanel, BorderLayout.NORTH);
      compareTable.setModel(compareTableModel);
      compareTable.setOpaque(true);
      compareTable.setAutoCreateColumnsFromModel(true);
      compareTable.setFillsViewportHeight(true);
      compareTable.setRowSelectionAllowed(true);
      compareTable.setColumnSelectionAllowed(false);
      compareTable.setCellSelectionEnabled(false);
      compareTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
      compareTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
      compareTable.setRowSelectionAllowed(true);
      compareTable.setColumnSelectionAllowed(false);
      compareTable.setDragEnabled(true);
      compareTable.setDropMode(DropMode.ON);

      // for Direction column
      compareTable.setDefaultRenderer(Direction.class, new DefaultTableCellRenderer() {
        final static long serialVersionUID = 42;

        Direction direction;
        //int column;

        @Override
        public Component getTableCellRendererComponent(JTable table,
                                                       Object direction,
                                                       boolean isSelected,
                                                       boolean hasFocus,
                                                       int row, int column) {
          // don't call super to prevent selection from coloring background
          // super.getTableCellRendererComponent(table, direction,
          //                                     isSelected, hasFocus,
          //                                     row, column);
          this.direction = compareData.get(row).direction;
          //this.column = column;
          CompareTableData data = compareData.get(row);
          setToolTipText(null); ////////////////
          return this;
        }

        @Override
        public void paint(Graphics g) {
          //super.paint(g); // enable this and above call to super to show text
          direction.icon(g,
                         compareTable.getColumnModel().getColumn(directionCol).getWidth(),
                         compareTable.getRowHeight() - 1);
        }
      });
      compareTable.setDefaultRenderer(String.class,
                                      new DefaultTableCellRenderer() {
        final static long serialVersionUID = 42;

        @Override
        public Component getTableCellRendererComponent(JTable table,
                                                       Object value,
                                                       boolean isSelected,
                                                       boolean hasFocus,
                                                       int row,
                                                       int viewColumn) {
          // don't call super to prevent selection from coloring background
          // super.getTableCellRendererComponent(table, direction,
          //                                     isSelected, hasFocus,
          //                                     row, column);
          int column = table.convertColumnIndexToModel(viewColumn);

          Color fg = table.getForeground();
          Color bg = table.getBackground();

          setForeground(fg);
          setBackground(bg);

          if(isSelected) {
            fg = table.getSelectionForeground();
            bg = table.getSelectionBackground();
          }
          setForeground(fg);
          setBackground(bg);

          CompareTableData data = compareData.get(row);
          setToolTipText(null);
          switch(column) {
          case checkCol: {
            //setValue(data.check); ////////////
            break;
          }
          case 1: {
            MyPath base = null;
            MyPath path = null;
            if(data.left != null) {
              base = left;
              path = data.left.path;
            } else if(data.right != null) {
              base = right;
              path = data.right.path;
            } //else path = "Both entries are NULL";
            setValue(getSuffix(base, path));
            setHorizontalAlignment(JLabel.LEFT);
            setToolTipText(getSuffix(base, path).toString());
            break;
          }
          case 2: {
            if(data.left == null) {
              setValue("");
            } else {
              if(data.left.path.isLink()) {
                byte[] bytes = data.left.path.readLink();
                if(bytes == null) setValue("?????");
                else setValue(bytesToString(bytes));
                setHorizontalAlignment(JLabel.LEFT);
              } else {
                setValue(addCommas("" + data.left.size));
                setHorizontalAlignment(JLabel.RIGHT);
              }
              if(!isSelected && data.left.path.isLink()) {
                setBackground(LIGHT_YELLOW);
              }
            }
            break;
          }
          case 3: {
            if(data.left == null) {
              setValue("");
            } else {
              Date date = new Date(data.left.mtime);
              setValue(date.toString());
              setHorizontalAlignment(JLabel.LEFT);
              if(!isSelected && data.left.path.isLink()) {
                setBackground(LIGHT_YELLOW);
              }
            }
            break;
          }
            //case directionCol: return data.direction;
          case 5: {
            if(data.right == null) {
              setValue("");
            } else {
              if(data.right.path.isLink()) {
                byte[] bytes = data.right.path.readLink();
                if(bytes == null) setValue("?????");
                else setValue(bytesToString(bytes));
                setHorizontalAlignment(JLabel.LEFT);
              } else {
                setValue(addCommas("" + data.right.size));
                setHorizontalAlignment(JLabel.RIGHT);
              }
              if(!isSelected && data.right.path.isLink()) {
                setBackground(LIGHT_YELLOW);
              }
            }
            break;
          }
          case 6: {
            if(data.right == null) {
              setValue("");
            } else {
              Date date = new Date(data.right.mtime);
              setValue(date.toString());
              setHorizontalAlignment(JLabel.LEFT);
              if(!isSelected && data.right.path.isLink()) {
                setBackground(LIGHT_YELLOW);
              }
            }
            break;
          }
          default: throw new Error("Bad table column: " + column);
          }
          return this;
        }
      });

      //compareTable.setPreferredSize(new Dimension(1000, 1500));
      int tableColumnWidths[] = {20,150,150,210,50,150,210};
      for(int i = 0; i < compareTableModel.tableColumnNames.length; ++i) {
        TableColumn column = compareTable.getColumnModel().getColumn(i);
        column.setPreferredWidth(tableColumnWidths[i]);
      }

      ((DefaultTableCellRenderer)compareTable.getTableHeader().getDefaultRenderer())
        .setHorizontalAlignment(SwingConstants.LEFT);

      compareTable.addMouseListener(new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            Point point = e.getPoint();
            int col = compareTable.convertColumnIndexToModel(compareTable.columnAtPoint(point));
            int row = compareTable.rowAtPoint(point);
            if(row < 0) return;
            TableData left = compareData.get(row).left;
            TableData right = compareData.get(row).right;
            if(col == directionCol && row >= 0 && e.getButton() == 1) {
              CompareTableData data = compareData.get(row);
              compareTableModel.setValueAt(data.direction.nextDirection(), row, col);
              updateCounts();
            } else if(col == directionCol && row >= 0 && e.getButton() == 3) {
              if(left != null && left.path.isFile() &&
                 right != null && right.path.isFile()) {
                diff(left.path, right.path);
              }
            } else if(e.getButton() == 3) {
              if(left != null) {
                if(col == 2 && left.path.isFile()) { // left size column
                  makeFileEditWindow(left.path);
                } else if(col == 3) { // left date column
                  makeBrowser(left.path);
                }
              }
              if(right != null) {
                if(col == 5 && right.path.isFile()) { // right size column
                  makeFileEditWindow(right.path);
                } else if(col == 6) {
                  makeBrowser(right.path);
                }
              }
            }
          }
        });

      JScrollPane tableScrollPane = new JScrollPane(compareTable);
      int scrollWidth = 0;
      for(int i = 0; i < tableColumnWidths.length; ++i) {
        scrollWidth += tableColumnWidths[i];
      }
      scrollWidth += 3*(tableColumnWidths.length - 1); // 3 pixels separation
      tableScrollPane.setPreferredSize(new Dimension(scrollWidth, 500));
      add(tableScrollPane, BorderLayout.CENTER);

      JPanel bottom = new JPanel(new GridBagLayout());
      JPanel buttons = new JPanel(new GridLayout(2, 0));

      buttons.add(new JButton("Select All") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  compareTable.selectAll();
                }
              });
          }
        });

      buttons.add(new JButton("Check All") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row = 0; row < compareData.size(); ++row) {
                    if(!compareData.get(row).check) compareTableModel.setValueAt(true, row, checkCol);
                  }
                  updateCounts();
                }
              });
          }
        });

      buttons.add(new JButton("Check Selected") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row : compareTable.getSelectedRows()) {
                    if(!compareData.get(row).check) compareTableModel.setValueAt(true, row, checkCol);
                  }
                  updateCounts();
                }
              });
          }
        });

      buttons.add(new JButton("Target Left") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row : compareTable.getSelectedRows()) {
                    CompareTableData data = compareData.get(row);
                    compareTableModel.setValueAt(data.direction.targetLeft(), row, directionCol);

                  }
                  updateCounts();
                }
              });
          }
        });

      buttons.add(new JButton("Target Right") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row : compareTable.getSelectedRows()) {
                    CompareTableData data = compareData.get(row);
                    compareTableModel.setValueAt(data.direction.targetRight(), row, directionCol);

                  }
                  updateCounts();
                }
              });
          }
        });

      buttons.add(new JButton("Unselect All") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  compareTable.clearSelection();
                }
              });
          }
        });

      buttons.add(new JButton("Uncheck All") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row = 0; row < compareData.size(); ++row) {
                    if(compareData.get(row).check) compareTableModel.setValueAt(false, row, checkCol);
                  }
                  updateCounts();
                }
              });
          }
        });

      buttons.add(new JButton("Uncheck Selected") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row : compareTable.getSelectedRows()) {
                    if(compareData.get(row).check) compareTableModel.setValueAt(false, row, checkCol);
                  }
                  updateCounts();
                }
              });
          }
        });

      buttons.add(new JButton("Default Target") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  for(int row : compareTable.getSelectedRows()) {
                    CompareTableData data = compareData.get(row);
                    compareTableModel.setValueAt(data.direction.targetDefault(), row, directionCol);

                  }
                  updateCounts();
                }
              });
          }
        });

      synchronize = new JButton("Synchronize") {
          final static long serialVersionUID = 42;

          {
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                  int buttonNumber = e.getButton();
                  if(buttonNumber != 1 && buttonNumber != 3) return;
                  Color background = getBackground();
                  Color rBack = refresh.getBackground();
                  if(background.equals(RED) ||
                     rBack.equals(RED)) {
                    System.err.println("BUSY");
                    return;
                  }
                  setBackground(RED);
                  new Thread("Synchronize") {
                    @Override
                    public void run() {
                      loop: for(int i = 0; i < compareData.size(); ++i) {
                        final int ii = i;
                        CompareTableData data = compareData.get(i);
                        if(data.check) {
                          MyPath l, r;
                          String s = null;
                          if(data.left != null) {
                            l = data.left.path;
                            s = getSuffix(left, l);
                          } else {
                            l = changePrefix(right, left, data.right.path);
                          }
                          if(data.right != null) {
                            r = data.right.path;
                            s = getSuffix(right, r);
                          } else {
                            r = changePrefix(left, right, data.left.path);
                          }
                          final String suffix = s;
                          SwingUtilities.invokeLater(new Runnable() {
                              @Override
                              public void run() {
                                message.setText(suffix);
                              }
                            });
                          boolean b;
                          if(buttonNumber == 1) {
                            b = data.direction.doOperation(l, r, message);
                          } else if(buttonNumber == 3) {
                            b = data.direction.doTime(l, r, message);
                          } else {
                            break loop;
                          }
                          if(b) {
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                  compareTableModel.setValueAt(false, ii, checkCol);
                                }
                              });
                          }
                        }
                      }
                      SwingUtilities.invokeLater(new Runnable() {
                          @Override
                          public void run() {
                            setBackground(background);
                          }
                        });
                    }
                  }.start();
                }
              });
          }
        };
      buttons.add(synchronize);

      bottom.add(buttons, new GridBagConstraints() {
          final static long serialVersionUID = 42;
          {
          gridx = 0;
          gridy = 0;
          gridwidth = REMAINDER;
          anchor = LINE_START;
          fill = HORIZONTAL;
          weightx = 0.5;
          }
        });

      JPanel counts = new JPanel(new GridLayout(4, 0));

      for(int h = 0; h < d.length; h += d.length/2) { // two rows
        JLabel label = new JLabel("Total");
        counts.add(label);
        countTotals[h == 0 ? 0 : 2] = label;
        for(int i = h; i < h + d.length/2; ++i) {
          int j = i;
          counts.add(new JLabel(" ") {
              final static long serialVersionUID = 42;

              {
                addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent e) {
                      for(int k = 0; k < compareData.size(); ++k) {
                        if(!compareTable.isRowSelected(k)
                           && compareData.get(k).direction.ordinal() == j) {
                          compareTable.addRowSelectionInterval(k, k);
                        }
                      }
                    }
                  });
                setToolTipText("Select matching rows: " + d[j]);
              }

              @Override
              public void paint(Graphics g) {
                super.paint(g); // enable this to super to show text
                d[j].icon(g, getWidth(), getHeight() - 1);
              }
            });
        }
        label = new JLabel("B");
        counts.add(label);
        countTotals[h == 0 ? 1 : 3] = label;
        for(int i = h; i < h + d.length/2; ++i) {
          int j = i;
          JLabel count = new JLabel("" + j, SwingConstants.CENTER);
          //counts.add(new JLabel("A"));
          countLabels[j] = count;
          counts.add(count); // must add to an array for updating
        }
      }
      bottom.add(counts, new GridBagConstraints() {
          final static long serialVersionUID = 42;
          {
          gridx = 0;
          gridy = 1;
          gridwidth = REMAINDER;
          anchor = LINE_START;
          fill = HORIZONTAL;
          weightx = 0.5;
          }
        });

      message = new JLabel("message", SwingConstants.LEFT);
      bottom.add(message, new GridBagConstraints() {
          final static long serialVersionUID = 42;
          {
            gridx = 0;
            gridy = 2;
            fill = HORIZONTAL;
            weightx = 0.5;
          }
        });

      add(bottom, BorderLayout.SOUTH);

      addWindowListener(new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            if(--windowCount == 0) {
              finish();
            }
            dispose();
          }
        });
      ++windowCount;
      updateCounts();
    } // void initPathCompare(MyPath leftPath, MyPath rightPath)
  } // class PathCompare extends JFrame

  /**************************************************************
   * A SSHWindow is a shell window on a remote system.
   **************************************************************/
  static class SSHWindow extends JFrame {
    final static long serialVersionUID = 42;

    final byte ALERT_CODE = 0x07; // ALERT (BELL) code
    final byte BACKSPACE_CODE = 0x08; // backspace
    final byte ESC_CODE = 0X1b; // ESC code
    final byte CNTRL_O = 0x0f; // cntrl-o

    final Color[] colors
      = new Color[]{BLACK, RED, GREEN, YELLOW,
                    BLUE, MAGENTA, CYAN, WHITE};

    Session session = null;
    ChannelShell channel = null;
    InputStream inputStream = null;
    OutputStream outputStream = null;
    ChannelShell channelShell;
    JTextPane text = new JTextPane();
    StyledDocument doc = text.getStyledDocument();
    int charWidth;
    int charHeight;
    int cursorLocation = 0; // don't use textPane caret
    Style style = text.addStyle("", null);
    JScrollPane scroll = new JScrollPane(text);
    String title;
    boolean dragging = false; // are we currently dragging a selection?

    {
      // must come first
      text.addMouseListener(new MouseListener() {
          @Override
          public void mouseEntered(MouseEvent e) {
            //e.consume();
          }
          @Override
          public void mouseExited(MouseEvent e) {
            //e.consume();
          }
          @Override
          public void mousePressed(MouseEvent e) {
            e.consume();
            switch(e.getButton()) {
            case 1: {
              Point p = e.getPoint();
              int i = text.viewToModel(p);
              text.setCaretPosition(i);
              dragging = true;
              break;
            }
            case 2: {
              try {
                String s = (String)clipboard.getData(DataFlavor.stringFlavor);
                outputStream.write(s.getBytes("UTF-8"));
                outputStream.flush();
              } catch(IOException ex) {
                ex.printStackTrace();
              } catch(UnsupportedFlavorException ex) {
                //ex.printStackTrace();
                try {
                  String s = (String)toolkit.getSystemClipboard().getData(DataFlavor.stringFlavor);
                  outputStream.write(s.getBytes("UTF-8"));
                  outputStream.flush();
                } catch(UnsupportedFlavorException | IOException exx) {
                  System.err.println(exx);
                  exx.printStackTrace();
                }
              } finally {}
              break;
            }
            case 3: {
              String user = session.getUserName();
              String host = session.getHost();
              String userHost = user + '@' + host;
              makeBrowser(stringToMyPath("//" + userHost));
              break;
            }
            default: {
              System.out.println(e.getButton());
              break;
            }
            }
          }
          @Override
          public void mouseReleased(MouseEvent e) {
            e.consume();
            dragging = false;
            if(e.getButton() == 1) {
              Point p = e.getPoint();
              int i = text.viewToModel(p);
              text.moveCaretPosition(i);
              // why is this not needed?
              //clipboard.setContents(new StringSelection(text.getSelectedText()), null);
            }
          }
          @Override
          public void mouseClicked(MouseEvent e) {
            e.consume();
          }
        });
      text.addMouseMotionListener(new MouseMotionListener() {
          @Override
          public void mouseMoved(MouseEvent e) {
            e.consume();
          }
          @Override
          public void mouseDragged(MouseEvent e) {
            e.consume();
            if(dragging) {
              Point p = e.getPoint();
              int i = text.viewToModel(p);
              text.moveCaretPosition(i);
            }
          }
        });

      // needed to intercept mouse events
      // this must come after the other text mouse listeners
      text.setCaret(new DefaultCaret() {
          final static long serialVersionUID = 42;
          {
            setBlinkRate(500);
          }
        });
    }

    /**
     * Initialize a shell window on a remote system. The validation
     * for the connection uses the same validation as the file browser
     * windows. The process is in two parts:
     *
     * 1. For all bytes from the remote end, process them via the
     * state machine and adjust the terminal window appropriately.
     *
     * 2. For all characters typed or pasted to the terminal window,
     * possibly encode them and write to the remote end.
     *
     * Note that characters are not echoed directly but sent to the
     * remote end which generally sends them back to the terminal to
     * be displayed. Certain characters (RETURN, NEW LINE, BACKSPACE,
     * arrow keys) are not echoed directly but send appropriate
     * controls to the terminal.
     *
     * @param userHost the user@host of the system to open the terminal on
     */
    SSHWindow(String userHost) {
      super(userHost);
      userHost = userHost.intern();
      title = userHost;
      this.title = title;
      //scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);//////////////////
      StyleConstants.setFontFamily(style, "Monospaced");
      int fontSize = 15;
      StyleConstants.setFontSize(style, fontSize);
      FontRenderContext frc = new FontRenderContext(null, false, false);
      Font font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
      TextLayout layout = new TextLayout("n", font, frc);
      float ascent = layout.getAscent();
      float descent = layout.getDescent();
      float leading = layout.getLeading();
      charWidth = (int)layout.getAdvance();
      charHeight = (int)Math.ceil(ascent) + (int)Math.ceil(descent) + (int)Math.ceil(leading);
      //System.out.println(charWidth + " x " + charHeight);
      try {
        session = getSession(userHost);
        channel = (ChannelShell)session.openChannel("shell");
        channel.setXForwarding(true);
        //channel.setAgentForwarding(true); //////////
      } catch(SftpOpenFailedException e) {
        e.printStackTrace();
        throw new Error("SftpOpenFailedException");
      } catch(JSchException e) {
        e.printStackTrace();
        throw new Error("JSchException");
      }
      setSession(channel);
      enableInputMethods(false);
      text.setPreferredSize(new Dimension(800, 448));
      getContentPane().add(scroll);
      addWindowListener(new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            if(channelShell != null) channelShell.disconnect();
            if(--windowCount == 0) {
              finish();
            }
            dispose();
          }
        });
      ++windowCount;
    } // SSHWindow(String userHost)

    void setSession(ChannelShell c) {
      channelShell = c;
      session.setConfig("StrictHostKeyChecking", "no");
      try {
        channel.connect(3*1000);
        inputStream = c.getInputStream();
        outputStream = c.getOutputStream();
      } catch(JSchException e) {
        //e.printStackTrace(); ///////// should channel fail to open
        System.err.println("Channel failed to open");
      } catch(IOException e) {
        e.printStackTrace();
      }

      // process data from the remote end and display appropriately
      new Thread("SSH main loop") {

        // default doc state
        Color defaultForeground = text.getForeground();
        Color defaultBackground = text.getBackground();

        // current doc state
        Color currentForeground = defaultForeground;
        Color currentBackground = defaultBackground;

        /**
         * Sets reverse video on the terminal. This just complements
         * the color values.
         */
        void setReverseVideo() {
          // int current = currentForeground.getRGB();
          // StyleConstants.setForeground(style, new Color(current ^ 0xffffff));
          // current = currentBackground.getRGB();
          // StyleConstants.setBackground(style, new Color(current ^ 0xffffff));
        }

        /**
         * Sets the forground and background back to their (uncomplemented)
         * colors.
         */
        void resetReverseVideo() {
          // int current = currentForeground.getRGB();
          // StyleConstants.setForeground(style, new Color(current));
          // current = currentBackground.getRGB();
          // StyleConstants.setBackground(style, new Color(current));
        }

        byte[] buffer = new byte[4096];
        int start; // first byte in buffer to process
        int end; // last valid byte in buffer + 1
        int index; // current byte to process
        byte b; // last read byte

        ArrayList<Integer> params = new ArrayList<Integer>();

        /** old read that does not remove null characters
         */
        void readx() throws IOException {
          normalizeBuffer(index);
          if(2*end > buffer.length) {
            for(int i = start; i < end; ++i) {
              buffer[i - start] = buffer[i];
            }
            end -= start;
            index -= start;
            start = 0;
          }
          if(inputStream == null) throw new IOException("can't get channel");
          int nBytes = inputStream.read(buffer, end, buffer.length - end);
          if(nBytes <= 0) throw new IOException("EOF or something wrong");
          System.out.println("nBytes: " + nBytes);
          System.out.println("read string: " + new String(buffer, end, nBytes));
          end += nBytes;
        }

        /**
         * Reads from inputStream at least one character or blocks
         * until input is available. Throws away all null characters.
         */
        void read() throws IOException {
          normalizeBuffer(index);
          if(2*end > buffer.length) {
            for(int i = start; i < end; ++i) {
              buffer[i - start] = buffer[i];
            }
            end -= start;
            index -= start;
            start = 0;
          }
          if(inputStream == null) throw new IOException("can't get channel");
          do {
            int c = inputStream.read();
            if(c < 0) throw new IOException("EOF or something wrong");
            if(c == 0) continue; /////////////????????
            buffer[end] = (byte)c;
            end += 1;
          } while(inputStream.available() > 0 && end < buffer.length);
        }

        void setDefaults() {
          // should be more flexible ////////////////
          StyleConstants.setForeground(style, defaultForeground);
          currentForeground = defaultForeground;
          StyleConstants.setBackground(style, defaultBackground);
          currentBackground = defaultBackground;
          StyleConstants.setBold(style, false);
        }

        /**
         * The buffer between start and index are characters that have
         * not yet been inserted into the document. Sends characters
         * from the buffer from start to index to the document and
         * then shifts the buffer if appropriate so start is
         * zero. Characters are inserted from start up to the
         * index. All characters in the buffer before start are not
         * needed so all bytes in the buffer can be shifted to make
         * start equal to zero.
         *
         * @param index the range from start to index to insert
         */
        void normalizeBuffer(int index) {
          if(start != index) {
            try {
              String s = new String(buffer, start, index - start, "UTF-8");
              try {
                if(cursorLocation + s.length() < doc.getLength()) {
                  doc.remove(cursorLocation, s.length());
                } else {
                  doc.remove(cursorLocation, doc.getLength() - cursorLocation);
                }
                doc.insertString(cursorLocation, s, style);
                cursorLocation += s.length();
              } catch(BadLocationException e) {
                e.printStackTrace();
              }
              SwingUtilities.invokeAndWait(new Runnable() {
                  @Override
                  public void run() {
                    text.setCaretPosition(cursorLocation);
                  }
                });
              start = index; // just wrote out String before index
            } catch(InterruptedException | InvocationTargetException | UnsupportedEncodingException e) {
              e.printStackTrace();
            }
          }
        }

        /**
         * Reads the next byte from the remote end and advances the
         * index. If there are no more unprocessed bytes in the buffer
         * a read from the remote end is performed.
         *
         * @return the next unprocessed byte from the remote end
         */
        byte getByte() throws IOException {
          if(index >= end) read();
          return b = buffer[index++];
        }

        /**
         * Reads the next integer from the unprocessed bytes in the
         * buffer. This method continues to read digits until a
         * non-digit is encountered. If the current byte is a
         * non-digit then the index is not incremented and zero is
         * returned.
         *
         * @return the integer that was read
         */
        int getInt() throws IOException {
          int n = 0;
          while(b >= '0' && b <= '9') {
            n = 10*n + b - '0';
            getByte();
          }
          return n;
        }

        /**
         * Gets the next [control character] terminated string from
         * the buffer. The bytes are converted to a string using the
         * UTF-8 character set. Strigs should be terminated with a
         * cntrl-g character (bell). If not, an error message is
         * printed.
         *
         * @return the string that was read
         */
        String getString() throws IOException {
          byte[] bytes = new byte[256]; ////////////////////////
          int i;
          for(i = 0; i < bytes.length; ++i) {
            if(b >= ' ') {
              bytes[i] = b;
              getByte();
            } else break;
          }
          if(b != 7) System.err.println("bad string terminator: " + b);
          String s = new String(bytes, 0, i, "UTF-8");
          return s;
        }

        /**
         * Used before initialiing curses. Used to get the $DISPLAY
         * value. Reads one line from the inputStream.
         *
         * @return the next line from inputStream
         */
        String readLineUnbuffered() throws IOException {
          if(inputStream == null) throw new IOException("can't get channel");
          int inByte = 0;
          String s = "";
          while((inByte = inputStream.read()) != '\n') {
            s += (char)inByte;
          }
          return s;
        }

        /**
         * Used before initialiing curses. Used to get the $DISPLAY
         * value. Writes the argument to outputStream.
         *
         * @param the string to write to the remote end
         */
        void writeUnbuffered(String s) throws IOException {
          for(int i = 0; i < s.length(); ++i) {
            outputStream.write((int)s.charAt(i));
            outputStream.flush();
          }
        }

        /**
         * This method is for debugging.
         *
         * Prints the contents of the terminal window with \n and \r
         * replaced with N and R respectively. Null characters are
         * replaced with a #. The position of the cursor is indicated
         * by the @ sign.
         *
         * @param label a label printed to identify the caller
         */
        void printText(String label) {
          try {
            System.out.println("printing text at: " + label);
            System.out.println("start: " + start);
            System.out.println("index: " + index);
            System.out.println("end: " + end);
            System.out.println("cursorLocation: " + cursorLocation);
            String s = doc.getText(0, doc.getLength());
            System.out.println("string length: " + s.length());
            System.out.println("doc length: " + doc.getLength());
            String docString = s.substring(0, cursorLocation)
              + '@' + s.substring(cursorLocation);
            System.out.println("doc string length: " + docString.length());
            s = s.replace('\n', 'N');
            s = s.replace('\r', 'R');
            s = s.replace((char)0, '#');
            for(int i = 0; i < docString.length(); i++) {
              System.out.print((int)docString.charAt(i) + "|");
            }
            System.out.println();
            System.out.println("====|====|====|====|====|====|====|====|");
            
            System.out.println(s.substring(0, cursorLocation)
                               + '@' + s.substring(cursorLocation));
            System.out.println("====|====|====|====|====|====|====|====|");
          } catch(BadLocationException e) {
            e.printStackTrace();
          }
        }

        /*
         * This is the Thread "run" method. It implements a state
         * machine where the state is kept in the program counter.
         */
        @Override
        public void run() {
          setDefaults();
          try {
            //get DISPLAY value
            writeUnbuffered("echo $DISPLAY\n");

            System.out.println("1: " + readLineUnbuffered());
            while(readLineUnbuffered().indexOf("DISPLAY") < 0) {}
            String serverPort = readLineUnbuffered();
            int colonIndex = serverPort.indexOf(':');
            System.out.println("5: " + serverPort);

            session.setX11Host(serverPort.substring(0, colonIndex));
            float port = Float.parseFloat(serverPort.substring(colonIndex + 1));
            session.setX11Port((int)port + 6000);
            while(true) {
              getByte();
              switch(b) {
              case ESC_CODE: {
                normalizeBuffer(index - 1); // don't display ESC
                getByte();
                switch(b) {
                case ']': {
                  getByte();
                  int n1 = getInt();
                  if(b != ';') System.err.println("Format Error");
                  getByte();
                  switch(n1) {
                  case 0:
                  case 2: {
                    String s = getString();
                    getByte();
                    try {
                      SwingUtilities.invokeAndWait(new Runnable() {
                          @Override
                          public void run() {
                            setTitle(s);
                          }
                        });
                    } catch(InterruptedException | InvocationTargetException e) {
                      e.printStackTrace();
                    }
                    break;
                  }
                  default: {
                    String s = getString();
                    System.err.println("Unknown escape sequence: ESC ] " + s);
                  }
                  }
                  start = index;
                  break;
                }
                case '[': { // CSI
                  params.clear();
                  getByte(); // skip '['
                  while(b >= '0' && b <= '9') {
                    params.add(getInt());
                    if(b != ';') break;
                    getByte(); // skip ';'
                  }
                  switch(b) {
                  case 'A': {
                    System.out.println("skipping ESC-[" + params + 'A');
                    start = index;
                    break;
                  }
                  case 'B': {
                    System.out.println("skipping ESC-[" + params + 'B');
                    start = index;
                    break;
                  }
                  case 'C': { // move right # of columns
                    try {
                      ++cursorLocation; //// could be more
                      text.setCaretPosition(cursorLocation);
                      start = index;
                    } catch(IllegalArgumentException e) {
                      e.printStackTrace();
                    }
                    break;
                  }
                  case 'D': {
                    --cursorLocation; //// could be more
                    text.setCaretPosition(cursorLocation);
                    start = index;
                    break;
                  }
                  case 'H': {
                    System.out.println("skipping ESC-[" + params + 'H');
                    start = index;
                    break;
                  }
                  case 'J': {
                    System.out.println("skipping ESC-[" + params + 'J');
                    start = index;
                    break;
                  }
                  case 'K': {
                    ////////// 0 erase from cursor to end of line
                    ////////// 1 erase from start of line to cursor
                    ////////// 2 erase whole line
                    if(params.size() == 0) params.add(0);
                    if(params.size() != 1) System.err.println("case K long params: "+ params.size());
                    int n = params.get(0);
                    switch(n) {
                    case 0: {
                      int docEnd = doc.getLength();
                      int pos = cursorLocation;
                      try {
                        doc.remove(pos, docEnd - pos);
                      } catch(BadLocationException e) {
                        e.printStackTrace();
                      }
                      break;
                    }
                    default: {
                      System.err.println("Unknown escape sequence: ESC [" + params + " K");
                    }
                    }
                    start = index;
                    break;
                  }
                  case 'm': {
                    for(int i = 0; i < params.size(); ++i) {
                      int n = params.get(i);
                      switch(n) {
                      case 0: {
                        setDefaults();
                        start = index;
                        break;
                      }
                      case 1: {
                        StyleConstants.setBold(style, true);
                        break;
                      }
                      case 7: {
                        setReverseVideo(); /////// copy to defaults?
                        break;
                      }
                      case 30:
                      case 31:
                      case 32:
                      case 33:
                      case 34:
                      case 35:
                      case 36:
                      case 37: {
                        StyleConstants.setForeground(style, colors[n%10]);
                        break;
                      }
                      case 40:
                      case 41:
                      case 42:
                      case 43:
                      case 44:
                      case 45:
                      case 46:
                      case 47: {
                        StyleConstants.setBackground(style, colors[n%10]);
                        break;
                      }
                      default: {
                        System.err.println("Unknown escape sequence: ESC [" + params + " m");
                      }
                      }
                    }
                    break;
                  }
                  case '?': { //// only exactly one argument
                    getByte();
                    int n = getInt();
                    System.out.println("skipping ESC-[" + '?' + n + (char)(b+0));
                    start = index;
                    break;
                  }
                    /* ignore ESC [ [ x    ----   also print this for debugging
                  case '[': {
                    break;
                  }
                    */
                  default: {
                    System.err.println("Unknown escape sequence: [" + params + (char)(b+0));
                  }
                  }
                  start = index;
                  break;
                } // case [
                case '=': {
                  System.out.println("skipping ESC-[" + params + '=');
                  start = index;
                  break;
                }
                case '>': {
                  System.out.println("skipping ESC-[" + params + '>');
                  start = index;
                  break;
                }
                default: {
                  System.err.println("Unknown escape sequence: ESC " + (char)(b+0));
                }
                }
                break;
              } // case ESC_CODE
              case ALERT_CODE: { // ignore alerts
                normalizeBuffer(index - 1); // up to ALERT //////////////////
                start = index;
                break;
              }
              case BACKSPACE_CODE: {
                normalizeBuffer(index - 1); // up to BACKSPACE ///////////////
                start = index;
                
                --cursorLocation;
                text.setCaretPosition(cursorLocation);
                break;
              }
              case '\r': {
                normalizeBuffer(index - 1); // skip return
                start = index;
                try {
                  int length = doc.getLength();
                  String s = doc.getText(0, length);
                  int begin = s.lastIndexOf('\n');
                  cursorLocation = begin + 1; ////****
                } catch(BadLocationException e) {
                  e.printStackTrace();
                }
                break;
              }
              case '\n': {
                cursorLocation = doc.getLength(); ////****
                break;
              }
              case CNTRL_O: { // change to default character set
                // not handled
                normalizeBuffer(index - 1); // up to ^O //////////////////
                start = index;
                break;
              }
              default: {
              }
              }
            }
          } catch(IOException e) {
            setTitle(title + " ***Disconnected***"); ///////////////////
            channel.disconnect();

          } finally {
            // ordinary character
          }
        }
      }.start();

      ////// handle window resize <ESC> ; height ; width t
      getContentPane().addComponentListener(new ComponentAdapter() {
          @Override
          public void componentResized(ComponentEvent e) {
            System.out.println("window size: " + e.getComponent().getSize());
            System.out.println(e.getComponent().getSize().width/charWidth + " " +
                               e.getComponent().getSize().height/charHeight);
            //channelShell.setPtySize(40,24,320,480);
          }
        });

      // process characters that are typed or pasted in and dispatch
      // to remote end.
      text.addKeyListener(new KeyListener() {
          @Override
          public void keyTyped(KeyEvent e) {
            char c = e.getKeyChar();
            try {
              if(outputStream == null) throw new IOException();
              if(c == 0x7f) {
                // so DEL deletes the character after the cursor
                outputStream.write(004);
                outputStream.flush();
              } else {
                outputStream.write(c);
                outputStream.flush();
              }
            } catch(IOException ex) {
              setTitle(title + " ***Disconnected***");
              channel.disconnect();
            }
            e.consume();
          }
          @Override
          public void keyPressed(KeyEvent e) {
            int code = e.getKeyCode();
            try {
              if(outputStream == null) throw new IOException();
              switch(code) {
              case 0x26: { // up arrow
                outputStream.write(new byte[]{033,'[','A'});
                outputStream.flush();
                break;
              }
              case 0x28: { // down arrow
                outputStream.write(new byte[]{033,'[','B'});
                outputStream.flush();
                break;
              }
              case 0x27: { // right arrow
                outputStream.write(new byte[]{033,'[','C'});
                outputStream.flush();
                break;
              }
              case 0x25: { // left arrow
                outputStream.write(new byte[]{033,'[','D'});
                outputStream.flush();
                break;
              }
              }
            } catch(IOException ex) {
              setTitle(title + " ***Disconnected***");
              channel.disconnect();
            }
            e.consume();
          }

          @Override
          public void keyReleased(KeyEvent e) {
            e.consume();
          }
        });
    } // void setSession(ChannelShell c)
  } // static class SSHWindow extends JFrame

  /**
   * Start a file browser on specified files. If no files are
   * specified then start a file browser on the roots of the file
   * system.
   *
   * -diff f1 f2 does a diff on f1 and f2 and shows result in window
   * -ssh x@y does an ssh to user x on machine y
   * [no params] brings up a file browser on root
   * f1 brings up a file browser on f1
   * f1 f2 brings up a file comparison window on f1, f2
   * [3 or more files] bring up a separate file browser on all files
   *
   * @param args list of files to start file browsers on
   */
  public static void main(String[] args) {
    if(args.length == 3 && args[0].equals("-diff")) {
      final MyPath leftPath = stringToMyPath(args[1]);
      final MyPath rightPath = stringToMyPath(args[2]);
      diff(leftPath, rightPath);
      return;
    }
    if(args.length >= 2 && args[0].equals("-ssh")) {
      for(int i = 1; i < args.length; ++i) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
              SSHWindow ssh = new SSHWindow(args[1]);
              ssh.setLocationByPlatform(true);
              ssh.pack();
              ssh.setVisible(true);
            }
          });
      }
      return;
    }
    if(args.length == 2) {
      final MyPath leftPath = stringToMyPath(args[0]);
      final MyPath rightPath = stringToMyPath(args[1]);
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            PathCompare pathCompare = new FileBrowser().new PathCompare("Comparing", leftPath, rightPath);
            pathCompare.setLocationByPlatform(true);
            pathCompare.pack();
            pathCompare.setVisible(true);
            return;
          }
        });
      return;
    }
    if(args.length == 0) {
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            //Create and set up the window.
            JFrame frame = new FileBrowser().new Browser();
            frame.setLocationByPlatform(true);
            //Display the window.
            frame.pack();
            frame.setVisible(true);
          }
        });
    } else {
      for(final String s : args) {
        makeBrowser(stringToMyPath(s));
      }
    }
  } // public static void main(String[] args)
} // class FileBrowser
