package com.github.xsavikx.androidscreencast.ui.explorer; import javax.swing.*; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeModel; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; public abstract class LazyLoadingTreeNode extends DefaultMutableTreeNode implements TreeWillExpandListener { /** * */ private static final long serialVersionUID = -4981073521761764327L; private static final String ESCAPE_ACTION_NAME = "escape"; private static final KeyStroke ESCAPE_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); /** * The JTree containing this Node */ private JTree tree; /** * Can the worker be Canceled ? */ private boolean cancelable; /** * Default Constructor * * @param userObject an Object provided by the user that constitutes the node's data * @param tree the JTree containing this Node * @param cancelable */ public LazyLoadingTreeNode(Object userObject, JTree tree, boolean cancelable) { super(userObject); tree.addTreeWillExpandListener(this); this.tree = tree; this.cancelable = cancelable; setAllowsChildren(true); } /** * @return <code>true</code> if there are some childrens */ protected boolean areChildrenLoaded() { return getChildCount() > 0 && getAllowsChildren(); } /** * @return a new Loading please wait node */ protected MutableTreeNode createLoadingNode() { return new DefaultMutableTreeNode("Loading Please Wait ...", false); } /** * Create worker that will load the nodes * * @param tree the tree * @return the newly created SwingWorker */ protected com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> createSwingWorker(final JTree tree) { com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker = new com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], Object>() { @Override protected MutableTreeNode[] doInBackground() { return loadChildren(tree); } @Override protected void done() { try { if (!isCancelled()) { MutableTreeNode[] nodes = get(); setAllowsChildren(nodes.length > 0); setChildren(nodes); unRegisterSwingWorkerForCancel(tree, this); } else { reset(); } } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }; registerSwingWorkerForCancel(tree, worker); return worker; } /** * If the * * @return false, this node can't be a leaf * @see #getAllowsChildren() */ @Override public boolean isLeaf() { return !getAllowsChildren(); } /** * This method will be executed in a background thread. If you have to do some GUI stuff use {@link SwingUtilities#invokeLater(Runnable)} * * @param tree the tree * @return the Created nodes */ public abstract MutableTreeNode[] loadChildren(JTree tree); /** * If the node is cancelable an escape Action is registered in the tree's InputMap and ActionMap that will cancel the execution * * @param tree the tree * @param worker the worker to cancel */ protected void registerSwingWorkerForCancel(JTree tree, com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker) { if (!cancelable) { return; } tree.getInputMap().put(ESCAPE_KEY, ESCAPE_ACTION_NAME); Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); if (action == null) { CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); cancelWorkerAction.addSwingWorker(worker); tree.getActionMap().put(ESCAPE_ACTION_NAME, cancelWorkerAction); } else { if (action instanceof CancelWorkersAction) { CancelWorkersAction cancelAction = (CancelWorkersAction) action; cancelAction.addSwingWorker(worker); } } } /** * Need some improvement ... This method should restore the Node initial state if the worker if canceled */ protected void reset() { DefaultTreeModel defaultModel = (DefaultTreeModel) tree.getModel(); int childCount = getChildCount(); if (childCount > 0) { for (int i = 0; i < childCount; i++) { defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); } } setAllowsChildren(true); } /** * Define nodes children * * @param nodes new nodes */ protected void setChildren(MutableTreeNode... nodes) { TreeModel model = tree.getModel(); if (model instanceof DefaultTreeModel) { DefaultTreeModel defaultModel = (DefaultTreeModel) model; int childCount = getChildCount(); if (childCount > 0) { for (int i = 0; i < childCount; i++) { defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); } } for (int i = 0; i < nodes.length; i++) { defaultModel.insertNodeInto(nodes[i], this, i); } } } /** * set the loading state */ private void setLoading() { setChildren(createLoadingNode()); TreeModel model = tree.getModel(); if (model instanceof DefaultTreeModel) { DefaultTreeModel defaultModel = (DefaultTreeModel) model; int[] indices = new int[getChildCount()]; for (int i = 0; i < indices.length; i++) { indices[i] = i; } defaultModel.nodesWereInserted(LazyLoadingTreeNode.this, indices); } } /** * Default empty implementation, do nothing on collapse event. */ @Override public void treeWillCollapse(TreeExpansionEvent event) { // ignore } /** * Node will expand, it's time to retrieve nodes */ @Override public void treeWillExpand(TreeExpansionEvent event) { if (this.equals(event.getPath().getLastPathComponent())) { if (areChildrenLoaded()) { return; } setLoading(); com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker = createSwingWorker(tree); worker.execute(); } } /** * Remove the swingWorker from the cancellable task of the tree * * @param tree * @param worker */ protected void unRegisterSwingWorkerForCancel(JTree tree, com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker) { if (!cancelable) { return; } Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); if (action != null && action instanceof CancelWorkersAction) { CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); cancelWorkerAction.removeSwingWorker(worker); } } /** * ActionMap can only store one Action for the same key, This Action Stores the list of SwingWorker to be canceled if the escape key is pressed. * * @author Thierry LEFORT 3 mars 08 */ protected static class CancelWorkersAction extends AbstractAction { /** * */ private static final long serialVersionUID = 3173288834368915117L; /** * the SwingWorkers */ private List<com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?>> workers = new ArrayList<>(); /** * Default constructor */ private CancelWorkersAction() { super(ESCAPE_ACTION_NAME); } /** * Do the Cancel */ @Override public void actionPerformed(ActionEvent e) { for (com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker : workers) { worker.cancel(true); } } /** * Add a Cancelable SwingWorker */ public void addSwingWorker(com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker) { workers.add(worker); } /** * Remove a SwingWorker */ public void removeSwingWorker(com.github.xsavikx.androidscreencast.ui.worker.SwingWorker<MutableTreeNode[], ?> worker) { workers.remove(worker); } } }