org.scijava.menu.ShadowMenu Maven / Gradle / Ivy
Show all versions of scijava-common Show documentation
/*
* #%L
* SciJava Common shared library for SciJava software.
* %%
* Copyright (C) 2009 - 2017 Board of Regents of the University of
* Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck
* Institute of Molecular Cell Biology and Genetics, University of
* Konstanz, and KNIME GmbH.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package org.scijava.menu;
import java.lang.reflect.Array;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.scijava.AbstractContextual;
import org.scijava.Context;
import org.scijava.MenuEntry;
import org.scijava.MenuPath;
import org.scijava.Named;
import org.scijava.event.EventService;
import org.scijava.log.LogService;
import org.scijava.menu.event.MenusAddedEvent;
import org.scijava.menu.event.MenusRemovedEvent;
import org.scijava.menu.event.MenusUpdatedEvent;
import org.scijava.module.ModuleInfo;
import org.scijava.module.ModuleService;
import org.scijava.plugin.Parameter;
import org.scijava.util.ClassUtils;
import org.scijava.util.MiscUtils;
import org.scijava.util.Types;
/**
* A tree representing a menu structure independent of any particular user
* interface.
*
* A {@code ShadowMenu} is a tree node with links to other tree nodes. It is
* possible to traverse the entire menu structure from any given node, though by
* convention the root node is used to represent the menu as a whole.
*
*
* The class is implemented as a {@link Collection} of modules (i.e.,
* {@link ModuleInfo} objects), with the tree structure generated from the
* modules' menu paths (see {@link ModuleInfo#getMenuPath()}). The class also
* implements {@link Runnable}, with each leaf node retaining a link to its
* corresponding {@link ModuleInfo}, and executing that module when
* {@link #run()} is called.
*
*
* @author Curtis Rueden
* @see MenuCreator
* @see MenuPath
* @see MenuEntry
*/
public class ShadowMenu extends AbstractContextual implements
Comparable, Collection, Runnable, Named
{
/** Icon to use for leaf entries by default, if no icon is specified. */
private static final String DEFAULT_ICON_PATH = "/icons/plugin.png";
/** The module linked to this node. Always null for non-leaf nodes. */
private final ModuleInfo moduleInfo;
/** The menu entry corresponding to this node. */
private final MenuEntry menuEntry;
/** How deep into the menu structure this node is. */
private final int menuDepth;
/** Reference to parent node. */
private final ShadowMenu parent;
/** Table of child nodes, keyed by name. */
private final Map children;
@Parameter(required = false)
private EventService es;
@Parameter(required = false)
private ModuleService moduleService;
@Parameter(required = false)
private LogService log;
/** Constructs a root menu node populated with the given modules. */
public ShadowMenu(final Context context,
final Collection extends ModuleInfo> modules)
{
this(context, null, -1, null);
addAll(modules);
}
private ShadowMenu(final Context context, final ModuleInfo moduleInfo,
final int menuDepth, final ShadowMenu parent)
{
setContext(context);
if (moduleInfo == null) {
this.moduleInfo = null;
menuEntry = null;
}
else {
final MenuPath menuPath = moduleInfo.getMenuPath();
// preserve moduleInfo reference only for leaf items
final boolean leaf = menuDepth == menuPath.size() - 1;
this.moduleInfo = leaf ? moduleInfo : null;
menuEntry = menuPath.get(menuDepth);
}
this.menuDepth = menuDepth;
this.parent = parent;
children = new HashMap<>();
}
// -- ShadowMenu methods --
/** Gets the module linked to this node, or null if node is not a leaf. */
public ModuleInfo getModuleInfo() {
return moduleInfo;
}
/**
* Gets the node with the given menu path (relative to this node), or null if
* no such menu node.
*
* For example, asking for "File > New > Image..." from the root
* application menu node would retrieve the node for "Image...", as would
* asking for "New > Image..." from the "File" node.
*
*/
public ShadowMenu getMenu(final MenuPath menuPath) {
return getMenu(menuPath, 0);
}
public ShadowMenu getMenu(final String path) {
return getMenu(new MenuPath(path), 0);
}
/**
* Gets the menu entry corresponding to this node. May be a non-leaf menu
* (e.g., "File") or a leaf item (e.g., "Exit").
*/
public MenuEntry getMenuEntry() {
return menuEntry;
}
/**
* Gets how deep into the menu structure this node is. For example, "File"
* would be at depth 1, whereas "Exit" (of "File>Exit") would be at depth
* 2.
*/
public int getMenuDepth() {
return menuDepth;
}
/** Gets this node's parent, or null if it is a root node. */
public ShadowMenu getParent() {
return parent;
}
/** Gets this node's children, sorted by weight. */
public List getChildren() {
// copy the children table into an ordered list
final List childList =
new ArrayList<>(children.values());
// sort the list by weight then alphabetically
Collections.sort(childList);
return childList;
}
/** Returns true if this node has no children. */
public boolean isLeaf() {
return children.isEmpty();
}
/** Returns true if this node is selectable (checkbox or radio button). */
public boolean isToggle() {
if (moduleInfo == null) return false;
return moduleInfo.isSelectable();
}
/** Returns true if this node is a checkbox. */
public boolean isCheckBox() {
if (!isToggle()) return false;
final String selectionGroup = moduleInfo.getSelectionGroup();
return selectionGroup == null || selectionGroup.isEmpty();
}
/** Returns true if this node is a radio button. */
public boolean isRadioButton() {
if (!isToggle()) return false;
final String selectionGroup = moduleInfo.getSelectionGroup();
return selectionGroup != null && !selectionGroup.isEmpty();
}
/**
* Gets the URL of the icon associated with this node's {@link MenuEntry}.
*
* @see org.scijava.plugin.PluginInfo#getIconURL()
*/
public URL getIconURL() {
if (menuEntry == null) return null;
String iconPath = menuEntry.getIconPath();
if (iconPath == null || iconPath.isEmpty()) {
if (isLeaf()) iconPath = DEFAULT_ICON_PATH;
else return null;
}
final String className = moduleInfo.getDelegateClassName();
try {
final Class> c = Types.load(className, false);
final URL iconURL = c.getResource(iconPath);
if (iconURL == null) {
if (log != null) log.error("Could not load icon: " + iconPath);
}
return iconURL;
}
catch (final IllegalArgumentException exc) {
final String message = "Could not load icon for class: " + className;
if (log.isDebug()) log.debug(message, exc);
else log.error(message);
return null;
}
}
/**
* Updates the menu structure to reflect changes in the given module. Does
* nothing unless the module is already in the menu structure.
*
* @return true if the module was successfully updated
*/
public boolean update(final ModuleInfo module) {
final ShadowMenu removed = removeInternal(module);
if (removed == null) return false; // was not in menu structure
final ShadowMenu node = addInternal(module);
if (node == null) return false;
if (es != null) es.publish(new MenusUpdatedEvent(node));
return true;
}
/**
* Updates the menu structure to reflect changes in the given modules. Does
* nothing unless at least one of the modules is already in the menu
* structure.
*
* @return true if at least one module was successfully updated
*/
public boolean updateAll(final Collection extends ModuleInfo> c) {
final HashSet nodes = new HashSet<>();
for (final ModuleInfo info : c) {
final ShadowMenu removed = removeInternal(info);
if (removed == null) continue; // was not in menu structure
final ShadowMenu node = addInternal(info);
if (node != null) nodes.add(node);
}
if (nodes.isEmpty()) return false;
if (es != null) es.publish(new MenusUpdatedEvent(nodes));
return true;
}
// -- Named methods --
@Override
public String getName() {
return menuEntry == null ? null : menuEntry.getName();
}
@Override
public void setName(final String name) {
if (menuEntry == null) return;
menuEntry.setName(name);
}
// -- Object methods --
@Override
public int compareTo(final ShadowMenu c) {
if (menuEntry == null || c.menuEntry == null) return 0;
// compare weights
final double w1 = menuEntry.getWeight();
final double w2 = c.menuEntry.getWeight();
if (w1 < w2) return -1;
if (w1 > w2) return 1;
// if weights are equal, sort alphabetically
final String n1 = menuEntry.getName();
final String n2 = c.menuEntry.getName();
return MiscUtils.compare(n1, n2);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i <= menuDepth; i++)
sb.append("\t");
final String name = getName();
sb.append(name == null ? "[-]" : name);
for (final ShadowMenu child : getChildren()) {
sb.append("\n" + child.toString());
}
return sb.toString();
}
// -- Runnable methods --
/**
* Executes the module linked to this node. Does nothing for non-leaf nodes.
*/
@Override
public void run() {
if (moduleInfo == null) return; // no module to run
if (moduleService != null) moduleService.run(moduleInfo, true);
}
// -- Collection methods --
/**
* Adds the given module to the menu structure. If the module is not visible
* (i.e., {@link ModuleInfo#isVisible()} returns false), it is ignored.
*
* {@inheritDoc}
*/
@Override
public boolean add(final ModuleInfo o) {
if (!o.isVisible()) return false;
final ShadowMenu node = addInternal(o);
if (node == null) return false;
if (es != null) es.publish(new MenusAddedEvent(node));
return true;
}
/**
* Adds the given modules to the menu structure. If a module is not visible
* (i.e., {@link ModuleInfo#isVisible()} returns false), it is ignored.
*
* {@inheritDoc}
*/
@Override
public boolean addAll(final Collection extends ModuleInfo> c) {
final HashSet nodes = new HashSet<>();
for (final ModuleInfo info : c) {
if (!info.isVisible()) continue;
final ShadowMenu node = addInternal(info);
if (node != null) nodes.add(node);
}
if (nodes.isEmpty()) return false;
if (es != null) es.publish(new MenusAddedEvent(nodes));
return true;
}
@Override
public void clear() {
children.clear();
}
@Override
public boolean contains(final Object o) {
if (o == moduleInfo) return true;
for (final ShadowMenu node : children.values()) {
if (node.contains(o)) return true;
}
return false;
}
@Override
public boolean containsAll(final Collection> c) {
for (final Object o : c) {
if (!contains(o)) return false;
}
return true;
}
@Override
public boolean isEmpty() {
return children.isEmpty();
}
@Override
public ShadowMenuIterator iterator() {
return new ShadowMenuIterator(this);
}
@Override
public boolean remove(final Object o) {
if (!(o instanceof ModuleInfo)) return false;
final ModuleInfo info = (ModuleInfo) o;
final ShadowMenu node = removeInternal(info);
if (node == null) return false;
if (es != null) es.publish(new MenusRemovedEvent(node));
return true;
}
@Override
public boolean removeAll(final Collection> c) {
final HashSet nodes = new HashSet<>();
for (final Object o : c) {
if (!(o instanceof ModuleInfo)) continue;
final ModuleInfo info = (ModuleInfo) o;
final ShadowMenu node = removeInternal(info);
if (node != null) nodes.add(node);
}
if (nodes.isEmpty()) return false;
if (es != null) es.publish(new MenusRemovedEvent(nodes));
return true;
}
@Override
public boolean retainAll(final Collection> c) {
final ArrayList