com.android.tools.idea.npw.assetstudio.wizard.GenerateIconsPanel.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.npw.assetstudio.wizard.GenerateIconsPanel.java

Source

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tools.idea.npw.assetstudio.wizard;

import com.android.assetstudiolib.NotificationIconGenerator;
import com.android.tools.idea.npw.assetstudio.icon.AndroidIconGenerator;
import com.android.tools.idea.npw.assetstudio.icon.AndroidIconType;
import com.android.tools.idea.npw.assetstudio.icon.CategoryIconMap;
import com.android.tools.idea.npw.assetstudio.ui.ConfigureIconPanel;
import com.android.tools.idea.npw.assetstudio.ui.PreviewIconsPanel;
import com.android.tools.idea.npw.project.AndroidProjectPaths;
import com.android.tools.idea.ui.ImageComponent;
import com.android.tools.idea.ui.properties.BindingsManager;
import com.android.tools.idea.ui.properties.ListenerManager;
import com.android.tools.idea.ui.properties.ObservableValue;
import com.android.tools.idea.ui.properties.core.ObservableBool;
import com.android.tools.idea.ui.properties.core.StringProperty;
import com.android.tools.idea.ui.properties.core.StringValueProperty;
import com.android.tools.idea.ui.properties.expressions.value.AsValueExpression;
import com.android.tools.idea.ui.properties.swing.SelectedItemProperty;
import com.android.tools.idea.ui.validation.Validator;
import com.android.tools.idea.ui.validation.ValidatorPanel;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.IconUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Map;

/**
 * A panel which presents a UI for selecting some source asset and converting it to a target set of
 * Android Icons. See {@link AndroidIconType} for the types of icons this can generate.
 *
 * Before generating icons, you should first check {@link #hasErrors()} to make sure there won't be
 * any errors in the generation process.
 *
 * This is a Swing port of the various icon generators provided by the
 * <a href="https://romannurik.github.io/AndroidAssetStudio/index.html">Asset Studio</a>
 * web application.
 */
public final class GenerateIconsPanel extends JPanel implements Disposable {

    private static final int ASSET_PREVIEW_HEIGHT = 96;

    private final AndroidProjectPaths myDefaultPaths;
    private final ValidatorPanel myValidatorPanel;

    private final BindingsManager myBindings = new BindingsManager();
    private final ListenerManager myListeners = new ListenerManager();

    private final Multimap<AndroidIconType, PreviewIconsPanel> myOutputPreviewPanels;
    private final ObservableValue<AndroidIconType> myOutputIconType;
    private final StringProperty myOutputName = new StringValueProperty();

    private JPanel myRootPanel;
    private JPanel myTopRightPanel;
    private ImageComponent mySourceAssetImage;
    private JPanel myOutputPreviewPanel;
    private JPanel myTopPanel;
    private JComboBox myIconTypeCombo;
    private JPanel myBottomPanel;
    private JPanel myConfigureIconPanels;
    private JPanel mySourceAssetPanel;
    private JPanel mySourceAssetMaxWidthPanel;

    @NotNull
    private AndroidProjectPaths myPaths;
    @Nullable
    private BufferedImage myEnqueuedImageToProcess;

