//----------------------------------------------------------------------------// // // // G l y p h B r o w s e r // // // //----------------------------------------------------------------------------// // <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.WellKnowns; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.glyph.BasicNest; import omr.glyph.GlyphRepository; import omr.glyph.GlyphSignature; import omr.glyph.GlyphsModel; import omr.glyph.Nest; import omr.glyph.facets.Glyph; import omr.lag.BasicLag; import omr.lag.Lag; import omr.lag.Section; import omr.run.Orientation; import omr.selection.GlyphEvent; import omr.selection.LocationEvent; import omr.selection.MouseMovement; import omr.selection.SelectionHint; import static omr.selection.SelectionHint.*; import omr.selection.SelectionService; import omr.selection.UserEvent; import omr.sheet.Sheet; import omr.ui.Board; import omr.ui.field.LTextField; import omr.ui.util.Panel; import omr.ui.view.LogSlider; import omr.ui.view.Rubber; import omr.ui.view.ScrollView; import omr.ui.view.Zoom; import omr.util.BlackList; import com.jgoodies.forms.builder.PanelBuilder; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** * Class {@code GlyphBrowser} gathers a navigator to move between * selected glyphs, a glyph board for glyph details, and a display for * graphical glyph view. * This is a (package private) companion of {@link SampleVerifier}. * * @author Hervé Bitteur */ class GlyphBrowser implements ChangeListener { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger( GlyphBrowser.class); /** Events that can be published on internal service (TODO: Check this!) */ private static final Class<?>[] locEvents = new Class<?>[]{ LocationEvent.class }; /** * Field constant {@code NO_INDEX} is a specific value {@value} to * indicate absence of index */ private static final int NO_INDEX = -1; //~ Instance fields -------------------------------------------------------- /** The concrete Swing component */ private JPanel component = new JPanel(); /** Reference of SampleVerifier */ private final SampleVerifier verifier; /** Repository of known glyphs */ private final GlyphRepository repository = GlyphRepository.getInstance(); /** Size of the lag display */ private Dimension modelSize; /** Contour of the lag display */ private Rectangle modelRectangle; /** Population of glyphs file names */ private List<String> names = Collections.emptyList(); /** Navigator instance to navigate through all glyphs names */ private Navigator navigator; /** Left panel : navigator, glyphboard, evaluator */ private JPanel leftPanel; /** Composite display (view + zoom slider) */ private Display display; /** Basic location event service */ private SelectionService locationService; /** Hosting Nest */ private Nest tNest; /** Vertical Lag */ private Lag vtLag; /** Horizontal Lag */ private Lag htLag; /** Basic glyph model */ private GlyphsController controller; /** The glyph view */ private NestView view; /** Glyph board with ability to delete a training glyph */ private GlyphBoard glyphBoard; //~ Constructors ----------------------------------------------------------- //--------------// // GlyphBrowser // //--------------// /** * Create an instance, with back-reference to SampleVerifier. * * @param verifier ref back to verifier */ public GlyphBrowser (SampleVerifier verifier) { this.verifier = verifier; // Layout component.setLayout(new BorderLayout()); resetBrowser(); } //~ Methods ---------------------------------------------------------------- //--------------// // getComponent // //--------------// /** * Report the UI component. * * @return the concrete component */ public JPanel getComponent () { return component; } //----------------// // loadGlyphNames // //----------------// /** * Programmatic use of Load action in Navigator: load the glyph names as * selected, and focus on first glyph. */ public void loadGlyphNames () { navigator.loadAction.actionPerformed(null); } //--------------// // stateChanged // //--------------// /** * Called when a new selection has been made in SampleVerifier * companion. * * @param e not used */ @Override public void stateChanged (ChangeEvent e) { int selNb = verifier.getGlyphCount(); navigator.loadAction.setEnabled(selNb > 0); } //----------------// // buildLeftPanel // //----------------// /** * Build a panel composed vertically of a Navigator, a GlyphBoard * and an EvaluationBoard. * * @return the UI component, ready to be inserted in Swing hierarchy */ private JPanel buildLeftPanel () { navigator = new Navigator(); // Specific glyph board glyphBoard = new MyGlyphBoard(controller); glyphBoard.connect(); glyphBoard.getDeassignAction() .setEnabled(false); // Passive evaluation board EvaluationBoard evalBoard = new EvaluationBoard(controller, true); evalBoard.connect(); // Layout FormLayout layout = new FormLayout("pref", "pref,pref,pref"); PanelBuilder builder = new PanelBuilder(layout); CellConstraints cst = new CellConstraints(); builder.setDefaultDialogBorder(); builder.add(navigator.getComponent(), cst.xy(1, 1)); builder.add(glyphBoard.getComponent(), cst.xy(1, 2)); builder.add(evalBoard.getComponent(), cst.xy(1, 3)); return builder.getPanel(); } //-------------// // removeGlyph // //-------------// private void removeGlyph () { int index = navigator.getIndex(); if (index >= 0) { // Delete glyph designated by index String gName = names.get(index); Glyph glyph = navigator.getGlyph(gName); // User confirmation is required ? if (constants.confirmDeletions.getValue()) { if (JOptionPane.showConfirmDialog( component, "Remove glyph '" + gName + "' ?") != JOptionPane.YES_OPTION) { return; } } // Shrink names list names.remove(index); // Update model & display repository.removeGlyph(gName); for (Section section : glyph.getMembers()) { section.delete(); } // Update the Glyph selector also ! verifier.deleteGlyphName(gName); // Perform file deletion if (repository.isIcon(gName)) { new SymbolsBlackList().add(new File(gName)); } else { File file = new File(WellKnowns.TRAIN_FOLDER, gName); new BlackList(file.getParentFile()).add(new File(gName)); } logger.info("Removed {}", gName); // Set new index ? if (index < names.size()) { navigator.setIndex(index, GLYPH_INIT); // Next } else { navigator.setIndex(index - 1, GLYPH_INIT); // Prev/None } } else { logger.warn("No selected glyph to remove!"); } } //--------------// // resetBrowser // //--------------// private void resetBrowser () { // Reset model tNest = new NoSigNest("tNest", null); htLag = new BasicLag("htLag", Orientation.HORIZONTAL); vtLag = new BasicLag("vtLag", Orientation.VERTICAL); locationService = new SelectionService("BrowserLocation", locEvents); controller = new BasicController(tNest, locationService); tNest.setServices(locationService); // Reset left panel if (leftPanel != null) { component.remove(leftPanel); } leftPanel = buildLeftPanel(); component.add(leftPanel, BorderLayout.WEST); // Reset display if (display != null) { component.remove(display); } display = new Display(); component.add(display, BorderLayout.CENTER); // TODO: Check if all this is really needed ... component.invalidate(); component.validate(); component.repaint(); } //~ Inner Classes ---------------------------------------------------------- //-----------------// // BasicController // //-----------------// /** * A very basic glyphs controller, with a sheet-less location service. */ private class BasicController extends GlyphsController { //~ Instance fields ---------------------------------------------------- /** A specific location service, not tied to a sheet */ private final SelectionService locationService; //~ Constructors ------------------------------------------------------- public BasicController (Nest nest, SelectionService locationService) { super(new BasicModel(nest)); this.locationService = locationService; } //~ Methods ------------------------------------------------------------ @Override public SelectionService getLocationService () { return this.locationService; } } //------------// // BasicModel // //------------// /** * A very basic glyphs model, used to handle the deletion of glyphs. */ private class BasicModel extends GlyphsModel { //~ Constructors ------------------------------------------------------- public BasicModel (Nest nest) { super(null, nest, null); } //~ Methods ------------------------------------------------------------ // Certainly not called ... @Override public void deassignGlyph (Glyph glyph) { removeGlyph(); } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Boolean confirmDeletions = new Constant.Boolean( true, "Should user confirm each glyph deletion" + " from training material"); } //----------------// // DeassignAction // //----------------// private class DeassignAction extends AbstractAction { //~ Constructors ------------------------------------------------------- public DeassignAction () { super("Remove"); putValue( Action.SHORT_DESCRIPTION, "Remove that glyph from training material"); } //~ Methods ------------------------------------------------------------ @SuppressWarnings("unchecked") @Override public void actionPerformed (ActionEvent e) { removeGlyph(); } } //---------// // Display // //---------// private class Display extends JPanel { //~ Instance fields ---------------------------------------------------- LogSlider slider; Rubber rubber; ScrollView slv; Zoom zoom; //~ Constructors ------------------------------------------------------- public Display () { view = new MyView(controller); view.setLocationService(locationService); view.subscribe(); modelRectangle = new Rectangle(); modelSize = new Dimension(0, 0); slider = new LogSlider(2, 5, LogSlider.VERTICAL, -3, 4, 0); zoom = new Zoom(slider, 1); // Default ratio set to 1 rubber = new Rubber(view, zoom); rubber.setMouseMonitor(view); view.setZoom(zoom); view.setRubber(rubber); slv = new ScrollView(view); // Layout setLayout(new BorderLayout()); add(slider, BorderLayout.WEST); add(slv.getComponent(), BorderLayout.CENTER); } } //------------// // LoadAction // //------------// private class LoadAction extends AbstractAction { //~ Constructors ------------------------------------------------------- public LoadAction () { super("Load"); } //~ Methods ------------------------------------------------------------ @Override public void actionPerformed (ActionEvent e) { // Get a (shrinkable, to allow deletions) list of glyph names names = verifier.getGlyphNames(); // Reset lag & display resetBrowser(); // Set navigator on first glyph, if any if (!names.isEmpty()) { navigator.setIndex(0, GLYPH_INIT); } else { if (e != null) { logger.warn("No glyphs selected in Glyph Selector"); } navigator.all.setEnabled(false); navigator.prev.setEnabled(false); navigator.next.setEnabled(false); } } } //--------------// // MyGlyphBoard // //--------------// private class MyGlyphBoard extends SymbolGlyphBoard { //~ Constructors ------------------------------------------------------- public MyGlyphBoard (GlyphsController controller) { super(controller, false, true); } //~ Methods ------------------------------------------------------------ @Override public Action getDeassignAction () { if (deassignAction == null) { deassignAction = new DeassignAction(); } return deassignAction; } } //--------// // MyView // //--------// private final class MyView extends NestView { //~ Constructors ------------------------------------------------------- public MyView (GlyphsController controller) { super(tNest, controller, Arrays.asList(htLag, vtLag)); setName("GlyphBrowser-View"); subscribe(); } //~ Methods ------------------------------------------------------------ //---------// // onEvent // //---------// /** * Call-back triggered from (local) selection objects. * * @param event the notified event */ @Override public void onEvent (UserEvent event) { try { // Ignore RELEASING if (event.movement == MouseMovement.RELEASING) { return; } // Keep normal view behavior (rubber, etc...) super.onEvent(event); // Additional tasks if (event instanceof LocationEvent) { LocationEvent sheetLocation = (LocationEvent) event; if (sheetLocation.hint == SelectionHint.LOCATION_INIT) { Rectangle rect = sheetLocation.getData(); if ((rect != null) && (rect.width == 0) && (rect.height == 0)) { // Look for pointed glyph int index = glyphLookup(rect); navigator.setIndex(index, sheetLocation.hint); } } } else if (event instanceof GlyphEvent) { GlyphEvent glyphEvent = (GlyphEvent) event; if (glyphEvent.hint == GLYPH_INIT) { Glyph glyph = glyphEvent.getData(); // Display glyph contour if (glyph != null) { locationService.publish( new LocationEvent( this, glyphEvent.hint, null, glyph.getBounds())); } } } } catch (Exception ex) { logger.warn(getClass().getName() + " onEvent error", ex); } } //-------------// // renderItems // //-------------// @Override public void renderItems (Graphics2D g) { // Mark the current glyph int index = navigator.getIndex(); if (index >= 0) { String gName = names.get(index); Glyph glyph = navigator.getGlyph(gName); g.setColor(Color.black); g.setXORMode(Color.darkGray); renderGlyphArea(glyph, g); } } //-------------// // glyphLookup // //-------------// /** * Lookup for a glyph that is pointed by rectangle location. This is a * very specific glyph lookup, for which we cannot rely on Nest * usual features. So we simply browse through the collection of glyphs * (names). * * @param rect location (upper left corner) * @return index in names collection if found, NO_INDEX otherwise */ private int glyphLookup (Rectangle rect) { int index = -1; for (String gName : names) { index++; if (repository.isLoaded(gName)) { Glyph glyph = navigator.getGlyph(gName); if (glyph.getNest() == tNest) { for (Section section : glyph.getMembers()) { if (section.contains(rect.x, rect.y)) { return index; } } } } } return NO_INDEX; // Not found } } //-----------// // Navigator // //-----------// /** * Class {@code Navigator} handles the navigation through the * collection of glyphs (names). */ private final class Navigator extends Board { //~ Instance fields ---------------------------------------------------- /** Current index in names collection (NO_INDEX if none) */ private int nameIndex = NO_INDEX; // Navigation actions & buttons LoadAction loadAction = new LoadAction(); JButton load = new JButton(loadAction); JButton all = new JButton("All"); JButton next = new JButton("Next"); JButton prev = new JButton("Prev"); LTextField nameField = new LTextField("", "File where glyph is stored"); //~ Constructors ------------------------------------------------------- //-----------// // Navigator // //-----------// Navigator () { super(Board.SAMPLE, null, null, false, true); defineLayout(); all.addActionListener( new ActionListener() { @Override public void actionPerformed (ActionEvent e) { // Load all (non icon) glyphs int index = -1; for (String gName : names) { index++; if (!repository.isIcon(gName)) { setIndex(index, GLYPH_INIT); } } // Load & point to first icon setIndex(0, GLYPH_INIT); } }); prev.addActionListener( new ActionListener() { @Override public void actionPerformed (ActionEvent e) { setIndex(nameIndex - 1, GLYPH_INIT); // To prev } }); next.addActionListener( new ActionListener() { @Override public void actionPerformed (ActionEvent e) { setIndex(nameIndex + 1, GLYPH_INIT); // To next } }); load.setToolTipText("Load the selected glyphs"); all.setToolTipText("Display all glyphs"); prev.setToolTipText("Go to previous glyph"); next.setToolTipText("Go to next glyph"); loadAction.setEnabled(false); all.setEnabled(false); prev.setEnabled(false); next.setEnabled(false); } //~ Methods ------------------------------------------------------------ //----------// // getGlyph // //----------// public Glyph getGlyph (String gName) { Glyph glyph = repository.getGlyph(gName, null); if (glyph == null) { return null; } if (glyph.getNest() != tNest) { tNest.addGlyph(glyph); Color color = glyph.getShape() .getColor(); for (Section section : glyph.getMembers()) { Lag lag = section.isVertical() ? vtLag : htLag; lag.addVertex(section); // Trick! section.setGraph(lag); section.setColor(color); } } return glyph; } //----------// // getIndex // //----------// /** * Report the current glyph index in the names collection. * * @return the current index, which may be NO_INDEX */ public final int getIndex () { return nameIndex; } // Just to please the Board interface @Override public void onEvent (UserEvent event) { throw new UnsupportedOperationException("Not supported yet."); } //----------// // setIndex // //----------// /** * Only method allowed to designate a glyph. * * @param index index of new current glyph * @param hint related processing hint */ public void setIndex (int index, SelectionHint hint) { Glyph glyph = null; if (index >= 0) { String gName = names.get(index); nameField.setText(gName); // Special case for icon : if we point to an icon, we have to // get rid of all other icons (standard glyphs can be kept) // Otherwise, they would all be displayed on top of the other if (repository.isIcon(gName)) { repository.unloadIconsFrom(names); } // Load the desired glyph if needed glyph = getGlyph(gName); if (glyph == null) { return; } // Extend view model size if needed Rectangle box = glyph.getBounds(); modelRectangle = modelRectangle.union(box); Dimension newSize = modelRectangle.getSize(); if (!newSize.equals(modelSize)) { modelSize = newSize; view.setModelSize(modelSize); } } else { nameField.setText(""); } nameIndex = index; tNest.getGlyphService() .publish(new GlyphEvent(this, hint, null, glyph)); // Enable buttons according to glyph selection all.setEnabled(!names.isEmpty()); prev.setEnabled(index > 0); next.setEnabled((index >= 0) && (index < (names.size() - 1))); } //--------------// // defineLayout // //--------------// private void defineLayout () { CellConstraints cst = new CellConstraints(); FormLayout layout = Panel.makeFormLayout(4, 3); PanelBuilder builder = new PanelBuilder(layout, super.getBody()); builder.setDefaultDialogBorder(); int r = 1; // -------------------------------- builder.add(load, cst.xy(11, r)); r += 2; // -------------------------------- builder.add(all, cst.xy(3, r)); builder.add(prev, cst.xy(7, r)); builder.add(next, cst.xy(11, r)); r += 2; // -------------------------------- JLabel file = new JLabel("File", SwingConstants.RIGHT); builder.add(file, cst.xy(1, r)); nameField.getField() .setHorizontalAlignment(JTextField.LEFT); builder.add(nameField.getField(), cst.xyw(3, r, 9)); } } //-----------// // NoSigNest // //-----------// /** * A specific glyph nest, with no handling of signature. */ private static class NoSigNest extends BasicNest { //~ Constructors ------------------------------------------------------- public NoSigNest (String name, Sheet sheet) { super(name, sheet); } //~ Methods ------------------------------------------------------------ @Override public Glyph getOriginal (GlyphSignature signature) { return null; } } }