SWTFileViewerDemo.java Source code

Java tutorial

Introduction

Here is the source code for SWTFileViewerDemo.java

Source

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Vector;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.TreeAdapter;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.program.Program;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

/**
 * File Viewer example
 */
public class SWTFileViewerDemo {
    private final static String DRIVE_A = "a:" + File.separator;

    private final static String DRIVE_B = "b:" + File.separator;

    /* UI elements */
    private Display display;

    private Shell shell;

    private ToolBar toolBar;

    private Label numObjectsLabel;

    private Label diskSpaceLabel;

    private File currentDirectory = null;

    private boolean initial = true;

    /* Drag and drop optimizations */
    private boolean isDragging = false; // if this app is dragging

    private boolean isDropping = false; // if this app is dropping

    private File[] processedDropFiles = null; // so Drag only deletes what it
    // needs to

    private File[] deferredRefreshFiles = null; // to defer notifyRefreshFiles
    // while we do DND

    private boolean deferredRefreshRequested = false; // to defer
    // notifyRefreshFiles
    // while we do DND

    private ProgressDialog progressDialog = null; // progress dialog for
    // locally-initiated
    // operations

    /* Combo view */
    private static final String COMBODATA_ROOTS = "Combo.roots";

    // File[]: Array of files whose paths are currently displayed in the combo
    private static final String COMBODATA_LASTTEXT = "Combo.lastText";

    // String: Previous selection text string

    private Combo combo;

    /* Tree view */
    private IconCache iconCache = new IconCache();

    private static final String TREEITEMDATA_FILE = "TreeItem.file";

    // File: File associated with tree item
    private static final String TREEITEMDATA_IMAGEEXPANDED = "TreeItem.imageExpanded";

    // Image: shown when item is expanded
    private static final String TREEITEMDATA_IMAGECOLLAPSED = "TreeItem.imageCollapsed";

    // Image: shown when item is collapsed
    private static final String TREEITEMDATA_STUB = "TreeItem.stub";

    // Object: if not present or null then the item has not been populated

    private Tree tree;

    private Label treeScopeLabel;

