001: /*BEGIN_COPYRIGHT_BLOCK
002: *
003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are met:
008: * * Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: * * Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
014: * names of its contributors may be used to endorse or promote products
015: * derived from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028: *
029: * This software is Open Source Initiative approved Open Source Software.
030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
031: *
032: * This file is part of DrJava. Download the current version of this project
033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
034: *
035: * END_COPYRIGHT_BLOCK*/
036:
037: package edu.rice.cs.util.docnavigation;
038:
039: import java.io.File;
040: import java.awt.*;
041: import javax.swing.*;
042: import javax.swing.event.*;
043: import java.util.*;
044: import edu.rice.cs.util.swing.Utilities;
045:
046: //import edu.rice.cs.util.swing.RightClickMouseAdapter;
047:
048: /** This class is an extension of JList that adds data shadowing the model embedded in a JList.
049: * Since all changes to the model (except for the selected item!) must go through this interface,
050: * we can support access to methods from non-event threads as long as these methods do not modify
051: * the model. However, all of the public methods that access and modify the model (the latter only running
052: * in the event thread) must be atomic relative to each other, so synchronization is required in most
053: * cases.
054: *
055: * TODO: generify this class and IDocumentNavigator with respect to its element type once JList is.
056: */
057:
058: class JListNavigator<ItemT extends INavigatorItem> extends JList
059: implements IDocumentNavigator<ItemT> {
060:
061: /** The list model (extending AbstractListModel) for this JList. */
062: protected DefaultListModel _model;
063:
064: /** The current selection value. A cached copy of getSelectedValue(). */
065: private volatile ItemT _current = null;
066:
067: // /** The index of _current */
068: // private int _currentIndex = -1;
069:
070: /** The cell renderer for this JList */
071: private volatile CustomListCellRenderer _renderer;
072:
073: /** the collection of INavigationListeners listening to this JListNavigator */
074: private final Vector<INavigationListener<? super ItemT>> navListeners = new Vector<INavigationListener<? super ItemT>>();
075:
076: /** Standard constructor. */
077: public JListNavigator() {
078: super ();
079: init(new DefaultListModel());
080: }
081:
082: private void init(DefaultListModel m) {
083: _model = m;
084: setModel(m);
085: setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
086: addListSelectionListener(new ListSelectionListener() {
087: /** Called when the list value has changed. Should only run in the event thread.
088: * @param e the event corresponding to the change
089: */
090: public void valueChanged(final ListSelectionEvent e) {
091: Utilities.invokeLater(new Runnable() {
092: public void run() {
093: if (!e.getValueIsAdjusting()
094: && !_model.isEmpty()) {
095: @SuppressWarnings("unchecked")
096: final ItemT newItem = (ItemT) getSelectedValue();
097: // final int newIndex = getSelectedIndex();
098: if (_current != newItem) {
099: final ItemT oldItem = _current;
100: NodeData<ItemT> oldData = new NodeData<ItemT>() {
101: public <Ret> Ret execute(
102: NodeDataVisitor<? super ItemT, Ret> v,
103: Object... p) {
104: return v.itemCase(oldItem, p);
105: }
106: };
107: NodeData<ItemT> newData = new NodeData<ItemT>() {
108: public <Ret> Ret execute(
109: NodeDataVisitor<? super ItemT, Ret> v,
110: Object... p) {
111: return v.itemCase(newItem, p);
112: }
113: };
114: for (INavigationListener<? super ItemT> listener : navListeners) {
115: if (oldItem != null)
116: listener
117: .lostSelection(
118: oldData,
119: isNextChangeModelInitiated());
120: if (newItem != null)
121: listener
122: .gainedSelection(
123: newData,
124: isNextChangeModelInitiated());
125: }
126: setNextChangeModelInitiated(false);
127: _current = newItem;
128: // _currentIndex = newIndex;
129: }
130: }
131: }
132: });
133: }
134: });
135:
136: _renderer = new CustomListCellRenderer();
137: _renderer.setOpaque(true);
138: this .setCellRenderer(_renderer);
139: }
140:
141: /** Adds the document doc to this navigator. Should only be executed in event thread.
142: * @param doc the document to add
143: */
144: public void addDocument(ItemT doc) {
145: synchronized (_model) {
146: _model.addElement(doc);
147: }
148: }
149:
150: /** Adds the document to this navigator and ignores the specified path. Should only be
151: * executed in event thread.
152: * @param doc the document to add -- assumed to be of type T
153: * @param path unused parameter in this class
154: */
155: public void addDocument(ItemT doc, String path) {
156: addDocument(doc);
157: }
158:
159: /** A typesafe version of {@code _model.get(i)}. This is a workaround for the
160: * non-generic implementation of DefaultListModel, and should be removed once that
161: * is fixed.
162: */
163: protected ItemT getFromModel(int i) {
164: @SuppressWarnings("unchecked")
165: ItemT result = (ItemT) _model.get(i);
166: return result;
167: }
168:
169: /** Gets the next document after doc in the series.
170: * @param doc the document to reference from
171: * @return the document after doc in the list; if doc is the last
172: * document, returns doc
173: */
174: public ItemT getNext(ItemT doc) {
175: synchronized (_model) {
176: int i = _model.indexOf(doc);
177: if (i == -1)
178: throw new IllegalArgumentException("No such document "
179: + doc.toString()
180: + " found in collection of open documents");
181: if (i + 1 == _model.size())
182: return doc;
183:
184: return getFromModel(i + 1);
185: }
186: }
187:
188: /** Gets the previous document in the series.
189: * @param doc to reference from
190: * @return the document which comes before doc in the list
191: */
192: public ItemT getPrevious(ItemT doc) {
193: synchronized (_model) {
194: int i = _model.indexOf(doc);
195: if (i == -1)
196: throw new IllegalArgumentException("No such document "
197: + doc.toString()
198: + " found in collection of open documents");
199: if (i == 0)
200: return doc;
201: return getFromModel(i - 1);
202: }
203: }
204:
205: /** Gets the first document in the series.
206: * @return the first document in the collection
207: */
208: public ItemT getFirst() {
209: synchronized (_model) {
210: return getFromModel(0);
211: }
212: }
213:
214: /** Gets the first document in the series.
215: * @return the first document in the collection
216: */
217: public ItemT getLast() {
218: synchronized (_model) {
219: return getFromModel(_model.size() - 1);
220: }
221: }
222:
223: /** Returns the currently selected item, or null if none. */
224: public ItemT getCurrent() {
225: return _current;
226: }
227:
228: /** Returns the model lock. */
229: public Object getModelLock() {
230: return _model;
231: }
232:
233: /** Removes the document from the navigator. Should only be executed in event thread.
234: * @param doc the document to remove
235: */
236: public ItemT removeDocument(ItemT doc) {
237: synchronized (_model) {
238: // System.err.println("removing from old list " + doc);
239: int i = _model.indexOf(doc);
240: if (i == -1)
241: throw new IllegalArgumentException("Document " + doc
242: + " not found in Document Navigator");
243: ItemT result = getFromModel(i);
244: _model.remove(i);
245: return result;
246: }
247: }
248:
249: /** Resets a given <code>INavigatorItem<code> in the tree. This may affect the placement of the item or its
250: * display to reflect any changes made in the model. Should only be executed in event thread.
251: * @param doc the docment to be refreshed
252: * @throws IllegalArgumentException if this navigator contains no document
253: * that is equal to the passed document.
254: */
255: public void refreshDocument(ItemT doc, String path) {
256: synchronized (_model) {
257: removeDocument(doc);
258: addDocument(doc);
259: }
260: }
261:
262: /** Sets the specified document as selected. Should only be called from event thread.
263: * @param doc the document to select
264: */
265: public void setActiveDoc(ItemT doc) {
266: boolean found;
267: synchronized (_model) {
268: if (_current == doc)
269: return; // doc is already _current (the active doc)
270: found = _model.contains(doc);
271: }
272: if (found)
273: setSelectedValue(doc, true);
274: // _current = doc; // already done by ListSelectionEvent listener created in init()
275: // }
276: }
277:
278: /** Returns whether or not the navigator contains the document
279: * @param doc the document to find
280: * @return true if this list contains doc (using identity as equality measure), false if not.
281: */
282: public boolean contains(ItemT doc) {
283: synchronized (_model) {
284: return _model.contains(doc);
285: }
286: }
287:
288: /** @return an Enumeration of the documents in this list (ordering is consistent with getNext() and getPrev()).
289: * This cast in this method required to work around the stupid partial generification of DefaultListModel in Java 1.5.
290: * The class should be generic: DefaultListModel<T> { ... Enumeration<T> elements() {...} ... } instead of
291: * DefaultListModel { ... Enumeration<?> elements() {...} ... }.
292: */
293: public Enumeration<ItemT> getDocuments() {
294: synchronized (_model) {
295: // Cast forced by lousy generic typing of DefaultListModel in Java 1.5
296: @SuppressWarnings("unchecked")
297: Enumeration<ItemT> result = (Enumeration<ItemT>) _model
298: .elements();
299: return result;
300: }
301: }
302:
303: /** Returns all the <code>IDocuments</code> contained in the specified bin. Always empty.
304: * @param binName name of bin
305: * @return an <code>INavigatorItem<code> enumeration of this navigator's contents.
306: */
307: public Enumeration<ItemT> getDocumentsInBin(String binName) {
308: return (new Vector<ItemT>()).elements();
309: }
310:
311: /** @return the number of documents in the navigator. */
312: public int getDocumentCount() {
313: return _model.size();
314: }
315:
316: /** @return whether or not the navigator is empty. */
317: public boolean isEmpty() {
318: return _model.isEmpty();
319: }
320:
321: /** Adds listener to the collection of listeners.
322: * @param listener
323: */
324: public void addNavigationListener(
325: INavigationListener<? super ItemT> listener) {
326: synchronized (_model) {
327: navListeners.add(listener);
328: }
329: }
330:
331: /** Unregisters the listener listener
332: * @param listener
333: */
334: public void removeNavigationListener(
335: INavigationListener<? super ItemT> listener) {
336: synchronized (_model) {
337: navListeners.remove(listener);
338: }
339: }
340:
341: /** @return the navigator listeners. */
342: public Collection<INavigationListener<? super ItemT>> getNavigatorListeners() {
343: return navListeners;
344: }
345:
346: /** Clears the navigator and removes all documents. Should only be executed from event thread. */
347: public void clear() {
348: synchronized (_model) {
349: _model.clear();
350: }
351: }
352:
353: /** Executes the list case of the visitor.
354: * @param algo the visitor to execute
355: * @param input the input to run on the visitor
356: */
357: public <InType, ReturnType> ReturnType execute(
358: IDocumentNavigatorAlgo<ItemT, InType, ReturnType> algo,
359: InType input) {
360: return algo.forList(this , input);
361: }
362:
363: /** Returns a Container representation of this navigator */
364: public Container asContainer() {
365: return this ;
366: }
367:
368: /** Selects the document at the x,y coordinate of the navigator pane and sets it to be
369: * the currently active document. Should only be called from event-handling thread.
370: * @param x the x coordinate of the navigator pane
371: * @param y the y coordinate of the navigator pane
372: *
373: */
374: public boolean selectDocumentAt(final int x, final int y) {
375: synchronized (_model) {
376: final int idx = locationToIndex(new java.awt.Point(x, y));
377: java.awt.Rectangle rect = getCellBounds(idx, idx);
378: if (rect.contains(x, y)) {
379: setActiveDoc(getFromModel(idx));
380: return true;
381: }
382: return false;
383: }
384: }
385:
386: /** Returns true if the item at the x,y coordinate of the navigator pane is currently selected.
387: * Always false for JListSortNavigator
388: * Only runs in event thread.
389: * @param x the x coordinate of the navigator pane
390: * @param y the y coordinate of the navigator pane
391: * @return true if the item is currently selected
392: */
393: public boolean isSelectedAt(int x, int y) {
394: return false;
395: // synchronized(_model) {
396: // final int idx = locationToIndex(new java.awt.Point(x,y));
397: // if (idx == -1) return false;
398: // return isSelectedIndex(idx);
399: // }
400: }
401:
402: /** @return the renderer for this object. */
403: public Component getRenderer() {
404: return _renderer;
405: }
406:
407: /** @return the number of selected items. Always 1 for JListNavigator */
408: public int getSelectionCount() {
409: return 1;
410: } // return getSelectedIndices().length; }
411:
412: /** @return true if at least one group of INavigatorItems is selected; always false for JListNavigator */
413: public boolean isGroupSelected() {
414: return false;
415: }
416:
417: /** @return the number of groups selected. Always 0 for JListSortNavigator */
418: public int getGroupSelectedCount() {
419: return 0;
420: }
421:
422: /** @return the folders currently selected. Always empty for JListSortNavigator */
423: public java.util.List<File> getSelectedFolders() {
424: return new ArrayList<File>();
425: }
426:
427: /** @return true if at least one document is selected; always true for JListNavigator */
428: public boolean isDocumentSelected() {
429: return true;
430: }
431:
432: /** @return the number of documents selected. Same as getSelectionCount for JListSortNavigator. */
433: public int getDocumentSelectedCount() {
434: return getSelectionCount();
435: }
436:
437: /** @return the documents currently selected. Only runs in event thread. */
438: @SuppressWarnings("unchecked")
439: public java.util.List<ItemT> getSelectedDocuments() {
440: // Object[] selected = getSelectedValues();
441: // ArrayList<ItemT> l = new ArrayList<ItemT>(selected.length);
442: // for (Object o: selected) { l.add((ItemT)o); }
443: ArrayList<ItemT> l = new ArrayList<ItemT>(1);
444: l.add((ItemT) getSelectedValue());
445: return l;
446: }
447:
448: /** Returns true if the root is selected. Only runs in event thread. */
449: public boolean isRootSelected() {
450: return false;
451: }
452:
453: /** @return true if the INavigatorItem is in the selected group, if a group is selected. */
454: public boolean isSelectedInGroup(ItemT i) {
455: return false;
456: }
457:
458: public void addTopLevelGroup(String name,
459: INavigatorItemFilter<? super ItemT> f) { /* noop */
460: }
461:
462: public boolean isTopLevelGroupSelected() {
463: return false;
464: }
465:
466: /** Returns the names of the top level groups that the selected items descend from.
467: * Always throws a GroupNotSelectedException for JListSortNavigator
468: */
469: public java.util.Set<String> getNamesOfSelectedTopLevelGroup()
470: throws GroupNotSelectedException {
471: throw new GroupNotSelectedException(
472: "A top level group is not selected");
473: }
474:
475: /** Since in the JListNavigator it is impossible to select anything but an INavigatorItem,
476: * this method doesn't need to do anything. See JTreeSortNavigator and IDocumentNavigator.
477: */
478: public void requestSelectionUpdate(ItemT doc) { /* nothing */
479: }
480:
481: // /** Notify this ListModel that doc has changed and may need updating (if it has changed
482: // * from modified to unmodified). Should only be performed in the event thread
483: // */
484: // public void activeDocumentModified() {
485: // synchronized(_model) {
486: // int current = _currentIndex;
487: // fireSelectionValueChanged(current, current, false);
488: // }
489: // }
490: //
491: public String toString() {
492: synchronized (_model) {
493: return _model.toString();
494: }
495: }
496:
497: /** The cell renderer for this list. */
498: private static class CustomListCellRenderer extends
499: DefaultListCellRenderer {
500:
501: /** Rreturns the renderer component for a cell
502: * @param list
503: * @param value
504: * @param index
505: * @param isSelected
506: * @param hasFocus
507: */
508: public Component getListCellRendererComponent(JList list,
509: Object value, int index, boolean isSelected,
510: boolean hasFocus) {
511:
512: super .getListCellRendererComponent(list, value, index,
513: isSelected, hasFocus);
514: setText(((INavigatorItem) value).getName());
515: return this ;
516: }
517: }
518:
519: /** Marks the next selection change as model-initiated (true) or user-initiated (false; default). */
520: public void setNextChangeModelInitiated(boolean b) {
521: putClientProperty(MODEL_INITIATED_PROPERTY_NAME,
522: b ? Boolean.TRUE : null);
523: }
524:
525: /** @return whether the next selection change is model-initiated (true) or user-initiated (false). */
526: public boolean isNextChangeModelInitiated() {
527: return getClientProperty(MODEL_INITIATED_PROPERTY_NAME) != null;
528: }
529: }
|