/*
* 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 java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.SystemColor;
import java.awt.event.MouseListener;
import java.util.Arrays;
import java.util.StringTokenizer;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.TableModel;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.View;
import tufts.Util;
import tufts.vue.DEBUG;
import tufts.vue.TableBag;
import tufts.vue.Resource;
import tufts.vue.VUE;
import tufts.vue.MetaMap;
import tufts.vue.gui.GUI;
import javax.swing.JButton;
/**
*
* This works somewhat analogous to a JTable, except that the renderer's are persistent.
* We fill a GridBagLayout with all the labels and value fields we might ever need, set their
* layout constraints just right, then set the text values as properties come in, and setting
* all the unused label's and fields invisible. There is a maximum number of rows that can
* be displayed (initally 20), but this number is doubled when exceeded.
*
*/
public class MetaDataPane extends tufts.vue.gui.Widget
implements TableBag.Listener, Runnable
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MetaDataPane.class);
private JLabel[] mLabels;
private JTextArea[] mValues;
private final ScrollableGrid mGridBag;
private final boolean inOwnedScroll;
private JScrollPane mScrollPane;
private boolean inScroll;
/** The current MetaMap we're displaying and listening to updates from */
private TableBag mProperties;
private final String mLabel;
// /** If the displayed properties were from a Resource, this will be set to it, otherwise null */
// private Resource mResource;
public MetaDataPane(String label, boolean scroll) {
super("contentInfo");
mLabel = label;
inOwnedScroll = scroll;
ensureSlots(20);
mLabels[0].setText("X"); // make sure label will know it's max height
final int scrollUnit = mLabels[0].getPreferredSize().height + 4;
mGridBag = new ScrollableGrid(this, scrollUnit);
Insets insets = (Insets) GUI.WidgetInsets.clone();
insets.top = insets.bottom = 0;
insets.right = 1;
mGridBag.setBorder(GUI.makeSpace(insets));
//mGridBag.setBorder(new LineBorder(Color.red));
addLabelTextRows(0, mLabels, mValues, mGridBag, null, null);
if (inOwnedScroll) {
mScrollPane = new JScrollPane();
mScrollPane.setViewportView(mGridBag);
mScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
mScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
mScrollPane.setOpaque(false);
mScrollPane.getViewport().setOpaque(false);
//scrollPane.setBorder(null); // no focus border
//scrollPane.getViewport().setBorder(null); // no focus border
add(mScrollPane);
} else {
add(mGridBag);
}
// if (DEBUG.Enabled)
// mScrollPane.getVerticalScrollBar().getModel().addChangeListener(new ChangeListener() {
// public void stateChanged(ChangeEvent e) {
// if (DEBUG.SCROLL) VUE.Log.debug("vertScrollChange " + e.getSource());
// }
// });
}
@Override
public void addNotify() {
super.addNotify();
if (mScrollPane == null)
mScrollPane = (JScrollPane) javax.swing.SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
inScroll = (mScrollPane != null);
}
private JLabel createLabel() {
final JLabel label;
// if (DEBUG.Enabled) {
// label = new tufts.Util.JLabelAA();
// //label.setBackground(Color.red);
// //label.setOpaque(true);
// } else
label = new JLabel();
return label;
}
private boolean wasDebug = DEBUG.Enabled;
private static final Font LabelFace;
private static final Font ValueFace;
private static final boolean EasyReading1 = false;
private static final boolean EasyReading2 = DEBUG.Enabled;
static {
GUI.Face nameFace = null;
GUI.Face dataFace = null;
if (true) {
String labelFont;
String dataFont;
int fontSize;
if (GUI.isMacAqua()) {
labelFont = "Lucida Grande";
dataFont = labelFont;
fontSize = 11;
if (DEBUG.DR) dataFont = "Lucida Sans Typewriter";
} else {
labelFont = "SansSerif";
fontSize = 12;
// On XP, Lucida Sans Unicode looks better for values not so much for
// bold labels tho) Note: this is a smaller font than SansSerif, and our
// layout code is primarily tuned to the pixel for SanSerif, so
// adjustments are need to use this, or we need to write the code to
// compute the layout using actual font metrics.
dataFont = "Lucida Sans Unicode";
if (DEBUG.DR) {
dataFont = "Lucida Console";
fontSize = 11;
}
}
//nameFace = new GUI.Face(labelFont, Font.BOLD, fontSize, Color.gray);
//dataFace = new GUI.Face(dataFont, Font.PLAIN, fontSize, Color.black);
nameFace = new GUI.Face(labelFont, Font.BOLD, fontSize, null);
dataFace = new GUI.Face(dataFont, Font.PLAIN, fontSize, null);
}
if (EasyReading1) {
LabelFace = nameFace;
ValueFace = dataFace;
} else {
LabelFace = GUI.LabelFace;
if (DEBUG.DR)
ValueFace = dataFace;
else
ValueFace = GUI.ValueFace;
}
}
/**
* Make sure at least this minimum number of slots is available.
* @return true if # of slots is expanded
**/
private boolean ensureSlots(int minSlots) {
int curSlots;
if (mLabels == null || (wasDebug != DEBUG.Enabled)) {
curSlots = 0;
wasDebug = DEBUG.Enabled;
} else
curSlots = mLabels.length;
if (minSlots <= curSlots)
return false;
int maxSlots;
if (curSlots == 0)
maxSlots = minSlots;
else
maxSlots = minSlots + 4;
if (DEBUG.RESOURCE) out("expanding slots to " + maxSlots);
mLabels = new JLabel[maxSlots];
mValues = new JTextArea[maxSlots];
final Color alternatingColor;
if (Util.isMacPlatform())
alternatingColor = Color.white;
else
alternatingColor = new Color(250,250,250);
final Border fillBorder = GUI.makeSpace(TopPad,4,BotPad,0);
final Border macAdjustBorder = GUI.makeSpace(0,4,0,0);
final Border winAdjustBorder = GUI.makeSpace(0,4,0,0);
final Border winAdjustBorder1 = GUI.makeSpace(1,0,0,0);
// if (EasyReading2)
// winAdjustBorder = GUI.makeSpace(0,4,0,0);
// else
// winAdjustBorder = GUI.makeSpace(0,4,1,0);
final Border WindowsPlatformAdjustBorder = GUI.makeSpace(0,0,2,0);
for (int i = 0; i < mLabels.length; i++) {
mLabels[i] = createLabel();
mValues[i] = new JTextArea();
mValues[i].setEditable(false);
mValues[i].setLineWrap(true);
GUI.apply(LabelFace, mLabels[i]);
GUI.apply(ValueFace, mValues[i]);
mLabels[i].setOpaque(false);
mValues[i].setOpaque(false);
mLabels[i].setVisible(false);
mValues[i].setVisible(false);
if (Util.isWindowsPlatform())
mValues[i].setBorder(WindowsPlatformAdjustBorder);
if (EasyReading2) {
if (i % 2 != 0) {
//mLabels[i].setBackground(alternatingColor);
mValues[i].setBackground(alternatingColor);
//mLabels[i].setOpaque(true);
mValues[i].setOpaque(true);
mLabels[i].setBorder(fillBorder);
mValues[i].setBorder(fillBorder);
} else {
if (Util.isMacPlatform())
mValues[i].setBorder(macAdjustBorder);
else
mValues[i].setBorder(winAdjustBorder);
}
} else if (EasyReading1) {
// todo: compute all this based on font metrics
if (Util.isWindowsPlatform()) {
mLabels[i].setBorder(winAdjustBorder1);
}
} else if (DEBUG.BOXES) {
if (i % 2 == 0) {
//mLabels[i].setBackground(new Color(255,255,255,128));
//mValues[i].setBackground(new Color(255,255,255,128));
mLabels[i].setBackground(Color.white);
mValues[i].setBackground(Color.white);
mLabels[i].setOpaque(true);
mValues[i].setOpaque(true);
}
}
mValues[i].addMouseListener(CommonURLListener);
}
return true;
}
private static final String CAN_OPEN = "vue.openable";
private class URLMouseListener extends tufts.vue.MouseAdapter {
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getClickCount() != 2)
return;
try {
final JTextArea value = (JTextArea) e.getSource();
if (value.getClientProperty(CAN_OPEN) == Boolean.TRUE) {
e.consume();
final Color c = value.getForeground();
value.setForeground(Color.red);
value.paintImmediately(0,0, value.getWidth(), value.getHeight());
tufts.vue.VueUtil.openURL(value.getText());
GUI.invokeAfterAWT(new Runnable() {
public void run() {
value.select(0,0);
GUI.invokeAfterAWT(new Runnable() { public void run() { value.setForeground(c); }});
}
});
}
} catch (Throwable t) {
Log.error(t);
}
}
}
// public void loadResource(Resource r) {
// if (DEBUG.RESOURCE) out("loadResource: " + r);
// loadProperties(r.getProperties());
// }
/** MetaMap.Listener event delivery: do not synchronize this method,
* as the call is usually coming from an ImgLoader thread, and
* we must not synchronize or we risk deadlock */
public void tableBagChanged(final TableBag source) {
// Note: we can deadlock here obtaining lock held by AWT (if we synchronize this method)
// This is normally called NOT from an AWT thread (e.g., from an ImageLoader)
// If we synchronize this method, this can happen:
// Thread-AWT is trying to load new properties (a ResourceSelection change has
// lead to MetaDataPane.loadProperties), and locks the singleton MetaDataPane to
// do so. It has not yet attempted to obtain any MetaMap locks.
// Thread-ImageLoader updated MetaMap-X (e.g., called releaseChanges()), and
// has locked MetaMap-X to notify all it's listeners of the change (in this
// case, call MetaDataPane.propertyMapChanged).
// The call in Thread-ImageLoader to propertyMapChanged cannot complete until
// the AWT lock on MetaDataPane is released.
// The call in Thread-AWT, already locking AWT on MetaDataPane, now attempting to
// lock MetaMap-X to remove us as a listener, cannot complete until Thread-ImageLoader
// releases the lock on MetaMap-X, which is attempting to notify all it's listeners.
// Thus, we hava a classic deadlock.
// To handle this, we force this event to ultimately be delivered on the AWT thread.
// if (DEBUG.IMAGE) Log.info("propertyMapChanged entry " + Util.tag(source));
// This update will most often be coming from an ImageLoader thread.
// updateDisplay, once it's holding the lock (it's synchronized), will check the
// current value of mProperties to see if it matches the current update source,
// and only perform the update if mProperties hasn't changed since we got this
// notification.
GUI.invokeAfterAWT(new Runnable() {
public void run() {
try {
updateDisplayAWT(source);
} catch (Throwable t) {
Log.error("udpateDisplayAWT: " + Util.tags(source) + ";", t);
}
}
});
// if (DEBUG.IMAGE) Log.info("propertyMapChanged exit " + Util.tag(source));
}
public void loadTable(final TableBag propertyMap) {
if (DEBUG.THREAD) Log.debug("loadProperties: " + Util.tag(propertyMap));
if (javax.swing.SwingUtilities.isEventDispatchThread()) {
// If called from AWT, (e.g., normally as the result of a ResourceSelection change),
// we can proceed immediately. This is the common case.
loadPropertiesAWT(propertyMap);
} else {
// If NOT called from AWT, force the event to happen on AWT or
// we risk a dead-lock. This is not the normal case, but
// we handle it to ensure we can never dead-lock.
GUI.invokeAfterAWT(new Runnable() {
public void run() {
loadPropertiesAWT(propertyMap);
}
});
}
}
// as we guarantee that we only ever run this in the AWT thread, this method
// doesn't need to be synchronized
//-------------------------------------------------------
// TODO: change this entire mechanism to handle this as a regular LWComponent event
// (e.g., all data events will go through the LWComponent) -- e.g., this
// may not be good enough -- if we're REPLACING the entire meta-map data
// object instance on the LWComponent, it does us no good to be listening
// to the old one.
//-------------------------------------------------------
private void loadPropertiesAWT(TableBag propertyMap)
{
if (DEBUG.THREAD || DEBUG.RESOURCE || DEBUG.IMAGE) out("loadPropertiesAWT: " + propertyMap.size() + " key/value pairs");
try {
// Note: if another thread has a lock on mProperties (e.g., MetaMap is
// in the middle of delivering an update to us about the same mProperties),
// we could dead-lock in propertyMapChanged above if it was a synchronized
// method (which is why it is not).
if (mProperties != propertyMap) {
if (mProperties != null)
mProperties.removeListener(this);
mProperties = propertyMap;
mProperties.addListener(this);
updateDisplayAWT(mProperties);
}
} catch (Throwable t) {
Log.error("loadPropertiesAWT: " + propertyMap, t);
}
}
// Note: should only be called on AWT Event Dispatch Thread.
// If it were synchronized, and this method is was from a non-AWT thread, we could risk deadlock.
private void updateDisplayAWT(final TableBag properties)
{
if (mProperties != properties) {
if (DEBUG.Enabled) Log.debug("too late for update to " + properties);
return;
}
if (DEBUG.THREAD || DEBUG.RESOURCE) out("updateDisplay: " + properties.size() + " key/value pairs");
if (DEBUG.SCROLL)
Log.debug("scroll model listeners: "
+ Arrays.asList(((DefaultBoundedRangeModel)
mScrollPane.getVerticalScrollBar().getModel())
.getListeners(ChangeListener.class)));
if (inScroll)
mScrollPane.getVerticalScrollBar().setValueIsAdjusting(true);
mGridBag.setPaintDisabled(true);
try {
if (DEBUG.RESOURCE || DEBUG.THREAD) out("updateDisplay: getTableModel() on " + tufts.Util.tag(properties));
// Note: MetaMap.getTableModel() is synchronized, as is MetaMap.addListener/removeListener,
// so if this (or they) are called while we already hold a lock on the MetaMap
// from another thread, we'll deadlock.
final TableModel model = properties.getTableModel();
// TODO: get as raw collection instead: old PropertyMap impl can
// provide a collection view of a table-model if need be for backward compat.
if (DEBUG.RESOURCE) out("updateDisplay: model=" + model
+ " modelSlots=" + model.getRowCount()
+ " slotsAvail=" + mLabels.length
);
if (ensureSlots(model.getRowCount() + 2)) { // allow for some debug
mGridBag.removeAll();
addLabelTextRows(0, mLabels, mValues, mGridBag, null, null);
}
loadAllRows(model, properties instanceof MetaMap ? (MetaMap) properties : null);
if (DEBUG.Enabled)
setTitle(mLabel + " (" + properties.size() + ")");
GUI.invokeAfterAWT(this);
} catch (Throwable t) {
mGridBag.setPaintDisabled(false);
Log.error("updateDisplayAWT: " + Util.tags(properties) + ";", t);
}
}
public synchronized void run() {
// TODO: move this code up to a generic Widget capability,
// merging it with similar code in InspectorPane Widget.
// And does this still need to be synchronized? This always
// runs in AWT, and now that updateDisplay always runs in AWT,
// our calls to JScrollBar.setValueIsAdjusting and
// mGridBag.setPaintDisabled (which are not threadsafe calls
// in of themseleves) should always be running synchronously.
try {
// Always put the scroll-bar back at the top, as it defaults to moving to the
// bottom. E.g., when selecting through search results that all have tons of
// meta-data, we're left looking at just the meta-data, not the preview.
// Ideally, we could check the scroller position first to see if we were
// already at the top at the start and only do this if that was the case.
// VUE.getInfoDock().scrollToTop();
// if (inOwnedScroll)
// mScrollPane.getVerticalScrollBar().setValue(0);
// // Now release all scroll-bar updates.
// if (inScroll)
// mScrollPane.getVerticalScrollBar().setValueIsAdjusting(false);
if (inScroll) {
//out("SCROLL-TO-TOP");
mScrollPane.getVerticalScrollBar().setValue(0);
mScrollPane.getVerticalScrollBar().setValueIsAdjusting(false);
}
// Now allow the grid to repaint.
} finally {
mGridBag.setPaintDisabled(false);
mGridBag.repaint();
}
}
private String valueToText(final Object value) {
if (value == null)
return "(empty)";
else if (value instanceof java.awt.Component)
return GUI.name(value);
else if (value instanceof java.lang.ref.Reference)
return "[" + value.getClass().getSimpleName() + "] "
+ valueToText(((java.lang.ref.Reference)value).get()); // note recursion
else
return value.toString();
}
private void loadAllRows(TableModel model, MetaMap dataMap) {
final int rows = model.getRowCount();
final int maxRow;
if (Util.getJavaVersion() > 1.5) {
maxRow = rows;
} else {
// prior to java 1.6, GridBag has serious bug in that it completely fails
// if more than 512 items are loaded into it.
if (rows > 512)
maxRow = 512;
else
maxRow = rows;
}
int rowIdx = 0;
mLastLabel = null;
for (int row = 0; row < maxRow; row++) {
final Object label = model.getValueAt(row, 0);
final String labelText = "" + label;
final Object value = model.getValueAt(row, 1);
final String valueText = valueToText(value);
// loadRow(row++, label, value); // debug non-HTML display
// FYI, some kind of HTML bug for text strings with leading slashes
// -- they show up empty. Right now, we're disable HTML for
// all synthetic keys, which covers URL.path, which was the problem.
//if (label.indexOf(".") < 0)
//value = "<html>"+value;
if (Resource.isHiddenPropertyKey(labelText)) {
//if (DEBUG.DR || DEBUG.DATA) {
if (DEBUG.Enabled) {
// Allow hidden properties to be seen
} else {
// default: hide the hidden property
//mLabels[row].setVisible(false);
//mValues[row].setVisible(false);
// we skip loading the row completely -- keep alternating colors in order
continue;
}
}
boolean loaded = false;
try {
loaded = loadRow(rowIdx, labelText, value, valueText);
} catch (Throwable t) {
Log.error("Failed to load row " + row + "; label= " + Util.tags(label) + "; value=" + Util.tags(value), new Throwable());
}
if (loaded)
rowIdx++;
}
if (DEBUG.Enabled && dataMap != null && dataMap.getSchema() != null) {
if (DEBUG.DR) loadRow(rowIdx++, "DATA", null, Util.tag(dataMap));
loadRow(rowIdx++, "SCHEMA", null, ""+dataMap.getSchema());
}
for (; rowIdx < mLabels.length; rowIdx++) {
//out(" clear row " + row);
mLabels[rowIdx].setVisible(false);
mValues[rowIdx].setVisible(false);
}
if (rows > maxRow) {
// prior to java 1.6, GridBag has serious bug in that it completely fails
// if more than 512 items are loaded into it. This merges all rows
// > 512 into a single field.
final StringBuilder mergeRow = new StringBuilder();
for (int r = maxRow - 1; r < rows; r++) {
final Object label = model.getValueAt(r, 0);
final Object value = model.getValueAt(r, 1);
mergeRow.append(label);
mergeRow.append(": ");
mergeRow.append(""+value);
mergeRow.append('\n');
}
loadRow(511, "(Remaining)", "Overflow Rows > 512", mergeRow.toString());
mLabels[511].setVisible(true);
mValues[511].setVisible(true);
}
//mScrollPane.getViewport().setViewPosition(new Point(0,0));
}
private MouseListener CommonURLListener = new URLMouseListener();
private final Color ObjectColor = new Color(128,0,0);
private String mLastLabel = null;
private JTextArea mLastValue = null;
private boolean loadRow(int row, final String labelText, final Object value, final String valueText) {
if (DEBUG.RESOURCE && DEBUG.META) out("adding row " + row + " " + labelText + "=[" + valueText + "]");
//Log.debug(String.format("lastLabel[%s], thisLabel[%s]", mLastLabel, labelText));
if (mLastLabel != null && mLastValue != null && labelText.startsWith(mLastLabel + "@")) {
// This hack removes Foo@attribute-name repeats in list -- is just a decoration
// tweak for now -- ultimately, this should be represtented in the MetaMap as an
// association between keys (will need sub-keys), which will be required to
// accurately represent XML data, which will currently cause a repeated
// key-value pair to be ignored: e.g., if a jira item had multiple comments,
// each with an "author=<username>" key-value with it, only the first unique
// instance will appear in the map: e.g., only the first "author=melanie" would
// appear -- subsequent comments by user melanie would have no author associated
// with them, as our data-map, although it allows multple values per key, is still
// flat and only allows one instance of key-value pair.
String attributeLabel = labelText.substring(mLastLabel.length() + 1);
mLastValue.setText(String.format("%s [%s=%s]", mLastValue.getText(), attributeLabel, value));
if (!DEBUG.DR) return false;
}
final JLabel label = mLabels[row];
final JTextArea field = mValues[row];
if (EasyReading2) {
label.setText(labelText);
} else {
final String txt;
//-----------------------------------------------------------------------------
// hack to trim some long XML names in RSS feeds until UI can wrap keys as
// well as labels (e.g. NY Times XML news items sometimes have an associated image)
final String trim1 = "media:group.media:content.media:";
final String trim2 = "media:group.media:";
final String lowText = labelText.toLowerCase();
if (lowText.startsWith(trim1)) {
txt = "Media" + labelText.substring(trim1.length()-1);
//txt = "MGMCM" + txt.substring(trim1.length()-1);
}
else if (lowText.startsWith(trim2)) {
txt = "Media" + labelText.substring(trim2.length()-1);
//txt = "MGM" + txt.substring(trim2.length()-1);
}
else if (lowText.equals("comments.comment")) {
// hack for jira comments
txt = "Comment";
}
// else if (lowText.indexOf("@") > 0) {
// txt = "<html>" + lowText + "<br>newline";
// }
else if (labelText.length() > 20) {
final int len = labelText.length();
final int half = len / 2 + 1;
txt = "<html>"
+ labelText.substring(0,half) + "<br>"
+ labelText.substring(half,len);
}
else {
if (DEBUG.DR || DEBUG.DATA)
txt = labelText;
else
txt = Util.upperCaseWords(labelText);
}
//-----------------------------------------------------------------------------
final StringBuilder buf = new StringBuilder(txt.length() + 1);
buf.append(txt);
buf.append(':');
label.setText(buf.toString());
}
mLastLabel = labelText;
mLastValue = field;
if (Resource.looksLikeURLorFile(valueText)) {
//field.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
field.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
field.putClientProperty(CAN_OPEN, Boolean.TRUE);
if (DEBUG.Enabled || EasyReading1) field.setForeground(Color.blue);
} else {
field.putClientProperty(CAN_OPEN, Boolean.FALSE);
if (DEBUG.Enabled || EasyReading1) field.setForeground(Color.black);
//label.removeMouseListener(CommonURLListener);
field.setCursor(Cursor.getDefaultCursor());
//GUI.apply(GUI.ValueFace, mValues[i]);
}
// JTextArea doesn't support HTML
// if (valueText.indexOf("</") > 0)
// field.setText("<html>" + valueText);
// else
// field.setText(valueText);
field.setText(valueText);
if (DEBUG.Enabled) {
if (Resource.canDump(value)) {
String txt = Resource.getDump(value);
txt = "<html><code>" + txt.replaceAll("\n", " <br> ") + "</code>";
field.setToolTipText(txt);
} else {
//if (value instanceof String == false)
field.setToolTipText(Util.tags(value));
}
if (value instanceof String == false)
field.setForeground(ObjectColor);
}
// if field has at least one space, use word wrap
if (valueText.indexOf(' ') >= 0)
field.setWrapStyleWord(true);
else
field.setWrapStyleWord(false);
label.setVisible(true);
field.setVisible(true);
return true;
}
private void out(Object o) {
Log.debug((o==null?"null":o.toString()));
}
//----------------------------------------------------------------------------------------
// Utility methods
//----------------------------------------------------------------------------------------
// private void addLabelTextPairs(Object[] labelTextPairs, Container gridBag) {
// JLabel[] labels = new JLabel[labelTextPairs.length / 2];
// JComponent[] values = new JComponent[labels.length];
// for (int i = 0, x = 0; x < labels.length; i += 2, x++) {
// //out("ALTP[" + x + "] label=" + labelTextPairs[i] + " value=" + GUI.name(labelTextPairs[i+1]));
// String labelText = (String) labelTextPairs[i];
// labels[x] = new JLabel(labelText + ":");
// values[x] = (JComponent) labelTextPairs[i+1];
// }
// addLabelTextRows(0, labels, values, gridBag, GUI.LabelFace, GUI.ValueFace);
// }
private final int TopPad = Util.isMacPlatform() ? 2 : 1;
private final int BotPad = Util.isMacPlatform() ? 2 : 0;
private final Insets labelInsets = new Insets(TopPad, 0, BotPad, GUI.LabelGapRight);
private final Insets fieldInsets = new Insets(TopPad, 0, BotPad, GUI.FieldGapRight);
// private final Insets labelInsets = new Insets(0,0,0,0);
// private final Insets fieldInsets = new Insets(0,0,0,0);
/** labels & values must be of same length */
private void addLabelTextRows(int starty,
JLabel[] labels,
JComponent[] values,
Container gridBag,
Font labelFace,
Font fieldFace)
{
// Note that the resulting alignment ends up being somehow FONT dependent!
// E.g., works great with Lucida Grand (MacOSX), but with system default,
// if the field value is a wrapping JTextPane (thus gets taller as window
// gets narrower), the first line of text rises slightly and is no longer
// in line with it's label.
GridBagConstraints c = new GridBagConstraints();
c.anchor = GridBagConstraints.EAST;
c.weighty = 0;
c.gridheight = 1;
for (int i = 0; i < labels.length; i++) {
//out("ALTR[" + i + "] label=" + GUI.name(labels[i]) + " value=" + GUI.name(values[i]));
boolean centerLabelVertically = false;
JLabel label = labels[i];
JComponent field = values[i];
if (labelFace != null)
GUI.apply(labelFace, label);
if (field instanceof JTextComponent) {
if (field instanceof JTextField)
centerLabelVertically = true;
// JTextComponent textField = (JTextComponent) field;
// editable = textField.isEditable();
// if (field instanceof JTextArea) {
// JTextArea textArea = (JTextArea) field;
// c.gridheight = textArea.getRows();
// } else if (field instanceof JTextField)
} else {
if (fieldFace != null)
GUI.apply(fieldFace, field);
}
//-------------------------------------------------------
// Add the field label
//-------------------------------------------------------
c.gridx = 0;
c.gridy = starty++;
c.insets = labelInsets;
c.gridwidth = GridBagConstraints.RELATIVE; // next-to-last in row
c.fill = GridBagConstraints.NONE; // the label never grows
if (centerLabelVertically)
c.anchor = GridBagConstraints.EAST;
else
c.anchor = GridBagConstraints.NORTHEAST;
c.weightx = 0.0; // do not expand
gridBag.add(label, c);
//-------------------------------------------------------
// Add the field value
//-------------------------------------------------------
c.gridx = 1;
c.gridwidth = GridBagConstraints.REMAINDER; // last in row
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.CENTER;
//c.anchor = GridBagConstraints.NORTH;
c.insets = fieldInsets;
c.weightx = 1.0; // field value expands horizontally to use all space
gridBag.add(field, c);
}
// add a default vertical expander to take up extra space
// (so the above stack isn't vertically centered if it
// doesn't fill the space).
c.weighty = 1;
c.weightx = 1;
c.gridx = 0;
c.fill = GridBagConstraints.BOTH;
c.gridwidth = GridBagConstraints.REMAINDER;
//this truly doesn't allow users to edit metdata all the time
// They can edit it only if they have permissions and the metdata will be
// published to dublin core supported repository
// JButton editMetadataButton = new JButton(new String("Edit"));
// c.weighty =0;
// c.weightx =0;
// c.gridx =1;
// c.fill = GridBagConstraints.NONE;
// c.anchor = GridBagConstraints.NORTHEAST;
// gridBag.add(editMetadataButton,c);
if (false) {
JComponent defaultExpander = new JPanel();
defaultExpander.setPreferredSize(new Dimension(Short.MAX_VALUE, 1));
if (DEBUG.BOXES) {
defaultExpander.setOpaque(true);
defaultExpander.setBackground(Color.orange);
} else
defaultExpander.setOpaque(false);
gridBag.add(defaultExpander, c);
}
return;
}
/*
** Return the number of lines of text, including wrapped lines.
*/
public static int getWrappedLines(JTextComponent c)
{
int len = c.getDocument().getLength();
int offset = 0;
// Increase 10% for extra newlines
StringBuffer buf = new StringBuffer((int) (len * 1.10));
try
{
while (offset < len)
{
int end = javax.swing.text.Utilities.getRowEnd(c, offset);
if (end < 0)
{
break;
}
// Include the last character on the line
end = Math.min(end + 1, len);
String s = c.getDocument().getText(offset, end - offset);
buf.append(s);
// Add a newline if s does not have one
if (!s.endsWith("\n"))
{
buf.append('\n');
}
offset = end;
}
}
catch (BadLocationException e)
{
}
StringTokenizer token = new StringTokenizer(buf.toString(), "\n");
int linesOfText = token.countTokens();
if (linesOfText == 0)
linesOfText =1;
return linesOfText;
}
// public void loadProperties(final MetaMap resourceProperties) {
// if (DEBUG.THREAD) Log.debug("loadProperties: " + Util.tag(resourceProperties));
// if (javax.swing.SwingUtilities.isEventDispatchThread()) {
// // If called from AWT, this should normally be the result of a ResourceSelection change,
// // and that should get immediate priority
// doLoadProperties(resourceProperties);
// } else {
// // If NOT called from AWT (e.g., an ImageLoder), this would normally be a LOW priority
// // update, which doesn't need to happen immediately.
// // Does this really make sense? All we're checking is if the
// // current properties has changed at all. If this was NOT a call
// // from propertyMapChanged, it makes no sense: TODO TODO TODO: MOVE THIS
// // CODE UP TO PROPERTY MAP CHANGED
// // TODO TODO: AND: CAN SPLIT OUT LOAD v.s. RELOAD-CODE: RELOAD-CODE
// // NEVER HAS TO CHECK FOR ADDING/REMOVING LISTENERS, OR SET mProperties!
// final Object lowPriorityProperties = mProperties;
// // 2008-04-12 SMF: if we're in an image loader thread when
// // we get this callback, we risk deadlock -- this should fix it.
// GUI.invokeAfterAWT(new Runnable() {
// public void run() {
// if (lowPriorityProperties == mProperties)
// doLoadProperties(resourceProperties);
// }
// });
// }
// }
// private synchronized void doLoadProperties(MetaMap rsrcProps)
// {
// // TODO: loops if we don't do this first: not safe! we should be loading
// // directly from the props themselves, and by synchronized on them... tho is
// // nice that only a single sorted list exists for each resource, tho of course,
// // then we have tons of cached sorted lists laying about.
// if (DEBUG.THREAD || DEBUG.RESOURCE) out("loadProperties: " + rsrcProps.size() + " key/value pairs");
// if (DEBUG.SCROLL)
// Log.debug("scroll model listeners: "
// + Arrays.asList(((DefaultBoundedRangeModel)
// mScrollPane.getVerticalScrollBar().getModel())
// .getListeners(ChangeListener.class)));
// if (scroll)
// mScrollPane.getVerticalScrollBar().setValueIsAdjusting(true);
// mGridBag.setPaintDisabled(true);
// try {
// // Description of a dead-lock that has been fixed by having
// // MetaMap.getTableModel() sync on it's own lock:
// // Example: VUE-ImageLoader49 holds changes on the props, then goes to notify
// // us here in the MetaData pane. But The AWT thread had already put is in
// // here, right below, trying to call getTableModel(), but before we can call
// // it, the above notification needs to be released. If the props had CHANGED
// // to entirely different set, from another resource, this wouldn't have been
// // a problem, because the update would have been skipped above in
// // propertyMapChanged.
// // Put another way: the MetaMap is trying to notify us, but is waiting
// // for us to break out of this method for the lock to release, so
// // propertyMapChanged can be called, but then we call getTableModel(), which
// // is locked on that same MetaMap that is waiting for us, and thus
// // deadlock...
// if (DEBUG.RESOURCE || DEBUG.THREAD) out("loadProperties: getTableModel() on " + tufts.Util.tag(rsrcProps));
// TableModel model = rsrcProps.getTableModel();
// if (DEBUG.RESOURCE) out("loadProperties: model=" + model
// + " modelSlots=" + model.getRowCount()
// + " slotsAvail=" + mLabels.length
// );
// if (mProperties != rsrcProps) {
// if (mProperties != null)
// mProperties.removeListener(this); // *AWT* THREAD: CAN DEADLOCK IN MetaMap.java line 175 (ImageLoader-26 contention)
// mProperties = rsrcProps;
// mProperties.addListener(this);
// }
// if (ensureSlots(model.getRowCount())) {
// mGridBag.removeAll();
// addLabelTextRows(0, mLabels, mValues, mGridBag, null, null);
// }
// loadAllRows(model);
// GUI.invokeAfterAWT(this); // TODO: Do we still need this as an invoke if this.scroll == false ?
// } catch (Throwable t) {
// mGridBag.setPaintDisabled(false);
// tufts.Util.printStackTrace(t);
// }
// /*
// // none of these sync's seem to making any difference
// synchronized (mScrollPane.getTreeLock()) {
// synchronized (mScrollPane.getViewport().getTreeLock()) {
// synchronized (getTreeLock()) {
// }}}
// */
// }
// private Dimension size = new Dimension(200,100);
// @Override
// public Dimension getMinimumSize()
// {
// if (scroll)
// return super.getMinimumSize();
// int height = 5;
// int lines = 1;
// for (int i = 0; i < mValues.length; i++)
// {
// lines = getWrappedLines(mValues[i]);
// if (mValues[i].isVisible())
// {
// FontMetrics fm = mValues[i].getFontMetrics(mValues[i].getFont());
// height +=((lines * fm.getHeight()) + TopPad + BotPad);
// }
// //I wasn't taking into account the space between values
// height +=4;
// }
// if (height > size.getHeight())
// return new Dimension(200,height);
// else
// return size;
// }
// @Override
// public Dimension getPreferredSize()
// {
// if (scroll)
// return super.getMinimumSize();
// return getMinimumSize();
// }
}