com.github.fritaly.dualcommander.DualCommander.java Source code

Java tutorial

Introduction

Here is the source code for com.github.fritaly.dualcommander.DualCommander.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.fritaly.dualcommander;

import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import net.miginfocom.swing.MigLayout;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.log4j.Logger;

import com.jgoodies.looks.windows.WindowsLookAndFeel;

public class DualCommander extends JFrame
        implements ChangeListener, WindowListener, KeyListener, PropertyChangeListener, FocusListener {

    private static final long serialVersionUID = 5445919782222373150L;

    // TODO Add support for Ctrl+C / Ctrl-X / Ctrl-V with the system clipboard
    // TODO Add support for drag'n'drop between file panes
    // TODO Add a contextual menu to file entries
    // TODO Use the new FileSystem API from Java 7 to introspect archive (zip, jar, war, etc) files
    // TODO Add icons for each action
    // TODO Implement the actions F3 -> F8
    private final class AboutAction extends AbstractAction {

        private static final long serialVersionUID = -8972233122891491665L;

        public AboutAction() {
            super("About", Icons.HELP_ICON);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JOptionPane.showMessageDialog(DualCommander.this, "Not implemented yet", "Error",
                    JOptionPane.ERROR_MESSAGE);
        }
    }

    private final class PreferencesAction extends AbstractAction {

        private static final long serialVersionUID = -4073587940746786910L;

        public PreferencesAction() {
            super("Preferences", Icons.COG_ICON);
        }

        public JButton getButton(final JOptionPane optionPane, String text, Icon icon) {
            final JButton button = new JButton(text, icon);
            button.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent actionEvent) {
                    optionPane.setValue(button.getText());
                }
            });

            return button;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            final UserPreferencesPanel panel = new UserPreferencesPanel(getPreferences());

            final JOptionPane optionPane = new JOptionPane();
            optionPane.setMessage(panel);
            optionPane.setMessageType(JOptionPane.PLAIN_MESSAGE);
            optionPane.setOptionType(JOptionPane.YES_NO_OPTION);

            final JButton applyButton = getButton(optionPane, "Apply", Icons.ACCEPT_ICON);
            final JButton cancelButton = getButton(optionPane, "Cancel", Icons.CANCEL_ICON);

            optionPane.setOptions(new Object[] { applyButton, cancelButton });

            final JDialog dialog = optionPane.createDialog(DualCommander.this, "Preferences");
            dialog.setVisible(true);

            final Object value = optionPane.getValue();

            if ("Apply".equals(value)) {
                // User clicked on "Apply"
                getPreferences().apply(panel.getPreferences());
            } else {
                // User clicked "Cancel" or closed the dialog
            }
        }
    }

    private final class ViewAction extends AbstractAction {

        private static final long serialVersionUID = -3885021368508066211L;

        public ViewAction() {
            super("F3 View");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                // What's the active pane's file selection ?
                final List<File> selection = activePane.getActiveBrowser().getSelection();

                if (!selection.isEmpty() && (selection.size() == 1)) {
                    final File file = selection.iterator().next();

                    if (file.isFile()) {
                        new ProcessBuilder(preferences.getViewFileCommand(), file.getAbsolutePath()).start();
                    }
                }
            } catch (IOException e1) {
                logger.error("Error when viewing file", e1);
            }
        }
    }

    private final class EditAction extends AbstractAction {

        private static final long serialVersionUID = -4570243231693650751L;

        public EditAction() {
            super("F4 Edit", Icons.EDIT_ICON);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                // What's the active pane's file selection ?
                final List<File> selection = activePane.getActiveBrowser().getSelection();

                if (!selection.isEmpty() && (selection.size() == 1)) {
                    final File file = selection.iterator().next();

                    if (file.isFile()) {
                        new ProcessBuilder(preferences.getEditFileCommand(), file.getAbsolutePath()).start();
                    }
                }
            } catch (IOException e1) {
                logger.error("Error when editing file", e1);
            }
        }
    }

    private final class CopyAction extends AbstractAction {

        private static final long serialVersionUID = -4681732083985135686L;

        public CopyAction() {
            super("F5 Copy", Icons.COPY_ICON);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            // What's the active pane's file selection ?
            final List<File> selection = activePane.getActiveBrowser().getSelection();

            if (selection.isEmpty()) {
                return;
            }

            // Count the # of files / folders to copy
            final Scan scan = Utils.scan(selection);

            final String message;

            if (scan.getFiles() > 0) {
                if (scan.getDirectories() > 0) {
                    message = String.format("Do you really want to copy %d file(s) & %d folder(s)", scan.getFiles(),
                            scan.getDirectories());
                } else {
                    message = String.format("Do you really want to copy %d file(s)", scan.getFiles());
                }
            } else {
                message = String.format("Do you really want to copy %d folders(s)", scan.getDirectories());
            }

            // TODO Set icon on dialog boxes
            final int reply = JOptionPane.showConfirmDialog(DualCommander.this, message, "Please confirm",
                    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

            if (reply == JOptionPane.YES_OPTION) {
                // Store the inactive pane before the active one loses the focus
                final TabbedPane inactivePane = getInactivePane();

                // Copy the file(s) in a background task
                final SwingWorker<Void, Void> task = new SwingWorker<Void, Void>() {
                    @Override
                    protected Void doInBackground() throws Exception {
                        // TODO Use a progress bar (to notify the progress)
                        final File targetDir = inactivePane.getActiveBrowser().getDirectory();

                        setWaitCursor();

                        try {
                            for (File file : selection) {
                                // TODO Check whether the target file already exists or
                                // not

                                if (file.isFile()) {
                                    FileUtils.copyFileToDirectory(file, targetDir, true);

                                    if (logger.isInfoEnabled()) {
                                        logger.info(String.format("Copied file %s to directory %s",
                                                file.getAbsolutePath(), targetDir.getAbsolutePath()));
                                    }
                                } else if (file.isDirectory()) {
                                    FileUtils.copyDirectoryToDirectory(file, targetDir);

                                    if (logger.isInfoEnabled()) {
                                        logger.info(String.format("Copied directory %s to directory %s",
                                                file.getAbsolutePath(), targetDir.getAbsolutePath()));
                                    }
                                }
                            }

                            if (logger.isInfoEnabled()) {
                                logger.info(String.format("Copied %d file(s)", selection.size()));
                            }
                        } catch (IOException e1) {
                            JOptionPane.showMessageDialog(DualCommander.this,
                                    "An error occured when copying the file(s)", "Error",
                                    JOptionPane.ERROR_MESSAGE);
                        }

                        return null;
                    }

                    @Override
                    protected void done() {
                        // Refresh the target panel (the inactive one)
                        inactivePane.getActiveBrowser().refresh();

                        setDefaultCursor();
                    }
                };

                task.execute();
            }
        }
    }

    private final class MoveAction extends AbstractAction {

        private static final long serialVersionUID = -3452333607164390841L;

        public MoveAction() {
            super("F6 Move");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            // What's the active pane's file selection ?
            final List<File> selection = activePane.getActiveBrowser().getSelection();

            if (selection.isEmpty()) {
                return;
            }

            // Count the # of files / folders to move
            final Scan scan = Utils.scan(selection);

            final String message;

            if (scan.getFiles() > 0) {
                if (scan.getDirectories() > 0) {
                    message = String.format("Do you really want to move %d file(s) & %d folder(s)", scan.getFiles(),
                            scan.getDirectories());
                } else {
                    message = String.format("Do you really want to move %d file(s)", scan.getFiles());
                }
            } else {
                message = String.format("Do you really want to move %d folders(s)", scan.getDirectories());
            }

            // TODO Set icon on dialog boxes
            final int reply = JOptionPane.showConfirmDialog(DualCommander.this, message, "Please confirm",
                    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

            if (reply == JOptionPane.YES_OPTION) {
                // Store the inactive pane before the active one loses the focus
                final TabbedPane inactivePane = getInactivePane();

                final SwingWorker<Void, Void> task = new SwingWorker<Void, Void>() {
                    @Override
                    protected Void doInBackground() throws Exception {
                        // Move the file(s)
                        setWaitCursor();

                        // TODO Use a progress bar (if necessary)
                        final File targetDir = inactivePane.getActiveBrowser().getDirectory();

                        for (File file : selection) {
                            final String initialPath = file.getAbsolutePath();

                            // TODO Check whether the target file already exists or
                            // not
                            final File targetFile = new File(targetDir, file.getName());

                            file.renameTo(targetFile);

                            if (logger.isInfoEnabled()) {
                                logger.info(String.format("Moved file %s to directory %s", initialPath,
                                        targetDir.getAbsolutePath()));
                            }
                        }

                        if (logger.isInfoEnabled()) {
                            logger.info(String.format("Moved %d file(s)", selection.size()));
                        }

                        return null;
                    }

                    @Override
                    protected void done() {
                        // Refresh the 2 panes
                        leftPane.getActiveBrowser().refresh();
                        rightPane.getActiveBrowser().refresh();

                        setDefaultCursor();
                    }
                };

                task.execute();
            }
        }
    }

    private final class MkdirAction extends AbstractAction {

        private static final long serialVersionUID = -6354029847164675819L;

        public MkdirAction() {
            super("F7 Mkdir");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JOptionPane.showMessageDialog(DualCommander.this, "Not implemented yet", "Error",
                    JOptionPane.ERROR_MESSAGE);
        }
    }

    private final class DeleteAction extends AbstractAction {

        private static final long serialVersionUID = -4059922323563836824L;

        public DeleteAction() {
            super("F8 Delete", Icons.DELETE_ICON);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            // What's the active pane's file selection ?
            final List<File> selection = activePane.getActiveBrowser().getSelection();

            if (selection.isEmpty()) {
                return;
            }

            // Count the # of files / folders to delete
            final Scan scan = Utils.scan(selection);

            final String message;

            if (scan.getFiles() > 0) {
                if (scan.getDirectories() > 0) {
                    message = String.format("Do you really want to delete %d file(s) & %d folder(s)",
                            scan.getFiles(), scan.getDirectories());
                } else {
                    message = String.format("Do you really want to delete %d file(s)", scan.getFiles());
                }
            } else {
                message = String.format("Do you really want to delete %d folders(s)", scan.getDirectories());
            }

            // TODO Set icon on dialog boxes
            final int reply = JOptionPane.showConfirmDialog(DualCommander.this, message, "Please confirm",
                    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

            if (reply == JOptionPane.YES_OPTION) {
                // Store the active pane before it loses the focus
                final TabbedPane activePane = getActivePane();

                // Delete the file(s) in a background thread
                final SwingWorker<Void, Void> task = new SwingWorker<Void, Void>() {
                    @Override
                    protected Void doInBackground() throws Exception {
                        // TODO Use a progress bar (if necessary)
                        setWaitCursor();

                        for (File file : selection) {
                            Utils.deleteRecursively(file, null);
                        }

                        if (logger.isInfoEnabled()) {
                            logger.info(String.format("Deleted %d file(s)", selection.size()));
                        }

                        return null;
                    }

                    @Override
                    protected void done() {
                        // Refresh the source panel (the active one)
                        activePane.getActiveBrowser().refresh();

                        setDefaultCursor();
                    }
                };

                task.execute();
            }
        }
    }

    private final class QuitAction extends AbstractAction {

        private static final long serialVersionUID = 388422465301643805L;

        public QuitAction() {
            super("Alt+F4 Quit", Icons.DOOR_OPEN_ICON);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            // TODO Ask a user confirm before exiting
            DualCommander.this.dispose();
        }
    }

    private static JButton createButton(Action action) {
        Validate.notNull(action, "The given action is null");

        final JButton button = new JButton(action);
        button.setFocusable(false);

        return button;
    }

    private final Logger logger = Logger.getLogger(this.getClass());

    // --- Actions --- //

    private final AboutAction aboutAction = new AboutAction();

    private final PreferencesAction preferencesAction = new PreferencesAction();

    private final ViewAction viewAction = new ViewAction();

    private final EditAction editAction = new EditAction();

    private final CopyAction copyAction = new CopyAction();

    private final MoveAction moveAction = new MoveAction();

    private final MkdirAction mkdirAction = new MkdirAction();

    private final DeleteAction deleteAction = new DeleteAction();

    private final QuitAction quitAction = new QuitAction();

    // --- Buttons --- //

    private final JButton viewButton = createButton(viewAction);

    private final JButton editButton = createButton(editAction);

    private final JButton copyButton = createButton(copyAction);

    private final JButton moveButton = createButton(moveAction);

    private final JButton mkdirButton = createButton(mkdirAction);

    private final JButton deleteButton = createButton(deleteAction);

    private final JButton quitButton = createButton(quitAction);

    private final TabbedPane leftPane;

    private final TabbedPane rightPane;

    private TabbedPane activePane;

    /**
     * Whether the "shift" key is currently pressed.
     */
    private volatile boolean shiftPressed;

    /**
     * Whether the "meta" key (aka "command") is currently pressed.
     */
    private volatile boolean metaPressed;

    private final UserPreferences preferences = new UserPreferences();

    public DualCommander() {
        // TODO Generate a fat jar at build time
        super(String.format("Dual Commander %s", Utils.getApplicationVersion()));

        if (logger.isInfoEnabled()) {
            logger.info(String.format("Dual Commander %s", Utils.getApplicationVersion()));
        }

        try {
            // Apply the JGoodies L&F
            UIManager.setLookAndFeel(new WindowsLookAndFeel());
        } catch (Exception e) {
            // Not supposed to happen
        }

        // Layout, columns & rows
        setLayout(new MigLayout("insets 0px", "[grow]0px[grow]", "[grow]0px[]"));

        // Create a menu bar
        final JMenu fileMenu = new JMenu("File");
        fileMenu.add(new JMenuItem(preferencesAction));
        fileMenu.add(new JSeparator());
        fileMenu.add(new JMenuItem(quitAction));

        final JMenu helpMenu = new JMenu("Help");
        helpMenu.add(new JMenuItem(aboutAction));

        final JMenuBar menuBar = new JMenuBar();
        menuBar.add(fileMenu);
        menuBar.add(helpMenu);

        setJMenuBar(menuBar);

        this.leftPane = new TabbedPane(preferences);
        this.leftPane.setName("Left");
        this.leftPane.addChangeListener(this);
        this.leftPane.addKeyListener(this);
        this.leftPane.addFocusListener(this);

        this.rightPane = new TabbedPane(preferences);
        this.rightPane.setName("Right");
        this.rightPane.addChangeListener(this);
        this.rightPane.addKeyListener(this);
        this.rightPane.addFocusListener(this);

        // Adding the 2 components to the same sizegroup ensures they always
        // keep the same width
        getContentPane().add(leftPane, "grow, sizegroup g1");
        getContentPane().add(rightPane, "grow, sizegroup g1, wrap");

        // The 7 buttons must all have the same width (they must belong to the
        // same size group)
        final JPanel buttonPanel = new JPanel(
                new MigLayout("insets 0px", StringUtils.repeat("[grow, sizegroup g1]", 7), "[grow]"));
        buttonPanel.add(viewButton, "grow");
        buttonPanel.add(editButton, "grow");
        buttonPanel.add(copyButton, "grow");
        buttonPanel.add(moveButton, "grow");
        buttonPanel.add(mkdirButton, "grow");
        buttonPanel.add(deleteButton, "grow");
        buttonPanel.add(quitButton, "grow");

        getContentPane().add(buttonPanel, "grow, span 2");

        // Register shortcuts at a global level (not on every component)
        final InputMap inputMap = this.leftPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0, true), "view");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0, true), "edit");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0, true), "copy");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0, true), "move");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0, true), "mkdir");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0, true), "delete");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.ALT_DOWN_MASK), "quit");

        final ActionMap actionMap = this.leftPane.getActionMap();
        actionMap.put("view", viewAction);
        actionMap.put("edit", editAction);
        actionMap.put("copy", copyAction);
        actionMap.put("move", moveAction);
        actionMap.put("mkdir", mkdirAction);
        actionMap.put("delete", deleteAction);
        actionMap.put("quit", quitAction);

        addWindowListener(this);
        addKeyListener(this);

        // Listen to preference change events
        this.preferences.addPropertyChangeListener(this);

        // Reload the last configuration and init the left & right panels
        // accordingly
        final Preferences prefs = Preferences.userNodeForPackage(this.getClass());

        // The user preferences must be loaded first because they're needed to
        // init the UI
        this.preferences.init(prefs.node("user.preferences"));
        this.leftPane.init(prefs.node("left.panel"));
        this.rightPane.init(prefs.node("right.panel"));

        if (logger.isInfoEnabled()) {
            logger.info("Loaded preferences");
        }

        // Init the buttons
        refreshButtons(this.leftPane.getActiveBrowser().getSelection());

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setExtendedState(JFrame.MAXIMIZED_BOTH);

        if (logger.isInfoEnabled()) {
            logger.info("UI initialized");
        }
    }

    public TabbedPane getActivePane() {
        return activePane;
    }

    public TabbedPane getInactivePane() {
        if (getActivePane() == leftPane) {
            return rightPane;
        }
        if (getActivePane() == rightPane) {
            return leftPane;
        }

        return null;
    }

    private void refreshButtons(List<File> selection) {
        final int size = selection.size();

        // Enable / disable the actions so that key shortcuts and buttons have a
        // consistent behavior

        // Only enabled if only one entry selected
        this.viewAction.setEnabled(size == 1 && selection.iterator().next().isFile());
        this.editButton.setEnabled(size == 1 && selection.iterator().next().isFile());

        // Only enabled if selection isn't empty
        this.copyAction.setEnabled(size > 0);
        this.moveAction.setEnabled(size > 0);
        this.deleteAction.setEnabled(size > 0);

        // Always enabled
        this.mkdirAction.setEnabled(true);
        this.quitAction.setEnabled(true);
    }

    public UserPreferences getPreferences() {
        return preferences;
    }

    @Override
    public void stateChanged(ChangeEvent e) {
        // Event fired when the pane's user selection changes
        if (e.getSource() == this.leftPane) {
            // Update the buttons based on the current selection
            refreshButtons(this.leftPane.getActiveBrowser().getSelection());
        } else if (e.getSource() == this.rightPane) {
            // Update the buttons based on the current selection
            refreshButtons(this.rightPane.getActiveBrowser().getSelection());
        }
    }

    @Override
    public void windowActivated(WindowEvent e) {
    }

    @Override
    public void windowClosed(WindowEvent e) {
    }

    @Override
    public void windowClosing(WindowEvent e) {
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Saving preferences ...");
            }

            // Save the program state
            final Preferences prefs = Preferences.userNodeForPackage(this.getClass());

            this.leftPane.saveState(prefs.node("left.panel"));
            this.rightPane.saveState(prefs.node("right.panel"));
            this.preferences.saveState(prefs.node("user.preferences"));

            prefs.sync();

            if (logger.isInfoEnabled()) {
                logger.info("Saved preferences");
            }
        } catch (BackingStoreException e1) {
            // Not a big deal
        }
    }

    @Override
    public void windowDeactivated(WindowEvent e) {
    }

    @Override
    public void windowDeiconified(WindowEvent e) {
    }

    @Override
    public void windowIconified(WindowEvent e) {
    }

    @Override
    public void windowOpened(WindowEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if ((e.getModifiers() | KeyEvent.SHIFT_MASK) == KeyEvent.SHIFT_MASK) {
            shiftPressed = true;

            if (logger.isDebugEnabled()) {
                logger.debug("[Shift] key pressed");
            }
        }
        if ((e.getModifiers() | KeyEvent.META_MASK) == KeyEvent.META_MASK) {
            metaPressed = true;

            if (logger.isDebugEnabled()) {
                logger.debug("[Meta] key pressed");
            }
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        if ((e.getModifiers() | KeyEvent.SHIFT_DOWN_MASK) == KeyEvent.SHIFT_DOWN_MASK) {
            shiftPressed = false;

            if (logger.isDebugEnabled()) {
                logger.debug("[Shift] key released");
            }
        }
        if ((e.getModifiers() | KeyEvent.META_DOWN_MASK) == KeyEvent.META_DOWN_MASK) {
            metaPressed = false;

            if (logger.isDebugEnabled()) {
                logger.debug("[Meta] key released");
            }
        }
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void focusGained(FocusEvent e) {
        if (e.getSource() == leftPane) {
            this.activePane = leftPane;

            if (logger.isDebugEnabled()) {
                logger.debug("Active pane is now left");
            }
        } else if (e.getSource() == rightPane) {
            this.activePane = rightPane;

            if (logger.isDebugEnabled()) {
                logger.debug("Active pane is now right");
            }
        }
    }

    @Override
    public void focusLost(FocusEvent e) {
        if (e.getSource() == leftPane) {
            this.activePane = null;

            if (logger.isDebugEnabled()) {
                logger.debug("Active pane is now null");
            }
        } else if (e.getSource() == rightPane) {
            this.activePane = null;

            if (logger.isDebugEnabled()) {
                logger.debug("Active pane is now null");
            }
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent e) {
        if (e.getSource() == preferences) {
            if (logger.isDebugEnabled()) {
                logger.debug(String.format("User preference '%s' changed: '%s' -> '%s'", e.getPropertyName(),
                        e.getOldValue(), e.getNewValue()));
            }

            // The 'show hidden' property changed, need to refresh the active
            // directory browsers
            // TODO Set a flag for the inactive directory browsers to refresh
            // when they get the focus
            this.leftPane.getActiveBrowser().refresh();
            this.rightPane.getActiveBrowser().refresh();
        }
    }

    public void setDefaultCursor() {
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }

    public void setWaitCursor() {
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new DualCommander().setVisible(true);
            }
        });
    }
}