AbstractNodeModel.java
/*******************************************************************************
* Copyhacked (H) 2012-2025.
* This program and the accompanying materials
* are made available under no term at all, use it like
* you want, but share and discuss it
* every time possible with every body.
*
* Contributors:
* ron190 at ymail dot com - initial implementation
******************************************************************************/
package com.jsql.view.swing.tree.model;
import com.jsql.model.bean.database.AbstractElementDatabase;
import com.jsql.model.suspendable.AbstractSuspendable;
import com.jsql.util.I18nUtil;
import com.jsql.util.LogLevelUtil;
import com.jsql.util.StringUtil;
import com.jsql.view.swing.tree.action.ActionLoadStop;
import com.jsql.view.swing.tree.action.ActionPauseUnpause;
import com.jsql.view.swing.tree.ImageOverlap;
import com.jsql.view.swing.tree.PanelNode;
import com.jsql.view.swing.tree.custom.JPopupMenuCustomExtract;
import com.jsql.view.swing.util.I18nViewUtil;
import com.jsql.view.swing.util.MediatorHelper;
import com.jsql.view.swing.util.UiStringUtil;
import com.jsql.view.swing.util.UiUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.MouseEvent;
/**
* Model adding functional layer to the node ; used by renderer and editor.
*/
public abstract class AbstractNodeModel {
/**
* Log4j logger sent to view.
*/
private static final Logger LOGGER = LogManager.getRootLogger();
private static final String TREE_BACKGROUND = "Tree.background";
/**
* Element from injection model in a linked list.
*/
private AbstractElementDatabase elementDatabase;
/**
* Text for empty node.
*/
private String textEmptyNode;
/**
* Current item injection progress regarding total number of elements.
*/
private int indexProgress = 0;
/**
* Used by checkbox node ; true if checkbox is checked, false otherwise.
*/
private boolean isSelected = false;
/**
* Indicates if process on current node is running.
*/
private boolean isRunning = false;
/**
* True if current table node has checkbox selected, false otherwise.
* Used to display popup menu and block injection start if no checkbox selected.
*/
private boolean isAnyCheckboxSelected = false;
/**
* True if current node has already been filled, false otherwise.
* Used to display correct popup menu and block injection start if already done.
*/
private boolean isLoaded = false;
/**
* True if current node is loading with unknown total number, false otherwise.
* Used to display loader.
*/
private boolean isProgressing = false;
/**
* True if current node is loading with total number known, false otherwise.
* Used to display progress bar.
*/
private boolean isLoading = false;
private PanelNode panelNode;
private boolean isEdited;
/**
* Create a functional model for tree node.
* @param elementDatabase Database structural component
*/
protected AbstractNodeModel(AbstractElementDatabase elementDatabase) {
this.elementDatabase = elementDatabase;
}
/**
* Create an empty model for tree node.
* @param emptyObject Empty tree default node
*/
protected AbstractNodeModel(String emptyObject) {
this.textEmptyNode = emptyObject;
}
/**
* Display a popupmenu on mouse right click if needed.
* @param tablePopupMenu Menu to display
* @param path Treepath of current node
*/
protected abstract void buildMenu(JPopupMenuCustomExtract tablePopupMenu, TreePath path);
/**
* Check if menu should be opened.
* i.e: does not show menu on database except during injection.
* @return True if popupup should be opened, false otherwise
*/
public abstract boolean isPopupDisplayable();
/**
* Get icon displayed next to the node text.
* @param isLeaf True will display an arrow icon, false won't
* @return Icon to display
*/
protected abstract Icon getLeafIcon(boolean isLeaf);
/**
* Run injection process (see GUIMediator.model().dao).
* Used by database and table nodes.
*/
public abstract void runAction();
/**
* Display a popup menu for a database or table node.
* @param currentTableNode Current node
* @param path Path of current node
*/
public void showPopup(DefaultMutableTreeNode currentTableNode, TreePath path, MouseEvent e) {
var popupMenu = new JPopupMenuCustomExtract();
AbstractSuspendable suspendableTask = MediatorHelper.model().getMediatorUtils().getThreadUtil().get(this.elementDatabase);
this.initItemLoadPause(currentTableNode, popupMenu, suspendableTask);
this.initItemRenameReload(currentTableNode, path, popupMenu);
this.buildMenu(popupMenu, path);
this.displayPopupMenu(e, popupMenu);
}
private void displayPopupMenu(MouseEvent e, JPopupMenuCustomExtract popupMenu) {
popupMenu.applyComponentOrientation(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()));
popupMenu.show(
MediatorHelper.treeDatabase(),
ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
? e.getX() - popupMenu.getWidth()
: e.getX(),
e.getY()
);
popupMenu.setLocation(
ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
? e.getXOnScreen() - popupMenu.getWidth()
: e.getXOnScreen(),
e.getYOnScreen()
);
}
private void initItemRenameReload(DefaultMutableTreeNode currentTableNode, TreePath path, JPopupMenuCustomExtract popupMenu) {
String textReload;
if (this instanceof NodeModelDatabase) {
textReload = I18nViewUtil.valueByKey("RELOAD_TABLES");
} else if (this instanceof NodeModelTable) {
textReload = I18nViewUtil.valueByKey("RELOAD_COLUMNS");
} else {
textReload = "?";
}
JMenuItem menuItemReload = new JMenuItem(textReload);
menuItemReload.setEnabled(!this.isRunning);
menuItemReload.addActionListener(actionEvent -> this.runAction());
JMenuItem menuItemRename = new JMenuItem(I18nViewUtil.valueByKey("RENAME_NODE"));
menuItemRename.setEnabled(!this.isRunning);
menuItemRename.addActionListener(actionEvent -> {
AbstractNodeModel nodeModel = (AbstractNodeModel) currentTableNode.getUserObject();
nodeModel.setIsEdited(true);
this.getPanel().getNodeLabel().setVisible(false);
this.getPanel().getTextFieldEditable().setVisible(true);
MediatorHelper.treeDatabase().setSelectionPath(path);
});
popupMenu.add(new JSeparator());
popupMenu.add(menuItemRename);
popupMenu.add(menuItemReload);
}
private void initItemLoadPause(
DefaultMutableTreeNode currentTableNode,
JPopupMenuCustomExtract popupMenu,
AbstractSuspendable suspendableTask
) {
JMenuItem menuItemLoad = new JMenuItem(
this.isRunning
? I18nViewUtil.valueByKey("THREAD_STOP")
: I18nViewUtil.valueByKey("THREAD_LOAD"),
'o'
);
if (!this.isAnyCheckboxSelected && !this.isRunning) {
menuItemLoad.setEnabled(false);
}
menuItemLoad.addActionListener(new ActionLoadStop(this, currentTableNode));
JMenuItem menuItemPause = new JMenuItem(
// Report #133: ignore if thread not found
suspendableTask != null && suspendableTask.isPaused()
? I18nViewUtil.valueByKey("THREAD_RESUME")
: I18nViewUtil.valueByKey("THREAD_PAUSE"),
's'
);
if (!this.isRunning) {
menuItemPause.setEnabled(false);
}
menuItemPause.addActionListener(new ActionPauseUnpause(this));
popupMenu.add(menuItemLoad);
popupMenu.add(menuItemPause);
}
/**
* Draw the panel component based on node model.
*/
public Component getComponent(
final JTree tree,
Object nodeRenderer,
final boolean isSelected,
boolean isLeaf,
boolean hasFocus
) {
DefaultMutableTreeNode currentNode = (DefaultMutableTreeNode) nodeRenderer;
this.panelNode = new PanelNode(tree, currentNode);
if (isSelected) {
this.panelNode.setBackground(
hasFocus
? UIManager.getColor("Tree.selectionBackground")
: UIManager.getColor("Tree.selectionInactiveBackground")
); // required for transparency
} else {
this.panelNode.setBackground(UIManager.getColor(AbstractNodeModel.TREE_BACKGROUND)); // required for transparency
}
this.initIcon(isLeaf);
AbstractNodeModel nodeModel = (AbstractNodeModel) currentNode.getUserObject();
this.initEditable(nodeModel.isEdited);
this.initLabel(isSelected, hasFocus, nodeModel.isEdited);
this.initProgress(currentNode);
return this.panelNode;
}
private void initIcon(boolean isLeaf) {
this.panelNode.showIcon();
this.panelNode.setIconNode(this.getLeafIcon(isLeaf));
}
private void initProgress(DefaultMutableTreeNode currentNode) {
if (this.isLoading) {
this.displayProgress(this.panelNode, currentNode);
this.panelNode.hideIcon();
} else if (this.isProgressing) {
this.panelNode.showLoader();
this.panelNode.hideIcon();
AbstractSuspendable suspendableTask = MediatorHelper.model().getMediatorUtils().getThreadUtil().get(this.elementDatabase);
if (suspendableTask != null && suspendableTask.isPaused()) {
this.panelNode.setLoaderIcon(new ImageOverlap(UiUtil.HOURGLASS.getIcon(), UiUtil.PATH_PAUSE));
}
}
}
private void initLabel(final boolean isSelected, boolean hasFocus, boolean isEdited) {
// Fix #90521: NullPointerException on setText()
JLabel nodeLabel = this.panelNode.getNodeLabel();
try {
nodeLabel.setText(UiStringUtil.detectUtf8Html(this.toString()));
} catch (NullPointerException e) {
LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
}
nodeLabel.setVisible(!isEdited);
if (isSelected) {
if (hasFocus) {
nodeLabel.setForeground(UIManager.getColor("Tree.selectionForeground")); // required by macOS light (opposite text color)
nodeLabel.setBackground(UIManager.getColor("Tree.selectionBackground"));
} else {
nodeLabel.setForeground(UIManager.getColor("Tree.selectionInactiveForeground")); // required by macOS light (opposite text color)
nodeLabel.setBackground(UIManager.getColor("Tree.selectionInactiveBackground"));
}
nodeLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
} else {
if (hasFocus) {
nodeLabel.setBackground(UIManager.getColor("Tree.foreground")); // required by macOS light (opposite text color)
nodeLabel.setBackground(UIManager.getColor(AbstractNodeModel.TREE_BACKGROUND));
nodeLabel.setBorder(new LineBorder(UIManager.getColor("Tree.selectionBorderColor"), 1, false));
} else {
nodeLabel.setBackground(UIManager.getColor("Tree.foreground")); // required by macOS light (opposite text color)
nodeLabel.setBackground(UIManager.getColor(AbstractNodeModel.TREE_BACKGROUND));
nodeLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
}
}
}
private void initEditable(boolean isEdited) {
if (StringUtil.isUtf8(this.getElementDatabase().toString())) {
this.panelNode.getTextFieldEditable().setFont(UiUtil.FONT_MONO_ASIAN);
} else {
this.panelNode.getTextFieldEditable().setFont(UiUtil.FONT_NON_MONO);
}
this.panelNode.getTextFieldEditable().setText(StringUtil.detectUtf8(this.getElementDatabase().toString()));
this.panelNode.getTextFieldEditable().setVisible(isEdited);
}
/**
* Update progressbar ; display the pause icon if node is paused.
* @param panelNode Panel that contains the bar to update
* @param currentNode Functional node model object
*/
protected void displayProgress(PanelNode panelNode, DefaultMutableTreeNode currentNode) {
int dataCount = this.elementDatabase.getChildCount();
panelNode.getProgressBar().setMaximum(dataCount);
panelNode.getProgressBar().setValue(this.indexProgress);
panelNode.getProgressBar().setVisible(true);
// Report #135: ignore if thread not found
AbstractSuspendable suspendableTask = MediatorHelper.model().getMediatorUtils().getThreadUtil().get(this.elementDatabase);
if (suspendableTask != null && suspendableTask.isPaused()) {
panelNode.getProgressBar().pause();
}
}
@Override
public String toString() {
return this.elementDatabase != null ? this.elementDatabase.getLabelWithCount() : this.textEmptyNode;
}
// Getter and setter
/**
* Get the database parent of current node.
* @return Parent
*/
protected AbstractElementDatabase getParent() {
return this.elementDatabase.getParent();
}
public AbstractElementDatabase getElementDatabase() {
return this.elementDatabase;
}
public void setIndexProgress(int indexProgress) {
this.indexProgress = indexProgress;
}
public boolean isSelected() {
return this.isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
public boolean isRunning() {
return this.isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public void setIsAnyCheckboxSelected(boolean isAnyCheckboxSelected) {
this.isAnyCheckboxSelected = isAnyCheckboxSelected;
}
public boolean isLoaded() {
return this.isLoaded;
}
public void setLoaded(boolean isLoaded) {
this.isLoaded = isLoaded;
}
public void setProgressing(boolean isProgressing) {
this.isProgressing = isProgressing;
}
public void setLoading(boolean isLoading) {
this.isLoading = isLoading;
}
public PanelNode getPanel() {
return this.panelNode;
}
public void setIsEdited(boolean isEdited) {
this.isEdited = isEdited;
}
public void setText(String textI18n) {
this.textEmptyNode = textI18n;
}
}