/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2019 Guardsquare NV
 *
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.gui;

import proguard.*;
import proguard.classfile.util.ClassUtil;
import proguard.gui.splash.*;
import proguard.util.ListUtil;

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.List;


/**
 * GUI for configuring and executing ProGuard and ReTrace.
 *
 * @author Eric Lafortune
 */
public class ProGuardGUI extends JFrame
{
    private static final String NO_SPLASH_OPTION = "-nosplash";

    private static final String TITLE_IMAGE_FILE          = "vtitle.png";
    private static final String BOILERPLATE_CONFIGURATION = "boilerplate.pro";
    private static final String DEFAULT_CONFIGURATION     = "default.pro";

    private static final String OPTIMIZATIONS_DEFAULT                = "*";
    private static final String KEEP_ATTRIBUTE_DEFAULT               = "Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,LocalVariable*Table,*Annotation*,Synthetic,EnclosingMethod";
    private static final String SOURCE_FILE_ATTRIBUTE_DEFAULT        = "SourceFile";
    private static final String ADAPT_RESOURCE_FILE_NAMES_DEFAULT    = "**.properties";
    private static final String ADAPT_RESOURCE_FILE_CONTENTS_DEFAULT = "**.properties,META-INF/MANIFEST.MF";
    private static final String ELLIPSIS_DOTS                        = "...";

