ImageDirectorySelector.java

/**
 * Copyright 2011, Aiki IT, FotoRenamer
 * <p/>
 * Licensed 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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 de.aikiit.fotorenamer.gui;

import de.aikiit.fotorenamer.exception.InvalidDirectoryException;
import de.aikiit.fotorenamer.exception.NoFilesFoundException;
import de.aikiit.fotorenamer.image.CreationDateFromExifImageRenamer;
import de.aikiit.fotorenamer.util.LocalizationHelper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;

import static de.aikiit.fotorenamer.util.LocalizationHelper.getBundleString;
import static de.aikiit.fotorenamer.util.LocalizationHelper.getParameterizedBundleString;

/**
 * This component provides a means to select images that are to be renamed.
 *
 * @author hirsch, 13.10.2003
 * @version 2004-01-08
 */
class ImageDirectorySelector extends JPanel {
    /**
     * The logger of this class.
     **/
    private static final Logger LOG =
            LogManager.getLogger(ImageDirectorySelector.class);

    /**
     * Contains the selected directory as a text field or any user input.
     */
    private JTextField textField = null;
    /**
     * The UI's button to start directory selection.
     */
    private JButton browseButton = null;
    /**
     * An image icon that is displayed as part of the button.
     */
    private final ImageIcon imageIcon;

    /**
     * Default constructor provides means to create an imageSelect with a given
     * image icon that is able to only work on directories.
     *
     * @param icon This icon is used as a picture in the select
     *             button.
     */
    ImageDirectorySelector(final ImageIcon icon) {
        super();
        this.imageIcon = icon;
        init();
    }

    /**
     * Provides a means to disable this component
     * (e.g. during run of file renaming).
     *
     * @param enable Enable/disable this component.
     */
    public final void setEnabled(final boolean enable) {
        textField.setEnabled(enable);
        browseButton.setEnabled(enable);
    }

    /**
     * This method is used as a blocking call until the user selects something
     * in the UI.
     *
     * @return Returns whether anything is selected within the current
     * configuration.
     */
    // TODO improve design, let class emit an event in case a directory was selected
    public final boolean isWaiting() {
        return Strings.isEmpty(getSelectedDirectory());
    }

    /**
     * Initialize internal UI components.
     */
    private void init() {
        createBaseLayout();
        initTextField();
        makeTextFieldDragAndDropable();
        makeTextFieldReactOnEnterOrCopyPasteFromMainUI();
    }

    private void createBaseLayout() {
        GridBagLayout grid = new GridBagLayout();
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(0, 2, 0, 2);
        setLayout(grid);

        // Add field.
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.gridheight = 1;
        gbc.gridwidth = 2;
        gbc.anchor = GridBagConstraints.WEST;
        textField = new JTextField(60);
        grid.setConstraints(textField, gbc);
        add(textField);

        // Add button.
        gbc.gridx = 2;
        gbc.gridy = 0;
        gbc.gridheight = 1;
        gbc.gridwidth = 1;
        gbc.anchor = GridBagConstraints.EAST;

        // show button
        browseButton = this.imageIcon == null
                ? new JButton(
                getBundleString(
                        "fotorenamer.ui.selector.title"))
                : new JButton(
                getBundleString(
                        "fotorenamer.ui.selector.title"), this.imageIcon);
        browseButton.setMnemonic(getBundleString("fotorenamer.ui.selector.title.mnemonic").charAt(0));
        browseButton.setMargin(new Insets(1, 1, 1, 1));
        grid.setConstraints(browseButton, gbc);
        add(browseButton);
    }

