org.daveware.passwordmakerapp.gui.GuiMain.java Source code

Java tutorial

Introduction

Here is the source code for org.daveware.passwordmakerapp.gui.GuiMain.java

Source

/*
 * PasswordMaker Java Edition - One Password To Rule Them All
 * Copyright (C) 2011 Dave Marotti
 * 
 * This program is free software: you can redistribute it and/or modify
 * 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/>.
 */
package org.daveware.passwordmakerapp.gui;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;

import org.daveware.passwordmaker.Account;
import org.daveware.passwordmaker.BuildInfo;
import org.daveware.passwordmaker.Database;
import org.daveware.passwordmaker.DatabaseListener;
import org.daveware.passwordmaker.GlobalSettingKey;
import org.daveware.passwordmaker.PasswordMaker;
import org.daveware.passwordmaker.RDFDatabaseReader;
import org.daveware.passwordmaker.RDFDatabaseWriter;
import org.daveware.passwordmaker.SecureCharArray;
import org.daveware.passwordmaker.Utilities;
import org.daveware.passwordmakerapp.AccountComparator;
import org.daveware.passwordmakerapp.CmdLineSettings;
import org.daveware.passwordmakerapp.SortOptions;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.URLTransfer;
import org.eclipse.swt.events.ArmEvent;
import org.eclipse.swt.events.ArmListener;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.MenuAdapter;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.wb.swt.SWTResourceManager;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.widgets.Spinner;

/**
 * Implements the main window for PasswordMakerJE.
 * 
 * TODO: this should really be using validation, but I'm yet to figure out how
 * to make it work properly. I can't find the damned fieldassist package of
 * jface.
 * 
 * @author Dave Marotti
 */
public class GuiMain implements DatabaseListener {
    // Text used in the account-filter box by default
    private final static String ACCOUNT_FILTER_DESC = "type filter text";
    private final static String TITLE_STRING = "PasswordMaker Java Edition";

    private final static String EXIT_PROMPT = "Your current passwords and/or settings have been modified, would you like to save?";

    protected Shell shlPasswordMaker;
    private Text editAccount;
    private Text editDesc;
    private Text editUsername;
    private Text editMP;
    private Spinner editCopySeconds;
    private Text editUrlSearch;
    private Canvas canvasOutput;
    private Display display;
    private Combo comboCopyBehavior;
    private CLabel filterIcon;
    private Tree accountTree;
    private TreeViewer accountTreeViewer;
    private Button btnCopyToClipboard;
    private MenuItem menuItemNewAccount;
    private MenuItem menuItemEditAccount;
    private MenuItem menuItemDeleteAccount;
    private MenuItem menuItemSort;
    private Text accountFilterText;
    private Button btnShowPassword;
    private Menu menu_1;
    private MenuItem mntmhelp;
    private Menu menu_2;
    private MenuItem mntmabout;
    private MenuItem mntmFile;
    private Menu menu_3;
    private MenuItem menuItemNew;
    private MenuItem menuItemOpen;
    private MenuItem menuItemExit;
    private MenuItem menuItemNewGroup;
    private MenuItem menuItemEditGroup;
    private MenuItem menuItemDeleteGroup;
    private MenuItem menuItemSave;
    private MenuItem menuItemSaveAs;
    private ControlDecoration secondsDecoration;

    // -------- BEGIN RESOURCES THAT MUST BE DISPOSED OF
    // ---------------------------------
    private Image searchImage = null;
    private Image cancelImage = null;
    private Image passwordImage = null;
    private Image eyeImage = null;
    private Image eyeClosedImage = null;
    private Image iconImage = null;

    private Font passwordFont = null;

    private Font regularSearchFont = null;
    private Font italicsSearchFont = null;

    private AccountTreeModel accountTreeModel = new AccountTreeModel();
    private AccountTreeLabelProvider accountTreeLabelProvider = new AccountTreeLabelProvider();
    // -------- End RESOURCES THAT MUST BE DISPOSED OF
    // -----------------------------------

    private Thread countdownThread = null;
    private CmdLineSettings cmdLineSettings;
    private String currentFilename = "";
    private Account selectedAccount = null;
    private PasswordMaker pwm = null;
    private Database db = null;
    private BuildInfo buildInfo = null;
    private SortOptions sortOptions = new SortOptions();
    private boolean urlSearchEnabled = true;

    private boolean isFiltering = false;
    private boolean showPassword = true;

    private boolean closeAfterTimer = false;
    private Text editInputUrl;
    private Label lblInputUrl;
    private Text editUrl;
    private Label lblUrl_1;
    private Text editModifier;
    private Button btnModifier;
    private Text editLength;
    private Button btnLength;

    public GuiMain(CmdLineSettings c) {
        cmdLineSettings = c;
        buildInfo = new BuildInfo();
    }

    private void showException(Exception e) {
        Shell bogus = new Shell();

        if (e == null) {
            MBox.showError(bogus,
                    "An exception has occurred but the data it contained was empty. Please consider filing a bug report at http://code.google.com/p/passwordmaker-je");
        } else {
            ExceptionDlg dlg = new ExceptionDlg(bogus, e);
            dlg.open();
        }

        bogus.dispose();
    }

    /**
     * Causes everything to happen. This is called by the calling class.
     * 
     * @return
     */
    public int run() {
        open();

        return 0;
    }

    /**
     * Open the window, builds the UI, and runs the event loop.
     * 
     * @wbp.parser.entryPoint
     * 
     */
    private void open() {
        try {
            display = Display.getDefault();
            createContents();

            setupAppIcon();
            setupDragQueens();
            setupFonts();
            setupTree();
            setupDecorators();

            setupOSX();

            shlPasswordMaker.open();
            shlPasswordMaker.layout();

            pwm = new PasswordMaker();
            loadFromCmdLineSettings();

            regeneratePasswordAndDraw();

            while (!shlPasswordMaker.isDisposed()) {
                if (!display.readAndDispatch()) {
                    display.sleep();
                }
            }
        } catch (NullPointerException ne) {
            showException(ne);
            return;
        } catch (Exception e) {
            showException(e);
        }

    }

    /**
     * Assigns the application icon.
     */
    private void setupAppIcon() {
        iconImage = SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/pwmlogo.png");
        shlPasswordMaker.setImage(iconImage);
    }

    /**
     * Adds drag'n'drop to the shell for URLs (and other text, but really for
     * URLs).
     */
    private void setupDragQueens() {
        DropTarget dt = new DropTarget(shlPasswordMaker, DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK);
        dt.setTransfer(new Transfer[] { TextTransfer.getInstance(), URLTransfer.getInstance() });
        dt.addDropListener(new DropTargetAdapter() {
            public void dragEnter(DropTargetEvent e) {
                e.detail = DND.DROP_COPY;
            }

            public void dragOperationChanged(DropTargetEvent e) {
                e.detail = DND.DROP_COPY;
            }

            public void drop(org.eclipse.swt.dnd.DropTargetEvent event) {
                editUrlSearch.setText(event.data.toString());
                editMP.setFocus();

                // TODO: *could* clear out the account filtering, but I'm not
                // sure why someone would
                // add a filter and then drag & drop.
            }
        });
    }

    protected void setupDecorators() {
        ControlDecoration[] decors = { secondsDecoration };
        Image image = FieldDecorationRegistry.getDefault().getFieldDecoration(FieldDecorationRegistry.DEC_ERROR)
                .getImage();
        for (ControlDecoration dec : decors) {
            dec.setImage(image);
            dec.hide();
        }
    }

    protected void setupFonts() {
        // Clone the font of the accountFilterText widget
        Font stockFont = accountFilterText.getFont();
        FontData[] fdItalics = display.getSystemFont().getFontData();

        // Default OSX font doesn't support italics (on my system)
        if (Utilities.isMac()) {
            italicsSearchFont = new Font(display, fdItalics[0].getName(), fdItalics[0].getHeight(), 0);
        } else {
            italicsSearchFont = new Font(display, fdItalics[0].getName(), fdItalics[0].getHeight(), SWT.ITALIC);
        }

        regularSearchFont = new Font(display, stockFont.getFontData());
        accountFilterText.setFont(italicsSearchFont);

        int fontSize = cmdLineSettings.fontSize > 0 ? cmdLineSettings.fontSize : 14;
        if (cmdLineSettings.pwFont != null) {
            passwordFont = new Font(Display.getCurrent(), cmdLineSettings.pwFont, fontSize, SWT.BOLD);
        } else {
            passwordFont = new Font(Display.getCurrent(),
                    JFaceResources.getFont(JFaceResources.TEXT_FONT).getFontData()[0].getName(), fontSize,
                    SWT.BOLD);
        }
    }

    /*
     * Does OSX-specific setup.
     * 
     * OSX has a separate application menu which requires special handling. This adds the special
     * handling for the quit, preferences, and about menu items of that menu.
     */
    protected void setupOSX() {
        if (System.getProperty("os.name").toLowerCase().contains("mac")) {
            CocoaUIEnhancer enh = new CocoaUIEnhancer("PWMJE");
            enh.hookApplicationMenu(display, new Listener() {
                @Override
                public void handleEvent(Event arg0) {
                    onCloseWindow(arg0);

                }
            }, new Action("&About PWMJE") {
                public void run() {
                    AboutDlg dlg = new AboutDlg(shlPasswordMaker, SWT.SHEET);
                    dlg.open();
                }
            }, new Action("&Preferences") {
                public void run() {

                }
            });
        }
    }

    protected void setupTree() {
        // The tree must have a brain!
        accountTreeLabelProvider.loadImages();
        accountTreeViewer.setContentProvider(accountTreeModel);
        accountTreeViewer.setLabelProvider(accountTreeLabelProvider);

        accountTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {

            @Override
            public void selectionChanged(SelectionChangedEvent e) {
                if (e.getSelection().isEmpty()) {
                    selectAccount(null);
                } else if (e.getSelection() instanceof IStructuredSelection) {
                    IStructuredSelection selection = (IStructuredSelection) e.getSelection();
                    Account account = (Account) selection.iterator().next();
                    selectAccount(account);
                }
            }
        });

        accountTreeViewer.addFilter(new ViewerFilter() {
            @Override
            public boolean select(Viewer viewer, Object parentElement, Object element) {
                ArrayList<Account> parentList = new ArrayList<Account>();

                if (isFiltering == false)
                    return true;

                if (element == null)
                    return false;

                String filterText = accountFilterText.getText().toLowerCase();
                Account account = (Account) element;

                if (account.isFolder() == false)
                    return account.getName().toLowerCase().contains(filterText);

                // Search through all children looking for a matching account.
                // If there is
                // a match, then this folder must remain visible.
                parentList.add(account);
                while (parentList.size() > 0) {
                    account = parentList.remove(0);

                    for (Account child : account.getChildren()) {
                        if (child.isFolder()) {
                            parentList.add(child);
                        } else {
                            if (child.getName().toLowerCase().contains(filterText))
                                return true;
                        }
                    }
                }

                return false;
            }
        });
    }

    /**
     * Create contents of the window.
     */
    protected void createContents() {
        shlPasswordMaker = new Shell();
        shlPasswordMaker.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent arg0) {
                onDisposing(arg0);
            }
        });
        shlPasswordMaker.addListener(SWT.Close, new Listener() {

            @Override
            public void handleEvent(Event arg0) {
                // For OSX we install a different close-handler during setup
                if (System.getProperty("os.name").toLowerCase().contains("mac") == false) {
                    onCloseWindow(arg0);
                }
            }

        });

        shlPasswordMaker.getDisplay().addFilter(SWT.KeyDown, new Listener() {
            @Override
            public void handleEvent(Event e) {
                if ((e.stateMask & SWT.CTRL) == SWT.CTRL || (e.stateMask & SWT.COMMAND) == SWT.COMMAND) {
                    if (e.keyCode == 'f') {
                        accountFilterText.setFocus();
                    }
                }
            }
        });
        searchImage = SWTResourceManager.getImage(GuiMain.class,
                "/org/daveware/passwordmakerapp/icons/magglass.png");
        cancelImage = SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/cancel.png");
        eyeImage = SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/eye.png");
        eyeClosedImage = SWTResourceManager.getImage(GuiMain.class,
                "/org/daveware/passwordmakerapp/icons/eye_closed.png");

        shlPasswordMaker.setMinimumSize(new Point(855, 345));
        shlPasswordMaker.setSize(855, 412);
        setTitle();
        // shlPasswordMaker.setText(TITLE_STRING + " - " +
        // buildInfo.getVersion());
        shlPasswordMaker.setLayout(new FormLayout());

        Sash sash = new Sash(shlPasswordMaker, SWT.VERTICAL);
        Group grpAccounts = new Group(shlPasswordMaker, SWT.NONE);
        FormData fd_grpAccounts = new FormData();
        fd_grpAccounts.left = new FormAttachment(0, 5);
        fd_grpAccounts.right = new FormAttachment(sash);
        fd_grpAccounts.bottom = new FormAttachment(100, -10);
        fd_grpAccounts.top = new FormAttachment(0, 5);
        grpAccounts.setLayoutData(fd_grpAccounts);
        grpAccounts.setText("Accounts");
        grpAccounts.setLayout(new GridLayout(1, false));

        Composite composite = new Composite(grpAccounts, SWT.NONE);
        composite.addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent arg0) {
                onFilterCompositeResized(arg0);
            }
        });
        composite.setLayout(null);
        composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1));

        filterIcon = new CLabel(composite, SWT.NONE);
        filterIcon.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseUp(MouseEvent arg0) {
                onFilterIconClicked();
            }
        });
        filterIcon.setBackground(SWTResourceManager.getColor(SWT.COLOR_LIST_BACKGROUND));
        filterIcon.setImage(
                SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/magglass.png"));
        filterIcon.setBounds(187, 1, 22, 20);
        filterIcon.setText("");

        accountFilterText = new Text(composite, SWT.BORDER);
        accountFilterText.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent arg0) {
                if (arg0.keyCode == SWT.CR) {
                    editMP.setFocus();
                }
            }
        });
        accountFilterText.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                onFilterModified(arg0);
            }
        });
        accountFilterText.setText("type filter text");
        accountFilterText.setBounds(0, 0, 209, 23);
        accountFilterText.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent arg0) {
                onAccountFilterTextFocusGained();
            }

            @Override
            public void focusLost(FocusEvent arg0) {
                onAccountFilterTextFocusLost();
            }
        });

        final FormData sashData = new FormData();
        sashData.left = new FormAttachment(22);
        sashData.top = new FormAttachment(0);
        sashData.bottom = new FormAttachment(100);
        sash.setLayoutData(sashData);
        sash.addListener(SWT.Selection, new Listener() {
            public void handleEvent(Event event) {
                if (event.detail != SWT.DRAG) {
                    sashData.left = new FormAttachment(0, event.x);
                    shlPasswordMaker.layout();
                }
            }
        });
        Group grpInput = new Group(shlPasswordMaker, SWT.NONE);
        grpInput.setText("Password Input && Search");
        FormData fd_grpInput = new FormData();
        fd_grpInput.right = new FormAttachment(100, -5);
        fd_grpInput.left = new FormAttachment(sash);
        fd_grpInput.bottom = new FormAttachment(grpAccounts, 0, SWT.BOTTOM);
        fd_grpInput.top = new FormAttachment(0, 5);
        grpInput.setLayoutData(fd_grpInput);
        GridLayout gl_grpInput = new GridLayout(2, false);
        grpInput.setLayout(gl_grpInput);

        Label lblUrl = new Label(grpInput, SWT.NONE);
        lblUrl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblUrl.setText("URL Search:");

        editUrlSearch = new Text(grpInput, SWT.BORDER);
        editUrlSearch.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent arg0) {
                ((Text) (arg0.widget)).selectAll();
            }
        });
        editUrlSearch.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        editUrlSearch.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                onUrlSearchModified(arg0);
            }
        });

        Label lblAccount = new Label(grpInput, SWT.RIGHT);
        lblAccount.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblAccount.setText("Account:");

        editAccount = new Text(grpInput, SWT.BORDER);
        editAccount.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        editAccount.setEnabled(false);
        editAccount.setEditable(false);

        Label lblDescription = new Label(grpInput, SWT.RIGHT);
        lblDescription.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1));
        lblDescription.setText("Description:");

        editDesc = new Text(grpInput, SWT.BORDER | SWT.V_SCROLL);
        editDesc.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
        editDesc.setEnabled(false);
        editDesc.setEditable(false);

        lblInputUrl = new Label(grpInput, SWT.NONE);
        lblInputUrl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblInputUrl.setText("Input URL:");

        editInputUrl = new Text(grpInput, SWT.BORDER);
        editInputUrl.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent arg0) {
                ((Text) (arg0.widget)).selectAll();
            }
        });
        editInputUrl.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                regeneratePasswordAndDraw();
            }
        });
        editInputUrl.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

        lblUrl_1 = new Label(grpInput, SWT.NONE);
        lblUrl_1.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblUrl_1.setText("URL:");

        editUrl = new Text(grpInput, SWT.BORDER);
        editUrl.setEnabled(false);
        editUrl.setEditable(false);
        editUrl.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

        btnModifier = new Button(grpInput, SWT.CHECK);
        btnModifier.setToolTipText("Override the selected account's password modifier");
        btnModifier.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                editModifier.setEnabled(btnModifier.getSelection());
                editModifier.setEditable(btnModifier.getSelection());
                regeneratePasswordAndDraw();
            }
        });
        btnModifier.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        btnModifier.setAlignment(SWT.RIGHT);
        btnModifier.setText("Modifier:");

        editModifier = new Text(grpInput, SWT.BORDER);
        editModifier.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                regeneratePasswordAndDraw();
            }
        });
        editModifier.setEditable(false);
        editModifier.setEnabled(false);
        editModifier.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

        btnLength = new Button(grpInput, SWT.CHECK);
        btnLength.setToolTipText("Override the selected account's password length");
        btnLength.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                editLength.setEnabled(btnLength.getSelection());
                editLength.setEditable(btnLength.getSelection());
                regeneratePasswordAndDraw();
            }
        });
        btnLength.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        btnLength.setAlignment(SWT.RIGHT);
        btnLength.setText("Length:");

        editLength = new Text(grpInput, SWT.BORDER);
        editLength.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                regeneratePasswordAndDraw();
            }
        });
        editLength.setEnabled(false);
        editLength.setEditable(false);
        editLength.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

        Label lblUsername = new Label(grpInput, SWT.RIGHT);
        lblUsername.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblUsername.setText("Username:");

        editUsername = new Text(grpInput, SWT.BORDER);
        editUsername.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent arg0) {
                ((Text) (arg0.widget)).selectAll();
            }
        });
        editUsername.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                regeneratePasswordAndDraw();
            }
        });
        editUsername.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

        Label lblMasterPw = new Label(grpInput, SWT.RIGHT);
        lblMasterPw.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblMasterPw.setText("Master PW:");

        editMP = new Text(grpInput, SWT.BORDER | SWT.PASSWORD);
        editMP.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent arg0) {
                ((Text) (arg0.widget)).selectAll();
            }
        });
        editMP.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        editMP.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent arg0) {
                regeneratePasswordAndDraw();
            }
        });

        Composite composite_1 = new Composite(grpInput, SWT.NONE);
        GridLayout gl_composite_1 = new GridLayout(1, false);
        gl_composite_1.marginHeight = 0;
        gl_composite_1.marginWidth = 0;
        gl_composite_1.verticalSpacing = 0;
        gl_composite_1.horizontalSpacing = 0;
        composite_1.setLayout(gl_composite_1);
        GridData gd_composite_1 = new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1);
        gd_composite_1.heightHint = 33;
        composite_1.setLayoutData(gd_composite_1);

        Label lblGenerated = new Label(composite_1, SWT.RIGHT);
        lblGenerated.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        lblGenerated.setText("Generated:");

        btnShowPassword = new Button(composite_1, SWT.FLAT | SWT.TOGGLE);
        btnShowPassword.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onShowPasswordClicked();
            }
        });
        btnShowPassword.setToolTipText("Toggles the visibility of the generated password.");
        GridData gd_btnShowPassword = new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1);
        gd_btnShowPassword.widthHint = 20;
        gd_btnShowPassword.heightHint = 17;
        btnShowPassword.setLayoutData(gd_btnShowPassword);
        btnShowPassword.setImage(
                SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/eye.png"));

        Canvas outputCanvas = new Canvas(grpInput, SWT.BORDER);
        outputCanvas.setFont(SWTResourceManager.getFont("DejaVu Sans Mono", 9, SWT.NORMAL));
        outputCanvas.setLayout(new FillLayout(SWT.HORIZONTAL));
        outputCanvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1));
        outputCanvas.addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                if (passwordImage != null)
                    passwordImage.dispose();
                passwordImage = new Image(Display.getCurrent(), canvasOutput.getClientArea());
                regeneratePasswordAndDraw();
            }

        });
        outputCanvas.addPaintListener(new PaintListener() {
            public void paintControl(PaintEvent arg0) {
                if (passwordImage != null) {
                    arg0.gc.drawImage(passwordImage, 0, 0);
                }
            }
        });
        canvasOutput = outputCanvas;
        new Label(grpInput, SWT.NONE);

        Composite compositeButtons = new Composite(grpInput, SWT.NONE);
        GridLayout gl_compositeButtons = new GridLayout(4, false);
        gl_compositeButtons.horizontalSpacing = 7;
        gl_compositeButtons.verticalSpacing = 0;
        gl_compositeButtons.marginWidth = 0;
        gl_compositeButtons.marginHeight = 0;
        compositeButtons.setLayout(gl_compositeButtons);

        btnCopyToClipboard = new Button(compositeButtons, SWT.NONE);
        btnCopyToClipboard.setToolTipText(
                "Copies the generated password to the clipboard and then based on the combobox next to the button:\r\n\r\n\"erase clipboard in\"\r\nClears the clipboard after X seconds.\r\n\r\n\"close PasswordMakerJE in\"\r\nSame thing as above but also closes PasswordMakerJE after X seconds.");
        btnCopyToClipboard.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onCopyToClipboard();
            }
        });
        btnCopyToClipboard.setText("Copy to clipboard, then");

        comboCopyBehavior = new Combo(compositeButtons, SWT.READ_ONLY);
        comboCopyBehavior.setToolTipText("");
        comboCopyBehavior.setItems(new String[] { "erase clipboard in", "close app and erase clipboard in" });
        comboCopyBehavior.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        comboCopyBehavior.select(0);

        editCopySeconds = new Spinner(compositeButtons, SWT.BORDER);
        editCopySeconds.addFocusListener(new FocusAdapter() {
            @Override
            public void focusLost(FocusEvent arg0) {
                onSecondsFocusLost();
            }
        });
        editCopySeconds.setMinimum(1);
        editCopySeconds.setSelection(5);
        GridData gd_editCloseSeconds = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1);
        gd_editCloseSeconds.widthHint = 33;
        editCopySeconds.setLayoutData(gd_editCloseSeconds);

        secondsDecoration = new ControlDecoration(editCopySeconds, SWT.LEFT | SWT.TOP);
        secondsDecoration.setDescriptionText("The number of seconds must be a positive value");

        Label lblSeconds = new Label(compositeButtons, SWT.NONE);
        lblSeconds.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        lblSeconds.setText("seconds");

        accountTreeViewer = new TreeViewer(grpAccounts, SWT.BORDER);
        accountTree = accountTreeViewer.getTree();
        accountTree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));

        Menu menu = new Menu(accountTree);
        menu.addMenuListener(new MenuAdapter() {
            @Override
            public void menuShown(MenuEvent arg0) {
                onAccountMenuShown();
            }
        });
        accountTree.setMenu(menu);

        menuItemNewAccount = new MenuItem(menu, SWT.NONE);
        menuItemNewAccount.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onNewAccountSelected();
            }
        });
        menuItemNewAccount.setImage(
                SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/key_add.png"));
        menuItemNewAccount.setText("&New Account");

        menuItemEditAccount = new MenuItem(menu, SWT.NONE);
        menuItemEditAccount.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onEditAccount();
            }
        });
        menuItemEditAccount.setText("&Edit Account");

        new MenuItem(menu, SWT.SEPARATOR);

        menuItemNewGroup = new MenuItem(menu, SWT.NONE);
        menuItemNewGroup.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onNewGroupSelected();
            }
        });
        menuItemNewGroup.setImage(
                SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/folder_add.png"));
        menuItemNewGroup.setText("New Group");

        menuItemEditGroup = new MenuItem(menu, SWT.NONE);
        menuItemEditGroup.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onEditAccount();
            }
        });
        menuItemEditGroup.setText("Edit Group");

        MenuItem menuItem = new MenuItem(menu, SWT.SEPARATOR);

        menuItemSort = new MenuItem(menu, SWT.NONE);
        menuItemSort.setImage(
                SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/sort_ascend.png"));
        menuItemSort.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onSort();
            }
        });
        menuItemSort.setText("Sort");

        new MenuItem(menu, SWT.SEPARATOR);

        menuItemDeleteAccount = new MenuItem(menu, SWT.NONE);
        menuItemDeleteAccount.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onDeleteAccount();
            }
        });
        menuItemDeleteAccount.setImage(
                SWTResourceManager.getImage(GuiMain.class, "/org/daveware/passwordmakerapp/icons/key_delete.png"));
        menuItemDeleteAccount.setText("&Delete Account");

        menuItemDeleteGroup = new MenuItem(menu, SWT.NONE);
        menuItemDeleteGroup.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                onDeleteAccount();
            }
        });
        menuItemDeleteGroup.setImage(SWTResourceManager.getImage(GuiMain.class,
                "/org/daveware/passwordmakerapp/icons/folder_delete.png"));
        menuItemDeleteGroup.setText("Delete Group");

        menu_1 = new Menu(shlPasswordMaker, SWT.BAR);
        shlPasswordMaker.setMenuBar(menu_1);

        mntmFile = new MenuItem(menu_1, SWT.CASCADE);
        mntmFile.addArmListener(new ArmListener() {
            public void widgetArmed(ArmEvent arg0) {
                onFileMenuArmed();
            }
        });
        mntmFile.setText("&File");

        menu_3 = new Menu(mntmFile);
        mntmFile.setMenu(menu_3);

        menuItemNew = new MenuItem(menu_3, SWT.NONE);
        menuItemNew.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                newFile();
            }
        });

        int specialKey = Utilities.isMac() ? SWT.COMMAND : SWT.CTRL;
        String specialKeyStr = Utilities.isMac() ? "\tCommand+" : "\tCtrl+";

        menuItemNew.setText("New Database\tCtrl+N");
        menuItemNew.setAccelerator(specialKey + 'N');

        menuItemOpen = new MenuItem(menu_3, SWT.NONE);
        menuItemOpen.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                openFile();
            }
        });
        menuItemOpen.setText("Open Database\tCtrl+O");
        menuItemOpen.setAccelerator(specialKey + 'O');

        menuItemSave = new MenuItem(menu_3, SWT.NONE);
        menuItemSave.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                saveFile();
            }
        });
        menuItemSave.setText("Save Database\tCtrl+S");
        menuItemSave.setAccelerator(specialKey + 'S');

        menuItemSaveAs = new MenuItem(menu_3, SWT.NONE);
        menuItemSaveAs.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                saveFileAs();
            }
        });
        menuItemSaveAs.setText("Save Database As");

        new MenuItem(menu_3, SWT.SEPARATOR);

        menuItemExit = new MenuItem(menu_3, SWT.NONE);
        menuItemExit.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                exit();
            }
        });
        menuItemExit.setText("Exit");

        // The MAC's "about" menuitem is part of the application menu. For everyone else
        // it is in a separate "Help" menu.
        if (System.getProperty("os.name").toLowerCase().contains("mac") == false) {
            mntmhelp = new MenuItem(menu_1, SWT.CASCADE);
            mntmhelp.setText("&Help");

            menu_2 = new Menu(mntmhelp);
            mntmhelp.setMenu(menu_2);

            mntmabout = new MenuItem(menu_2, SWT.NONE);
            mntmabout.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent arg0) {
                    AboutDlg dlg = new AboutDlg(shlPasswordMaker, SWT.SHEET);
                    dlg.open();
                }
            });
            mntmabout.setText("&About PasswordMakerJE");
        }
        shlPasswordMaker.setTabList(new Control[] { grpInput, grpAccounts, sash });
    }

    /******************************
     * Below here is my stuff.
     */

    /**
     * Disables all controls when the copy-to-clipboard timer is running.
     */
    private void disableControlsForCopy() {
        Control[] controls = { accountTree, editUsername, editMP, editUrlSearch, editCopySeconds, comboCopyBehavior,
                accountFilterText, filterIcon };
        for (Control c : controls) {
            c.setEnabled(false);
        }

        btnCopyToClipboard.setText("Cancel countdown");

        filterIcon.setVisible(false);
    }

    /**
     * Enables all controls once the copy-to-clipboard timer has expired.
     */
    private void enableControlsForCopy() {
        Control[] controls = { accountTree, editUsername, editMP, editUrlSearch, editCopySeconds, comboCopyBehavior,
                accountFilterText, filterIcon };
        for (Control c : controls) {
            c.setEnabled(true);
        }

        btnCopyToClipboard.setText("Copy to clipboard, then");

        filterIcon.setVisible(true);
    }

    /**
     * Copies the currently generated password to the clipboard.
     */
    private void copyGeneratedToClipboard() {
        SecureCharArray generated = null;

        try {
            generated = generateOutput();
            Utilities.copyToClipboard(generated);
        } catch (Exception e) {
        } finally {
            if (generated != null)
                generated.erase();
        }
    }

    /**
     * Locates the current account based on information in the config.
     * 
     * @return 0 if found, else non-zero.
     */
    public int findAccountByUrl() {
        // Url search gets disabled when manually setting the value of the
        // editUrlSearch widget,
        // otherwise it invokes this function
        if (!urlSearchEnabled)
            return 0;

        int ret = 1;

        Account acc = null;
        String matchUrl = editUrlSearch.getText();

        try {
            acc = db.findAccountByUrl(matchUrl);
            if (acc != null)
                accountTreeViewer.setSelection(new StructuredSelection(acc));
            else
                accountTreeViewer.setSelection(null);
            // selectAccount(acc);
            ret = 0;
        } catch (Exception e) {
            MBox.showError(shlPasswordMaker, "Unable to locate account based on URL.\n" + e.getMessage());
            ret = 1;
        }

        return ret;
    }

    /**
     * Exits (disposes) this shell, prompting if db is dirty.
     */
    private void exit() {
        if (db.isDirty()) {
            switch (MBox.showYesNoCancel(shlPasswordMaker, EXIT_PROMPT)) {
            case SWT.YES:
                if (saveFile() == false)
                    return;
                break;

            case SWT.NO:
                break;

            case SWT.CANCEL:
                return;
            }
        }

        shlPasswordMaker.dispose();
    }

    /**
     * Generates the password and returns it.
     * 
     * @return A SecureCharArray object with the generated password.
     */
    private SecureCharArray generateOutput() {
        SecureCharArray mpw = null;
        SecureCharArray output = null;

        try {
            if (selectedAccount != null) {
                // If the username text has been edited by the user then it will
                // need to be updated
                // in the account. But since the account shouldn't be saved that
                // way, use a temporary
                // account.
                Account tempAccount = new Account();
                tempAccount.copySettings(selectedAccount);
                tempAccount.setUsername(editUsername.getText());
                tempAccount.setId(selectedAccount.getId());
                tempAccount.setUrl(editUrl.getText());

                // If the custom length/modifier buttons are checked, override 
                // the account's modifier/length settings.
                if (btnModifier.getSelection()) {
                    tempAccount.setModifier(editModifier.getText());
                }
                if (btnLength.getSelection()) {
                    int length = tempAccount.getLength();
                    try {
                        length = Integer.parseInt(editLength.getText());
                    } catch (Exception e) {
                    }
                    tempAccount.setLength(length);
                }

                mpw = new SecureCharArray(editMP.getText());

                output = pwm.makePassword(mpw, tempAccount);
            } else {
                output = new SecureCharArray();
            }
        } catch (Exception e) {
        } finally {
            if (mpw != null)
                mpw.erase();
        }

        return output;
    }

    /**
     * Creates and returns a thread which will countdown from whatever the
     * current countdown value is.
     * 
     * @param b
     *            The button to set the number of seconds left to.
     * @return The created thread.
     */
    private Thread getCountdownThread(Spinner spinner, int numSeconds) {
        final Spinner spinnerControl = spinner;
        final int countdownValue = numSeconds;

        return new Thread() {
            public void run() {
                boolean wasInterrupted = false;
                try {
                    // This is kinda dirty. A 'final' integer needs to be used
                    // inside the anonymous class
                    // so currentI is created each iteration. Gross.
                    for (int i = 0; i < countdownValue && wasInterrupted == false; ++i) {
                        final int currentI = i;
                        display.asyncExec(new Runnable() {
                            public void run() {
                                spinnerControl.setSelection(countdownValue - currentI);
                            }
                        });
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException iExc) {
                    wasInterrupted = true;
                }

                // gross again... the async execution below needs a final var
                final boolean anotherWasInterrupted = wasInterrupted;
                display.asyncExec(new Runnable() {
                    public void run() {
                        Utilities.clearClipboard();

                        if (anotherWasInterrupted == false && closeAfterTimer == true)
                            shlPasswordMaker.dispose();
                        else {
                            enableControlsForCopy();
                            int seconds = 5;
                            try {
                                seconds = Integer.parseInt(db.getGlobalSetting(GlobalSettingKey.CLIPBOARD_TIMEOUT));
                            } catch (NumberFormatException e) {
                            }
                            editCopySeconds.setSelection(seconds);
                        }
                    }
                });
            }
        };
    }

    /**
     * Sets the various fields of the GUI based on the settings in the database.
     */
    private void setGuiFromGlobalSettings() {
        int seconds = 5;
        try {
            seconds = Integer.parseInt(db.getGlobalSetting(GlobalSettingKey.CLIPBOARD_TIMEOUT));
        } catch (NumberFormatException e) {
        }
        editCopySeconds.setSelection(seconds);
        btnShowPassword.setSelection(db.getGlobalSetting(GlobalSettingKey.SHOW_GEN_PW).compareTo("true") == 0);
        onShowPasswordClicked(); // due to manual "setSelection" not triggering
                                 // an event
    }

    private boolean loadPredefinedLocations() {
        // Scan the current directory looking for anything ending in ".rdf"
        try {
            String curDir = System.getProperty("user.dir");
            if (curDir != null) {
                File curDirFile = new File(curDir);
                File[] filelist = curDirFile.listFiles(new FileFilter() {

                    @Override
                    public boolean accept(File pathname) {
                        return pathname.getAbsolutePath().endsWith(".rdf");
                    }
                });

                for (File f : filelist) {
                    if (openFile(f.getAbsolutePath(), true))
                        return true;
                }
            }
        } catch (Exception e) {

        }

        // Now try the known home-directory files
        try {
            String[] locations = { "passwordmaker.rdf", ".passwordmaker.rdf", "pwmje.rdf", ".pwmje.rdf",
                    ".passwordmakerrc", };
            String home = System.getProperty("user.home");
            if (home != null) {
                for (String pathPiece : locations) {
                    String fullPath = home + File.separator + pathPiece;
                    if (openFile(fullPath, true) == true) {
                        return true;
                    }
                }
            }
        } catch (Exception e) {

        }
        return false;
    }

    /**
     * Loads the various fields with data from the config files.
     */
    private void loadFromCmdLineSettings() {
        if (cmdLineSettings.matchUrl != null)
            editUrlSearch.setText(cmdLineSettings.matchUrl);
        if (cmdLineSettings.inputFilename != null) {
            openFile(cmdLineSettings.inputFilename, false);
        } else {
            if (loadPredefinedLocations() == false)
                newFile();
        }

        // Only attempt an initial find if something was passed on the
        // commandline
        if (cmdLineSettings.matchUrl != null && cmdLineSettings.matchUrl.length() > 0)
            findAccountByUrl();
    }

    /**
     * Sets the title of the window based on the current data state.
     */
    private void setTitle() {
        String title = (currentFilename.trim().length() == 0 ? "Untitled" : currentFilename) + " - " + TITLE_STRING
                + " - " + buildInfo.getVersion();
        String dirty = " ";
        if (db != null && db.isDirty())
            dirty = "* ";
        shlPasswordMaker.setText(dirty + title);
    }

    /**
     * Creates a new blank(sorta) database. This will attempt to save first if
     * the current database is dirty and abort the new file operation if the
     * save fails.
     * 
     * @return
     */
    private boolean newFile() {
        if (db != null && db.isDirty()) {
            switch (MBox.showYesNoCancel(shlPasswordMaker, EXIT_PROMPT)) {
            case SWT.YES:
                // Attempt to save the file, if that fails then abort the new
                // file operation
                if (saveFile() == false)
                    return false;
                break;

            case SWT.NO:
                break;

            case SWT.CANCEL:
                return false;
            }
        }

        db = new Database();
        db.addDatabaseListener(this);
        accountTreeViewer.setInput(db);

        try {
            db.addDefaultAccount();

            // TODO: This 2nd "setInput" is to work around a problem with the
            // DatabaseListener not getting
            // messages - I'm not exactly sure why.
            accountTreeViewer.setInput(db);
            selectFirstAccount();
            setGuiFromGlobalSettings();
            db.setDirty(false);
        } catch (Exception e) {
            // This REALLY should be impossible ... but, handle it anyway
            MBox.showError(shlPasswordMaker, "Unable to create default account.\n" + e.getMessage());
        }

        selectFirstAccount();

        return true;
    }

    /**
     * Handles when focus is gained on the accountFilter text box.
     */
    private void onAccountFilterTextFocusGained() {
        String text = accountFilterText.getText();

        if (text.compareTo(ACCOUNT_FILTER_DESC) == 0)
            accountFilterText.setText("");

        /* Clear the italics */
        accountFilterText.setFont(regularSearchFont);

        filterIcon.setVisible(false);

        accountFilterText.selectAll();
    }

    /**
     * Handles when focus is lost from the accountFilter text box.
     */
    private void onAccountFilterTextFocusLost() {
        String text = accountFilterText.getText();
        if (text.length() == 0) {
            accountFilterText.setText(ACCOUNT_FILTER_DESC);
            accountFilterText.setFont(italicsSearchFont);
            filterIcon.setImage(searchImage);
        } else {
            accountFilterText.setFont(regularSearchFont);
            filterIcon.setImage(cancelImage);
        }
        filterIcon.setVisible(true);
    }

    /**
     * Invoked when the account menu is about to be shown.
     */
    private void onAccountMenuShown() {
        if (selectedAccount != null) {
            if (selectedAccount.isFolder()) {
                menuItemEditGroup.setEnabled(true);
                menuItemDeleteGroup.setEnabled(true);
                menuItemEditAccount.setEnabled(false);
                menuItemDeleteAccount.setEnabled(false);
                menuItemSort.setEnabled(true);
            } else {
                menuItemEditGroup.setEnabled(false);
                menuItemDeleteGroup.setEnabled(false);
                menuItemEditAccount.setEnabled(true);
                menuItemDeleteAccount
                        .setEnabled(selectedAccount.isDefault() == false && selectedAccount.isRoot() == false);
                menuItemSort.setEnabled(true);
            }
        } else {
            menuItemEditGroup.setEnabled(false);
            menuItemDeleteGroup.setEnabled(false);
            menuItemEditAccount.setEnabled(false);
            menuItemDeleteAccount.setEnabled(false);
            menuItemSort.setEnabled(false);
        }
    }

    /**
     * Method invoked when the copy-and-exit button is clicked.
     * 
     * @param btn
     *            The button clicked.
     */
    private void onCopyToClipboard() {
        // If a thread is already running, the button serves as a cancellation
        if (countdownThread != null && countdownThread.isAlive()) {
            countdownThread.interrupt();
        } else {
            // First make sure the seconds-value is valid. If not, set it to 5 and
            // carry on.
            int seconds;

            closeAfterTimer = comboCopyBehavior.getSelectionIndex() == 0 ? false : true;

            // Perform a dirty check if the app will close after the operation
            if (closeAfterTimer && db.isDirty()) {
                switch (MBox.showYesNoCancel(shlPasswordMaker, EXIT_PROMPT)) {
                case SWT.YES:
                    // Only continue if the save succeeded
                    if (saveFile() != true)
                        return;
                    break;

                case SWT.NO:
                    break;

                case SWT.CANCEL:
                    // user aborted
                    return;
                }
            }

            try {
                seconds = Integer.parseInt(editCopySeconds.getText());
                if (seconds < 1) {
                    secondsDecoration.show();
                    editCopySeconds.setFocus();
                    return;
                }

                secondsDecoration.hide();
            } catch (Exception ee) {
                secondsDecoration.show();
                editCopySeconds.setFocus();
                return;
            }

            disableControlsForCopy();

            copyGeneratedToClipboard();
            countdownThread = getCountdownThread(editCopySeconds, seconds);
            if (countdownThread != null)
                countdownThread.start();
            else {
                MBox.showError(shlPasswordMaker,
                        "Unable to create a timer. Your password is on the clipboard and WILL NOT be erased. This is a bug, please file a bug report.");
            }
        }
    }

    /**
     * Handles the deletion of the selected account.
     */
    private void onDeleteAccount() {
        if (selectedAccount == null) {
            MBox.showError(shlPasswordMaker,
                    "No account is selected for deletion, this should not be possible. Please file a bug report.");
            return;
        }

        String type = selectedAccount.isFolder() ? "folder" : "account";
        String msg = "Are you sure you wish to delete " + type + " '" + selectedAccount.getName() + "'";
        int numChildren = selectedAccount.getNestedChildCount();
        if (numChildren > 0)
            msg += " and it's " + numChildren + " children?";
        else
            msg += "?";

        if (MBox.showYesNo(shlPasswordMaker, msg) == SWT.YES) {
            Account nearestRelative = db.findNearestRelative(selectedAccount);
            db.removeAccount(selectedAccount);
            if (nearestRelative == null)
                accountTreeViewer.setSelection(null);
            else
                accountTreeViewer.setSelection(new StructuredSelection(nearestRelative));
        }
    }

    private void onCloseWindow(Event e) {
        if (db.isDirty() == true) {
            switch (MBox.showYesNoCancel(shlPasswordMaker, EXIT_PROMPT)) {
            case SWT.YES:
                if (saveFile() == true) {
                    e.doit = true;
                }
                break;

            case SWT.NO:
                e.doit = true;
                break;

            case SWT.CANCEL:
                e.doit = false;
                break;

            default:
                e.doit = true;
                break;
            }
        }
    }

    /**
     * Invoked when the shell is being disposed (closed).
     * 
     * @param arg0
     *            The dispose event.
     */
    private void onDisposing(DisposeEvent arg0) {
        Utilities.clearClipboard();

        if (passwordImage != null)
            passwordImage.dispose();
        if (searchImage != null)
            searchImage.dispose();
        if (cancelImage != null)
            cancelImage.dispose();
        if (eyeImage != null)
            eyeImage.dispose();
        if (eyeClosedImage != null)
            eyeClosedImage.dispose();
        if (iconImage != null)
            iconImage.dispose();
        if (passwordFont != null)
            passwordFont.dispose();
        if (regularSearchFont != null)
            regularSearchFont.dispose();
        if (italicsSearchFont != null)
            italicsSearchFont.dispose();
        if (accountTreeModel != null)
            accountTreeModel.dispose();
        if (accountTreeLabelProvider != null)
            accountTreeLabelProvider.dispose();

        passwordImage = null;
        searchImage = null;
        cancelImage = null;
        eyeImage = null;
        eyeClosedImage = null;
        regularSearchFont = null;
        accountTreeModel = null;
        accountTreeLabelProvider = null;
    }

    /**
     * Handles editing an account.
     */
    private void onEditAccount() {
        AccountDlg dlg = null;
        SecureCharArray mpw = null;
        if (selectedAccount != null) {
            try {
                mpw = new SecureCharArray(editMP.getText());
                dlg = new AccountDlg(selectedAccount, mpw, passwordFont, pwm, eyeImage, eyeClosedImage,
                        showPassword);

                // A copy of the edited account is returned if "ok" is clicked.
                Account newAccount = dlg.open();
                if (newAccount != null) {
                    selectedAccount.copySettings(newAccount);
                    db.changeAccount(selectedAccount);
                    // accountTreeViewer.refresh(account, true);

                    // The tree already has the account selected. Applying the
                    // same selection actually
                    // has the side-effect of unselecting the account. So
                    // instead just invoke the selectAccount()
                    // method which is normally invoked by the tree causing the
                    // selection.
                    selectAccount(selectedAccount);
                }
            } finally {
                if (mpw != null) {
                    mpw.erase();
                    mpw = null;
                }
            }
        }
    }

    /**
     * Invoked when the File menu is armed.
     */
    private void onFileMenuArmed() {
        menuItemSave.setEnabled(db.isDirty());
    }

    /**
     * Invoked when the filter composite is resized so that the text box and
     * filter icon can be re-aligned.
     * 
     * @param arg0
     *            The composite-resize event.
     */
    private void onFilterCompositeResized(ControlEvent arg0) {
        Composite frame = (Composite) arg0.widget;
        accountFilterText.setSize(frame.getSize());
        filterIcon.setLocation(frame.getSize().x - filterIcon.getSize().x - 1, 1);
    }

    /**
     * Handles when the X is clicked in the accountFilterText widget.
     * 
     * This will reset the text/image/font back to defaults.
     */
    private void onFilterIconClicked() {
        accountFilterText.setText(ACCOUNT_FILTER_DESC);
        accountFilterText.setFont(italicsSearchFont);
        filterIcon.setImage(searchImage);
        filterIcon.setVisible(true);
        isFiltering = false;
    }

    /**
     * Invoked when the filter text is modified.
     * 
     * @param e
     *            The modification event.
     */
    private void onFilterModified(ModifyEvent e) {
        String text = accountFilterText.getText();
        if (text.compareTo(ACCOUNT_FILTER_DESC) == 0 || text.length() == 0) {
            isFiltering = false;
        } else {
            isFiltering = true;
        }

        if (accountTreeViewer != null) {
            accountTreeViewer.refresh();
            if (text.length() > 0) {
                accountTreeViewer.expandAll();
                selectFirstLeafAccount();
            }
        }
    }

    /**
     * Selects the first available account that is not a folder, eg a leaf node.
     * It takes into consideration filtering rules.
     */
    private void selectFirstLeafAccount() {
        // This is a gross hack that doesn't operate on what accountTreeViewer
        // tells us, and that's
        // because I can't for the life of me figure out how to get it to tell
        // me what leaf nodes
        // are visible. I *hate* SWT's documentation.
        //
        // This is inefficient as it is walking the entire account database tree
        // looking
        // for nodes that match the text, essentially re-filtering. If anyone
        // sees this and knows
        // how to get at the filtered data, please let me know.

        // For some reason only on OSX, selectFirstLeafAccount() gets called
        // before the database
        // is ever created. So...
        if (db == null)
            return;

        String filterText = accountFilterText.getText().toLowerCase();
        ArrayList<Account> parentStack = new ArrayList<Account>();
        parentStack.add(db.getRootAccount());

        while (parentStack.size() > 0) {
            Account parentAccount = parentStack.remove(0);

            for (Account child : parentAccount.getChildren()) {
                if (child.isFolder())
                    parentStack.add(child);
                else {
                    if (filterText.length() == 0 || child.getName().toLowerCase().contains(filterText)) {
                        accountTreeViewer.setSelection(new StructuredSelection(child));
                        return;
                    }
                }
            }
        }
    }

    private void onNewAccountSelected() {
        Account parentAccount = null;
        AccountDlg dlg = null;

        // If no account is selected, then default to the root
        if (selectedAccount == null)
            parentAccount = db.getRootAccount();
        else {
            // Otherwise decide if it will be a sibling of the selected account
            // or a child
            // of the selected group.
            parentAccount = selectedAccount;

            // If the parent is not a folder, it needs to be created as a
            // sibling. So locate
            // who the real parent is.
            if (parentAccount.isFolder() == false) {
                parentAccount = db.findParent(parentAccount);
                if (parentAccount == null) {
                    MBox.showError(shlPasswordMaker,
                            "Unable to locate parent account of '" + selectedAccount.getName() + "' id="
                                    + selectedAccount.getId() + ", cannot add new account.");
                    return;
                }
            }
        }

        // Create a new blank account with default settings and the dialog
        Account newAccount = new Account();
        SecureCharArray mpw = new SecureCharArray(editMP.getText());
        try {
            dlg = new AccountDlg(newAccount, mpw, passwordFont, pwm, eyeImage, eyeClosedImage, showPassword);

            // A copy of the account is returned if "ok" is clicked.
            newAccount = dlg.open();
            if (newAccount != null) {
                newAccount.setId(Account.createId(newAccount));
                db.addAccount(parentAccount, newAccount);
            }
        } catch (Exception e) {
            MBox.showError(shlPasswordMaker,
                    "While creating the new account, an error occurred. You should save your work to a new file and restart.\n"
                            + e.getMessage());
        } finally {
            mpw.erase();
            mpw = null;
        }
    }

    private void onNewGroupSelected() {
        Account parentAccount = null;
        AccountDlg dlg = null;

        // If no account is selected, then default to the root
        if (selectedAccount == null)
            parentAccount = db.getRootAccount();
        else {
            // Otherwise decide if it will be a sibling of the selected account
            // or a child
            // of the selected group.
            parentAccount = selectedAccount;

            // If the parent is not a folder, it needs to be created as a
            // sibling. So locate
            // who the real parent is.
            if (parentAccount.isFolder() == false) {
                parentAccount = db.findParent(parentAccount);
                if (parentAccount == null) {
                    MBox.showError(shlPasswordMaker,
                            "Unable to locate parent account of '" + selectedAccount.getName() + "' id="
                                    + selectedAccount.getId() + ", cannot add new group.");
                    return;
                }
            }
        }

        // Create a new blank account with default settings and the dialog
        Account newAccount = new Account();
        newAccount.setIsFolder(true);
        SecureCharArray mpw = new SecureCharArray(editMP.getText());
        try {
            dlg = new AccountDlg(newAccount, mpw, passwordFont, pwm, eyeImage, eyeClosedImage, showPassword);
        } finally {
            mpw.erase();
            mpw = null;
        }

        // A copy of the account is returned if "ok" is clicked.
        newAccount = dlg.open();
        if (newAccount != null) {
            try {
                newAccount.setId(Account.createId(newAccount));
                db.addAccount(parentAccount, newAccount);
            } catch (Exception e) {
                MBox.showError(shlPasswordMaker,
                        "While creating the new group, an error occurred. You should save your work to a new file and restart.\n"
                                + e.getMessage());
            }
        }
    }

    private void onSecondsFocusLost() {
        int numSeconds;

        try {
            numSeconds = editCopySeconds.getSelection();
            if (numSeconds < 1) {
                // TODO: should this also check for some kind of max?
                secondsDecoration.show();
                return;
            }

            db.setGlobalSetting(GlobalSettingKey.CLIPBOARD_TIMEOUT, editCopySeconds.getText());
            secondsDecoration.hide();
        } catch (Exception e) {
            secondsDecoration.show();
        }
    }

    private void onSort() {
        // shouldn't be possible
        if (selectedAccount == null) {
            MBox.showError(shlPasswordMaker,
                    "An account must be selected before sorting can occur (how did you do this?).");
            return;
        }

        // Locate the parent if it is just an account
        Account parentAccount = selectedAccount;
        if (selectedAccount.isFolder() == false) {
            parentAccount = db.findParent(selectedAccount);
            if (parentAccount == null) {
                MBox.showError(shlPasswordMaker, "Unable to locate parent account for " + selectedAccount.getName()
                        + " (how did you do this?).");
                return;
            }
        }

        int style = SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL;
        if (Utilities.isMac())
            style = SWT.SHEET;

        SortDlg dlg = new SortDlg(shlPasswordMaker, style, sortOptions);
        SortOptions newOptions = null;
        newOptions = dlg.open();
        if (newOptions != null) {
            sortOptions = newOptions;
            Collections.sort(parentAccount.getChildren(), new AccountComparator(sortOptions));

            if (parentAccount.isRoot())
                accountTreeViewer.refresh(null, true);
            else
                accountTreeViewer.refresh(parentAccount, true);

            db.setDirty(true);
        }
    }

    /**
     * Invoked when the eyeball is clicked.
     */
    private void onShowPasswordClicked() {
        showPassword = btnShowPassword.getSelection();
        if (showPassword)
            btnShowPassword.setImage(eyeImage);
        else
            btnShowPassword.setImage(eyeClosedImage);

        db.setGlobalSetting(GlobalSettingKey.SHOW_GEN_PW, Boolean.toString(showPassword));

        regeneratePasswordAndDraw();
    }

    /**
     * Invoked when the URL field is modified. Causes the accounts to be
     * searched for anything matching the current URL text.
     * 
     * @param arg0
     *            Ignored, can be null.
     */
    private void onUrlSearchModified(ModifyEvent arg0) {
        findAccountByUrl();
    }

    /**
     * Opens up an "open" dialog and then opens the selected file.
     * 
     * @return true on success.
     */
    private boolean openFile() {
        if (db != null && db.isDirty()) {
            switch (MBox.showYesNoCancel(shlPasswordMaker, EXIT_PROMPT)) {
            case SWT.YES:
                if (saveFile() == false)
                    return false;
                break;

            case SWT.NO:
                break;

            case SWT.CANCEL:
                return false;
            }
        }

        FileDialog fd = new FileDialog(shlPasswordMaker, SWT.OPEN);
        fd.setText("Open RDF File");
        fd.setFilterExtensions(new String[] { "*.rdf", "*.*" });
        String selected = fd.open();
        if (selected != null && selected.length() > 0)
            return openFile(selected, false);
        return false;
    }

    /**
     * Reads a file in and sets up the necessary widgets with the data. If this
     * fails then it will create a new empty database and return false.
     * 
     * @param filename
     *            The filename (assumes RDF).
     * @param inhibitErrors
     *            If true, failures to load will not be displayed. This is used
     *            for auto-loading of passwordmaker databases.
     * @return true on success.
     */
    private boolean openFile(String filename, boolean inhibitErrors) {
        RDFDatabaseReader rdfReader = null;
        File inputFile = null;
        FileInputStream fin = null;
        boolean ret = false;

        try {
            rdfReader = new RDFDatabaseReader();
            inputFile = new File(filename);
            fin = new FileInputStream(inputFile);
            db = rdfReader.read(fin);
            db.addDatabaseListener(this);
            currentFilename = filename;

            // Widget setup
            accountTreeViewer.setInput(db);

            selectFirstAccount();
            setGuiFromGlobalSettings();

            db.setDirty(false);
            ret = true;
        } catch (Exception ex) {
            currentFilename = "";
            db = new Database();
            db.addDatabaseListener(this);
            accountTreeViewer.setInput(db);

            if (!inhibitErrors)
                MBox.showError(shlPasswordMaker, "Unable to open " + filename + "\n" + ex.getMessage());
        } finally {
            try {
                if (fin != null)
                    fin.close();
            } catch (Exception exinner) {
            }
        }

        return ret;
    }

    /**
     * Causes the password to be regenerated.
     * 
     * This calculates the new generated password and draws it to the image
     * which is used to display its value. A standard text control is not used
     * because they take a "String" which cannot be reliably erased when
     * finished. The password is drawn character by character so the full string
     * never sits on the stack anywhere.
     */
    private void regeneratePasswordAndDraw() {
        SecureCharArray output = null;
        GC gc = null;

        try {
            updateUrlFromInputUrl();

            gc = new GC(passwordImage);
            gc.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK));
            gc.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE));
            gc.fillRectangle(canvasOutput.getClientArea());

            if (selectedAccount != null && selectedAccount.isFolder() == false && editMP.getText().length() > 0) {
                output = generateOutput();
                if (output != null && showPassword == true) {
                    gc.setFont(passwordFont);
                    int x = 0;
                    int xPos = 5;
                    int yPos = 1;

                    if (Utilities.isMac())
                        yPos = 7;

                    // TODO: really should calculate the text extents and center
                    // off of it
                    for (x = 0; x < output.getData().length; x++) {
                        char strBytes[] = { output.getData()[x] };
                        String str = new String(strBytes);
                        gc.drawText(str, xPos, yPos);
                        xPos += gc.stringExtent(str).x + 2;
                    }
                }
            }

            canvasOutput.redraw();
        } catch (Exception e) {
        } finally {
            if (output != null)
                output.erase();
            if (gc != null)
                gc.dispose();
        }
    }

    /**
     * Attempts to save a file to the current filename. If there is no current
     * file name, then save-as is invoked. This also clears the dirty status on
     * success.
     * 
     * @return true on success.
     */
    private boolean saveFile() {
        boolean ret = false;

        // If we don't have a filename yet, call saveAs() which will call this
        // function
        // in return with a filename set.
        if (currentFilename.trim().length() == 0) {
            return saveFileAs();
        }

        try {
            RDFDatabaseWriter out = new RDFDatabaseWriter();
            File newFile = new File(currentFilename);
            if (newFile.exists() == false)
                newFile.createNewFile();

            FileOutputStream fout = new FileOutputStream(newFile);
            out.write(fout, db);
            db.setDirty(false);
            ret = true;
        } catch (Exception e) {
            MBox.showError(shlPasswordMaker, "Unable to save to " + currentFilename + ".\n" + e.getMessage());
        }

        return ret;
    }

    /**
     * Opens up a dialog-box allowing the user to select a file to save to. This
     * will invoke saveFile behind the scenes and update currentFilename on
     * success.
     * 
     * @return true on success.
     */
    private boolean saveFileAs() {
        FileDialog fd = new FileDialog(shlPasswordMaker, SWT.SAVE);
        fd.setText("Save RDF As");
        fd.setFilterExtensions(new String[] { "*.rdf", "*.*" });
        String selected = fd.open();
        if (selected != null && selected.length() > 0) {
            String oldFilename = currentFilename;
            currentFilename = selected;

            File fileTest = new File(selected);
            if (fileTest.exists()) {
                if (MBox.showYesNo(shlPasswordMaker,
                        "File " + selected + " already exists, would you like to replace it?") != SWT.YES)
                    return false;
            }

            if (saveFile() == true) {
                return true;
            }

            // it failed if we get here, restore the filename
            currentFilename = oldFilename;
        }

        return false;
    }

    /**
     * Called when an account is selected. This is invoked by the tree when the
     * selection is made.
     * 
     * @param acc
     *            The new account selection.
     */
    private void selectAccount(Account acc) {
        // Store the newly selected account
        selectedAccount = acc;
        if (acc != null) {
            btnCopyToClipboard.setEnabled(selectedAccount.isFolder() == false);
            editAccount.setText(selectedAccount.getName());
            editDesc.setText(selectedAccount.getDesc());
            editUsername.setText(selectedAccount.getUsername());

            // When the default account is selected, the user can enter an URL
            // to have
            // the account settings applied against.
            if (acc.isDefault()) {
                editInputUrl.setEnabled(true);
                lblInputUrl.setEnabled(true);
                editInputUrl.setText("");

                urlSearchEnabled = false;
                editUrlSearch.setText("");
                urlSearchEnabled = true;
            } else {
                editInputUrl.setEnabled(false);
                lblInputUrl.setEnabled(false);
                editUrl.setText(acc.getUrl());
            }

            editModifier.setText(acc.getModifier());
            editLength.setText(Integer.toString(acc.getLength()));

        } else {
            btnCopyToClipboard.setEnabled(false);
            editAccount.setText("NO ACCOUNT SELECTED");
            editDesc.setText("");
            editUsername.setText("");
        }

        regeneratePasswordAndDraw();
    }

    private void selectFirstAccount() {
        if (db.getRootAccount().getChildren().size() > 0)
            accountTreeViewer.setSelection(new StructuredSelection(db.getRootAccount().getChildren().get(0)));
        else
            accountTreeViewer.setSelection(null);
    }

    /**
     * Updates editUrl with modified text from editInputUrl based in the rules
     * of the current account if it is the default account.
     */
    private void updateUrlFromInputUrl() {
        if (selectedAccount != null) {
            if (selectedAccount.isDefault()) {
                String origUrl = editInputUrl.getText();
                String newUrl = pwm.getModifiedInputText(origUrl, selectedAccount);
                editUrl.setText(newUrl);
            }
        }
    }

    // ////////////////////////////////////////////////////////////
    //
    // DATABASELISTENER INTERFACE
    //
    // ////////////////////////////////////////////////////////////

    @Override
    public void accountAdded(Account parent, Account account) {
        // I'm not sure why, but if you add a node off the root level,
        // refreshing the root node
        // will not make it show up. Refreshing the whole tree does.
        if (parent.isRoot())
            accountTreeViewer.refresh();
        else
            accountTreeViewer.refresh(parent);

        accountTreeViewer.setSelection(new StructuredSelection(account));
    }

    @Override
    public void accountRemoved(Account parent, Account account) {
        // I'm not sure why, but if you remove a node off the root level,
        // refreshing the root node
        // will not make it show up. Refreshing the whole tree does.
        if (parent.isRoot())
            accountTreeViewer.refresh();
        else
            accountTreeViewer.refresh(parent);
    }

    @Override
    public void accountChanged(Account account) {
        // accountTreeViewer.refresh(account);
        accountTreeViewer.update(account, null);
    }

    @Override
    public void dirtyStatusChanged(boolean status) {
        setTitle();
    }
}