Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* #%L
* JAXX :: Runtime
* %%
* Copyright (C) 2008 - 2024 Code Lutin, Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 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 Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nuiton.jaxx.runtime.swing.nav;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.nuiton.jaxx.runtime.swing.nav.tree.AbstractNavTreeCellRenderer;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Tree helper to deal with the build of trees and other usefull operations.
*
* A helper acts as an handler for a {@code tree}. It owns the {@link #getModel()} of
* the {@code #tree}.
*
* Note: A helper can NOT be used to manage multi-trees.
*
Internal states
*
Internal model
* To create the model, use method {@link #createModel(NavNode, Object...)} given a
* root node.
*
* To obtain the model, use method {@link #getModel()}.
*
* Note: The helper internal model can be different from the tree model,
* but must be the lowest model, other models must listen nicely this
* model to obtain model modification and selection notifications.
*
Internal tree
* As said before, a helper matches exactly one tree.
*
* To register the tree, use method {@link #setUI(Object, boolean, TreeSelectionListener)}.
*
* To obtain the tree, use method {@link #getUI()}.
*
Internal data provider
* To populate childs nodes and render nodes, we use a {@link NavDataProvider}.
*
* To register the data provider, use method {@link #setDataProvider(NavDataProvider)}.
*
* To obtain the data provider, use method {@link #getDataProvider()}.
*
Internal listeners
* Several listeners are used to manage the auto-loading of nodes in model :
*
{@link #expandListener}
* This listener will load node's childs before node expands if the node is not loaded.
*
* See the {@link NavNode#isLoaded()} method.
*
{@link #treeModelListener}
* To listen modification of the model, it will mainly repopulate nodes when
* required.
*
* See the method {@link #populateNode(NavNode, Object[], boolean)}.
*
{@link #selectionListener}
* To listen modification of the selection, it will mainly expand paths if required.
*
* This is a requirement, since childs of a node should NOT be loaded, so when
* selects a node, always check the path from root to selected node are all fully
* loaded.
*
Model methods
* The helper offers some methods to modify and query the internal tree model.
*
Model modification
*
*
{@link #createModel(NavNode, Object...)}
*
{@link #insertNode(NavNode, NavNode)}
*
{@link #removeNode(NavNode)}
*
{@link #moveNode(NavNode, NavNode, int)}
*
{@link #refreshNode(NavNode, boolean)}
*
{@link #loadAllNodes(NavNode, NavDataProvider)}
*
*
*
Model selection modification
*
*
{@link #selectNode(NavNode)}
*
{@link #selectNode(String...)}
*
{@link #selectParentNode()}
*
*
Model query
*
*
{@link #findNode(NavNode, String...)}
*
*
Child loadors factory
* The class offers a factory of {@link NavNodeChildLoador}, use the method
* {@link #getChildLoador(Class)} to obtain the correct child loador given his type.
*
* @param Type of ui to bridge
* @author Tony Chemit - [email protected]
* @see NavNode
* @see NavNodeChildLoador
* @see AbstractNavTreeCellRenderer
* @since 2.1
*/
public abstract class NavHelper, N extends NavNode> {
/** Logger */
static private final Logger log = LogManager.getLogger(NavHelper.class);
//--------------------------------------------------------------------------
//-- Methods to implement in your helper (related only to ui)
//--------------------------------------------------------------------------
public abstract void scrollPathToVisible(TreePath path);
public abstract void setSelectionPath(TreePath path);
public abstract void addSelectionPath(TreePath path);
public abstract void addSelectionPaths(TreePath[] paths);
public abstract void removeSelectionPath(TreePath path);
public abstract void removeSelectionPaths(TreePath[] paths);
public abstract TreeSelectionModel getSelectionModel();
public abstract boolean isExpanded(TreePath pathToExpand);
public abstract void expandPath(TreePath pathToExpand);
/**
* Register a new root node.
*
* If internal {@link #getModel()} does not exists, creates a new one from
* his given root {@code node}, otherwise just set the new root on the
* existing model.
*
* Note: As a side-effect, the model will be keep in field {@link #getModel()}
* and the {@link #treeModelListener} will be registred on this model.
*
* @param node the root node of the new model
* @param extraArgs extra args to create initial model
* @return the new model
*/
protected abstract M createModel(N node, Object... extraArgs);
/**
* Obtains the {@link AbstractNavTreeCellRenderer} renderer of the
* registred tree.
*
* @return the renderer of the registred tree or null if no tree was
* registred nor the renderer is a {@link AbstractNavTreeCellRenderer}.
*/
public abstract AbstractNavTreeCellRenderer getTreeCellRenderer();
/**
* Obtains the selected node of the registred tree.
*
* @return the selected tree or {@code null} if no registred tree nor
* selection empty.
*/
public abstract N getSelectedNode();
/**
* Obtains the selected nodes of the registred tree.
*
* @return the selected tree or {@code null} if no registred tree nor
* selection empty.
*/
public abstract List getSelectedNodes();
/**
* Registers the given {@code tree} for this helper.
*
* Note: as a side-effect, it will register (if required) the
* {@link #expandListener} listener and (if required) the
* {@link #selectionListener}.
* Note : as a second side-effect, it will register the given {@code willExpandListener} (if not null)
* for the ui and do it BEFORE the {@link #expandListener}. The main
* idea here is to be able to block any expand (or collapse).
*
* @param tree the tree to register
* @param addExpandTreeListener a flag to add expand listener
* @param addOneClickSelectionListener a flag to expend when selection
* @param listener the optional selection listener to add
* @param willExpandListener the optional will expand listener to add BEFORE the default expand tree listener (if he was required)
* @since 2.1.2
*/
public abstract void setUI(U tree,
boolean addExpandTreeListener,
boolean addOneClickSelectionListener,
TreeSelectionListener listener,
TreeWillExpandListener willExpandListener);
/**
* The shared bridge.
*
* A helper deals with only ONE model (this one), becuase we add some
* listeners on it, we prefer always to keep ONE instance (any way this is
* a good thing).
*
* If you want to create a new model, just creates the good root node and
* push it in this model.
*
* For example, if you wrap the shared model with a filter model... Anyway, all
* listeners of this helper apply always of THIs model.
*/
private final B bridge;
/** the associated ui component */
private U ui;
/** The shared data provider used to obtain datas to populate nodes and render them. */
protected NavDataProvider dataProvider;
/**
* A {@link TreeWillExpandListener} used to listen when tree should expand.
*
* If so, the listener will load selected node childs if required
* (says when the {@link NavNode#isLoaded()} is sets to {@code false}).
*/
protected final TreeWillExpandListener expandListener;
/**
* pour ouvrir les fils d'un noeud que l'on vient de sélectionner pour
* éviter d'avoir à faire des doubles clics.
*/
protected final TreeSelectionListener selectionListener;
/**
* pour recharger le rendu des noeuds (et charger les fils si nécessaires)
* lors d'une modification dans le modèle de l'arbre.
*/
protected final TreeModelListener treeModelListener;
/** Cache of child loadors. */
protected static Set super NavNodeChildLoador, ?, ?, ?, ?>> childLoadors;
protected static Set super NavNodeChildLoador, ?, ?, ?, ?>> getChildLoadors() {
if (childLoadors == null) {
childLoadors = new HashSet<>();
}
return childLoadors;
}
/**
* Obtains the {@link NavNodeChildLoador} of the given {@code type} from
* internal cache.
*
* Note: The loador will be instanciated if not found, and push in cache.
*
* @param type the type of loador to get
* @param the type of loador to get
* @return the loador from cache
*/
@SuppressWarnings({"unchecked"})
public static > L getChildLoador(Class type) {
Set super NavNodeChildLoador, ?, ?, ?, ?>> cache = getChildLoadors();
NavNodeChildLoador, ?, ?, ?, ?> result = null;
for (Object loador : cache) {
if (type.equals(loador.getClass())) {
result = (NavNodeChildLoador, ?, ?, ?, ?>) loador;
break;
}
}
if (result == null) {
// add it in cache
try {
result = type.newInstance();
cache.add(result);
if (log.isDebugEnabled()) {
log.debug("Add " + result + " in loadors cache (new size:" + cache.size() + ").");
}
} catch (Exception e) {
throw new IllegalArgumentException("Could not instanciate loador [" + type.getName() + "]", e);
}
}
return (L) result;
}
public NavHelper(B bridge) {
this.bridge = bridge;
selectionListener = e -> {
if (!checkModel()) {
return;
}
// Hack, because event.getSource for TreeTable doesnt return selectionModel
TreeSelectionModel source = getSelectionModel();
if (source.isSelectionEmpty()) {
// empty selection
if (log.isDebugEnabled()) {
log.debug("Selection is empty.");
}
return;
}
boolean debugEnabled = log.isDebugEnabled();
boolean traceEnabled = log.isTraceEnabled();
for (TreePath path : e.getPaths()) {
N node = getNode(path);
if (node == null) {
// pas de noeud selectionne
if (debugEnabled) {
log.debug("Skip for null node.");
}
continue;
}
boolean isAdded = e.isAddedPath(path);
TreePath pathToExpand = new TreePath(NavHelper.this.bridge.getPathToRoot(node));
boolean pathExpanded = isExpanded(pathToExpand);
if (traceEnabled || isAdded && debugEnabled) {
log.debug("==== Node selection ====================================");
log.debug("node ? " + node);
log.debug("is added ? " + isAdded);
log.debug("is path expanded ? " + pathExpanded);
log.debug("is node static ? " + node.isStaticNode());
log.debug("is node loaded ? " + node.isLoaded());
log.debug("is node leaf ? " + node.isLeaf());
log.debug("node nb childs ? " + node.getChildCount());
}
if (isAdded && !pathExpanded) {
// ask to expand path
if (log.isDebugEnabled()) {
log.debug("expand node [" + pathToExpand + "]");
}
expandPath(pathToExpand);
}
}
};
expandListener = new TreeWillExpandListener() {
@Override
public void treeWillExpand(TreeExpansionEvent event) {
if (!checkModel()) {
// no model
return;
}
N source = getNode(event.getPath());
if (source.isLoaded()) {
// node is already loaded, nothing to do
return;
}
if (log.isDebugEnabled()) {
log.debug("will load childs of node [" + source + "]");
}
// populate childs of node
source.populateChilds(getBridge(), getDataProvider());
}
@Override
public void treeWillCollapse(TreeExpansionEvent event) {
}
};
treeModelListener = new TreeModelListener() {
@Override
public void treeNodesInserted(TreeModelEvent e) {
if (!checkModel()) {
// no model
return;
}
N source = getNode(e.getTreePath());
Object[] children = e.getChildren();
if (log.isDebugEnabled()) {
log.debug(getMessage("inserted ", source, children));
}
// ask to populate children nodes
populateNode(null, children, false);
}
@SuppressWarnings({"unchecked"})
@Override
public void treeNodesRemoved(TreeModelEvent e) {
if (!checkModel()) {
// no model
return;
}
N source = getNode(e.getTreePath());
Object[] children = e.getChildren();
if (log.isDebugEnabled()) {
log.debug(getMessage("removed ", source, children));
}
// Invalidates nodes in renderer cache (if any)
AbstractNavTreeCellRenderer renderer = getTreeCellRenderer();
if (children != null && renderer != null) {
for (Object child : children) {
renderer.invalidateCache((N) child);
}
}
}
@Override
public void treeNodesChanged(TreeModelEvent e) {
if (!checkModel()) {
// no model
return;
}
N source = getNode(e.getTreePath());
Object[] children = e.getChildren();
if (log.isDebugEnabled()) {
log.debug(getMessage("changed ", source, children));
}
// ask to populate modified child nodes
populateNode(null, children, false);
}
@Override
public void treeStructureChanged(TreeModelEvent e) {
if (!checkModel()) {
// no model
return;
}
N source = getNode(e.getTreePath());
Object[] children = e.getChildren();
if (log.isDebugEnabled()) {
log.debug(getMessage("structure changed", source, children));
}
// ask to populate structure modified node and nodes recursively
populateNode(source, children, true);
}
protected String getMessage(String action, N source, Object[] children) {
StringBuilder sb = new StringBuilder();
sb.append("==== Nodes ");
sb.append(action);
sb.append(" =================");
sb.append("\nsource : ").append(source);
sb.append("\nnb nodes : ");
sb.append(children == null ? 0 : children.length);
if (children != null) {
int i = 0;
for (Object child : children) {
sb.append("\n [");
sb.append(i++);
sb.append("] - ");
sb.append(child);
}
}
return sb.toString();
}
};
}
/**
* Obtains the attached data provider used to populate and render nodes.
*
* @return the attached data provider
*/
protected NavDataProvider getDataProvider() {
return dataProvider;
}
/**
* Obtains the model.
*
* @return the internal tree model or {@code null} if none was created.
*/
public M getModel() {
return bridge.getModel();
}
/**
* Obtains the bridge .
*
* @return the internal bridge used by helper.
*/
protected B getBridge() {
return bridge;
}
/**
* Obtains the ui associated with model in helper.
*
* @return the ui (or {@code null} if no ui attached)
*/
public U getUI() {
return ui;
}
public N getRootNode() {
if (!checkModel()) {
return null;
}
return bridge.getRoot();
}
/**
* Obtains the path of ids fro the root node to the selected node on the
* registred tree.
*
* @return the array of ids from root node to selected node.
*/
public String[] getSelectedIds() {
List result = new ArrayList<>();
N selectedNode = getSelectedNode();
while (selectedNode != null && !selectedNode.isRoot()) {
result.add(selectedNode.getId());
selectedNode = selectedNode.getParent();
}
Collections.reverse(result);
return result.toArray(new String[result.size()]);
}
/**
* Registers the given {@code tree} for this helper.
*
* Note: as a side-effect, it will register (if required) the
* {@link #expandListener} listener and the {@link #selectionListener}.
*
* @param tree the tree to register
* @param addExpandTreeListener a flag to add expand listener
*/
public void setUI(U tree,
boolean addExpandTreeListener) {
setUI(tree, addExpandTreeListener, null);
}
/**
* Registers the given {@code tree} for this helper.
*
* Note: as a side-effect, it will register (if required) the
* {@link #expandListener} listener and the {@link #selectionListener}.
*
* @param tree the tree to register
* @param addExpandTreeListener a flag to add expand listener
* @param listener the optional selection listener to add
*/
public void setUI(U tree,
boolean addExpandTreeListener,
TreeSelectionListener listener) {
setUI(tree, addExpandTreeListener, true, listener);
}
/**
* Registers the given {@code tree} for this helper.
*
* Note: as a side-effect, it will register (if required) the
* {@link #expandListener} listener and (if required) the
* {@link #selectionListener}.
*
* @param tree the tree to register
* @param addExpandTreeListener a flag to add expand listener
* @param addOneClickSelectionListener a flag to expend when selection
* @param listener the optional selection listener to add
*/
public void setUI(U tree,
boolean addExpandTreeListener,
boolean addOneClickSelectionListener,
TreeSelectionListener listener) {
setUI(tree,
addExpandTreeListener,
addOneClickSelectionListener,
listener,
null
);
}
/**
* Registers the {@code dataProvider} for the helper.
*
* Node: As a side-effect, the provider will be propagate to the
* renderer of the registred tree (if any).
*
* @param dataProvider the data provider to use
*/
public void setDataProvider(NavDataProvider dataProvider) {
this.dataProvider = dataProvider;
AbstractNavTreeCellRenderer renderer = getTreeCellRenderer();
if (renderer != null) {
// dispatch provider to renderer
renderer.setDataProvider(dataProvider);
}
}
/**
* Inserts the given node to the given {@code parentNode}.
*
* The node will be added to his parent, then creation listeners will be
* fired.
*
* @param parentNode the parent node where to insert the new node *
* @param newNode the node to insert
*/
public void insertNode(N parentNode, N newNode) {
parentNode.add(newNode);
bridge.notifyNodeInserted(newNode);
}
/**
* Inserts the given node to the given {@code parentNode}.
*
* The node will be added to his parent, then creation listeners will be
* fired.
*
* @param parentNode the parent node where to insert the new node *
* @param newNode the node to insert
* @param position position of node is inserted
*/
public void insertNode(N parentNode, N newNode, int position) {
parentNode.insert(newNode, position);
bridge.notifyNodeInserted(newNode);
}
/**
* Removes the given {@code node} from the registred tree model and returns
* his parent.
*
* @param node the node to remove
* @return the parent node of the removed node.
*/
public N removeNode(N node) {
N parentNode = node.getParent();
bridge.removeNodeFromParent(node);
return parentNode;
}
/**
* Moves the given {@code node} to the new {@code position}.
*
* @param parentNode the parent node
* @param node the node to move
* @param position the new position of the node
*/
public void moveNode(N parentNode, N node, int position) {
parentNode.remove(node);
parentNode.insert(node, position);
bridge.nodeStructureChanged(parentNode);
}
/**
* Refreshs the given {@code node}.
*
* If flag {@code deep} is set to {@code true}, then it will refresh
* recursively children nodes.
*
* Note:As a side-effect, evvery node involved will become
* {@code dirty}.
*
* @param node the node to refresh
* @param deep un flag pour activer la repainte de la descendance du
* noeud
* @see NavNode#isDirty()
*/
@SuppressWarnings({"unchecked"})
public void refreshNode(N node, boolean deep) {
if (log.isDebugEnabled()) {
log.debug("Will refresh (deep ? " + deep + ") node " + node);
}
bridge.nodeChanged(node);
if (deep) {
// repaint childs nodes
Enumeration e =(Enumeration) node.children();
while (e.hasMoreElements()) {
N child = e.nextElement();
refreshNode(child, true);
}
}
}
/**
* To load all nodes of a model.
*
* @param node the root node to load
* @param dataProvider the data provider used to populate nodes
*/
@SuppressWarnings({"unchecked"})
public void loadAllNodes(N node, NavDataProvider dataProvider) {
if (!checkModel()) {
return;
}
if (!node.isLoaded()) {
node.populateChilds(getBridge(), dataProvider);
Enumeration extends NavNode, ?>> enumeration = (Enumeration)node.children();
while (enumeration.hasMoreElements()) {
N jaxxNode = (N) enumeration.nextElement();
loadAllNodes(jaxxNode, dataProvider);
}
}
}
/**
* Selects the parent of the currently selected node.
*
* Note: If selection is empty, then throws a NPE.
*
* @throws NullPointerException if selection is empty
*/
public void selectParentNode() throws NullPointerException {
N node = getSelectedNode();
if (node == null) {
// pas de noeud selectionne
throw new NullPointerException("no selected node in context");
}
node = node.getParent();
selectNode(node);
}
/**
* Selects the given {@code node} in the registred tree.
*
* @param node the node to select
*/
public void selectNode(N node) {
if (!checkModel()) {
// no model
return;
}
if (log.isDebugEnabled()) {
log.debug("try to select node [" + node + "]");
}
TreePath path = new TreePath(bridge.getPathToRoot(node));
addSelectionPath(path);
scrollPathToVisible(path);
}
/**
* Selects the given {@code nodes} in the registred tree.
*
* @param nodes the nodes to select
*/
public void selectNodes(List nodes) {
if (!checkModel()) {
// no model
return;
}
List paths = new ArrayList<>();
for (N node : nodes) {
paths.add(new TreePath(bridge.getPathToRoot(node)));
}
addSelectionPaths(paths.toArray(new TreePath[paths.size()]));
}
/**
* Unselects the given {@code node} in the registred tree.
*
* @param node the node to select
*/
public void unSelectNode(N node) {
if (!checkModel()) {
// no model
return;
}
if (log.isDebugEnabled()) {
log.debug("try to select node [" + node + "]");
}
TreePath path = new TreePath(bridge.getPathToRoot(node));
removeSelectionPath(path);
}
/**
* Selects the given {@code nodes} in the registred tree.
*
* @param nodes the nodes to select
*/
public void unSelectNodes(List nodes) {
if (!checkModel()) {
// no model
return;
}
for (N node : nodes) {
unSelectNode(node);
}
}
/**
* Selects the node described by his given {@code path} of ids.
*
* @param path the absolute path of ids from root node to node to select.
*/
public void selectNode(String... path) {
if (!checkModel()) {
// no model
return;
}
if (log.isDebugEnabled()) {
log.debug("try to select node from ids " + Arrays.toString(path));
}
N root = bridge.getRoot();
N node = findNode(root, path);
if (log.isDebugEnabled()) {
log.debug("selected node [" + node + "]");
}
if (node != null) {
selectNode(node);
}
}
/**
* Finds a node from the given root {@code node}, applying the path given
* by {@code ids}.
*
* @param node the starting node
* @param ids the path of ids to apply on the node.
* @return the find node or {@code null} if no node matchs.
*/
public N findNode(N node, String... ids) {
if (!checkModel()) {
// no model
return null;
}
N result = null;
for (String id : ids) {
result = node.findNodeById(id, getBridge(), getDataProvider());
if (result == null) {
// un des noeud n'a pas ete trouve, on sort
break;
}
node = result;
}
return result;
}
/**
* Finds a node from the given root {@code node}, and return child searched
*
* @param node the starting node
* @param id id of searched child
* @return the find node or {@code null} if no node matchs.
*/
public N getChild(N node, String id) {
if (!checkModel()) {
// no model
return null;
}
return node.getChild(id, getBridge(), getDataProvider());
}
/**
* Checks if internal model was created.
*
* @return {@code true} if model was created, {@code false} otherwise.
*/
protected boolean checkModel() {
if (getModel() == null) {
// no model set,
if (log.isWarnEnabled()) {
log.warn("No model set in " + this);
}
return false;
}
// model is set
return true;
}
/**
* Populates nodes.
*
* If {@code node} is not {@code null}, then populate it.
*
* If {@code children} is not {@code null}, then populate them, moreover
* if {@code recurse} is set to {@code true} then do a recurse refresh on
* children.
*
* @param node the parent node to populate (optional)
* @param children the child nodes to populate (optional)
* @param recurse flag sets to {@code true} if should do recurse refresh on
* given {@code children} nodes.
*/
@SuppressWarnings({"unchecked"})
protected void populateNode(N node,
Object[] children,
boolean recurse) {
NavDataProvider dataProvider = getDataProvider();
if (node != null) {
if (log.isDebugEnabled()) {
log.debug("Will populate node : " + node);
}
node.populateNode(getBridge(), dataProvider, false);
}
if (children != null) {
for (Object o : children) {
N child = (N) o;
if (log.isDebugEnabled()) {
log.debug("Will populate child node : " + child);
}
child.populateNode(getBridge(), dataProvider, recurse);
}
}
}
/**
* Convinient method to objet the casted node of a {@link TreePath}.
*
* @param path the path contaning the node.
* @return the casted node from the path.
*/
@SuppressWarnings({"unchecked"})
protected N getNode(TreePath path) {
return (N) path.getLastPathComponent();
}
protected void setUI(U ui) {
this.ui = ui;
}
}