com.chrisrm.idea.MTLafComponent.java Source code

Java tutorial

Introduction

Here is the source code for com.chrisrm.idea.MTLafComponent.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2017 Chris Magnussen and Elior Boukhobza
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 *
 */

package com.chrisrm.idea;

import com.chrisrm.idea.config.BeforeConfigNotifier;
import com.chrisrm.idea.config.ConfigNotifier;
import com.chrisrm.idea.config.ui.MTForm;
import com.chrisrm.idea.messages.MaterialThemeBundle;
import com.chrisrm.idea.ui.*;
import com.chrisrm.idea.utils.MTUiUtils;
import com.chrisrm.idea.utils.UIReplacer;
import com.intellij.CommonBundle;
import com.intellij.ide.ui.LafManager;
import com.intellij.openapi.actionSystem.impl.ChameleonAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ProjectManagerListener;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.wm.impl.IdeBackgroundUtil;
import com.intellij.ui.CaptionPanel;
import com.intellij.ui.components.JBPanel;
import com.intellij.util.messages.MessageBusConnection;
import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;

/**
 * Component for working on the Material Look And Feel
 */
public final class MTLafComponent extends JBPanel implements ApplicationComponent {

    private boolean willRestartIde = false;

    static {
        hackTitleLabel();
        hackIdeaActionButton();
        hackBackgroundFrame();
    }

