//----------------------------------------------------------------------------// // // // S a m p l e V e r i f i 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.glyph.GlyphRepository; import omr.glyph.Shape; import omr.ui.MainGui; import com.jgoodies.forms.builder.PanelBuilder; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import org.jdesktop.application.ResourceMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import javax.swing.BorderFactory; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.ListCellRenderer; import javax.swing.border.EtchedBorder; import javax.swing.border.TitledBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; /** * Class {@code SampleVerifier} provides a user interface to browse * through all glyphs samples recorded for evaluator training, * to visually check the correctness of their assigned shape, * and to remove spurious samples when necessary. * * <p>One, several or all recorded sheets can be selected. * * <p>Within the contained glyphs, one, several or all can be selected, the * selected glyphs can then be browsed in any direction. * * <p>The current glyph is displayed, with its appearance in a properly * translated Nest view, and its characteristics in a dedicated panel. * If the user wants to discard the glyph, it can be removed from the repository * of training material. * * @author Hervé Bitteur */ public class SampleVerifier { //~ Static fields/initializers --------------------------------------------- /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(SampleVerifier.class); /** The unique instance */ private static volatile SampleVerifier INSTANCE; //~ Instance fields -------------------------------------------------------- /** Repository of known glyphs */ private final GlyphRepository repository = GlyphRepository.getInstance(); /** The dedicated frame */ private final JFrame frame; /** The panel in charge of the current glyph */ private GlyphBrowser glyphBrowser = new GlyphBrowser(this); /** The panel in charge of the glyphs selection */ private GlyphSelector glyphSelector = new GlyphSelector(glyphBrowser); /** The panel in charge of the shapes selection */ private ShapeSelector shapeSelector = new ShapeSelector(glyphSelector); /** The panel in charge of the sheets (or icons folder) selection */ private FolderSelector folderSelector = new FolderSelector(shapeSelector); /** Sheets folder */ private final File sheetsFolder = repository.getSheetsFolder(); /** Samples folder */ private final File samplesFolder = repository.getSamplesFolder(); //~ Constructors ----------------------------------------------------------- //----------------// // SampleVerifier // //----------------// /** * Create an instance of SampleVerifier. */ private SampleVerifier () { // Pane split vertically: selectors then browser JSplitPane vertSplitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT, getSelectorsPanel(), glyphBrowser.getComponent()); vertSplitPane.setName("SampleVerifierSplitPane"); vertSplitPane.setDividerSize(1); // Hosting frame frame = new JFrame(); frame.setName("SampleVerifierFrame"); frame.add(vertSplitPane); // Resource injection ResourceMap resource = MainGui.getInstance().getContext(). getResourceMap( getClass()); resource.injectComponents(frame); } //~ Methods ---------------------------------------------------------------- //-------------// // getInstance // //-------------// /** * Give access to the single instance of this class. * * @return the SampleVerifier instance */ public static SampleVerifier getInstance () { if (INSTANCE == null) { INSTANCE = new SampleVerifier(); } return INSTANCE; } //------------// // setVisible // //------------// /** * Make the UI frame visible or not. * * @param bool true for visible, false for hidden */ public void setVisible (boolean bool) { MainGui.getInstance().show(frame); } //--------// // verify // //--------// /** * Focus the verifier on a provided collection of glyphs. * (typically this collection are the glyphs that are not recognized, * or mistaken, by the evaluator) * * @param glyphNames the names of the specific glyphs to inspect */ public void verify (Collection<String> glyphNames) { // Glyphs glyphSelector.populateWith(glyphNames); glyphSelector.selectAll(); // Shapes EnumSet<Shape> shapeSet = EnumSet.noneOf(Shape.class); for (String gName : glyphNames) { File file = new File(gName); shapeSet.add(Shape.valueOf(radixOf(file.getName()))); } shapeSelector.populateWith(shapeSet); shapeSelector.selectAll(); // Sheets / Icons folder SortedSet<String> folderSet = new TreeSet<>(); for (String gName : glyphNames) { File file = new File(gName); folderSet.add(file.getParent()); } folderSelector.populateWith(folderSet); folderSelector.selectAll(); // Load the first glyph in the browser glyphBrowser.loadGlyphNames(); } //-----------------// // deleteGlyphName // //-----------------// /** * Remove a glyph name from the current selection. * * @param gName the glyph name to remove */ void deleteGlyphName (String gName) { // Remove entry from glyph list glyphSelector.model.removeElement(gName); } //---------------// // getGlyphCount // //---------------// /** * Report the number of currently selected glyphs names. * * @return the number of selected glyphs names */ int getGlyphCount () { return glyphSelector.list.getSelectedIndices().length; } //---------------// // getGlyphNames // //---------------// /** * Report the collection of currently selected glyphs names. * * @return an list of glyphs names */ List<String> getGlyphNames () { return glyphSelector.list.getSelectedValuesList(); } //---------// // radixOf // //---------// private static String radixOf (String path) { int i = path.indexOf('.'); if (i >= 0) { return path.substring(0, i); } else { return ""; } } //--------------// // getActualDir // //--------------// /** * Report the real directory that corresponds to a given folder name. * (either the sheets or samples directory or the symbols directory) * * @param folder the folder name, such as 'symbols' or 'sheets/batuque' or * 'samples/batuque' * @return the concrete directory */ private File getActualDir (String folder) { if (repository.isIconsFolder(folder)) { return WellKnowns.SYMBOLS_FOLDER; } else { int slashPos = folder.indexOf(File.separatorChar); String root = folder.substring(0, slashPos); String name = folder.substring(slashPos + 1); if (root.equals(sheetsFolder.getName())) { return new File(sheetsFolder, name); } else if (root.equals(samplesFolder.getName())) { return new File(samplesFolder, name); } else { throw new IllegalArgumentException("Unexpected root: " + root); } } } //-------------------// // getSelectorsPanel // //-------------------// private JPanel getSelectorsPanel () { FormLayout layout = new FormLayout( "max(100dlu;pref),max(150dlu;pref),max(200dlu;pref):grow", // Cols "pref:grow"); // Rows PanelBuilder builder = new PanelBuilder(layout); builder.setDefaultDialogBorder(); CellConstraints cst = new CellConstraints(); int r = 1; // -------------------------------- builder.add(folderSelector, cst.xy(1, r)); builder.add(shapeSelector, cst.xy(2, r)); builder.add(glyphSelector, cst.xy(3, r)); return builder.getPanel(); } //~ Inner Classes ---------------------------------------------------------- //----------------// // FolderSelector // //----------------// private class FolderSelector extends Selector<String> { //~ Constructors ------------------------------------------------------- public FolderSelector (ChangeListener listener) { super("Folders", listener, 300); load.setEnabled(true); } //~ Methods ------------------------------------------------------------ // Triggered by load button @Override public void actionPerformed (ActionEvent e) { model.removeAllElements(); // First insert the dedicated icons folder model.addElement(WellKnowns.SYMBOLS_FOLDER.getName()); // Then the sheets folders String root = repository.getSheetsFolder().getName(); ArrayList<String> folders = new ArrayList<>(); for (File file : repository.getSheetDirectories()) { folders.add(root + File.separator + file.getName()); } // Finally, the samples folders root = repository.getSamplesFolder().getName(); for (File file : repository.getSampleDirectories()) { folders.add(root + File.separator + file.getName()); } Collections.sort(folders); for (String folder : folders) { model.addElement(folder); } updateCardinal(); } } //---------------// // GlyphSelector // //---------------// private class GlyphSelector extends Selector<String> { //~ Constructors ------------------------------------------------------- public GlyphSelector (ChangeListener listener) { super("Glyphs", listener, 300); } //~ Methods ------------------------------------------------------------ // Triggered by the load button @Override public void actionPerformed (ActionEvent e) { final List<String> folders = folderSelector.list. getSelectedValuesList(); final List<Shape> shapes = shapeSelector.list. getSelectedValuesList(); // Debug if (logger.isDebugEnabled()) { logger.debug("Glyph Selector. Got Folders:"); for (String fName : folders) { logger.debug(fName); } logger.debug("Glyph Selector. Got Shapes:"); for (Shape shape : shapes) { logger.debug(shape.toString()); } } if (shapes.isEmpty()) { logger.warn("No shapes selected in Shape Selector"); } else { model.removeAllElements(); // Populate with all possible glyphs, sorted by gName for (String folder : folders) { // Add proper glyphs files from this directory ArrayList<String> gNames = new ArrayList<>(); File dir = getActualDir(folder); for (File file : repository.getGlyphsIn(dir)) { String shapeName = radixOf(file.getName()); Shape shape = Shape.valueOf(shapeName); if (shapes.contains(shape)) { gNames.add( folder + File.separator + file.getName()); } } Collections.sort(gNames); for (String gName : gNames) { model.addElement(gName); } } updateCardinal(); } } } //----------// // Selector // //----------// /** * Class {@code Selector} defines the common properties of sheet, * shape and glyph selectors. * Each selector is made of a list of names, which can be selected and * deselected at will. */ private abstract static class Selector<E> extends TitledPanel implements ActionListener, ChangeListener { //~ Instance fields ---------------------------------------------------- /** The title base for this selector */ private final String title; /** Other entity interested in items selected by this selector */ private ChangeListener listener; /** Change event, lazily created */ private ChangeEvent changeEvent; // Buttons protected JButton load = new JButton("Load"); protected JButton selectAll = new JButton( "Select All"); protected JButton cancelAll = new JButton( "Cancel All"); // List of items, with its model protected final DefaultListModel<E> model = new DefaultListModel<>(); protected JList<E> list = new JList<>(model); // ScrollPane around the list protected JScrollPane scrollPane = new JScrollPane(list); //~ Constructors ------------------------------------------------------- //----------// // Selector // //----------// /** * Create a selector. * * @param title label for this selector * @param listener potential (external) listener for changes * @param preferred width */ public Selector (String title, ChangeListener listener, int width) { super(title, width); this.title = title; this.listener = listener; // Precise action to be specified in each subclass load.addActionListener(this); ///list.setVisibleRowCount(10); ///scrollPane.setMinimumSize(new Dimension(250, 300)); // To be informed of mouse (de)selections (not programmatic) list.addListSelectionListener( new ListSelectionListener() { @Override public void valueChanged (ListSelectionEvent e) { updateCardinal(); // Brute force !!! } }); // Same action whatever the subclass : select all items selectAll.addActionListener( new ActionListener() { @Override public void actionPerformed (ActionEvent e) { selectAll(); } }); // Same action whatever the subclass : deselect all items cancelAll.addActionListener( new ActionListener() { @Override public void actionPerformed (ActionEvent e) { list.setSelectedIndices(new int[0]); updateCardinal(); } }); JPanel buttons = new JPanel(new GridLayout(3, 1)); buttons.add(load); buttons.add(selectAll); buttons.add(cancelAll); // All buttons are initially disabled load.setEnabled(false); selectAll.setEnabled(false); cancelAll.setEnabled(false); add(buttons, BorderLayout.WEST); add(scrollPane, BorderLayout.CENTER); } //~ Methods ------------------------------------------------------------ //--------------// // populateWith // //--------------// public void populateWith (Collection<E> items) { model.removeAllElements(); for (E item : items) { model.addElement(item); } updateCardinal(); } //-----------// // selectAll // //-----------// public void selectAll () { list.setSelectionInterval(0, model.size() - 1); updateCardinal(); } //--------------// // stateChanged // //--------------// @Override public void stateChanged (ChangeEvent e) { Selector<?> selector = (Selector<?>) e.getSource(); int selNb = selector.list.getSelectedIndices().length; load.setEnabled(selNb > 0); } //----------------// // updateCardinal // //----------------// protected void updateCardinal () { int[] selection = list.getSelectedIndices(); int selectNb = selection.length; TitledBorder border = (TitledBorder) getBorder(); if (selectNb > 0) { border.setTitle(title + ": " + selectNb); } else { border.setTitle(title); } // Buttons selectAll.setEnabled(model.size() > 0); cancelAll.setEnabled(selection.length > 0); // Notify other entity if (listener != null) { if (changeEvent == null) { changeEvent = new ChangeEvent(this); } listener.stateChanged(changeEvent); } repaint(); } } //-------------------// // ShapeCellRenderer // //-------------------// private class ShapeCellRenderer extends JLabel implements ListCellRenderer<Shape> { //~ Constructors ------------------------------------------------------- public ShapeCellRenderer () { setOpaque(true); } //~ Methods ------------------------------------------------------------ /* * This method finds the image and text corresponding * to the selected value and returns the label, set up * to display the text and image. */ @Override public Component getListCellRendererComponent ( JList<? extends Shape> list, Shape shape, int index, boolean isSelected, boolean cellHasFocus) { if (isSelected) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } else { setBackground(list.getBackground()); setForeground(shape.getColor()); } setFont(list.getFont()); setText(shape.toString()); setIcon(shape.getDecoratedSymbol()); return this; } } //---------------// // ShapeSelector // //---------------// private class ShapeSelector extends Selector<Shape> { //~ Constructors ------------------------------------------------------- public ShapeSelector (ChangeListener listener) { super("Shapes", listener, 150); list.setCellRenderer(new ShapeCellRenderer()); ///list.setFixedCellHeight(60); } //~ Methods ------------------------------------------------------------ // Triggered by load button @Override public void actionPerformed (ActionEvent e) { // Populate with shape names found in selected folders List<String> folders = folderSelector.list.getSelectedValuesList(); if (folders.isEmpty()) { logger.warn("No folders selected in Folder Selector"); } else { EnumSet<Shape> shapeSet = EnumSet.noneOf(Shape.class); for (String folder : folders) { File dir = getActualDir(folder); // Add all glyphs files from this directory for (File file : repository.getGlyphsIn(dir)) { shapeSet.add(Shape.valueOf(radixOf(file.getName()))); } } populateWith(shapeSet); } } } //-------------// // TitledPanel // //-------------// private static class TitledPanel extends JPanel { //~ Instance fields ---------------------------------------------------- protected final int height = 200; //~ Constructors ------------------------------------------------------- public TitledPanel (String title, int width) { setBorder( BorderFactory.createTitledBorder( new EtchedBorder(), title, TitledBorder.LEFT, TitledBorder.TOP)); setLayout(new BorderLayout()); setMinimumSize(new Dimension(200, height)); setPreferredSize(new Dimension(width, height)); } } }