MainUIWindow.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.image.RemoveExifPrefixRenamer;
import de.aikiit.fotorenamer.image.RenameProgress;
import de.aikiit.fotorenamer.image.RenameResult;
import de.aikiit.fotorenamer.util.ComponentGaugeUtil;
import de.aikiit.fotorenamer.util.Version;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CancellationException;

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

/**
 * This class holds the main application window and allows to select a directory
 * to perform the image name processing.
 *
 * @author hirsch, 13.10.2003
 * @version 2004-01-08
 */
public final class MainUIWindow extends JFrame implements ActionListener {
    /**
     * The logger of this class.
     **/
    private static final Logger LOG =
            LogManager.getLogger(MainUIWindow.class);

    /**
     * Provide version information in the UI (taken from maven).
     */
    private static final String VERSION = getParameterizedBundleString("fotorenamer.ui.main.version", //
            new SimpleDateFormat(getBundleString("fotorenamer.datepattern")).
                    format(new Date(Long.parseLong(
                            Version.TIMESTAMP))), //
            Version.VERSION, //
            Version.TIMESTAMP
    );
    /**
     * Relative location of the UI's image icon.
     */
    private static final String IMAGE_LOCATION =
            "/image/miniCamera.png";

    /**
     * Component containing the help window of this application.
     */
    private final HelpWindow helpWindow = new HelpWindow();
    /**
     * The UI's help button.
     */
    private JButton helpButton = null;
    /**
     * The UI's start button.
     */
    private JButton goButton = null;
    /**
     * The UI's exit button.
     */
    private JButton endButton = null;
    /**
     * The UI's info/versioning button.
     */
    private JButton infoButton = null;
    /**
     * The UI's revert renaming button.
     */
    private JButton revertButton = null;
    /**
     * Component that selects the directory to work on.
     */
    private ImageDirectorySelector imageDirectorySelector = null;
    // Accessed only on the EDT, including the selector's Enter action.
    private boolean renaming;

    private MainUIWindow() {
        init();
    }

    /**
     * Creates and shows the main UI window on the event dispatch thread.
     * Completes synchronously when called on that thread; otherwise queues startup.
     */
    public static void build() {
        if (SwingUtilities.isEventDispatchThread()) {
            new MainUIWindow();
        } else {
            SwingUtilities.invokeLater(MainUIWindow::new);
        }
    }

    /**
     * Helper class to perform the internal initialization of the UI.
     */
    private void init() {
        String os = "[" + System.getProperty("os.name");
        os += " " + System.getProperty("os.version");
        os += " " + System.getProperty("os.arch") + "]";

        this.setTitle(getParameterizedBundleString("fotorenamer.ui.main.title", os));
        this.getContentPane().setLayout(new BorderLayout());
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        // select directory
        JPanel verzeichnis = new JPanel(new BorderLayout());
        this.imageDirectorySelector = new ImageDirectorySelector(
                ComponentGaugeUtil.createImageIcon(IMAGE_LOCATION));
        imageDirectorySelector.onStart(() -> startRenaming(false));
        verzeichnis.add(imageDirectorySelector);

        // help
        JPanel knoepfe = new JPanel(new FlowLayout());
        this.helpButton = new JButton(getBundleString("fotorenamer.ui.main.menu.help"));
        this.helpButton.addActionListener(this);
        this.helpButton.setMnemonic(getBundleString("fotorenamer.ui.main.menu.help.mnemonic").charAt(0));
        knoepfe.add(this.helpButton);

        // info
        this.infoButton = new JButton(getBundleString("fotorenamer.ui.main.menu.info"));
        this.infoButton.addActionListener(this);
        this.infoButton.setMnemonic(getBundleString("fotorenamer.ui.main.menu.info.mnemonic").charAt(0));
        knoepfe.add(this.infoButton);

        // exit
        this.endButton = new JButton(getBundleString("fotorenamer.ui.main.menu.end"));
        this.endButton.addActionListener(this);
        this.endButton.setMnemonic(getBundleString("fotorenamer.ui.main.menu.end.mnemonic").charAt(0));
        knoepfe.add(this.endButton);

        // start
        this.goButton = new JButton(getBundleString("fotorenamer.ui.main.menu.start"));
        this.goButton.addActionListener(this);
        this.goButton.setMnemonic(getBundleString("fotorenamer.ui.main.menu.start.mnemonic").charAt(0));
        knoepfe.add(this.goButton);

        // revert
        this.revertButton = new JButton(getBundleString("fotorenamer.ui.main.menu.revert"));
        this.revertButton.addActionListener(this);
        this.revertButton.setMnemonic(getBundleString("fotorenamer.ui.main.menu.revert.mnemonic").charAt(0));
        knoepfe.add(this.revertButton);

        this.getContentPane().add(verzeichnis, BorderLayout.NORTH);
        this.getContentPane().add(knoepfe, BorderLayout.CENTER);
        this.pack();
        ComponentGaugeUtil.makeCentered(this);
        this.setVisible(true);
    }

