/**
* OrbisGIS is a java GIS application dedicated to research in GIScience.
* OrbisGIS is developed by the GIS group of the DECIDE team of the
* Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
*
* The GIS group of the DECIDE team is located at :
*
* Laboratoire Lab-STICC – CNRS UMR 6285
* Equipe DECIDE
* UNIVERSITÉ DE BRETAGNE-SUD
* Institut Universitaire de Technologie de Vannes
* 8, Rue Montaigne - BP 561 56017 Vannes Cedex
*
* OrbisGIS is distributed under GPL 3 license.
*
* Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
* Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285)
*
* This file is part of OrbisGIS.
*
* OrbisGIS is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* OrbisGIS 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 Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
*
* For more information, please consult: <http://www.orbisgis.org/>
* or contact directly:
* info_at_ orbisgis.org
*/
package org.orbisgis.sif.components.actions;
import org.orbisgis.commons.events.BeanPropertyChangeSupport;
import org.orbisgis.sif.components.CustomButton;
import org.orbisgis.sif.components.actions.intern.RemoveActionControls;
import org.orbisgis.sif.components.button.DropDownButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.ComponentInputMap;
import javax.swing.DefaultButtonModel;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JSeparator;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.MenuElement;
import java.awt.Component;
import java.awt.Container;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Provide a way to expose actions through multiple controls.
* - Add/Remove actions at any time, but insert action group before.
* - Register/UnRegister controls at any time
* @author Nicolas Fortin
*/
public class ActionCommands extends BeanPropertyChangeSupport implements ActionsHolder {
private static final Logger LOGGER = LoggerFactory.getLogger(ActionCommands.class);
// Actions
private List<Action> actions = new ArrayList<Action>();
// Menu containers
private List<JComponent> containers = new ArrayList<JComponent>();
private Map<ActionFactoryService, MenuTrackerAction> actionFromFactory = new HashMap<>();
/**
* Actions will be inserted in the registered tool bar.
* @param toolBar JToolBar instance
*/
public void registerContainer(JToolBar toolBar) {
containers.add(toolBar);
applyActionsOnMenuContainer(toolBar,
actions.toArray(new Action[actions.size()]),true);
}
public void registerContainer(JPopupMenu menu) {
containers.add(menu);
applyActionsOnMenuContainer(menu,
actions.toArray(new Action[actions.size()]),true);
}
@Override
public <TargetComponent> void addActionFactory(ActionFactoryService<TargetComponent> factory, TargetComponent
targetComponent) throws IllegalArgumentException {
if(!actionFromFactory.containsKey(factory)) {
MenuTrackerAction<TargetComponent> menuTrackerAction =
new MenuTrackerAction<>(factory, factory.createActions(targetComponent), targetComponent);
addActions(menuTrackerAction.getActions());
actionFromFactory.put(factory, menuTrackerAction);
} else {
// Developer exception
throw new IllegalArgumentException("ActionFactoryService instance is already pushed");
}
}
@Override
public <TargetComponent> void removeActionFactory(ActionFactoryService<TargetComponent> actionFactoryService) {
MenuTrackerAction<TargetComponent> trackerAction = actionFromFactory.get(actionFactoryService);
if(trackerAction != null) {
removeActions(trackerAction.getActions());
actionFactoryService.disposeActions(trackerAction.getTargetComponent(), trackerAction.getActions());
actionFromFactory.remove(actionFactoryService);
}
}
/**
* Copy only enabled actions to the provided popup menu.
* Removed actions are not removed from this menu.
* This function is useful for temporary Popup menu.
* @param menu
*/
public void copyEnabledActions(JPopupMenu menu) {
applyActionsOnMenuContainer(menu,
getEnabledActions(),false);
}
private Action[] getEnabledActions() {
List<Action> enabledActions = new ArrayList<Action>(actions.size());
for(Action action : actions) {
if(action.isEnabled()) {
enabledActions.add(action);
}
}
return enabledActions.toArray(new Action[enabledActions.size()]);
}
/**
*
* @param menuBar JMenuBar instance
*/
public void registerContainer(JMenuBar menuBar) {
containers.add(menuBar);
applyActionsOnMenuContainer(menuBar,
actions.toArray(new Action[actions.size()]),true);
}
/**
* Remove a linked container.
* @param component JMenuBar,JPopupMenu or JToolBar instance.
* @return true is found and removed
*/
public boolean unregisterContainer(JComponent component) {
//Remove property change
removePropertyChangeListeners(component);
return containers.remove(component);
}
/**
* Remove the reference of Action to the container. Removed action will not remove components in container.
* @param container JMenuBar,JPopupMenu or JToolBar instance.
*/
private void removePropertyChangeListeners(Container container) {
Component[] components = getSubElements(container);
for(Component component : components) {
Action action = getAction(component);
if(action instanceof AbstractAction) {
AbstractAction act = (AbstractAction)action;
List<PropertyChangeListener> copyOfListenerList = Arrays.asList(act.getPropertyChangeListeners());
for(PropertyChangeListener listener : copyOfListenerList) {
if(listener instanceof RemoveActionControls) {
RemoveActionControls removeListener = (RemoveActionControls)listener;
if(removeListener.getContainer().equals(container)) {
action.removePropertyChangeListener(listener);
}
}
}
}
if(component instanceof Container) {
removePropertyChangeListeners((Container)component);
}
}
}
private Action getAction(Component component) {
if(component instanceof AbstractButton) {
return ((AbstractButton) component).getAction();
}
// Action cannot be retrieved from this container
return null;
}
@Override
public void addAction(Action action) {
if (!actions.contains(action)) {
actions.add(action);
applyActionsOnAllControls(new Action[]{action});
propertyChangeSupport.fireIndexedPropertyChange(PROP_ACTIONS,actions.size()-1,null,action);
}
}
@Override
public void addActions(List<Action> newActions) {
List<Action> oldActionList = new ArrayList<Action>(actions);
for(Action action : newActions) {
if (!actions.contains(action)) {
actions.add(action);
}
}
applyActionsOnAllControls(newActions.toArray(new Action[newActions.size()]));
propertyChangeSupport.firePropertyChange(PROP_ACTIONS,oldActionList,actions);
}
@Override
public boolean removeAction(Action action) {
action.putValue(RemoveActionControls.DELETED_PROPERTY, true);
int index = actions.indexOf(action);
if(actions.remove(action)) {
propertyChangeSupport.fireIndexedPropertyChange(PROP_ACTIONS,index,action,null);
return true;
} else {
return false;
}
}
@Override
public void removeActions(List<Action> actionList) {
List<Action> oldActionList = new ArrayList<Action>(actions);
// Update property, removal listeners may use it to remove the action's components.
for(Action action : actionList) {
action.putValue(RemoveActionControls.DELETED_PROPERTY, true);
}
actions.removeAll(actionList);
propertyChangeSupport.firePropertyChange(PROP_ACTIONS,oldActionList,actions);
}
/**
* Search the menu item by its action id in provided menu items and sub-menu recursively.
* @param actionId #MENU_ID Action identifier
* @param menuItems Collection of menu elements
* @return Found menu with the same action id or null.
*/
public MenuElement getActionMenu(String actionId, MenuElement[] menuItems) {
for(MenuElement menu : menuItems) {
if(menu instanceof JMenuItem) {
JMenuItem menuItem = (JMenuItem)menu;
Action action = menuItem.getAction();
if(action!=null) {
if(ActionTools.getMenuId(action).equals(actionId)) {
return menu;
}
}
MenuElement subMenu = getActionMenu(actionId,menu.getSubElements());
if(subMenu!=null) {
return subMenu;
}
}
}
return null;
}
/**
* Get the managed actions.
* @return Unmodifiable list of actions.
*/
public List<Action> getActions() {
return Collections.unmodifiableList(actions);
}
private void applyActionsOnAllControls(Action[] actionsAr) {
for(JComponent component : containers) {
applyActionsOnMenuContainer(component, actionsAr,true);
}
}
/**
* Extract sub elements that should contains actions.
* @param container
* @return
*/
private Component[] getSubElements(Container container) {
if(container instanceof JMenu) {
return ((JMenu)container).getMenuComponents();
} else {
return container.getComponents();
}
}
private void feedMap(Container container, Map<String,Container> subContainers,Map<String,ButtonGroup> buttonGroups) {
Component[] subElements = getSubElements(container);
for(Component menuEl : subElements) {
if(menuEl instanceof AbstractButton) {
AbstractButton menu = (AbstractButton)menuEl;
Action menuAction = menu.getAction();
if(menuAction!=null) {
String menuId = ActionTools.getMenuId(menuAction);
if(!menuId.isEmpty()) {
subContainers.put(menuId, menu);
}
String buttonGroup = ActionTools.getToggleGroup(menuAction);
if(!buttonGroup.isEmpty() && !buttonGroups.containsKey(buttonGroup)) {
//New button group
ButtonModel bm = menu.getModel();
if(bm instanceof DefaultButtonModel) {
buttonGroups.put(buttonGroup,((DefaultButtonModel) bm).getGroup());
}
}
}
}
if(menuEl instanceof DropDownButton) {
DropDownButton button = (DropDownButton)menuEl;
if(button.getComponentPopupMenu()==null) {
button.setComponentPopupMenu(new JPopupMenu());
}
feedMap(button.getComponentPopupMenu(),subContainers,buttonGroups);
} else if(menuEl instanceof Container) {
feedMap((Container)menuEl,subContainers,buttonGroups);
}
}
}
/**
* Add provided actions to rootMenu.
* Convert Actions in swing controls.
* @param rootMenu JPopupMenu, JToolBar or JMenuBar
* @param actionsAr Action items to convert.
* @param addRemoveListener If true, a listener is inserted in action that
* contain a reference to container is inserted.
* This listener remove the swing component if the Action is set as removed.
*/
private void applyActionsOnMenuContainer(JComponent rootMenu, Action[] actionsAr,boolean addRemoveListener) {
// Map of Parent->Menu
Map<String,Container> subContainers = new HashMap<String,Container>();
// Map of TOGGLE_GROUP -> ButtonGroup instance
Map<String,ButtonGroup> buttonGroups = new HashMap<String, ButtonGroup>();
subContainers.put("",rootMenu);
// Add existing menu in map
feedMap(rootMenu, subContainers,buttonGroups);
// Insert new menu groups in map
for(Action action : actionsAr) {
if(ActionTools.isMenu(action)) {
subContainers.put(ActionTools.getMenuId(action),
new TemporaryContainer(action));
}
}
// Insert
for(Action action : actionsAr) {
// Fetch parent menu of this action
String parentId = ActionTools.getParentMenuId(action);
Container parent;
if(parentId.isEmpty()) {
parent = rootMenu;
} else {
parent = subContainers.get(parentId);
if(parent==null) { //Orphan action
LOGGER.warn("Menu action ("+action+") parent '"+parentId+"' does not exists.");
} else {
if(parent instanceof TemporaryContainer) {
parent = convertContainer((TemporaryContainer)parent,subContainers);
}
}
}
if(parent!=null) {
Component child;
// Creation of Child items
// Child item class depends on action properties and parent class.
if(ActionTools.isMenu(action)) {
child = subContainers.get(ActionTools.getMenuId(action));
if(child instanceof TemporaryContainer) {
child = convertContainer((TemporaryContainer)child,subContainers);
}
} else {
String buttonGroup = ActionTools.getToggleGroup(action);
if(!(parent instanceof JToolBar)) {
if(buttonGroup.isEmpty()) {
child = new JMenuItem(action);
} else {
JRadioButtonMenuItem radioMenu = new JRadioButtonMenuItem(action);
ButtonGroup bGroup = getOrPutButtonGroup(buttonGroups,action);
bGroup.add(radioMenu);
child = radioMenu;
}
} else {
if(buttonGroup.isEmpty()) {
child = new CustomButton(action);
} else {
JToggleButton button = new JToggleButton(action);
ButtonGroup bGroup = getOrPutButtonGroup(buttonGroups,action);
bGroup.add(button);
child = button;
}
}
}
insertMenu(parent, child, action,addRemoveListener);
}
}
}
private ButtonGroup getOrPutButtonGroup(Map<String,ButtonGroup> existingGroups,Action action) {
String actionGroup = ActionTools.getToggleGroup(action);
ButtonGroup actionBGroup = existingGroups.get(actionGroup);
if(actionBGroup==null) {
actionBGroup = new ButtonGroup();
existingGroups.put(actionGroup,actionBGroup);
}
return actionBGroup;
}
/***
* TemporaryContainer is temporary because the parent container
* need to be known before creating the child container.
* @param current Current container
* @param subContainers Action, Container map
* @return Final instance of the container
*/
private Container convertContainer(TemporaryContainer current,Map<String,Container> subContainers) {
//Get parent container
Action action = current.getAction();
Container parent = subContainers.get(ActionTools.getParentMenuId(action));
if(parent instanceof TemporaryContainer) {
parent = convertContainer((TemporaryContainer)parent,subContainers);
}
Container child;
if(parent instanceof JToolBar) {
DropDownButton button = new DropDownButton(action);
if(ActionTools.getIcon(action)==null) { //Get icon from selected menu
button.setButtonAsMenuItem(true);
}
button.setComponentPopupMenu(new JPopupMenu());
child = button;
} else {
child = new JMenu(action);
}
// Update link between action and control
subContainers.put(ActionTools.getMenuId(action),child);
return child;
}
/**
* Find the most appropriate action insertion index.
* This is sorting by insertion. But it doesn't guaranty to solve complex order issues.
* @param parent MenuItem container
* @param action Action to insert
* @return Advised insertion id [0-parent.getComponentCount()]
*/
private int getInsertPosition(Container parent, Action action) {
if(ActionTools.isFirstInsertion(action)) {
return 0;
}
Component[] components;
if(parent instanceof JMenu) {
// Special case, JMenu use an internal JPopupMenu
components = ((JMenu)parent).getMenuComponents();
} else if(parent instanceof DropDownButton) {
components = ((DropDownButton) parent).getComponentPopupMenu().getComponents();
} else {
components = parent.getComponents();
}
for(int i=0;i<components.length;i++) {
Component comp = components[i];
if(comp instanceof AbstractButton) {
Action compAction = ((AbstractButton)comp).getAction();
if(compAction!=null) {
int position = getInsertionPosition(i,action,compAction);
if(position!=-1) {
return position;
}
}
}
}
return components.length;
}
/**
*
* @param newActionIndex
* @param newAction
* @param otherAction
* @return -1 (no link between the two actions), newActionIndex or newActionIndex+1
*/
public static int getInsertionPosition(int newActionIndex,Action newAction,Action otherAction) {
final String newElMenuId = ActionTools.getMenuId(newAction);
final String otherMenuId = ActionTools.getMenuId(otherAction);
if((!otherMenuId.isEmpty() && ActionTools.getInsertAfterMenuId(newAction).equals(otherMenuId)) ||
(!newElMenuId.isEmpty() && ActionTools.getInsertBeforeMenuId(otherAction).equals(newElMenuId))) {
return newActionIndex+1;
}
if((!otherMenuId.isEmpty() && ActionTools.getInsertBeforeMenuId(newAction).equals(otherMenuId)) ||
(!newElMenuId.isEmpty() && ActionTools.getInsertBeforeMenuId(otherAction).equals(newElMenuId))) {
return newActionIndex;
}
return -1; //No link between these two actions.
}
/**
* Insert a separator at insertPosition if action and otherComp are not in the same logical group.
* @param parent
* @param otherComp
* @param action
* @param insertPosition
*/
private void insertSeparator(Container parent,Component otherComp,Action action,int insertPosition) {
String logicalGroup = ActionTools.getLogicalGroup(action);
Action bAction = getAction(otherComp);
if(bAction!=null) {
// 2 consecutive actions with != logical action group
if(!logicalGroup.equals(ActionTools.getLogicalGroup(bAction))) {
parent.add(new JSeparator(),insertPosition);
}
}
}
private void insertMenu(Container parent, Component child, Action action,boolean addRemoveListener) {
// Get insertion index
int insertPosition = getInsertPosition(parent, action);
// Insert the action at is right place
parent.add(child, insertPosition);
// Insert Separator
if(insertPosition>0) {
insertSeparator(parent,getSubElements(parent)[insertPosition-1],action,insertPosition);
}
if(insertPosition<getSubElements(parent).length) {
insertSeparator(parent,getSubElements(parent)[insertPosition],action,insertPosition+1);
}
if(addRemoveListener) {
// Remove child from parentComponent when action is removed
action.addPropertyChangeListener(new RemoveActionControls(parent,child));
}
}
private JMenu createMenu(Action action) {
return new JMenu(action);
}
/**
* @deprecated Use register instead
*/
public void feedPopupMenu(JPopupMenu areaMenu) {
boolean addSeparator = areaMenu.getComponentCount() != 0;
int customMenuCounter=0; // default position of components
for(Action action : actions) {
JMenuItem actionItem = new JMenuItem(action);
areaMenu.insert(actionItem, customMenuCounter++);
}
if(addSeparator) {
//Separator at the end
areaMenu.insert(new JSeparator(),customMenuCounter++);
}
}
/**
* Remove accelerators previously applied to the component.
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
* @param condition one of WHEN_IN_FOCUSED_WINDOW, WHEN_FOCUSED,
* WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
* @param actionFactoryService Action factory previously registered through {@link #addActionFactory(ActionFactoryService, Object)}
*/
public void unsetAccelerators(JComponent component, int condition, ActionFactoryService actionFactoryService) {
MenuTrackerAction<?> trackerAction = actionFromFactory.get(actionFactoryService);
if(trackerAction != null) {
unsetAccelerators(component, condition, trackerAction.getActions());
}
}
/**
* Remove accelerators previously applied to the component.
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
* @param condition one of WHEN_IN_FOCUSED_WINDOW, WHEN_FOCUSED,
* WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
* @param actionList List of actions
*/
public void unsetAccelerators(JComponent component, int condition, List<Action> actionList) {
InputMap im = component.getInputMap(condition);
ActionMap actionMap = component.getActionMap();
for(Action action : actionList) {
KeyStroke actionStroke = ActionTools.getKeyStroke(action);
if(actionStroke!=null) {
im.remove(actionStroke);
actionMap.remove(action);
//Additionnal strokes
List<KeyStroke> strokes = ActionTools.getAdditionalKeyStroke(action);
if(strokes!=null) {
for(KeyStroke stroke : strokes) {
im.remove(stroke);
}
}
}
}
}
/**
* Apply to the component the actions
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
*/
public void setAccelerators(JComponent component) {
setAccelerators(component, JComponent.WHEN_FOCUSED);
}
/**
* Apply to the component the actions
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
* @param condition one of WHEN_IN_FOCUSED_WINDOW, WHEN_FOCUSED,
* WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
*/
public void setAccelerators(JComponent component, int condition) {
setAccelerators(component, condition, false);
}
/**
* Apply to the component the actions
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
* @param condition one of WHEN_IN_FOCUSED_WINDOW, WHEN_FOCUSED,
* WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
* @param overwrite Overwrite accelerators already in place.
*/
public void setAccelerators(JComponent component, int condition, boolean overwrite) {
setAccelerators(component, condition, overwrite, actions);
}
/**
* Apply to the component the actions
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
* @param condition one of WHEN_IN_FOCUSED_WINDOW, WHEN_FOCUSED,
* WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
* @param overwrite Overwrite accelerators already in place.
* @param actionFactoryService Action factory previously registered through {@link #addActionFactory(ActionFactoryService, Object)}
*/
public void setAccelerators(JComponent component, int condition, boolean overwrite , ActionFactoryService actionFactoryService) {
MenuTrackerAction<?> trackerAction = actionFromFactory.get(actionFactoryService);
if(trackerAction != null) {
setAccelerators(component, condition,overwrite, trackerAction.getActions());
}
}
/**
* Apply to the component the actions
* Text key shortcuts, Accelerators
* @param component Component that hold actionMap
* @param condition one of WHEN_IN_FOCUSED_WINDOW, WHEN_FOCUSED,
* WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
* @param overwrite Overwrite accelerators already in place.
* @param actionList List of actions
*/
public void setAccelerators(JComponent component, int condition, boolean overwrite, List<Action> actionList) {
InputMap im;
if(!overwrite) {
im = component.getInputMap(condition);
} else {
if(condition == JComponent.WHEN_IN_FOCUSED_WINDOW) {
im = new ComponentInputMap(component);
} else {
im = new InputMap();
}
}
ActionMap actionMap = component.getActionMap();
for(Action action : actionList) {
KeyStroke actionStroke = ActionTools.getKeyStroke(action);
if(actionStroke!=null) {
im.put(actionStroke, action);
actionMap.put(action, action);
//Additionnal strokes
List<KeyStroke> strokes = ActionTools.getAdditionalKeyStroke(action);
if(strokes!=null) {
for(KeyStroke stroke : strokes) {
im.put(stroke, action);
}
}
}
}
if(overwrite) {
component.setInputMap(condition, im);
}
}
/**
* Return a set of button to control actions
*
* @param setButtonText If true, a text is set on the buttons
* @return Instance of JToolBar
* @deprecated Use register instead
*/
public JToolBar getEditorToolBar(boolean setButtonText) {
JToolBar commandToolBar = new JToolBar();
//Add all registered actions
for(Action action : actions) {
if(ActionTools.getIcon(action)!=null) {
JButton newButton = new JButton(action);
// Remove this button when action is removed.
action.addPropertyChangeListener(new RemoveActionControls(commandToolBar, newButton));
commandToolBar.add(newButton);
}
}
//Final separator
commandToolBar.add(new JSeparator());
return commandToolBar;
}
/**
* Postpone the creation of the action control
*/
private class TemporaryContainer extends Container {
private Action action;
private TemporaryContainer(Action action) {
this.action = action;
}
public Action getAction() {
return action;
}
}
}