    private static void hackBackgroundFrame() {
        // Hack method
        try {
            final ClassPool cp = new ClassPool(true);
            cp.insertClassPath(new ClassClassPath(IdeBackgroundUtil.class));
            final CtClass ctClass = cp.get("com.intellij.openapi.wm.impl.IdePanePanel");

            final CtMethod paintBorder = ctClass.getDeclaredMethod("getBackground");
            paintBorder.instrument(new ExprEditor() {
                @Override
                public void edit(final MethodCall m) throws CannotCompileException {
                    if (m.getMethodName().equals("getIdeBackgroundColor")) {
                        m.replace("{ $_ = javax.swing.UIManager.getColor(\"Viewport.background\"); }");
                    }
                }
            });
            ctClass.toClass();
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    private MessageBusConnection connect;
    private UIManager.LookAndFeelInfo currentLookAndFeel = LafManager.getInstance().getCurrentLookAndFeel();

    public MTLafComponent(final LafManager lafManager) {
        lafManager.addLafManagerListener(source -> installMaterialComponents());
        lafManager.addLafManagerListener(this::askResetCustomTheme);
    }

    @Override
    public void initComponent() {
        installMaterialComponents();

        // Patch UI components
        UIReplacer.patchUI();

        // Listen for changes on the settings
        connect = ApplicationManager.getApplication().getMessageBus().connect();
        connect.subscribe(ConfigNotifier.CONFIG_TOPIC, this::onSettingsChanged);
        connect.subscribe(BeforeConfigNotifier.BEFORE_CONFIG_TOPIC, (this::onBeforeSettingsChanged));
    }

    /**
     * For better dialog titles (since I have no idea how to know when dialogs appear, I can't attach events so I'm directly hacking
     * the source code). I hate doing this.
     */
    public static void hackTitleLabel() {
        // Hack method
        try {
            final ClassPool cp = new ClassPool(true);
            cp.insertClassPath(new ClassClassPath(CaptionPanel.class));
            final CtClass ctClass = cp.get("com.intellij.ui.TitlePanel");
            final CtConstructor declaredConstructor = ctClass.getDeclaredConstructor(
                    new CtClass[] { cp.get("javax.swing.Icon"), cp.get("javax.swing" + ".Icon") });
            declaredConstructor.instrument(new ExprEditor() {
                @Override
                public void edit(final MethodCall m) throws CannotCompileException {
                    if (m.getMethodName().equals("empty")) {
                        // Replace insets
                        m.replace("{ $1 = 10; $2 = 10; $3 = 10; $4 = 10; $_ = $proceed($$); }");
                    } else if (m.getMethodName().equals("setHorizontalAlignment")) {
                        // Set title at the left
                        m.replace("{ $1 = javax.swing.SwingConstants.LEFT; $_ = $proceed($$); }");
                    } else if (m.getMethodName().equals("setBorder")) {
                        // Bigger heading
                        m.replace(
                                "{ $_ = $proceed($$); myLabel.setFont(myLabel.getFont().deriveFont(1, com.intellij.util.ui.JBUI.scale(16.0f))); }");
                    }
                }
            });
            ctClass.toClass();
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Change Look and feel of Action buttons
     */
    private static void hackIdeaActionButton() {
        try {
            final ClassPool cp = new ClassPool(true);
            cp.insertClassPath(new ClassClassPath(ChameleonAction.class));
            final CtClass ctClass = cp.get("com.intellij.openapi.actionSystem.impl.IdeaActionButtonLook");

            // Edit paintborder
            final CtClass[] paintBorderParams = new CtClass[] { cp.get("java.awt.Graphics"),
                    cp.get("java.awt.Dimension"), cp.get("int") };
            final CtMethod paintBorder = ctClass.getDeclaredMethod("paintBorder", paintBorderParams);
            paintBorder.instrument(new ExprEditor() {
                @Override
                public void edit(final MethodCall m) throws CannotCompileException {
                    if (m.getMethodName().equals("setColor")) {
                        m.replace("{ $1 = javax.swing.UIManager.getColor(\"Focus.color\"); $_ = $proceed($$); }");
                    } else if (m.getMethodName().equals("draw")) {
                        m.replace("{ if ($1.getBounds().width > 30) { " + "$proceed($$); " + "} else { "
                                + "$0.fillOval(1, 1, $1.getBounds().width, $1.getBounds().height); } " + "}");
                    }
                }
            });

            // Edit paintborder
            // outdated in EAP 2017.3
            final CtClass[] paintBackgroundParams = new CtClass[] { cp.get("java.awt.Graphics"),
                    cp.get("java.awt.Dimension"), cp.get("java.awt.Color"), cp.get("int") };
            final CtMethod paintBackground = ctClass.getDeclaredMethod("paintBackground");
            paintBackground.instrument(new ExprEditor() {
                @Override
                public void edit(final MethodCall m) throws CannotCompileException {
                    if (m.getMethodName().equals("paintBackground")) {
                        m.replace("{ }");
                    }
                }
            });

            ctClass.toClass();

            final CtClass comboBoxActionButtonClass = cp
                    .get("com.intellij.openapi.actionSystem.ex.ComboBoxAction$ComboBoxButton");
            final CtMethod paint = comboBoxActionButtonClass.getDeclaredMethod("paint");
            paint.instrument(new ExprEditor() {
                @Override
                public void edit(final MethodCall m) throws CannotCompileException {
                    if (m.getMethodName().equals("drawRoundRect")) {
                        m.replace("{ $2 = $4; $5 = 0; $6 = 0; $_ = $proceed($$); }");
                    } else if (m.getMethodName().equals("setPaint")) {
                        final String color = "javax.swing.UIManager.getColor(\"TextField.selectedSeparatorColor\")";

                        m.replace("{ $1 = $1 instanceof com.intellij.ui.JBColor && myMouseInside ? " + color
                                + " : $1; $_ = $proceed($$); }");
                    }
                }
            });

            comboBoxActionButtonClass.toClass();
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void disposeComponent() {
        connect.disconnect();
    }

    @NotNull
    @Override
    public String getComponentName() {
        return getClass().getName();
    }

    /**
     * Called when MT Config settings are changeds
     *
     * @param mtConfig
     * @param form
     */
    private void onBeforeSettingsChanged(final MTConfig mtConfig, final MTForm form) {
        // Force restart if material design is switched
        restartIdeIfNecessary(mtConfig, form);
    }

    /**
     * Called when MT Config settings are changeds
     *
     * @param mtConfig
     */
    private void onSettingsChanged(final MTConfig mtConfig) {
        // Trigger file icons and statuses update
        MTThemeManager.getInstance().updateFileIcons();

        if (willRestartIde) {
            MTUiUtils.restartIde();
        }
    }

    /**
     * Restart IDE if necessary (ex: material design components)
     *
     * @param mtConfig
     * @param form
     */
    private void restartIdeIfNecessary(final MTConfig mtConfig, final MTForm form) {
        // Restart the IDE if changed
        if (mtConfig.needsRestart(form)) {
            final String title = MaterialThemeBundle.message("mt.restartDialog.title");
            final String message = MaterialThemeBundle.message("mt.restartDialog.content");

            final int answer = Messages.showYesNoDialog(message, title, Messages.getQuestionIcon());
            if (answer == Messages.YES) {
                willRestartIde = true;
            }
        }
    }

    /**
     * Ask for resetting custom theme colors when the LafManager is switched from or to dark mode
     *
     * @param source
     */
    private void askResetCustomTheme(final LafManager source) {
        // If switched look and feel and asking for reset (default true)
        if (source.getCurrentLookAndFeel() != currentLookAndFeel
                && !MTCustomThemeConfig.getInstance().isDoNotAskAgain()) {
            final int dialog = Messages.showOkCancelDialog(
                    MaterialThemeBundle.message("mt.resetCustomTheme.message"),
                    MaterialThemeBundle.message("mt.resetCustomTheme.title"), CommonBundle.getOkButtonText(),
                    CommonBundle.getCancelButtonText(), Messages.getQuestionIcon(),
                    new DialogWrapper.DoNotAskOption.Adapter() {
                        @Override
                        public void rememberChoice(final boolean isSelected, final int exitCode) {
                            if (exitCode != -1) {
                                MTCustomThemeConfig.getInstance().setDoNotAskAgain(isSelected);
                            }
                        }
                    });

            if (dialog == Messages.YES) {
                MTCustomThemeConfig.getInstance().setDefaultValues();
                currentLookAndFeel = source.getCurrentLookAndFeel();

                MTThemeManager.getInstance().activate();
            }
        }
        currentLookAndFeel = source.getCurrentLookAndFeel();
    }

    /**
     * Replace Table headers
     */
    private void replaceTableHeaders() {
        UIManager.put("TableHeaderUI", MTTableHeaderUI.class.getName());
        UIManager.getDefaults().put(MTTableHeaderUI.class.getName(), MTTableHeaderUI.class);

        UIManager.put("TableHeader.border", new MTTableHeaderBorder());
    }

    /**
     * Replace progress bar
     */
    private void replaceProgressBar() {
        UIManager.put("ProgressBarUI", MTProgressBarUI.class.getName());
        UIManager.getDefaults().put(MTProgressBarUI.class.getName(), MTProgressBarUI.class);

        UIManager.put("ProgressBar.border", new MTProgressBarBorder());
    }

    /**
     * Replace text fields
     */
    private void replaceTextFields() {
        UIManager.put("TextFieldUI", MTTextFieldUI.class.getName());
        UIManager.getDefaults().put(MTTextFieldUI.class.getName(), MTTextFieldUI.class);

        UIManager.put("PasswordFieldUI", MTPasswordFieldUI.class.getName());
        UIManager.getDefaults().put(MTPasswordFieldUI.class.getName(), MTPasswordFieldUI.class);

        UIManager.put("TextField.border", new MTTextBorder());
        UIManager.put("PasswordField.border", new MTTextBorder());
    }

    /**
     * Replace buttons
     */
    private void replaceButtons() {
        UIManager.put("ButtonUI", MTButtonUI.class.getName());
        UIManager.getDefaults().put(MTButtonUI.class.getName(), MTButtonUI.class);

        UIManager.put("Button.border", new MTButtonPainter());
    }

    /**
     * Install Material Design components
     */
    private void installMaterialComponents() {
        final MTConfig mtConfig = MTConfig.getInstance();

        if (mtConfig.getIsMaterialDesign()) {
            replaceButtons();
            replaceTextFields();
            replaceDropdowns();
            replaceProgressBar();
            replaceTree();
            replaceTableHeaders();
            replaceTables();
            replaceStatusBar();
            replaceSpinners();
            replaceCheckboxes();
            replaceRadioButtons();
            replaceSliders();
            //      replaceTextAreas();
        }
    }

    private void replaceSliders() {
        UIManager.put("SliderUI", MTSliderUI.class.getName());
        UIManager.getDefaults().put(MTSliderUI.class.getName(), MTSliderUI.class);
    }

    private void replaceCheckboxes() {
        UIManager.put("CheckBoxUI", MTCheckBoxUI.class.getName());
        UIManager.getDefaults().put(MTCheckBoxUI.class.getName(), MTCheckBoxUI.class);

        UIManager.put("CheckBox.border", new MTCheckBoxBorder());
    }

    private void replaceRadioButtons() {
        UIManager.put("RadioButtonUI", MTRadioButtonUI.class.getName());
        UIManager.getDefaults().put(MTRadioButtonUI.class.getName(), MTRadioButtonUI.class);
    }

    private void replaceTextAreas() {
        UIManager.put("TextAreaUI", MTTextAreaUI.class.getName());
        UIManager.getDefaults().put(MTTextAreaUI.class.getName(), MTTextAreaUI.class);
    }

    private void replaceDropdowns() {
        UIManager.put("ComboBoxUI", MTComboBoxUI.class.getName());
        UIManager.getDefaults().put(MTComboBoxUI.class.getName(), MTComboBoxUI.class);
    }

    private void replaceSpinners() {
        UIManager.put("SpinnerUI", MTSpinnerUI.class.getName());
        UIManager.getDefaults().put(MTSpinnerUI.class.getName(), MTSpinnerUI.class);

        UIManager.put("Spinner.border", new MTSpinnerBorder());
    }

    private void replaceTables() {
        UIManager.put("Table.cellNoFocusBorder", new MTTableCellNoFocusBorder());
        UIManager.put("Table.focusCellHighlightBorder", new MTTableSelectedCellHighlightBorder());
    }

    private void replaceStatusBar() {
        final MessageBusConnection connect = ApplicationManager.getApplication().getMessageBus().connect();

        // On app init, set the statusbar borders
        connect.subscribe(ProjectManager.TOPIC, new ProjectManagerListener() {
            @Override
            public void projectOpened(@Nullable final Project projectFromCommandLine) {
                MTThemeManager.getInstance().setStatusBarBorders();
            }
        });

        // And also on config change
        connect.subscribe(ConfigNotifier.CONFIG_TOPIC,
                mtConfig -> MTThemeManager.getInstance().setStatusBarBorders());
    }

    /**
     * Replace trees
     */
    private void replaceTree() {
        UIManager.put("TreeUI", MTTreeUI.class.getName());
        UIManager.getDefaults().put(MTTreeUI.class.getName(), MTTreeUI.class);

        UIManager.put("List.sourceListSelectionBackgroundPainter", new MTSelectedTreePainter());
        UIManager.put("List.sourceListFocusedSelectionBackgroundPainter", new MTSelectedTreePainter());
    }
}