//----------------------------------------------------------------------------//
// //
// S h a p e B o a r d //
// //
//----------------------------------------------------------------------------//
// <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.Main;
import omr.constant.Constant;
import omr.constant.ConstantSet;
import omr.glyph.Glyphs;
import omr.glyph.Shape;
import omr.glyph.ShapeSet;
import omr.glyph.facets.Glyph;
import omr.script.InsertTask;
import omr.selection.MouseMovement;
import omr.selection.UserEvent;
import omr.sheet.Sheet;
import omr.ui.Board;
import omr.ui.dnd.AbstractGhostDropListener;
import omr.ui.dnd.GhostDropAdapter;
import omr.ui.dnd.GhostDropEvent;
import omr.ui.dnd.GhostDropListener;
import omr.ui.dnd.GhostGlassPane;
import omr.ui.dnd.GhostMotionAdapter;
import omr.ui.dnd.ScreenPoint;
import omr.ui.symbol.MusicFont;
import omr.ui.symbol.ShapeSymbol;
import omr.ui.util.Panel;
import omr.ui.view.RubberPanel;
import omr.ui.view.ScrollView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JFrame;
/**
* Class {@code ShapeBoard} hosts a palette of shapes for insertion and
* assignment of glyph.
* <ul>
* <li>Direct insertion is performed by drag and drop to the target score
* view or sheet view</li>
* <li>Assignment of existing glyph is performed by a double-click</li>
* </ul>
*
* @author Hervé Bitteur
*/
public class ShapeBoard
extends Board
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(ShapeBoard.class);
/** To force the width of the various panels */
private static final int BOARD_WIDTH = 280;
/**
* To force the height of the various shape panels (just a dirty hack)
*/
private static final Map<ShapeSet, Integer> heights = new HashMap<>();
static {
heights.put(ShapeSet.Accidentals, 40);
heights.put(ShapeSet.Articulations, 60);
heights.put(ShapeSet.Attributes, 40);
heights.put(ShapeSet.Barlines, 100);
heights.put(ShapeSet.Beams, 60);
heights.put(ShapeSet.Clefs, 140);
heights.put(ShapeSet.Dynamics, 220);
heights.put(ShapeSet.Flags, 130);
heights.put(ShapeSet.Keys, 220);
heights.put(ShapeSet.NoteHeads, 40);
heights.put(ShapeSet.Markers, 120);
heights.put(ShapeSet.Notes, 40);
heights.put(ShapeSet.Ornaments, 80);
heights.put(ShapeSet.Rests, 120);
heights.put(ShapeSet.Times, 130);
heights.put(ShapeSet.Physicals, 150);
}
//~ Instance fields --------------------------------------------------------
/** Related sheet */
private final Sheet sheet;
/** The controller in charge of symbol assignments */
private final SymbolsController symbolsController;
/**
* Method called when a range is selected: the panel of ranges is
* replaced by the panel of shapes that compose the selected range.
*/
private ActionListener rangeListener = new ActionListener()
{
@Override
public void actionPerformed (ActionEvent e)
{
// Remove panel of ranges
getBody()
.remove(rangesPanel);
// Replace by proper panel of range shapes
String rangeName = ((JButton) e.getSource()).getName();
ShapeSet range = ShapeSet.getShapeSet(rangeName);
shapesPanel = shapesPanels.get(range);
if (shapesPanel == null) {
// Lazily populate the map of shapesPanel instances
shapesPanels.put(range, shapesPanel = defineShapesPanel(range));
}
getBody()
.add(shapesPanel);
// Perhaps this is too much ... TODO
JFrame frame = Main.getGui()
.getFrame();
frame.invalidate();
frame.validate();
frame.repaint();
}
};
/**
* Method called when a panel of shapes is closed: the panel is
* replaced by the panel of ranges to allow the selection of another
* range.
*/
private ActionListener closeListener = new ActionListener()
{
@Override
public void actionPerformed (ActionEvent e)
{
// Remove current panel of shapes
getBody()
.remove(shapesPanel);
// Replace by panel of ranges
getBody()
.add(rangesPanel);
// Perhaps this is too much ... TODO
JFrame frame = Main.getGui()
.getFrame();
frame.invalidate();
frame.validate();
frame.repaint();
}
};
/**
* Method called when a shape button is clicked.
*/
private MouseListener mouseListener = new MouseAdapter()
{
// Ability to use the button for direct assignment via double-click
@Override
public void mouseClicked (MouseEvent e)
{
if (e.getClickCount() == 2) {
Glyph glyph = sheet.getNest()
.getSelectedGlyph();
if (glyph != null) {
ShapeButton button = (ShapeButton) e.getSource();
// Actually assign the shape
symbolsController.asyncAssignGlyphs(
Glyphs.sortedSet(glyph),
button.shape,
false);
}
}
}
};
/** Panel of all ranges */
private final Panel rangesPanel;
/** Map of shape panels */
private final Map<ShapeSet, Panel> shapesPanels = new HashMap<>();
/** Current panel of shapes */
private Panel shapesPanel;
/** GlassPane */
private GhostGlassPane glassPane = Main.getGui()
.getGlassPane();
// Update image and forward mouse location
private final MyMotionAdapter motionAdapter = new MyMotionAdapter(
glassPane);
// When symbol is dropped
private final GhostDropListener<Shape> dropListener = new MyDropListener();
// When mouse pressed (start) and released (stop)
private final GhostDropAdapter<Shape> dropAdapter = new MyDropAdapter(
glassPane);
//~ Constructors -----------------------------------------------------------
//------------//
// ShapeBoard //
//------------//
/**
* Create a new ShapeBoard object.
*
* @param sheet the related sheet
* @param symbolsController the UI controller for symbols
* @param expanded true if initially expanded
*/
public ShapeBoard (Sheet sheet,
SymbolsController symbolsController,
boolean expanded)
{
super(Board.SHAPE, null, null, false, expanded);
this.symbolsController = symbolsController;
this.sheet = sheet;
dropAdapter.addDropListener(dropListener);
getBody()
.add(rangesPanel = defineRangesPanel());
}
//~ Methods ----------------------------------------------------------------
//---------//
// onEvent //
//---------//
/**
* Unused in this board.
*
* @param event unused
*/
@Override
public void onEvent (UserEvent event)
{
// Empty
}
//-------------------//
// defineRangesPanel //
//-------------------//
/**
* Define the global panel of ranges.
*
* @return the global panel of ranges
*/
private Panel defineRangesPanel ()
{
Panel panel = new Panel();
panel.setNoInsets();
panel.setPreferredSize(new Dimension(BOARD_WIDTH, 180));
FlowLayout layout = new FlowLayout();
layout.setAlignment(FlowLayout.LEADING);
panel.setLayout(layout);
for (ShapeSet range : ShapeSet.getShapeSets()) {
Shape rep = range.getRep();
if (rep != null) {
JButton button = new JButton();
button.setIcon(rep.getDecoratedSymbol());
button.setName(range.getName());
button.addActionListener(rangeListener);
button.setToolTipText(range.getName());
button.setBorderPainted(false);
panel.add(button);
}
}
return panel;
}
//-------------------//
// defineShapesPanel //
//-------------------//
/**
* Define the panel of shapes for a given range.
*
* @param range the given range of shapes
* @return the panel of shapes for the provided range
*/
private Panel defineShapesPanel (ShapeSet range)
{
Panel panel = new Panel();
panel.setNoInsets();
panel.setPreferredSize(new Dimension(BOARD_WIDTH, heights.get(range)));
FlowLayout layout = new FlowLayout();
layout.setAlignment(FlowLayout.LEADING);
panel.setLayout(layout);
// Button to close this shapes panel and return to ranges panel
JButton close = new JButton(range.getName());
close.addActionListener(closeListener);
close.setToolTipText("Back to ranges");
close.setBorderPainted(false);
panel.add(close);
// One button per shape
for (Shape shape : range.getSortedShapes()) {
ShapeButton button = new ShapeButton(shape);
button.addMouseListener(dropAdapter); // For DnD transfer
button.addMouseListener(mouseListener); // For double-click
button.addMouseMotionListener(motionAdapter); // For dragging
panel.add(button);
}
return panel;
}
//--------------//
// getIconImage //
//--------------//
/**
* Get the image to draw as an icon for the provided shape.
*
* @param shape the provided shape
* @return an image properly sized for an icon
*/
private BufferedImage getIconImage (Shape shape)
{
ShapeSymbol symbol = (shape == Shape.BEAM_HOOK)
? shape.getPhysicalShape()
.getSymbol() : shape.getDecoratedSymbol();
return symbol.getIconImage();
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Constant.Boolean publishLocationWhileDragging = new Constant.Boolean(
false,
"Should we publish the current location while dragging a shape?");
}
//-------------//
// ShapeButton //
//-------------//
/**
* A button dedicated to a shape.
*/
private static class ShapeButton
extends JButton
{
//~ Instance fields ----------------------------------------------------
final Shape shape;
//~ Constructors -------------------------------------------------------
public ShapeButton (Shape shape)
{
this.shape = shape;
setIcon(shape.getDecoratedSymbol());
setName(shape.toString());
setToolTipText(shape.toString());
setBorderPainted(true);
}
}
//---------------//
// MyDropAdapter //
//---------------//
/**
* DnD adapter called when mouse is pressed and released.
*/
private class MyDropAdapter
extends GhostDropAdapter<Shape>
{
//~ Constructors -------------------------------------------------------
public MyDropAdapter (GhostGlassPane glassPane)
{
super(glassPane, null);
}
//~ Methods ------------------------------------------------------------
/** Start of DnD, set pay load */
@Override
public void mousePressed (MouseEvent e)
{
// Reset the motion adapter
motionAdapter.reset();
ShapeButton button = (ShapeButton) e.getSource();
Shape shape = button.shape;
// Set shape & image
if (shape.isDraggable()) {
action = shape;
image = getIconImage(shape);
} else {
action = Shape.NON_DRAGGABLE;
image = Shape.NON_DRAGGABLE.getSymbol()
.getIconImage();
}
super.mousePressed(e);
}
}
//----------------//
// MyDropListener //
//----------------//
/**
* Listener called when DnD shape is dropped.
*/
private class MyDropListener
extends AbstractGhostDropListener<Shape>
{
//~ Constructors -------------------------------------------------------
public MyDropListener ()
{
// Target will be any view of sheet assembly
super(null);
}
//~ Methods ------------------------------------------------------------
@Override
public void dropped (GhostDropEvent<Shape> e)
{
Shape shape = e.getAction();
if (shape != Shape.NON_DRAGGABLE) {
ScreenPoint screenPoint = e.getDropLocation();
// The (zoomed) sheet view
ScrollView scrollView = sheet.getAssembly()
.getSelectedView();
if (screenPoint.isInComponent(
scrollView.getComponent().getViewport())) {
RubberPanel view = scrollView.getView();
Point localPt = screenPoint.getLocalPoint(view);
view.getZoom()
.unscale(localPt);
// Asynchronously insert the desired shape at proper location
new InsertTask(
sheet,
shape,
Collections.singleton(
new Point(localPt.x, localPt.y))).launch(
sheet);
}
}
}
}
//-----------------//
// MyMotionAdapter //
//-----------------//
/**
* Adapter in charge of forwarding the current mouse location and
* updating the dragged image according to the target under the mouse.
*/
private class MyMotionAdapter
extends GhostMotionAdapter
{
//~ Instance fields ----------------------------------------------------
// Optimization: remember the latest component on target
private WeakReference<Component> prevComponent;
//~ Constructors -------------------------------------------------------
public MyMotionAdapter (GhostGlassPane glassPane)
{
super(glassPane);
reset();
}
//~ Methods ------------------------------------------------------------
/**
* In this specific implementation, we update the size of the
* shape image according to the interline scale and to the
* display zoom of the droppable target underneath.
*
* @param e the mouse event
*/
@Override
public void mouseDragged (MouseEvent e)
{
ShapeButton button = (ShapeButton) e.getSource();
Shape shape = button.shape;
Point absPt = e.getLocationOnScreen();
ScreenPoint screenPoint = new ScreenPoint(absPt.x, absPt.y);
// The (zoomed) sheet view
ScrollView scrollView = sheet.getAssembly()
.getSelectedView();
Component component = scrollView.getComponent()
.getViewport();
if (screenPoint.isInComponent(component)) {
RubberPanel view = scrollView.getView();
// Publish the current location?
if (constants.publishLocationWhileDragging.getValue()) {
Point localPt = screenPoint.getLocalPoint(view);
view.getZoom()
.unscale(localPt);
view.pointSelected(localPt, MouseMovement.DRAGGING);
}
// Moving into this component?
if (component != prevComponent.get()) {
glassPane.setOverTarget(true);
// Try to use full image size, adapted to current zoom
int zoomedInterline = (int) Math.rint(
view.getZoom().getRatio() * sheet.getScale().getInterline());
Shape displayedShape = shape.isDraggable() ? shape
: Shape.NON_DRAGGABLE;
BufferedImage image = MusicFont.buildImage(
displayedShape,
zoomedInterline,
true); // Decorated
if (image != null) {
// Use of perfectly sized font-based image
glassPane.setImage(image);
}
prevComponent = new WeakReference<>(component);
}
} else if (prevComponent.get() != null) {
// No longer on a droppable target, reuse initial image & size
glassPane.setOverTarget(false);
glassPane.setImage(dropAdapter.getImage());
reset();
}
// This triggers a repaint of glassPane
glassPane.setPoint(screenPoint);
}
public final void reset ()
{
prevComponent = new WeakReference<>(null);
}
}
}