/* JstringListBox.java An implementation of JListBox used to display strings. Created: 21 Aug 1997 Module By: Mike Mulvaney ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2013 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.JDataComponent; import java.awt.Component; import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Vector; import java.util.Comparator; import javax.swing.DefaultListModel; import javax.swing.DefaultListSelectionModel; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.ListModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import arlut.csd.Util.VecQuickSort; /*------------------------------------------------------------------------------ class JstringListBox ------------------------------------------------------------------------------*/ /** * <p>A sorted listbox that handles {@link * arlut.csd.JDataComponent.listHandle listHandle}'s. JstringListBox * supports pop-up menus and uses the {@link * arlut.csd.JDataComponent.JsetValueCallback JsetValueCallback} * interface to report selection and pop-up menu activity to the * registered callback.</p> * * <p>listHandles are wrappers that can hold both a String and * (optionally) a related object, such as an Invid. The * JstringListBox uses them to allow the client to manipulate labeled * object pointers.</p> * * <p>The {@link arlut.csd.JDataComponent.StringSelector * StringSelector} class uses JstringListBoxes to support adding or * removing Strings and Objects from String and Invid vector * fields.</p> * * @see arlut.csd.ganymede.common.Invid * @see arlut.csd.JDataComponent.listHandle * @see arlut.csd.JDataComponent.StringSelector * @see arlut.csd.JDataComponent.JsetValueCallback * @author Mike Mulvaney */ public class JstringListBox extends JList implements ActionListener, FocusListener, ListSelectionListener, MouseListener, MouseMotionListener, Comparator { static final boolean debug = false; // --- int width, popUpIndex = -1; DefaultListModel model = new DefaultListModel(); /** * <p>If true, this JstringListBox will allow nodes to be dragged up * and down in the list.</p> */ private boolean dragOk = false; /** * <p>If true, the JstringListBox will sort items. This variable is * set by the value of the sort parameter in the most recent {@link * arlut.csd.JDataComponent.JstringListBox#load(java.util.Vector * items, int width, boolean sort, java.util.Comparator comparator) * load()} call.</p> */ private boolean doSort = false; /** * <p>The callback we'l use to report user activities.</p> */ JsetValueCallback callback; /** * <p>The popup menu to be displayed on right-click.</p> */ JPopupMenu popup = null; private int startDragIndex = -1, dragNode = -1; /** * <p>The comparator to use for putting items in sort order if the * JstringListBox was most recently with sorting request.</p> */ Comparator comparator; /** * <p>The default maximum width string</p> */ String maxWidthString = "this is the minimum!"; /* -- */ /** * Default Constructor */ public JstringListBox() { addMouseListener(this); addMouseMotionListener(this); setSelectionMode(DefaultListSelectionModel.MULTIPLE_INTERVAL_SELECTION); getSelectionModel().addListSelectionListener(this); } /** * <p>This method associates a node-linked popup menu to this * listbox.</p> * * @param popup A menu to be shown when an item is right-clicked */ public void registerPopupMenu(JPopupMenu popup) { this.popup = popup; if (popup == null) { return; } Component[] c = popup.getComponents(); for (int i = 0; i < c.length; i++) { if (c[i] instanceof JMenuItem) { JMenuItem pm = (JMenuItem)c[i]; pm.addActionListener(this); } else { throw new IllegalArgumentException("Hey, you are supposed to use JMenuItems in JPopupMenus, buddy."); } } } /** * <p>This method loads a Vector of items into this list box. * Elements of the items Vector may be Strings or {@link * arlut.csd.JDataComponent.listHandle listHandle} objects.</p> * * <p>If sort is true, the items will be sorted in display order * before being loaded into this list box.</p> * * <p>Any values previously in the list box will be removed.</p> * * @param items A Vector of Strings or listHandles * @param width If less than zero, the listbox's cell width will * be left unchanged. If set to zero, the listbox's cell width will * be calculated based on the longest string in the items Vector. If * greater than zero, the cell width will be set to <width> columns * wide. * @param sort If true, the items Vector will be sorted in place before * being set into the listbox. * @param comparator Typically an instance of an inner class that implements * the Comparator interface, used to guide the sort process. If this * is null, the sort will be performed using a normal string ordering sort. */ public synchronized void load(Vector items, int width, boolean sort, Comparator comparator) { this.maxWidthString = "this is the minimum!"; model = new DefaultListModel(); try { doSort = sort; if (comparator == null) { this.comparator = this; } else { this.comparator = comparator; } if (items == null || items.size() == 0) { return; } if (items.elementAt(0) instanceof listHandle) { if (doSort) { new VecQuickSort(items, comparator).sort(); } for (int i = 0; i < items.size(); i++) { listHandle handle = (listHandle) items.elementAt(i); insertHandleAt(handle, i); if (handle.toString().length() > maxWidthString.length()) { maxWidthString = handle.toString(); } } } else // It must be a String, or it will throw a // ClassCastException { Vector convertedVect = new Vector(items.size()); for (int i = 0; i < items.size(); i++) { String s = (String) items.elementAt(i); if (s.length() > maxWidthString.length()) { maxWidthString = s; } convertedVect.addElement(new listHandle(s,s)); } if (doSort) { new VecQuickSort(convertedVect, comparator).sort(); } for (int i = 0; i < convertedVect.size(); i++) { listHandle handle = (listHandle) convertedVect.elementAt(i); insertHandleAt(handle, i); } } } finally { setModel(model); repaint(); } } /** * This method is used to change the dynamically label of an object in this * StringSelector. * * @param object The object to be relabeled * @param newLabel The new label to be applied to object */ public synchronized void relabelObject(Object object, String newLabel) { // only bother if we have _objects_ ListModel model = getModel(); if ((model.getSize() == 0) || !(model.getElementAt(0) instanceof listHandle)) { return; } for (int i = 0; i < model.getSize(); i++) { listHandle handle = (listHandle) model.getElementAt(i); if (handle.getObject() != null && handle.getObject().equals(object)) { handle.setLabel(newLabel); break; } } if (doSort) { resort(); } repaint(); } /** * This method resorts the elements in this JstringListBox. */ public void resort() { ListModel model = getModel(); if (model.getSize() < 2) { return; } Vector sortedList = new Vector(); for (int i = 0; i < model.getSize(); i++) { sortedList.addElement(model.getElementAt(i)); } if (this.comparator == null) { this.comparator = this; } new VecQuickSort(sortedList, this.comparator).sort(); setListData(sortedList); } /** * <p>This method sets the default cell width for this list.</p> * * @param width The width to be set in pixels for items in this list. */ public void setCellWidth(int width) { this.width = width; if (width > 0) { if (debug) { System.err.println("JstringListBox: setting fixed cell width to " + width); } setFixedCellWidth(width); } else if (width == 0) { setPrototypeCellValue(maxWidthString); } } /** * <p>This method sets the default cell width for this list.</p> * * @param template This String will be rendered to calculate the * default cell width for this list. */ public void setCellWidth(String template) { setPrototypeCellValue(template); } /** * <p>This method enables and disables item dragging in this list.</p> * * @param dragOk If true, items can be dragged up and down in this * list. */ public void setDragEnabled(boolean dragOk) { this.dragOk = dragOk; if (!dragOk) { dragNode = -1; startDragIndex = -1; } } /** * Convenience method to set the size on the model. * * @param size New size for hte model. */ public void setSize(int size) { model.setSize(size); } /** * <p>Connects this JstringListBox to an implementaton of the {@link * arlut.csd.JDataComponent.JsetValueCallback JsetValueCallback} * interface in order to provide live notification of changes * performed by the user. The JsetValueCallback implementation is * given the opportunity to approve any change made by the user * before the GUI is updated to show the change. The * JsetValueCallback interface is also used to pass pop-up menu * commands to the client.</p> * * <p>JstringListBox uses the following subclasses of {@link * arlut.csd.JDataComponent.JValueObject JValueObject} to pass * status updates to the callback. * * <ul> * <li>{@link arlut.csd.JDataComponent.JParameterValueObject} Action * from a PopupMenu. The Parameter is the ActionCommand string for * the pop-up menu item selected, and the value is the object (or * string if no object defined) associated with the item selected * when the pop-up menu was fired.</li> * <li>{@link arlut.csd.JDataComponent.JAddValueObject} Object has * been selected. Value is the object (or string) added.</li> * <li>{@link arlut.csd.JDataComponent.JInsertValueObject} Object * has been double-clicked. Value is the object (or string) * double-clicked.</li> * <li>{@link arlut.csd.JDataComponent.JMoveValueObject} Object has * been dragged up or down. Index holds the index the object has * been moved to.</li> * <li>{@link arlut.csd.JDataComponent.JErrorValueObject} Something * went wrong. Value is the error message to be displayed to the * user in whatever fashion is appropriate.</li> * </ul> * * @param callback The callback to be called by when the user * manipulates this JstringListBox. * * @see JsetValueCallback * @see JValueObject */ public void setCallback(JsetValueCallback callback) { if (debug) { System.out.println("Setting callback in JstringListBox"); } this.callback = callback; } /** * Add an item to the list box. * * @param o Can be a String or listHandle. */ public void addItem(Object o) { if (debug) { System.err.println("JstringListBox.addItem(" + String.valueOf(o) + ")"); } listHandle lh = null; if (o instanceof String) { lh = new listHandle((String)o, (String)o); } else // we'll throw a ClassCastException if we need to { lh = (listHandle)o; } if (doSort) { int i = 0; int total = model.getSize(); // find the insertion point while ((i < total) && (comparator.compare(lh, model.getElementAt(i))>0)) { i++; } insertHandleAt(lh, i); addSelectionInterval(i, i); if (debug) { System.err.println("Adding sorted"); } ensureIndexIsVisible(i); } else { int topIndex = model.getSize(); model.addElement(lh); addSelectionInterval(topIndex, topIndex); if (debug) { System.err.println("Adding non-sorted"); } ensureIndexIsVisible(topIndex); } invalidate(); getParent().validate(); } /** * <p>This method moves an item up or down in the list.</p> * * @param sourceRow The row to move * @param targetRow The index to place sourceRow at after the move. */ public void moveItem(int sourceRow, int targetRow) { if (sourceRow == targetRow) { return; } listHandle h = (listHandle) model.remove(sourceRow); if (h == null) { return; } model.insertElementAt(h, targetRow); ensureIndexIsVisible(targetRow); invalidate(); getParent().validate(); } /** * <p>Use this one to skip the sorting. Called by all the add * methods.</p> * * @param handle The item to place in the list. * @param row The position to place handle in the list. */ public void insertHandleAt(listHandle handle, int row) { model.insertElementAt(handle, row); } /** * <p>Remove an item from list.</p> * * @param o can be listHandle or String */ public void removeItem(Object o) { if (o instanceof listHandle) { model.removeElement((listHandle)o); } else if (o instanceof String) { if (debug) { System.out.println("I am guessing you want me to remove a label..."); } removeLabel((String) o); } else { if (debug) { System.out.println("Ok, i will look for this object in the listHnaldes."); } for (int i = 0; i < model.getSize(); i++) { if ((((listHandle)model.elementAt(i)).getObject()).equals(o)) { model.removeElementAt(i); break; } } } repaint(); } /** * <p>Remove an object by label</p> * * @param s Label of object to remove. */ public void removeLabel(String s) { for (int i = 0; i < model.getSize(); i++) { if ((((listHandle)model.elementAt(i)).getLabel()).equals(s)) { model.removeElementAt(i); break; //Assume there is only one? } } } public int getSizeOfList() { return model.getSize(); } /** * @param string The label to search for. * @return True if the list contains an object with the specified * label. */ public boolean containsLabel(String string) { return containsString(string); } /** * <p>Since everything is a listHandle internally, this is the same * as containsLabel</p> * * @param string The label to search for. * @return True if the list contains an object with the specified * label. */ public boolean containsString(String string) { for (int i = 0; i < model.getSize(); i++) { if ((((listHandle)model.elementAt(i)).getLabel()).equals(string)) { return true; } } return false; } /** * @param o Can be a String(label) or listHandle * @return True if the item is in the list */ public boolean containsItem(Object o) { if (o instanceof String) { return containsLabel((String)o); } else if (o instanceof listHandle) { return model.contains(o); } else { for (int i = 0; i < model.getSize(); i++) { if (((listHandle)model.elementAt(i)).getObject().equals(o)) { return true; } } } return false; } /** * <p>This selects the item with the given label.</p> * * @param s The label to select. */ public void setSelectedLabel(String s) { this.setSelectedLabel(s, false); } /** * <p>This selects the item with the given label.</p> * * @param s The label to select. * @param ensureVisible If true, the list will be scrolled to show * the objet with the given label. */ public void setSelectedLabel(String s, boolean ensureVisible) { int size = model.getSize(); listHandle lh = null; for (int i = 0; i < size; i++) { lh = (listHandle)model.getElementAt(i); if (lh.getLabel().equals(s)) { setSelected(lh); if (ensureVisible) { ensureIndexIsVisible(i); } break; } } } /** * <p>Sets the selected item.</p> * * @param o Can be listHandle or String */ public void setSelected(Object o) { if (o instanceof listHandle) { setSelectedValue(o, true); // use the JList.setSelectedValue(Object, boolean shouldScroll) } else // we'll throw ClassCastException if we need to { setSelectedLabel((String)o); } } /** * @return The label of the selected item, if any. */ public String getSelectedLabel() { return ((listHandle)model.elementAt(getSelectedIndex())).getLabel(); } /** * @return The listHandle of the selected item, if any. */ public listHandle getSelectedHandle() { return (listHandle)model.elementAt(getSelectedIndex()); } /** * Returns all the selected handles. */ @SuppressWarnings("deprecation") public Vector getSelectedHandles() { Vector v = new Vector(); Object[] values = getSelectedValues(); for (int i =0; i < values.length; i++) { v.addElement(values[i]); } return v; } /** * Returns all handles */ public Vector getHandles() { Vector v = new Vector(); for (int i =0; i < getModel().getSize(); i++) { v.addElement(getModel().getElementAt(i)); } return v; } /** * @return The object selected in the list, without the label. */ public Object getSelectedItem() { try { listHandle lh = (listHandle) model.elementAt(getSelectedIndex()); return lh.getObject(); } catch (Exception e) { return null; } } public void valueChanged(ListSelectionEvent e) { int selectedIndex = getSelectedIndex(); if (selectedIndex == -1 || dragNode != -1) { return; // don't notify our container on deselect } if (callback != null) { boolean ok = false; try { ok = callback.setValuePerformed(new JAddValueObject(this, selectedIndex)); } catch (java.rmi.RemoteException rx) { throw new RuntimeException("Could not setValuePerformed: " + rx); } if (ok) { //do something } else { //put it back } } } public void mouseClicked(MouseEvent e) { if (callback != null) { if (SwingUtilities.isLeftMouseButton(e)) { // we only want to respond to a double-click. mouseDown with no modifier // will be treated as a drag initiation if (e.getClickCount() == 2) { boolean ok = false; int index = locationToIndex(e.getPoint()); if (debug) { System.out.println("Double clicked on Item " + index); } try { ok = callback.setValuePerformed(new JInsertValueObject(this, index)); } catch (java.rmi.RemoteException rx) { throw new RuntimeException("Double click produced: " + rx); } if (debug) { System.out.println("setValue from JstringListBox=" + ok); } } } else if (SwingUtilities.isRightMouseButton(e)) { if (debug) { System.out.println("Its a popup trigger!"); } popUpIndex = locationToIndex(e.getPoint()); boolean found = false; try { if (model.elementAt(popUpIndex) != null) { found = true; } } catch (ArrayIndexOutOfBoundsException ex) { } if (found) { clearSelection(); setSelectedIndex(popUpIndex); if (popup != null) { popup.setVisible(false); popup.show(e.getComponent(), e.getX(), e.getY()); } } } } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { if (e.isShiftDown() || e.isControlDown() || !dragOk) { dragNode = -1; startDragIndex = -1; return; } this.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); dragNode = locationToIndex(e.getPoint()); startDragIndex = dragNode; } public void mouseReleased(MouseEvent e) { if (startDragIndex != -1) { if (callback != null) { boolean ok = false; try { ok = callback.setValuePerformed(new JMoveValueObject(this, startDragIndex, dragNode)); } catch (java.rmi.RemoteException rx) { throw new RuntimeException("Could not setValuePerformed: " + rx); } if (!ok) { moveItem(dragNode, startDragIndex); } } } this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); dragNode = -1; startDragIndex = -1; } public void mouseDragged(MouseEvent e) { if (dragNode == -1) { return; } int overIndex = locationToIndex(e.getPoint()); if (overIndex >= (model.getSize() - 1)) { overIndex = model.getSize() - 1; } if (overIndex != -1 && overIndex != dragNode) { moveItem(dragNode, overIndex); dragNode = overIndex; setSelectedIndex(overIndex); } } public void mouseMoved(MouseEvent e) { } /** * <p>For the pop up menu callback. We use the popUpIndex variable * to identify the item in the list that the popup menu was issued * on.</p> * * @param e The event received from the popup menu. */ public void actionPerformed(ActionEvent e) { if (callback != null) { if (e.getSource() instanceof JMenuItem) { String string = ((JMenuItem)e.getSource()).getActionCommand(); Object popSelectedItem = null; try { popSelectedItem = ((listHandle) model.elementAt(popUpIndex)).getObject(); } catch (ArrayIndexOutOfBoundsException ex) { popSelectedItem = null; } if (debug) { System.err.println("Forwarding selected item.. (" + popUpIndex + ": " + popSelectedItem + ")"); } try { callback.setValuePerformed(new JParameterValueObject(this, popUpIndex, popSelectedItem, string)); } catch (java.rmi.RemoteException rx) { throw new RuntimeException("Could not set value from JstringListBox: " + rx); } } } } // FocusListener methods ------------------------------------------------------ public void focusLost(FocusEvent e) { if (debug) { System.out.println("JstringListBox: focusLost"); } } public void focusGained(FocusEvent e) { if (debug) { System.out.println("focusGained"); } JComponent parent = (JComponent) this.getParent(); if (parent != null) { parent.scrollRectToVisible(this.getBounds()); } } /** * <p>Default comparator, does a string comparison on the * toString() output of the objects for ordering.</p> * * @param a The first object to compare * @param b The second object to compare * * @return -1 if a is less then b, 0 if they are equal, 1 if a is * greater than b. */ public int compare(Object a, Object b) { return a.toString().compareTo(b.toString()); } }