    /**
     * Create a panel which can generate Android icons. The supported types passed in will be
     * presented to the user in a pulldown menu (unless there's only one supported type). If no
     * supported types are passed in, then all types will be supported by default.
     */
    public GenerateIconsPanel(@NotNull Disposable disposableParent, @NotNull AndroidProjectPaths defaultPaths,
            @NotNull AndroidIconType... supportedTypes) {
        super(new BorderLayout());

        myDefaultPaths = defaultPaths;
        myPaths = myDefaultPaths;

        if (supportedTypes.length == 0) {
            supportedTypes = AndroidIconType.values();
        }

        DefaultComboBoxModel supportedTypesModel = new DefaultComboBoxModel(supportedTypes);
        myIconTypeCombo.setModel(supportedTypesModel);
        myIconTypeCombo.setVisible(supportedTypes.length > 1);

        assert myConfigureIconPanels.getLayout() instanceof CardLayout;
        for (AndroidIconType iconType : supportedTypes) {
            myConfigureIconPanels.add(new ConfigureIconPanel(this, iconType), iconType.toString());
        }

        mySourceAssetMaxWidthPanel.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                updateSourceAssetPreview(getActiveIconPanel().getAsset().toImage());
            }
        });

        // @formatter:off
        ImmutableMultimap.Builder<AndroidIconType, PreviewIconsPanel> previewPanelBuilder = ImmutableMultimap
                .builder();
        previewPanelBuilder.putAll(AndroidIconType.ACTIONBAR,
                new PreviewIconsPanel("", PreviewIconsPanel.Theme.TRANSPARENT));
        previewPanelBuilder.putAll(AndroidIconType.LAUNCHER,
                new PreviewIconsPanel("", PreviewIconsPanel.Theme.TRANSPARENT));
        previewPanelBuilder.putAll(AndroidIconType.NOTIFICATION,
                new PreviewIconsPanel("API 11+", PreviewIconsPanel.Theme.DARK,
                        new CategoryIconMap.NotificationFilter(NotificationIconGenerator.Version.V11)),
                new PreviewIconsPanel("API 9+", PreviewIconsPanel.Theme.LIGHT,
                        new CategoryIconMap.NotificationFilter(NotificationIconGenerator.Version.V9)),
                new PreviewIconsPanel("Older APIs", PreviewIconsPanel.Theme.GRAY,
                        new CategoryIconMap.NotificationFilter(NotificationIconGenerator.Version.OLDER)));
        myOutputPreviewPanels = previewPanelBuilder.build();
        // @formatter:on

        for (PreviewIconsPanel iconsPanel : myOutputPreviewPanels.values()) {
            myOutputPreviewPanel.add(iconsPanel);
        }

        myOutputIconType = new AsValueExpression<>(new SelectedItemProperty<>(myIconTypeCombo));

        initializeListenersAndBindings();

        myValidatorPanel = new ValidatorPanel(this, myRootPanel);
        initializeValidators();

        Disposer.register(disposableParent, this);
        Disposer.register(this, myValidatorPanel);
        add(myValidatorPanel);
    }

    private void initializeListenersAndBindings() {
        ActionListener onAssetModified = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                renderIconPreviews();
            }
        };

        for (Component component : myConfigureIconPanels.getComponents()) {
            ((ConfigureIconPanel) component).addAssetListener(onAssetModified);
        }

        myListeners.receiveAndFire(myOutputIconType, iconType -> {
            ((CardLayout) myConfigureIconPanels.getLayout()).show(myConfigureIconPanels, iconType.toString());

            ConfigureIconPanel iconPanel = getActiveIconPanel();
            myBindings.bind(myOutputName, iconPanel.outputName());

            for (PreviewIconsPanel previewPanel : myOutputPreviewPanels.values()) {
                previewPanel.setVisible(false);
            }
            for (PreviewIconsPanel previewPanel : myOutputPreviewPanels.get(iconType)) {
                previewPanel.setVisible(true);
            }

            renderIconPreviews();
        });
    }

    @NotNull
    private ConfigureIconPanel getActiveIconPanel() {
        for (Component component : myConfigureIconPanels.getComponents()) {
            if (component.isVisible()) {
                return (ConfigureIconPanel) component;
            }
        }

        throw new IllegalStateException("GenerateIconPanel configured incorrectly. Please report this error.");
    }

    private void initializeValidators() {
        myValidatorPanel.registerValidator(myOutputName, new Validator<String>() {
            @NotNull
            @Override
            public Result validate(@NotNull String outputName) {
                String trimmedName = outputName.trim();
                if (trimmedName.isEmpty()) {
                    return new Result(Severity.ERROR, "Icon name must be set");
                } else if (iconExists()) {
                    return new Result(Severity.WARNING,
                            "An icon with the same name already exists and will be overwritten.");
                } else {
                    return Result.OK;
                }
            }
        });
    }

    /**
     * Set the target project paths that this panel should use when generating assets. If not set,
     * this panel will attempt to use reasonable defaults for the project.
     */
    public void setProjectPaths(@Nullable AndroidProjectPaths projectPaths) {
        myPaths = (projectPaths != null) ? projectPaths : myDefaultPaths;
    }

    /**
     * Set the output name for the icon type currently being edited. This is exposed as some UIs may wish
     * to set this explicitly instead of relying on defaults.
     */
    public void setOutputName(@NotNull String name) {
        getActiveIconPanel().outputName().set(name);
    }

    /**
     * Return an icon generator which will create Android icons using the panel's current settings.
     */
    @NotNull
    public AndroidIconGenerator getIconGenerator() {
        return getActiveIconPanel().getIconGenerator();
    }

    /**
     * A boolean property which will be true if validation logic catches any problems with any of the
     * current icon settings, particularly the output name / path. You should probably not generate
     * icons if there are any errors.
     */
    @NotNull
    public ObservableBool hasErrors() {
        return myValidatorPanel.hasErrors();
    }

    private boolean iconExists() {
        Map<File, BufferedImage> pathImageMap = getIconGenerator().generateIntoFileMap(myPaths);
        for (File path : pathImageMap.keySet()) {
            if (path.exists()) {
                return true;
            }
        }

        return false;
    }

    private void renderIconPreviews() {
        // This method is often called as the result of a UI property changing which may also cause
        // some other properties to change. Invoke its logic later just to make sure everything gets a
        // chance to settle first.
        ApplicationManager.getApplication().invokeLater(new Runnable() {
            @Override
            public void run() {
                BufferedImage assetImage = getActiveIconPanel().getAsset().toImage();
                updateSourceAssetPreview(assetImage);
                enqueueGenerateNotificationIcons(assetImage);
            }
        }, ModalityState.any());
    }

    private void updateSourceAssetPreview(BufferedImage assetImage) {
        // Preserve aspect ratio as much as we can (up to a reasonable maximum width)
        int myMaxAssetPreviewWidth = mySourceAssetMaxWidthPanel.getWidth();

        double aspectRatio = 1.0;
        if (assetImage.getHeight() > 0) {
            aspectRatio = (double) assetImage.getWidth() / assetImage.getHeight();
        }
        int finalWidth = (int) Math.round(ASSET_PREVIEW_HEIGHT * aspectRatio);
        finalWidth = Math.min(finalWidth, myMaxAssetPreviewWidth);
        Dimension d = new Dimension(finalWidth, ASSET_PREVIEW_HEIGHT);

        mySourceAssetPanel.setPreferredSize(d);
        mySourceAssetImage.setIcon(IconUtil.createImageIcon(assetImage));
    }

    /**
     * Generating notification icons is not a lightweight process, and if we try to do it
     * synchronously, it stutters the UI. So instead we enqueue the request to run on a background
     * thread. If several requests are made in a row while an existing worker is still in progress,
     * only the most recently added will be handled, whenever the worker finishes.
     */
    private void enqueueGenerateNotificationIcons(@NotNull final BufferedImage assetImage) {
        ApplicationManager.getApplication().assertIsDispatchThread();

        boolean currentlyWorking = myEnqueuedImageToProcess != null;
        myEnqueuedImageToProcess = assetImage;

        if (currentlyWorking) {
            return;
        }

        SwingWorker<Void, Void> generateIconsWorker = new SwingWorker<Void, Void>() {

            @Nullable
            private CategoryIconMap myCategoryIconMap;

            @Override
            protected Void doInBackground() throws Exception {
                if (getIconGenerator().sourceAsset().get().isPresent()) {
                    myCategoryIconMap = getIconGenerator().generateIntoMemory();
                }
                return null;
            }

            @Override
            protected void done() {
                if (myCategoryIconMap != null) {
                    for (PreviewIconsPanel generatedIconsPanel : myOutputPreviewPanels
                            .get(myOutputIconType.get())) {
                        generatedIconsPanel.updateImages(myCategoryIconMap);
                    }
                    myCategoryIconMap = null;
                }

                // At this point, we've finished preparing the previews. Let's see if another request to
                // render more previews was added while we were working, and if so, start on it right away.
                BufferedImage nextImage = null;
                if (myEnqueuedImageToProcess != assetImage) {
                    nextImage = myEnqueuedImageToProcess;
                }
                myEnqueuedImageToProcess = null;

                if (nextImage != null) {
                    final BufferedImage finalNextImage = nextImage;
                    ApplicationManager.getApplication().invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            enqueueGenerateNotificationIcons(finalNextImage);
                        }
                    }, ModalityState.any());
                }
            }
        };

        generateIconsWorker.execute();
    }

    @Override
    public void dispose() {
        myBindings.releaseAll();
        myListeners.releaseAll();
    }
}