/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue.ui;
import tufts.Util;
import tufts.vue.*;
import tufts.vue.gui.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import javax.swing.*;
/**
* An icon for the preview of a given resource.
*
* TODO: merge common code with PreviewPane, and perhaps put in a 3rd class
* so can have multiple icons referencing the same underlying image.
*
* @version $Revision: 1.19 $ / $Date: 2010-02-03 19:16:31 $ / $Author: mike $
* @author Scott Fraize
*/
public class ResourceIcon
implements javax.swing.Icon, Images.Listener, Runnable
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ResourceIcon.class);
//private final Image NoImage = VueResources.getImage("/icon_noimage32.gif");
private static final Image NoImage = GUI.NoImage32;
private Resource mResource;
private Object mPreviewData;
private Image mImage;
private int mImageWidth;
private int mImageHeight;
private boolean isLoading = false;
private Component mPainter = null;
private int mWidth = -1;
private int mHeight = -1;
//private final JLabel StatusLabel = new JLabel("(status)", JLabel.CENTER);
/**
*
* This constructor will be needed if the icon is to be placed into a component that
* uses cell renderers, such as JList, JTable, or JTree. In the normal course,
* ResourceIcon's repaint when their data is loaded, and to do so they remember the
* component they're being painted on, and ask it to repaint when the data comes in.
*
* But this repaint doesn't work if what they're being asked to paint on is a renderer.
*
* The problem with renderer's is that "repaint" is overriden and defined as a
* no-op, as they're only used to temporarily take on and draw their values when
* they're asked to paint.
*
**/
// above could be handled w/out having to pass the painter around by requiring that
// any cell renderer that wants to display a ResourceIcon to do a putClientProperty
// of a key (e.g., ResourceIcon.PAINTER) with a reference to the parent repainter,
// which we could grab the first time we repaint here.
public ResourceIcon(Resource r, int width, int height, Component painter) {
setSize(width, height);
//StatusLabel.setVisible(false);
//add(StatusLabel);
//loadResource(r);
mResource = r;
mPainter = painter;
if (DEBUG.IMAGE) out("Constructed " + width + "x" + height + " " + r + " painter=" + GUI.namex(painter));
}
/** Act like a regular Icon */
public ResourceIcon(Resource r, int width, int height) {
this(r, width, height, null);
}
/** Expand's to fit the size of the component it's painted into */
public ResourceIcon() {}
public void setSize(int w, int h) {
if (w != mWidth || h != mHeight) {
mWidth = w;
mHeight = h;
repaint();
}
}
private void repaint() {
if (mPainter != null) {
if (DEBUG.IMAGE) out("repaint in " + GUI.name(mPainter));
mPainter.repaint();
}
// FYI, this will not repaint if the parent is a DefaultTreeCellRenderer,
// and the parent of the parent (a JLabel), is null, so we can't get that.
}
public int getIconWidth() { return mWidth; }
public int getIconHeight() { return mHeight; }
private void showLoadingStatus() {
//StatusLabel.setText("Loading...");
//StatusLabel.setVisible(true);
}
private void status(String msg) {
//StatusLabel.setText("<HTML>"+msg);
//StatusLabel.setVisible(true);
}
private void clearStatus() {
//StatusLabel.setVisible(false);
}
public Resource getResource() {
return mResource;
}
/** If the image for the icon has been loaded, this will return it, otherwise, null. */
public Image getImage() {
return mImage;
}
synchronized void loadResource(Resource r) {
if (DEBUG.RESOURCE || DEBUG.IMAGE) out("loadResource: " + Util.tag(r) + " " + r);
mResource = r;
if (r != null)
mPreviewData = r.getPreview();
else
mPreviewData = null;
mImage = null;
// URLResource decides this
//if (mPreviewData == null && mResource.isImage())
// mPreviewData = mResource;
loadPreview(mPreviewData);
}
private void loadPreview(Object previewData)
{
// todo: handle if preview is a Component,
// todo: handle a String as preview data.
if (false /*&& r.getIcon() != null*/) { // these not currently valid from Osid2AssetResource (size=-1x-1)
//displayIcon(r.getIcon());
} else if (previewData instanceof java.awt.Component) {
out("TODO: handle Component preview " + previewData);
displayImage(NoImage);
} else if (previewData != null) { // todo: check an Images.isImageableSource
loadImage(previewData);
} else {
displayImage(NoImage);
}
}
// TODO: if this triggered from an LWImage selection, and LWImage had
// an image error, also notify the LWImage of good data if it comes
// in as a result of selection.
private synchronized void loadImage(Object imageData) {
if (DEBUG.IMAGE) out("loadImage " + imageData);
// test of synchronous loading:
//out("***GOT IMAGE " + Images.getImage(imageData));
// TODO: refactor ResourceIcon to use the new ImageRef --
// will be much simpler.
if (Images.getImage(imageData, this) == null) {
// will make callback to gotImage when we have it
isLoading = true;
showLoadingStatus();
} else {
// gotImage has already been called
isLoading = false;
}
}
// /** @see Images.Listener */
// public void gotImageUpdate(Object key, Images.Progress p) {}
public synchronized void gotImageSize(Object imageSrc, int width, int height, long byteSize, int[] ss) {
//if (imageSrc != mPreviewData) return;
mImageWidth = width;
mImageHeight = height;
}
/** @see Images.Listener */
public synchronized void gotImageProgress(Object imageSrc, long bytesSoFar, float pct) {}
/** @see Images.Listener */
public synchronized void gotImage(Object imageSrc, Images.Handle handle) {
//if (imageSrc != mPreviewData) return;
displayImage(handle.image);
isLoading = false;
}
/** @see Images.Listener */
public synchronized void gotImageError(Object imageSrc, String msg) {
//if (imageSrc != mPreviewData) return;
displayImage(NoImage);
status("Error: " + msg);
isLoading = false;
}
private void displayImage(Image image) {
if (DEBUG.RESOURCE || DEBUG.IMAGE) out("displayImage " + Util.tag(image));
mImage = image;
if (mImage != null) {
mImageWidth = mImage.getWidth(null);
mImageHeight = mImage.getHeight(null);
if (DEBUG.IMAGE) out("displayImage " + mImageWidth + "x" + mImageHeight);
}
clearStatus();
repaint();
}
/*@Override*/ public synchronized void run() {
//loadPreview(mPreviewData);
if (!isLoading)
loadResource(mResource);
}
private static final double MaxZoom = 1.0;
private static final boolean DrawBorder = false; // todo: doesn't handle cropped / squared off
private static final int BorderWidth = 1; // width of border (todo: only works as 1)
private static final int BorderGap = 1; // whitespace around drawn border
private static final int BorderSpace = BorderWidth + BorderGap;
private static final boolean CropToSquare = true;
/*@Override*/ public void paintIcon(Component c, Graphics g, int x, int y)
{
final boolean expandToFit = (mWidth < 1);
if (DEBUG.IMAGE && DEBUG.META) out("paintIcon; onto=" + GUI.name(c)
+ " painter=" + GUI.name(mPainter) + "@" + Integer.toHexString((mPainter.hashCode())));
if (mPainter == null) {
// note this means repaint updates would stop in a new parent,
// tho assuming it's loaded by then, regular paints would work fine.
mPainter = c;
}
if (DrawBorder && !expandToFit) {
g.setColor(Color.gray);
g.drawRect(x, y, mWidth-1, mHeight-1);
}
if (mImage == null) {
if (!isLoading /*&& mPreviewData != null*/) {
synchronized (this) {
if (!isLoading /*&& mPreviewData != null*/)
VUE.invokeAfterAWT(ResourceIcon.this); // load the preview
}
}
g.setColor(Color.gray);
g.drawRect(x, y, mWidth-1, mHeight-1);
return;
}
int fitWidth, fitHeight;
final Dimension maxImageSize;
if (expandToFit) {
// fill the given component
fitWidth = c.getWidth();
fitHeight = c.getHeight();
maxImageSize = c.getSize();
} else {
// paint at our fixed size
fitWidth = mWidth;
fitHeight = mHeight;
if (DrawBorder)
maxImageSize = new Dimension(fitWidth - BorderSpace*2, fitHeight - BorderSpace*2);
else
maxImageSize = new Dimension(fitWidth, fitHeight);
if (DEBUG.IMAGE && DEBUG.META) out("paintIcon; into " + GUI.name(maxImageSize));
}
double zoomFit;
if (mImage == NoImage && expandToFit) {
zoomFit = 1;
} else {
Rectangle2D imageBounds;
if (CropToSquare) {
// square off image, then fit in icon (todo: better; crop to icon)
int smallestAxis = mImageWidth > mImageHeight ? mImageHeight : mImageWidth;
imageBounds = new Rectangle2D.Float(0, 0, smallestAxis, smallestAxis);
} else {
// fit entire image in icon
imageBounds = new Rectangle2D.Float(0, 0, mImageWidth, mImageHeight);
}
zoomFit = ZoomTool.computeZoomFit(maxImageSize,
0,
imageBounds,
null,
false);
if (zoomFit > MaxZoom)
zoomFit = MaxZoom;
}
final int drawW = (int) (mImageWidth * zoomFit + 0.5);
final int drawH = (int) (mImageHeight * zoomFit + 0.5);
int xoff = x;
int yoff = y;
// center if drawable area is bigger than image
if (drawW != fitWidth)
xoff += (fitWidth - drawW) / 2;
if (drawH != fitHeight)
yoff += (fitHeight - drawH) / 2;
Shape oldClip = null;
if (CropToSquare && !expandToFit) {
oldClip = g.getClip();
g.clipRect(x, y, mWidth, mHeight);
}
if (DEBUG.IMAGE && DEBUG.META) out("paintIcon; " + Util.tag(mImage) + " as " + drawW + "x" + drawH);
g.drawImage(mImage, xoff, yoff, drawW, drawH, null);
if (DEBUG.BOXES) {
g.setColor(Color.green);
((Graphics2D)g).setComposite(java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, 0.2f));
g.drawRect(x, y, mWidth-1, mHeight-1);
((Graphics2D)g).setComposite(java.awt.AlphaComposite.SrcOver);
}
if (CropToSquare && !expandToFit)
g.setClip(oldClip);
}
private void out(String s) {
String name = "ResourceIcon" + "@" + Integer.toHexString(hashCode());
Log.debug(name + " " + s);
}
}