    /* Table view */
    private static final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
            DateFormat.MEDIUM);

    private static final String TABLEITEMDATA_FILE = "TableItem.file";

    // File: File associated with table row
    private static final String TABLEDATA_DIR = "Table.dir";

    // File: Currently visible directory
    private static final int[] tableWidths = new int[] { 150, 60, 75, 150 };

    private final String[] tableTitles = new String[] { SWTFileViewerDemo.getResourceString("table.Name.title"),
            SWTFileViewerDemo.getResourceString("table.Size.title"),
            SWTFileViewerDemo.getResourceString("table.Type.title"),
            SWTFileViewerDemo.getResourceString("table.Modified.title") };

    private Table table;

    private Label tableContentsOfLabel;

    /* Table update worker */
    // Control data
    private final Object workerLock = new Object();

    // Lock for all worker control data and state
    private volatile Thread workerThread = null;

    // The worker's thread
    private volatile boolean workerStopped = false;

    // True if the worker must exit on completion of the current cycle
    private volatile boolean workerCancelled = false;

    // True if the worker must cancel its operations prematurely perhaps due to
    // a state update

    // Worker state information -- this is what gets synchronized by an update
    private volatile File workerStateDir = null;

    // State information to use for the next cycle
    private volatile File workerNextDir = null;

    /* Simulate only flag */
    // when true, disables actual filesystem manipulations and outputs results
    // to standard out
    private boolean simulateOnly = true;

    /**
     * Runs main program.
     */
    public static void main(String[] args) {
        Display display = new Display();
        SWTFileViewerDemo application = new SWTFileViewerDemo();
        Shell shell = application.open(display);
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }
        application.close();
        display.dispose();
    }

    /**
     * Opens the main program.
     */
    public Shell open(Display display) {
        // Create the window
        this.display = display;
        iconCache.initResources(display);
        shell = new Shell();
        createShellContents();
        notifyRefreshFiles(null);
        shell.open();
        return shell;
    }

    /**
     * Closes the main program.
     */
    void close() {
        workerStop();
        iconCache.freeResources();
    }

    /**
     * Returns a string from the resource bundle. We don't want to crash because
     * of a missing String. Returns the key if not found.
     */
    static String getResourceString(String key) {
        return key;
    }

    /**
     * Returns a string from the resource bundle and binds it with the given
     * arguments. If the key is not found, return the key.
     */
    static String getResourceString(String key, Object[] args) {
        try {
            return MessageFormat.format(getResourceString(key), args);
        } catch (MissingResourceException e) {
            return key;
        } catch (NullPointerException e) {
            return "!" + key + "!";
        }
    }

    /**
     * Construct the UI
     * 
     * @param container
     *            the ShellContainer managing the Shell we are rendering inside
     */
    private void createShellContents() {
        shell.setText(getResourceString("Title", new Object[] { "" }));
        shell.setImage(iconCache.stockImages[iconCache.shellIcon]);
        Menu bar = new Menu(shell, SWT.BAR);
        shell.setMenuBar(bar);
        createFileMenu(bar);
        createHelpMenu(bar);

        GridLayout gridLayout = new GridLayout();
        gridLayout.numColumns = 3;
        gridLayout.marginHeight = gridLayout.marginWidth = 0;
        shell.setLayout(gridLayout);

        GridData gridData = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
        gridData.widthHint = 185;
        createComboView(shell, gridData);
        gridData = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
        gridData.horizontalSpan = 2;
        createToolBar(shell, gridData);

        SashForm sashForm = new SashForm(shell, SWT.NONE);
        sashForm.setOrientation(SWT.HORIZONTAL);
        gridData = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL);
        gridData.horizontalSpan = 3;
        sashForm.setLayoutData(gridData);
        createTreeView(sashForm);
        createTableView(sashForm);
        sashForm.setWeights(new int[] { 2, 5 });

        numObjectsLabel = new Label(shell, SWT.BORDER);
        gridData = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_FILL);
        gridData.widthHint = 185;
        numObjectsLabel.setLayoutData(gridData);

        diskSpaceLabel = new Label(shell, SWT.BORDER);
        gridData = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_FILL);
        gridData.horizontalSpan = 2;
        diskSpaceLabel.setLayoutData(gridData);
    }

    /**
     * Creates the File Menu.
     * 
     * @param parent
     *            the parent menu
     */
    private void createFileMenu(Menu parent) {
        Menu menu = new Menu(parent);
        MenuItem header = new MenuItem(parent, SWT.CASCADE);
        header.setText(getResourceString("menu.File.text"));
        header.setMenu(menu);

        final MenuItem simulateItem = new MenuItem(menu, SWT.CHECK);
        simulateItem.setText(getResourceString("menu.File.SimulateOnly.text"));
        simulateItem.setSelection(simulateOnly);
        simulateItem.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                simulateOnly = simulateItem.getSelection();
            }
        });

        MenuItem item = new MenuItem(menu, SWT.PUSH);
        item.setText(getResourceString("menu.File.Close.text"));
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                shell.close();
            }
        });
    }

    /**
     * Creates the Help Menu.
     * 
     * @param parent
     *            the parent menu
     */
    private void createHelpMenu(Menu parent) {
        Menu menu = new Menu(parent);
        MenuItem header = new MenuItem(parent, SWT.CASCADE);
        header.setText(getResourceString("menu.Help.text"));
        header.setMenu(menu);

        MenuItem item = new MenuItem(menu, SWT.PUSH);
        item.setText(getResourceString("menu.Help.About.text"));
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                MessageBox box = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
                box.setText(getResourceString("dialog.About.title"));
                box.setMessage(getResourceString("dialog.About.description",
                        new Object[] { System.getProperty("os.name") }));
                box.open();
            }
        });
    }

    /**
     * Creates the toolbar
     * 
     * @param shell
     *            the shell on which to attach the toolbar
     * @param layoutData
     *            the layout data
     */
    private void createToolBar(final Shell shell, Object layoutData) {
        toolBar = new ToolBar(shell, SWT.NULL);
        toolBar.setLayoutData(layoutData);
        ToolItem item = new ToolItem(toolBar, SWT.SEPARATOR);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdParent]);
        item.setToolTipText(getResourceString("tool.Parent.tiptext"));
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                doParent();
            }
        });
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdRefresh]);
        item.setToolTipText(getResourceString("tool.Refresh.tiptext"));
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                doRefresh();
            }
        });
        SelectionAdapter unimplementedListener = new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                MessageBox box = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
                box.setText(getResourceString("dialog.NotImplemented.title"));
                box.setMessage(getResourceString("dialog.ActionNotImplemented.description"));
                box.open();
            }
        };

        item = new ToolItem(toolBar, SWT.SEPARATOR);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdCut]);
        item.setToolTipText(getResourceString("tool.Cut.tiptext"));
        item.addSelectionListener(unimplementedListener);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdCopy]);
        item.setToolTipText(getResourceString("tool.Copy.tiptext"));
        item.addSelectionListener(unimplementedListener);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdPaste]);
        item.setToolTipText(getResourceString("tool.Paste.tiptext"));
        item.addSelectionListener(unimplementedListener);

        item = new ToolItem(toolBar, SWT.SEPARATOR);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdDelete]);
        item.setToolTipText(getResourceString("tool.Delete.tiptext"));
        item.addSelectionListener(unimplementedListener);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdRename]);
        item.setToolTipText(getResourceString("tool.Rename.tiptext"));
        item.addSelectionListener(unimplementedListener);

        item = new ToolItem(toolBar, SWT.SEPARATOR);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdSearch]);
        item.setToolTipText(getResourceString("tool.Search.tiptext"));
        item.addSelectionListener(unimplementedListener);
        item = new ToolItem(toolBar, SWT.PUSH);
        item.setImage(iconCache.stockImages[iconCache.cmdPrint]);
        item.setToolTipText(getResourceString("tool.Print.tiptext"));
        item.addSelectionListener(unimplementedListener);
    }

    /**
     * Creates the combo box view.
     * 
     * @param parent
     *            the parent control
     */
    private void createComboView(Composite parent, Object layoutData) {
        combo = new Combo(parent, SWT.NONE);
        combo.setLayoutData(layoutData);
        combo.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                final File[] roots = (File[]) combo.getData(COMBODATA_ROOTS);
                if (roots == null)
                    return;
                int selection = combo.getSelectionIndex();
                if (selection >= 0 && selection < roots.length) {
                    notifySelectedDirectory(roots[selection]);
                }
            }

            public void widgetDefaultSelected(SelectionEvent e) {
                final String lastText = (String) combo.getData(COMBODATA_LASTTEXT);
                String text = combo.getText();
                if (text == null)
                    return;
                if (lastText != null && lastText.equals(text))
                    return;
                combo.setData(COMBODATA_LASTTEXT, text);
                notifySelectedDirectory(new File(text));
            }
        });
    }

    /**
     * Creates the file tree view.
     * 
     * @param parent
     *            the parent control
     */
    private void createTreeView(Composite parent) {
        Composite composite = new Composite(parent, SWT.NONE);
        GridLayout gridLayout = new GridLayout();
        gridLayout.numColumns = 1;
        gridLayout.marginHeight = gridLayout.marginWidth = 2;
        gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
        composite.setLayout(gridLayout);

        treeScopeLabel = new Label(composite, SWT.BORDER);
        treeScopeLabel.setText(SWTFileViewerDemo.getResourceString("details.AllFolders.text"));
        treeScopeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_FILL));

        tree = new Tree(composite, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE);
        tree.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL));

        tree.addSelectionListener(new SelectionListener() {
            public void widgetSelected(SelectionEvent event) {
                final TreeItem[] selection = tree.getSelection();
                if (selection != null && selection.length != 0) {
                    TreeItem item = selection[0];
                    File file = (File) item.getData(TREEITEMDATA_FILE);

                    notifySelectedDirectory(file);
                }
            }

            public void widgetDefaultSelected(SelectionEvent event) {
                final TreeItem[] selection = tree.getSelection();
                if (selection != null && selection.length != 0) {
                    TreeItem item = selection[0];
                    item.setExpanded(true);
                    treeExpandItem(item);
                }
            }
        });
        tree.addTreeListener(new TreeAdapter() {
            public void treeExpanded(TreeEvent event) {
                final TreeItem item = (TreeItem) event.item;
                final Image image = (Image) item.getData(TREEITEMDATA_IMAGEEXPANDED);
                if (image != null)
                    item.setImage(image);
                treeExpandItem(item);
            }

            public void treeCollapsed(TreeEvent event) {
                final TreeItem item = (TreeItem) event.item;
                final Image image = (Image) item.getData(TREEITEMDATA_IMAGECOLLAPSED);
                if (image != null)
                    item.setImage(image);
            }
        });
        createTreeDragSource(tree);
        createTreeDropTarget(tree);
    }

    /**
     * Creates the Drag & Drop DragSource for items being dragged from the tree.
     * 
     * @return the DragSource for the tree
     */
    private DragSource createTreeDragSource(final Tree tree) {
        DragSource dragSource = new DragSource(tree, DND.DROP_MOVE | DND.DROP_COPY);
        dragSource.setTransfer(new Transfer[] { FileTransfer.getInstance() });
        dragSource.addDragListener(new DragSourceListener() {
            TreeItem[] dndSelection = null;

            String[] sourceNames = null;

            public void dragStart(DragSourceEvent event) {
                dndSelection = tree.getSelection();
                sourceNames = null;
                event.doit = dndSelection.length > 0;
                isDragging = true;
                processedDropFiles = null;
            }

            public void dragFinished(DragSourceEvent event) {
                dragSourceHandleDragFinished(event, sourceNames);
                dndSelection = null;
                sourceNames = null;
                isDragging = false;
                processedDropFiles = null;
                handleDeferredRefresh();
            }

            public void dragSetData(DragSourceEvent event) {
                if (dndSelection == null || dndSelection.length == 0)
                    return;
                if (!FileTransfer.getInstance().isSupportedType(event.dataType))
                    return;

                sourceNames = new String[dndSelection.length];
                for (int i = 0; i < dndSelection.length; i++) {
                    File file = (File) dndSelection[i].getData(TREEITEMDATA_FILE);
                    sourceNames[i] = file.getAbsolutePath();
                }
                event.data = sourceNames;
            }
        });
        return dragSource;
    }

    /**
     * Creates the Drag & Drop DropTarget for items being dropped onto the tree.
     * 
     * @return the DropTarget for the tree
     */
    private DropTarget createTreeDropTarget(final Tree tree) {
        DropTarget dropTarget = new DropTarget(tree, DND.DROP_MOVE | DND.DROP_COPY);
        dropTarget.setTransfer(new Transfer[] { FileTransfer.getInstance() });
        dropTarget.addDropListener(new DropTargetAdapter() {
            public void dragEnter(DropTargetEvent event) {
                isDropping = true;
            }

            public void dragLeave(DropTargetEvent event) {
                isDropping = false;
                handleDeferredRefresh();
            }

            public void dragOver(DropTargetEvent event) {
                dropTargetValidate(event, getTargetFile(event));
                event.feedback |= DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL;
            }

            public void drop(DropTargetEvent event) {
                File targetFile = getTargetFile(event);
                if (dropTargetValidate(event, targetFile))
                    dropTargetHandleDrop(event, targetFile);
            }

            private File getTargetFile(DropTargetEvent event) {
                // Determine the target File for the drop
                TreeItem item = tree.getItem(tree.toControl(new Point(event.x, event.y)));
                File targetFile = null;
                if (item != null) {
                    // We are over a particular item in the tree, use the item's
                    // file
                    targetFile = (File) item.getData(TREEITEMDATA_FILE);
                }
                return targetFile;
            }
        });
        return dropTarget;
    }

    /**
     * Handles expand events on a tree item.
     * 
     * @param item
     *            the TreeItem to fill in
     */
    private void treeExpandItem(TreeItem item) {
        shell.setCursor(iconCache.stockCursors[iconCache.cursorWait]);
        final Object stub = item.getData(TREEITEMDATA_STUB);
        if (stub == null)
            treeRefreshItem(item, true);
        shell.setCursor(iconCache.stockCursors[iconCache.cursorDefault]);
    }

    /**
     * Traverse the entire tree and update only what has changed.
     * 
     * @param roots
     *            the root directory listing
     */
    private void treeRefresh(File[] masterFiles) {
        TreeItem[] items = tree.getItems();
        int masterIndex = 0;
        int itemIndex = 0;
        for (int i = 0; i < items.length; ++i) {
            final TreeItem item = items[i];
            final File itemFile = (File) item.getData(TREEITEMDATA_FILE);
            if ((itemFile == null) || (masterIndex == masterFiles.length)) {
                // remove bad item or placeholder
                item.dispose();
                continue;
            }
            final File masterFile = masterFiles[masterIndex];
            int compare = compareFiles(masterFile, itemFile);
            if (compare == 0) {
                // same file, update it
                treeRefreshItem(item, false);
                ++itemIndex;
                ++masterIndex;
            } else if (compare < 0) {
                // should appear before file, insert it
                TreeItem newItem = new TreeItem(tree, SWT.NULL, itemIndex);
                treeInitVolume(newItem, masterFile);
                new TreeItem(newItem, SWT.NULL); // placeholder child item to
                // get "expand" button
                ++itemIndex;
                ++masterIndex;
                --i;
            } else {
                // should appear after file, delete stale item
                item.dispose();
            }
        }
        for (; masterIndex < masterFiles.length; ++masterIndex) {
            final File masterFile = masterFiles[masterIndex];
            TreeItem newItem = new TreeItem(tree, SWT.NULL);
            treeInitVolume(newItem, masterFile);
            new TreeItem(newItem, SWT.NULL); // placeholder child item to get
            // "expand" button
        }
    }

    /**
     * Traverse an item in the tree and update only what has changed.
     * 
     * @param dirItem
     *            the tree item of the directory
     * @param forcePopulate
     *            true iff we should populate non-expanded items as well
     */
    private void treeRefreshItem(TreeItem dirItem, boolean forcePopulate) {
        final File dir = (File) dirItem.getData(TREEITEMDATA_FILE);

        if (!forcePopulate && !dirItem.getExpanded()) {
            // Refresh non-expanded item
            if (dirItem.getData(TREEITEMDATA_STUB) != null) {
                treeItemRemoveAll(dirItem);
                new TreeItem(dirItem, SWT.NULL); // placeholder child item to
                // get "expand" button
                dirItem.setData(TREEITEMDATA_STUB, null);
            }
            return;
        }
        // Refresh expanded item
        dirItem.setData(TREEITEMDATA_STUB, this); // clear stub flag

        /* Get directory listing */
        File[] subFiles = (dir != null) ? SWTFileViewerDemo.getDirectoryList(dir) : null;
        if (subFiles == null || subFiles.length == 0) {
            /* Error or no contents */
            treeItemRemoveAll(dirItem);
            dirItem.setExpanded(false);
            return;
        }

        /* Refresh sub-items */
        TreeItem[] items = dirItem.getItems();
        final File[] masterFiles = subFiles;
        int masterIndex = 0;
        int itemIndex = 0;
        File masterFile = null;
        for (int i = 0; i < items.length; ++i) {
            while ((masterFile == null) && (masterIndex < masterFiles.length)) {
                masterFile = masterFiles[masterIndex++];
                if (!masterFile.isDirectory())
                    masterFile = null;
            }

            final TreeItem item = items[i];
            final File itemFile = (File) item.getData(TREEITEMDATA_FILE);
            if ((itemFile == null) || (masterFile == null)) {
                // remove bad item or placeholder
                item.dispose();
                continue;
            }
            int compare = compareFiles(masterFile, itemFile);
            if (compare == 0) {
                // same file, update it
                treeRefreshItem(item, false);
                masterFile = null;
                ++itemIndex;
            } else if (compare < 0) {
                // should appear before file, insert it
                TreeItem newItem = new TreeItem(dirItem, SWT.NULL, itemIndex);
                treeInitFolder(newItem, masterFile);
                new TreeItem(newItem, SWT.NULL); // add a placeholder child
                // item so we get the
                // "expand" button
                masterFile = null;
                ++itemIndex;
                --i;
            } else {
                // should appear after file, delete stale item
                item.dispose();
            }
        }
        while ((masterFile != null) || (masterIndex < masterFiles.length)) {
            if (masterFile != null) {
                TreeItem newItem = new TreeItem(dirItem, SWT.NULL);
                treeInitFolder(newItem, masterFile);
                new TreeItem(newItem, SWT.NULL); // add a placeholder child
                // item so we get the
                // "expand" button
                if (masterIndex == masterFiles.length)
                    break;
            }
            masterFile = masterFiles[masterIndex++];
            if (!masterFile.isDirectory())
                masterFile = null;
        }
    }

    /**
     * Foreign method: removes all children of a TreeItem.
     * 
     * @param treeItem
     *            the TreeItem
     */
    private static void treeItemRemoveAll(TreeItem treeItem) {
        final TreeItem[] children = treeItem.getItems();
        for (int i = 0; i < children.length; ++i) {
            children[i].dispose();
        }
    }

    /**
     * Initializes a folder item.
     * 
     * @param item
     *            the TreeItem to initialize
     * @param folder
     *            the File associated with this TreeItem
     */
    private void treeInitFolder(TreeItem item, File folder) {
        item.setText(folder.getName());
        item.setImage(iconCache.stockImages[iconCache.iconClosedFolder]);
        item.setData(TREEITEMDATA_FILE, folder);
        item.setData(TREEITEMDATA_IMAGEEXPANDED, iconCache.stockImages[iconCache.iconOpenFolder]);
        item.setData(TREEITEMDATA_IMAGECOLLAPSED, iconCache.stockImages[iconCache.iconClosedFolder]);
    }

    /**
     * Initializes a volume item.
     * 
     * @param item
     *            the TreeItem to initialize
     * @param volume
     *            the File associated with this TreeItem
     */
    private void treeInitVolume(TreeItem item, File volume) {
        item.setText(volume.getPath());
        item.setImage(iconCache.stockImages[iconCache.iconClosedDrive]);
        item.setData(TREEITEMDATA_FILE, volume);
        item.setData(TREEITEMDATA_IMAGEEXPANDED, iconCache.stockImages[iconCache.iconOpenDrive]);
        item.setData(TREEITEMDATA_IMAGECOLLAPSED, iconCache.stockImages[iconCache.iconClosedDrive]);
    }

    /**
     * Creates the file details table.
     * 
     * @param parent
     *            the parent control
     */
    private void createTableView(Composite parent) {
        Composite composite = new Composite(parent, SWT.NONE);
        GridLayout gridLayout = new GridLayout();
        gridLayout.numColumns = 1;
        gridLayout.marginHeight = gridLayout.marginWidth = 2;
        gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
        composite.setLayout(gridLayout);
        tableContentsOfLabel = new Label(composite, SWT.BORDER);
        tableContentsOfLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_FILL));

        table = new Table(composite, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI | SWT.FULL_SELECTION);
        table.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL));

        for (int i = 0; i < tableTitles.length; ++i) {
            TableColumn column = new TableColumn(table, SWT.NONE);
            column.setText(tableTitles[i]);
            column.setWidth(tableWidths[i]);
        }
        table.setHeaderVisible(true);
        table.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                notifySelectedFiles(getSelectedFiles());
            }

            public void widgetDefaultSelected(SelectionEvent event) {
                doDefaultFileAction(getSelectedFiles());
            }

            private File[] getSelectedFiles() {
                final TableItem[] items = table.getSelection();
                final File[] files = new File[items.length];

                for (int i = 0; i < items.length; ++i) {
                    files[i] = (File) items[i].getData(TABLEITEMDATA_FILE);
                }
                return files;
            }
        });

        createTableDragSource(table);
        createTableDropTarget(table);
    }

    /**
     * Creates the Drag & Drop DragSource for items being dragged from the
     * table.
     * 
     * @return the DragSource for the table
     */
    private DragSource createTableDragSource(final Table table) {
        DragSource dragSource = new DragSource(table, DND.DROP_MOVE | DND.DROP_COPY);
        dragSource.setTransfer(new Transfer[] { FileTransfer.getInstance() });
        dragSource.addDragListener(new DragSourceListener() {
            TableItem[] dndSelection = null;

            String[] sourceNames = null;

            public void dragStart(DragSourceEvent event) {
                dndSelection = table.getSelection();
                sourceNames = null;
                event.doit = dndSelection.length > 0;
                isDragging = true;
            }

            public void dragFinished(DragSourceEvent event) {
                dragSourceHandleDragFinished(event, sourceNames);
                dndSelection = null;
                sourceNames = null;
                isDragging = false;
                handleDeferredRefresh();
            }

            public void dragSetData(DragSourceEvent event) {
                if (dndSelection == null || dndSelection.length == 0)
                    return;
                if (!FileTransfer.getInstance().isSupportedType(event.dataType))
                    return;

                sourceNames = new String[dndSelection.length];
                for (int i = 0; i < dndSelection.length; i++) {
                    File file = (File) dndSelection[i].getData(TABLEITEMDATA_FILE);
                    sourceNames[i] = file.getAbsolutePath();
                }
                event.data = sourceNames;
            }
        });
        return dragSource;
    }

    /**
     * Creates the Drag & Drop DropTarget for items being dropped onto the
     * table.
     * 
     * @return the DropTarget for the table
     */
    private DropTarget createTableDropTarget(final Table table) {
        DropTarget dropTarget = new DropTarget(table, DND.DROP_MOVE | DND.DROP_COPY);
        dropTarget.setTransfer(new Transfer[] { FileTransfer.getInstance() });
        dropTarget.addDropListener(new DropTargetAdapter() {
            public void dragEnter(DropTargetEvent event) {
                isDropping = true;
            }

            public void dragLeave(DropTargetEvent event) {
                isDropping = false;
                handleDeferredRefresh();
            }

            public void dragOver(DropTargetEvent event) {
                dropTargetValidate(event, getTargetFile(event));
                event.feedback |= DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL;
            }

            public void drop(DropTargetEvent event) {
                File targetFile = getTargetFile(event);
                if (dropTargetValidate(event, targetFile))
                    dropTargetHandleDrop(event, targetFile);
            }

            private File getTargetFile(DropTargetEvent event) {
                // Determine the target File for the drop
                TableItem item = table.getItem(table.toControl(new Point(event.x, event.y)));
                File targetFile = null;
                if (item == null) {
                    // We are over an unoccupied area of the table.
                    // If it is a COPY, we can use the table's root file.
                    if (event.detail == DND.DROP_COPY) {
                        targetFile = (File) table.getData(TABLEDATA_DIR);
                    }
                } else {
                    // We are over a particular item in the table, use the
                    // item's file
                    targetFile = (File) item.getData(TABLEITEMDATA_FILE);
                }
                return targetFile;
            }
        });
        return dropTarget;
    }

    /**
     * Notifies the application components that a new current directory has been
     * selected
     * 
     * @param dir
     *            the directory that was selected, null is ignored
     */
    void notifySelectedDirectory(File dir) {
        if (dir == null)
            return;
        if (currentDirectory != null && dir.equals(currentDirectory))
            return;
        currentDirectory = dir;
        notifySelectedFiles(null);

        /*
         * Shell: Sets the title to indicate the selected directory
         */
        shell.setText(getResourceString("Title", new Object[] { currentDirectory.getPath() }));

        /*
         * Table view: Displays the contents of the selected directory.
         */
        workerUpdate(dir, false);

        /*
         * Combo view: Sets the combo box to point to the selected directory.
         */
        final File[] comboRoots = (File[]) combo.getData(COMBODATA_ROOTS);
        int comboEntry = -1;
        if (comboRoots != null) {
            for (int i = 0; i < comboRoots.length; ++i) {
                if (dir.equals(comboRoots[i])) {
                    comboEntry = i;
                    break;
                }
            }
        }
        if (comboEntry == -1)
            combo.setText(dir.getPath());
        else
            combo.select(comboEntry);

        /*
         * Tree view: If not already expanded, recursively expands the parents
         * of the specified directory until it is visible.
         */
        Vector /* of File */ path = new Vector();
        // Build a stack of paths from the root of the tree
        while (dir != null) {
            path.add(dir);
            dir = dir.getParentFile();
        }
        // Recursively expand the tree to get to the specified directory
        TreeItem[] items = tree.getItems();
        TreeItem lastItem = null;
        for (int i = path.size() - 1; i >= 0; --i) {
            final File pathElement = (File) path.elementAt(i);

            // Search for a particular File in the array of tree items
            // No guarantee that the items are sorted in any recognizable
            // fashion, so we'll
            // just sequential scan. There shouldn't be more than a few thousand
            // entries.
            TreeItem item = null;
            for (int k = 0; k < items.length; ++k) {
                item = items[k];
                if (item.isDisposed())
                    continue;
                final File itemFile = (File) item.getData(TREEITEMDATA_FILE);
                if (itemFile != null && itemFile.equals(pathElement))
                    break;
            }
            if (item == null)
                break;
            lastItem = item;
            if (i != 0 && !item.getExpanded()) {
                treeExpandItem(item);
                item.setExpanded(true);
            }
            items = item.getItems();
        }
        tree.setSelection((lastItem != null) ? new TreeItem[] { lastItem } : new TreeItem[0]);
    }

    /**
     * Notifies the application components that files have been selected
     * 
     * @param files
     *            the files that were selected, null or empty array indicates no
     *            active selection
     */
    void notifySelectedFiles(File[] files) {
        /*
         * Details: Update the details that are visible on screen.
         */
        if ((files != null) && (files.length != 0)) {
            numObjectsLabel.setText(getResourceString("details.NumberOfSelectedFiles.text",
                    new Object[] { new Integer(files.length) }));
            long fileSize = 0L;
            for (int i = 0; i < files.length; ++i) {
                fileSize += files[i].length();
            }
            diskSpaceLabel.setText(getResourceString("details.FileSize.text", new Object[] { new Long(fileSize) }));
        } else {
            // No files selected
            diskSpaceLabel.setText("");
            if (currentDirectory != null) {
                int numObjects = getDirectoryList(currentDirectory).length;
                numObjectsLabel.setText(getResourceString("details.DirNumberOfObjects.text",
                        new Object[] { new Integer(numObjects) }));
            } else {
                numObjectsLabel.setText("");
            }
        }
    }

    /**
     * Notifies the application components that files must be refreshed
     * 
     * @param files
     *            the files that need refreshing, empty array is a no-op, null
     *            refreshes all
     */
    void notifyRefreshFiles(File[] files) {
        if (files != null && files.length == 0)
            return;

        if ((deferredRefreshRequested) && (deferredRefreshFiles != null) && (files != null)) {
            // merge requests
            File[] newRequest = new File[deferredRefreshFiles.length + files.length];
            System.arraycopy(deferredRefreshFiles, 0, newRequest, 0, deferredRefreshFiles.length);
            System.arraycopy(files, 0, newRequest, deferredRefreshFiles.length, files.length);
            deferredRefreshFiles = newRequest;
        } else {
            deferredRefreshFiles = files;
            deferredRefreshRequested = true;
        }
        handleDeferredRefresh();
    }

    /**
     * Handles deferred Refresh notifications (due to Drag & Drop)
     */
    void handleDeferredRefresh() {
        if (isDragging || isDropping || !deferredRefreshRequested)
            return;
        if (progressDialog != null) {
            progressDialog.close();
            progressDialog = null;
        }

        deferredRefreshRequested = false;
        File[] files = deferredRefreshFiles;
        deferredRefreshFiles = null;

        shell.setCursor(iconCache.stockCursors[iconCache.cursorWait]);

        /*
         * Table view: Refreshes information about any files in the list and
         * their children.
         */
        boolean refreshTable = false;
        if (files != null) {
            for (int i = 0; i < files.length; ++i) {
                final File file = files[i];
                if (file.equals(currentDirectory)) {
                    refreshTable = true;
                    break;
                }
                File parentFile = file.getParentFile();
                if ((parentFile != null) && (parentFile.equals(currentDirectory))) {
                    refreshTable = true;
                    break;
                }
            }
        } else
            refreshTable = true;
        if (refreshTable)
            workerUpdate(currentDirectory, true);

        /*
         * Combo view: Refreshes the list of roots
         */
        final File[] roots = getRoots();

        if (files == null) {
            boolean refreshCombo = false;
            final File[] comboRoots = (File[]) combo.getData(COMBODATA_ROOTS);

            if ((comboRoots != null) && (comboRoots.length == roots.length)) {
                for (int i = 0; i < roots.length; ++i) {
                    if (!roots[i].equals(comboRoots[i])) {
                        refreshCombo = true;
                        break;
                    }
                }
            } else
                refreshCombo = true;

            if (refreshCombo) {
                combo.removeAll();
                combo.setData(COMBODATA_ROOTS, roots);
                for (int i = 0; i < roots.length; ++i) {
                    final File file = roots[i];
                    combo.add(file.getPath());
                }
            }
        }

        /*
         * Tree view: Refreshes information about any files in the list and
         * their children.
         */
        treeRefresh(roots);

        // Remind everyone where we are in the filesystem
        final File dir = currentDirectory;
        currentDirectory = null;
        notifySelectedDirectory(dir);

        shell.setCursor(iconCache.stockCursors[iconCache.cursorDefault]);
    }

    /**
     * Performs the default action on a set of files.
     * 
     * @param files
     *            the array of files to process
     */
    void doDefaultFileAction(File[] files) {
        // only uses the 1st file (for now)
        if (files.length == 0)
            return;
        final File file = files[0];

        if (file.isDirectory()) {
            notifySelectedDirectory(file);
        } else {
            final String fileName = file.getAbsolutePath();
            if (!Program.launch(fileName)) {
                MessageBox dialog = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
                dialog.setMessage(getResourceString("error.FailedLaunch.message", new Object[] { fileName }));
                dialog.setText(shell.getText());
                dialog.open();
            }
        }
    }

    /**
     * Navigates to the parent directory
     */
    void doParent() {
        if (currentDirectory == null)
            return;
        File parentDirectory = currentDirectory.getParentFile();
        notifySelectedDirectory(parentDirectory);
    }

    /**
     * Performs a refresh
     */
    void doRefresh() {
        notifyRefreshFiles(null);
    }

    /**
     * Validates a drop target as a candidate for a drop operation.
     * <p>
     * Used in dragOver() and dropAccept().<br>
     * Note event.detail is set to DND.DROP_NONE by this method if the target is
     * not valid.
     * </p>
     * 
     * @param event
     *            the DropTargetEvent to validate
     * @param targetFile
     *            the File representing the drop target location under
     *            inspection, or null if none
     */
    private boolean dropTargetValidate(DropTargetEvent event, File targetFile) {
        if (targetFile != null && targetFile.isDirectory()) {
            if (event.detail != DND.DROP_COPY && event.detail != DND.DROP_MOVE) {
                event.detail = DND.DROP_MOVE;
            }
        } else {
            event.detail = DND.DROP_NONE;
        }
        return event.detail != DND.DROP_NONE;
    }

    /**
     * Handles a drop on a dropTarget.
     * <p>
     * Used in drop().<br>
     * Note event.detail is modified by this method.
     * </p>
     * 
     * @param event
     *            the DropTargetEvent passed as parameter to the drop() method
     * @param targetFile
     *            the File representing the drop target location under
     *            inspection, or null if none
     */
    private void dropTargetHandleDrop(DropTargetEvent event, File targetFile) {
        // Get dropped data (an array of filenames)
        if (!dropTargetValidate(event, targetFile))
            return;
        final String[] sourceNames = (String[]) event.data;
        if (sourceNames == null)
            event.detail = DND.DROP_NONE;
        if (event.detail == DND.DROP_NONE)
            return;

        // Open progress dialog
        progressDialog = new ProgressDialog(shell,
                (event.detail == DND.DROP_MOVE) ? ProgressDialog.MOVE : ProgressDialog.COPY);
        progressDialog.setTotalWorkUnits(sourceNames.length);
        progressDialog.open();

        // Copy each file
        Vector /* of File */ processedFiles = new Vector();
        for (int i = 0; (i < sourceNames.length) && (!progressDialog.isCancelled()); i++) {
            final File source = new File(sourceNames[i]);
            final File dest = new File(targetFile, source.getName());
            if (source.equals(dest))
                continue; // ignore if in same location

            progressDialog.setDetailFile(source, ProgressDialog.COPY);
            while (!progressDialog.isCancelled()) {
                if (copyFileStructure(source, dest)) {
                    processedFiles.add(source);
                    break;
                } else if (!progressDialog.isCancelled()) {
                    if (event.detail == DND.DROP_MOVE && (!isDragging)) {
                        // It is not possible to notify an external drag source
                        // that a drop
                        // operation was only partially successful. This is
                        // particularly a
                        // problem for DROP_MOVE operations since unless the
                        // source gets
                        // DROP_NONE, it will delete the original data including
                        // bits that
                        // may not have been transferred successfully.
                        MessageBox box = new MessageBox(shell, SWT.ICON_ERROR | SWT.RETRY | SWT.CANCEL);
                        box.setText(getResourceString("dialog.FailedCopy.title"));
                        box.setMessage(
                                getResourceString("dialog.FailedCopy.description", new Object[] { source, dest }));
                        int button = box.open();
                        if (button == SWT.CANCEL) {
                            i = sourceNames.length;
                            event.detail = DND.DROP_NONE;
                            break;
                        }
                    } else {
                        // We can recover gracefully from errors if the drag
                        // source belongs
                        // to this application since it will look at
                        // processedDropFiles.
                        MessageBox box = new MessageBox(shell, SWT.ICON_ERROR | SWT.ABORT | SWT.RETRY | SWT.IGNORE);
                        box.setText(getResourceString("dialog.FailedCopy.title"));
                        box.setMessage(
                                getResourceString("dialog.FailedCopy.description", new Object[] { source, dest }));
                        int button = box.open();
                        if (button == SWT.ABORT)
                            i = sourceNames.length;
                        if (button != SWT.RETRY)
                            break;
                    }
                }
                progressDialog.addProgress(1);
            }
        }
        if (isDragging) {
            // Remember exactly which files we processed
            processedDropFiles = ((File[]) processedFiles.toArray(new File[processedFiles.size()]));
        } else {
            progressDialog.close();
            progressDialog = null;
        }
        notifyRefreshFiles(new File[] { targetFile });
    }

    /**
     * Handles the completion of a drag on a dragSource.
     * <p>
     * Used in dragFinished().<br>
     * </p>
     * 
     * @param event
     *            the DragSourceEvent passed as parameter to the dragFinished()
     *            method
     * @param sourceNames
     *            the names of the files that were dragged (event.data is
     *            invalid)
     */
    private void dragSourceHandleDragFinished(DragSourceEvent event, String[] sourceNames) {
        if (sourceNames == null)
            return;
        if (event.detail != DND.DROP_MOVE)
            return;

        // Get array of files that were actually transferred
        final File[] sourceFiles;
        if (processedDropFiles != null) {
            sourceFiles = processedDropFiles;
        } else {
            sourceFiles = new File[sourceNames.length];
            for (int i = 0; i < sourceNames.length; ++i)
                sourceFiles[i] = new File(sourceNames[i]);
        }
        if (progressDialog == null)
            progressDialog = new ProgressDialog(shell, ProgressDialog.MOVE);
        progressDialog.setTotalWorkUnits(sourceFiles.length);
        progressDialog.setProgress(0);
        progressDialog.open();

        // Delete each file
        for (int i = 0; (i < sourceFiles.length) && (!progressDialog.isCancelled()); i++) {
            final File source = sourceFiles[i];
            progressDialog.setDetailFile(source, ProgressDialog.DELETE);
            while (!progressDialog.isCancelled()) {
                if (deleteFileStructure(source)) {
                    break;
                } else if (!progressDialog.isCancelled()) {
                    MessageBox box = new MessageBox(shell, SWT.ICON_ERROR | SWT.ABORT | SWT.RETRY | SWT.IGNORE);
                    box.setText(getResourceString("dialog.FailedDelete.title"));
                    box.setMessage(getResourceString("dialog.FailedDelete.description", new Object[] { source }));
                    int button = box.open();
                    if (button == SWT.ABORT)
                        i = sourceNames.length;
                    if (button == SWT.RETRY)
                        break;
                }
            }
            progressDialog.addProgress(1);
        }
        notifyRefreshFiles(sourceFiles);
        progressDialog.close();
        progressDialog = null;
    }

    /**
     * Gets filesystem root entries
     * 
     * @return an array of Files corresponding to the root directories on the
     *         platform, may be empty but not null
     */
    File[] getRoots() {
        /*
         * On JDK 1.22 only...
         */
        // return File.listRoots();
        /*
         * On JDK 1.1.7 and beyond... -- PORTABILITY ISSUES HERE --
         */
        if (System.getProperty("os.name").indexOf("Windows") != -1) {
            Vector /* of File */ list = new Vector();
            list.add(new File(DRIVE_A));
            list.add(new File(DRIVE_B));
            for (char i = 'c'; i <= 'z'; ++i) {
                File drive = new File(i + ":" + File.separator);
                if (drive.isDirectory() && drive.exists()) {
                    list.add(drive);
                    if (initial && i == 'c') {
                        currentDirectory = drive;
                        initial = false;
                    }
                }
            }
            File[] roots = (File[]) list.toArray(new File[list.size()]);
            sortFiles(roots);
            return roots;
        } else {
            File root = new File(File.separator);
            if (initial) {
                currentDirectory = root;
                initial = false;
            }
            return new File[] { root };
        }
    }

    /**
     * Gets a directory listing
     * 
     * @param file
     *            the directory to be listed
     * @return an array of files this directory contains, may be empty but not
     *         null
     */
    static File[] getDirectoryList(File file) {
        File[] list = file.listFiles();
        if (list == null)
            return new File[0];
        sortFiles(list);
        return list;
    }

    /**
     * Copies a file or entire directory structure.
     * 
     * @param oldFile
     *            the location of the old file or directory
     * @param newFile
     *            the location of the new file or directory
     * @return true iff the operation succeeds without errors
     */
    boolean copyFileStructure(File oldFile, File newFile) {
        if (oldFile == null || newFile == null)
            return false;

        // ensure that newFile is not a child of oldFile or a dupe
        File searchFile = newFile;
        do {
            if (oldFile.equals(searchFile))
                return false;
            searchFile = searchFile.getParentFile();
        } while (searchFile != null);

        if (oldFile.isDirectory()) {
            /*
             * Copy a directory
             */
            if (progressDialog != null) {
                progressDialog.setDetailFile(oldFile, ProgressDialog.COPY);
            }
            if (simulateOnly) {
                // System.out.println(getResourceString("simulate.DirectoriesCreated.text",
                // new Object[] { newFile.getPath() }));
            } else {
                if (!newFile.mkdirs())
                    return false;
            }
            File[] subFiles = oldFile.listFiles();
            if (subFiles != null) {
                if (progressDialog != null) {
                    progressDialog.addWorkUnits(subFiles.length);
                }
                for (int i = 0; i < subFiles.length; i++) {
                    File oldSubFile = subFiles[i];
                    File newSubFile = new File(newFile, oldSubFile.getName());
                    if (!copyFileStructure(oldSubFile, newSubFile))
                        return false;
                    if (progressDialog != null) {
                        progressDialog.addProgress(1);
                        if (progressDialog.isCancelled())
                            return false;
                    }
                }
            }
        } else {
            /*
             * Copy a file
             */
            if (simulateOnly) {
                // System.out.println(getResourceString("simulate.CopyFromTo.text",
                // new Object[] { oldFile.getPath(), newFile.getPath() }));
            } else {
                FileReader in = null;
                FileWriter out = null;
                try {
                    in = new FileReader(oldFile);
                    out = new FileWriter(newFile);

                    int count;
                    while ((count = in.read()) != -1)
                        out.write(count);
                } catch (FileNotFoundException e) {
                    return false;
                } catch (IOException e) {
                    return false;
                } finally {
                    try {
                        if (in != null)
                            in.close();
                        if (out != null)
                            out.close();
                    } catch (IOException e) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Deletes a file or entire directory structure.
     * 
     * @param oldFile
     *            the location of the old file or directory
     * @return true iff the operation succeeds without errors
     */
    boolean deleteFileStructure(File oldFile) {
        if (oldFile == null)
            return false;
        if (oldFile.isDirectory()) {
            /*
             * Delete a directory
             */
            if (progressDialog != null) {
                progressDialog.setDetailFile(oldFile, ProgressDialog.DELETE);
            }
            File[] subFiles = oldFile.listFiles();
            if (subFiles != null) {
                if (progressDialog != null) {
                    progressDialog.addWorkUnits(subFiles.length);
                }
                for (int i = 0; i < subFiles.length; i++) {
                    File oldSubFile = subFiles[i];
                    if (!deleteFileStructure(oldSubFile))
                        return false;
                    if (progressDialog != null) {
                        progressDialog.addProgress(1);
                        if (progressDialog.isCancelled())
                            return false;
                    }
                }
            }
        }
        if (simulateOnly) {
            // System.out.println(getResourceString("simulate.Delete.text",
            // new Object[] { oldFile.getPath(), oldFile.getPath() }));
            return true;
        } else {
            return oldFile.delete();
        }
    }

    /**
     * Sorts files lexicographically by name.
     * 
     * @param files
     *            the array of Files to be sorted
     */
    static void sortFiles(File[] files) {
        /* Very lazy merge sort algorithm */
        sortBlock(files, 0, files.length - 1, new File[files.length]);
    }

    private static void sortBlock(File[] files, int start, int end, File[] mergeTemp) {
        final int length = end - start + 1;
        if (length < 8) {
            for (int i = end; i > start; --i) {
                for (int j = end; j > start; --j) {
                    if (compareFiles(files[j - 1], files[j]) > 0) {
                        final File temp = files[j];
                        files[j] = files[j - 1];
                        files[j - 1] = temp;
                    }
                }
            }
            return;
        }
        final int mid = (start + end) / 2;
        sortBlock(files, start, mid, mergeTemp);
        sortBlock(files, mid + 1, end, mergeTemp);
        int x = start;
        int y = mid + 1;
        for (int i = 0; i < length; ++i) {
            if ((x > mid) || ((y <= end) && compareFiles(files[x], files[y]) > 0)) {
                mergeTemp[i] = files[y++];
            } else {
                mergeTemp[i] = files[x++];
            }
        }
        for (int i = 0; i < length; ++i)
            files[i + start] = mergeTemp[i];
    }

    private static int compareFiles(File a, File b) {
        // boolean aIsDir = a.isDirectory();
        // boolean bIsDir = b.isDirectory();
        // if (aIsDir && ! bIsDir) return -1;
        // if (bIsDir && ! aIsDir) return 1;

        // sort case-sensitive files in a case-insensitive manner
        int compare = a.getName().compareToIgnoreCase(b.getName());
        if (compare == 0)
            compare = a.getName().compareTo(b.getName());
        return compare;
    }

    /*
     * This worker updates the table with file information in the background.
     * <p> Implementation notes: <ul> <li> It is designed such that it can be
     * interrupted cleanly. <li> It uses asyncExec() in some places to ensure
     * that SWT Widgets are manipulated in the right thread. Exclusive use of
     * syncExec() would be inappropriate as it would require a pair of context
     * switches between each table update operation. </ul> </p>
     */

    /**
     * Stops the worker and waits for it to terminate.
     */
    void workerStop() {
        if (workerThread == null)
            return;
        synchronized (workerLock) {
            workerCancelled = true;
            workerStopped = true;
            workerLock.notifyAll();
        }
        while (workerThread != null) {
            if (!display.readAndDispatch())
                display.sleep();
        }
    }

    /**
     * Notifies the worker that it should update itself with new data. Cancels
     * any previous operation and begins a new one.
     * 
     * @param dir
     *            the new base directory for the table, null is ignored
     * @param force
     *            if true causes a refresh even if the data is the same
     */
    void workerUpdate(File dir, boolean force) {
        if (dir == null)
            return;
        if ((!force) && (workerNextDir != null) && (workerNextDir.equals(dir)))
            return;

        synchronized (workerLock) {
            workerNextDir = dir;
            workerStopped = false;
            workerCancelled = true;
            workerLock.notifyAll();
        }
        if (workerThread == null) {
            workerThread = new Thread(workerRunnable);
            workerThread.start();
        }
    }

    /**
     * Manages the worker's thread
     */
    private final Runnable workerRunnable = new Runnable() {
        public void run() {
            while (!workerStopped) {
                synchronized (workerLock) {
                    workerCancelled = false;
                    workerStateDir = workerNextDir;
                }
                workerExecute();
                synchronized (workerLock) {
                    try {
                        if ((!workerCancelled) && (workerStateDir == workerNextDir))
                            workerLock.wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            workerThread = null;
            // wake up UI thread in case it is in a modal loop awaiting thread
            // termination
            // (see workerStop())
            display.wake();
        }
    };

    /**
     * Updates the table's contents
     */
    private void workerExecute() {
        File[] dirList;
        // Clear existing information
        display.syncExec(new Runnable() {
            public void run() {
                tableContentsOfLabel.setText(SWTFileViewerDemo.getResourceString("details.ContentsOf.text",
                        new Object[] { workerStateDir.getPath() }));
                table.removeAll();
                table.setData(TABLEDATA_DIR, workerStateDir);
            }
        });
        dirList = getDirectoryList(workerStateDir);

        for (int i = 0; (!workerCancelled) && (i < dirList.length); i++) {
            workerAddFileDetails(dirList[i]);
        }

    }

    /**
     * Adds a file's detail information to the directory list
     */
    private void workerAddFileDetails(final File file) {
        final String nameString = file.getName();
        final String dateString = dateFormat.format(new Date(file.lastModified()));
        final String sizeString;
        final String typeString;
        final Image iconImage;

        if (file.isDirectory()) {
            typeString = getResourceString("filetype.Folder");
            sizeString = "";
            iconImage = iconCache.stockImages[iconCache.iconClosedFolder];
        } else {
            sizeString = getResourceString("filesize.KB", new Object[] { new Long((file.length() + 512) / 1024) });

            int dot = nameString.lastIndexOf('.');
            if (dot != -1) {
                String extension = nameString.substring(dot);
                Program program = Program.findProgram(extension);
                if (program != null) {
                    typeString = program.getName();
                    iconImage = iconCache.getIconFromProgram(program);
                } else {
                    typeString = getResourceString("filetype.Unknown", new Object[] { extension.toUpperCase() });
                    iconImage = iconCache.stockImages[iconCache.iconFile];
                }
            } else {
                typeString = getResourceString("filetype.None");
                iconImage = iconCache.stockImages[iconCache.iconFile];
            }
        }
        final String[] strings = new String[] { nameString, sizeString, typeString, dateString };

        display.syncExec(new Runnable() {
            public void run() {
                // guard against the shell being closed before this runs
                if (shell.isDisposed())
                    return;
                TableItem tableItem = new TableItem(table, 0);
                tableItem.setText(strings);
                tableItem.setImage(iconImage);
                tableItem.setData(TABLEITEMDATA_FILE, file);
            }
        });
    }

    /**
     * Instances of this class manage a progress dialog for file operations.
     */
    class ProgressDialog {
        public final static int COPY = 0;

        public final static int DELETE = 1;

        public final static int MOVE = 2;

        Shell shell;

        Label messageLabel, detailLabel;

        ProgressBar progressBar;

        Button cancelButton;

        boolean isCancelled = false;

        final String operationKeyName[] = { "Copy", "Delete", "Move" };

        /**
         * Creates a progress dialog but does not open it immediately.
         * 
         * @param parent
         *            the parent Shell
         * @param style
         *            one of COPY, MOVE
         */
        public ProgressDialog(Shell parent, int style) {
            shell = new Shell(parent, SWT.BORDER | SWT.TITLE | SWT.APPLICATION_MODAL);
            GridLayout gridLayout = new GridLayout();
            shell.setLayout(gridLayout);
            shell.setText(getResourceString("progressDialog." + operationKeyName[style] + ".title"));
            shell.addShellListener(new ShellAdapter() {
                public void shellClosed(ShellEvent e) {
                    isCancelled = true;
                }
            });

            messageLabel = new Label(shell, SWT.HORIZONTAL);
            messageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_FILL));
            messageLabel.setText(getResourceString("progressDialog." + operationKeyName[style] + ".description"));

            progressBar = new ProgressBar(shell, SWT.HORIZONTAL | SWT.WRAP);
            progressBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_FILL));
            progressBar.setMinimum(0);
            progressBar.setMaximum(0);

            detailLabel = new Label(shell, SWT.HORIZONTAL);
            GridData gridData = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_BEGINNING);
            gridData.widthHint = 400;
            detailLabel.setLayoutData(gridData);

            cancelButton = new Button(shell, SWT.PUSH);
            cancelButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_FILL));
            cancelButton.setText(getResourceString("progressDialog.cancelButton.text"));
            cancelButton.addSelectionListener(new SelectionAdapter() {
                public void widgetSelected(SelectionEvent e) {
                    isCancelled = true;
                    cancelButton.setEnabled(false);
                }
            });
        }

        /**
         * Sets the detail text to show the filename along with a string
         * representing the operation being performed on that file.
         * 
         * @param file
         *            the file to be detailed
         * @param operation
         *            one of COPY, DELETE
         */
        public void setDetailFile(File file, int operation) {
            detailLabel.setText(getResourceString("progressDialog." + operationKeyName[operation] + ".operation",
                    new Object[] { file }));
        }

        /**
         * Returns true if the Cancel button was been clicked.
         * 
         * @return true if the Cancel button was clicked.
         */
        public boolean isCancelled() {
            return isCancelled;
        }

        /**
         * Sets the total number of work units to be performed.
         * 
         * @param work
         *            the total number of work units
         */
        public void setTotalWorkUnits(int work) {
            progressBar.setMaximum(work);
        }

        /**
         * Adds to the total number of work units to be performed.
         * 
         * @param work
         *            the number of work units to add
         */
        public void addWorkUnits(int work) {
            setTotalWorkUnits(progressBar.getMaximum() + work);
        }

        /**
         * Sets the progress of completion of the total work units.
         * 
         * @param work
         *            the total number of work units completed
         */
        public void setProgress(int work) {
            progressBar.setSelection(work);
            while (display.readAndDispatch()) {
            } // enable event processing
        }

        /**
         * Adds to the progress of completion of the total work units.
         * 
         * @param work
         *            the number of work units completed to add
         */
        public void addProgress(int work) {
            setProgress(progressBar.getSelection() + work);
        }

        /**
         * Opens the dialog.
         */
        public void open() {
            shell.pack();
            final Shell parentShell = (Shell) shell.getParent();
            Rectangle rect = parentShell.getBounds();
            Rectangle bounds = shell.getBounds();
            bounds.x = rect.x + (rect.width - bounds.width) / 2;
            bounds.y = rect.y + (rect.height - bounds.height) / 2;
            shell.setBounds(bounds);
            shell.open();
        }

        /**
         * Closes the dialog and disposes its resources.
         */
        public void close() {
            shell.close();
            shell.dispose();
            shell = null;
            messageLabel = null;
            detailLabel = null;
            progressBar = null;
            cancelButton = null;
        }
    }
}

