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