    private void initTextField() {
        // Add action listener and take current contents of textfield as start directory
        browseButton.addActionListener(event -> {

            String currentPath = getSelectedDirectory();
            JFileChooser fileDlg = new JFileChooser(com.google.common.base.Strings.isNullOrEmpty(currentPath) ? null : currentPath);

            fileDlg.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
            fileDlg.setDialogTitle(
                    getBundleString(
                            "fotorenamer.ui.selector.directory"));
            fileDlg.setApproveButtonText(getBundleString(
                    "fotorenamer.ui.selector.select"));

            if (fileDlg.showOpenDialog(this)
                    == JFileChooser.APPROVE_OPTION) {
                // use getCanonicalPath() to avoid ..-path manipulations and
                // try to set the selected file in the UI
                try {
                    textField.setText(
                            fileDlg.getSelectedFile().getCanonicalPath());
                } catch (IOException ioe) {
                    LOG.error("Error while selecting directory, extracted text is: "
                            + textField.getText());
                    LOG.error(ioe.getMessage());
                }
            }
        });
    }

    private void makeTextFieldReactOnEnterOrCopyPasteFromMainUI() {
        // make textfield react on Enter/copied over from MainUIWindow
        textField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(final KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ENTER) {

                    SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
                        @Override
                        protected Void doInBackground() {
                            if (isWaiting()) {
                                showErrorPopup(getBundleString("fotorenamer.ui.error.nodirectory"), getBundleString("fotorenamer.ui.error.nodirectory.title"));
                                return null;
                            }

                            // perform renaming
                            try {
                                CreationDateFromExifImageRenamer renamer =
                                        new CreationDateFromExifImageRenamer(getSelectedDirectory());
                                new Thread(renamer).start();
                            } catch (InvalidDirectoryException uv) {
                                LOG.info("Invalid directory selected: {}", uv.getMessage());
                                showErrorPopup(getParameterizedBundleString("fotorenamer.ui.error.invaliddirectory", uv.getMessage()), getBundleString("fotorenamer.ui.error.invaliddirectory.title"));
                            } catch (NoFilesFoundException kde) {
                                LOG.info("No files found in {}", kde.getMessage());
                                showErrorPopup(getParameterizedBundleString("fotorenamer.ui.error.nofiles", kde.getMessage()), getBundleString("fotorenamer.ui.error.nofiles.title"));
                            }
                            return null;
                        }

                        @Override
                        protected void done() {
                            // TODO how can I communicate with the surrounding UI to block the user from pressing the buttons
                            LOG.debug("Finished working, cannot reset UI from the selector itself. Should find a way to lock the startbutton somehow.");
                        }
                    };
                    // Execute the SwingWorker; GUI will not freeze
                    worker.execute();
                }
            }

        });
    }

    private void makeTextFieldDragAndDropable() {
        // make textfield drag'n'dropable
        textField.setDropTarget(new DropTarget() {
            public synchronized void drop(DropTargetDropEvent evt) {
                try {
                    evt.acceptDrop(DnDConstants.ACTION_COPY);
                    Object transferData = evt
                            .getTransferable().getTransferData(
                                    DataFlavor.javaFileListFlavor);

                    if (transferData instanceof java.util.List) {
                        //noinspection unchecked
                        java.util.List<File> droppedFiles = (java.util.List<File>) transferData;
                        if (!droppedFiles.isEmpty()) {
                            for (File droppedFile : droppedFiles) {
                                if (droppedFile.isDirectory()) {
                                    final String path = droppedFile.getAbsolutePath();
                                    LOG.info("Drag'n'drop done for file: {} with {} element(s) received", path, droppedFiles.size());
                                    textField.setText(path);
                                    break;
                                }
                            }
                        }
                    }

                } catch (Exception ex) {
                    LOG.info("Drag'd'drop did not work due to exception", ex);
                }
            }
        });
    }

    void showErrorPopup(final String message, final String title) {
        JOptionPane.showMessageDialog(null,
                message,
                title,
                JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Current directory is the representation of this component.
     *
     * @return The currently selected directory.
     */
    final String getSelectedDirectory() {
        String currentSelection = LocalizationHelper.removeCrLf(this.textField.getText());
        if (!com.google.common.base.Strings.isNullOrEmpty(currentSelection)) {
            currentSelection = currentSelection.replaceAll("~", System.getProperty("user.home"));
            currentSelection = currentSelection.trim();
            LOG.debug("User input transformed into {}", currentSelection);
        }
        return currentSelection;
    }
}