//----------------------------------------------------------------------------// // // // G l y p h M e n u // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.glyph.ui; import omr.glyph.Nest; import omr.glyph.Shape; import omr.glyph.ShapeSet; import omr.glyph.facets.Glyph; import omr.selection.GlyphEvent; import omr.selection.SelectionHint; import omr.sheet.Sheet; import omr.ui.util.SeparableMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.swing.AbstractAction; import javax.swing.JMenu; import javax.swing.JMenuItem; /** * Abstract class {@code GlyphMenu} is the base for glyph-based * menus such as {@link SymbolMenu}. * It also provides implementation for basic actions: copy, paste, assign, * compound, deassign and dump. * * <p>In a menu, actions are physically grouped by semantic tag and separators * are inserted between such groups.</p> * * <p>Actions are also organized according to their target menu level, to * allow actions to be dispatched into a hierarchy of menus. * Although currently all levels are set to 0.</p> * * @author Hervé Bitteur */ public abstract class GlyphMenu { //~ Static fields/initializers --------------------------------------------- /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(GlyphMenu.class); //~ Instance fields -------------------------------------------------------- /** Map action -> tag to update according to context */ private final Map<DynAction, Integer> dynActions = new LinkedHashMap<>(); /** Map action -> menu level */ private final Map<DynAction, Integer> levels = new LinkedHashMap<>(); /** Concrete menu */ private final SeparableMenu menu = new SeparableMenu(); /** The controller in charge of user gesture */ protected final GlyphsController controller; /** Related sheet */ protected final Sheet sheet; /** Related nest */ protected final Nest nest; /** Current number of selected glyphs */ protected int glyphNb; /** Current number of known glyphs */ protected int knownNb; /** Current number of stems */ protected int stemNb; /** Current number of virtual glyphs */ protected int virtualNb; /** Sure we have no virtual glyphs? */ protected boolean noVirtuals; //~ Constructors ----------------------------------------------------------- //-----------// // GlyphMenu // //-----------// /** * Creates a new GlyphMenu object. * * @param controller the related glyphs controller */ public GlyphMenu (GlyphsController controller) { this.controller = controller; sheet = controller.sheet; nest = controller.getNest(); buildMenu(); } //~ Methods ---------------------------------------------------------------- //---------// // getMenu // //---------// /** * Report the concrete menu. * * @return the menu */ public JMenu getMenu () { return menu; } //------------// // updateMenu // //------------// /** * Update the menu according to the currently selected glyphs. * * @return the number of selected glyphs */ public int updateMenu () { // Analyze the context glyphNb = 0; knownNb = 0; stemNb = 0; virtualNb = 0; Set<Glyph> glyphs = nest.getSelectedGlyphSet(); if (glyphs != null) { glyphNb = glyphs.size(); for (Glyph glyph : glyphs) { if (glyph.isKnown()) { knownNb++; if (glyph.getShape() == Shape.STEM) { stemNb++; } } if (glyph.isVirtual()) { virtualNb++; } } } noVirtuals = (virtualNb == 0); // Update all dynamic actions accordingly for (DynAction action : dynActions.keySet()) { action.update(); } // Update the menu root item menu.setEnabled(glyphNb > 0); if (glyphNb > 0) { menu.setText("Glyphs ..."); } else { menu.setText("no glyph"); } return glyphNb; } //-----------------// // registerActions // //-----------------// /** * Register all actions to be used in the menu */ protected abstract void registerActions (); //----------// // register // //----------// /** * Register this action instance in the set of dynamic actions * * @param menuLevel which menu should host the action item * @param action the action to register */ protected void register (int menuLevel, DynAction action) { levels.put(action, menuLevel); dynActions.put(action, action.tag); } //-----------// // buildMenu // //-----------// /** * Build the menu instance, grouping the actions with the same tag * and separating them from other tags, and organize actions into * their target menu level. */ private void buildMenu () { // Register actions registerActions(); // Sort actions on their tag SortedSet<Integer> tags = new TreeSet<>(dynActions.values()); // Retrieve the highest menu level int maxLevel = 0; for (Integer level : levels.values()) { maxLevel = Math.max(maxLevel, level); } // Initially update all the action items for (DynAction action : dynActions.keySet()) { action.update(); } // Generate the hierarchy of menus SeparableMenu prevMenu = menu; for (int level = 0; level <= maxLevel; level++) { SeparableMenu currentMenu = (level == 0) ? menu : new SeparableMenu("Continued ..."); for (Integer tag : tags) { for (Entry<DynAction, Integer> entry : dynActions.entrySet()) { if (entry.getValue() .equals(tag)) { DynAction action = entry.getKey(); if (levels.get(action) == level) { currentMenu.add(action.getMenuItem()); } } } currentMenu.addSeparator(); } currentMenu.trimSeparator(); if ((level > 0) && (currentMenu.getMenuComponentCount() > 0)) { // Insert this menu as a submenu of the previous one prevMenu.addSeparator(); prevMenu.add(currentMenu); prevMenu = currentMenu; } } } //~ Inner Classes ---------------------------------------------------------- //----------------// // AssignListener // //----------------// /** * A standard listener used in all shape assignment menus. */ protected class AssignListener implements ActionListener { //~ Instance fields ---------------------------------------------------- private final boolean compound; //~ Constructors ------------------------------------------------------- /** * Creates the AssignListener, with the compound flag. * * @param compound true if we assign a compound, false otherwise */ public AssignListener (boolean compound) { this.compound = compound; } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { JMenuItem source = (JMenuItem) e.getSource(); controller.asyncAssignGlyphs( nest.getSelectedGlyphSet(), Shape.valueOf(source.getText()), compound); } } //----------------// // CompoundAction // //----------------// /** * Build a compound and assign the shape selected in the menu. */ protected class CompoundAction extends DynAction { //~ Constructors ------------------------------------------------------- public CompoundAction () { super(30); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { // Default action is to open the menu assert false; } @Override public JMenuItem getMenuItem () { JMenu menu = new JMenu(this); ShapeSet.addAllShapes(menu, new AssignListener(true)); return menu; } @Override public void update () { if ((glyphNb > 1) && noVirtuals) { setEnabled(true); putValue(NAME, "Build compound as ..."); putValue(SHORT_DESCRIPTION, "Manually build a compound"); } else { setEnabled(false); putValue(NAME, "No compound"); putValue(SHORT_DESCRIPTION, "No glyphs for a compound"); } } } //------------// // CopyAction // //------------// /** * Copy the shape of the selected glyph shape (in order to replicate * the assignment to another glyph later). */ protected class CopyAction extends DynAction { //~ Constructors ------------------------------------------------------- public CopyAction () { super(10); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { Glyph glyph = nest.getSelectedGlyph(); if (glyph != null) { Shape shape = glyph.getShape(); if (shape != null) { controller.setLatestShapeAssigned(shape); } } } @Override public void update () { Glyph glyph = nest.getSelectedGlyph(); if (glyph != null) { Shape shape = glyph.getShape(); if (shape != null) { setEnabled(true); putValue(NAME, "Copy " + shape); putValue(SHORT_DESCRIPTION, "Copy this shape"); return; } } setEnabled(false); putValue(NAME, "Copy"); putValue(SHORT_DESCRIPTION, "No shape to copy"); } } //------------// // DumpAction // //------------// /** * Dump each glyph in the selected collection of glyphs. */ protected class DumpAction extends DynAction { //~ Constructors ------------------------------------------------------- public DumpAction () { super(40); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { for (Glyph glyph : nest.getSelectedGlyphSet()) { logger.info(glyph.dumpOf()); } } @Override public void update () { if (glyphNb > 0) { setEnabled(true); StringBuilder sb = new StringBuilder(); sb.append("Dump ") .append(glyphNb) .append(" glyph"); if (glyphNb > 1) { sb.append("s"); } putValue(NAME, sb.toString()); putValue(SHORT_DESCRIPTION, "Dump selected glyphs"); } else { setEnabled(false); putValue(NAME, "Dump"); putValue(SHORT_DESCRIPTION, "No glyph to dump"); } } } //-----------// // DynAction // //-----------// /** * Base implementation, to register the dynamic actions that need * to be updated according to the current glyph selection context. */ protected abstract class DynAction extends AbstractAction { //~ Instance fields ---------------------------------------------------- /** Semantic tag */ protected final int tag; //~ Constructors ------------------------------------------------------- public DynAction (int tag) { this.tag = tag; } //~ Methods ------------------------------------------------------------ /** * Method to update the action according to the current context */ public abstract void update (); /** * Report which item class should be used to the related menu item * * @return the precise menu item class */ public JMenuItem getMenuItem () { return new JMenuItem(this); } } //--------------// // AssignAction // //--------------// /** * Assign to each glyph the shape selected in the menu. */ protected class AssignAction extends DynAction { //~ Constructors ------------------------------------------------------- public AssignAction () { super(20); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { // Default action is to open the menu assert false; } @Override public JMenuItem getMenuItem () { JMenu menu = new JMenu(this); ShapeSet.addAllShapes(menu, new AssignListener(false)); return menu; } @Override public void update () { if ((glyphNb > 0) && noVirtuals) { setEnabled(true); if (glyphNb == 1) { putValue(NAME, "Assign glyph as ..."); } else { putValue(NAME, "Assign each glyph as ..."); } putValue(SHORT_DESCRIPTION, "Manually force an assignment"); } else { setEnabled(false); putValue(NAME, "Assign glyph as ..."); putValue(SHORT_DESCRIPTION, "No glyph to assign a shape to"); } } } //----------------// // DeassignAction // //----------------// /** * Deassign each glyph in the selected collection of glyphs. */ protected class DeassignAction extends DynAction { //~ Constructors ------------------------------------------------------- public DeassignAction () { super(20); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { // Remember which is the current selected glyph Glyph glyph = nest.getSelectedGlyph(); // Actually deassign the whole set Set<Glyph> glyphs = nest.getSelectedGlyphSet(); if (noVirtuals) { controller.asyncDeassignGlyphs(glyphs); // Update focus on current glyph, if reused in a compound if (glyph != null) { Glyph newGlyph = glyph.getFirstSection() .getGlyph(); if (glyph != newGlyph) { nest.getGlyphService() .publish( new GlyphEvent( this, SelectionHint.GLYPH_INIT, null, newGlyph)); } } } else { controller.asyncDeleteVirtualGlyphs(glyphs); } } @Override public void update () { if ((knownNb > 0) && (noVirtuals || (virtualNb == knownNb))) { setEnabled(true); StringBuilder sb = new StringBuilder(); if (noVirtuals) { sb.append("Deassign "); sb.append(knownNb) .append(" glyph"); if (knownNb > 1) { sb.append("s"); } } else { sb.append("Delete "); if (virtualNb > 0) { sb.append(virtualNb) .append(" virtual glyph"); if (virtualNb > 1) { sb.append("s"); } } } if (stemNb > 0) { sb.append(" w/ ") .append(stemNb) .append(" stem"); if (stemNb > 1) { sb.append("s"); } } putValue(NAME, sb.toString()); putValue(SHORT_DESCRIPTION, "Deassign selected glyphs"); } else { setEnabled(false); putValue(NAME, "Deassign"); putValue(SHORT_DESCRIPTION, "No glyph to deassign"); } } } //-------------// // PasteAction // //-------------// /** * Paste the latest shape to the glyph(s) at end. */ protected class PasteAction extends DynAction { //~ Static fields/initializers ----------------------------------------- private static final String PREFIX = "Paste "; //~ Constructors ------------------------------------------------------- public PasteAction () { super(10); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { JMenuItem source = (JMenuItem) e.getSource(); Shape shape = Shape.valueOf( source.getText().substring(PREFIX.length())); Glyph glyph = nest.getSelectedGlyph(); if (glyph != null) { controller.asyncAssignGlyphs( Collections.singleton(glyph), shape, false); } } @Override public void update () { Shape latest = controller.getLatestShapeAssigned(); if ((glyphNb > 0) && (latest != null) && noVirtuals) { setEnabled(true); putValue(NAME, PREFIX + latest.toString()); putValue(SHORT_DESCRIPTION, "Assign latest shape"); } else { setEnabled(false); putValue(NAME, PREFIX); putValue(SHORT_DESCRIPTION, "No shape to assign again"); } } } }