    private static final Border BORDER = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);

    static boolean systemOutRedirected;

    private final JFileChooser configurationChooser = new JFileChooser("");
    private final JFileChooser fileChooser          = new JFileChooser("");

    private final SplashPanel splashPanel;

    private final ClassPathPanel programPanel = new ClassPathPanel(this, true);
    private final ClassPathPanel libraryPanel = new ClassPathPanel(this, false);

    private       KeepClassSpecification[] boilerplateKeep;
    private final JCheckBox[]              boilerplateKeepCheckBoxes;
    private final JTextField[]             boilerplateKeepTextFields;

    private final KeepSpecificationsPanel additionalKeepPanel = new KeepSpecificationsPanel(this, true, false, false, false, false, false);

    private       KeepClassSpecification[] boilerplateKeepNames;
    private final JCheckBox[]              boilerplateKeepNamesCheckBoxes;
    private final JTextField[]             boilerplateKeepNamesTextFields;

    private final KeepSpecificationsPanel additionalKeepNamesPanel = new KeepSpecificationsPanel(this, true, false, false, true, false, false);

    private       ClassSpecification[] boilerplateNoSideEffectMethods;
    private final JCheckBox[]          boilerplateNoSideEffectMethodCheckBoxes;

    private final ClassSpecificationsPanel additionalNoSideEffectsPanel = new ClassSpecificationsPanel(this, false, false);

    private final ClassSpecificationsPanel whyAreYouKeepingPanel = new ClassSpecificationsPanel(this, false, true);

    private final JCheckBox shrinkCheckBox     = new JCheckBox(msg("shrink"));
    private final JCheckBox printUsageCheckBox = new JCheckBox(msg("printUsage"));

    private final JCheckBox optimizeCheckBox                    = new JCheckBox(msg("optimize"));
    private final JCheckBox allowAccessModificationCheckBox     = new JCheckBox(msg("allowAccessModification"));
    private final JCheckBox mergeInterfacesAggressivelyCheckBox = new JCheckBox(msg("mergeInterfacesAggressively"));
    private final JLabel    optimizationsLabel                  = new JLabel(msg("optimizations"));
    private final JLabel    optimizationPassesLabel             = new JLabel(msg("optimizationPasses"));

    private final JSpinner optimizationPassesSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 9, 1));

    private final JCheckBox obfuscateCheckBox                     = new JCheckBox(msg("obfuscate"));
    private final JCheckBox printMappingCheckBox                  = new JCheckBox(msg("printMapping"));
    private final JCheckBox applyMappingCheckBox                  = new JCheckBox(msg("applyMapping"));
    private final JCheckBox obfuscationDictionaryCheckBox         = new JCheckBox(msg("obfuscationDictionary"));
    private final JCheckBox classObfuscationDictionaryCheckBox    = new JCheckBox(msg("classObfuscationDictionary"));
    private final JCheckBox packageObfuscationDictionaryCheckBox  = new JCheckBox(msg("packageObfuscationDictionary"));
    private final JCheckBox overloadAggressivelyCheckBox          = new JCheckBox(msg("overloadAggressively"));
    private final JCheckBox useUniqueClassMemberNamesCheckBox     = new JCheckBox(msg("useUniqueClassMemberNames"));
    private final JCheckBox useMixedCaseClassNamesCheckBox        = new JCheckBox(msg("useMixedCaseClassNames"));
    private final JCheckBox keepPackageNamesCheckBox              = new JCheckBox(msg("keepPackageNames"));
    private final JCheckBox flattenPackageHierarchyCheckBox       = new JCheckBox(msg("flattenPackageHierarchy"));
    private final JCheckBox repackageClassesCheckBox              = new JCheckBox(msg("repackageClasses"));
    private final JCheckBox keepAttributesCheckBox                = new JCheckBox(msg("keepAttributes"));
    private final JCheckBox keepParameterNamesCheckBox            = new JCheckBox(msg("keepParameterNames"));
    private final JCheckBox newSourceFileAttributeCheckBox        = new JCheckBox(msg("renameSourceFileAttribute"));
    private final JCheckBox adaptClassStringsCheckBox             = new JCheckBox(msg("adaptClassStrings"));
    private final JCheckBox adaptResourceFileNamesCheckBox        = new JCheckBox(msg("adaptResourceFileNames"));
    private final JCheckBox adaptResourceFileContentsCheckBox     = new JCheckBox(msg("adaptResourceFileContents"));

    private final JCheckBox preverifyCheckBox    = new JCheckBox(msg("preverify"));
    private final JCheckBox microEditionCheckBox = new JCheckBox(msg("microEdition"));
    private final JCheckBox androidCheckBox      = new JCheckBox(msg("android"));
    private final JCheckBox targetCheckBox       = new JCheckBox(msg("target"));

    private final JComboBox targetComboBox = new JComboBox(ListUtil.commaSeparatedList(msg("targets")).toArray());

    private final JCheckBox verboseCheckBox                          = new JCheckBox(msg("verbose"));
    private final JCheckBox noteCheckBox                             = new JCheckBox(msg("note"));
    private final JCheckBox warnCheckBox                             = new JCheckBox(msg("warn"));
    private final JCheckBox ignoreWarningsCheckBox                   = new JCheckBox(msg("ignoreWarnings"));
    private final JCheckBox skipNonPublicLibraryClassesCheckBox      = new JCheckBox(msg("skipNonPublicLibraryClasses"));
    private final JCheckBox skipNonPublicLibraryClassMembersCheckBox = new JCheckBox(msg("skipNonPublicLibraryClassMembers"));
    private final JCheckBox keepDirectoriesCheckBox                  = new JCheckBox(msg("keepDirectories"));
    private final JCheckBox forceProcessingCheckBox                  = new JCheckBox(msg("forceProcessing"));
    private final JCheckBox printSeedsCheckBox                       = new JCheckBox(msg("printSeeds"));
    private final JCheckBox printConfigurationCheckBox               = new JCheckBox(msg("printConfiguration"));
    private final JCheckBox dumpCheckBox                             = new JCheckBox(msg("dump"));

    private final JTextField printUsageTextField                    = new JTextField(40);
    private final JTextField optimizationsTextField                 = new JTextField(40);
    private final JTextField printMappingTextField                  = new JTextField(40);
    private final JTextField applyMappingTextField                  = new JTextField(40);
    private final JTextField obfuscationDictionaryTextField         = new JTextField(40);
    private final JTextField classObfuscationDictionaryTextField    = new JTextField(40);
    private final JTextField packageObfuscationDictionaryTextField  = new JTextField(40);
    private final JTextField keepPackageNamesTextField              = new JTextField(40);
    private final JTextField flattenPackageHierarchyTextField       = new JTextField(40);
    private final JTextField repackageClassesTextField              = new JTextField(40);
    private final JTextField keepAttributesTextField                = new JTextField(40);
    private final JTextField newSourceFileAttributeTextField        = new JTextField(40);
    private final JTextField adaptClassStringsTextField             = new JTextField(40);
    private final JTextField adaptResourceFileNamesTextField        = new JTextField(40);
    private final JTextField adaptResourceFileContentsTextField     = new JTextField(40);
    private final JTextField noteTextField                          = new JTextField(40);
    private final JTextField warnTextField                          = new JTextField(40);
    private final JTextField keepDirectoriesTextField               = new JTextField(40);
    private final JTextField printSeedsTextField                    = new JTextField(40);
    private final JTextField printConfigurationTextField            = new JTextField(40);
    private final JTextField dumpTextField                          = new JTextField(40);

    private final JTextArea  consoleTextArea = new JTextArea(msg("processingInfo"), 3, 40);

    private final JCheckBox  reTraceVerboseCheckBox  = new JCheckBox(msg("verbose"));
    private final JTextField reTraceMappingTextField = new JTextField(40);
    private final JTextArea  stackTraceTextArea      = new JTextArea(3, 40);
    private final JTextArea  reTraceTextArea         = new JTextArea(msg("reTraceInfo"), 3, 40);


    /**
     * Creates a new ProGuardGUI.
     */
    public ProGuardGUI()
    {
        setTitle("ProGuard");
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        // Create some constraints that can be reused.
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;
        constraints.insets = new Insets(0, 4, 0, 4);

        GridBagConstraints constraintsStretch = new GridBagConstraints();
        constraintsStretch.fill    = GridBagConstraints.HORIZONTAL;
        constraintsStretch.weightx = 1.0;
        constraintsStretch.anchor  = GridBagConstraints.WEST;
        constraintsStretch.insets  = constraints.insets;

        GridBagConstraints constraintsLast = new GridBagConstraints();
        constraintsLast.gridwidth = GridBagConstraints.REMAINDER;
        constraintsLast.anchor    = GridBagConstraints.WEST;
        constraintsLast.insets    = constraints.insets;

        GridBagConstraints constraintsLastStretch = new GridBagConstraints();
        constraintsLastStretch.gridwidth = GridBagConstraints.REMAINDER;
        constraintsLastStretch.fill      = GridBagConstraints.HORIZONTAL;
        constraintsLastStretch.weightx   = 1.0;
        constraintsLastStretch.anchor    = GridBagConstraints.WEST;
        constraintsLastStretch.insets    = constraints.insets;

        GridBagConstraints splashPanelConstraints = new GridBagConstraints();
        splashPanelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        splashPanelConstraints.fill      = GridBagConstraints.BOTH;
        splashPanelConstraints.weightx   = 1.0;
        splashPanelConstraints.weighty   = 0.02;
        splashPanelConstraints.anchor    = GridBagConstraints.NORTHWEST;
        //splashPanelConstraints.insets    = constraints.insets;

        GridBagConstraints welcomePaneConstraints = new GridBagConstraints();
        welcomePaneConstraints.gridwidth = GridBagConstraints.REMAINDER;
        welcomePaneConstraints.fill      = GridBagConstraints.NONE;
        welcomePaneConstraints.weightx   = 1.0;
        welcomePaneConstraints.weighty   = 0.01;
        welcomePaneConstraints.anchor    = GridBagConstraints.CENTER;//NORTHWEST;
        welcomePaneConstraints.insets    = new Insets(20, 40, 20, 40);

        GridBagConstraints panelConstraints = new GridBagConstraints();
        panelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        panelConstraints.fill      = GridBagConstraints.HORIZONTAL;
        panelConstraints.weightx   = 1.0;
        panelConstraints.anchor    = GridBagConstraints.NORTHWEST;
        panelConstraints.insets    = constraints.insets;

        GridBagConstraints stretchPanelConstraints = new GridBagConstraints();
        stretchPanelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        stretchPanelConstraints.fill      = GridBagConstraints.BOTH;
        stretchPanelConstraints.weightx   = 1.0;
        stretchPanelConstraints.weighty   = 1.0;
        stretchPanelConstraints.anchor    = GridBagConstraints.NORTHWEST;
        stretchPanelConstraints.insets    = constraints.insets;

        GridBagConstraints containerConstraints = new GridBagConstraints();
        containerConstraints.gridwidth = GridBagConstraints.REMAINDER;
        containerConstraints.fill      = GridBagConstraints.BOTH;
        containerConstraints.weightx   = 1.0;
        containerConstraints.weighty   = 1.0;
        containerConstraints.anchor    = GridBagConstraints.NORTHWEST;

        GridBagConstraints glueConstraints = new GridBagConstraints();
        glueConstraints.fill    = GridBagConstraints.BOTH;
        glueConstraints.weightx = 0.01;
        glueConstraints.weighty = 0.01;
        glueConstraints.anchor  = GridBagConstraints.NORTHWEST;
        glueConstraints.insets  = constraints.insets;

        GridBagConstraints bottomButtonConstraints = new GridBagConstraints();
        bottomButtonConstraints.anchor = GridBagConstraints.SOUTHEAST;
        bottomButtonConstraints.insets = new Insets(2, 2, 4, 6);
        bottomButtonConstraints.ipadx  = 10;
        bottomButtonConstraints.ipady  = 2;

        GridBagConstraints lastBottomButtonConstraints = new GridBagConstraints();
        lastBottomButtonConstraints.gridwidth = GridBagConstraints.REMAINDER;
        lastBottomButtonConstraints.anchor    = GridBagConstraints.SOUTHEAST;
        lastBottomButtonConstraints.insets    = bottomButtonConstraints.insets;
        lastBottomButtonConstraints.ipadx     = bottomButtonConstraints.ipadx;
        lastBottomButtonConstraints.ipady     = bottomButtonConstraints.ipady;

        // Leave room for a growBox on Mac OS X.
        if (System.getProperty("os.name").toLowerCase().startsWith("mac os x"))
        {
            lastBottomButtonConstraints.insets = new Insets(2, 2, 4, 6 + 16);
        }

        GridBagLayout layout = new GridBagLayout();

        configurationChooser.addChoosableFileFilter(
            new ExtensionFileFilter(msg("proExtension"), new String[] { ".pro" }));

        // Create the opening panel.
        Sprite splash =
            new CompositeSprite(new Sprite[]
        {
            new ColorSprite(new ConstantColor(Color.gray),
            new FontSprite(new ConstantFont(new Font("sansserif", Font.BOLD, 90)),
            new TextSprite(new ConstantString("ProGuard"),
                           new ConstantInt(160),
                           new LinearInt(-10, 120, new SmoothTiming(500, 1000))))),

            new ColorSprite(new ConstantColor(Color.white),
            new FontSprite(new ConstantFont(new Font("sansserif", Font.BOLD, 45)),
            new ShadowedSprite(new ConstantInt(3),
                               new ConstantInt(3),
                               new ConstantDouble(0.4),
                               new ConstantInt(1),
                               new CompositeSprite(new Sprite[]
            {
                new TextSprite(new ConstantString(msg("shrinking")),
                               new LinearInt(1000, 60, new SmoothTiming(1000, 2000)),
                               new ConstantInt(70)),
                new TextSprite(new ConstantString(msg("optimization")),
                               new LinearInt(1000, 400, new SmoothTiming(1500, 2500)),
                               new ConstantInt(60)),
                new TextSprite(new ConstantString(msg("obfuscation")),
                               new LinearInt(1000, 10, new SmoothTiming(2000, 3000)),
                               new ConstantInt(145)),
                new TextSprite(new ConstantString(msg("preverification")),
                               new LinearInt(1000, 350, new SmoothTiming(2500, 3500)),
                               new ConstantInt(140)),
                new FontSprite(new ConstantFont(new Font("sansserif", Font.BOLD, 30)),
                new TextSprite(new TypeWriterString(msg("developed"), new LinearTiming(3500, 5500)),
                               new ConstantInt(250),
                               new ConstantInt(200))),
            })))),
        });
        splashPanel = new SplashPanel(splash, 0.5, 5500L);
        splashPanel.setPreferredSize(new Dimension(0, 200));

        JEditorPane welcomePane = new JEditorPane("text/html", msg("proGuardInfo"));
        welcomePane.setPreferredSize(new Dimension(640, 350));
        // The constant HONOR_DISPLAY_PROPERTIES isn't present yet in JDK 1.4.
        //welcomePane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
        welcomePane.putClientProperty("JEditorPane.honorDisplayProperties", Boolean.TRUE);
        welcomePane.setOpaque(false);
        welcomePane.setEditable(false);
        welcomePane.setBorder(new EmptyBorder(20, 20, 20, 20));
        addBorder(welcomePane, "welcome");

        JPanel proGuardPanel = new JPanel(layout);
        proGuardPanel.add(splashPanel,      splashPanelConstraints);
        proGuardPanel.add(welcomePane,  welcomePaneConstraints);

        // Create the input panel.
        // TODO: properly clone the ClassPath objects.
        // This is awkward to implement in the generic ListPanel.addElements(...)
        // method, since the Object.clone() method is not public.
        programPanel.addCopyToPanelButton("moveToLibraries", "moveToLibrariesTip", libraryPanel);
        libraryPanel.addCopyToPanelButton("moveToProgram",   "moveToProgramTip",   programPanel);

        // Collect all buttons of these panels and make sure they are equally
        // sized.
        List panelButtons = new ArrayList();
        panelButtons.addAll(programPanel.getButtons());
        panelButtons.addAll(libraryPanel.getButtons());
        setCommonPreferredSize(panelButtons);

        addBorder(programPanel, "programJars" );
        addBorder(libraryPanel, "libraryJars" );

        JPanel inputOutputPanel = new JPanel(layout);
        inputOutputPanel.add(tip(programPanel, "programJarsTip"), stretchPanelConstraints);
        inputOutputPanel.add(tip(libraryPanel, "libraryJarsTip"), stretchPanelConstraints);

        // Load the boiler plate options.
        loadBoilerplateConfiguration();

        // Create the boiler plate keep panels.
        boilerplateKeepCheckBoxes = new JCheckBox[boilerplateKeep.length];
        boilerplateKeepTextFields = new JTextField[boilerplateKeep.length];

        JButton printUsageBrowseButton = createBrowseButton(printUsageTextField,
                                                            msg("selectUsageFile"));

        JPanel shrinkingOptionsPanel = new JPanel(layout);
        addBorder(shrinkingOptionsPanel, "options");

        shrinkingOptionsPanel.add(tip(shrinkCheckBox,         "shrinkTip"),       constraintsLastStretch);
        shrinkingOptionsPanel.add(tip(printUsageCheckBox,     "printUsageTip"),   constraints);
        shrinkingOptionsPanel.add(tip(printUsageTextField,    "outputFileTip"),   constraintsStretch);
        shrinkingOptionsPanel.add(tip(printUsageBrowseButton, "selectUsageFile"), constraintsLast);

        JPanel shrinkingPanel = new JPanel(layout);

        shrinkingPanel.add(shrinkingOptionsPanel, panelConstraints);
        addClassSpecifications(extractClassSpecifications(boilerplateKeep),
                               boilerplateKeepCheckBoxes,
                               boilerplateKeepTextFields,
                               shrinkingPanel);

        addBorder(additionalKeepPanel, "keepAdditional");
        shrinkingPanel.add(tip(additionalKeepPanel, "keepAdditionalTip"), stretchPanelConstraints);

        // Create the boiler plate keep names panels.
        boilerplateKeepNamesCheckBoxes = new JCheckBox[boilerplateKeepNames.length];
        boilerplateKeepNamesTextFields = new JTextField[boilerplateKeepNames.length];

        JButton printMappingBrowseButton                = createBrowseButton(printMappingTextField,
                                                                             msg("selectPrintMappingFile"));
        JButton applyMappingBrowseButton                = createBrowseButton(applyMappingTextField,
                                                                             msg("selectApplyMappingFile"));
        JButton obfucationDictionaryBrowseButton        = createBrowseButton(obfuscationDictionaryTextField,
                                                                             msg("selectObfuscationDictionaryFile"));
        JButton classObfucationDictionaryBrowseButton   = createBrowseButton(classObfuscationDictionaryTextField,
                                                                             msg("selectObfuscationDictionaryFile"));
        JButton packageObfucationDictionaryBrowseButton = createBrowseButton(packageObfuscationDictionaryTextField,
                                                                             msg("selectObfuscationDictionaryFile"));

        JPanel obfuscationOptionsPanel = new JPanel(layout);
        addBorder(obfuscationOptionsPanel, "options");

        obfuscationOptionsPanel.add(tip(obfuscateCheckBox,                       "obfuscateTip"),                     constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(printMappingCheckBox,                    "printMappingTip"),                  constraints);
        obfuscationOptionsPanel.add(tip(printMappingTextField,                   "outputFileTip"),                    constraintsStretch);
        obfuscationOptionsPanel.add(tip(printMappingBrowseButton,                "selectPrintMappingFile"),           constraintsLast);
        obfuscationOptionsPanel.add(tip(applyMappingCheckBox,                    "applyMappingTip"),                  constraints);
        obfuscationOptionsPanel.add(tip(applyMappingTextField,                   "inputFileTip"),                     constraintsStretch);
        obfuscationOptionsPanel.add(tip(applyMappingBrowseButton,                "selectApplyMappingFile"),           constraintsLast);
        obfuscationOptionsPanel.add(tip(obfuscationDictionaryCheckBox,           "obfuscationDictionaryTip"),         constraints);
        obfuscationOptionsPanel.add(tip(obfuscationDictionaryTextField,          "inputFileTip"),                     constraintsStretch);
        obfuscationOptionsPanel.add(tip(obfucationDictionaryBrowseButton,        "selectObfuscationDictionaryFile"),  constraintsLast);
        obfuscationOptionsPanel.add(tip(classObfuscationDictionaryCheckBox,      "classObfuscationDictionaryTip"),    constraints);
        obfuscationOptionsPanel.add(tip(classObfuscationDictionaryTextField,     "inputFileTip"),                     constraintsStretch);
        obfuscationOptionsPanel.add(tip(classObfucationDictionaryBrowseButton,   "selectObfuscationDictionaryFile"),  constraintsLast);
        obfuscationOptionsPanel.add(tip(packageObfuscationDictionaryCheckBox,    "packageObfuscationDictionaryTip"),  constraints);
        obfuscationOptionsPanel.add(tip(packageObfuscationDictionaryTextField,   "inputFileTip"),                     constraintsStretch);
        obfuscationOptionsPanel.add(tip(packageObfucationDictionaryBrowseButton, "selectObfuscationDictionaryFile"),  constraintsLast);
        obfuscationOptionsPanel.add(tip(overloadAggressivelyCheckBox,            "overloadAggressivelyTip"),          constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(useUniqueClassMemberNamesCheckBox,       "useUniqueClassMemberNamesTip"),     constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(useMixedCaseClassNamesCheckBox,          "useMixedCaseClassNamesTip"),        constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(keepPackageNamesCheckBox,                "keepPackageNamesTip"),              constraints);
        obfuscationOptionsPanel.add(tip(keepPackageNamesTextField,               "packageNamesTip"),                  constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(flattenPackageHierarchyCheckBox,         "flattenPackageHierarchyTip"),       constraints);
        obfuscationOptionsPanel.add(tip(flattenPackageHierarchyTextField,        "packageTip"),                       constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(repackageClassesCheckBox,                "repackageClassesTip"),              constraints);
        obfuscationOptionsPanel.add(tip(repackageClassesTextField,               "packageTip"),                       constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(keepAttributesCheckBox,                  "keepAttributesTip"),                constraints);
        obfuscationOptionsPanel.add(tip(keepAttributesTextField,                 "attributesTip"),                    constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(keepParameterNamesCheckBox,              "keepParameterNamesTip"),            constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(newSourceFileAttributeCheckBox,          "renameSourceFileAttributeTip"),     constraints);
        obfuscationOptionsPanel.add(tip(newSourceFileAttributeTextField,         "sourceFileAttributeTip"),           constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(adaptClassStringsCheckBox,               "adaptClassStringsTip"),             constraints);
        obfuscationOptionsPanel.add(tip(adaptClassStringsTextField,              "classNamesTip"),                    constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(adaptResourceFileNamesCheckBox,          "adaptResourceFileNamesTip"),        constraints);
        obfuscationOptionsPanel.add(tip(adaptResourceFileNamesTextField,         "fileNameFilterTip"),                constraintsLastStretch);
        obfuscationOptionsPanel.add(tip(adaptResourceFileContentsCheckBox,       "adaptResourceFileContentsTip"),     constraints);
        obfuscationOptionsPanel.add(tip(adaptResourceFileContentsTextField,      "fileNameFilterTip"),                constraintsLastStretch);

        JPanel obfuscationPanel = new JPanel(layout);

        obfuscationPanel.add(obfuscationOptionsPanel, panelConstraints);
        addClassSpecifications(extractClassSpecifications(boilerplateKeepNames),
                               boilerplateKeepNamesCheckBoxes,
                               boilerplateKeepNamesTextFields,
                               obfuscationPanel);

        addBorder(additionalKeepNamesPanel, "keepNamesAdditional");
        obfuscationPanel.add(tip(additionalKeepNamesPanel, "keepNamesAdditionalTip"), stretchPanelConstraints);

        // Create the boiler plate "no side effect methods" panels.
        boilerplateNoSideEffectMethodCheckBoxes = new JCheckBox[boilerplateNoSideEffectMethods.length];

        JPanel optimizationOptionsPanel = new JPanel(layout);
        addBorder(optimizationOptionsPanel, "options");

        JButton optimizationsButton =
            createOptimizationsButton(optimizationsTextField);

        optimizationOptionsPanel.add(tip(optimizeCheckBox,                    "optimizeTip"),                    constraintsLastStretch);
        optimizationOptionsPanel.add(tip(allowAccessModificationCheckBox,     "allowAccessModificationTip"),     constraintsLastStretch);
        optimizationOptionsPanel.add(tip(mergeInterfacesAggressivelyCheckBox, "mergeInterfacesAggressivelyTip"), constraintsLastStretch);
        optimizationOptionsPanel.add(tip(optimizationsLabel,                  "optimizationsTip"),               constraints);
        optimizationOptionsPanel.add(tip(optimizationsTextField,              "optimizationsFilterTip"),         constraintsStretch);
        optimizationOptionsPanel.add(tip(optimizationsButton,                 "optimizationsSelectTip"),         constraintsLast);
        optimizationOptionsPanel.add(tip(optimizationPassesLabel,             "optimizationPassesTip"),          constraints);
        optimizationOptionsPanel.add(tip(optimizationPassesSpinner,           "optimizationPassesTip"),          constraintsLast);

        JPanel optimizationPanel = new JPanel(layout);

        optimizationPanel.add(optimizationOptionsPanel, panelConstraints);
        addClassSpecifications(boilerplateNoSideEffectMethods,
                               boilerplateNoSideEffectMethodCheckBoxes,
                               null,
                               optimizationPanel);

        addBorder(additionalNoSideEffectsPanel, "assumeNoSideEffectsAdditional");
        optimizationPanel.add(tip(additionalNoSideEffectsPanel, "assumeNoSideEffectsAdditionalTip"), stretchPanelConstraints);

        // Create the options panel.
        JPanel preverificationOptionsPanel = new JPanel(layout);
        addBorder(preverificationOptionsPanel, "preverificationAndTargeting");

        preverificationOptionsPanel.add(tip(preverifyCheckBox,    "preverifyTip"),    constraintsLastStretch);
        preverificationOptionsPanel.add(tip(microEditionCheckBox, "microEditionTip"), constraintsLastStretch);
        preverificationOptionsPanel.add(tip(androidCheckBox,      "androidTip"),      constraintsLastStretch);
        preverificationOptionsPanel.add(tip(targetCheckBox,       "targetTip"),       constraints);
        preverificationOptionsPanel.add(tip(targetComboBox,       "targetTip"),       constraintsLast);

        JButton printSeedsBrowseButton =
            createBrowseButton(printSeedsTextField, msg("selectSeedsFile"));

        JButton printConfigurationBrowseButton =
            createBrowseButton(printConfigurationTextField, msg( "selectConfigurationFile"));

        JButton dumpBrowseButton =
            createBrowseButton(dumpTextField, msg("selectDumpFile"));

        // Select the most recent target by default.
        targetComboBox.setSelectedIndex(targetComboBox.getItemCount() - 1);

        JPanel consistencyPanel = new JPanel(layout);
        addBorder(consistencyPanel, "consistencyAndCorrectness");

        consistencyPanel.add(tip(verboseCheckBox,                          "verboseTip"),                          constraintsLastStretch);
        consistencyPanel.add(tip(noteCheckBox,                             "noteTip"),                             constraints);
        consistencyPanel.add(tip(noteTextField,                            "noteFilterTip"),                             constraintsLastStretch);
        consistencyPanel.add(tip(warnCheckBox,                             "warnTip"),                             constraints);
        consistencyPanel.add(tip(warnTextField,                            "warnFilterTip"),                             constraintsLastStretch);
        consistencyPanel.add(tip(ignoreWarningsCheckBox,                   "ignoreWarningsTip"),                   constraintsLastStretch);
        consistencyPanel.add(tip(skipNonPublicLibraryClassesCheckBox,      "skipNonPublicLibraryClassesTip"),      constraintsLastStretch);
        consistencyPanel.add(tip(skipNonPublicLibraryClassMembersCheckBox, "skipNonPublicLibraryClassMembersTip"), constraintsLastStretch);
        consistencyPanel.add(tip(keepDirectoriesCheckBox,                  "keepDirectoriesTip"),                  constraints);
        consistencyPanel.add(tip(keepDirectoriesTextField,                 "directoriesTip"),                      constraintsLastStretch);
        consistencyPanel.add(tip(forceProcessingCheckBox,                  "forceProcessingTip"),                  constraintsLastStretch);
        consistencyPanel.add(tip(printSeedsCheckBox,                       "printSeedsTip"),                       constraints);
        consistencyPanel.add(tip(printSeedsTextField,                      "outputFileTip"),                       constraintsStretch);
        consistencyPanel.add(tip(printSeedsBrowseButton,                   "selectSeedsFile"),                     constraintsLast);
        consistencyPanel.add(tip(printConfigurationCheckBox,               "printConfigurationTip"),               constraints);
        consistencyPanel.add(tip(printConfigurationTextField,              "outputFileTip"),                       constraintsStretch);
        consistencyPanel.add(tip(printConfigurationBrowseButton,           "selectConfigurationFile"),             constraintsLast);
        consistencyPanel.add(tip(dumpCheckBox,                             "dumpTip"),                             constraints);
        consistencyPanel.add(tip(dumpTextField,                            "outputFileTip"),                       constraintsStretch);
        consistencyPanel.add(tip(dumpBrowseButton,                         "selectDumpFile"),                      constraintsLast);

        // Collect all components that are followed by text fields and make
        // sure they are equally sized. That way the text fields start at the
        // same horizontal position.
        setCommonPreferredSize(Arrays.asList(new JComponent[] {
            printMappingCheckBox,
            applyMappingCheckBox,
            flattenPackageHierarchyCheckBox,
            repackageClassesCheckBox,
            newSourceFileAttributeCheckBox,
        }));

        JPanel optionsPanel = new JPanel(layout);

        optionsPanel.add(preverificationOptionsPanel, panelConstraints);
        optionsPanel.add(consistencyPanel,            panelConstraints);

        addBorder(whyAreYouKeepingPanel, "whyAreYouKeeping");
        optionsPanel.add(tip(whyAreYouKeepingPanel, "whyAreYouKeepingTip"), stretchPanelConstraints);

        // Create the process panel.
        consoleTextArea.setOpaque(false);
        consoleTextArea.setEditable(false);
        consoleTextArea.setLineWrap(false);
        consoleTextArea.setWrapStyleWord(false);
        JScrollPane consoleScrollPane = new JScrollPane(consoleTextArea);
        consoleScrollPane.setBorder(new EmptyBorder(1, 1, 1, 1));
        addBorder(consoleScrollPane, "processingConsole");

        JPanel processPanel = new JPanel(layout);
        processPanel.add(consoleScrollPane, stretchPanelConstraints);

        // Create the load, save, and process buttons.
        JButton loadButton = new JButton(msg("loadConfiguration"));
        loadButton.addActionListener(new MyLoadConfigurationActionListener());

        JButton viewButton = new JButton(msg("viewConfiguration"));
        viewButton.addActionListener(new MyViewConfigurationActionListener());

        JButton saveButton = new JButton(msg("saveConfiguration"));
        saveButton.addActionListener(new MySaveConfigurationActionListener());

        JButton processButton = new JButton(msg("process"));
        processButton.addActionListener(new MyProcessActionListener());

        // Create the ReTrace panel.
        JPanel reTraceSettingsPanel = new JPanel(layout);
        addBorder(reTraceSettingsPanel, "reTraceSettings");

        JButton reTraceMappingBrowseButton = createBrowseButton(reTraceMappingTextField,
                                                                msg("selectApplyMappingFile"));

        JLabel reTraceMappingLabel = new JLabel(msg("mappingFile"));
        reTraceMappingLabel.setForeground(reTraceVerboseCheckBox.getForeground());

        reTraceSettingsPanel.add(tip(reTraceVerboseCheckBox,     "verboseTip"),             constraintsLastStretch);
        reTraceSettingsPanel.add(tip(reTraceMappingLabel,        "mappingFileTip"),         constraints);
        reTraceSettingsPanel.add(tip(reTraceMappingTextField,    "inputFileTip"),           constraintsStretch);
        reTraceSettingsPanel.add(tip(reTraceMappingBrowseButton, "selectApplyMappingFile"), constraintsLast);

        stackTraceTextArea.setOpaque(true);
        stackTraceTextArea.setEditable(true);
        stackTraceTextArea.setLineWrap(false);
        stackTraceTextArea.setWrapStyleWord(true);
        JScrollPane stackTraceScrollPane = new JScrollPane(stackTraceTextArea);
        addBorder(stackTraceScrollPane, "obfuscatedStackTrace");

        reTraceTextArea.setOpaque(false);
        reTraceTextArea.setEditable(false);
        reTraceTextArea.setLineWrap(true);
        reTraceTextArea.setWrapStyleWord(true);
        JScrollPane reTraceScrollPane = new JScrollPane(reTraceTextArea);
        reTraceScrollPane.setBorder(new EmptyBorder(1, 1, 1, 1));
        addBorder(reTraceScrollPane, "deobfuscatedStackTrace");

        JPanel reTracePanel = new JPanel(layout);
        reTracePanel.add(reTraceSettingsPanel,                                 panelConstraints);
        reTracePanel.add(tip(stackTraceScrollPane, "obfuscatedStackTraceTip"), panelConstraints);
        reTracePanel.add(reTraceScrollPane,                                    stretchPanelConstraints);

        // Create the load button.
        JButton loadStackTraceButton = new JButton(msg("loadStackTrace"));
        loadStackTraceButton.addActionListener(new MyLoadStackTraceActionListener());

        JButton reTraceButton = new JButton(msg("reTrace"));
        reTraceButton.addActionListener(new MyReTraceActionListener());

        // Make the shrinking panel so far scrollable.
        JPanel scrollableShrinkingPanel = shrinkingPanel;
        shrinkingPanel = new JPanel(layout);
        shrinkingPanel.add(new JScrollPane(scrollableShrinkingPanel), containerConstraints);

        // Make the obfuscation panel so far scrollable.
        JPanel scrollableObfuscationPanel = obfuscationPanel;
        obfuscationPanel = new JPanel(layout);
        obfuscationPanel.add(new JScrollPane(scrollableObfuscationPanel), containerConstraints);

        // Make the optimization panel so far scrollable.
        JPanel scrollableOptimizationPanel = optimizationPanel;
        optimizationPanel = new JPanel(layout);
        optimizationPanel.add(new JScrollPane(scrollableOptimizationPanel), containerConstraints);

        // Create the main tabbed pane.
        TabbedPane tabs = new TabbedPane();
        tabs.add(msg("proGuardTab"),     proGuardPanel);
        tabs.add(msg("inputOutputTab"),  inputOutputPanel);
        tabs.add(msg("shrinkingTab"),    shrinkingPanel);
        tabs.add(msg("obfuscationTab"),  obfuscationPanel);
        tabs.add(msg("optimizationTab"), optimizationPanel);
        tabs.add(msg("informationTab"),  optionsPanel);
        tabs.add(msg("processTab"),      processPanel);
        tabs.add(msg("reTraceTab"),      reTracePanel);
        tabs.addImage(Toolkit.getDefaultToolkit().getImage(
            this.getClass().getResource(TITLE_IMAGE_FILE)));

        // Add the bottom buttons to each panel.
        proGuardPanel    .add(Box.createGlue(),                                    glueConstraints);
        proGuardPanel    .add(tip(loadButton,             "loadConfigurationTip"), bottomButtonConstraints);
        proGuardPanel    .add(createNextButton(tabs),                              lastBottomButtonConstraints);

        inputOutputPanel .add(Box.createGlue(),           glueConstraints);
        inputOutputPanel .add(createPreviousButton(tabs), bottomButtonConstraints);
        inputOutputPanel .add(createNextButton(tabs),     lastBottomButtonConstraints);

        shrinkingPanel   .add(Box.createGlue(),           glueConstraints);
        shrinkingPanel   .add(createPreviousButton(tabs), bottomButtonConstraints);
        shrinkingPanel   .add(createNextButton(tabs),     lastBottomButtonConstraints);

        obfuscationPanel .add(Box.createGlue(),           glueConstraints);
        obfuscationPanel .add(createPreviousButton(tabs), bottomButtonConstraints);
        obfuscationPanel .add(createNextButton(tabs),     lastBottomButtonConstraints);

        optimizationPanel.add(Box.createGlue(),           glueConstraints);
        optimizationPanel.add(createPreviousButton(tabs), bottomButtonConstraints);
        optimizationPanel.add(createNextButton(tabs),     lastBottomButtonConstraints);

        optionsPanel     .add(Box.createGlue(),           glueConstraints);
        optionsPanel     .add(createPreviousButton(tabs), bottomButtonConstraints);
        optionsPanel     .add(createNextButton(tabs),     lastBottomButtonConstraints);

        processPanel     .add(Box.createGlue(),                                    glueConstraints);
        processPanel     .add(createPreviousButton(tabs),                          bottomButtonConstraints);
        processPanel     .add(tip(viewButton,             "viewConfigurationTip"), bottomButtonConstraints);
        processPanel     .add(tip(saveButton,             "saveConfigurationTip"), bottomButtonConstraints);
        processPanel     .add(tip(processButton,          "processTip"),           lastBottomButtonConstraints);

        reTracePanel     .add(Box.createGlue(),                                    glueConstraints);
        reTracePanel     .add(tip(loadStackTraceButton,   "loadStackTraceTip"),    bottomButtonConstraints);
        reTracePanel     .add(tip(reTraceButton,          "reTraceTip"),           lastBottomButtonConstraints);

        // Add the main tabs to the frame.
        getContentPane().add(tabs);

        // Pack the entire GUI before setting some default values.
        pack();

        // Initialize the GUI settings to reasonable defaults.
        loadConfiguration(this.getClass().getResource(DEFAULT_CONFIGURATION));
    }


    public void startSplash()
    {
        splashPanel.start();
    }


    public void skipSplash()
    {
        splashPanel.stop();
    }


    /**
     * Loads the boilerplate keep class options from the boilerplate file
     * into the boilerplate array.
     */
    private void loadBoilerplateConfiguration()
    {
        try
        {
            // Parse the boilerplate configuration file.
            ConfigurationParser parser = new ConfigurationParser(
                this.getClass().getResource(BOILERPLATE_CONFIGURATION),
                System.getProperties());

            Configuration configuration = new Configuration();

            try
            {
                parser.parse(configuration);

                // We're interested in the keep options.
                boilerplateKeep =
                    extractKeepSpecifications(configuration.keep, false, false);

                // We're interested in the keep options.
                boilerplateKeepNames =
                    extractKeepSpecifications(configuration.keep, true, false);

                // We're interested in the side effects options.
                boilerplateNoSideEffectMethods = new ClassSpecification[configuration.assumeNoSideEffects.size()];
                configuration.assumeNoSideEffects.toArray(boilerplateNoSideEffectMethods);
            }
            finally
            {
                parser.close();
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }


    /**
     * Returns an array containing the ClassSpecifications instances with
     * matching flags.
     */
    private KeepClassSpecification[] extractKeepSpecifications(List    keepSpecifications,
                                                               boolean allowShrinking,
                                                               boolean allowObfuscation)
    {
        List matches = new ArrayList();

        for (int index = 0; index < keepSpecifications.size(); index++)
        {
            KeepClassSpecification keepClassSpecification = (KeepClassSpecification)keepSpecifications.get(index);
            if (keepClassSpecification.allowShrinking   == allowShrinking &&
                keepClassSpecification.allowObfuscation == allowObfuscation)
            {
                 matches.add(keepClassSpecification);
            }
        }

        KeepClassSpecification[] matchingKeepClassSpecifications = new KeepClassSpecification[matches.size()];
        matches.toArray(matchingKeepClassSpecifications);

        return matchingKeepClassSpecifications;
    }


    /**
     * Returns an array containing the ClassSpecification instances of the
     * given array of KeepClassSpecification instances.
     */
    private ClassSpecification[] extractClassSpecifications(KeepClassSpecification[] keepClassSpecifications)
    {
        ClassSpecification[] classSpecifications = new ClassSpecification[keepClassSpecifications.length];

        for (int index = 0; index < classSpecifications.length; index++)
        {
            classSpecifications[index] = keepClassSpecifications[index];
        }

        return classSpecifications;
    }


    /**
     * Creates a panel with the given boiler plate class specifications.
     */
    private void addClassSpecifications(ClassSpecification[] boilerplateClassSpecifications,
                                        JCheckBox[]          boilerplateCheckBoxes,
                                        JTextField[]         boilerplateTextFields,
                                        JPanel               classSpecificationsPanel)
    {
        // Create some constraints that can be reused.
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;
        constraints.insets = new Insets(0, 4, 0, 4);

        GridBagConstraints constraintsLastStretch = new GridBagConstraints();
        constraintsLastStretch.gridwidth = GridBagConstraints.REMAINDER;
        constraintsLastStretch.fill      = GridBagConstraints.HORIZONTAL;
        constraintsLastStretch.weightx   = 1.0;
        constraintsLastStretch.anchor    = GridBagConstraints.WEST;
        constraintsLastStretch.insets    = constraints.insets;

        GridBagConstraints panelConstraints = new GridBagConstraints();
        panelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        panelConstraints.fill      = GridBagConstraints.HORIZONTAL;
        panelConstraints.weightx   = 1.0;
        panelConstraints.anchor    = GridBagConstraints.NORTHWEST;
        panelConstraints.insets    = constraints.insets;

        GridBagConstraints containerConstraints = new GridBagConstraints();
        containerConstraints.gridwidth = GridBagConstraints.REMAINDER;
        containerConstraints.fill      = GridBagConstraints.BOTH;
        containerConstraints.weightx   = 1.0;
        containerConstraints.weighty   = 1.0;
        containerConstraints.anchor    = GridBagConstraints.NORTHWEST;

        GridBagLayout layout = new GridBagLayout();

        JPanel panel = new JPanel(layout);

        String lastPanelName = null;
        JPanel subpanel  = null;
        for (int index = 0; index < boilerplateClassSpecifications.length; index++)
        {
            // The panel structure is derived from the comments.
            String comments = boilerplateClassSpecifications[index].comments;

            // Do we have a comment, for a new set of options?
            if (comments != null)
            {
                // Create a new line in the panel.
                int    dashIndex   = comments.indexOf('-');
                int    periodIndex = comments.indexOf('.', dashIndex);
                String panelName   = comments.substring(0, dashIndex).trim();
                String optionName  = comments.substring(dashIndex + 1, periodIndex).replace('_', '.').trim();
                String toolTip     = comments.substring(periodIndex + 1);
                if (!panelName.equals(lastPanelName))
                {
                    // Create a new subpanel and add it.
                    subpanel = new JPanel(layout);
                    if (panelName.endsWith(ELLIPSIS_DOTS))
                    {
                        subpanel.setVisible(false);
                    }

                    // Make the contents foldable.
                    final JPanel       foldableSubpanel = new JPanel(layout);
                    final TitledBorder titledBorder         = BorderFactory.createTitledBorder(BORDER, panelName);
                    foldableSubpanel.setBorder(titledBorder);
                    foldableSubpanel.add(subpanel, containerConstraints);

                    // Fold and unfold the contents when the foldable panel
                    // is clicked.
                    final JPanel finalSubpanel = subpanel;
                    foldableSubpanel.addMouseListener(new MouseAdapter()
                    {
                        public void mouseClicked(MouseEvent mouseEvent)
                        {
                            // Toggle the visibility and the title.
                            String title = titledBorder.getTitle();
                            if (finalSubpanel.isVisible())
                            {
                                finalSubpanel.setVisible(false);
                                titledBorder.setTitle(title + ELLIPSIS_DOTS);
                            }
                            else
                            {
                                finalSubpanel.setVisible(true);
                                titledBorder.setTitle(title.substring(0, title.length()-3));
                            }
                        }
                    });

                    classSpecificationsPanel.add(foldableSubpanel, panelConstraints);

                    lastPanelName = panelName;
                }

                // Should we add a text field for the class names?
                boolean addTextField =
                    boilerplateTextFields != null                              &&
                    (index+1 == boilerplateClassSpecifications.length ||
                     boilerplateClassSpecifications[index+1].comments != null) &&
                    boilerplateClassSpecifications[index].className == null;
;
                // Add the check box to the subpanel.
                JCheckBox boilerplateCheckBox = new JCheckBox(optionName);
                boilerplateCheckBox.setToolTipText(toolTip);
                subpanel.add(boilerplateCheckBox,
                             addTextField ?
                                 constraints :
                                 constraintsLastStretch);

                boilerplateCheckBoxes[index] = boilerplateCheckBox;

                // Add the text field to the subpanel, if relevant.
                if (addTextField)
                {
                    JTextField boilerplateTextField = new JTextField(40);
                    subpanel.add(tip(boilerplateTextField, "classNamesTip"), constraintsLastStretch);

                    boilerplateTextFields[index] = boilerplateTextField;
                }
            }
            else
            {
                // Reuse the previous line from the panel, notably the check
                // box.
                boilerplateCheckBoxes[index] = boilerplateCheckBoxes[index-1];
            }
        }
    }


    /**
     * Adds a standard border with the title that corresponds to the given key
     * in the GUI resources.
     */
    private void addBorder(JComponent component, String titleKey)
    {
        Border oldBorder = component.getBorder();
        Border newBorder = BorderFactory.createTitledBorder(BORDER, msg(titleKey));

        component.setBorder(oldBorder == null ?
            newBorder :
            new CompoundBorder(newBorder, oldBorder));
    }


    /**
     * Creates a Previous button for the given tabbed pane.
     */
    private JButton createPreviousButton(final TabbedPane tabbedPane)
    {
        JButton browseButton = new JButton(msg("previous"));
        browseButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                tabbedPane.previous();
            }
        });

        return browseButton;
    }


    /**
     * Creates a Next button for the given tabbed pane.
     */
    private JButton createNextButton(final TabbedPane tabbedPane)
    {
        JButton browseButton = new JButton(msg("next"));
        browseButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                tabbedPane.next();
            }
        });

        return browseButton;
    }


    /**
     * Creates a browse button that opens a file browser for the given text field.
     */
    private JButton createBrowseButton(final JTextField textField,
                                       final String     title)
    {
        JButton browseButton = new JButton(msg("browse"));
        browseButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                // Update the file chooser.
                fileChooser.setDialogTitle(title);
                fileChooser.setSelectedFile(new File(textField.getText()));

                int returnVal = fileChooser.showDialog(ProGuardGUI.this, msg("ok"));
                if (returnVal == JFileChooser.APPROVE_OPTION)
                {
                    // Update the text field.
                    textField.setText(fileChooser.getSelectedFile().getPath());
                }
            }
        });

        return browseButton;
    }


    protected JButton createOptimizationsButton(final JTextField textField)
    {
        final OptimizationsDialog optimizationsDialog = new OptimizationsDialog(ProGuardGUI.this);

        JButton optimizationsButton = new JButton(msg("select"));
        optimizationsButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                // Update the dialog.
                optimizationsDialog.setFilter(textField.getText());

                int returnValue = optimizationsDialog.showDialog();
                if (returnValue == OptimizationsDialog.APPROVE_OPTION)
                {
                    // Update the text field.
                    textField.setText(optimizationsDialog.getFilter());
                }
            }
        });

        return optimizationsButton;
    }


    /**
     * Sets the preferred sizes of the given components to the maximum of their
     * current preferred sizes.
     */
    private void setCommonPreferredSize(List components)
    {
        // Find the maximum preferred size.
        Dimension maximumSize = null;
        for (int index = 0; index < components.size(); index++)
        {
            JComponent component = (JComponent)components.get(index);
            Dimension  size      = component.getPreferredSize();
            if (maximumSize == null ||
                size.getWidth() > maximumSize.getWidth())
            {
                maximumSize = size;
            }
        }

        // Set the size that we found as the preferred size for all components.
        for (int index = 0; index < components.size(); index++)
        {
            JComponent component = (JComponent)components.get(index);
            component.setPreferredSize(maximumSize);
        }
    }


    /**
     * Updates to GUI settings to reflect the given ProGuard configuration.
     */
    private void setProGuardConfiguration(Configuration configuration)
    {
        // Set up the input and output jars and directories.
        programPanel.setClassPath(configuration.programJars);
        libraryPanel.setClassPath(configuration.libraryJars);

        // Set up the boilerplate keep options.
        for (int index = 0; index < boilerplateKeep.length; index++)
        {
            String classNames =
                findMatchingKeepSpecifications(boilerplateKeep[index],
                                               boilerplateKeepTextFields[index] == null,
                                               configuration.keep);

            boilerplateKeepCheckBoxes[index].setSelected(classNames != null);
            if (boilerplateKeepTextFields[index] != null)
            {
                boilerplateKeepTextFields[index].setText(classNames == null ? "*" : classNames);
            }
        }


        // Set up the boilerplate keep names options.
        for (int index = 0; index < boilerplateKeepNames.length; index++)
        {
            String classNames =
                findMatchingKeepSpecifications(boilerplateKeepNames[index],
                                               boilerplateKeepNamesTextFields[index] == null,
                                               configuration.keep);

            boilerplateKeepNamesCheckBoxes[index].setSelected(classNames != null);
            if (boilerplateKeepNamesTextFields[index] != null)
            {
                boilerplateKeepNamesTextFields[index].setText(classNames == null ? "*" : classNames);
            }
        }

        // Set up the additional keep options. Note that the matched boilerplate
        // options have been removed from the list.
        additionalKeepPanel.setClassSpecifications(filteredKeepSpecifications(configuration.keep,
                                                                              false));

        // Set up the additional keep options. Note that the matched boilerplate
        // options have been removed from the list.
        additionalKeepNamesPanel.setClassSpecifications(filteredKeepSpecifications(configuration.keep,
                                                                                   true));


        // Set up the boilerplate "no side effect methods" options.
        for (int index = 0; index < boilerplateNoSideEffectMethods.length; index++)
        {
            boolean found =
                findClassSpecification(boilerplateNoSideEffectMethods[index],
                                       configuration.assumeNoSideEffects);

            boilerplateNoSideEffectMethodCheckBoxes[index].setSelected(found);
        }

        // Set up the additional keep options. Note that the matched boilerplate
        // options have been removed from the list.
        additionalNoSideEffectsPanel.setClassSpecifications(configuration.assumeNoSideEffects);

        // Set up the "why are you keeping" options.
        whyAreYouKeepingPanel.setClassSpecifications(configuration.whyAreYouKeeping);

        // Set up the other options.
        shrinkCheckBox                          .setSelected(configuration.shrink);
        printUsageCheckBox                      .setSelected(configuration.printUsage != null);

        optimizeCheckBox                        .setSelected(configuration.optimize);
        allowAccessModificationCheckBox         .setSelected(configuration.allowAccessModification);
        mergeInterfacesAggressivelyCheckBox     .setSelected(configuration.mergeInterfacesAggressively);
        optimizationPassesSpinner.getModel()    .setValue(new Integer(configuration.optimizationPasses));

        obfuscateCheckBox                       .setSelected(configuration.obfuscate);
        printMappingCheckBox                    .setSelected(configuration.printMapping                  != null);
        applyMappingCheckBox                    .setSelected(configuration.applyMapping                  != null);
        obfuscationDictionaryCheckBox           .setSelected(configuration.obfuscationDictionary         != null);
        classObfuscationDictionaryCheckBox      .setSelected(configuration.classObfuscationDictionary    != null);
        packageObfuscationDictionaryCheckBox    .setSelected(configuration.packageObfuscationDictionary  != null);
        overloadAggressivelyCheckBox            .setSelected(configuration.overloadAggressively);
        useUniqueClassMemberNamesCheckBox       .setSelected(configuration.useUniqueClassMemberNames);
        useMixedCaseClassNamesCheckBox          .setSelected(configuration.useMixedCaseClassNames);
        keepPackageNamesCheckBox                .setSelected(configuration.keepPackageNames              != null);
        flattenPackageHierarchyCheckBox         .setSelected(configuration.flattenPackageHierarchy       != null);
        repackageClassesCheckBox                .setSelected(configuration.repackageClasses              != null);
        keepAttributesCheckBox                  .setSelected(configuration.keepAttributes                != null);
        keepParameterNamesCheckBox              .setSelected(configuration.keepParameterNames);
        newSourceFileAttributeCheckBox          .setSelected(configuration.newSourceFileAttribute        != null);
        adaptClassStringsCheckBox               .setSelected(configuration.adaptClassStrings             != null);
        adaptResourceFileNamesCheckBox          .setSelected(configuration.adaptResourceFileNames        != null);
        adaptResourceFileContentsCheckBox       .setSelected(configuration.adaptResourceFileContents     != null);

        preverifyCheckBox                       .setSelected(configuration.preverify);
        microEditionCheckBox                    .setSelected(configuration.microEdition);
        androidCheckBox                         .setSelected(configuration.android);
        targetCheckBox                          .setSelected(configuration.targetClassVersion != 0);

        verboseCheckBox                         .setSelected(configuration.verbose);
        noteCheckBox                            .setSelected(configuration.note == null || !configuration.note.isEmpty());
        warnCheckBox                            .setSelected(configuration.warn == null || !configuration.warn.isEmpty());
        ignoreWarningsCheckBox                  .setSelected(configuration.ignoreWarnings);
        skipNonPublicLibraryClassesCheckBox     .setSelected(configuration.skipNonPublicLibraryClasses);
        skipNonPublicLibraryClassMembersCheckBox.setSelected(configuration.skipNonPublicLibraryClassMembers);
        keepDirectoriesCheckBox                 .setSelected(configuration.keepDirectories    != null);
        forceProcessingCheckBox                 .setSelected(configuration.lastModified == Long.MAX_VALUE);
        printSeedsCheckBox                      .setSelected(configuration.printSeeds         != null);
        printConfigurationCheckBox              .setSelected(configuration.printConfiguration != null);
        dumpCheckBox                            .setSelected(configuration.dump               != null);

        printUsageTextField                     .setText(fileName(configuration.printUsage));
        optimizationsTextField                  .setText(configuration.optimizations             == null ? OPTIMIZATIONS_DEFAULT                : ListUtil.commaSeparatedString(configuration.optimizations, true));
        printMappingTextField                   .setText(fileName(configuration.printMapping));
        applyMappingTextField                   .setText(fileName(configuration.applyMapping));
        obfuscationDictionaryTextField          .setText(fileName(configuration.obfuscationDictionary));
        classObfuscationDictionaryTextField     .setText(fileName(configuration.classObfuscationDictionary));
        packageObfuscationDictionaryTextField   .setText(fileName(configuration.packageObfuscationDictionary));
        keepPackageNamesTextField               .setText(configuration.keepPackageNames          == null ? ""                                   : ClassUtil.externalClassName(ListUtil.commaSeparatedString(configuration.keepPackageNames, true)));
        flattenPackageHierarchyTextField        .setText(configuration.flattenPackageHierarchy);
        repackageClassesTextField               .setText(configuration.repackageClasses);
        keepAttributesTextField                 .setText(configuration.keepAttributes                == null ? KEEP_ATTRIBUTE_DEFAULT                    : ListUtil.commaSeparatedString(configuration.keepAttributes, true));
        newSourceFileAttributeTextField         .setText(configuration.newSourceFileAttribute        == null ? SOURCE_FILE_ATTRIBUTE_DEFAULT             : configuration.newSourceFileAttribute);
        adaptClassStringsTextField              .setText(configuration.adaptClassStrings             == null ? ""                                        : ClassUtil.externalClassName(ListUtil.commaSeparatedString(configuration.adaptClassStrings, true)));
        adaptResourceFileNamesTextField         .setText(configuration.adaptResourceFileNames        == null ? ADAPT_RESOURCE_FILE_NAMES_DEFAULT         : ListUtil.commaSeparatedString(configuration.adaptResourceFileNames, true));
        adaptResourceFileContentsTextField      .setText(configuration.adaptResourceFileContents     == null ? ADAPT_RESOURCE_FILE_CONTENTS_DEFAULT      : ListUtil.commaSeparatedString(configuration.adaptResourceFileContents, true));
        noteTextField                           .setText(ListUtil.commaSeparatedString(configuration.note, true));
        warnTextField                           .setText(ListUtil.commaSeparatedString(configuration.warn, true));
        keepDirectoriesTextField                .setText(ListUtil.commaSeparatedString(configuration.keepDirectories, true));
        printSeedsTextField                     .setText(fileName(configuration.printSeeds));
        printConfigurationTextField             .setText(fileName(configuration.printConfiguration));
        dumpTextField                           .setText(fileName(configuration.dump));

        if (configuration.targetClassVersion != 0)
        {
            targetComboBox.setSelectedItem(ClassUtil.externalClassVersion(configuration.targetClassVersion));
        }
        else
        {
            targetComboBox.setSelectedIndex(targetComboBox.getItemCount() - 1);
        }

        if (configuration.printMapping != null)
        {
            reTraceMappingTextField.setText(fileName(configuration.printMapping));
        }
    }


    /**
     * Returns the ProGuard configuration that reflects the current GUI settings.
     */
    private Configuration getProGuardConfiguration()
    {
        Configuration configuration = new Configuration();

        // Get the input and output jars and directories.
        configuration.programJars = programPanel.getClassPath();
        configuration.libraryJars = libraryPanel.getClassPath();

        List keep = new ArrayList();

        // Collect the additional keep options.
        List additionalKeep = additionalKeepPanel.getClassSpecifications();
        if (additionalKeep != null)
        {
            keep.addAll(additionalKeep);
        }

        // Collect the additional keep names options.
        List additionalKeepNames = additionalKeepNamesPanel.getClassSpecifications();
        if (additionalKeepNames != null)
        {
            keep.addAll(additionalKeepNames);
        }

        // Collect the boilerplate keep options.
        for (int index = 0; index < boilerplateKeep.length; index++)
        {
            if (boilerplateKeepCheckBoxes[index].isSelected())
            {
                keep.add(classSpecification(boilerplateKeep[index],
                                            boilerplateKeepTextFields[index] == null ? null :
                                            boilerplateKeepTextFields[index].getText()));
            }
        }

        // Collect the boilerplate keep names options.
        for (int index = 0; index < boilerplateKeepNames.length; index++)
        {
            if (boilerplateKeepNamesCheckBoxes[index].isSelected())
            {
                keep.add(classSpecification(boilerplateKeepNames[index],
                                            boilerplateKeepNamesTextFields[index] == null ? null :
                                            boilerplateKeepNamesTextFields[index].getText()));
            }
        }

        // Put the list of keep specifications in the configuration.
        if (keep.size() > 0)
        {
            configuration.keep = keep;
        }


        // Collect the boilerplate "no side effect methods" options.
        List noSideEffectMethods = new ArrayList();

        for (int index = 0; index < boilerplateNoSideEffectMethods.length; index++)
        {
            if (boilerplateNoSideEffectMethodCheckBoxes[index].isSelected())
            {
                noSideEffectMethods.add(boilerplateNoSideEffectMethods[index]);
            }
        }

        // Collect the additional "no side effect methods" options.
        List additionalNoSideEffectOptions = additionalNoSideEffectsPanel.getClassSpecifications();
        if (additionalNoSideEffectOptions != null)
        {
            noSideEffectMethods.addAll(additionalNoSideEffectOptions);
        }

        // Put the list of "no side effect methods" options in the configuration.
        if (noSideEffectMethods.size() > 0)
        {
            configuration.assumeNoSideEffects = noSideEffectMethods;
        }


        // Collect the "why are you keeping" options.
        configuration.whyAreYouKeeping = whyAreYouKeepingPanel.getClassSpecifications();


        // Get the other options.
        configuration.shrink                           = shrinkCheckBox                          .isSelected();
        configuration.printUsage                       = printUsageCheckBox                      .isSelected() ? new File(printUsageTextField                                         .getText()) : null;

        configuration.optimize                         = optimizeCheckBox                        .isSelected();
        configuration.allowAccessModification          = allowAccessModificationCheckBox         .isSelected();
        configuration.mergeInterfacesAggressively      = mergeInterfacesAggressivelyCheckBox     .isSelected();
        configuration.optimizations                    = optimizationsTextField.getText().length() > 1 ?         ListUtil.commaSeparatedList(optimizationsTextField                   .getText()) : null;
        configuration.optimizationPasses               = ((SpinnerNumberModel)optimizationPassesSpinner.getModel()).getNumber().intValue();

        configuration.obfuscate                        = obfuscateCheckBox                       .isSelected();
        configuration.printMapping                     = printMappingCheckBox                    .isSelected() ? new File(printMappingTextField                                       .getText()) : null;
        configuration.applyMapping                     = applyMappingCheckBox                    .isSelected() ? new File(applyMappingTextField                                       .getText()) : null;
        configuration.obfuscationDictionary            = obfuscationDictionaryCheckBox           .isSelected() ? url(obfuscationDictionaryTextField                                   .getText()) : null;
        configuration.classObfuscationDictionary       = classObfuscationDictionaryCheckBox      .isSelected() ? url(classObfuscationDictionaryTextField                              .getText()) : null;
        configuration.packageObfuscationDictionary     = packageObfuscationDictionaryCheckBox    .isSelected() ? url(packageObfuscationDictionaryTextField                            .getText()) : null;
        configuration.overloadAggressively             = overloadAggressivelyCheckBox            .isSelected();
        configuration.useUniqueClassMemberNames        = useUniqueClassMemberNamesCheckBox       .isSelected();
        configuration.useMixedCaseClassNames           = useMixedCaseClassNamesCheckBox          .isSelected();
        configuration.keepPackageNames                 = keepPackageNamesCheckBox                .isSelected() ? keepPackageNamesTextField.getText().length()  > 0 ? ListUtil.commaSeparatedList(ClassUtil.internalClassName(keepPackageNamesTextField.getText()))  : new ArrayList() : null;
        configuration.flattenPackageHierarchy          = flattenPackageHierarchyCheckBox         .isSelected() ? ClassUtil.internalClassName(flattenPackageHierarchyTextField         .getText()) : null;
        configuration.repackageClasses                 = repackageClassesCheckBox                .isSelected() ? ClassUtil.internalClassName(repackageClassesTextField                .getText()) : null;
        configuration.keepAttributes                   = keepAttributesCheckBox                  .isSelected() ? ListUtil.commaSeparatedList(keepAttributesTextField                  .getText()) : null;
        configuration.keepParameterNames               = keepParameterNamesCheckBox              .isSelected();
        configuration.newSourceFileAttribute           = newSourceFileAttributeCheckBox          .isSelected() ? newSourceFileAttributeTextField                                      .getText()  : null;
        configuration.adaptClassStrings                = adaptClassStringsCheckBox               .isSelected() ? adaptClassStringsTextField.getText().length() > 0 ? ListUtil.commaSeparatedList(ClassUtil.internalClassName(adaptClassStringsTextField.getText())) : new ArrayList() : null;
        configuration.adaptResourceFileNames           = adaptResourceFileNamesCheckBox          .isSelected() ? ListUtil.commaSeparatedList(adaptResourceFileNamesTextField          .getText()) : null;
        configuration.adaptResourceFileContents        = adaptResourceFileContentsCheckBox       .isSelected() ? ListUtil.commaSeparatedList(adaptResourceFileContentsTextField       .getText()) : null;

        configuration.preverify                        = preverifyCheckBox                       .isSelected();
        configuration.microEdition                     = microEditionCheckBox                    .isSelected();
        configuration.targetClassVersion               = targetCheckBox                          .isSelected() ? ClassUtil.internalClassVersion(targetComboBox.getSelectedItem().toString()) : 0;

        configuration.verbose                          = verboseCheckBox                         .isSelected();
        configuration.note                             = noteCheckBox                            .isSelected() ? noteTextField.getText().length() > 0 ? ListUtil.commaSeparatedList(ClassUtil.internalClassName(noteTextField.getText())) : null : new ArrayList();
        configuration.warn                             = warnCheckBox                            .isSelected() ? warnTextField.getText().length() > 0 ? ListUtil.commaSeparatedList(ClassUtil.internalClassName(warnTextField.getText())) : null : new ArrayList();
        configuration.ignoreWarnings                   = ignoreWarningsCheckBox                  .isSelected();
        configuration.skipNonPublicLibraryClasses      = skipNonPublicLibraryClassesCheckBox     .isSelected();
        configuration.skipNonPublicLibraryClassMembers = skipNonPublicLibraryClassMembersCheckBox.isSelected();
        configuration.keepDirectories                  = keepDirectoriesCheckBox                 .isSelected() ? keepDirectoriesTextField.getText().length() > 0 ? ListUtil.commaSeparatedList(ClassUtil.internalClassName(keepDirectoriesTextField.getText())) : new ArrayList() : null;
        configuration.lastModified                     = forceProcessingCheckBox                 .isSelected() ? Long.MAX_VALUE : System.currentTimeMillis();
        configuration.printSeeds                       = printSeedsCheckBox                      .isSelected() ? new File(printSeedsTextField                                         .getText()) : null;
        configuration.printConfiguration               = printConfigurationCheckBox              .isSelected() ? new File(printConfigurationTextField                                 .getText()) : null;
        configuration.dump                             = dumpCheckBox                            .isSelected() ? new File(dumpTextField                                               .getText()) : null;

        return configuration;
    }


    /**
     * Looks in the given list for a class specification that is identical to
     * the given template. Returns true if it is found, and removes the matching
     * class specification as a side effect.
     */
    private boolean findClassSpecification(ClassSpecification classSpecificationTemplate,
                                           List                classSpecifications)
    {
        if (classSpecifications == null)
        {
            return false;
        }

        for (int index = 0; index < classSpecifications.size(); index++)
        {
            if (classSpecificationTemplate.equals(classSpecifications.get(index)))
            {
                // Remove the matching option as a side effect.
                classSpecifications.remove(index);

                return true;
            }
        }

        return false;
    }


    /**
     * Returns the subset of the given list of keep specifications, with
     * matching shrinking flag.
     */
    private List filteredKeepSpecifications(List    keepSpecifications,
                                            boolean allowShrinking)
    {
        List filteredKeepSpecifications = new ArrayList();

        for (int index = 0; index < keepSpecifications.size(); index++)
        {
            KeepClassSpecification keepClassSpecification =
                (KeepClassSpecification)keepSpecifications.get(index);

            if (keepClassSpecification.allowShrinking == allowShrinking)
            {
                filteredKeepSpecifications.add(keepClassSpecification);
            }
        }

        return filteredKeepSpecifications;
    }


    /**
     * Looks in the given list for keep specifications that match the given
     * template. Returns a comma-separated string of class names from
     * matching keep specifications, and removes the matching keep
     * specifications as a side effect.
     */
    private String findMatchingKeepSpecifications(KeepClassSpecification keepClassSpecificationTemplate,
                                                  boolean                matchName,
                                                  List                   keepSpecifications)
    {
        if (keepSpecifications == null)
        {
            return null;
        }

        StringBuffer buffer = null;

        for (int index = 0; index < keepSpecifications.size(); index++)
        {
            KeepClassSpecification listedKeepClassSpecification =
                (KeepClassSpecification)keepSpecifications.get(index);

            // Should we match the original template class name?
            String className = listedKeepClassSpecification.className;
            if (!matchName)
            {
                keepClassSpecificationTemplate.className = className;
            }

            if (keepClassSpecificationTemplate.equals(listedKeepClassSpecification))
            {
                if (buffer == null)
                {
                    buffer = new StringBuffer();
                }
                else
                {
                    buffer.append(',');
                }
                buffer.append(className == null ? "*" : ClassUtil.externalClassName(className));

                // Remove the matching option as a side effect.
                keepSpecifications.remove(index--);
            }
        }

        return buffer == null ? null : buffer.toString();
    }


    /**
     * Returns a class specification or keep specification, based on the given
     * template and the class name to be filled in.
     */
    private ClassSpecification classSpecification(ClassSpecification classSpecificationTemplate,
                                                  String             className)
    {
        // Create a copy of the template.
        ClassSpecification classSpecification =
            (ClassSpecification)classSpecificationTemplate.clone();

        // Set the class name in the copy.
        if (className != null)
        {
            classSpecification.className =
                className.equals("") ||
                className.equals("*") ?
                    null :
                    ClassUtil.internalClassName(className);
        }

        // Return the modified copy.
        return classSpecification;
    }


    // Methods and internal classes related to actions.

    /**
     * Loads the given ProGuard configuration into the GUI.
     */
    private void loadConfiguration(File file)
    {
        // Set the default directory and file in the file choosers.
        configurationChooser.setSelectedFile(file.getAbsoluteFile());
        fileChooser.setCurrentDirectory(file.getAbsoluteFile().getParentFile());

        try
        {
            // Parse the configuration file.
            ConfigurationParser parser = new ConfigurationParser(file,
                                                                 System.getProperties());

            Configuration configuration = new Configuration();

            try
            {
                parser.parse(configuration);

                // Let the GUI reflect the configuration.
                setProGuardConfiguration(configuration);
            }
            catch (ParseException ex)
            {
                JOptionPane.showMessageDialog(getContentPane(),
                                              msg("cantParseConfigurationFile", file.getPath()),
                                              msg("warning"),
                                              JOptionPane.ERROR_MESSAGE);
            }
            finally
            {
                parser.close();
            }
        }
        catch (IOException ex)
        {
            JOptionPane.showMessageDialog(getContentPane(),
                                          msg("cantOpenConfigurationFile", file.getPath()),
                                          msg("warning"),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }


    /**
     * Loads the given ProGuard configuration into the GUI.
     */
    private void loadConfiguration(URL url)
    {
        try
        {
            // Parse the configuration file.
            ConfigurationParser parser = new ConfigurationParser(url,
                                                                 System.getProperties());

            Configuration configuration = new Configuration();

            try
            {
                parser.parse(configuration);

                // Let the GUI reflect the configuration.
                setProGuardConfiguration(configuration);
            }
            catch (ParseException ex)
            {
                JOptionPane.showMessageDialog(getContentPane(),
                                              msg("cantParseConfigurationFile", url),
                                              msg("warning"),
                                              JOptionPane.ERROR_MESSAGE);
            }
            finally
            {
                parser.close();
            }
        }
        catch (IOException ex)
        {
            JOptionPane.showMessageDialog(getContentPane(),
                                          msg("cantOpenConfigurationFile", url),
                                          msg("warning"),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }


    /**
     * Saves the current ProGuard configuration to the given file.
     */
    private void saveConfiguration(File file)
    {
        try
        {
            // Save the configuration file.
            ConfigurationWriter writer = new ConfigurationWriter(file);

            try
            {
                writer.write(getProGuardConfiguration());
            }
            finally
            {
                writer.close();
            }
        }
        catch (Exception ex)
        {
            JOptionPane.showMessageDialog(getContentPane(),
                                          msg("cantSaveConfigurationFile", file.getPath()),
                                          msg("warning"),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }


    /**
     * Loads the given stack trace into the GUI.
     */
    private void loadStackTrace(File file)
    {
        try
        {
            StringBuffer buffer = new StringBuffer(1024);

            Reader reader =
                new BufferedReader(
                new InputStreamReader(
                new FileInputStream(file), "UTF-8"));

            try
            {
                while (true)
                {
                    int c = reader.read();
                    if (c < 0)
                    {
                        break;
                    }

                    buffer.append(c);
                }
            }
            finally
            {
                reader.close();
            }

            // Put the stack trace in the text area.
            stackTraceTextArea.setText(buffer.toString());
        }
        catch (IOException ex)
        {
            JOptionPane.showMessageDialog(getContentPane(),
                                          msg("cantOpenStackTraceFile", fileName(file)),
                                          msg("warning"),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }


    /**
     * This ActionListener loads a ProGuard configuration file and initializes
     * the GUI accordingly.
     */
    private class MyLoadConfigurationActionListener implements ActionListener
    {
        public void actionPerformed(ActionEvent e)
        {
            configurationChooser.setDialogTitle(msg("selectConfigurationFile"));

            int returnValue = configurationChooser.showOpenDialog(ProGuardGUI.this);
            if (returnValue == JFileChooser.APPROVE_OPTION)
            {
                loadConfiguration(configurationChooser.getSelectedFile());
            }
        }
    }


    /**
     * This ActionListener saves a ProGuard configuration file based on the
     * current GUI settings.
     */
    private class MySaveConfigurationActionListener implements ActionListener
    {
        public void actionPerformed(ActionEvent e)
        {
            configurationChooser.setDialogTitle(msg("saveConfigurationFile"));

            int returnVal = configurationChooser.showSaveDialog(ProGuardGUI.this);
            if (returnVal == JFileChooser.APPROVE_OPTION)
            {
                saveConfiguration(configurationChooser.getSelectedFile());
            }
        }
    }


    /**
     * This ActionListener displays the ProGuard configuration specified by the
     * current GUI settings.
     */
    private class MyViewConfigurationActionListener implements ActionListener
    {
        public void actionPerformed(ActionEvent e)
        {
            // Make sure System.out has not been redirected yet.
            if (!systemOutRedirected)
            {
                consoleTextArea.setText("");

                PrintWriter printWriter =
                    new PrintWriter(
                    new TextAreaWriter(consoleTextArea));

                try
                {
                    // TODO: write out relative path names and path names with system properties.

                    // Write the configuration.
                    ConfigurationWriter configurationWriter =
                        new ConfigurationWriter(printWriter);

                    try
                    {
                        configurationWriter.write(getProGuardConfiguration());
                    }
                    finally
                    {
                        configurationWriter.close();
                    }
                }
                catch (IOException ignore)
                {
                    // This shouldn't happen.
                }

                // Scroll to the top of the configuration.
                consoleTextArea.setCaretPosition(0);
            }
        }
    }


    /**
     * This ActionListener executes ProGuard based on the current GUI settings.
     */
    private class MyProcessActionListener implements ActionListener
    {
        public void actionPerformed(ActionEvent e)
        {
            // Make sure System.out has not been redirected yet.
            if (!systemOutRedirected)
            {
                systemOutRedirected = true;

                // Get the informational configuration file name.
                File configurationFile = configurationChooser.getSelectedFile();
                String configurationFileName = configurationFile != null ?
                    configurationFile.getName() :
                    msg("sampleConfigurationFileName");

                // Create the ProGuard thread.
                Thread proGuardThread =
                    new Thread(new ProGuardRunnable(consoleTextArea,
                                                    getProGuardConfiguration(),
                                                    configurationFileName));

                // Run it.
                proGuardThread.start();
            }
        }
    }


    /**
     * This ActionListener loads an obfuscated stack trace from a file and puts
     * it in the proper text area.
     */
    private class MyLoadStackTraceActionListener implements ActionListener
    {
        public void actionPerformed(ActionEvent e)
        {
            fileChooser.setDialogTitle(msg("selectStackTraceFile"));
            fileChooser.setSelectedFile(null);

            int returnValue = fileChooser.showOpenDialog(ProGuardGUI.this);
            if (returnValue == JFileChooser.APPROVE_OPTION)
            {

                loadStackTrace(fileChooser.getSelectedFile());
            }
        }
    }


    /**
     * This ActionListener executes ReTrace based on the current GUI settings.
     */
    private class MyReTraceActionListener implements ActionListener
    {
        public void actionPerformed(ActionEvent e)
        {
            // Make sure System.out has not been redirected yet.
            if (!systemOutRedirected)
            {
                systemOutRedirected = true;

                boolean verbose            = reTraceVerboseCheckBox.isSelected();
                File    retraceMappingFile = new File(reTraceMappingTextField.getText());
                String  stackTrace         = stackTraceTextArea.getText();

                // Create the ReTrace runnable.
                Runnable reTraceRunnable = new ReTraceRunnable(reTraceTextArea,
                                                               verbose,
                                                               retraceMappingFile,
                                                               stackTrace);

                // Run it in this thread, because it won't take long anyway.
                reTraceRunnable.run();
            }
        }
    }


    // Small utility methods.

    /**
     * Returns the URL for the given path.
     */
    private URL url(String path)
    {
        try
        {
            return new URL(path);
        }
        catch (MalformedURLException e)
        {
            try
            {
                return new File(path).toURI().toURL();
            }
            catch (MalformedURLException e1)
            {
                return null;
            }
        }
    }


    /**
     * Returns the path for the given URL, or the empty string if the URL name
     * is empty.
     */
    private String fileName(URL url)
    {
        if (isURL(url))
        {
            if (url.getProtocol().equals("file"))
            {
                try
                {
                    return fileName(new File(url.toURI()));
                }
                catch (URISyntaxException ignore) {}
            }

            return url.toExternalForm();
        }
        else
        {
            return "";
        }
    }


    /**
     * Returns whether the given URL is actually a URL, or just a placeholder
     * for the standard output.
     */
    private boolean isURL(URL url)
    {
        return url != null &&
               url.getPath().length() > 0;
    }


    /**
     * Returns the canonical file name for the given file, or the empty string
     * if the file name is empty.
     */
    private String fileName(File file)
    {
        if (file == null)
        {
            return "";
        }
        else
        {
            try
            {
                return file.getCanonicalPath();
            }
            catch (IOException ex)
            {
                return file.getPath();
            }
        }
    }


    /**
     * Attaches the tool tip from the GUI resources that corresponds to the
     * given key, to the given component.
     */
    private static JComponent tip(JComponent component, String messageKey)
    {
        component.setToolTipText(msg(messageKey));

        return component;
    }


    /**
     * Returns the message from the GUI resources that corresponds to the given
     * key.
     */
    private static String msg(String messageKey)
    {
         return GUIResources.getMessage(messageKey);
    }


    /**
     * Returns the message from the GUI resources that corresponds to the given
     * key and argument.
     */
    private String msg(String messageKey,
                       Object messageArgument)
    {
         return GUIResources.getMessage(messageKey, new Object[] {messageArgument});
    }


    /**
     * The main method for the ProGuard GUI.
     */
    public static void main(final String[] args)
    {
        try
        {
            SwingUtil.invokeAndWait(new Runnable()
            {
                public void run()
                {
                    try
                    {
                        ProGuardGUI gui = new ProGuardGUI();

                        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
                        Dimension guiSize    = gui.getSize();
                        gui.setLocation((screenSize.width - guiSize.width)   / 2,
                                        (screenSize.height - guiSize.height) / 2);
                        gui.show();

                        // Start the splash animation, unless specified otherwise.
                        int argIndex = 0;
                        if (argIndex < args.length &&
                            NO_SPLASH_OPTION.startsWith(args[argIndex]))
                        {
                            gui.skipSplash();
                            argIndex++;
                        }
                        else
                        {
                            gui.startSplash();
                        }

                        // Load an initial configuration, if specified.
                        if (argIndex < args.length)
                        {
                            gui.loadConfiguration(new File(args[argIndex]));
                            argIndex++;
                        }

                        if (argIndex < args.length)
                        {
                            System.out.println(gui.getClass().getName() + ": ignoring extra arguments [" + args[argIndex] + "...]");
                        }
                    }
                    catch (Exception e)
                    {
                        System.out.println("Internal problem starting the ProGuard GUI (" + e.getMessage() + ")");
                        e.printStackTrace();
                    }
                }
            });
        }
        catch (Exception e)
        {
            System.out.println("Internal problem starting the ProGuard GUI (" + e.getMessage() + ")");
            e.printStackTrace();
        }
    }
}
