package org.freeplane.core.ui.ribbon; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JMenuItem; import javax.swing.JSeparator; import javax.swing.KeyStroke; import javax.swing.event.TreeSelectionEvent; import org.freeplane.core.resources.ResourceController; import org.freeplane.core.ui.AFreeplaneAction; import org.freeplane.core.ui.AccelerateableAction; import org.freeplane.core.ui.IAcceleratorChangeListener; import org.freeplane.core.ui.ribbon.RibbonSeparatorContributorFactory.RibbonSeparator; import org.freeplane.core.ui.ribbon.StructureTree.StructurePath; import org.freeplane.core.ui.ribbon.event.AboutToPerformEvent; import org.freeplane.core.util.Compat; import org.freeplane.core.util.LogUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.mode.Controller; import org.pushingpixels.flamingo.api.common.AbstractCommandButton; import org.pushingpixels.flamingo.api.common.JCommandButton; import org.pushingpixels.flamingo.api.common.JCommandButton.CommandButtonKind; import org.pushingpixels.flamingo.api.common.JCommandMenuButton; import org.pushingpixels.flamingo.api.common.JCommandToggleButton; import org.pushingpixels.flamingo.api.common.JCommandToggleMenuButton; import org.pushingpixels.flamingo.api.common.RichTooltip; import org.pushingpixels.flamingo.api.common.icon.ImageWrapperResizableIcon; import org.pushingpixels.flamingo.api.common.icon.ResizableIcon; import org.pushingpixels.flamingo.api.common.popup.JCommandPopupMenu; import org.pushingpixels.flamingo.api.common.popup.JPopupPanel; import org.pushingpixels.flamingo.api.common.popup.PopupPanelCallback; import org.pushingpixels.flamingo.api.ribbon.RibbonElementPriority; public class RibbonActionContributorFactory implements IRibbonContributorFactory { public static final String ACTION_KEY_PROPERTY = "ACTION_KEY"; public static final String ACTION_NAME_PROPERTY = "ACTION_NAME"; public static final String ACTION_CHANGE_LISTENER = "ACTION_CHANGE_LISTENER"; public static final String MANDATORY_PROPERTY = "MANDATORY"; public static ResizableIcon BLANK_ACTION_ICON; static { URL location = ResourceController.getResourceController().getResource("/images/blank_icon_48x48.png"); if (location != null) { BLANK_ACTION_ICON = ImageWrapperResizableIcon.getIcon(location, new Dimension(48, 48)); } } private final RibbonBuilder builder; private ActionAcceleratorChangeListener changeListener; /*********************************************************************************** * CONSTRUCTORS **********************************************************************************/ public RibbonActionContributorFactory(RibbonBuilder builder) { this.builder = builder; builder.getAcceleratorManager().addAcceleratorChangeListener(getAccelChangeListener()); } /*********************************************************************************** * METHODS **********************************************************************************/ public static RibbonElementPriority getPriority(String attr) { RibbonElementPriority prio = RibbonElementPriority.MEDIUM; if("top".equals(attr.trim().toLowerCase())) { prio = RibbonElementPriority.TOP; } else if("low".equals(attr.trim().toLowerCase())) { prio = RibbonElementPriority.LOW; } return prio; } public static JButton createButton(final AFreeplaneAction action) { String title = getActionTitle(action); ResizableIcon icon = getActionIcon(action); final JButton button = new JButton(title, icon); // updateRichTooltip(button, action, null); button.addActionListener(new RibbonActionListener(action)); button.setFocusable(false); return button; } public static JCommandButton createCommandButton(final AFreeplaneAction action) { String title = getActionTitle(action); ResizableIcon icon = getActionIcon(action); final JCommandButton button = new JCommandButton(title, icon); updateRichTooltip(button, action, null); button.addActionListener(new RibbonActionListener(action)); button.setFocusable(false); return button; } public static JCommandToggleButton createCommandToggleButton(final AFreeplaneAction action) { String title = getActionTitle(action); ResizableIcon icon = getActionIcon(action); final JCommandToggleButton button = new JCommandToggleButton(title, icon); updateRichTooltip(button, action, null); button.addActionListener(new RibbonActionListener(action)); button.setFocusable(false); return button; } public static JCommandMenuButton createCommandMenuButton(final AFreeplaneAction action) { String title = getActionTitle(action); ResizableIcon icon = getActionIcon(action); final JCommandMenuButton button = new JCommandMenuButton(title, icon); updateRichTooltip(button, action, null); button.addActionListener(new RibbonActionListener(action)); button.setFocusable(false); return button; } public static JCommandToggleMenuButton createCommandToggleMenuButton(final AFreeplaneAction action) { String title = getActionTitle(action); ResizableIcon icon = getActionIcon(action); final JCommandToggleMenuButton button = new JCommandToggleMenuButton(title, icon); updateRichTooltip(button, action, null); button.addActionListener(new RibbonActionListener(action)); button.setFocusable(false); return button; } public static void updateRichTooltip(final AbstractCommandButton button, AFreeplaneAction action, KeyStroke ks) { RichTooltip tip = null; final String tooltip = TextUtils.getRawText(action.getTooltipKey(), null); if (tooltip != null && !"".equals(tooltip)) { tip = new RichTooltip(getActionTitle(action), TextUtils.removeTranslateComment(tooltip)); } if(ks != null) { if(tip == null) { tip = new RichTooltip(getActionTitle(action), " "); } tip.addFooterSection(formatShortcut(ks)); } if(tip != null) { button.setActionRichTooltip(tip); } else { button.setActionRichTooltip(null); } } public static String formatShortcut(KeyStroke ks) { StringBuilder sb = new StringBuilder(); if(ks != null) { String[] st = ks.toString().split("[\\s]+"); for (String s : st) { if("pressed".equals(s.trim())) { continue; } if(sb.length() > 0) { sb.append(" + "); } sb.append(s.substring(0, 1).toUpperCase()); sb.append(s.substring(1)); } } return sb.toString(); } public static ResizableIcon getActionIcon(final AFreeplaneAction action) { ResizableIcon icon = null; ImageIcon ico = (ImageIcon) action.getValue(Action.SMALL_ICON); if(ico != null) { icon = ImageWrapperResizableIcon.getIcon(ico.getImage(), new Dimension(ico.getIconWidth(), ico.getIconHeight())); } else { String resource = ResourceController.getResourceController().getProperty(action.getIconKey(), null); if (resource != null) { URL location = ResourceController.getResourceController().getResource(resource); if (location != null) { icon = ImageWrapperResizableIcon.getIcon(location, new Dimension(16, 16)); } } } if(icon == null) { icon = BLANK_ACTION_ICON; } return icon; } public static String getActionTitle(final AFreeplaneAction action) { String title = (String)action.getValue(Action.NAME); if(title == null || title.isEmpty()) { title = TextUtils.getText(action.getTextKey()); } if(title == null || title.isEmpty()) { title = action.getTextKey(); } return TextUtils.removeTranslateComment(title); } public static AFreeplaneAction getDummyAction(final String key) { return new AFreeplaneAction(/*"ribbon.action."+*/key) { private static final long serialVersionUID = -5405032373977903024L; public String getTextKey() { return getKey(); } public void actionPerformed(ActionEvent e) { //RIBBONS - do nothing } }; } protected ActionAcceleratorChangeListener getAccelChangeListener() { if(changeListener == null) { changeListener = new ActionAcceleratorChangeListener(); } return changeListener; } public static void updateActionState(AFreeplaneAction action, AbstractCommandButton button) { if(AFreeplaneAction.checkEnabledOnChange(action)) { action.setEnabled(); button.setEnabled(action.isEnabled()); } if(isSelectionListener(action)) { action.setSelected(); button.getActionModel().setSelected(action.isSelected()); } } public static boolean isSelectionListener(AFreeplaneAction action) { return AFreeplaneAction.checkSelectionOnChange(action) || AFreeplaneAction.checkSelectionOnPopup(action) || AFreeplaneAction.checkSelectionOnPropertyChange(action); } /*********************************************************************************** * REQUIRED METHODS FOR INTERFACES **********************************************************************************/ public ARibbonContributor getContributor(final Properties attributes) { final String actionKey = attributes.getProperty("action"); if(actionKey != null) { String accel = attributes.getProperty("accelerator", null); if (accel != null) { if (Compat.isMacOsX()) { accel = accel.replaceFirst("CONTROL", "META").replaceFirst("control", "meta"); } builder.getAcceleratorManager().setDefaultAccelerator(actionKey, accel); } } return new ARibbonContributor() { private List<Component> childButtons = new ArrayList<Component>(); public String getKey() { String key = attributes.getProperty("action"); if(key == null) { key = attributes.getProperty("name"); } return key; } public void contribute(RibbonBuildContext context, ARibbonContributor parent) { final String actionKey = attributes.getProperty("action"); final boolean mandatory = Boolean.parseBoolean(attributes.getProperty("mandatory", "false").toLowerCase()); ChildProperties childProps = new ChildProperties(parseOrderSettings(attributes.getProperty("orderPriority", ""))); childProps.set(RibbonElementPriority.class, getPriority(attributes.getProperty("priority", "medium"))); if(actionKey != null) { AFreeplaneAction action = context.getBuilder().getMode().getAction(actionKey); if(action != null) { if(mandatory) { action.putValue(MANDATORY_PROPERTY, mandatory); } AbstractCommandButton button; if(isSelectionListener(action)) { button = createCommandToggleButton(action); if (context.hasChildren(context.getCurrentPath())) { LogUtils.severe("RibbonActionContributorFactory.getContributor(): can't add popup menu to toggle button for action: "+context.getCurrentPath().toString()); } } else { button = createCommandButton(action); if(context.hasChildren(context.getCurrentPath())) { StructurePath path = context.getCurrentPath(); ((JCommandButton)button).setPopupCallback(getPopupPanelCallBack(path, context)); ((JCommandButton)button).setCommandButtonKind(CommandButtonKind.ACTION_AND_POPUP_MAIN_ACTION); KeyStroke ks = context.getBuilder().getAcceleratorManager().getAccelerator(actionKey); updateRichTooltip(button, action, ks); updateActionState(action, button); } } button.putClientProperty(ACTION_KEY_PROPERTY, action); KeyStroke ks = context.getBuilder().getAcceleratorManager().getAccelerator(actionKey); if(ks != null) { updateRichTooltip(button, action, ks); } getAccelChangeListener().addAction(actionKey, button); builder.getMapChangeAdapter().addListener(new ActionChangeListener(action, button)); parent.addChild(button, childProps); } } else { final String name = attributes.getProperty("name"); if(name != null) { AFreeplaneAction action = getDummyAction(name); final JCommandButton button = new JCommandButton(getActionTitle(action), getActionIcon(action)); button.putClientProperty(ACTION_NAME_PROPERTY, action); updateRichTooltip(button, action, null); if(context.hasChildren(context.getCurrentPath())) { StructurePath path = context.getCurrentPath(); button.setPopupCallback(getPopupPanelCallBack(path, context)); button.setCommandButtonKind(CommandButtonKind.POPUP_ONLY); } button.setFocusable(false); parent.addChild(button, childProps); } } } private PopupPanelCallback getPopupPanelCallBack(StructurePath path, final RibbonBuildContext context) { childButtons.clear(); context.processChildren(path, this); return new PopupPanelCallback() { public JPopupPanel getPopupPanel(JCommandButton commandButton) { JCommandPopupMenu popupmenu = new JCommandPopupMenu(); for (Component comp : childButtons) { if(comp instanceof JSeparator) { popupmenu.addMenuSeparator(); } else if(comp instanceof AbstractCommandButton) { AbstractCommandButton button = (AbstractCommandButton) comp; AbstractCommandButton menuButton = null; AFreeplaneAction action = (AFreeplaneAction)button.getClientProperty(ACTION_KEY_PROPERTY); if(action != null) { if(isSelectionListener(action)) { menuButton = createCommandToggleMenuButton(action); popupmenu.addMenuButton((JCommandToggleMenuButton) menuButton); } else { menuButton = createCommandMenuButton(action); popupmenu.addMenuButton((JCommandMenuButton) menuButton); } menuButton.setEnabled(button.isEnabled()); menuButton.putClientProperty(ACTION_KEY_PROPERTY, action); KeyStroke ks = context.getBuilder().getAcceleratorManager().getAccelerator(action.getKey()); updateRichTooltip(menuButton, action, ks); updateActionState(action, menuButton); } else { action = (AFreeplaneAction)button.getClientProperty(ACTION_NAME_PROPERTY); menuButton = createCommandMenuButton(action); if(action != null) { menuButton.putClientProperty(ACTION_NAME_PROPERTY, action); updateRichTooltip(menuButton, action, null); } } if(button instanceof JCommandButton) { if(((JCommandButton) button).getPopupCallback() != null) { ((JCommandMenuButton)menuButton).setPopupCallback(((JCommandButton) button).getPopupCallback()); ((JCommandMenuButton)menuButton).setCommandButtonKind(((JCommandButton) button).getCommandButtonKind()); } } //clear all RibbonActionListeners from the menuButton for (ActionListener listener : menuButton.getListeners(ActionListener.class)) { if(listener instanceof RibbonActionListener) { menuButton.removeActionListener(listener); } } //add for (ActionListener listener : button.getListeners(ActionListener.class)) { if(listener instanceof RibbonActionListener) { menuButton.addActionListener(listener); } } } } return popupmenu; } }; } public void addChild(Object child, ChildProperties properties) { if(child instanceof AbstractCommandButton) { childButtons.add((AbstractCommandButton) child); Object obj = ((AbstractCommandButton) child).getClientProperty(ACTION_KEY_PROPERTY); if(obj != null) { try { builder.getMapChangeAdapter().removeListener((IChangeObserver) ((AFreeplaneAction) obj).getValue(ACTION_CHANGE_LISTENER)); getAccelChangeListener().removeAction(((AFreeplaneAction) obj).getKey()); } catch(Exception e) { LogUtils.info("RibbonActionContributorFactory.getContributor(...).new ARibbonContributor() {...}.addChild(): "+e.getMessage()); } } } if(child instanceof RibbonSeparator) { childButtons.add(new JSeparator(JSeparator.HORIZONTAL)); } } }; } /*********************************************************************************** * NESTED TYPE DECLARATIONS **********************************************************************************/ public static class RibbonActionListener implements ActionListener { private final String key; private final RibbonBuilder builder; protected RibbonActionListener(AFreeplaneAction action) { this.key = action.getKey(); this.builder = Controller.getCurrentModeController().getUserInputListenerFactory().getRibbonBuilder(); } public void actionPerformed(ActionEvent e) { AFreeplaneAction action = Controller.getCurrentModeController().getAction(key); if(action == null || linkAccelerator(action, e)) { return; } if ((0 != (e.getModifiers() & ActionEvent.CTRL_MASK))) { builder.getAcceleratorManager().newAccelerator(action, null); return; } builder.getRibbonActionEventHandler().fireAboutToPerformEvent(new AboutToPerformEvent(action)); action.actionPerformed(e); } private boolean linkAccelerator(AFreeplaneAction action, ActionEvent e) { final boolean newAcceleratorOnNextClickEnabled = AccelerateableAction.isNewAcceleratorOnNextClickEnabled(); if (newAcceleratorOnNextClickEnabled) { AccelerateableAction.getAcceleratorOnNextClickActionDialog().setVisible(false); } final Object source = e.getSource(); if ((newAcceleratorOnNextClickEnabled || 0 != (e.getModifiers() & ActionEvent.CTRL_MASK)) && source instanceof AbstractCommandButton) { builder.getAcceleratorManager().newAccelerator(action, AccelerateableAction.getAcceleratorForNextClick()); return true; } return false; } } public static class ActionAcceleratorChangeListener implements IAcceleratorChangeListener { private final Map<String, AbstractCommandButton> actionMap = new HashMap<String, AbstractCommandButton>(); /*********************************************************************************** * CONSTRUCTORS **********************************************************************************/ /*********************************************************************************** * METHODS **********************************************************************************/ public void addAction(String actionKey, AbstractCommandButton button) { actionMap.put(actionKey, button); } public void removeAction(String actionKey) { actionMap.remove(actionKey); } public void clear() { actionMap.clear(); } /*********************************************************************************** * REQUIRED METHODS FOR INTERFACES **********************************************************************************/ public void acceleratorChanged(JMenuItem action, KeyStroke oldStroke, KeyStroke newStroke) { } public void acceleratorChanged(AFreeplaneAction action, KeyStroke oldStroke, KeyStroke newStroke) { AbstractCommandButton button = actionMap.get(action.getKey()); if(button != null) { updateRichTooltip(button, action, newStroke); } } } public static class ActionChangeListener implements IChangeObserver { private final AFreeplaneAction action; private final AbstractCommandButton button; /*********************************************************************************** * CONSTRUCTORS **********************************************************************************/ public ActionChangeListener(AFreeplaneAction action, AbstractCommandButton button) { if(button == null || action == null) { throw new IllegalArgumentException("NULL"); } this.action = action; this.button = button; action.putValue(ACTION_CHANGE_LISTENER, this); } /*********************************************************************************** * METHODS **********************************************************************************/ public void updateState(CurrentState state) { if(state.isNodeChangeEvent()) { updateActionState(action, button); } else if(state.allMapsClosed()) { if (action.getValue(MANDATORY_PROPERTY) == null) { action.setEnabled(false); button.setEnabled(false); } } else if (state.get(TreeSelectionEvent.class) == null) { action.setEnabled(true); button.setEnabled(true); } } }; }