    /**
     * Action listener method to react on events.
     *
     * @param event Events that was fired on this component.
     */
    public void actionPerformed(final ActionEvent event) {
        Object source = event.getSource();
        // end
        if (this.endButton.equals(source)) {
            LOG.info("Bye Bye :-)");
            System.exit(0);
        } else if (this.helpButton.equals(source)) {
            LOG.info("Displaying help window.");
            showHelpWindow();
        } else if (this.infoButton.equals(source)) {
            // info
            JOptionPane.showMessageDialog(null,
                    getParameterizedBundleString("fotorenamer.ui.about", VERSION, new SimpleDateFormat("yyyy").
                            format(new Date(
                                    Long.parseLong(
                                            Version.TIMESTAMP)))),
                    getBundleString("fotorenamer.ui.main.version.title"),
                    JOptionPane.INFORMATION_MESSAGE);
        } else if (this.revertButton.equals(source) || this.goButton.equals(source)) {
            startRenaming(revertButton.equals(source));
        }
    }

    /** Takes a snapshot of UI input before starting a single background job. */
    private void startRenaming(final boolean reverse) {
        if (renaming) {
            return;
        }
        String directory = imageDirectorySelector.getSelectedDirectory();
        if (directory == null || directory.isBlank()) {
            JOptionPane.showMessageDialog(this, getBundleString("fotorenamer.ui.error.nodirectory"),
                    getBundleString("fotorenamer.ui.error.nodirectory.title"), JOptionPane.ERROR_MESSAGE);
            return;
        }
        setProcessing(true, reverse);
        ProgressBar progress;
        try {
            progress = new ProgressBar(0);
        } catch (RuntimeException e) {
            setProcessing(false, reverse);
            showFailure(directory, e);
            return;
        }
        RenameWorker.View view = new RenameWorker.View() {
            @Override
            public void progress(RenameProgress update) {
                progress.updateProgress(update);
            }

            @Override
            public void finished() {
                try {
                    progress.dispose();
                } finally {
                    setProcessing(false, reverse);
                }
            }

            @Override
            public void result(RenameResult result) {
                showResult(directory, reverse, result);
            }

            @Override
            public void failure(Throwable cause) {
                showFailure(directory, cause);
            }
        };
        RenameWorker worker = new RenameWorker(() -> reverse
                ? new RemoveExifPrefixRenamer(directory) : new CreationDateFromExifImageRenamer(directory), view);
        progress.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent event) {
                worker.requestCancellation();
                progress.setTitle(getBundleString("fotorenamer.ui.progress.cancelling"));
            }
        });
        try {
            worker.execute();
        } catch (RuntimeException e) {
            view.finished();
            view.failure(e);
        }
    }

    private void setProcessing(final boolean busy, final boolean reverse) {
        renaming = busy;
        goButton.setEnabled(!busy);
        revertButton.setEnabled(!busy);
        imageDirectorySelector.setEnabled(!busy);
        goButton.setText(getBundleString(busy && !reverse
                ? "fotorenamer.ui.main.progress" : "fotorenamer.ui.main.menu.start"));
        revertButton.setText(getBundleString(busy && reverse
                ? "fotorenamer.ui.main.progress" : "fotorenamer.ui.main.menu.revert"));
    }

    private void showResult(final String directory, final boolean reverse, final RenameResult result) {
        String titleKey = result.cancelled() ? "fotorenamer.ui.rename.cancelled"
                : result.failed() > 0 ? "fotorenamer.ui.rename.result.warning"
                : reverse ? "fotorenamer.ui.rerename.success.title" : "fotorenamer.ui.rename.success.title";
        String message = getParameterizedBundleString("fotorenamer.ui.rename.result.summary", directory,
                result.processed(), result.total(), result.renamed(), result.skipped(), result.failed(),
                result.withoutExifDate());
        JOptionPane.showMessageDialog(this, message, getBundleString(titleKey),
                result.cancelled() || result.failed() > 0 ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE);
    }

    private void showFailure(final String directory, final Throwable cause) {
        if (cause instanceof CancellationException) {
            LOG.info("Processing cancelled for '{}'", directory);
            JOptionPane.showMessageDialog(this, getBundleString("fotorenamer.ui.rename.cancelled"),
                    getBundleString("fotorenamer.ui.rename.cancelled"), JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        LOG.error("Unable to process images in '{}'", directory, cause);
        String messageKey = cause instanceof InvalidDirectoryException ? "fotorenamer.ui.error.invaliddirectory"
                : cause instanceof NoFilesFoundException ? "fotorenamer.ui.error.nofiles" : "fotorenamer.ui.rename.error";
        String titleKey = cause instanceof InvalidDirectoryException ? "fotorenamer.ui.error.invaliddirectory.title"
                : cause instanceof NoFilesFoundException ? "fotorenamer.ui.error.nofiles.title" : "fotorenamer.ui.rename.error.title";
        String detail = cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage();
        JOptionPane.showMessageDialog(this, getParameterizedBundleString(messageKey, detail),
                getBundleString(titleKey), JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Helper to enable visibility of help window.
     *
     * @see HelpWindow
     */
    private void showHelpWindow() {
        helpWindow.setVisible(true);
    }
}