package com.explodingpixels.macwidgets;
import java.awt.BorderLayout;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.TransferHandler;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import com.explodingpixels.macwidgets.plaf.SourceListTreeUI;
import com.explodingpixels.widgets.TreeUtils;
/**
* An implementation of an OS X Source List. For a full descrption of what a Source List is, see the
* <a href="http://developer.apple.com/documentation/UserExperience/Conceptual/AppleHIGuidelines/XHIGWindows/chapter_18_section_4.html#//apple_ref/doc/uid/20000961-CHDDIGDE">Source Lists</a>
* section of Apple's Human Interface Guidelines.
* <p/>
* This component provides the two basic sytles of Source List: focusble and non-focusable.
* As the name implies, focusable Source Lists and recieve keyboard focus, and thus can be navigated
* using the arrow keys. Non-focusable, cannot receive keyboard focus, and thus cannot be
* navigated via the arrow keys. The two styles of {@code SourceList} are pictured below:
* <br>
* <table>
* <tr><td align="center"><img src="../../../../graphics/iTunesSourceList.png"></td>
* <td align="center"><img src="../../../../graphics/MailSourceList.png"></td></tr>
* <tr><td align="center"><font size="2" face="arial"><b>Focusable SourceList<b></font></td>
* <td align="center"><font size="2" face="arial"><b>Non-focusable SourceList<b></font></td></tr>
* </table>
* <br>
* Here's how to create a simple {@code SourceList} with one item:
* <pre>
* SourceListModel model = new SourceListModel();
* SourceListCategory category = new SourceListCategory("Category");
* model.addCategory(category);
* model.addItemToCategory(new SourceListItem("Item"), category);
* SourceList sourceList = new SourceList(model);
* </pre>
* <p>
* To install a selection listener on the {@code SourceList}, add a
* {@link com.explodingpixels.macwidgets.SourceListSelectionListener}.
* </p>
* <p/>
* To install a context-menu provider, call
* {@link #setSourceListContextMenuProvider(com.explodingpixels.macwidgets.SourceListContextMenuProvider)} with an implementation
* of {@link com.explodingpixels.macwidgets.SourceListContextMenuProvider}.
*/
public class SourceList {
private final SourceListModel fModel;
private final SourceListModelListener fModelListener = createSourceListModelListener();
private final List<SourceListSelectionListener> fSourceListSelectionListeners =
new ArrayList<SourceListSelectionListener>();
private final List<SourceListExpansionListener> fSourceListExpansionListeners =
new ArrayList<SourceListExpansionListener>();
private DefaultMutableTreeNode fRoot = new DefaultMutableTreeNode("root");
private DefaultTreeModel fTreeModel = new DefaultTreeModel(fRoot);
private JTree fTree = new CustomJTree(fTreeModel);
private JScrollPane fScrollPane = MacWidgetFactory.createSourceListScrollPane(fTree);
private final JPanel fComponent = new JPanel(new BorderLayout());
private TreeSelectionListener fTreeSelectionListener = createTreeSelectionListener();
private TreeExpansionListener fTreeExpansionListener = createTreeExpansionListener();
private TreeWillExpandListener fTreeWillExpandListener = createTreeWillExpandListener();
private MouseListener fMouseListener = createMouseListener();
private SourceListControlBar fSourceListControlBar;
private SourceListContextMenuProvider fContextMenuProvider =
new EmptySourceListContextMenuProvider();
private List<SourceListClickListener> fSourceListClickListeners =
new ArrayList<SourceListClickListener>();
private SourceListToolTipProvider fToolTipProvider = new EmptyToolTipProvider();
/**
* Creates a {@code SourceList} with an empty {@link com.explodingpixels.macwidgets.SourceListModel}.
*/
public SourceList() {
this(new SourceListModel());
}
/**
* Creates a {@code SourceList} with the given {@link com.explodingpixels.macwidgets.SourceListModel}.
*
* @param model the {@code SourceListModel} to use.
*/
public SourceList(SourceListModel model) {
if (model == null) {
throw new IllegalArgumentException("Groups cannot be null.");
}
fModel = model;
fModel.addSourceListModelListener(fModelListener);
initUi();
// add each category and its sub-items to backing JTree.
for (int i = 0; i < model.getCategories().size(); i++) {
doAddCategory(model.getCategories().get(i), i);
}
}
private void initUi() {
fComponent.add(fScrollPane, BorderLayout.CENTER);
fTree.addTreeSelectionListener(fTreeSelectionListener);
fTree.addTreeExpansionListener(fTreeExpansionListener);
fTree.addTreeWillExpandListener(fTreeWillExpandListener);
fTree.addMouseListener(fMouseListener);
}
/**
* Installs the given {@link com.explodingpixels.macwidgets.SourceListControlBar} at the base of this {@code SourceList}. This
* method can be called only once, and should generally be called during creation of the
* {@code SourceList}.
*
* @param sourceListControlBar the {@link com.explodingpixels.macwidgets.SourceListControlBar} to add.
* @throws IllegalStateException if a {@code SourceListControlBar} has already been installed
* on this {@code SourceList}.
* @throws IllegalArgumentException if the {@code SourceListControlBar} is null.
*/
public void installSourceListControlBar(SourceListControlBar sourceListControlBar) {
if (fSourceListControlBar != null) {
throw new IllegalStateException("A SourceListControlBar has already been installed on" +
" this SourceList.");
}
if (sourceListControlBar == null) {
throw new IllegalArgumentException("SourceListControlBar cannot be null.");
}
fSourceListControlBar = sourceListControlBar;
fComponent.add(fSourceListControlBar.getComponent(), BorderLayout.SOUTH);
}
/**
* True if there is a {@link com.explodingpixels.macwidgets.SourceListControlBar} installed on this {@code SourceList}.
*
* @return true if there is a {@link com.explodingpixels.macwidgets.SourceListControlBar} installed on this {@code SourceList}.
*/
public boolean isSourceListControlBarInstalled() {
return fSourceListControlBar != null;
}
/**
* Sets the {@link com.explodingpixels.macwidgets.SourceListContextMenuProvider} to use for this {@code SourceList}.
*
* @param contextMenuProvider the {@link com.explodingpixels.macwidgets.SourceListContextMenuProvider} to use for this
* {@code SourceList}.
* @throws IllegalArgumentException if the {@code SourceListContextMenuProvider} is null.
*/
public void setSourceListContextMenuProvider(SourceListContextMenuProvider contextMenuProvider) {
if (contextMenuProvider == null) {
throw new IllegalArgumentException("SourceListContextMenuProvider cannot be null.");
}
fContextMenuProvider = contextMenuProvider;
}
/**
* Uninstalls any listeners that this {@code SourceList} installed on creation, thereby allowing
* it to be garbage collected.
*/
public void dispose() {
fModel.removeSourceListModelListener(fModelListener);
}
/**
* Gets the selected {@link com.explodingpixels.macwidgets.SourceListItem}.
*
* @return the selected {@code SourceListItem}.
*/
public SourceListItem getSelectedItem() {
SourceListItem selectedItem = null;
if (fTree.getSelectionPath() != null
&& fTree.getSelectionPath().getLastPathComponent() != null) {
DefaultMutableTreeNode selectedNode =
(DefaultMutableTreeNode) fTree.getSelectionPath().getLastPathComponent();
assert selectedNode.getUserObject() instanceof SourceListItem
: "Only SourceListItems can be selected.";
selectedItem = (SourceListItem) selectedNode.getUserObject();
}
return selectedItem;
}
/**
* Selects the given {@link com.explodingpixels.macwidgets.SourceListItem} in the list.
*
* @param item the item to select.
* @throws IllegalArgumentException if the given item is not in the list.
*/
public void setSelectedItem(SourceListItem item) {
getModel().validateItemIsInModel(item);
DefaultMutableTreeNode treeNode = getNodeForObject(fRoot, item);
fTree.setSelectionPath(new TreePath(treeNode.getPath()));
}
/**
* Clears the current selection, if there is one.
*/
public void clearSelection() {
fTree.clearSelection();
}
/**
* Sets whether this {@code SourceList} can have focus. When focusable and this
* {@code SourceList} has focus, the keyboard can be used for navigation.
*
* @param focusable true if this {@code SourceList} should be focusable.
*/
public void setFocusable(boolean focusable) {
fTree.setFocusable(focusable);
}
/**
* Installs iApp style scroll bars on this {@code SourceList}.
*
* @see com.explodingpixels.macwidgets.IAppWidgetFactory#makeIAppScrollPane
*/
public void useIAppStyleScrollBars() {
IAppWidgetFactory.makeIAppScrollPane(fScrollPane);
}
/**
* Gets the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceList} uses.
*
* @return the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceList} uses.
*/
public SourceListColorScheme getColorScheme() {
return ((SourceListTreeUI) fTree.getUI()).getColorScheme();
}
/**
* Sets the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceList} uses.
*
* @param colorScheme the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceList} uses.
*/
public void setColorScheme(SourceListColorScheme colorScheme) {
((SourceListTreeUI) fTree.getUI()).setColorScheme(colorScheme);
}
/**
* Set's the {@link javax.swing.TransferHandler} that this {@code SourceList} should use
* during drag and drop operations. If the given handler not null, then
* dragging will be turned on for the {@code SourceList}. If the handler is
* null, then dragging will be turned off.
*
* @param transferHandler the {@code TransferHandler} for this
* {@code SourceList}. Can be null.
*/
public void setTransferHandler(TransferHandler transferHandler) {
fTree.setDragEnabled(transferHandler != null);
fTree.setTransferHandler(transferHandler);
}
/**
* Scrolls the given {@link com.explodingpixels.macwidgets.SourceListItem} to be visible.
*
* @param item the {@code SourceListItem} to scroll to visible.
*/
public void scrollItemToVisible(SourceListItem item) {
getModel().validateItemIsInModel(item);
DefaultMutableTreeNode treeNode = getNodeForObject(fRoot, item);
fTree.scrollPathToVisible(new TreePath(treeNode.getPath()));
}
/**
* Sets the expanded state of the given {@link com.explodingpixels.macwidgets.SourceListCategory}.
*
* @param category the category to set the expanded state on.
* @param expanded true if the given category should be expanded, false if it should be
* collapsed.
* @throws IllegalArgumentException if the given {@code SourceListCategory} is not part of the
* associated {@link com.explodingpixels.macwidgets.SourceListModel}.
*/
public void setExpanded(SourceListCategory category, boolean expanded) {
DefaultMutableTreeNode categoryNode = getNodeForObject(category);
checkCategoryNodeNotNull(categoryNode);
TreeUtils.setExpandedOnEdt(fTree, new TreePath(categoryNode.getPath()), expanded);
}
/**
* Sets the expanded state of the given {@link com.explodingpixels.macwidgets.SourceListItem}.
*
* @param item the item to set the expanded state on.
* @param expanded true if the given item should be expanded, false if it should be
* collapsed.
* @throws IllegalArgumentException if the given {@code SourceListItem} is not part of the
* associated {@link com.explodingpixels.macwidgets.SourceListModel}.
*/
public void setExpanded(SourceListItem item, boolean expanded) {
DefaultMutableTreeNode itemNode = getNodeForObject(item);
checkItemNodeNotNull(itemNode);
TreeUtils.setExpandedOnEdt(fTree, new TreePath(itemNode.getPath()), expanded);
}
private DefaultMutableTreeNode getNodeForObject(Object userObject) {
return getNodeForObject(fRoot, userObject);
}
private static DefaultMutableTreeNode getNodeForObject(DefaultMutableTreeNode parentNode,
Object userObject) {
if (parentNode.getUserObject().equals(userObject)) {
return parentNode;
} else if (parentNode.children().hasMoreElements()) {
for (int i = 0; i < parentNode.getChildCount(); i++) {
DefaultMutableTreeNode childNode =
(DefaultMutableTreeNode) parentNode.getChildAt(i);
DefaultMutableTreeNode retVal =
getNodeForObject(childNode, userObject);
if (retVal != null) {
return retVal;
}
}
} else {
return null;
}
return null;
}
/**
* Gets the user interface component representing this {@code SourceList}. The returned
* {@link javax.swing.JComponent} should be added to a container that will be displayed.
*
* @return the user interface component representing this {@code SourceList}.
*/
public JComponent getComponent() {
return fComponent;
}
/**
* Gets the {@link com.explodingpixels.macwidgets.SourceListModel} backing this {@code SourceList}.
*
* @return the {@code SourceListModel} backing this {@code SourceList}.
*/
public SourceListModel getModel() {
return fModel;
}
/**
* Sets the {@link com.explodingpixels.macwidgets.SourceListToolTipProvider} to use.
*
* @param toolTipProvider the {@code SourceListToolTipProvider to use.
*/
public void setToolTipProvider(SourceListToolTipProvider toolTipProvider) {
if (toolTipProvider == null) {
throw new IllegalArgumentException("SourceListToolTipProvider cannot be null.");
}
fToolTipProvider = toolTipProvider;
}
private void doAddCategory(SourceListCategory category, int index) {
DefaultMutableTreeNode categoryNode = new DefaultMutableTreeNode(category);
fTreeModel.insertNodeInto(categoryNode, fRoot, index);
// add each of the categories child items to the tree.
for (int i = 0; i < category.getItems().size(); i++) {
doAddItemToCategory(category.getItems().get(i), category, i);
}
TreeUtils.expandPathOnEdt(fTree, new TreePath(categoryNode.getPath()));
}
private void doRemoveCategory(SourceListCategory category) {
DefaultMutableTreeNode categoryNode = getNodeForObject(fRoot, category);
checkCategoryNodeNotNull(categoryNode);
fTreeModel.removeNodeFromParent(categoryNode);
}
private void doAddItemToCategory(SourceListItem itemToAdd, SourceListCategory category, int index) {
DefaultMutableTreeNode categoryNode = getNodeForObject(fRoot, category);
checkCategoryNodeNotNull(categoryNode);
doAddItemToNode(itemToAdd, categoryNode, index);
}
private void doRemoveItemFromCategory(SourceListItem itemToRemove, SourceListCategory category) {
DefaultMutableTreeNode categoryNode = getNodeForObject(fRoot, category);
checkCategoryNodeNotNull(categoryNode);
DefaultMutableTreeNode itemNode = getNodeForObject(categoryNode, itemToRemove);
checkCategoryNodeNotNull(itemNode);
fTreeModel.removeNodeFromParent(itemNode);
}
private void doAddItemToItem(SourceListItem itemToAdd, SourceListItem parentItem, int index) {
DefaultMutableTreeNode parentItemNode = getNodeForObject(fRoot, parentItem);
checkCategoryNodeNotNull(parentItemNode);
doAddItemToNode(itemToAdd, parentItemNode, index);
}
private void doRemoveItemFromItem(SourceListItem itemToRemove, SourceListItem parentItem) {
DefaultMutableTreeNode parentNode = getNodeForObject(fRoot, parentItem);
checkCategoryNodeNotNull(parentNode);
DefaultMutableTreeNode itemNode = getNodeForObject(parentNode, itemToRemove);
checkCategoryNodeNotNull(itemNode);
fTreeModel.removeNodeFromParent(itemNode);
}
private void doAddItemToNode(SourceListItem itemToAdd, DefaultMutableTreeNode parentNode, int index) {
DefaultMutableTreeNode itemNode = new DefaultMutableTreeNode(itemToAdd);
fTreeModel.insertNodeInto(itemNode, parentNode, index);
// add each of the newly added item's children nodes.
for (int i = 0; i < itemToAdd.getChildItems().size(); i++) {
doAddItemToItem(itemToAdd.getChildItems().get(i), itemToAdd, i);
}
// if the parent node is a new node, expand it. thus the default behavior is to expand a
// parent node.
if (parentNode.getChildCount() == 1) {
TreeUtils.expandPathOnEdt(fTree, new TreePath(parentNode.getPath()));
}
}
private void doItemChanged(SourceListItem item) {
DefaultMutableTreeNode itemNode = getNodeForObject(fRoot, item);
checkItemNodeNotNull(itemNode);
fTreeModel.nodeChanged(itemNode);
}
private void doShowContextMenu(MouseEvent event) {
// grab the item or category under the mouse events point if there is
// there is an item or category under this point.
Object itemOrCategory = getItemOrCategoryUnderPoint(event.getPoint());
// if there was no item under the click, then call the generic contribution method.
// else if there was a SourceListItem under the click, call the corresponding contribution
// method.
// else if there was a SourceListCategory under the click, call the corresponding contribution
// method.
JPopupMenu popup = null;
if (itemOrCategory == null) {
popup = fContextMenuProvider.createContextMenu();
} else if (itemOrCategory instanceof SourceListItem) {
popup = fContextMenuProvider.createContextMenu((SourceListItem) itemOrCategory);
} else if (itemOrCategory instanceof SourceListCategory) {
popup = fContextMenuProvider.createContextMenu((SourceListCategory) itemOrCategory);
}
// only show the context-menu if menu items have been added to it.
if (popup != null && popup.getComponentCount() > 0) {
popup.show(fTree, event.getX(), event.getY());
}
}
private void doSourceListClicked(MouseEvent event) {
// grab the item or category under the mouse events point if there is
// there is an item or category under this point.
Object itemOrCategory = getItemOrCategoryUnderPoint(event.getPoint());
SourceListClickListener.Button button =
SourceListClickListener.Button.getButton(event.getButton());
int clickCount = event.getClickCount();
if (itemOrCategory == null) {
// do nothing.
} else if (itemOrCategory instanceof SourceListItem) {
fireSourceListItemClicked((SourceListItem) itemOrCategory, button, clickCount);
} else if (itemOrCategory instanceof SourceListCategory) {
fireSourceListCategoryClicked((SourceListCategory) itemOrCategory, button, clickCount);
}
}
private Object getItemOrCategoryUnderPoint(Point point) {
// grab the path under the given point.
TreePath path = fTree.getPathForLocation(point.x, point.y);
// if there is a tree item under that point, cast it to a DefaultMutableTreeNode and grab
// the user object which will either be a SourceListItem or SourceListCategory.
return path == null
? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
}
private TreeSelectionListener createTreeSelectionListener() {
return new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
fireSourceListItemSelected(getSelectedItem());
}
};
}
private TreeExpansionListener createTreeExpansionListener() {
return new TreeExpansionListener() {
public void treeExpanded(TreeExpansionEvent event) {
onExpandedOrCollapsed(event, true);
}
public void treeCollapsed(TreeExpansionEvent event) {
onExpandedOrCollapsed(event, false);
}
private void onExpandedOrCollapsed(TreeExpansionEvent event, boolean expanded) {
Object itemOrCategory = getItemOrCategoryFromTreeExpansionEvent(event);
if (itemOrCategory != null) {
if (itemOrCategory instanceof SourceListCategory) {
SourceListCategory category = (SourceListCategory) itemOrCategory;
if (expanded) {
fireSourceListCategoryExpanded(category);
} else {
fireSourceListCategoryCollapsed(category);
}
} else if (itemOrCategory instanceof SourceListItem) {
SourceListItem sourceListItem = (SourceListItem) itemOrCategory;
if (expanded) {
fireSourceListItemExpanded(sourceListItem);
} else {
fireSourceListItemCollapsed(sourceListItem);
}
}
}
}
};
}
private TreeWillExpandListener createTreeWillExpandListener() {
return new TreeWillExpandListener() {
public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
onWillExpandOrCollapse(event, true);
}
public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
onWillExpandOrCollapse(event, false);
}
private void onWillExpandOrCollapse(TreeExpansionEvent event, boolean expanded) throws ExpandVetoException {
Object itemOrCategory = getItemOrCategoryFromTreeExpansionEvent(event);
if (itemOrCategory != null) {
if (itemOrCategory instanceof SourceListCategory) {
SourceListCategory category = (SourceListCategory) itemOrCategory;
if (expanded) {
fireSourceListCategoryWillExpand(event, category);
} else {
fireSourceListCategoryWillCollapse(event, category);
}
} else if (itemOrCategory instanceof SourceListItem) {
SourceListItem sourceListItem = (SourceListItem) itemOrCategory;
if (expanded) {
fireSourceListItemWillExpand(event, sourceListItem);
} else {
fireSourceListItemWillCollapse(event, sourceListItem);
}
}
}
}
};
}
private SourceListModelListener createSourceListModelListener() {
return new SourceListModelListener() {
public void categoryAdded(SourceListCategory category, int index) {
doAddCategory(category, index);
}
public void categoryRemoved(SourceListCategory category) {
doRemoveCategory(category);
}
public void itemAddedToCategory(SourceListItem item, SourceListCategory category, int index) {
doAddItemToCategory(item, category, index);
}
public void itemRemovedFromCategory(SourceListItem item, SourceListCategory category) {
doRemoveItemFromCategory(item, category);
}
public void itemAddedToItem(SourceListItem item, SourceListItem parentItem, int index) {
doAddItemToItem(item, parentItem, index);
}
public void itemRemovedFromItem(SourceListItem item, SourceListItem parentItem) {
doRemoveItemFromItem(item, parentItem);
}
public void itemChanged(SourceListItem item) {
doItemChanged(item);
}
};
}
private MouseListener createMouseListener() {
return new MouseAdapter() {
// TODO there is an interesting point of contention here: should
// TODO the context menu always be shown as if it were on a Mac (on
// TODO mouse press) or based on the platform on which it is running.
// TODO always doing the same thing would actually be harder,
// TODO because we wouldn't be able to rely on the isPopupTrigger
// TODO method and there is no way to determine when the
// TODO popup-menu-trigger button is.
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
doShowContextMenu(e);
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
doShowContextMenu(e);
}
}
@Override
public void mouseClicked(MouseEvent e) {
doSourceListClicked(e);
}
};
}
private Object getItemOrCategoryFromTreeExpansionEvent(TreeExpansionEvent event) {
// Get the object that was expanded/collapsed from the expasion event
Object lastPathComponent = event.getPath().getLastPathComponent();
// Cast it to a DefaultMutableTreeNode and grab
// the user object which will either be a SourceListItem or SourceListCategory.
return ((DefaultMutableTreeNode) lastPathComponent).getUserObject();
}
// SourceListClickListener support. ///////////////////////////////////////
private void fireSourceListItemClicked(
SourceListItem item, SourceListClickListener.Button button,
int clickCount) {
for (SourceListClickListener listener : fSourceListClickListeners) {
listener.sourceListItemClicked(item, button, clickCount);
}
}
private void fireSourceListCategoryClicked(
SourceListCategory category, SourceListClickListener.Button button,
int clickCount) {
for (SourceListClickListener listener : fSourceListClickListeners) {
listener.sourceListCategoryClicked(category, button, clickCount);
}
}
/**
* Adds the {@link com.explodingpixels.macwidgets.SourceListClickListener} to the list of listeners.
*
* @param listener the {@code SourceListClickListener} to add.
*/
public void addSourceListClickListener(SourceListClickListener listener) {
fSourceListClickListeners.add(listener);
}
/**
* Removes the {@link com.explodingpixels.macwidgets.SourceListClickListener} to the list of listeners.
*
* @param listener the {@code SourceListClickListener} to remove.
*/
public void removeSourceListClickListener(SourceListClickListener listener) {
fSourceListClickListeners.remove(listener);
}
// SourceListSelectionListener support. ///////////////////////////////////
private void fireSourceListItemSelected(SourceListItem item) {
for (SourceListSelectionListener listener : fSourceListSelectionListeners) {
listener.sourceListItemSelected(item);
}
}
/**
* Adds the {@link com.explodingpixels.macwidgets.SourceListSelectionListener} to the list of listeners.
*
* @param listener the {@code SourceListSelectionListener} to add.
*/
public void addSourceListSelectionListener(SourceListSelectionListener listener) {
fSourceListSelectionListeners.add(listener);
}
/**
* Removes the {@link com.explodingpixels.macwidgets.SourceListSelectionListener} from the list of listeners.
*
* @param listener the {@code SourceListSelectionListener} to remove.
*/
public void removeSourceListSelectionListener(SourceListSelectionListener listener) {
fSourceListSelectionListeners.remove(listener);
}
// SourceListExpansionListener support. ///////////////////////////////////
private void fireSourceListItemExpanded(SourceListItem item) {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
listener.sourceListItemExpanded(item);
}
}
private void fireSourceListItemCollapsed(SourceListItem item) {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
listener.sourceListItemCollapsed(item);
}
}
private void fireSourceListItemWillExpand(TreeExpansionEvent event, SourceListItem item) throws ExpandVetoException {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
handleShouldExpandOrCollapseResponse(listener.shouldExpandSourceListItem(item), event);
}
}
private void fireSourceListItemWillCollapse(TreeExpansionEvent event, SourceListItem item) throws ExpandVetoException {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
handleShouldExpandOrCollapseResponse(listener.shouldCollapseSourceListItem(item), event);
}
}
private void fireSourceListCategoryExpanded(SourceListCategory category) {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
listener.sourceListCategoryExpanded(category);
}
}
private void fireSourceListCategoryCollapsed(SourceListCategory category) {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
listener.sourceListCategoryCollapsed(category);
}
}
private void fireSourceListCategoryWillExpand(TreeExpansionEvent event, SourceListCategory category) throws ExpandVetoException {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
handleShouldExpandOrCollapseResponse(listener.shouldExpandSourceListCategory(category), event);
}
}
private void fireSourceListCategoryWillCollapse(TreeExpansionEvent event, SourceListCategory category) throws ExpandVetoException {
for (SourceListExpansionListener listener : fSourceListExpansionListeners) {
handleShouldExpandOrCollapseResponse(listener.shouldToCollapseSourceListCategory(category), event);
}
}
private void handleShouldExpandOrCollapseResponse(boolean shouldExpandOrCollapse, TreeExpansionEvent event) throws ExpandVetoException {
if (!shouldExpandOrCollapse) {
throw new ExpandVetoException(event);
}
}
/**
* Adds the {@link com.explodingpixels.macwidgets.SourceListExpansionListener} to the list of listeners.
*
* @param listener the {@code SourceListExpansionListener} to add.
*/
public void addSourceListExpansionListener(SourceListExpansionListener listener) {
fSourceListExpansionListeners.add(listener);
}
/**
* Removes the {@link com.explodingpixels.macwidgets.SourceListExpansionListener} from the list of listeners.
*
* @param listener the {@code SourceListExpansionListener} to remove.
*/
public void removeSourceListExpansionListener(SourceListExpansionListener listener) {
fSourceListExpansionListeners.remove(listener);
}
// Utility methods. ///////////////////////////////////////////////////////////////////////////
private static void checkCategoryNodeNotNull(MutableTreeNode node) {
if (node == null) {
throw new IllegalArgumentException("The given SourceListCategory " +
"does not exist in this SourceList.");
}
}
private static void checkItemNodeNotNull(MutableTreeNode node) {
if (node == null) {
throw new IllegalArgumentException("The given SourceListItem " +
"does not exist in this SourceList.");
}
}
// EmptySourceListContextMenuProvider implementation. /////////////////////////////////////////
private static class EmptySourceListContextMenuProvider implements SourceListContextMenuProvider {
public JPopupMenu createContextMenu() {
return null;
}
public JPopupMenu createContextMenu(SourceListItem item) {
return null;
}
public JPopupMenu createContextMenu(SourceListCategory category) {
return null;
}
}
// Custom JTree implementation that always returns SourceListTreeUI delegate. /////////////////
private class CustomJTree extends JTree {
public CustomJTree(TreeModel newModel) {
super(newModel);
ToolTipManager.sharedInstance().registerComponent(this);
}
@Override
public void updateUI() {
setUI(new SourceListTreeUI());
invalidate();
}
@Override
public void collapsePath(TreePath path) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
Object categoryOrItem = node.getUserObject();
if (categoryOrItem instanceof SourceListCategory
&& !((SourceListCategory) categoryOrItem).isCollapsable()) {
// ignore the request to collapse, as this is a non-collapsable category.
} else {
super.collapsePath(path);
}
}
@Override
public String getToolTipText(MouseEvent event) {
TreePath path = getPathForLocation(event.getX(), event.getY());
Object userObject = path == null
? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
String toolTipText = null;
if (userObject instanceof SourceListCategory) {
toolTipText = fToolTipProvider.getTooltip((SourceListCategory) userObject);
} else if (userObject instanceof SourceListItem) {
toolTipText = fToolTipProvider.getTooltip((SourceListItem) userObject);
}
return toolTipText;
}
}
// Empty SourceListTooltipProvider. ///////////////////////////////////////////////////////////
private static class EmptyToolTipProvider implements SourceListToolTipProvider {
public String getTooltip(SourceListCategory category) {
return null;
}
public String getTooltip(SourceListItem item) {
return null;
}
}
}