/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors: IBM Corporation - initial API and implementation
 ******************************************************************************/

/**
 * Manages icons for the application. This is necessary as we could easily end
 * up creating thousands of icons bearing the same image.
 */
class IconCache {
    // Stock images
    public final int shellIcon = 0, iconClosedDrive = 1, iconClosedFolder = 2, iconFile = 3, iconOpenDrive = 4,
            iconOpenFolder = 5, cmdCopy = 6, cmdCut = 7, cmdDelete = 8, cmdParent = 9, cmdPaste = 10, cmdPrint = 11,
            cmdRefresh = 12, cmdRename = 13, cmdSearch = 14;

    public final String[] stockImageLocations = { "generic_example.gif", "icon_ClosedDrive.gif",
            "icon_ClosedFolder.gif", "icon_File.gif", "icon_OpenDrive.gif", "icon_OpenFolder.gif", "cmd_Copy.gif",
            "cmd_Cut.gif", "cmd_Delete.gif", "cmd_Parent.gif", "cmd_Paste.gif", "cmd_Print.gif", "cmd_Refresh.gif",
            "cmd_Rename.gif", "cmd_Search.gif" };

    public Image stockImages[];

    // Stock cursors
    public final int cursorDefault = 0, cursorWait = 1;

    public Cursor stockCursors[];

