/*
* 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.DEBUG;
import tufts.vue.Actions;
import tufts.vue.DataSourceViewer;
import tufts.vue.FavoritesDataSource;
import tufts.vue.FavoritesWindow;
import tufts.vue.LWComponent;
import tufts.vue.LWContainer;
import tufts.vue.LWImage;
import tufts.vue.LWLink;
import tufts.vue.LWNode;
import tufts.vue.LWPathway;
import tufts.vue.LWSelection;
import tufts.vue.LWSlide;
import tufts.vue.NodeTool;
import tufts.vue.PathwayTableModel;
import tufts.vue.Resource;
import tufts.vue.VUE;
import tufts.vue.VueResources;
import tufts.vue.gui.GUI;
import tufts.vue.gui.WidgetStack;
import tufts.vue.gui.WindowDisplayAction;
import java.util.*;
import java.awt.*;
import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.metadata.VueMetadataElement;
/**
* A list of Resource's with their icons & title's that is selectable, draggable & double-clickable
* for resource actions. Also uses a "masking" data-model that can abbreviate the results
* until a synthetic model item at the end of this shortened list is selected, at which
* time the rest of the items are "unmaksed" and displayed.
*
* @version $Revision: 1.30 $ / $Date: 2010-02-03 19:16:31 $ / $Author: mike $
*/
public class ResourceList extends JList
implements DragGestureListener, /*tufts.vue.ResourceSelection.Listener,*/ MouseListener,ActionListener
{
public static final Color DividerColor = VueResources.getColor("ui.resourceList.dividerColor", 204,204,204);
public static final boolean ALL_DATA = true; // use all data while comparing similarity between two LW Components. All includes notes and metadata
private static ImageIcon DragIcon = tufts.vue.VueResources.getImageIcon("favorites.leafIcon");
private static int PreviewItems = 4;
private static int PreviewModelSize = PreviewItems + 1;
private static int LeftInset = 2;
private static int IconSize = 32;
private static int IconTextGap = new JLabel().getIconTextGap();
private static int RowHeight = IconSize + 5;
private DefaultListModel mDataModel;
private boolean isMaskingModel = false; // are we using a masking model?
private boolean isMasking = false; // if using a masking model, is it currently masking most entries?
private final String mName;
private static class MsgLabel extends JLabel {
MsgLabel(String txt) {
super(txt);
setFont(GUI.TitleFace);
setForeground(WidgetStack.BottomGradient);
setPreferredSize(new Dimension(getWidth(), RowHeight / 2));
int leftInset = LeftInset + IconSize + IconTextGap;
setBorder(new CompoundBorder(new MatteBorder(0,0,1,0, DividerColor),
new EmptyBorder(0,leftInset,0,0)));
}
}
private JLabel mMoreLabel = new MsgLabel("?"); // failsafe
private JLabel mLessLabel = new MsgLabel(String.format(Locale.getDefault(), VueResources.getString("resourcelist.showless"), PreviewItems));
/**
* A model that can intially "mask" out all but a set of initial
* items to preview the contents of the list, and provide a
* special list item at the end of the preview range, that when
* selected, unmasks the rest of the items in the model.
*/
private class MaskingModel extends javax.swing.DefaultListModel
{
public int getSize() {
return isMasking ? Math.min(PreviewModelSize, size()) : size() + 1;
}
public Object getElementAt(int index) {
if (isMasking) {
if (index == PreviewItems) {
return mMoreLabel;
} else if (index > PreviewItems)
return "MASKED INDEX " + index; // should never see this
} else if (index == size()) {
return mLessLabel;
}
return super.getElementAt(index);
}
private void expand() {
isMasking = false;
//fireIntervalAdded(this, PreviewItems, size() - 1);
fireContentsChanged(this, PreviewItems, size());
}
private void collapse() {
isMasking = true;
fireIntervalRemoved(this, PreviewItems, size());
}
}
public ResourceList(Collection resourceBag)
{
this(resourceBag, null);
}
public ResourceList(Collection resourceBag, String name)
{
setName(name);
mName = name;
setFixedCellHeight(RowHeight);
setCellRenderer(new RowRenderer());
addMouseListener(this);
// Set up the data-model
//final javax.swing.DefaultListModel model;
if (resourceBag.size() > PreviewModelSize) {
mDataModel = new MaskingModel();
isMaskingModel = true;
mMoreLabel = new MsgLabel(String.format(Locale.getDefault(), VueResources.getString("resourcelist.showmore"), (resourceBag.size() - PreviewItems)));
isMasking = true;
} else
mDataModel = new javax.swing.DefaultListModel();
// can easily change this to faster ArrayList v.s. vector by subclassing AbstractListModel
// We don't need synchronized as list only in use one at a time, by the awt.
Iterator i = resourceBag.iterator();
while (i.hasNext())
mDataModel.addElement(i.next());
setModel(mDataModel);
// Set up the selection-model
DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setSelectionModel(selectionModel);
selectionModel.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
if (DEBUG.RESOURCE || DEBUG.SELECTION || DEBUG.FOCUS) {
System.out.println(ResourceList.this + " valueChanged: " + e
+ " index=" + getSelectedIndex()
+ " picked=" + getPicked());
// TODO: Leopard java 1.5 2008-05-28 SMF: a tiny drag on a
// resource leading to an aborted drag leaves us in a state such
// that all the events coming in as a result of up/down arrow
// key movement (which do move the selection), claim
// valueIsAdjusting! So the arrows break after doing this,
// until the mouse click is used to change the resource
// selection, which resets us. This is looking like a java bug.
if (e.getValueIsAdjusting())
System.out.println(ResourceList.this + " isAdjusting: ignoring");
}
if (e.getValueIsAdjusting())
return;
if (DEBUG.SELECTION) tufts.Util.printStackTrace("ResourceList valueChanged " + ResourceList.this);
if (isMaskingModel) {
if (isMasking && getSelectedIndex() >= PreviewItems)
((MaskingModel)mDataModel).expand();
else if (!isMasking && getSelectedIndex() == mDataModel.size()) {
// For now: do nothing: force them to click on the collapse
// (keyboarding to it creates a user-loop when holding the
// down arrow, where it collpases, goes back to the top, then
// selects till it expands, then selects down to end and collapses
// again...
//((MaskingModel)model).collapse();
// "selected" item doesn't exist at this point, so nothing is "picked"
// this will follow with a second selection event, which will
// set the resource selection to null, as nothing is picked by default.
return;
}
}
if (getPicked() != null)
tufts.vue.VUE.setActive(Resource.class, ResourceList.this, getPicked());
//tufts.vue.VUE.getResourceSelection().setTo(getPicked(), ResourceList.this);
}
});
setDragEnabled(false);
// Set up double-click handler for displaying content
addMouseListener(new tufts.vue.MouseAdapter("resourceList") {
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getClickCount() == 2) {
Resource r = getPicked();
if (r != null)
r.displayContent();
} else if (e.getClickCount() == 1) {
if (isMaskingModel && getSelectedValue() == mLessLabel)
((MaskingModel)mDataModel).collapse();
}
}
});
// attempt mouseDragged tracking ourselves...
addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
public void mouseDragged(java.awt.event.MouseEvent me) {
Resource picked = getPicked();
if (picked != null) {
Image image = null;
Object o = getSelectedValue();
if (getSelectedValue() instanceof ResourceIcon)
image = ((ResourceIcon)o).getImage(); // todo: more generic on Resource class
// TODO: If Resource's were uniquely atomic via a Factory and real ID's,
// we could maybe make the ResourceIcon simpler, and have the Resource itself
// cache the thumbnail image (and we could thusly also request it here, instead of
// from the ResourceIcon)
GUI.startSystemDrag(ResourceList.this, me, image, new GUI.ResourceTransfer(picked));
}
}
});
tufts.vue.VUE.addActiveListener(Resource.class, this);
//tufts.vue.VUE.getResourceSelection().addListener(this);
// Set up the drag handler
/*
DragSource dragSource = DragSource.getDefaultDragSource();
dragSource.createDefaultDragGestureRecognizer(this, // Component
DnDConstants.ACTION_COPY |
DnDConstants.ACTION_MOVE |
DnDConstants.ACTION_LINK,
this); // DragGestureListener
*/
}
@Override
public void removeNotify() {
//tufts.Util.printStackTrace("removeNotify " + this);
tufts.vue.VUE.removeActiveListener(Resource.class, this);
//tufts.vue.VUE.getResourceSelection().removeListener(this);
}
public void activeChanged(final tufts.vue.ActiveEvent e, final tufts.vue.Resource resource) {
if (getPicked() == resource) {
; // do nothing; already selected
} else {
// TODO: if list contains selected item, select it!
clearSelection();
}
}
private Resource getPicked() {
Object o = getSelectedValue();
if (o instanceof Resource)
return (Resource) o;
else if (o instanceof ResourceIcon)
return ((ResourceIcon)o).getResource();
else
return null;
//return (Resource) getSelectedValue();
}
public void dragGestureRecognized(DragGestureEvent e)
{
if (getSelectedIndex() != -1) {
Resource r = getPicked();
if (DEBUG.DND || DEBUG.SELECTION) System.out.println("ResourceList: startDrag: " + r);
e.startDrag(DragSource.DefaultCopyDrop, // cursor
DragIcon.getImage(),
new Point(-10,-10), // drag image offset
new tufts.vue.gui.GUI.ResourceTransfer(r),
new tufts.vue.gui.GUI.DragSourceAdapter());
}
}
public String toString() {
String tag;
if (mName == null)
tag = "";
else
tag = "[" + mName + "]";
return "ResourceList@" + Integer.toHexString(hashCode()) + tag;
}
private class RowRenderer extends DefaultListCellRenderer
{
RowRenderer() {
//setOpaque(false); // selection stops working!
//setFont(ResourceList.this.getFont()); // leave default label font
// Border: 1 pix gray at bottom, then LeftInset in from left
setBorder(new CompoundBorder(new MatteBorder(0,0,1,0, DividerColor),
new EmptyBorder(0,LeftInset,0,0)));
setAlignmentY(0.5f);
}
public Component getListCellRendererComponent(
JList list,
Object value, // value to display
int index, // cell index
boolean isSelected, // is the cell selected
boolean cellHasFocus) // the list and the cell have the focus
{
if (value == mMoreLabel)
return mMoreLabel;
else if (value == mLessLabel)
return mLessLabel;
ResourceIcon icon;
Resource r;
// The model starts as a list of Resources, but if asked to render
// we replace it with a ResourceIcon, with painter set to this JList.
// (We can still get the Resource later from the ResourceIcon)
if (value instanceof Resource) {
r = (Resource) value;
icon = new ResourceIcon(r, IconSize, IconSize, list);
mDataModel.set(index, icon); // ideally, wouldn't want to trigger a model change tho...
} else {
icon = (ResourceIcon) value;
r = icon.getResource();
}
setIcon(icon);
if (false)
setText("<HTML>" + r.getTitle());
else
setText(r.getTitle());
/* if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}*/
Color bg = null;
if (isSelected) {
bg = GUI.getTextHighlightColor();
} else {
bg = list.getBackground();
}
setBackground(bg);
//setEnabled(list.isEnabled());
return this;
}
}
private void displayContextMenu(MouseEvent e) {
getPopup(e).show(e.getComponent(), e.getX(), e.getY());
}
private JPopupMenu m = null;
private final JMenuItem launchResource = new JMenuItem(VueResources.getString("menu.popup.resource.launchresource"));
private final WindowDisplayAction infoAction = new WindowDisplayAction(VUE.getInfoDock());
private final JCheckBoxMenuItem infoCheckBox = new JCheckBoxMenuItem(infoAction);
private final JMenuItem addToMap = new JMenuItem(VueResources.getString("menu.popup.resource.addtomap"));
private final JMenuItem addToNode = new JMenuItem(VueResources.getString("menu.popup.resource.addtoselectednode"));
private final JMenuItem addToSlide = new JMenuItem(VueResources.getString("menu.popup.resource.addtoslide"));
private final JMenuItem addToSavedContent = new JMenuItem(VueResources.getString("menu.popup.resource.addtomysavedcontent"));
private JPopupMenu getPopup(MouseEvent e)
{
if (m == null)
{
m = new JPopupMenu(VueResources.getString("menu.popup.resource"));
infoCheckBox.setLabel(VueResources.getString("menu.popup.resource.resourceinfo"));
if (VUE.getInfoDock().isShowing())
infoCheckBox.setSelected(true);
m.add(infoCheckBox);
m.addSeparator();
m.add(launchResource);
m.addSeparator();
m.add(addToMap);
m.add(addToNode);
m.add(addToSlide);
m.add(addToSavedContent);
launchResource.addActionListener(this);
addToMap.addActionListener(this);
addToNode.addActionListener(this);
addToSlide.addActionListener(this);
addToSavedContent.addActionListener(this);
}
LWSelection sel = VUE.getActiveViewer().getSelection();
LWComponent c = sel.only();
if (c != null && c instanceof LWNode)
{
addToNode.setEnabled(true);
addToSlide.setEnabled(false);
if (c.hasResource())
addToNode.setLabel(VueResources.getString("menu.popup.resource.replaceresourceonnode"));
}
else if (c != null && c instanceof LWSlide)
{
addToNode.setEnabled(false);
addToSlide.setEnabled(true);
if (c.hasResource())
addToNode.setLabel(VueResources.getString("menu.popup.resource.addtoselectednode"));
}
else
{
addToNode.setEnabled(false);
addToSlide.setEnabled(false);
if (c != null && c.hasResource())
addToNode.setLabel(VueResources.getString("menu.popup.resource.addtoselectednode"));
}
return m;
}
Point lastMouseClick = null;
public void mouseClicked(MouseEvent arg0) {
}
private final static int MAX_LABEL_LINE_LENGTH=30;
public void actionPerformed(ActionEvent e)
{
if (e.getSource().equals(launchResource))
{
int index = this.locationToIndex(lastMouseClick);
ResourceIcon o = (ResourceIcon)this.getModel().getElementAt(index);
o.getResource().displayContent();
//this.get
//System.out.println(o.toString());
} else if (e.getSource().equals(addToMap))
{
int index = this.locationToIndex(lastMouseClick);
ResourceIcon o = (ResourceIcon)this.getModel().getElementAt(index);
String label = o.getResource().getName();
label = Util.formatLines(label, MAX_LABEL_LINE_LENGTH);
LWNode end = NodeTool.NodeModeTool.createNewNode(label);
end.setResource(o.getResource());
VUE.getActiveMap().addNode(end);
if (true)
tufts.vue.VueUtil.setXYByClustering(end);
else
setXYByClustering(end);
} else if (e.getSource().equals(addToSlide))
{
int index = this.locationToIndex(lastMouseClick);
ResourceIcon o = (ResourceIcon)this.getModel().getElementAt(index);
LWSelection sel = VUE.getActiveViewer().getSelection();
LWSlide c = (LWSlide)sel.only();
LWImage end = new LWImage();
end.setResource(o.getResource());
end.setStyle(c);
end.setResource(o.getResource());
end.setLabel(o.getResource().getName());
c.addChild(end);
} else if (e.getSource().equals(addToNode))
{
int index = this.locationToIndex(lastMouseClick);
ResourceIcon o = (ResourceIcon)this.getModel().getElementAt(index);
LWSelection sel = VUE.getActiveViewer().getSelection();
LWComponent c = sel.only();
VUE.setActive(LWComponent.class, this, null);
c.setResource(o.getResource());
c.setLabel(o.getResource().getName());
VUE.setActive(LWComponent.class, this, c);
} else if (e.getSource().equals(addToSavedContent))
{
int index = this.locationToIndex(lastMouseClick);
ResourceIcon o = (ResourceIcon)this.getModel().getElementAt(index);
FavoritesDataSource repository = DataSourceViewer.getDefualtFavoritesDS();
((FavoritesWindow)repository.getResourceViewer()).getFavoritesTree().addResource(o.getResource());
}
}
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
{
lastMouseClick = e.getPoint();
displayContextMenu(e);
}
}
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
{
lastMouseClick = e.getPoint();
displayContextMenu(e);
}
}
public static void setXYByClustering(LWNode node) {
Iterator<LWComponent> i = VUE.getActiveMap().getAllDescendents(
LWContainer.ChildKind.PROPER).iterator();
float xNumerator = 0 ;
float yNumerator = 0 ;
float denominator = 0 ;
while (i.hasNext()) {
LWComponent c = i.next();
if (c instanceof LWNode) {
LWNode mapNode = (LWNode)c;
if(mapNode != node) {
double score = computeScore(node,mapNode);
xNumerator += score*score*mapNode.getX();
yNumerator += score*score*mapNode.getY();
denominator += score*score;
}
}
}
if(denominator != 0) {
float x = xNumerator/denominator;
float y = yNumerator/denominator;
node.setX(x);
node.setY(y);
}
i = VUE.getActiveMap().getAllDescendents(
LWContainer.ChildKind.PROPER).iterator();
while (i.hasNext()) {
LWComponent c = i.next();
if (c instanceof LWNode) {
LWNode mapNode = (LWNode)c;
if(checkCollision(mapNode,node)) {
Actions.projectNodes(node, 24, Actions.PUSH_ALL );
}
}
}
}
public static double computeScore (LWNode n1,LWNode n2) {
double score = 0.0;
String content1 = n1.getLabel();
String content2 = n1.getLabel();
if(ALL_DATA) {
content1 += " "+n1.getNotes();
content2 += " "+n2.getNotes();
if(n1.getResource()!= null) content1 += " "+n1.getResource().getSpec();
if(n2.getResource()!= null) content2 += " "+n2.getResource().getSpec();
MetadataList mList1 = n1.getMetadataList();
for(VueMetadataElement vme: mList1.getMetadata()){
content1 +=" "+vme.getKey();
content1 +=" "+vme.getValue();
}
MetadataList mList2 = n2.getMetadataList();
for(VueMetadataElement vme: mList2.getMetadata()){
content2 +=" "+vme.getKey();
content2 +=" "+vme.getValue();
}
}
String[] words1 = content1.split("\\s+");
String[] words2 = content2.split("\\s+");
int matches = 0;
for(int i = 0;i<words1.length;i++) {
if(n2.getLabel().contains(words1[i])){
matches++;
}
}
double p1 = (double) matches / words1.length;
double p2 = (double) matches/words2.length;
if(p1== 0 && p2 == 0 ){
score = 0.0;
} else {
score = 2*p1*p2/(p1+p2); // harmonic mean
}
return score;
}
public static boolean checkCollision(LWComponent c1, LWComponent c2) {
boolean collide = false;
if(c2.getX()>= c1.getX() && c2.getX() <= c1.getX()+c1.getWidth() && c2.getY() >= c1.getY() && c2.getY() <=c1.getY()+c2.getHeight()) {
collide = true;
}
return collide;
}
}