swingx-master/swingx-demos/swingx-demos-swingxset/src/main/java/org/jdesktop/swingx/appframework/StyledTextActions.java
/*
 * $Id$
 *
 * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library 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 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.appframework;

import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.ActionMap;
import javax.swing.JComponent;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.Caret;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import org.jdesktop.application.Action;
import org.jdesktop.application.ApplicationActionMap;
import org.jdesktop.application.ApplicationContext;
import org.jdesktop.application.ProxyActions;
import org.jdesktop.beans.AbstractBean;

/**
 */
public class StyledTextActions extends AbstractBean {
    private static boolean installed;
    
    private final ApplicationContext context;
    private final CaretListener textComponentCaretListener;
    private final PropertyChangeListener textComponentPCL;
    private final String markerActionKey = "StyledTextActions.markerAction";
    private final javax.swing.Action markerAction;
    
    private boolean boldEnabled = false;
    private boolean boldSelected = false;
    private boolean italicEnabled = false;
    private boolean italicSelected = false;
    private boolean underlineEnabled = false;
    private boolean underlineSelected = false;

    //TODO justification actions
//    private boolean leftJustifyEnabled = false;
//    private boolean centerJustifyEnabled = false;
//    private boolean rightJustifyEnabled = false;
//    private boolean fullJustifyEnabled = false;
    
    @SuppressWarnings("serial")
    StyledTextActions(ApplicationContext context) {
        this.context = context;
        markerAction = new javax.swing.AbstractAction() {
            public void actionPerformed(ActionEvent e) {
            }
        };
        textComponentCaretListener = new TextComponentCaretListener();
        textComponentPCL = new TextComponentPCL();
        
        installProxyActions();
    }

    public static synchronized void install(final ApplicationContext context) {
        //TODO this should be one per context
        if (!installed) {
            context.addPropertyChangeListener("focusOwner", new PropertyChangeListener() {
                private StyledTextActions textActions = new StyledTextActions(context);

                public void propertyChange(PropertyChangeEvent evt) {
                    textActions.updateFocusOwner((JComponent) evt.getOldValue(),
                            (JComponent) evt.getNewValue());
                }

            });
            
            installed = true;
        }
    }
    
    private ApplicationContext getContext() {
        return context;
    }

    /* Called by the KeyboardFocus PropertyChangeListener in ApplicationContext,
     * before any other focus-change related work is done.
     */
    public void updateFocusOwner(JComponent oldOwner, JComponent newOwner) {
        if (oldOwner instanceof JTextComponent) {
            JTextComponent text = (JTextComponent) oldOwner;
            text.removeCaretListener(textComponentCaretListener);
            text.removePropertyChangeListener(textComponentPCL);
        }
        if (newOwner instanceof JTextComponent) {
            JTextComponent text = (JTextComponent) newOwner;
            maybeInstallTextActions(text);
            updateTextActions(text);
            text.addCaretListener(textComponentCaretListener);
            text.addPropertyChangeListener(textComponentPCL);
        } else if (newOwner == null) {
            setBoldEnabled(false);
            setBoldSelected(false);
        }
    }

    private final class TextComponentCaretListener implements CaretListener {
        public void caretUpdate(CaretEvent e) {
            updateTextActions((JTextComponent) (e.getSource()));
        }
    }

    private final class TextComponentPCL implements PropertyChangeListener {
        public void propertyChange(PropertyChangeEvent e) {
            String propertyName = e.getPropertyName();
            if ((propertyName == null) || "editable".equals(propertyName)) {
                updateTextActions((JTextComponent) (e.getSource()));
            }
        }
    }
    
    @ProxyActions({"bold", "italic", "underline"})
    public static final class ProxyActionsContainer {
        //exists to provide proxy action cover for the actions in the outer class
    }

    //TODO hack around loosing actions from map
    private ApplicationActionMap main;
    
    private void installProxyActions() {
        ProxyActionsContainer pac = new ProxyActionsContainer();
        ApplicationActionMap aam = getContext().getActionMap(pac);
        main = getContext().getActionMap();
        
        for (Object key : aam.allKeys()) {
            main.put(key, aam.get(key));
        }
    }
    
    private void updateTextActions(JTextComponent text) {
        if (text.getDocument() instanceof StyledDocument) {
            StyledDocument document = (StyledDocument) text.getDocument();
            boolean editable = text.isEditable();
            
            Caret caret = text.getCaret();
            int dot = caret.getDot();
            dot = dot > 0 ? dot - 1 : dot;
            Element elem = document.getCharacterElement(dot);
            AttributeSet set = elem.getAttributes();
            
            setBoldEnabled(editable);
            setBoldSelected(StyleConstants.isBold(set));
            setItalicEnabled(editable);
            setItalicSelected(StyleConstants.isItalic(set));
            setUnderlineEnabled(editable);
            setUnderlineSelected(StyleConstants.isUnderline(set));
        } else {
            setBoldEnabled(false);
            setBoldSelected(false);
            setItalicEnabled(false);
            setItalicSelected(false);
            setUnderlineEnabled(false);
            setUnderlineSelected(false);
        }
        
    }