    // Cached icons
    private Hashtable iconCache; /* map Program to Image */

    public IconCache() {
    }

    /**
     * Loads the resources
     * 
     * @param display
     *            the display
     */
    public void initResources(Display display) {
        if (stockImages == null) {
            stockImages = new Image[stockImageLocations.length];

            for (int i = 0; i < stockImageLocations.length; ++i) {
                Image image = createStockImage(display, stockImageLocations[i]);
                if (image == null) {
                    freeResources();
                    throw new IllegalStateException(
                            SWTFileViewerDemo.getResourceString("error.CouldNotLoadResources"));
                }
                stockImages[i] = image;
            }
        }
        if (stockCursors == null) {
            stockCursors = new Cursor[] { null, new Cursor(display, SWT.CURSOR_WAIT) };
        }
        iconCache = new Hashtable();
    }

    /**
     * Frees the resources
     */
    public void freeResources() {
        if (stockImages != null) {
            for (int i = 0; i < stockImages.length; ++i) {
                final Image image = stockImages[i];
                if (image != null)
                    image.dispose();
            }
            stockImages = null;
        }
        if (iconCache != null) {
            for (Enumeration it = iconCache.elements(); it.hasMoreElements();) {
                Image image = (Image) it.nextElement();
                image.dispose();
            }
        }
        if (stockCursors != null) {
            for (int i = 0; i < stockCursors.length; ++i) {
                final Cursor cursor = stockCursors[i];
                if (cursor != null)
                    cursor.dispose();
            }
            stockCursors = null;
        }
    }

    /**
     * Creates a stock image
     * 
     * @param display
     *            the display
     * @param path
     *            the relative path to the icon
     */
    private Image createStockImage(Display display, String path) {
        InputStream stream = IconCache.class.getResourceAsStream(path);
        ImageData imageData = new ImageData(stream);
        ImageData mask = imageData.getTransparencyMask();
        Image result = new Image(display, imageData, mask);
        try {
            stream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * Gets an image for a file associated with a given program
     * 
     * @param program
     *            the Program
     */
    public Image getIconFromProgram(Program program) {
        Image image = (Image) iconCache.get(program);
        if (image == null) {
            ImageData imageData = program.getImageData();
            if (imageData != null) {
                image = new Image(null, imageData, imageData.getTransparencyMask());
                iconCache.put(program, image);
            } else {
                image = stockImages[iconFile];
            }
        }
        return image;
    }
}