All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.jaxx.runtime.swing.action.JAXXObjectActionSupport Maven / Gradle / Ivy

There is a newer version: 3.1.5
Show newest version
package org.nuiton.jaxx.runtime.swing.action;

/*-
 * #%L
 * JAXX :: Runtime
 * %%
 * Copyright (C) 2008 - 2023 Code Lutin, Ultreia.io
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 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 Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

import com.google.common.collect.ImmutableMap;
import io.ultreia.java4all.bean.AbstractJavaBean;
import io.ultreia.java4all.bean.spi.GenerateJavaBeanDefinition;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.LayerUI;
import org.nuiton.jaxx.runtime.JAXXObject;
import org.nuiton.jaxx.runtime.swing.BlockingLayerUI;
import org.nuiton.jaxx.runtime.swing.JOptionPanes;
import org.nuiton.jaxx.runtime.swing.SwingUtil;

import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.Component;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.beans.PropertyChangeListener;
import java.util.Objects;

import static io.ultreia.java4all.i18n.I18n.t;

@GenerateJavaBeanDefinition
public abstract class JAXXObjectActionSupport extends AbstractJavaBean implements Action {

    public static final String ACTION_TYPE = "actionType";
    public static final String EDITOR = "editor";
    public static final String ACTIVATE_FROM_POPUP = "activateFromPopup";
    private static final Logger log = LogManager.getLogger(JAXXObjectActionSupport.class);
    private static final String ENABLED = "enabled";

    private static final ImmutableMap PROPERTY_MAPPING = ImmutableMap.builder()
            .put(ACTION_COMMAND_KEY, "actionCommandKey")
            .put(NAME, "text")
            .put(SHORT_DESCRIPTION, "tooltipText")
            .put(ACCELERATOR_KEY, "acceleratorKey")
            .put(SMALL_ICON, "icon")
            .put(LARGE_ICON_KEY, "largeIcon")
            .put(MNEMONIC_KEY, "mnemonic")
            .put(SELECTED_KEY, "selectedKey")
            .put(DISPLAYED_MNEMONIC_INDEX_KEY, "displayMnemonicIndexKey")
            .build();
    private final PropertyChangeListener onEnabledChanged;
    protected UI ui;
    protected AbstractButton editor;
    private String name;
    private String text;
    private String tooltipText;
    private boolean enabled = true;
    private boolean selectedKey;
    private boolean addKeyStrokeToText = true;
    private boolean addMnemonicAsKeyStroke = true;
    private KeyStroke keyStroke;
    private int mnemonic;
    private int displayMnemonicIndexKey = -1;
    private Icon icon;
    private Icon largeIcon;
    private boolean checkMenuItemIsArmed = true;

    /**
     * Install this listener on menu to disable any accelerator in menu while menu is not displayed.
     *
     * @author Tony Chemit - [email protected]
     * @see JAXXObjectActionSupport#makeActionsEnabledOnlyIfMenuItemParentIsOpened(JMenuBar)
     */
    static class MakeActionsEnabledOnlyIfMenuItemParentIsOpenedPopupMenuListener implements PopupMenuListener {

        private static final Logger log = LogManager.getLogger(MakeActionsEnabledOnlyIfMenuItemParentIsOpenedPopupMenuListener.class);

        private final JMenu jMenu;
        private boolean adjusting;

        public MakeActionsEnabledOnlyIfMenuItemParentIsOpenedPopupMenuListener(JMenu jMenu) {
            this.jMenu = Objects.requireNonNull(jMenu);
        }

        @Override
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            updateElements(true);
        }

        @Override
        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            SwingUtilities.invokeLater(() -> updateElements(false));
        }

        @Override
        public void popupMenuCanceled(PopupMenuEvent e) {
            updateElements(false);
        }

        public void updateElements(boolean activateFromPopup) {
            if (adjusting) {
                return;
            }
            adjusting = true;

            try {
                String state = activateFromPopup ? "enabled" : "disabled";
                for (Component subElement : jMenu.getMenuComponents()) {
                    if (subElement instanceof JMenuItem) {
                        JMenuItem component = (JMenuItem) subElement;
                        if (component.getAccelerator() != null && component.getAction() instanceof JAXXObjectActionSupport) {
                            log.info(String.format("Menu (%s) item (%s) will %s", jMenu.getName(), component.getName(), state));
                            component.putClientProperty(ACTIVATE_FROM_POPUP, activateFromPopup);
                        }
                    }
                }
            } finally {
                adjusting = false;
            }
        }
    }

    public static void makeActionsEnabledOnlyIfMenuItemParentIsOpened(JMenuBar menuBar) {
        for (Component component : menuBar.getComponents()) {
            if (component instanceof JMenu) {
                makeActionsEnabledOnlyIfMenuItemParentIsOpened((JMenu) component);
            }
        }
    }

    protected static void makeActionsEnabledOnlyIfMenuItemParentIsOpened(JMenu menu) {
        MakeActionsEnabledOnlyIfMenuItemParentIsOpenedPopupMenuListener listener = new MakeActionsEnabledOnlyIfMenuItemParentIsOpenedPopupMenuListener(menu);
        menu.getPopupMenu().addPopupMenuListener(listener);
        for (Component component : menu.getMenuComponents()) {
            if (component instanceof JMenu) {
                makeActionsEnabledOnlyIfMenuItemParentIsOpened((JMenu) component);
            }
        }
        listener.updateElements(false);
    }

    public static  & Runnable> void run(U ui, A action) {
        action.setUi(ui);
        action.run();
    }

    public static > A init(U ui, AbstractButton editor, A action) {
        log.info(String.format("[%s] init action: %s", ui.getClass().getSimpleName(), action.getName()));
        action.setUi(ui);
        action.setEditor(editor);
        action.init();
        return action;
    }

    protected JAXXObjectActionSupport(String label, String shortDescription, String actionIcon, KeyStroke acceleratorKey) {
        this(null, label, shortDescription, actionIcon, acceleratorKey);
    }

    protected JAXXObjectActionSupport(String actionCommandKey, String label, String shortDescription, String actionIcon, KeyStroke acceleratorKey) {
        this(actionCommandKey, label, shortDescription, actionIcon);
        this.keyStroke = acceleratorKey;
    }

    protected JAXXObjectActionSupport(String label, String shortDescription, String actionIcon, char acceleratorKey) {
        this(null, label, shortDescription, actionIcon, acceleratorKey);
    }

    protected JAXXObjectActionSupport(String actionCommandKey, String label, String shortDescription, String actionIcon, char acceleratorKey) {
        this(actionCommandKey, label, shortDescription, actionIcon);
        this.mnemonic = acceleratorKey;
    }

    private JAXXObjectActionSupport(String actionCommandKey, String label, String shortDescription, String actionIcon) {
        this.name = actionCommandKey == null ? getClass().getName() : actionCommandKey;
        this.text = t(label);
        this.tooltipText = t(shortDescription);
        if (actionIcon != null) {
            icon = SwingUtil.getUIManagerActionIcon(actionIcon);
        }
        onEnabledChanged = e -> setEnabled((Boolean) e.getNewValue());
    }

    protected abstract InputMap getInputMap(UI ui, int inputMapCondition);

    protected abstract int getInputMapCondition();

    protected abstract ActionMap getActionMap(UI ui);

    public void init() {
        AbstractButton editor = getEditor();
        if (editor != null) {
            JAXXObjectActionSupport previousAction = (JAXXObjectActionSupport) editor.getClientProperty("$$JaxxAction$$");
            if (previousAction != null) {
                previousAction.removePreviousAction();
            }
            editor.putClientProperty("$$JaxxAction$$", this);
            editor.addPropertyChangeListener("enabled", onEnabledChanged);
            if (editor instanceof JMenuItem) {
                setAddKeyStrokeToText(false);
                setAddMnemonicAsKeyStroke(false);
                if (getMnemonic() != 0) {
                    setDisplayMnemonicIndexKey(0);
                    setKeyStroke(KeyStroke.getKeyStroke(getMnemonic(), 0));
                }
                if (editor instanceof JCheckBoxMenuItem) {
                    Icon largeIcon = getLargeIcon();
                    if (largeIcon != null) {
                        editor.setIcon(largeIcon);
                    }
                }
            }
            if (this instanceof MenuAction) {
                ((MenuAction) this).initUI();
            }
        }
        defaultInit(getInputMap(ui, getInputMapCondition()), getActionMap(ui));
    }

    protected abstract void doActionPerformed(ActionEvent e, UI ui);

    @Override
    public final Object getValue(String key) {
        return get(PROPERTY_MAPPING.getOrDefault(key, key));
    }

    @Override
    public final void putValue(String key, Object value) {
        set(PROPERTY_MAPPING.getOrDefault(key, key), value);
    }

    @Override
    protected Class getJavaBeanType() {
        return JAXXObjectActionSupport.class;
    }

    @Override
    public final boolean isEnabled() {
        return enabled;
    }

    @Override
    public final void setEnabled(boolean enabled) {
        boolean oldValue = isEnabled();
        this.enabled = enabled;
        log.debug(String.format("Action [%s] - enabled? %s → %s", getActionCommandKey(), oldValue, enabled));
        firePropertyChange(ENABLED, oldValue, enabled);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (canExecuteAction(e)) {
            doActionPerformed(e, ui);
        }
    }

    public boolean isCheckMenuItemIsArmed() {
        return checkMenuItemIsArmed;
    }

    public void setCheckMenuItemIsArmed(boolean checkMenuItemIsArmed) {
        this.checkMenuItemIsArmed = checkMenuItemIsArmed;
    }

    public final String getName() {
        return name;
    }

    public final void setName(String name) {
        this.name = name;
    }

    public final String getText() {
        return text;
    }

    public final void setText(String text) {
        this.text = text;
    }

    public final String getTooltipText() {
        return tooltipText;
    }

    public final void setTooltipText(String tooltipText) {
        this.tooltipText = tooltipText;
    }

    public final KeyStroke getKeyStroke() {
        return keyStroke;
    }

    public final void setKeyStroke(KeyStroke keyStroke) {
        this.keyStroke = keyStroke;
    }

    public final int getMnemonic() {
        return mnemonic;
    }

    public final void setMnemonic(int mnemonic) {
        this.mnemonic = mnemonic;
    }

    public final Icon getIcon() {
        return icon;
    }

    public final void setIcon(Icon icon) {
        this.icon = icon;
    }

    public boolean isSelectedKey() {
        return selectedKey;
    }

    public final void setSelectedKey(boolean selectedKey) {
        this.selectedKey = selectedKey;
    }

    public int getDisplayMnemonicIndexKey() {
        return displayMnemonicIndexKey;
    }

    public final void setDisplayMnemonicIndexKey(int displayMnemonicIndexKey) {
        this.displayMnemonicIndexKey = displayMnemonicIndexKey;
    }

    public boolean isAddMnemonicAsKeyStroke() {
        return addMnemonicAsKeyStroke;
    }

    public void setAddMnemonicAsKeyStroke(boolean addMnemonicAsKeyStroke) {
        this.addMnemonicAsKeyStroke = addMnemonicAsKeyStroke;
    }

    public Icon getLargeIcon() {
        return largeIcon;
    }

    public final void setLargeIcon(Icon largeIcon) {
        this.largeIcon = largeIcon;
    }

    public final KeyStroke getAcceleratorKey() {
        return keyStroke;
    }

    public final String getActionCommandKey() {
        return name;
    }

    public final UI getUi() {
        return ui;
    }

    public final void setUi(UI ui) {
        this.ui = ui;
    }

    public final AbstractButton getEditor() {
        return editor;
    }

    public void setEditor(AbstractButton editor) {
        this.editor = editor;
    }

    public boolean isAddKeyStrokeToText() {
        return addKeyStrokeToText;
    }

    public void setAddKeyStrokeToText(boolean addKeyStrokeToText) {
        this.addKeyStrokeToText = addKeyStrokeToText;
    }

    public final void register(InputMap inputMap, ActionMap actionMap) {
        if (keyStroke != null && inputMap != null && actionMap != null) {
            String actionCommandKey = getActionCommandKey();
            inputMap.put(keyStroke, actionCommandKey);
            actionMap.put(actionCommandKey, this);
        }
    }

    public final void unregister(InputMap inputMap, ActionMap actionMap) {
        if (keyStroke != null && inputMap != null && actionMap != null) {
            String actionCommandKey = getActionCommandKey();
            inputMap.remove(keyStroke);
            actionMap.remove(actionCommandKey);
        }
    }

    public void displayInfo(String title, String text) {
        JOptionPanes.displayInfo((Component) ui, title, text);
    }

    public void displayWarning(String title, String text) {
        JOptionPanes.displayWarning((Component) ui, title, text);
    }

    public int askUser(String title, String message, int typeMessage, Object[] options, int defaultOption) {
        return JOptionPanes.askUser((Component) ui, title, message, typeMessage, options, defaultOption);
    }

    public int askUser(String title, Object message, int typeMessage, Object[] options, int defaultOption) {
        return JOptionPanes.askUser((Component) ui, title, message, typeMessage, options, defaultOption);
    }

    public int askUser(JOptionPane pane, String title, Object[] options) {
        return JOptionPanes.askUser((Frame) ui, pane, title, options);
    }

    protected boolean canExecuteAction(ActionEvent e) {
        AbstractButton editor = getEditor();
        Objects.requireNonNull(editor);
        if (!editor.isVisible()) {
            log.info(String.format("Reject action : %s from editor not visible.", getName()));
            return false;
        }
        if (!editor.isEnabled()) {
            log.info(String.format("Reject action : %s from editor not enabled.", getName()));
            return false;
        }
        if (editor instanceof JMenuItem) {
            return canExecutionActionFromMenuItem((JMenuItem) editor, e);
        }
        if (!editor.isShowing()) {
            log.info(String.format("Reject action : %s from editor not showing.", getName()));
            return false;
        }
        boolean result = canExecutionActionFromLayer(getEditor(), e);
        if (result) {
            log.info(String.format("Accept action : %s", getName()));
        } else {
            log.info(String.format("Reject action : %s", getName()));
        }
        return result;
    }

    protected boolean canExecutionActionFromMenuItem(JMenuItem editor, ActionEvent ignored) {
        boolean armed = editor.isArmed();
        if (isCheckMenuItemIsArmed() && !armed) {
            log.info(String.format("Reject action : %s from editor not armed.", getName()));
            return false;
        }
        Boolean activateFromPopup = (Boolean) editor.getClientProperty(ACTIVATE_FROM_POPUP);
        if (activateFromPopup != null) {
            if (!activateFromPopup) {
                log.info(String.format("Reject action : %s from editor (popup is not visible).", getName()));
                return false;
            }
        }
        return true;
    }

    protected boolean canExecutionActionFromLayer(Component editor, ActionEvent e) {
        if (editor == null) {
            return true;
        }
        JXLayer ancestor = (JXLayer) SwingUtilities.getAncestorOfClass(JXLayer.class, editor);
        if (ancestor == null) {
            return true;
        }
        LayerUI ui = ancestor.getUI();
        if (ui instanceof BlockingLayerUI) {
            if (!((BlockingLayerUI) ui).acceptEventOrConsumeIt(e)) {
                log.info(String.format("Reject action %s from blockingUI: %s", getName(), ui));
                return false;
            }
        }
        return canExecutionActionFromLayer(ancestor, e);
    }

    protected void defaultInit(InputMap inputMap, ActionMap actionMap) {
        if (editor != null) {
            log.debug("init action " + getActionCommandKey());
            String text = editor.getText();
            if (StringUtils.isNotEmpty(text)) {
                setText(text);
            }
            String tip = editor.getToolTipText();
            if (StringUtils.isNotEmpty(tip)) {
                setTooltipText(tip);
            }
            Icon icon = editor.getIcon();
            if (icon != null) {
                setIcon(icon);
            }
            KeyStroke keyStroke = (KeyStroke) editor.getClientProperty("keyStroke");
            if (keyStroke != null) {
                this.keyStroke = keyStroke;
            }
            if (addMnemonicAsKeyStroke && keyStroke == null && mnemonic != 0) {
                setKeyStroke(KeyStroke.getKeyStroke(mnemonic, InputEvent.ALT_DOWN_MASK));
            }
            rebuildTexts(false);
            setEnabled(editor.isEnabled());
            editor.setAction(this);
        }
        register(inputMap, actionMap);
    }

    public void rebuildTexts(boolean updateAction) {
        if (addKeyStrokeToText && this.keyStroke != null) {
            if (updateAction) {
                removeKeyStrokeToText();
            }
            addKeyStrokeToText(getText(), getTooltipText());
        }
        updateEditorTexts();
    }

    protected void updateEditorTexts() {
        if (editor == null) {
            return;
        }
        editor.setText(getText());
        editor.setToolTipText(getTooltipText());
    }

    protected void removePreviousAction() {
        if (editor == null) {
            return;
        }
        InputMap inputMap = getInputMap(getUi(), getInputMapCondition());
        ActionMap actionMap = getActionMap(getUi());
        unregister(inputMap, actionMap);
        editor.removePropertyChangeListener("enabled", onEnabledChanged);
        removeKeyStrokeToText();
        rebuildTexts(true);
    }

    protected void addKeyStrokeToText(String label, String shortDescription) {
        if (keyStroke == null) {
            return;
        }
        String acceleratorStr = SwingUtil.keyStrokeToStr(keyStroke);
        setText(label == null ? null : (t(label) + acceleratorStr));
        setTooltipText(shortDescription == null ? null : (t(shortDescription) + acceleratorStr));
    }

    protected void removeKeyStrokeToText() {
        if (keyStroke == null) {
            return;
        }
        String acceleratorStr = SwingUtil.keyStrokeToStr(keyStroke);
        String text = getText();
        if (text != null && text.endsWith(acceleratorStr)) {
            setText(text.substring(0, text.length() - acceleratorStr.length()).trim());
        }
        String toolTipText = getTooltipText();
        if (toolTipText != null && toolTipText.endsWith(acceleratorStr)) {
            setTooltipText(toolTipText.substring(0, toolTipText.length() - acceleratorStr.length()).trim());
        }
    }
}