View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.view.swing.tree.model;
12  
13  import com.jsql.model.bean.database.AbstractElementDatabase;
14  import com.jsql.model.suspendable.AbstractSuspendable;
15  import com.jsql.util.I18nUtil;
16  import com.jsql.util.LogLevelUtil;
17  import com.jsql.util.StringUtil;
18  import com.jsql.view.swing.tree.action.ActionLoadStop;
19  import com.jsql.view.swing.tree.action.ActionPauseUnpause;
20  import com.jsql.view.swing.tree.ImageOverlap;
21  import com.jsql.view.swing.tree.PanelNode;
22  import com.jsql.view.swing.tree.custom.JPopupMenuCustomExtract;
23  import com.jsql.view.swing.util.I18nViewUtil;
24  import com.jsql.view.swing.util.MediatorHelper;
25  import com.jsql.view.swing.util.UiStringUtil;
26  import com.jsql.view.swing.util.UiUtil;
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  
30  import javax.swing.*;
31  import javax.swing.border.LineBorder;
32  import javax.swing.tree.DefaultMutableTreeNode;
33  import javax.swing.tree.TreePath;
34  import java.awt.*;
35  import java.awt.event.MouseEvent;
36  
37  /**
38   * Model adding functional layer to the node ; used by renderer and editor.
39   */
40  public abstract class AbstractNodeModel {
41  
42      private static final Logger LOGGER = LogManager.getRootLogger();
43      private static final String TREE_BACKGROUND = "Tree.background";
44  
45      /**
46       * Element from injection model in a linked list.
47       */
48      private AbstractElementDatabase elementDatabase;
49  
50      /**
51       * Text for empty node.
52       */
53      private String textEmptyNode;
54  
55      /**
56       * Current item injection progress regarding total number of elements.
57       */
58      private int indexProgress = 0;
59  
60      /**
61       * Used by checkbox node ; true if checkbox is checked, false otherwise.
62       */
63      private boolean isSelected = false;
64  
65      /**
66       * Indicates if process on current node is running.
67       */
68      private boolean isRunning = false;
69  
70      /**
71       * True if current table node has checkbox selected, false otherwise.
72       * Used to display popup menu and block injection start if no checkbox selected.
73       */
74      private boolean isAnyCheckboxSelected = false;
75  
76      /**
77       * True if current node has already been filled, false otherwise.
78       * Used to display correct popup menu and block injection start if already done.
79       */
80      private boolean isLoaded = false;
81  
82      /**
83       * True if current node is loading with unknown total number, false otherwise.
84       * Used to display loader.
85       */
86      private boolean isProgressing = false;
87  
88      /**
89       * True if current node is loading with total number known, false otherwise.
90       * Used to display progress bar.
91       */
92      private boolean isLoading = false;
93      
94      private PanelNode panelNode;
95  
96      private boolean isEdited;
97  
98      /**
99       * Create a functional model for tree node.
100      * @param elementDatabase Database structural component
101      */
102     protected AbstractNodeModel(AbstractElementDatabase elementDatabase) {
103         this.elementDatabase = elementDatabase;
104     }
105 
106     /**
107      * Create an empty model for tree node.
108      * @param emptyObject Empty tree default node
109      */
110     protected AbstractNodeModel(String emptyObject) {
111         this.textEmptyNode = emptyObject;
112     }
113     
114     /**
115      * Display a popupmenu on mouse right click if needed.
116      * @param tablePopupMenu Menu to display
117      * @param path Treepath of current node
118      */
119     protected abstract void buildMenu(JPopupMenuCustomExtract tablePopupMenu, TreePath path);
120     
121     /**
122      * Check if menu should be opened.
123      * i.e: does not show menu on database except during injection.
124      * @return True if popupup should be opened, false otherwise
125      */
126     public abstract boolean isPopupDisplayable();
127     
128     /**
129      * Get icon displayed next to the node text.
130      * @param isLeaf True will display an arrow icon, false won't
131      * @return Icon to display
132      */
133     protected abstract Icon getLeafIcon(boolean isLeaf);
134     
135     /**
136      * Run injection process (see GUIMediator.model().dao).
137      * Used by database and table nodes.
138      */
139     public abstract void runAction();
140 
141     /**
142      * Display a popup menu for a database or table node.
143      * @param currentTableNode Current node
144      * @param path Path of current node
145      */
146     public void showPopup(DefaultMutableTreeNode currentTableNode, TreePath path, MouseEvent e) {
147         var popupMenu = new JPopupMenuCustomExtract();
148         AbstractSuspendable suspendableTask = MediatorHelper.model().getMediatorUtils().getThreadUtil().get(this.elementDatabase);
149 
150         this.initItemLoadPause(currentTableNode, popupMenu, suspendableTask);
151         this.initItemRenameReload(currentTableNode, path, popupMenu);
152         this.buildMenu(popupMenu, path);
153         this.displayPopupMenu(e, popupMenu);
154     }
155 
156     private void displayPopupMenu(MouseEvent e, JPopupMenuCustomExtract popupMenu) {
157         popupMenu.applyComponentOrientation(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()));
158 
159         popupMenu.show(
160             MediatorHelper.treeDatabase(),
161             ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
162             ? e.getX() - popupMenu.getWidth()
163             : e.getX(),
164             e.getY()
165         );
166         
167         popupMenu.setLocation(
168             ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
169             ? e.getXOnScreen() - popupMenu.getWidth()
170             : e.getXOnScreen(),
171             e.getYOnScreen()
172         );
173     }
174 
175     private void initItemRenameReload(DefaultMutableTreeNode currentTableNode, TreePath path, JPopupMenuCustomExtract popupMenu) {
176         String textReload;
177         
178         if (this instanceof NodeModelDatabase) {
179             textReload = I18nViewUtil.valueByKey("RELOAD_TABLES");
180         } else if (this instanceof NodeModelTable) {
181             textReload = I18nViewUtil.valueByKey("RELOAD_COLUMNS");
182         } else {
183             textReload = "?";
184         }
185         
186         JMenuItem menuItemReload = new JMenuItem(textReload);
187         menuItemReload.setEnabled(!this.isRunning);
188         menuItemReload.addActionListener(actionEvent -> this.runAction());
189         
190         JMenuItem menuItemRename = new JMenuItem(I18nViewUtil.valueByKey("RENAME_NODE"));
191         menuItemRename.setEnabled(!this.isRunning);
192         menuItemRename.addActionListener(actionEvent -> {
193             AbstractNodeModel nodeModel = (AbstractNodeModel) currentTableNode.getUserObject();
194             nodeModel.setIsEdited(true);
195 
196             this.getPanel().getNodeLabel().setVisible(false);
197             this.getPanel().getTextFieldEditable().setVisible(true);
198             
199             MediatorHelper.treeDatabase().setSelectionPath(path);
200         });
201         
202         popupMenu.add(new JSeparator());
203         popupMenu.add(menuItemRename);
204         popupMenu.add(menuItemReload);
205     }
206 
207     private void initItemLoadPause(
208         DefaultMutableTreeNode currentTableNode,
209         JPopupMenuCustomExtract popupMenu,
210         AbstractSuspendable suspendableTask
211     ) {
212         JMenuItem menuItemLoad = new JMenuItem(
213             this.isRunning
214             ? I18nViewUtil.valueByKey("THREAD_STOP")
215             : I18nViewUtil.valueByKey("THREAD_LOAD"),
216             'o'
217         );
218         if (!this.isAnyCheckboxSelected && !this.isRunning) {
219             menuItemLoad.setEnabled(false);
220         }
221         menuItemLoad.addActionListener(new ActionLoadStop(this, currentTableNode));
222 
223         JMenuItem menuItemPause = new JMenuItem(
224             // Report #133: ignore if thread not found
225             suspendableTask != null && suspendableTask.isPaused()
226             ? I18nViewUtil.valueByKey("THREAD_RESUME")
227             : I18nViewUtil.valueByKey("THREAD_PAUSE"),
228             's'
229         );
230         if (!this.isRunning) {
231             menuItemPause.setEnabled(false);
232         }
233         menuItemPause.addActionListener(new ActionPauseUnpause(this));
234         
235         popupMenu.add(menuItemLoad);
236         popupMenu.add(menuItemPause);
237     }
238     
239     /**
240      * Draw the panel component based on node model.
241      */
242     public Component getComponent(
243         final JTree tree,
244         Object nodeRenderer,
245         final boolean isSelected,
246         boolean isLeaf,
247         boolean hasFocus
248     ) {
249         DefaultMutableTreeNode currentNode = (DefaultMutableTreeNode) nodeRenderer;
250         this.panelNode = new PanelNode(tree, currentNode);
251 
252         if (isSelected) {
253             this.panelNode.setBackground(
254                 hasFocus
255                 ? UIManager.getColor("Tree.selectionBackground")
256                 : UIManager.getColor("Tree.selectionInactiveBackground")
257             );  // required for transparency
258         } else {
259             this.panelNode.setBackground(UIManager.getColor(AbstractNodeModel.TREE_BACKGROUND));  // required for transparency
260         }
261 
262         this.initIcon(isLeaf);
263         
264         AbstractNodeModel nodeModel = (AbstractNodeModel) currentNode.getUserObject();
265         this.initEditable(nodeModel.isEdited);
266         this.initLabel(isSelected, hasFocus, nodeModel.isEdited);
267         this.initProgress(currentNode);
268         
269         return this.panelNode;
270     }
271 
272     private void initIcon(boolean isLeaf) {
273         this.panelNode.showIcon();
274         this.panelNode.setIconNode(this.getLeafIcon(isLeaf));
275     }
276 
277     private void initProgress(DefaultMutableTreeNode currentNode) {
278         if (this.isLoading) {
279             this.displayProgress(this.panelNode, currentNode);
280             this.panelNode.hideIcon();
281         } else if (this.isProgressing) {
282             this.panelNode.showLoader();
283             this.panelNode.hideIcon();
284 
285             AbstractSuspendable suspendableTask = MediatorHelper.model().getMediatorUtils().getThreadUtil().get(this.elementDatabase);
286             if (suspendableTask != null && suspendableTask.isPaused()) {
287                 this.panelNode.setLoaderIcon(new ImageOverlap(UiUtil.HOURGLASS.getIcon(), UiUtil.PATH_PAUSE));
288             }
289         }
290     }
291 
292     private void initLabel(final boolean isSelected, boolean hasFocus, boolean isEdited) {
293         // Fix #90521: NullPointerException on setText()
294         JLabel nodeLabel = this.panelNode.getNodeLabel();
295         try {
296             nodeLabel.setText(UiStringUtil.detectUtf8Html(this.toString()));
297         } catch (NullPointerException e) {
298             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
299         }
300         nodeLabel.setVisible(!isEdited);
301 
302         if (isSelected) {
303             if (hasFocus) {
304                 nodeLabel.setForeground(UIManager.getColor("Tree.selectionForeground"));  // required by macOS light (opposite text color)
305                 nodeLabel.setBackground(UIManager.getColor("Tree.selectionBackground"));
306             } else {
307                 nodeLabel.setForeground(UIManager.getColor("Tree.selectionInactiveForeground"));  // required by macOS light (opposite text color)
308                 nodeLabel.setBackground(UIManager.getColor("Tree.selectionInactiveBackground"));
309             }
310             nodeLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
311         } else {
312             if (hasFocus) {
313                 nodeLabel.setBackground(UIManager.getColor("Tree.foreground"));  // required by macOS light (opposite text color)
314                 nodeLabel.setBackground(UIManager.getColor(AbstractNodeModel.TREE_BACKGROUND));
315                 nodeLabel.setBorder(new LineBorder(UIManager.getColor("Tree.selectionBorderColor"), 1, false));
316             } else {
317                 nodeLabel.setBackground(UIManager.getColor("Tree.foreground"));  // required by macOS light (opposite text color)
318                 nodeLabel.setBackground(UIManager.getColor(AbstractNodeModel.TREE_BACKGROUND));
319                 nodeLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
320             }
321         }
322     }
323 
324     private void initEditable(boolean isEdited) {
325         if (StringUtil.isUtf8(this.getElementDatabase().toString())) {
326             this.panelNode.getTextFieldEditable().setFont(UiUtil.FONT_MONO_ASIAN);
327         } else {
328             this.panelNode.getTextFieldEditable().setFont(UiUtil.FONT_NON_MONO);
329         }
330         
331         this.panelNode.getTextFieldEditable().setText(StringUtil.detectUtf8(this.getElementDatabase().toString()));
332         this.panelNode.getTextFieldEditable().setVisible(isEdited);
333     }
334     
335     /**
336      * Update progressbar ; display the pause icon if node is paused.
337      * @param panelNode Panel that contains the bar to update
338      * @param currentNode Functional node model object
339      */
340     protected void displayProgress(PanelNode panelNode, DefaultMutableTreeNode currentNode) {
341         int dataCount = this.elementDatabase.getChildCount();
342         panelNode.getProgressBar().setMaximum(dataCount);
343         panelNode.getProgressBar().setValue(this.indexProgress);
344         panelNode.getProgressBar().setVisible(true);
345         
346         // Report #135: ignore if thread not found
347         AbstractSuspendable suspendableTask = MediatorHelper.model().getMediatorUtils().getThreadUtil().get(this.elementDatabase);
348         if (suspendableTask != null && suspendableTask.isPaused()) {
349             panelNode.getProgressBar().pause();
350         }
351     }
352     
353     @Override
354     public String toString() {
355         return this.elementDatabase != null ? this.elementDatabase.getLabelWithCount() : this.textEmptyNode;
356     }
357     
358     
359     // Getter and setter
360 
361     /**
362      * Get the database parent of current node.
363      * @return Parent
364      */
365     protected AbstractElementDatabase getParent() {
366         return this.elementDatabase.getParent();
367     }
368 
369     public AbstractElementDatabase getElementDatabase() {
370         return this.elementDatabase;
371     }
372 
373     public void setIndexProgress(int indexProgress) {
374         this.indexProgress = indexProgress;
375     }
376 
377     public boolean isSelected() {
378         return this.isSelected;
379     }
380 
381     public void setSelected(boolean isSelected) {
382         this.isSelected = isSelected;
383     }
384 
385     public boolean isRunning() {
386         return this.isRunning;
387     }
388 
389     public void setRunning(boolean isRunning) {
390         this.isRunning = isRunning;
391     }
392 
393     public void setIsAnyCheckboxSelected(boolean isAnyCheckboxSelected) {
394         this.isAnyCheckboxSelected = isAnyCheckboxSelected;
395     }
396 
397     public boolean isLoaded() {
398         return this.isLoaded;
399     }
400 
401     public void setLoaded(boolean isLoaded) {
402         this.isLoaded = isLoaded;
403     }
404 
405     public void setProgressing(boolean isProgressing) {
406         this.isProgressing = isProgressing;
407     }
408 
409     public void setLoading(boolean isLoading) {
410         this.isLoading = isLoading;
411     }
412 
413     public PanelNode getPanel() {
414         return this.panelNode;
415     }
416 
417     public void setIsEdited(boolean isEdited) {
418         this.isEdited = isEdited;
419     }
420     
421     public void setText(String textI18n) {
422         this.textEmptyNode = textI18n;
423     }
424 }