ca.phon.app.project.ProjectWindow.java Source code

Java tutorial

Introduction

Here is the source code for ca.phon.app.project.ProjectWindow.java

Source

/*
 * Phon - An open source tool for research in phonology.
 * Copyright (C) 2005 - 2015, Gregory Hedlund <ghedlund@mun.ca> and Yvan Rose <yrose@mun.ca>
 * Dept of Linguistics, Memorial University <https://phon.ca>
 * 
 * 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 ca.phon.app.project;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragSource;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;

import org.apache.commons.lang3.StringUtils;
import org.jdesktop.swingx.HorizontalLayout;
import org.jdesktop.swingx.JXBusyLabel;

import ca.phon.app.project.actions.AnonymizeAction;
import ca.phon.app.project.actions.CheckTranscriptionsAction;
import ca.phon.app.project.actions.DeleteCorpusAction;
import ca.phon.app.project.actions.DeleteSessionAction;
import ca.phon.app.project.actions.DeriveSessionAction;
import ca.phon.app.project.actions.DuplicateCorpusAction;
import ca.phon.app.project.actions.DuplicateSessionAction;
import ca.phon.app.project.actions.NewCorpusAction;
import ca.phon.app.project.actions.NewSessionAction;
import ca.phon.app.project.actions.OpenCorpusTemplateAction;
import ca.phon.app.project.actions.OpenSessionAction;
import ca.phon.app.project.actions.RefreshAction;
import ca.phon.app.project.actions.RenameCorpusAction;
import ca.phon.app.project.actions.RenameSessionAction;
import ca.phon.app.project.git.ProjectGitController;
import ca.phon.app.project.git.actions.CommitAction;
import ca.phon.app.project.git.actions.InitAction;
import ca.phon.app.project.git.actions.PullAction;
import ca.phon.app.project.git.actions.PushAction;
import ca.phon.app.workspace.WorkspaceDialog;
import ca.phon.app.workspace.WorkspaceTextStyler;
import ca.phon.plugin.PluginEntryPointRunner;
import ca.phon.plugin.PluginException;
import ca.phon.project.Project;
import ca.phon.project.ProjectListener;
import ca.phon.project.ProjectPath;
import ca.phon.project.ProjectRefresh;
import ca.phon.session.SessionPath;
import ca.phon.ui.CommonModuleFrame;
import ca.phon.ui.MultiActionButton;
import ca.phon.ui.action.PhonActionEvent;
import ca.phon.ui.action.PhonUIAction;
import ca.phon.ui.decorations.DialogHeader;
import ca.phon.ui.fonts.FontPreferences;
import ca.phon.ui.menu.MenuManager;
import ca.phon.ui.toast.ToastFactory;
import ca.phon.util.icons.IconManager;
import ca.phon.util.icons.IconSize;

import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

/**
 * The project window.
 *
 */
public class ProjectWindow extends CommonModuleFrame implements WindowListener {

    private static final Logger LOGGER = Logger.getLogger(ProjectWindow.class.getName());

    private static final long serialVersionUID = -4771564010497815447L;

    /** The corpus list */
    private JPanel corpusPanel;
    private MultiActionButton newCorpusButton;
    private MultiActionButton createCorpusButton;
    private JList<String> corpusList;
    private CorpusListModel corpusModel;
    private CorpusDetailsPane corpusDetails;

    /** The session list */
    private JPanel sessionPanel;
    private MultiActionButton newSessionButton;
    private MultiActionButton createSessionButton;
    private JList<String> sessionList;
    private SessionListModel sessionModel;
    private SessionDetailsPane sessionDetails;

    /** The checkbox */
    private JCheckBox blindModeBox;

    /** Label for messages */
    private StatusPanel msgPanel;

    /** Project path (used to load the project) */
    private String projectLoadPath = new String();

    private final ProjectListener myProjectListener;

    /** Constructor */
    public ProjectWindow(Project project, String projectPath) {
        super("");

        setWindowName("Project Manager");

        putExtension(Project.class, project);

        myProjectListener = new ProjectWindowProjectListener(project);

        project.addProjectListener(myProjectListener);

        this.projectLoadPath = projectPath;

        this.addWindowListener(this);
        this.setTitle("Phon : " + project.getName() + " : Project Manager");

        this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);

        init();

        final JMenuBar menuBar = MenuManager.createWindowMenuBar(this);
        setJMenuBar(menuBar);
    }

    public MultiActionButton getCorpusButton() {
        return this.createCorpusButton;
    }

    public JList<String> getCorpusList() {
        return this.corpusList;
    }

    public MultiActionButton getSessionButton() {
        return this.createSessionButton;
    }

    public JList<String> getSessionList() {
        return this.sessionList;
    }

    public String getSelectedCorpus() {
        return corpusList.getSelectedValue();
    }

    public List<String> getSelectedCorpora() {
        return corpusList.getSelectedValuesList();
    }

    public String getSelectedSessionName() {
        return sessionList.getSelectedValue();
    }

    public List<String> getSelectedSessionNames() {
        return sessionList.getSelectedValuesList();
    }

    public SessionPath getSelectedSessionPath() {
        return new SessionPath(getSelectedCorpus(), getSelectedSessionName());
    }

    public boolean isBlindMode() {
        return blindModeBox.isSelected();
    }

    /**
     * Get the project
     * @return project
     */
    public Project getProject() {
        return getExtension(Project.class);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(700, 500);
    }

    @Override
    public void setJMenuBar(JMenuBar menu) {
        super.setJMenuBar(menu);

        JMenu projectMenu = new JMenu("Project");

        int projectMenuIndex = -1;
        // get the edit menu and add view commands
        for (int i = 0; i < menu.getMenuCount(); i++) {
            JMenu currentBar = menu.getMenu(i);

            if (currentBar != null && currentBar.getText() != null && currentBar.getText().equals("Workspace")) {
                projectMenuIndex = i + 1;
            }
        }

        if (projectMenuIndex > 0) {
            menu.add(projectMenu, projectMenuIndex);
        }

        // refresh lists 
        final RefreshAction refreshItem = new RefreshAction(this);
        projectMenu.add(refreshItem);
        projectMenu.addSeparator();

        // create corpus item
        final NewCorpusAction newCorpusItem = new NewCorpusAction(this);
        projectMenu.add(newCorpusItem);

        //       create corpus item
        final NewSessionAction newSessionItem = new NewSessionAction(this);
        projectMenu.add(newSessionItem);

        projectMenu.addSeparator();

        final AnonymizeAction anonymizeParticipantInfoItem = new AnonymizeAction(this);
        projectMenu.add(anonymizeParticipantInfoItem);

        final CheckTranscriptionsAction repairItem = new CheckTranscriptionsAction(this);
        projectMenu.add(repairItem);

        // merge/split sessions
        final DeriveSessionAction deriveItem = new DeriveSessionAction(this);
        projectMenu.add(deriveItem);

        final JMenu teamMenu = new JMenu("Team");
        teamMenu.addMenuListener(new MenuListener() {

            @Override
            public void menuSelected(MenuEvent e) {
                teamMenu.removeAll();
                if (getProject() != null) {
                    final ProjectGitController gitController = new ProjectGitController(getProject());
                    if (gitController.hasGitFolder()) {
                        teamMenu.add(new CommitAction(ProjectWindow.this));

                        teamMenu.addSeparator();

                        teamMenu.add(new PullAction(ProjectWindow.this));
                        teamMenu.add(new PushAction(ProjectWindow.this));

                    } else {
                        final InitAction initRepoAct = new InitAction(ProjectWindow.this);
                        teamMenu.add(initRepoAct);
                    }
                }
            }

            @Override
            public void menuDeselected(MenuEvent e) {

            }

            @Override
            public void menuCanceled(MenuEvent e) {

            }
        });
        projectMenu.addSeparator();
        projectMenu.add(teamMenu);
    }

    private void init() {
        /* Layout */
        setLayout(new BorderLayout());

        final ProjectDataTransferHandler transferHandler = new ProjectDataTransferHandler(this);

        /* Create components */
        newCorpusButton = createNewCorpusButton();
        createCorpusButton = createCorpusButton();

        corpusList = new JList<String>();
        corpusModel = new CorpusListModel(getProject());
        corpusList.setModel(corpusModel);
        corpusList.setCellRenderer(new CorpusListCellRenderer());
        corpusList.setVisibleRowCount(20);
        corpusList.addListSelectionListener(e -> {
            if (getSelectedCorpus() != null) {
                String corpus = getSelectedCorpus();
                sessionModel.setCorpus(corpus);
                sessionList.clearSelection();
                corpusDetails.setCorpus(corpus);

                if (getProject().getCorpusSessions(corpus).size() == 0) {
                    onSwapNewAndCreateSession(newSessionButton);
                } else {
                    onSwapNewAndCreateSession(createSessionButton);
                }
            }
        });
        corpusList.addMouseListener(new MouseInputAdapter() {

            @Override
            public void mousePressed(MouseEvent e) {
                doPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                doPopup(e);
            }

            public void doPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    int clickedIdx = corpusList.locationToIndex(e.getPoint());
                    if (clickedIdx >= 0 && Arrays.binarySearch(corpusList.getSelectedIndices(), clickedIdx) < 0) {
                        corpusList.setSelectedIndex(clickedIdx);
                    }
                    showCorpusListContextMenu(e.getPoint());
                }
            }
        });

        final DragSource corpusDragSource = new DragSource();
        corpusDragSource.createDefaultDragGestureRecognizer(corpusList, DnDConstants.ACTION_COPY, (event) -> {
            final List<ProjectPath> paths = new ArrayList<>();
            for (String corpus : getSelectedCorpora()) {
                final ProjectPath corpusPath = new ProjectPath(getProject(), corpus, null);
                paths.add(corpusPath);
            }
            final ProjectPathTransferable transferable = new ProjectPathTransferable(paths);
            event.startDrag(DragSource.DefaultCopyDrop, transferable);
        });

        corpusList.setDragEnabled(true);
        corpusList.setTransferHandler(transferHandler);

        corpusDetails = new CorpusDetailsPane(getProject());
        corpusDetails.setWrapStyleWord(true);
        corpusDetails.setRows(6);
        corpusDetails.setLineWrap(true);
        corpusDetails.setBackground(Color.white);
        corpusDetails.setOpaque(true);
        JScrollPane corpusDetailsScroller = new JScrollPane(corpusDetails);

        sessionList = new JList<String>();
        newSessionButton = createNewSessionButton();
        createSessionButton = createSessionButton();
        sessionModel = new SessionListModel(getProject());
        sessionList.setModel(sessionModel);
        sessionList.setCellRenderer(new SessionListCellRenderer());
        sessionList.setVisibleRowCount(20);
        sessionList.addListSelectionListener(e -> {
            if (sessionList.getSelectedValue() != null && !e.getValueIsAdjusting()) {
                String corpus = getSelectedCorpus();
                String session = getSelectedSessionName();

                sessionDetails.setSession(corpus, session);
            }
        });
        sessionList.addMouseListener(new MouseInputAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() == 2 && e.getButton() == 1) {
                    // get the clicked item
                    int clickedItem = sessionList.locationToIndex(e.getPoint());
                    if (sessionList.getModel().getElementAt(clickedItem) == null)
                        return;

                    final String session = sessionList.getModel().getElementAt(clickedItem).toString();
                    final String corpus = ((SessionListModel) sessionList.getModel()).getCorpus();

                    msgPanel.reset();
                    msgPanel.setMessageLabel("Opening '" + corpus + "." + session + "'");
                    msgPanel.setIndeterminate(true);
                    msgPanel.repaint();

                    SwingUtilities.invokeLater(() -> {
                        final ActionEvent ae = new ActionEvent(sessionList, -1, "openSession");
                        (new OpenSessionAction(ProjectWindow.this, corpus, session)).actionPerformed(ae);

                        msgPanel.setIndeterminate(false);
                    });
                }
            }

            @Override
            public void mousePressed(MouseEvent e) {
                doPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                doPopup(e);
            }

            public void doPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    int clickedIdx = sessionList.locationToIndex(e.getPoint());
                    if (clickedIdx >= 0 && Arrays.binarySearch(sessionList.getSelectedIndices(), clickedIdx) < 0) {
                        sessionList.setSelectedIndex(clickedIdx);
                    }
                    showSessionListContextMenu(e.getPoint());
                }
            }
        });

        sessionList.setDragEnabled(true);
        sessionList.setTransferHandler(transferHandler);

        final DragSource sessionDragSource = new DragSource();
        sessionDragSource.createDefaultDragGestureRecognizer(sessionList, DnDConstants.ACTION_COPY, (event) -> {
            final List<ProjectPath> paths = new ArrayList<>();
            final String corpus = getSelectedCorpus();
            if (corpus == null)
                return;
            for (String session : getSelectedSessionNames()) {
                final ProjectPath sessionPath = new ProjectPath(getProject(), corpus, session);
                paths.add(sessionPath);
            }
            final ProjectPathTransferable transferable = new ProjectPathTransferable(paths);
            event.startDrag(DragSource.DefaultCopyDrop, transferable);
        });

        sessionDetails = new SessionDetailsPane(getProject());
        sessionDetails.setLineWrap(true);
        sessionDetails.setRows(6);
        sessionDetails.setWrapStyleWord(true);
        sessionDetails.setBackground(Color.white);
        sessionDetails.setOpaque(true);
        JScrollPane sessionDetailsScroller = new JScrollPane(sessionDetails);

        JScrollPane corpusScroller = new JScrollPane(corpusList);
        JScrollPane sessionScroller = new JScrollPane(sessionList);

        blindModeBox = new JCheckBox("Blind transcription");
        blindModeBox.setSelected(false);

        msgPanel = new StatusPanel();

        corpusPanel = new JPanel(new BorderLayout());
        corpusPanel.add(newCorpusButton, BorderLayout.NORTH);
        corpusPanel.add(corpusScroller, BorderLayout.CENTER);
        corpusPanel.add(corpusDetailsScroller, BorderLayout.SOUTH);

        sessionPanel = new JPanel(new BorderLayout());
        sessionPanel.add(newSessionButton, BorderLayout.NORTH);
        sessionPanel.add(sessionScroller, BorderLayout.CENTER);
        sessionPanel.add(sessionDetailsScroller, BorderLayout.SOUTH);

        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        splitPane.setLeftComponent(corpusPanel);
        splitPane.setRightComponent(sessionPanel);
        splitPane.setResizeWeight(0.5);

        // invoke later
        SwingUtilities.invokeLater(() -> {
            splitPane.setDividerLocation(0.5);
        });

        // the frame layout
        String projectName = null;
        projectName = getProject().getName();

        DialogHeader header = new DialogHeader(projectName, StringUtils.abbreviate(projectLoadPath, 80));

        add(header, BorderLayout.NORTH);

        CellConstraints cc = new CellConstraints();
        final JPanel topPanel = new JPanel(new FormLayout("pref, fill:pref:grow, right:pref", "pref"));
        topPanel.add(msgPanel, cc.xy(1, 1));
        topPanel.add(blindModeBox, cc.xy(3, 1));

        add(splitPane, BorderLayout.CENTER);
        add(topPanel, BorderLayout.SOUTH);

        // if no corpora are currently available, 'prompt' the user to create a new one
        if (getProject().getCorpora().size() == 0) {
            SwingUtilities.invokeLater(() -> {
                onSwapNewAndCreateCorpus(newCorpusButton);
            });
        } else {
            SwingUtilities.invokeLater(() -> {
                corpusList.setSelectedIndex(0);
            });
        }
    }

    private MultiActionButton createNewCorpusButton() {
        MultiActionButton retVal = new MultiActionButton();

        ImageIcon folderNewIcn = IconManager.getInstance().getIcon("places/folder", IconSize.SMALL);
        ImageIcon newIcnL = IconManager.getInstance().getIcon("actions/list-add", IconSize.MEDIUM);
        ImageIcon removeIcnL = IconManager.getInstance().getIcon("actions/list-remove", IconSize.MEDIUM);
        ImageIcon renameIcnL = IconManager.getInstance().getIcon("actions/edit-rename", IconSize.MEDIUM);

        String s1 = "Corpus";

        retVal.getTopLabel().setText(WorkspaceTextStyler.toHeaderText(s1));
        retVal.getTopLabel().setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
        retVal.getTopLabel().setFont(FontPreferences.getTitleFont());
        retVal.getTopLabel().setIcon(folderNewIcn);
        retVal.setOpaque(false);

        PhonUIAction newAct = new PhonUIAction(this, "onSwapNewAndCreateCorpus", retVal);
        newAct.putValue(Action.LARGE_ICON_KEY, newIcnL);
        newAct.putValue(Action.SMALL_ICON, folderNewIcn);
        newAct.putValue(Action.NAME, "New corpus");
        newAct.putValue(Action.SHORT_DESCRIPTION, "Create a new corpus folder");

        DeleteCorpusAction deleteCurrentAct = new DeleteCorpusAction(this);
        deleteCurrentAct.putValue(Action.LARGE_ICON_KEY, removeIcnL);

        PhonUIAction renameCurrentAct = new PhonUIAction(this, "onRenameCorpus");
        renameCurrentAct.putValue(Action.LARGE_ICON_KEY, renameIcnL);
        renameCurrentAct.putValue(Action.NAME, "Rename corpus");
        renameCurrentAct.putValue(Action.SHORT_DESCRIPTION, "Rename selected corpus");

        retVal.setDisplayDefaultAction(true);
        retVal.addAction(deleteCurrentAct);
        retVal.addAction(renameCurrentAct);

        retVal.setDefaultAction(newAct);

        retVal.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));

        return retVal;
    }

    public void onRenameCorpus(PhonActionEvent pae) {
        if (getSelectedCorpus() == null) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Please select a corpus").start(corpusList);
            return;
        }
        (new RenameCorpusAction(this)).actionPerformed(pae.getActionEvent());
    }

    private MultiActionButton createCorpusButton() {
        MultiActionButton retVal = new MultiActionButton();

        ImageIcon newIcn = IconManager.getInstance().getIcon("places/folder", IconSize.SMALL);

        String s1 = "Corpus";
        String s2 = "Enter corpus name and press enter.  Press escape to cancel.";

        retVal.getTopLabel().setText(WorkspaceTextStyler.toHeaderText(s1));
        retVal.getTopLabel().setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
        retVal.getTopLabel().setFont(FontPreferences.getTitleFont());
        retVal.getTopLabel().setIcon(newIcn);
        retVal.setAlwaysDisplayActions(true);

        retVal.setOpaque(false);

        ImageIcon cancelIcn = IconManager.getInstance().getIcon("actions/button_cancel", IconSize.SMALL);
        ImageIcon cancelIcnL = cancelIcn;

        PhonUIAction btnSwapAct = new PhonUIAction(this, "onSwapNewAndCreateCorpus", retVal);
        btnSwapAct.putValue(Action.ACTION_COMMAND_KEY, "CANCEL_CREATE_ITEM");
        btnSwapAct.putValue(Action.NAME, "Cancel create");
        btnSwapAct.putValue(Action.SHORT_DESCRIPTION, "Cancel create");
        btnSwapAct.putValue(Action.SMALL_ICON, cancelIcn);
        btnSwapAct.putValue(Action.LARGE_ICON_KEY, cancelIcnL);
        retVal.addAction(btnSwapAct);

        JPanel corpusNamePanel = new JPanel(new BorderLayout());
        corpusNamePanel.setOpaque(false);

        final JTextField corpusNameField = new JTextField();
        corpusNameField.setDocument(new NameDocument());
        corpusNameField.setText("Corpus Name");
        corpusNamePanel.add(corpusNameField, BorderLayout.CENTER);

        ActionMap actionMap = retVal.getActionMap();
        actionMap.put(btnSwapAct.getValue(Action.ACTION_COMMAND_KEY), btnSwapAct);
        InputMap inputMap = retVal.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);

        inputMap.put(ks, btnSwapAct.getValue(Action.ACTION_COMMAND_KEY));

        retVal.setActionMap(actionMap);
        retVal.setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, inputMap);

        PhonUIAction createNewCorpusAct = new PhonUIAction(this, "onCreateCorpus", corpusNameField);
        createNewCorpusAct.putValue(Action.SHORT_DESCRIPTION, "Create new corpus folder");
        createNewCorpusAct.putValue(Action.SMALL_ICON,
                IconManager.getInstance().getIcon("actions/list-add", IconSize.SMALL));

        JButton createBtn = new JButton(createNewCorpusAct);
        corpusNamePanel.add(createBtn, BorderLayout.EAST);

        corpusNameField.setAction(createNewCorpusAct);

        // swap bottom component in new project button
        retVal.setBottomLabelText(WorkspaceTextStyler.toDescText(s2));
        retVal.add(corpusNamePanel, BorderLayout.CENTER);

        retVal.addFocusListener(new FocusListener() {

            @Override
            public void focusLost(FocusEvent e) {
            }

            @Override
            public void focusGained(FocusEvent e) {
                corpusNameField.requestFocus();
            }
        });

        return retVal;
    }

    public void onCreateCorpus(PhonActionEvent pae) {
        final JTextField textField = (JTextField) pae.getData();
        final String corpusName = textField.getText().trim();
        if (corpusName.length() == 0) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Corpus name cannot be empty").start(textField);
            return;
        }

        final NewCorpusAction newCorpusAct = new NewCorpusAction(this, corpusName);
        newCorpusAct.actionPerformed(pae.getActionEvent());
        if (newCorpusAct.isCorpusCreated()) {
            onSwapNewAndCreateCorpus(createCorpusButton);
            corpusList.setSelectedValue(corpusName, true);
        }
    }

    public void onSwapNewAndCreateCorpus(MultiActionButton btn) {
        if (btn == newCorpusButton) {
            corpusPanel.remove(newCorpusButton);
            corpusPanel.add(createCorpusButton, BorderLayout.NORTH);
            createCorpusButton.requestFocus();
        } else {
            corpusPanel.remove(createCorpusButton);
            corpusPanel.add(newCorpusButton, BorderLayout.NORTH);
        }
        corpusPanel.revalidate();
        corpusPanel.repaint();
    }

    private MultiActionButton createNewSessionButton() {
        MultiActionButton retVal = new MultiActionButton();

        ImageIcon newIcn = IconManager.getInstance().getIcon("mimetypes/text-xml", IconSize.SMALL);
        ImageIcon newIcnL = IconManager.getInstance().getIcon("actions/list-add", IconSize.MEDIUM);
        ImageIcon removeIcnL = IconManager.getInstance().getIcon("actions/list-remove", IconSize.MEDIUM);
        ImageIcon renameIcnL = IconManager.getInstance().getIcon("actions/edit-rename", IconSize.MEDIUM);
        ImageIcon openIcnL = IconManager.getInstance().getIcon("actions/view", IconSize.MEDIUM);

        String s1 = "Session";

        retVal.getTopLabel().setText(WorkspaceTextStyler.toHeaderText(s1));
        retVal.getTopLabel().setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
        retVal.getTopLabel().setFont(FontPreferences.getTitleFont());
        retVal.getTopLabel().setIcon(newIcn);
        retVal.setOpaque(false);

        PhonUIAction newAct = new PhonUIAction(this, "onSwapNewAndCreateSession", retVal);
        newAct.putValue(Action.LARGE_ICON_KEY, newIcnL);
        newAct.putValue(Action.SMALL_ICON, newIcn);
        newAct.putValue(Action.NAME, "New session");
        newAct.putValue(Action.SHORT_DESCRIPTION, "Create a new session in selected corpus");
        retVal.setDefaultAction(newAct);

        DeleteSessionAction deleteCurrentAct = new DeleteSessionAction(this);
        deleteCurrentAct.putValue(Action.LARGE_ICON_KEY, removeIcnL);

        PhonUIAction renameCurrentAct = new PhonUIAction(this, "onRenameSession");
        renameCurrentAct.putValue(Action.LARGE_ICON_KEY, renameIcnL);
        renameCurrentAct.putValue(Action.NAME, "Rename session");
        renameCurrentAct.putValue(Action.SHORT_DESCRIPTION, "Rename selected session");

        PhonUIAction openCurrentAct = new PhonUIAction(this, "onOpenSession");
        openCurrentAct.putValue(Action.LARGE_ICON_KEY, openIcnL);
        openCurrentAct.putValue(Action.NAME, "Open session");
        openCurrentAct.putValue(Action.SHORT_DESCRIPTION, "Open selected session");

        retVal.setDisplayDefaultAction(true);
        retVal.addAction(deleteCurrentAct);
        retVal.addAction(renameCurrentAct);
        retVal.addAction(openCurrentAct);

        retVal.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));

        return retVal;
    }

    public void onRenameSession(PhonActionEvent pae) {
        if (getSelectedCorpus() == null) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Please select a corpus").start(corpusList);
            return;
        }
        if (getSelectedSessionName() == null) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Please select a session").start(sessionList);
            return;
        }
        (new RenameSessionAction(this)).actionPerformed(pae.getActionEvent());
    }

    public void onOpenSession(PhonActionEvent pae) {
        if (getSelectedCorpus() == null) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Please select a corpus").start(corpusList);
            return;
        }
        if (getSelectedSessionName() == null) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Please select a session").start(sessionList);
            return;
        }
        (new OpenSessionAction(this, getSelectedCorpus(), getSelectedSessionName()))
                .actionPerformed(pae.getActionEvent());
    }

    private MultiActionButton createSessionButton() {
        MultiActionButton retVal = new MultiActionButton();

        ImageIcon newIcn = IconManager.getInstance().getIcon("mimetypes/text-xml", IconSize.SMALL);

        String s1 = "Session";
        String s2 = "Enter session name and press enter.  Press escape to cancel.";

        retVal.getTopLabel().setText(WorkspaceTextStyler.toHeaderText(s1));
        retVal.getTopLabel().setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
        retVal.getTopLabel().setFont(FontPreferences.getTitleFont());
        retVal.getTopLabel().setIcon(newIcn);
        retVal.setAlwaysDisplayActions(true);

        retVal.setOpaque(false);

        ImageIcon cancelIcn = IconManager.getInstance().getIcon("actions/button_cancel", IconSize.SMALL);
        ImageIcon cancelIcnL = cancelIcn;

        PhonUIAction btnSwapAct = new PhonUIAction(this, "onSwapNewAndCreateSession", retVal);
        btnSwapAct.putValue(Action.ACTION_COMMAND_KEY, "CANCEL_CREATE_ITEM");
        btnSwapAct.putValue(Action.NAME, "Cancel create");
        btnSwapAct.putValue(Action.SHORT_DESCRIPTION, "Cancel create");
        btnSwapAct.putValue(Action.SMALL_ICON, cancelIcn);
        btnSwapAct.putValue(Action.LARGE_ICON_KEY, cancelIcnL);
        retVal.addAction(btnSwapAct);

        JPanel sessionNamePanel = new JPanel(new BorderLayout());
        sessionNamePanel.setOpaque(false);

        final JTextField sessionNameField = new JTextField();
        sessionNameField.setDocument(new NameDocument());
        sessionNameField.setText("Session Name");
        sessionNamePanel.add(sessionNameField, BorderLayout.CENTER);

        ActionMap actionMap = retVal.getActionMap();
        actionMap.put(btnSwapAct.getValue(Action.ACTION_COMMAND_KEY), btnSwapAct);
        InputMap inputMap = retVal.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);

        inputMap.put(ks, btnSwapAct.getValue(Action.ACTION_COMMAND_KEY));

        retVal.setActionMap(actionMap);
        retVal.setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, inputMap);

        PhonUIAction createNewSessionAct = new PhonUIAction(this, "onCreateSession", sessionNameField);
        createNewSessionAct.putValue(Action.SHORT_DESCRIPTION, "Create new session in selected corpus");
        createNewSessionAct.putValue(Action.SMALL_ICON,
                IconManager.getInstance().getIcon("actions/list-add", IconSize.SMALL));

        JButton createBtn = new JButton(createNewSessionAct);
        sessionNamePanel.add(createBtn, BorderLayout.EAST);

        sessionNameField.setAction(createNewSessionAct);

        // swap bottom component in new project button
        retVal.setBottomLabelText(WorkspaceTextStyler.toDescText(s2));
        retVal.add(sessionNamePanel, BorderLayout.CENTER);

        retVal.addFocusListener(new FocusListener() {

            @Override
            public void focusLost(FocusEvent e) {

            }

            @Override
            public void focusGained(FocusEvent e) {
                sessionNameField.requestFocus();
            }
        });

        return retVal;
    }

    public void onSwapNewAndCreateSession(MultiActionButton btn) {
        if (btn == newSessionButton) {
            if (corpusList.getSelectedValue() == null) {
                Toolkit.getDefaultToolkit().beep();
                ToastFactory.makeToast("Please select a corpus").start(newSessionButton);
                return;
            }
            sessionPanel.remove(newSessionButton);
            sessionPanel.add(createSessionButton, BorderLayout.NORTH);
            createSessionButton.requestFocus();
        } else {
            sessionPanel.remove(createSessionButton);
            sessionPanel.add(newSessionButton, BorderLayout.NORTH);
        }
        sessionPanel.revalidate();
        sessionPanel.repaint();
    }

    public void onCreateSession(PhonActionEvent pae) {
        final JTextField textField = (JTextField) pae.getData();
        final String sessionName = textField.getText().trim();
        if (sessionName.length() == 0) {
            Toolkit.getDefaultToolkit().beep();
            ToastFactory.makeToast("Session name cannot be empty").start(textField);
            return;
        }

        final NewSessionAction newSessionAct = new NewSessionAction(this, getSelectedCorpus(), sessionName);
        newSessionAct.actionPerformed(pae.getActionEvent());
        if (newSessionAct.isSessionCreated()) {
            onSwapNewAndCreateSession(createSessionButton);
            sessionList.setSelectedValue(sessionName, true);
        }
    }

    /** 
     * Displays the corpus list menu
     * 
     * @param clickPoint
     */
    private void showCorpusListContextMenu(Point clickPoint) {
        List<String> corpora = getSelectedCorpora();

        JPopupMenu contextMenu = new JPopupMenu();

        if (corpora.size() == 1) {
            // new session item
            JMenuItem newSessionItem = new JMenuItem(new NewSessionAction(this));
            contextMenu.add(newSessionItem);

            contextMenu.addSeparator();

            JMenuItem templateItem = new JMenuItem(new OpenCorpusTemplateAction(this));
            contextMenu.add(templateItem);

            contextMenu.addSeparator();
        }

        JMenuItem dupItem = new JMenuItem(new DuplicateCorpusAction(this));
        if (corpora.size() > 1) {
            dupItem.setText("Duplicate Corpora");
        }
        contextMenu.add(dupItem);

        if (corpora.size() == 1) {
            // rename
            JMenuItem renameItem = new JMenuItem(new RenameCorpusAction(this));
            contextMenu.add(renameItem);
        }

        // delete
        JMenuItem deleteItem = new JMenuItem(new DeleteCorpusAction(this));
        if (corpora.size() > 1) {
            deleteItem.setText("Delete Corpora");
        }
        contextMenu.add(deleteItem);

        contextMenu.show(corpusList, clickPoint.x, clickPoint.y);
    }

    /**
     * Displays the session list menu
     * 
     * @param clickPoint
     */
    private void showSessionListContextMenu(Point clickPoint) {
        List<String> selectedSessions = getSelectedSessionNames();

        JPopupMenu contextMenu = new JPopupMenu();

        if (selectedSessions.size() == 1) {
            // open item
            JMenuItem openItem = new JMenuItem(new OpenSessionAction(this));
            contextMenu.add(openItem);

            contextMenu.addSeparator();
        }

        // rename item
        JMenuItem duplicateItem = new JMenuItem(new DuplicateSessionAction(this));
        if (selectedSessions.size() > 1) {
            duplicateItem.setText("Duplicate Sessions");
        }
        contextMenu.add(duplicateItem);

        if (selectedSessions.size() == 1) {
            JMenuItem renameItem = new JMenuItem(new RenameSessionAction(this));
            contextMenu.add(renameItem);
        }

        // delete item
        JMenuItem deleteItem = new JMenuItem(new DeleteSessionAction(this));
        if (selectedSessions.size() > 1) {
            deleteItem.setText("Delete Sessions");
        }
        contextMenu.add(deleteItem);

        contextMenu.show(sessionList, clickPoint.x, clickPoint.y);
    }

    public void windowActivated(WindowEvent e) {

    }

    public void windowClosed(WindowEvent e) {
        getProject().removeProjectListener(myProjectListener);

        // are there any other project windows open?
        boolean otherProjectsOpen = false;

        // close all other project windows
        for (CommonModuleFrame f : CommonModuleFrame.getOpenWindows()) {

            if (f instanceof ProjectWindow && f != this)
                otherProjectsOpen = true;
            else if (f instanceof WorkspaceDialog)
                otherProjectsOpen = true; // also don't close if workspace window is still open
        }

        // open the open-project window on Windows if no other project window
        // is open
        if (!otherProjectsOpen) {
            try {
                PluginEntryPointRunner.executePlugin("Exit");
            } catch (PluginException e1) {
                LOGGER.log(Level.SEVERE, e1.getLocalizedMessage(), e1);
                System.exit(1);
            }
        }
    }

    public void windowClosing(WindowEvent e) {
    }

    public void windowDeactivated(WindowEvent e) {

    }

    public void windowDeiconified(WindowEvent e) {

    }

    public void windowIconified(WindowEvent e) {

    }

    public void windowOpened(WindowEvent e) {

    }

    @Override
    public void dispose() {
        super.dispose();
    }

    public void updateLists() {
        CorpusListModel corpusListModel = (CorpusListModel) corpusList.getModel();
        corpusListModel.refresh();
        corpusList.repaint();

        SessionListModel sessionListModel = (SessionListModel) sessionList.getModel();
        sessionListModel.refresh();
        if (corpusList.getSelectedIndex() >= 0 && corpusList.getSelectedIndex() < corpusList.getModel().getSize()
                && corpusList.getSelectedValue() != null)
            sessionListModel.setCorpus(corpusList.getSelectedValue().toString());
        else
            sessionListModel.setCorpus(null);

        sessionList.repaint();
    }

    public StatusPanel getStatusPanel() {
        return msgPanel;
    }

    public void refreshProject() {
        final Project project = getProject();
        final ProjectRefresh impl = project.getExtension(ProjectRefresh.class);
        if (impl != null) {
            impl.refresh();
            updateLists();
        }
    }

    public class StatusPanel extends JComponent {

        private JLabel msgLabel = new JLabel("");
        private JXBusyLabel progressBar = new JXBusyLabel(new Dimension(16, 16));

        public StatusPanel() {
            super();
            init();
        }

        private void init() {
            setLayout(new HorizontalLayout(3));

            add(progressBar);
            add(msgLabel);
        }

        public void setIndeterminate(final boolean v) {
            SwingUtilities.invokeLater(new Runnable() {

                public void run() {
                    progressBar.setBusy(v);
                    if (!v)
                        msgLabel.setText("");
                }

            });
        }

        public void setMessageLabel(String txt) {
            final String text = txt;
            SwingUtilities.invokeLater(new Runnable() {

                public void run() {
                    msgLabel.setText(text);
                }

            });
        }

        public void reset() {
            setIndeterminate(false);
            setMessageLabel("");
        }

    }

    public class NameDocument extends PlainDocument {
        /**
         * Ensure proper project names.
         * 
         * Project name must start with a letter, and can be followed
         * by at most 30 letters, numbers, underscores, dashes.
         */
        private String projectRegex = "[a-zA-Z0-9][- a-zA-Z_0-9]{0,29}";

        @Override
        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
            // only allow valid inputs
            String p1 = super.getText(0, offs);
            String p2 = super.getText(offs, getLength() - offs);
            String val = p1 + str + p2;

            if (val.matches(projectRegex)) {
                super.insertString(offs, str, a);
            }
        }
    }

}