    // TBD: what if text.getActionMap is null, or if it's parent isn't the UI-installed actionMap
    private void maybeInstallTextActions(JTextComponent text) {
        ActionMap actionMap = text.getActionMap();
        if (actionMap.get(markerActionKey) == null) {
            actionMap.put(markerActionKey, markerAction);
            ActionMap textActions = getContext().getActionMap(getClass(), this);
            for (Object key : textActions.keys()) {
                actionMap.put(key, textActions.get(key));
            }
        }
    }

    /* This method lifted from JTextComponent.java 
     */
    private int getCurrentEventModifiers() {
        int modifiers = 0;
        AWTEvent currentEvent = EventQueue.getCurrentEvent();
        if (currentEvent instanceof InputEvent) {
            modifiers = ((InputEvent) currentEvent).getModifiers();
        } else if (currentEvent instanceof ActionEvent) {
            modifiers = ((ActionEvent) currentEvent).getModifiers();
        }
        return modifiers;
    }

    private void invokeTextAction(JTextComponent text, String actionName) {
        ActionMap actionMap = text.getActionMap().getParent();
        long eventTime = EventQueue.getMostRecentEventTime();
        int eventMods = getCurrentEventModifiers();
        ActionEvent actionEvent =
                new ActionEvent(text, ActionEvent.ACTION_PERFORMED, actionName, eventTime, eventMods);
        actionMap.get(actionName).actionPerformed(actionEvent);
    }

    /**
     * @return the boldEnabled
     */
    public boolean isBoldEnabled() {
        return boldEnabled;
    }

    /**
     * @param boldEnabled the boldEnabled to set
     */
    public void setBoldEnabled(boolean boldEnabled) {
        boolean oldValue = isBoldEnabled();
        this.boldEnabled = boldEnabled;
        firePropertyChange("boldEnabled", oldValue, isBoldEnabled());
    }

    /**
     * @return the boldSelected
     */
    public boolean isBoldSelected() {
        return boldSelected;
    }

    /**
     * @param boldSelected the boldSelected to set
     */
    public void setBoldSelected(boolean boldSelected) {
        boolean oldValue = isBoldSelected();
        this.boldSelected = boldSelected;
        firePropertyChange("boldSelected", oldValue, isBoldSelected());
    }

    @Action(enabledProperty = "boldEnabled", selectedProperty = "boldSelected")
    public void bold(ActionEvent e) {
        Object src = e.getSource();
        if (src instanceof JTextComponent) {
            invokeTextAction((JTextComponent) src, "font-bold");
        }
    }
    
    /**
     * @return the boldEnabled
     */
    public boolean isItalicEnabled() {
        return italicEnabled;
    }
    
    /**
     * @param italicEnabled the boldEnabled to set
     */
    public void setItalicEnabled(boolean italicEnabled) {
        boolean oldValue = isItalicEnabled();
        this.italicEnabled = italicEnabled;
        firePropertyChange("italicEnabled", oldValue, isItalicEnabled());
    }
    
    /**
     * @return the boldSelected
     */
    public boolean isItalicSelected() {
        return italicSelected;
    }
    
    /**
     * @param italicSelected the boldSelected to set
     */
    public void setItalicSelected(boolean italicSelected) {
        boolean oldValue = isItalicSelected();
        this.italicSelected = italicSelected;
        firePropertyChange("italicSelected", oldValue, isItalicSelected());
    }
    
    @Action(enabledProperty = "italicEnabled", selectedProperty = "italicSelected")
    public void italic(ActionEvent e) {
        Object src = e.getSource();
        if (src instanceof JTextComponent) {
            invokeTextAction((JTextComponent) src, "font-italic");
        }
    }
    
    /**
     * @return the underlineEnabled
     */
    public boolean isUnderlineEnabled() {
        return underlineEnabled;
    }
    
    /**
     * @param underlineEnabled the underlineEnabled to set
     */
    public void setUnderlineEnabled(boolean underlineEnabled) {
        boolean oldValue = isUnderlineEnabled();
        this.underlineEnabled = underlineEnabled;
        firePropertyChange("underlineEnabled", oldValue, isUnderlineEnabled());
    }
    
    /**
     * @return the underlineSelected
     */
    public boolean isUnderlineSelected() {
        return underlineSelected;
    }
    
    /**
     * @param underlineSelected the underlineSelected to set
     */
    public void setUnderlineSelected(boolean underlineSelected) {
        boolean oldValue = isUnderlineSelected();
        this.underlineSelected = underlineSelected;
        firePropertyChange("underlineSelected", oldValue, isUnderlineSelected());
    }
    
    @Action(enabledProperty = "underlineEnabled", selectedProperty = "underlineSelected")
    public void underline(ActionEvent e) {
        Object src = e.getSource();
        if (src instanceof JTextComponent) {
            invokeTextAction((JTextComponent) src, "font-underline");
        }
    }
}
