/*
* 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 tufts.vue.NotePanel;
//import tufts.vue.filter.NodeFilterEditor;
import tufts.vue.ActiveEvent;
import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.table.*;
import javax.swing.event.*;
import javax.swing.border.*;
import edu.tufts.vue.metadata.ui.MetadataEditor;
import edu.tufts.vue.metadata.ui.OntologicalMembershipPane;
import edu.tufts.vue.fsm.event.SearchEvent;
import edu.tufts.vue.fsm.event.SearchListener;
import org.apache.commons.lang.StringEscapeUtils;
/**
* Display information about the selected Resource, or LWComponent and it's Resource.
*
* @version $Revision: 1.131 $ / $Date: 2010-02-03 19:16:31 $ / $Author: mike $
*/
public class InspectorPane extends WidgetStack
implements VueConstants, LWSelection.Listener, SearchListener, Runnable
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(InspectorPane.class);
// public static final int META_VERSION = VueResources.getInt("metadata.version");
// /** meta-data */ public static final int OLD = 0;
// /** meta-data */ public static final int NEW = 1;
private final Image NoImage = VueResources.getImage("NoImage");
private final boolean isMacAqua = GUI.isMacAqua();
//private static final boolean EASY_READING_DESCRIPTION = VUE.VUE3;
//-------------------------------------------------------
// Node panes
//-------------------------------------------------------
private final LabelPane mLabelPane = new LabelPane(); // old style; new SummaryPane()
private final NotePanel mNotes = new NotePanel("LWNotes");
private final NotePanel mPathwayNotes = new NotePanel("PathwayNotes", false);
private final UserMetaData mKeywords = new UserMetaData();
private final OntologicalMembershipPane ontologicalMetadata = new OntologicalMembershipPane();
//-------------------------------------------------------
// Resource panes
//-------------------------------------------------------
private final MetaDataPane mResourceMetaData = new MetaDataPane("Resource Properties", false);
private final MetaDataPane mDataSetData = new MetaDataPane("Data Set Fields", false);
private final Widget mDescription = new Widget("contentInfo"); // for GUI.init property applicaton (use same as meta-data pane)
private final Preview mPreview = new Preview();
private final JLabel mSelectionInfo = new JLabel("", JLabel.CENTER);
private final WidgetStack stack;
private Resource mResource; // the current resource
private JScrollPane mScrollPane;
private boolean inScroll;
private static class Pane {
static Collection<Pane> AllPanes = new ArrayList();
final String name;
final JComponent widget;
final float size;
final int bits;
Pane(String s, JComponent c, float sz, int b) {
name = s;
widget = c;
size = sz;
bits = b;
AllPanes.add(this);
}
}
private static final int INFO = 1;
private static final int NOTES = 2;
private static final int KEYWORD = 4;
private static final int RESOURCE = 8;
private static final int DATA = 16;
public InspectorPane()
{
super("Info");
stack = this;
Widget.setWantsScroller(stack, true);
Widget.setWantsScrollerAlways(stack, true);
final float EXACT_SIZE = 0f;
final float EXPANDER = 1f;
mDescription.setBorder(GUI.WidgetInsetBorder);
mDescription.setOpaque(true);
mDescription.setBackground(Color.white);
mSelectionInfo.setFont(tufts.vue.gui.GUI.StatusFace);
//mSelectionInfo.setFont(tufts.vue.gui.GUI.StatusFaceSmall);
//mSelectionInfo.setFont(VueConstants.SmallFont);
mSelectionInfo.setForeground(Color.darkGray);
mSelectionInfo.setBorder(GUI.WidgetInsetBorder);
new Pane("_multi-selection-info", mSelectionInfo, EXACT_SIZE, 0);
new Pane(VueResources.getString("inspectorpane.label"), mLabelPane, EXACT_SIZE, INFO+NOTES+KEYWORD);
new Pane(VueResources.getString("inspectorpane.contentpreviews"),mPreview,EXACT_SIZE, RESOURCE);
new Pane(VueResources.getString("inspectorpane.datasetfields"),mDataSetData,EXACT_SIZE, INFO+NOTES+KEYWORD+DATA);
new Pane(VueResources.getString("inspectorpane.contentinfo"),mResourceMetaData, EXACT_SIZE, RESOURCE);
new Pane(VueResources.getString("inspectorpane.contentsummary"),mDescription, 0.5f, RESOURCE+DATA);
new Pane(VueResources.getString("nodeNotesTabName"),mNotes,EXACT_SIZE, INFO+NOTES);
new Pane(VueResources.getString("inspectorpane.pathwaynotes"),mPathwayNotes,EXPANDER,INFO+NOTES);
new Pane(VueResources.getString("jlabel.keyword"),mKeywords,EXACT_SIZE, KEYWORD);
new Pane(VueResources.getString("combobox.mergepropertychoices.ontologicalmembership"), ontologicalMetadata, EXACT_SIZE, 0);
for (Pane p : Pane.AllPanes)
stack.addPane(p.name, p.widget, p.size);
VUE.getSelection().addListener(this);
VUE.addActiveListener(LWComponent.class, this);
VUE.addActiveListener(Resource.class, this);
VUE.addActiveListener(LWPathway.Entry.class, this);
VUE.addActiveListener(MetaMap.class, this);
//VUE.getResourceSelection().addListener(this);
Widget.setHelpAction(mLabelPane,VueResources.getString("dockWindow.Info.summaryPane.helpText"));;
Widget.setHelpAction(mPreview,VueResources.getString("dockWindow.Info.previewPane.helpText"));;
Widget.setHelpAction(mResourceMetaData,VueResources.getString("dockWindow.Info.resourcePane.helpText"));;
Widget.setHelpAction(mNotes,VueResources.getString("dockWindow.Info.notesPane.helpText"));;
Widget.setHelpAction(mKeywords,VueResources.getString("dockWindow.Info.userPane.helpText"));;
Widget.setHelpAction(ontologicalMetadata,VueResources.getString("dockWindow.Info.ontologicalMetadata.helpText"));
//Set the default state of the inspector pane to completely empty as nothign
//is selected and its misleading to have the widgets on there.
hideAll();
// These two are present, but un-expanded by default:
Widget.setExpanded(ontologicalMetadata, false);
Widget.setExpanded(mKeywords, false);
setMinimumSize(new Dimension(300,500)); // if WidgetStack overrides getMinimumSize w/out checking for a set, this won't work.
}
public WidgetStack getWidgetStack() {
return stack;
}
@Override
public void addNotify() {
super.addNotify();
if (mScrollPane == null) {
mScrollPane = (JScrollPane) javax.swing.SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
if (DEBUG.Enabled) Log.debug("got scroller " + GUI.name(mScrollPane));
}
inScroll = (mScrollPane != null);
}
private void displayHold() {
if (inScroll) {
mScrollPane.getVerticalScrollBar().setValueIsAdjusting(true);
}
}
private void displayRelease() {
if (inScroll) {
//out("DO-SCROLL");
GUI.invokeAfterAWT(this);
}
}
/** interface Runnable */
public void run() {
// TODO: move this code up to a generic Widget capability,
// merging it with similar code in MetaDataPane Widget.
// 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.
//out("SCROLL-TO-TOP");
mScrollPane.getVerticalScrollBar().setValue(0);
mScrollPane.getVerticalScrollBar().setValueIsAdjusting(false);
//VUE.getInfoDock().scrollToTop();
// No idea why we might need to do this twice right now...
GUI.invokeAfterAWT(new Runnable() {
public void run() {
mScrollPane.getVerticalScrollBar().setValue(0);
mScrollPane.getVerticalScrollBar().setValueIsAdjusting(false);
//VUE.getInfoDock().scrollToTop();
}
});
}
public void activeChanged(final tufts.vue.ActiveEvent e, final Resource resource)
{
if (resource == null)
return;
if (e.source instanceof ActiveEvent && ((ActiveEvent)e.source).type == LWComponent.class) {
// if this resource change was due to the component change, ignore: is being
// handled all at once via our selectionChanged listener
return;
}
if (notShowing(e)) return;
displayHold();
//if (DEBUG.RESOURCE) out("resource selected: " + e.selected);
// showNodePanes(false);
// showResourcePanes(true);
displayPanes(RESOURCE);
loadResource(resource, null);
setVisible(true);
stack.setTitleItem("Content");
displayRelease();
}
// These events generally coming from the DataTree for data-set browsing (there's
// no LWComponent to browse against)
public void activeChanged(final tufts.vue.ActiveEvent e, final MetaMap dataMap)
{
if (dataMap == null)
return;
if (notShowing(e)) return;
// todo: below essentially repeats Resource active changed
displayHold();
displayPanes(DATA);
loadContentSummary(null, dataMap);
mDataSetData.loadTable(dataMap);
setVisible(true);
stack.setTitleItem("Data");
displayRelease();
}
private LWComponent activeEntrySelectionSync;
private LWPathway.Entry loadedEntry; // not needed at the moment
public void activeChanged(final tufts.vue.ActiveEvent _e, final LWPathway.Entry entry)
{
if (notShowing(_e)) return;
displayHold();
final int index = (entry == null ? -9 : entry.index() + 1);
loadedEntry = entry;
// //final boolean slidesShowing = LWPathway.isShowingSlideIcons();
// final boolean slidesShowing = entry.pathway.isShowingSlides();
if (index < 1 || entry.pathway.isShowingSlides()) {
Widget.hide(mPathwayNotes);
mPathwayNotes.detach();
activeEntrySelectionSync = null;
} else {
// We always get activeChanged events for the LWPathway.Entry BEFORE the
// resulting selection change on the map, so we can reliably set
// activeEntrySelectioSync here, and check for it later when are notified
// that the selection has changed.
// if (slidesShowing) {
// // This adds the reverse case: display node notes when a slide is selected:
// activeEntrySelectionSync = entry.getSlide();
// mPathwayNotes.attach(entry.node);
// mPathwayNotes.setTitle("Node Notes");
// } else {
activeEntrySelectionSync = entry.node;
mPathwayNotes.attach(entry.getSlide());
mPathwayNotes.setTitle("Pathway Notes (" + entry.pathway.getLabel() + ": #" + index + ")");
// }
Widget.show(mPathwayNotes);
}
displayRelease(); // can optimize out: should be able to rely on follow-on selectionChanged...
}
private boolean selectionChanged;
public void activeChanged(final tufts.vue.ActiveEvent e, final LWComponent c) {
// if (VUE.getSelection().size() == 0 && c != null) {
// // to pick up active layer sets
// loadSingleSelection(c);
// setVisible(true);
// }
// selectionChanged = false;
// GUI.invokeAfterAWT(new Runnable() {
// public void run() {
// if (!selectionChanged) {
// Log.debug("ACTIVE COMPONENT CHANGE W/OUT SELECTION CHANGE: " + c);
// loadSingleSelection(c);
// }
// }
// });
}
private boolean notShowing(Object event) {
// hack for now:
return VUE.inPresentMode();
// not good enough: may need to check our parent window, not the widget stack, which may
// not be visible yet, even if the parent window is
// if (isShowing()) {
// if (DEBUG.Enabled) Log.debug("SHOWING");
// return false;
// } else {
// if (DEBUG.Enabled) Log.debug("NOT SHOWING");
// return true;
// }
}
public void selectionChanged(final LWSelection s)
{
selectionChanged = true;
if (notShowing(s)) return;
displayHold();
if (s.size() == 0) {
hideAll();
mSelectionInfo.setText(VueResources.getString("inspectionpane.nothingselected.tooltip"));
Widget.show(mSelectionInfo);
setVisible(true);
} else if (s.size() == 1) {
loadSingleSelection(s.first());
setVisible(true);
} else {
loadMultiSelection(s);
setVisible(true);
}
displayRelease();
}
private void loadSingleSelection(LWComponent c)
{
Widget.hide(mSelectionInfo);
//displayPanes(NODE);
showNodePanes(true);
if (c instanceof LWSlide || c.hasAncestorOfType(LWSlide.class)) {
Widget.hide(mKeywords);
Widget.hide(ontologicalMetadata);
}
if (activeEntrySelectionSync != c) {
Widget.hide(mPathwayNotes);
loadedEntry = null;
}
activeEntrySelectionSync = null;
loadAllNodePanes(c);
// if (c.hasResource()) {
// //loadResource(c.getResource());
// loadResource(c.getResource(), c);
// showResourcePanes(true);
// } else {
// showResourcePanes(false);
// }
}
private static final String ItemsSelectedFmt = VueResources.getString("infowindow.itemselected");
private void loadMultiSelection(final LWSelection s)
{
loadedEntry = null;
hideAllPanes();
mKeywords.loadKeywords(null);
setTitleItem(String.format(ItemsSelectedFmt, s.size()));
final String desc = getSelectionDescription(s);
if (DEBUG.Enabled) Log.debug("selection-description: " + Util.tags(desc));
// why after AWT?
//GUI.invokeAfterAWT(new Runnable() { public void run() {
mSelectionInfo.setText(desc);
//}});
mLabelPane.loadLabel(s);
Widget.show(mLabelPane); // connect up to schematic-field style node?
Widget.show(mSelectionInfo);
//Widget.setExpanded(mKeywords, true);
Widget.show(mKeywords);
}
private static final String SelectionStatsFmt = VueResources.local("mapinspectorpanel.objectStats.format");
private static final String WordNodes = VueResources.local("mapinspectorpanel.objectStats.nodes");
private static final String WordLinks = VueResources.local("mapinspectorpanel.objectStats.links");
private static final String WordGroups = VueResources.local("mapinspectorpanel.objectStats.groups");
private static final String WordImages = "Images:";
private static final String DescriptionFormat = "<html>%s %d %s %d %s %d</html>";
private static final String SP = " ";
private static final String SP3 = " ";
private static final String GAP = " ";
/** A convenience class whose toString() returns "" the first time, and real content after that.
* Useful for generating lines of output with separators that aren't of course wanted the first time.
*/private static final class EmptyOnce {
final String s; boolean did;
EmptyOnce(String in) { s = in; }
public String toString() { if (did) return s; else { did = true; return ""; } }
}
//private static final EmptyOnce Gap = new EmptyOnce("<br>");
private static final EmptyOnce Gap = new EmptyOnce(GAP);
private static final String Post = "";
//private static final String Gap = "<ul>";
//private static final String Post = "</ul>";
protected static String getSelectionDescription(final LWSelection s)
{
final StringBuilder b = new StringBuilder(40);
// todo: could actually call getTypes() on LWSelection to count types of each kind
final int nodeCount = s.count(LWNode.class);
final int linkCount = s.count(LWLink.class);
final int groupCount = s.count(LWGroup.class);
final int imageCount = s.count(LWImage.class);
Gap.did = false;
// Recall that <font color=green>, etc, actually works as well. Try below if want to play w/justified labels/values.
// b.append("<html><b><br><table border=0 cellpadding=1><tr align=right><td>foo</td><td>barbella</td><tr><td>bizwing</td><td>box</td></table>");
b.append("<html><b>");
if (nodeCount > 0) { b.append(Gap).append(WordNodes) .append(SP).append(nodeCount).append(Post); }
if (linkCount > 0) { b.append(Gap).append(WordLinks) .append(SP).append(linkCount).append(Post); }
if (imageCount > 0) { b.append(Gap).append(WordImages).append(SP).append(imageCount).append(Post); }
if (groupCount > 0) { b.append(Gap).append(WordGroups).append(SP).append(groupCount).append(Post); }
if (s.getDescription() != null) {
b.append("<br>");
b.append(s.getDescription());
}
b.append("</html>");
return b.toString();
}
// Selection already keeps counts:
// return String.format(Locale.getDefault(), SelectionStatsFmt,
// WordNodes, s.count(LWNode.class),
// WordLinks, s.count(LWLink.class),
// WordGroups, s.count(LWGroup.class));
// protected String countObjects(LWSelection sel) {
// int nodeCount = 0,
// linkCount = 0,
// groupCount = 0;
// for (LWComponent comp : sel) {
// if (comp instanceof LWNode) {
// nodeCount++;
// } else if (comp instanceof LWLink) {
// linkCount++;
// } else if (comp instanceof LWGroup) {
// groupCount++;
// }
// }
// return String.format(Locale.getDefault(), VueResources.getString("mapinspectorpanel.objectStats.format"),
// VueResources.getString("mapinspectorpanel.objectStats.nodes"), nodeCount,
// VueResources.getString("mapinspectorpanel.objectStats.links"), linkCount,
// VueResources.getString("mapinspectorpanel.objectStats.groups"), groupCount);
// }
private void loadAllNodePanes(LWComponent c) {
LWComponent slideTitle = null;
if (c instanceof LWSlide && c.isPathwayOwned() && c.hasChildren()) {
search:
try {
final LWComponent titleStyle = ((LWSlide)c).getEntry().pathway.getMasterSlide().getTitleStyle();
if (titleStyle == null)
break search;
// check top-level children first:
for (LWComponent sc : c.getChildren()) {
if (sc.getStyle() == titleStyle) {
slideTitle = sc;
break search;
}
}
// check everything else second
// We wind up rechecking the slide's immediate children, but this is just a fallback condition.
for (LWComponent sc : c.getAllDescendents()) {
if (sc.getStyle() == titleStyle) {
slideTitle = sc;
break search;
}
}
} catch (Throwable t) {
Log.error("load " + c, t);
}
}
if (slideTitle == null)
mLabelPane.loadLabel(c);
else
mLabelPane.loadLabel(slideTitle, c);
if (c.getDataTable() == null) {
Widget.hide(mDataSetData);
} else {
mDataSetData.loadTable(c.getDataTable());
Widget.show(mDataSetData);
}
if (c.hasResource()) {
//loadResource(c.getResource());
loadResource(c.getResource(), c);
showResourcePanes(true);
} else {
showResourcePanes(false);
}
mKeywords.loadKeywords(c);
if (DEBUG.Enabled)
setTitleItem(c.getUniqueComponentTypeLabel());
else
setTitleItem(c.getComponentTypeLabel());
}
private static void setTypeName(JComponent component, LWComponent c, String suffix)
{
final String type;
if (c == null)
type = null;
else if (DEBUG.Enabled)
type = c.getUniqueComponentTypeLabel();
else
type = c.getComponentTypeLabel();
String title;
if (suffix != null) {
if (type == null)
title = suffix;
else
title = type + " " + suffix;
} else
title = type;
component.setName(title);
}
// //-----------------------------------------------------------------------------
// // experimental code to export to PropertyMap:
// private static final class Key<T> {
// final String name;
// //Key(T t) { type = t; }
// Key(String s) { name = s; }
// //Key() { name = T.class; } // can't query type for class
// @Override
// public String toString() {
// return name;
// }
// public T cast(Object o) { // would this be needed / handy? (would it even work to throw a cast-class, or is this all type-erased?)
// return (T) o;
// }
// }
// //private static final Key<JComponent> DESCRIPTION_VIEWER = new Key(JComponent.class);
// private static final Key<JComponent> DESCRIPTION_VIEWER = new Key("description_viewer");
// private static final Key<Integer> DESCRIPTION_VIEWER_N = new Key("test_int");
// private static <T> T get(Key<T> key) { return (T) null; }
// // or could infer a new key from arguments? (of course, would need to cache the keys tho)
// //private static <T> void put(Class<T> type, String name, T value) {
// private static <T> T put(String name, T value) {
// Key<T> key = new Key(name);
// return get(key);
// }
// static {
// // int x = put("foo", 1);
// // long z = put("foo", 1);
// // char c = put("foo", 'x');
// // JLabel l = put("bar", new JLabel("baz"));
// // //JLabel m = put("bar", new JPanel()); // error, as appropriate
// // JComponent foo = get(DESCRIPTION_VIEWER);
// // int i = get(DESCRIPTION_VIEWER_N);
// }
// //-----------------------------------------------------------------------------
private void loadResource(final Resource r, LWComponent node) {
if (DEBUG.RESOURCE) out("loadResource: " + r);
if (r == null)
return;
mResource = r;
//mResourceMetaData.loadResource(r);
mResourceMetaData.loadTable(r.getProperties());
mPreview.loadResource(r);
Widget.hide(mSelectionInfo);
loadContentSummary(r, node == null ? null : node.getRawData());
}
private static final String DESCRIPTION_VIEWER_KEY = Resource.HIDDEN_RUNTIME_PREFIX + "ipCache";
private boolean loadContentSummary(Resource r, MetaMap data) {
JComponent descriptionView = (r == null ? null : (JComponent) r.getPropertyValue(DESCRIPTION_VIEWER_KEY));
boolean gotView = (descriptionView != null);
if (descriptionView == null) {
try {
descriptionView = buildSummary(r, data);
if (descriptionView != null)
gotView = true;
} catch (Throwable t) {
Log.error("loadResource " + r, t);
// html will enable text-wrap
descriptionView = new JLabel("<html>"+ r + "<p>" + t.toString());
}
// todo: the below should ideally be auto-cleared when any change to the
// resource is made and/or any change to it's component properties.
// (would need to separate "real" meta-data properties from runtime
// client properties such as this one in that case). It would be nice
// if resource properties could individually be specified as having
// these special attributes themselves. (e.g., "soft" or
// "auto-clear-on-change")
if (gotView && r != null)
r.setProperty(DESCRIPTION_VIEWER_KEY, new java.lang.ref.SoftReference(descriptionView));
// r.putSoft
// r.data.putSoft
}
mDescription.removeAll();
if (gotView) {
mDescription.add(descriptionView);
mDescription.repaint();
return true;
} else {
mDescription.setHidden(true);
return false;
}
}
private static final HyperlinkListener DefaultHyperlinkListener =
new HyperlinkListener() {
public void hyperlinkUpdate(HyperlinkEvent e) {
//Log.debug(e);
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
java.net.URL url = null;
try {
url = e.getURL();
if (url == null) {
Log.warn("no URL in " + e);
} else {
tufts.vue.VueUtil.openURL(url.toString());
}
} catch (Throwable t) {
Log.warn("hyperlinkUpdate; url=" + url + "; " + e, t);
}
}
}
};
// private static String findProperty(Resource r, LWComponent node, String... keys) {
// for (String k : keys) {
// String value = null;
// if (node != null) {
// value = node.getDataValue(k);
// if (value != null)
// return value;
// }
// if (r != null) {
// value = r.getProperty(k);
// if (value != null)
// return value;
// }
// }
// return null;
// }
private static String findProperty(Resource r, MetaMap data, String... keys) {
for (String k : keys) {
String value = null;
if (data != null) {
value = data.getString(k);
if (value != null)
return value;
if (Character.isUpperCase(k.charAt(0)))
value = data.getString(k.toLowerCase());
if (value != null)
return value;
}
if (r != null) {
value = r.getProperty(k);
if (value != null)
return value;
if (Character.isUpperCase(k.charAt(0)))
value = r.getProperty(k.toLowerCase());
if (value != null)
return value;
}
}
return null;
}
private static JComponent buildSummary(final Resource r, final MetaMap data)
{
final JTextPane htmlText = new JTextPane();
htmlText.setEditable(false);
htmlText.setContentType("text/html");
htmlText.setName("description:" + r);
htmlText.addHyperlinkListener(DefaultHyperlinkListener);
//final MetaMap data = (node == null ? null : node.getRawData());
final String desc = findProperty(r, data, "descriptionSummary", "Description", "Summary");
final String summary;
if (desc != null || r == null) {
summary = buildSummaryWithDescription(r, data, desc);
if (summary == null || summary.length() == 0)
return null;
//htmlText.setText("No Description");// todo: should just hide panel
else
htmlText.setText(summary);
if (DEBUG.DATA && data != null) data.put("$reformatted", "[" + summary + "]");
} else { //if (r != null) {
//------------------------------------------------------------------
// No description was found: build a summary from just the Resource
//------------------------------------------------------------------
summary = buildSummaryFromResource(r);
htmlText.setText(summary);
if (DEBUG.DATA) r.setProperty("~reformatted", summary);
}
//else return null;
// This must be done last. Why this doesn't work up front I don't know: there
// must be some other way...
GUI.setDocumentFont(htmlText, GUI.ContentFace);
return htmlText;
// if (loader != null) {
// // this not actually helping, other than we get to see "Loading..." before it hangs.
// final Thread _loader = loader;
// GUI.invokeAfterAWT(new Runnable() { public void run() {
// _loader.start();
// }});
// }
}
private static String buildSummaryFromResource(final Resource r)
{
final StringBuilder b = new StringBuilder(128);
final String title = r.getTitle();
if (title != null) {
b.append("<b>");
// note that the title should already have been HTML UNescaped when set:
// now we're escaping it again in case it's natural state actually
// contains anything that looks like an HTML tag. (technically, we
// should really only need to escape '<', '>' and '&' at this point)
b.append(StringEscapeUtils.escapeHtml(title));
b.append("</b>");
b.append("<p>");
}
String spec = r.getSpec();
//-----------------------------------------------------------------------------
// We escape HTML here because at least on Mac OS X, valid HTML tags could
// appear in a file name. Note that doing this also effects behaviour of
// our line-breaking tweaks below. We also want to do this before before we
// insert any special unicode characters. E.g., otherwise, inserting
// zero-width unicode space character \u200B would result in
// inserting HTML entity , which does still appear to work as a break,
// but is waste to encode.
// In case of muti-encoded URL's (contains embedded URL's / queries),
// decode multiple times for a more readable display.
//spec = Util.decodeURL(spec);
// todo: start by looking for any top-level query: if none found, decodeURL once, then start
// over looking for a top level query. Then process each key/value, indenting if embedded queries found,
// and always applying an extry decodeURL to each value in "&key=value"
// also: don't make entire thing a multi-line href -- all the whitespace will be accidentally
// clickable to launch, and also we'll want to inspect each value for an http: value
// (or any key name ending in "url") and make THAT a link, so it can be explored separately.
spec = Util.decodeURL(spec);
spec = StringEscapeUtils.escapeHtml(spec);
//-----------------------------------------------------------------------------
// will allow text pane to line-break the url (plus makes easier to read queries)
// \u200B is Unicode zero-width space: JTextPane will break on these. We add
// as desired to break URL's / file paths as painlessly as possible. Note
// that apparently including any special unicode character, or perhaps and
// break-related unicode character into the JTextPane, appears to
// automatically turn on breaking at basic punctuation: e.g. '.', '-' and '?'.
// But it does NOT slash or ampersand which we really need, nor '=', or underscore.
// BTW, \uFEFF is the Unicode zero-width NO-BREAK space, which JTextPane,
// treats properly as non-breaking.
//-----------------------------------------------------------------------------
// break after these (the special char should end the previous line)
//-----------------------------------------------------------------------------
spec = spec.replaceAll("/", "/\u200B");
spec = spec.replaceAll("=", "=\u200B");
//-----------------------------------------------------------------------------
// break before these (the special char should start the new line):
//-----------------------------------------------------------------------------
// note that breaking AFTER '&' will probably not work, as now we're
// inserting breaks into a string that has been HTML escaped, so
// at this point, we're actually inserting a break before any HTML entity,
// (e.g., & "', etc) not just '&'
if (false) {
spec = spec.replaceAll("&", "\u200B&");
} else {
//spec = spec.replaceAll("&", "<br>&");
//spec = spec.replaceAll("&", "<li>");
spec = spec.replaceAll("&", "<br>& ");
spec = spec.replaceFirst("\\?", "<br>?");
spec = spec.replaceFirst("\\*\\*", "<br>**"); // yahoo image URL's use this
}
spec = spec.replaceAll("_", "\u200B_");
spec = spec.replaceAll("@", "\u200B@");
// spec = spec.replaceAll(":", "\u200B:"); // todo: for generic descriptions which might contain big URN's (e.g., tufts DL)
spec = spec.replaceAll("\\+", "\u200B+");
spec = spec.replaceAll("\\?", "\u200B?");
// todo: ideally, also put break before lower-case to upper-case char transitions
//-----------------------------------------------------------------------------
b.append("<font size=-1>");
b.append("<a href=\"");
try {
if (r.isLocalFile()) {
if (r.getSpec().startsWith("file:"))
b.append(r.getSpec());
else
b.append("file://" + r.getSpec());
} else
b.append(r.getSpec());
//b.append(r.getActiveDataFile().toURL());
} catch (Throwable t) {
Log.warn(r, t);
b.append(r.getSpec());
}
b.append("\">");
b.append(spec);
b.append("</a>");
return b.toString();
}
private static String cleanForTextPaneAndTrim(String desc) {
// text pane doesn't handle (e.g. <br/> is very common)
desc = desc.replaceAll("/>", ">");
// remote some initial space + breaks
desc = desc.replaceAll("^\\s*<br>\\s*", "");
desc = desc.replaceAll("^\\s*<br>\\s*", "");
desc = desc.replaceAll("^\\s*<br>\\s*", "");
// remote some trailing space + breaks
desc = desc.replaceAll("\\s*<br>\\s*$", "");
desc = desc.replaceAll("\\s*<br>\\s*$", "");
desc = desc.replaceAll("\\s*<br>\\s*$", "");
return desc;
}
private static String buildSummaryWithDescription(final Resource r,
final MetaMap data,
String desc)
{
final StringBuilder buf = new StringBuilder(128 + (desc == null ? 0 : desc.length()));
if (desc != null)
desc = cleanForTextPaneAndTrim(desc);
String title = findProperty(r, data, "Title");
if (title == null && r != null)
title = r.getTitle();
if (title == null && desc == null)
return null;
if (desc == null || desc.indexOf("<style") < 0) {
// only add a title if no style sheet present ("complex content" e.g., jackrabbit jira)
// final String thumb = findProperty(r, data, "@Thumb", "thumbnailURL");
// if (thumb != null) {
// // Note we're dealing with ANCIENT java text-pane HTML...
// //buf.append("<img valign=middle src=");
// buf.append("<img align=middle src=");
// buf.append(thumb);
// buf.append(">");
// //buf.append("<br>");
// }
if (title != null) {
buf.append("<b><font size=+1>"); // note: h1/h2 are useless here. font+1 a bit more than we want tho...
buf.append(title);
buf.append("</font></b>");
}
final String published = findProperty(r, data, "Published", "pubDate", "dc:date", "Date", "Created", "dateUpdated");
if (published != null) {
buf.append("<br>\n");
//buf.append("<font size=-1 color=808080>");
buf.append("<font color=B0B0B0><b>");
buf.append(published);
String author = findProperty(r, data, "Author", "dc:creator", "Creator", "Publisher", "Reporter", "Name");
if (author != null) {
buf.append(" - ");
buf.append(author);
}
buf.append("</b></font>");
//buf.append("Published " + r.getProperty("Published"));
}
// if (r.hasProperty("Published")) {
// buf.append("<p>");
// //buf.append("<font size=-1 color=808080>");
// buf.append("<font color=B0B0B0><b>");
// buf.append(r.getProperty("Published"));
// //buf.append("Published " + r.getProperty("Published"));
// }
if (desc != null) {
// first, handle white-space around breaks so as not to overbreak later handling newlines
desc = desc.replaceAll("<br>\\s*<br>\\s*", "<p>");
// now handle common paragraph breaks
desc = desc.replaceAll("\n\n", "<p>");
if (!desc.startsWith("<p>"))
buf.append("<p>\n");
}
}
// final boolean willDoNetworkIO;
// if (DEBUG.Enabled)
// willDoNetworkIO = desc.indexOf("<img") >= 0;
// else
// willDoNetworkIO = false;
boolean hasDesc = false;
if (desc != null && desc.length() > 0 && !desc.equals(title)) {
// some OSID's create with spec same as title (!?) e.g., NCBI
// (URL property is set, but the spec is not)
if (DEBUG.IO) {
// some images don't seem to appear:
desc = desc.replaceAll("<img", "<i>[IO:IMAGE]</i><img");
// actually, those appear to be embedded invisible refs
// so content providers know when their content is displayed
} else {
// display the image tag and let it display for debugging:
//desc = desc.replaceAll("<img ", "");
}
// not all HTML entities are handled by JTextPane, for example: — €
//if (DEBUG.DR) buf.append('[');
buf.append(StringEscapeUtils.unescapeHtml(desc));
//if (DEBUG.DR) buf.append(']');
hasDesc = true;
}
// final String media = findProperty(r, data, "media:content@url");
// if (media != null && Resource.looksLikeURLorFile(media) && Resource.looksLikeImageFile(media)) {
// buf.append(String.format("<center><a href=\"%s\"><img src=\"%s\"></a><br>", media, media));
// }
String link = findProperty(r, data, "Link");
if (link != null && Resource.looksLikeURLorFile(link)) {
if (hasDesc)
buf.append("<p>");
else if (!buf.toString().endsWith("<br>"))
buf.append("<br>");
//if (DEBUG.DR && hasDesc) buf.append("hadDescription:");
buf.append(String.format("<a href=\"%s\">%s</a><br>",
link,
Util.decodeURL(link)));
}
// todo: the lookup of "Cover Image" is temporary hack: add regex lookup for *image* and/or
// the auto-detection of file/URL encoded fields in data sets.
final String image = findProperty(r, data, "Cover Image");
if (image != null && Resource.looksLikeURLorFile(image) && Resource.looksLikeImageFile(image)) {
buf.append(String.format("<center><a href=\"%s\"><img src=\"%s\"></a>", image, image));
}
//if (DEBUG.DATA && r != null) r.setProperty("~reformatted", reformatted);
return buf.toString();
// if (! willDoNetworkIO) {
// htmlText.setText(reformatted);
// } else {
// // TODO: this can hang if the network is down while the new
// // StyledDocument is being created. Trying to set the new document
// // in another thread is of no help, in that AWT still then just
// // hangs when it attempts to paint the component.
// //mDescription.setText("Loading...");
// loader =
// new Thread("loadHTML " + r) {
// @Override
// public void run() {
// Log.debug("SET-TEXT...");
// try {
// Document doc = mDescription.getDocument();
// doc.remove(0, doc.getLength());
// Reader r = new StringReader(reformatted);
// EditorKit kit = mDescription.getEditorKit();
// kit.read(r, doc, 0); // no help: just hangs here
// GUI.setDocumentFont(mDescription, GUI.ContentFace);
// } catch (Throwable t) {
// Log.error(t);
// }
// Log.debug("SET-TEXT COMPLETED.");
// //mDescription.setText(reformatted);
// }
// };
// }
//mDescription.setToolTipText(reformatted);
}
public void removeAll()
{
super.removeAll();
Pane.AllPanes.clear();
Pane.AllPanes = null;
Pane.AllPanes = new ArrayList<Pane>();
}
private void hideAllPanes()
{
for (Pane p : Pane.AllPanes)
Widget.setHidden(p.widget, true);
}
private void hideAll()
{
setVisible(false);
hideAllPanes();
setTitleItem(VueResources.getString("infowindow.nothingselected"));
}
private void expandCollapsePanes(int type) {
for (Pane p : Pane.AllPanes) {
if (Widget.isHidden(p.widget))
continue;
if ((p.bits & type) != 0) {
if (!Widget.isExpanded(p.widget))
Widget.setExpanded(p.widget, true);
} else {
if (Widget.isExpanded(p.widget))
Widget.setExpanded(p.widget, false);
}
}
}
private void setPanesVisible(int type, boolean visible) {
//boolean anyVisible = false;
for (Pane p : Pane.AllPanes) {
if ((p.bits & type) != 0) {
Widget.setHidden(p.widget, !visible);
// if (visible)
// anyVisible = true;
}
}
// if (anyVisible)
// Widget.setHidden(mSelectionInfo, true);
}
private void displayPanes(int type) {
boolean anyVisible = false;
for (Pane p : Pane.AllPanes) {
if ((p.bits & type) != 0) {
Widget.setHidden(p.widget, false);
anyVisible = true;
} else
Widget.setHidden(p.widget, true);
}
if (anyVisible)
Widget.setHidden(mSelectionInfo, true);
}
private void showNodePanes(boolean visible) {
// todo: should be using setPanesVisible here
Widget.setHidden(mLabelPane, !visible);
Widget.setHidden(mNotes, !visible);
Widget.setHidden(mDataSetData, !visible);
Widget.setHidden(mKeywords, !visible);
Widget.setHidden(ontologicalMetadata, !visible);
}
private void showResourcePanes(boolean visible) {
setPanesVisible(RESOURCE, visible);
}
public void showKeywordView()
{
expandCollapsePanes(KEYWORD);
}
public void showNotesView()
{
expandCollapsePanes(NOTES);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner();
mNotes.getTextPane().requestFocusInWindow();
}
} );
}
public void showInfoView() {
expandCollapsePanes(INFO);
}
private class Preview extends tufts.vue.ui.PreviewPane
{
Preview() {
setName("contentPreview");
}
void loadResource(Resource r) {
super.loadResource(r);
String title = r.getTitle();
if (title == null)
title = r.getProperty("title");
// TODO: resource property lookups should be case insensitive
if (title == null)
title = r.getProperty("Title");
if (title == null)
title = "Content Preview";
Widget.setTitle(this, title);
setToolTipText(title);
}
}
private class InlineTitleResourcePreview extends tufts.vue.ui.PreviewPane
{
private final JLabel mTitleField;
//private final JTextPane mTitleField;
//private final JTextArea mTitleField;
//private final PreviewPane mPreviewPane = new PreviewPane();
InlineTitleResourcePreview() {
//super(new BorderLayout());
// JTextArea -- no good (no HTML)
//mTitleField = new JTextArea();
//mTitleField.setEditable(false);
// JTextPane -- no good, too fuckin hairy and slow (who needs an HTML editor?)
//mTitleField = new JTextPane();
//StyledDocument doc = new javax.swing.text.html.HTMLDocument();
//mTitleField.setStyledDocument(doc);
//mTitleField.setEditable(false);
mTitleField = new JLabel("", JLabel.CENTER);
//-------------------------------------------------------
GUI.apply(GUI.TitleFace, mTitleField);
mTitleField.setAlignmentX(0.5f);
//mTitleField.setBorder(new LineBorder(Color.red));
//mTitleField.setOpaque(false);
//mTitleField.setBorder(new EmptyBorder(0,2,5,2));
//mTitleField.setSize(200,50);
//mTitleField.setPreferredSize(new Dimension(200,30));
//mTitleField.setMaximumSize(new Dimension(Short.MAX_VALUE,Short.MAX_VALUE));
//mTitleField.setMinimumSize(new Dimension(100, 30));
//add(mPreviewPane, BorderLayout.CENTER);
add(mTitleField, BorderLayout.SOUTH);
}
void loadResource(Resource r) {
super.loadResource(r);
String title = r.getTitle();
//mPreviewPane.setVisible(false);
if (title == null || title.length() < 1) {
mTitleField.setVisible(false);
return;
}
// Always use HTML, which creates auto line-wrapping for JLabels
title = "<HTML><center>" + title;
mTitleField.setVisible(true);
if (true) {
mTitleField.setText(title);
} else {
//remove(mTitleField);
out("OLD size=" + mTitleField.getSize());
out("OLD preferredSize=" + mTitleField.getPreferredSize());
mTitleField.setText(title);
out("NOLAY size=" + mTitleField.getSize());
out("NOLAY preferredSize=" + mTitleField.getPreferredSize());
//mTitleField.setSize(298, mTitleField.getHeight());
//mTitleField.setSize(298, mTitleField.getHeight());
//mTitleField.setPreferredSize(new Dimension(298, mTitleField.getHeight()));
//mTitleField.setSize(mTitleField.getPreferredSize());
//mTitleField.setSize(mTitleField.getPreferredSize());
out("SETSZ size=" + mTitleField.getSize());
out("SETSZ preferredSize=" + mTitleField.getPreferredSize());
//out("SETSZ preferredSize=" + mTitleField.getPreferredSize());
mTitleField.setPreferredSize(null);
//add(mTitleField, BorderLayout.SOUTH);
//mTitleField.revalidate();
//repaint();
//mTitleField.setVisible(true);
}
//mPreviewPane.loadResource(mResource);
//mPreviewPane.setVisible(true);
//VUE.invokeAfterAWT(this);
}
/*
public void run() {
//mTitleField.revalidate();
//mTitleField.setVisible(true);
VUE.invokeAfterAWT(new Runnable() { public void run() {
mPreviewPane.loadResource(mResource);
}});
}
*/
}
/*
public static class NodeTree extends JPanel
{
private final OutlineViewTree tree;
public NodeTree()
{
super(new BorderLayout());
setName("Nested Nodes");
tree = new OutlineViewTree();
JScrollPane mTreeScrollPane = new JScrollPane(tree);
mTreeScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
add(mTreeScrollPane);
}
public void load(LWComponent c)
{
// if the tree is not intiliazed, hidden, or doesn't contain the given node,
// then it switches the model of the tree using the given node
if (!tree.isInitialized() || !isVisible() || !tree.contains(c)) {
//panelLabel.setText("Node: " + pNode.getLabel());
if (c instanceof LWContainer)
tree.switchContainer((LWContainer)c);
else if (c instanceof LWLink)
tree.switchContainer(null);
}
}
}
*/
public void ontologicalMetadataUpdated()
{
ontologicalMetadata.refresh();
}
public class UserMetaData extends Widget
{
//private NodeFilterEditor userMetaDataEditor = null;
private MetadataEditor userMetadataEditor = null;
public UserMetaData()
{
super("Keywords");
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
// todo in VUE to create map before adding panels or have a model that
// has selection loaded when map is added.
// userMetaDataEditor = new NodeFilterEditor(mNode.getNodeFilter(),true);
// add(userMetaDataEditor);
}
// might someday be useful for Metadata version NEW
// but only in that case (see ontologicalMetadataUpdated() above)
public void refresh()
{
Log.debug("REFRESH " + this);
if (userMetadataEditor != null)
userMetadataEditor.listChanged();
}
void loadKeywords(LWComponent c) {
if (userMetadataEditor == null) {
setOpaque(false);
userMetadataEditor = new MetadataEditor(c,true,true);
LWSelection selection = VUE.getSelection();
if(selection.contents().size() > 1) {
userMetadataEditor.selectionChanged(selection);
}
add(userMetadataEditor,BorderLayout.CENTER);
}
if (c != null && c.hasMetaData())
mKeywords.setTitle("Keywords (" + c.getMetadataList().getCategoryListSize() + ")");
else
mKeywords.setTitle("Keywords");
}
// @Override
// public Dimension getMinimumSize() {
// //return new Dimension(200,150);
// Dimension s = getPreferredSize();
// s.height += 9;
// return s;
// }
// void load(LWComponent c) {
// //setTypeName(this, c, "Keywords");
// if(META_VERSION == OLD)
// {
// if (DEBUG.SELECTION) System.out.println("NodeFilterPanel.updatePanel: " + c);
// if (userMetaDataEditor != null) {
// //System.out.println("USER META SET: " + c.getNodeFilter());
// userMetaDataEditor.setNodeFilter(c.getNodeFilter());
// } else {
// if (VUE.getActiveMap() != null && c.getNodeFilter() != null) {
// // NodeFilter bombs entirely if no active map, so don't let
// // it mess us up if there isn't one.
// userMetaDataEditor = new NodeFilterEditor(c.getNodeFilter(), true);
// add(userMetaDataEditor, BorderLayout.CENTER);
// //System.out.println("USER META DATA ADDED: " + userMetaDataEditor);
// }
// }
// }
// else
// {
// if(userMetadataEditor == null)
// {
// setOpaque(false);
// userMetadataEditor = new MetadataEditor(c,true,true);
// LWSelection selection = VUE.getSelection();
// if(selection.contents().size() > 1)
// {
// userMetadataEditor.selectionChanged(selection);
// }
// add(userMetadataEditor,BorderLayout.CENTER);
// }
// }
// }
}
private JLabel makeLabel(String s) {
JLabel label = new JLabel(s);
GUI.apply(GUI.LabelFace, label);
//label.setBorder(new EmptyBorder(0,0,0, GUI.LabelGapRight));
return label;
}
// summary fields
/*
private final Object[] labelTextPairs = {
"-Title", mTitleField,
"-Where", mWhereField,
"-Size", mSizeField,
};
*/
public class LabelPane extends tufts.Util.JPanelAA
{
private LWSelection selection;
private LWComponent dataStyle;
private final VueTextPane labelValue = new VueTextPane() {
@Override
protected void applyText(String text) {
// TODO: as dataStyle nodes in the Schema/Field are not part of the map model
// proper, their edits will not be undoable. Can we just manually deliver the
// event up thru the active map? Oh, damn... the dataStyle is owned the
// Schema, which while saved with the map is actually within the DataTree at
// runtime and applies to all open maps, and we have no "global" portion of the
// undo queue to handle this... well, we could try and just ignore the global
// aspect and see what happens...
super.applyText(text);
if (selection != null && text != null && text.trim().length() > 0) {
Log.debug("LabelPane: manually applying to " + selection);
for (LWComponent c : selection) {
c.setLabel(text);
}
if (dataStyle != null)
VUE.markUndo(selection.size() + " Data Labels");
else
VUE.markUndo(selection.size() + " Labels");
}
}
};
LabelPane() {
super(new BorderLayout());
labelValue.setFont(tufts.vue.gui.GUI.LabelFace);
labelValue.setName(getClass().getName());
final int insetInner = 5;
if (Util.isMacPlatform()) {
final int so = 7; // size outer
// be sure to fetch and include the existing border, in case it's a special mac hilighting border
labelValue.setBorder(new CompoundBorder(new MatteBorder(so,so,so,so,SystemColor.control),
new CompoundBorder(labelValue.getBorder(),
GUI.makeSpace(insetInner)
)));
} else {
setBorder(GUI.makeSpace(5));
labelValue.setBorder(new CompoundBorder(new EtchedBorder(EtchedBorder.LOWERED),
GUI.makeSpace(insetInner)));
}
setName("nodeLabel");
add(labelValue);
}
void loadLabel(LWSelection s) {
if (DEBUG.Enabled) Log.debug("LabelPane LOADING SELECTION w/style " + s.getStyleRecord());
labelValue.detachProperty();
if (s.getStyleRecord() != null) {
labelValue.attachProperty(s.getStyleRecord(), LWKey.Label);
setName("Multiple Data Labels");
//selection = null;
dataStyle = s.getStyleRecord();
} else {
dataStyle = null;
setName(String.format(VueResources.getString("infowindow.multiplelabel"), s.size()));
}
// selection = s;
// The clone is usually overkill, but needed in case selection is cleared before our applyText handler is called.
// (which can happen if the user manages to click empty space on the map w/out the mouse-entered
// handler have triggered a save-text).
selection = s.clone();
//labelValue.loadText(String.format("<changes will apply to all %d nodes>", s.size()));
//setTypeName(this, null, "Multiple Labels");
labelValue.setEditable(true);
}
void loadLabel(LWComponent c) {
loadLabel(c, c);
}
private boolean first = true;
private Border border = null;
void loadLabel(LWComponent c, LWComponent editType) {
selection = null;
if (first)
{
border = labelValue.getBorder();
first = false;
}
setTypeName(this, editType, VueResources.getString("inspectorpane.label"));
if (c instanceof LWText)
{
labelValue.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
labelValue.setBackground(this.getBackground());
labelValue.setEditable(false);
}
else
{
labelValue.setBackground(Color.white);
labelValue.setBorder(border);
labelValue.setEditable(true);
}
labelValue.attachProperty(c, LWKey.Label);
}
}
public class SummaryPane extends tufts.Util.JPanelAA
implements Runnable
{
final VueTextPane labelValue = new VueTextPane();
final JScrollBar labelScrollBar;
//final VueTextField contentValue = new VueTextField();
SummaryPane()
{
super(new GridBagLayout());
//setBorder(new EmptyBorder(4, GUI.WidgetInsets.left, 4, 0));
setBorder(GUI.WidgetInsetBorder);
setName("nodeSummary");
labelValue.setName(getClass().getSimpleName());
//If you're trying to debug lists, you'll want to see the HTML code somewhere,
//and here is as good as any place right now. It may be a TODO to put this label
//on a tabbed pane with an editable version of the HTML code so you can tweak it
//in case of an error.
// if (!DEBUG.LISTS)
// labelValue.setContentType("text/html");
//contentValue.setEditable(false);
labelValue.setBorder(null);
JScrollPane labelScroller = new JScrollPane(labelValue,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
);
int lineHeight = labelValue.getFontMetrics(labelValue.getFont()).getHeight();
labelScroller.setMinimumSize(new Dimension(70, (lineHeight*3)+5));
labelScrollBar = labelScroller.getVerticalScrollBar();
addLabelTextPairs(new Object[] {
"Label", labelScroller,
//"Content", contentValue,
},
this);
//setPreferredSize(new Dimension(Short.MAX_VALUE,100));
//setMinimumSize(new Dimension(200,90));
//setMinimumSize(new Dimension(200,63));
//setMaximumSize(new Dimension(Short.MAX_VALUE,63));
// Workaround: if we don't do this, sometimes the top-pixel of our Widget
// title gets clipped when the InspectorPane content grows large enough
// to activate the scroll-bar
setMinimumSize(new Dimension(200,80));
setPreferredSize(new Dimension(200,80));
}
// public Dimension getMinimumSize() {
// return new Dimension(200,80);
// }
// public Dimension getPreferredSize() {
// return new Dimension(200,80);
// }
public void run() {
labelScrollBar.setValue(0);
labelScrollBar.setValueIsAdjusting(false);
}
void load(LWComponent c) {
setTypeName(this, c, "Information");
if (c instanceof LWText)
labelValue.setEditable(false);
else
labelValue.setEditable(true);
labelScrollBar.setValueIsAdjusting(true);
labelValue.attachProperty(c, LWKey.Label);
/*
if (c.hasResource()) {
contentValue.setText(c.getResource().toString());
} else {
contentValue.setText("");
}
*/
GUI.invokeAfterAWT(this);
//out("ROWS " + labelValue.getRows() + " border=" + labelValue.getBorder());
}
}
/**
*
* 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.
*
*/
//----------------------------------------------------------------------------------------
// 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 = 2;
private final int botPad = 2;
private final Insets labelInsets = new Insets(topPad, 0, botPad, GUI.LabelGapRight);
private final Insets fieldInsets = new Insets(topPad, 0, botPad, GUI.FieldGapRight);
/** 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;
final JLabel label = labels[i];
final 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;
JComponent defaultExpander = new JPanel();
defaultExpander.setPreferredSize(new Dimension(Short.MAX_VALUE, 1));
if (DEBUG.BOXES) {
defaultExpander.setOpaque(true);
defaultExpander.setBackground(Color.red);
} else
defaultExpander.setOpaque(false);
gridBag.add(defaultExpander, c);
}
private void loadText(JTextComponent c, String text) {
String hasText = c.getText();
// This prevents flashing where fields of
// length greater the the visible area do
// a flash-scroll when setting the text, even
// if it's the same as what's there.
if (hasText != text && !hasText.equals(text))
c.setText(text);
}
private void loadText(JLabel c, String text) {
String hasText = c.getText();
// This prevents flashing where fields of
// length greater the the visible area do
// a flash-scroll when setting the text, even
// if it's the same as what's there.
if (hasText != text && !hasText.equals(text))
c.setText(text);
}
private void out(Object o) {
Log.debug(o==null?"null":o.toString());
}
public static void displayTestPane(String rsrc)
{
// //MapResource r = new MapResource("file:///System/Library/Frameworks/JavaVM.framework/Versions/1.4.2/Home");
// if (rsrc == null)
// rsrc = "file:///VUE/src/tufts/vue/images/splash_graphic_1.0.gif";
// InspectorPane p = new InspectorPane();
// LWComponent node = new LWNode("Test Node");
// node.setNotes("I am a note.");
// System.out.println("Loading resource[" + rsrc + "]");
// Resource r = Resource.getFactory().get(rsrc);
// System.out.println("Got resource " + r);
// r.setTitle("A Very Long Long Resource Title Ya Say");
// node.setResource(r);
// for (int i = 1; i < 6; i++)
// r.setProperty("field_" + i, "value_" + i);
// DockWindow w = null;
// if (false) {
// //ToolWindow w = VUE.createToolWindow("LWCInfoPanel", p);
// javax.swing.JScrollPane sp = new javax.swing.JScrollPane(p,
// JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
// //JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
// JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
// );
// w = GUI.createDockWindow("Inspector", sp);
// }
// else
// {
// w = GUI.createDockWindow(p);
// //w = GUI.createDockWindow("Resource Inspector", p.mLabelPane);
// //tufts.Util.displayComponent(p);
// }
// if (w != null) {
// w.setUpperRightCorner(GUI.GScreenWidth, GUI.GInsets.top);
// w.setVisible(true);
// }
// VUE.getSelection().setTo(node); // setLWComponent does diddly -- need this
}
public static void main(String args[]) {
// VUE.init(args);
// // Must have at least ONE active frame for our focus manager to work
// //new Frame("An Active Frame").setVisible(true);
// String rsrc = null;
// if (args.length > 0 && args[0].charAt(0) != '-')
// rsrc = args[0];
// Resource r = Resource.getFactory().get("file:///VUE/src/tufts/vue/images/splash_graphic_1.0.gif");
// if (true) {
// displayTestPane(rsrc);
// } else {
// InspectorPane ip = new InspectorPane();
// VUE.getResourceSelection().setTo(r, "main::test");
// Widget.setExpanded(ip.mResourceMetaData, true);
// GUI.createDockWindow("Test Properties", ip).setVisible(true);
// }
}
public void searchPerformed(SearchEvent evt) {
// if ((VUE.getSelection().size() > 0) && (VUE.getResourceSelection().get() == null))
// return;
if (VUE.getSelection().size() > 0 && VUE.getActiveResource() == null)
return;
showNodePanes(false);
showResourcePanes(false);
LWSelection selection = VUE.getSelection();
LWComponent c = selection.first();
if (c != null) {
if (c.hasResource()) {
loadResource(c.getResource(), null);
showNodePanes(true);
showResourcePanes(true);
}
else
showNodePanes(true);
}
}
}