All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.jaxx.runtime.swing.nav.NavHelper Maven / Gradle / Ivy

There is a newer version: 3.1.5
Show newest version
/*
 * #%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> childLoadors; protected static Set> 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> 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> 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy