/*
* Copyright (C) 2006-2014 Gabriel Burca (gburca dash virtmus at ebixio dot com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.ebixio.annotations;
import com.ebixio.annotations.tools.DrawingTool;
import com.ebixio.annotations.tools.ToolFreehand;
import com.ebixio.annotations.tools.ToolLine;
import com.ebixio.annotations.tools.ToolRect;
import com.ebixio.virtmus.CommonExplorers;
import com.ebixio.virtmus.MusicPage;
import java.awt.Color;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ItemEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.CancellationException;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JSlider;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openide.ErrorManager;
import org.openide.awt.UndoRedo;
import org.openide.explorer.ExplorerManager;
import org.openide.nodes.Node;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
/**
* Top component which displays the music page annotations.
*/
public final class AnnotTopComponent extends TopComponent
implements ComponentListener {
private static AnnotTopComponent instance;
/** path to the icon used by the component and its open action */
static final String ICON_PATH = "com/ebixio/annotations/annot-tab-icon.png";
private static final String PREFERRED_ID = "AnnotTopComponent";
private PlanarImage source = null;
private PlanarImage scaledSource = null;
private MusicPage currentlyShowing = null;
transient private final PropertyChangeListener eListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (ExplorerManager.PROP_SELECTED_NODES.equals(evt.getPropertyName())) {
Node[] selectedNodes = (Node[]) evt.getNewValue();
updateSelection(selectedNodes);
}
}
};
static {
// Linux paints the slider value above the slider throwing off the toolbar size
UIManager.put("Slider.paintValue", Boolean.FALSE);
}
private AnnotTopComponent() {
initComponents();
setName(NbBundle.getMessage(AnnotTopComponent.class, "CTL_AnnotTopComponent"));
setToolTipText(NbBundle.getMessage(AnnotTopComponent.class, "HINT_AnnotTopComponent"));
setIcon(ImageUtilities.loadImage(ICON_PATH, true));
panner.setBorder(new LineBorder(Color.RED, 2));
panner.setVisible(false);
this.addComponentListener(this);
// Initialize the canvas with the default alpha value
jsAlphaStateChanged(null);
// Initialize the color chooser with a random bright color
colorChooser.setColor(Color.getHSBColor((float)Math.random(), 0.9F, 0.9F));
colorChooserActionPerformed(null);
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jToolBar = new javax.swing.JToolBar();
toolChooser = new javax.swing.JComboBox<DrawingTool>();
jSeparator6 = new javax.swing.JToolBar.Separator();
jLabel1 = new javax.swing.JLabel();
colorChooser = new net.java.dev.colorchooser.ColorChooser();
jSeparator4 = new javax.swing.JToolBar.Separator();
jLabel2 = new javax.swing.JLabel();
jsBrushSize = new javax.swing.JSlider();
brushPreview = new com.ebixio.annotations.BrushPreview();
jSeparator5 = new javax.swing.JToolBar.Separator();
jLabel4 = new javax.swing.JLabel();
jsAlpha = new javax.swing.JSlider();
jSeparator2 = new javax.swing.JToolBar.Separator();
jbClear = new javax.swing.JButton();
jSeparator1 = new javax.swing.JToolBar.Separator();
jLabel3 = new javax.swing.JLabel();
jsZoom = new javax.swing.JSlider();
canvasPanel = new javax.swing.JPanel();
canvas = new com.ebixio.annotations.AnnotCanvas();
panner = new com.ebixio.jai.Panner();
setBackground(new java.awt.Color(153, 255, 153));
jToolBar.setFloatable(false);
toolChooser.setModel(getTools());
toolChooser.setMaximumSize(new java.awt.Dimension(100, 20));
toolChooser.addItemListener(new java.awt.event.ItemListener() {
public void itemStateChanged(java.awt.event.ItemEvent evt) {
toolChooserItemStateChanged(evt);
}
});
jToolBar.add(toolChooser);
jToolBar.add(jSeparator6);
org.openide.awt.Mnemonics.setLocalizedText(jLabel1, "Color:");
jToolBar.add(jLabel1);
colorChooser.setToolTipText("Foreground color");
colorChooser.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
colorChooserActionPerformed(evt);
}
});
javax.swing.GroupLayout colorChooserLayout = new javax.swing.GroupLayout(colorChooser);
colorChooser.setLayout(colorChooserLayout);
colorChooserLayout.setHorizontalGroup(
colorChooserLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 22, Short.MAX_VALUE)
);
colorChooserLayout.setVerticalGroup(
colorChooserLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 22, Short.MAX_VALUE)
);
jToolBar.add(colorChooser);
jToolBar.add(jSeparator4);
org.openide.awt.Mnemonics.setLocalizedText(jLabel2, "Size: ");
jLabel2.setHorizontalTextPosition(javax.swing.SwingConstants.RIGHT);
jToolBar.add(jLabel2);
jsBrushSize.setMajorTickSpacing(4);
jsBrushSize.setMaximum(24);
jsBrushSize.setMinimum(1);
jsBrushSize.setToolTipText("Brush size");
jsBrushSize.setMaximumSize(new java.awt.Dimension(100, 25));
jsBrushSize.setPreferredSize(new java.awt.Dimension(85, 38));
jsBrushSize.setValue(canvas.getDiam());
jsBrushSize.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
jsBrushSizeStateChanged(evt);
}
});
jToolBar.add(jsBrushSize);
brushPreview.setMaximumSize(new java.awt.Dimension(32, 32));
javax.swing.GroupLayout brushPreviewLayout = new javax.swing.GroupLayout(brushPreview);
brushPreview.setLayout(brushPreviewLayout);
brushPreviewLayout.setHorizontalGroup(
brushPreviewLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 32, Short.MAX_VALUE)
);
brushPreviewLayout.setVerticalGroup(
brushPreviewLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 32, Short.MAX_VALUE)
);
jToolBar.add(brushPreview);
jToolBar.add(jSeparator5);
org.openide.awt.Mnemonics.setLocalizedText(jLabel4, "Opacity:");
jToolBar.add(jLabel4);
jsAlpha.setMinimum(1);
jsAlpha.setToolTipText("Opacity");
jsAlpha.setValue(70);
jsAlpha.setMaximumSize(new java.awt.Dimension(100, 25));
jsAlpha.setPreferredSize(new java.awt.Dimension(85, 38));
jsAlpha.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
jsAlphaStateChanged(evt);
}
});
jToolBar.add(jsAlpha);
jToolBar.add(jSeparator2);
org.openide.awt.Mnemonics.setLocalizedText(jbClear, "Clear");
jbClear.setToolTipText("Remove all annotations");
jbClear.setBorder(javax.swing.BorderFactory.createBevelBorder(javax.swing.border.BevelBorder.RAISED));
jbClear.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jbClearActionPerformed(evt);
}
});
jToolBar.add(jbClear);
jToolBar.add(jSeparator1);
org.openide.awt.Mnemonics.setLocalizedText(jLabel3, "Zoom:");
jLabel3.setToolTipText("Zoom");
jToolBar.add(jLabel3);
jsZoom.setMajorTickSpacing(1);
jsZoom.setMaximum(1000);
jsZoom.setMinimum(10);
jsZoom.setMinorTickSpacing(1);
jsZoom.setToolTipText("Zoom");
jsZoom.setValue(1000);
jsZoom.setMaximumSize(new java.awt.Dimension(100, 38));
jsZoom.setPreferredSize(new java.awt.Dimension(85, 38));
jsZoom.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
jsZoomStateChanged(evt);
}
});
jToolBar.add(jsZoom);
canvasPanel.setBackground(new java.awt.Color(0, 0, 0));
canvas.setOpaque(true);
panner.setBackground(new java.awt.Color(204, 204, 255));
panner.setMaximumSize(new java.awt.Dimension(64, 100));
panner.setMinimumSize(new java.awt.Dimension(64, 100));
panner.setPreferredSize(new java.awt.Dimension(64, 100));
javax.swing.GroupLayout canvasLayout = new javax.swing.GroupLayout(canvas);
canvas.setLayout(canvasLayout);
canvasLayout.setHorizontalGroup(
canvasLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, canvasLayout.createSequentialGroup()
.addContainerGap(713, Short.MAX_VALUE)
.addComponent(panner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap())
);
canvasLayout.setVerticalGroup(
canvasLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(canvasLayout.createSequentialGroup()
.addContainerGap()
.addComponent(panner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap(271, Short.MAX_VALUE))
);
javax.swing.GroupLayout canvasPanelLayout = new javax.swing.GroupLayout(canvasPanel);
canvasPanel.setLayout(canvasPanelLayout);
canvasPanelLayout.setHorizontalGroup(
canvasPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(canvas, javax.swing.GroupLayout.DEFAULT_SIZE, 787, Short.MAX_VALUE)
);
canvasPanelLayout.setVerticalGroup(
canvasPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(canvas, javax.swing.GroupLayout.DEFAULT_SIZE, 346, Short.MAX_VALUE)
);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jToolBar, javax.swing.GroupLayout.DEFAULT_SIZE, 787, Short.MAX_VALUE)
.addComponent(canvasPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(jToolBar, javax.swing.GroupLayout.PREFERRED_SIZE, 35, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(canvasPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
}// </editor-fold>//GEN-END:initComponents
private void jsZoomStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_jsZoomStateChanged
resizeImage((float)jsZoom.getValue() / (float)jsZoom.getMaximum());
}//GEN-LAST:event_jsZoomStateChanged
private void jsBrushSizeStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_jsBrushSizeStateChanged
if (evt == null) return;
JSlider js = (JSlider) evt.getSource();
int newValue = js.getValue();
canvas.setDiam( newValue );
canvas.setThreshold(255 - newValue);
brushPreview.setDiam(newValue);
}//GEN-LAST:event_jsBrushSizeStateChanged
private void jbClearActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbClearActionPerformed
canvas.clear();
canvas.undoManager.discardAllEdits();
//canvas.musicPage.popAnnotation();
}//GEN-LAST:event_jbClearActionPerformed
private void colorChooserActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_colorChooserActionPerformed
canvas.setPaint (colorChooser.getColor());
brushPreview.setColor(colorChooser.getColor());
}//GEN-LAST:event_colorChooserActionPerformed
private void jsAlphaStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_jsAlphaStateChanged
canvas.setAlpha(jsAlpha.getValue() / 100.0F);
}//GEN-LAST:event_jsAlphaStateChanged
private void toolChooserItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_toolChooserItemStateChanged
if (evt.getStateChange() == ItemEvent.SELECTED) {
canvas.tool = (DrawingTool)evt.getItem();
}
}//GEN-LAST:event_toolChooserItemStateChanged
// Variables declaration - do not modify//GEN-BEGIN:variables
private com.ebixio.annotations.BrushPreview brushPreview;
private com.ebixio.annotations.AnnotCanvas canvas;
private javax.swing.JPanel canvasPanel;
private net.java.dev.colorchooser.ColorChooser colorChooser;
private javax.swing.JLabel jLabel1;
private javax.swing.JLabel jLabel2;
private javax.swing.JLabel jLabel3;
private javax.swing.JLabel jLabel4;
private javax.swing.JToolBar.Separator jSeparator1;
private javax.swing.JToolBar.Separator jSeparator2;
private javax.swing.JToolBar.Separator jSeparator4;
private javax.swing.JToolBar.Separator jSeparator5;
private javax.swing.JToolBar.Separator jSeparator6;
private javax.swing.JToolBar jToolBar;
private javax.swing.JButton jbClear;
private javax.swing.JSlider jsAlpha;
private javax.swing.JSlider jsBrushSize;
private javax.swing.JSlider jsZoom;
private com.ebixio.jai.Panner panner;
private javax.swing.JComboBox<DrawingTool> toolChooser;
// End of variables declaration//GEN-END:variables
// <editor-fold defaultstate="collapsed" desc=" Singleton ">
/**
* Gets default instance. Do not use directly: reserved for *.settings files only,
* i.e. deserialization routines; otherwise you could get a non-deserialized instance.
* To obtain the singleton instance, use {@link findInstance}.
* @return The AnnotTopComponent singleton
*/
public static synchronized AnnotTopComponent getDefault() {
if (instance == null) {
instance = new AnnotTopComponent();
}
return instance;
}
/**
* Obtain the AnnotTopComponent instance. Never call {@link #getDefault} directly!
* @return The AnnotTopComponent singleton
*/
public static synchronized AnnotTopComponent findInstance() {
TopComponent win = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (win == null) {
ErrorManager.getDefault().log(ErrorManager.WARNING,
"Cannot find AnnotTopComponent component. It will not be located properly in the window system.");
return getDefault();
}
if (win instanceof AnnotTopComponent) {
return (AnnotTopComponent)win;
}
ErrorManager.getDefault().log(ErrorManager.WARNING,
"There seem to be multiple components with the '" + PREFERRED_ID +
"' ID. That is a potential source of errors and unexpected behavior.");
return getDefault();
}
// </editor-fold>
@Override
public int getPersistenceType() {
return TopComponent.PERSISTENCE_ALWAYS;
}
@Override
public void addNotify() {
CommonExplorers.MainExplorerManager.addPropertyChangeListener(eListener);
CommonExplorers.TagsExplorerManager.addPropertyChangeListener(eListener);
super.addNotify();
}
@Override
public void removeNotify() {
super.removeNotify();
CommonExplorers.MainExplorerManager.removePropertyChangeListener(eListener);
CommonExplorers.TagsExplorerManager.removePropertyChangeListener(eListener);
}
@Override
public UndoRedo getUndoRedo() {
return canvas.undoManager;
}
/**
* This function gets called every time the selected nodes change.
* The results change to "nothing" when the focus moves away from the TopComponent
* that contains the nodes. It gets changed to the selected node when focus returns
* to the TopComponent, etc...
*
* Since setImage is very time-consuming for large images, we want to do it only if
* it is different from the image being currently displayed.
*/
private void updateSelection(Node[] nodes) {
if (nodes.length > 0) {
MusicPage mp = nodes[0].getLookup().lookup(MusicPage.class);
// Only change the currently showing node if another MusicPage was
// selected. Ignore Song or PlayList nodes.
if (mp != null && currentlyShowing != mp) {
currentlyShowing = mp;
this.showPage(mp);
}
} else {
currentlyShowing = null;
this.showPage(null);
}
}
/**
*
* @param scale A scaling percentage between [0.1 .. 1]
*/
public void resizeImage(float scale) {
if (source == null) {
return;
}
if (scale < 0.1) {
scale = 0.1F;
} else if (scale > 1) {
scale = 1.0F;
}
canvas.setScale(scale);
// Use "SubsampleAverage" because it looks much better.
RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
scaledSource = JAI.create("SubsampleAverage", source, (double)scale, (double)scale, qualityHints);
showImage(scaledSource);
}
public void showPage(MusicPage page) {
source = scaledSource = null;
// Reset zoom so the change property fires when the page is scaled to fit.
this.jsZoom.setValue(100 * 10);
canvas.setMusicPage(page);
if (page == null) {
canvas.repaint();
return;
}
page.setChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
canvas.repaint();
}
});
PlanarImage src = currentlyShowing.imgSrc.getFullImg();
if (src != null) {
source = src;
} else {
return;
}
// Loading an image takes some time. We do it on a separate thread.
SwingWorker w = new SwingWorker<Boolean, Void>() {
@Override
protected Boolean doInBackground() {
try {
// This forces the image to load.
canvas.imgBounds = source.getBounds();
} catch (Exception ex) {
ex.printStackTrace();
}
return new Boolean(true);
}
@Override
public void done() {
resizeImgToFit();
}
};
w.execute();
}
public void showImage(PlanarImage imgSource) {
canvas.set(imgSource);
/* We need to wait for the imgLoader in canvas.set to finish before attempting to configure
* the panner, or else the image won't be fully loaded when we do source.getHeight() in
* configurePanner() and we'll block the UI thread waiting for it to load.
*/
SwingWorker w = new SwingWorker<Boolean, Void>() {
@Override
public Boolean doInBackground() {
try {
if (canvas.imgLoader != null) {
// This call will block until imgLoader.doInBackground is finished
canvas.imgLoader.get(); // The returned value is irrelevant
}
} catch (Exception ex) {
if (! ex.getClass().equals(CancellationException.class)) {
ex.printStackTrace();
}
}
return new Boolean(true);
}
@Override
public void done() {
configurePanner();
}
};
w.execute();
// Need to re-paint the areas outside the canvas when the canvas shrinks
this.canvasPanel.repaint();
}
private void resizeImgToFit() {
if (source == null) return;
double dScale = 1000 * com.ebixio.virtmus.Utils.scaleProportional(canvasPanel.getBounds(), source.getBounds());
int scale;
if (dScale < 1) {
scale = 1;
} else if (dScale >= 1000) {
scale = 999;
} else {
scale = (int)dScale;
}
// If the zoom value changes, the listeners will be notified.
this.jsZoom.setValue(scale);
}
/**
* The panner should only be shown if the image is larger than the canvas.
*/
private void configurePanner() {
PlanarImage currentSource = (scaledSource != null) ? scaledSource : source;
if (currentSource == null) {
panner.setVisible(false);
return;
}
// source.getWidth() (or Height) could throw an exception if the source is invalid
try {
/* source.getWidth/Height() will block until the image is decoded, so this function
* should only be called after "source" has loaded its image.
* See: showPage and ImageDisplay.set
*/
if (currentSource.getWidth() > canvas.getWidth() || currentSource.getHeight() > canvas.getHeight()) {
panner.set(canvas, currentSource, 128);
panner.setVisible(true);
canvas.revalidate();
} else {
panner.setVisible(false);
canvas.setOrigin(0, 0);
canvas.revalidate();
}
} catch (Exception e) {
}
}
/**
* The list of drawing tools we will use with the toolChooser JComboBox
* @return A set of drawing tools
*/
public ComboBoxModel<DrawingTool> getTools() {
DefaultComboBoxModel<DrawingTool> cbm = new DefaultComboBoxModel<>();
cbm.addElement(new ToolRect(canvas));
cbm.addElement(new ToolLine(canvas));
cbm.addElement(new ToolFreehand(canvas));
//cbm.addElement(new ToolDot(canvas)); // The line tool makes this redundant
return cbm;
}
// <editor-fold defaultstate="collapsed" desc=" ComponentListener interface ">
@Override
public void componentResized(ComponentEvent e) {
resizeImgToFit();
}
@Override
public void componentMoved(ComponentEvent e) {
}
@Override
public void componentShown(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) {
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" Wizzard generated ">
final static class ResolvableHelper implements Serializable {
private static final long serialVersionUID = 1L;
public Object readResolve() {
return AnnotTopComponent.getDefault();
}
}
/** replaces this in object stream
* @return Something or another.
*/
@Override
public Object writeReplace() {
return new ResolvableHelper();
}
@Override
protected String preferredID() {
return PREFERRED_ID;
}
// </editor-fold>
}