AbstractNodeModel.java

/*******************************************************************************
 * Copyhacked (H) 2012-2020.
 * This program and the accompanying materials
 * are made available under no term at all, use it like
 * you want, but share and discuss about 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.menubar.JMenuItemWithMargin;
import com.jsql.view.swing.tree.*;
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.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();
    
    /**
     * 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 isContainingSelection = 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 gif 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.initializeItemLoadPause(currentTableNode, popupMenu, suspendableTask);
        this.initializeItemRenameReload(currentTableNode, path, popupMenu);

        this.buildMenu(popupMenu, path);
        
        this.displayPopupMenu(e, popupMenu);
    }

    private void displayPopupMenu(MouseEvent e, JPopupMenuCustomExtract popupMenu) {
        
        popupMenu.applyComponentOrientation(ComponentOrientation.getOrientation(I18nUtil.getLocaleDefault()));

        popupMenu.show(
            MediatorHelper.treeDatabase(),
            ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getLocaleDefault()))
            ? e.getX() - popupMenu.getWidth()
            : e.getX(),
            e.getY()
        );
        
        popupMenu.setLocation(
            ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getLocaleDefault()))
            ? e.getXOnScreen() - popupMenu.getWidth()
            : e.getXOnScreen(),
            e.getYOnScreen()
        );
    }

    private void initializeItemRenameReload(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 JMenuItemWithMargin(textReload);

        menuItemReload.setEnabled(!this.isRunning);
        menuItemReload.addActionListener(actionEvent -> AbstractNodeModel.this.runAction());
        
        JMenuItem menuItemRename = new JMenuItemWithMargin(I18nViewUtil.valueByKey("RENAME_NODE"));
        
        menuItemRename.setEnabled(!this.isRunning);
        menuItemRename.addActionListener(actionEvent -> {
            
            AbstractNodeModel nodeModel = (AbstractNodeModel) currentTableNode.getUserObject();
            nodeModel.setIsEdited(true);
            
            AbstractNodeModel.this.getPanel().getLabel().setVisible(false);
            AbstractNodeModel.this.getPanel().getEditable().setVisible(true);
            
            MediatorHelper.treeDatabase().setSelectionPath(path);
        });
        
        popupMenu.add(new JSeparator());
        popupMenu.add(menuItemRename);
        popupMenu.add(menuItemReload);
    }

    private void initializeItemLoadPause(
        DefaultMutableTreeNode currentTableNode,
        JPopupMenuCustomExtract popupMenu,
        AbstractSuspendable suspendableTask
    ) {
        JMenuItem menuItemLoad = new JMenuItemWithMargin(
            this.isRunning
            ? I18nViewUtil.valueByKey("THREAD_STOP")
            : I18nViewUtil.valueByKey("THREAD_LOAD"),
            'o'
        );
        
        if (!this.isContainingSelection && !this.isRunning) {
            menuItemLoad.setEnabled(false);
        }
        
        menuItemLoad.addActionListener(new ActionLoadStop(this, currentTableNode));

        JMenuItem menuItemPause = new JMenuItemWithMargin(
            // 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.
     * @param tree
     * @param nodeRenderer
     * @param isSelected
     * @param isLeaf
     * @param hasFocus
     * @return
     */
    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);

        this.initializeIcon(isLeaf);
        
        AbstractNodeModel nodeModel = (AbstractNodeModel) currentNode.getUserObject();
        
        this.initializeEditable(nodeModel.isEdited);
        this.initializeLabel(isSelected, hasFocus, nodeModel.isEdited);
        this.initializeProgress(currentNode);
        
        return this.panelNode;
    }

    private void initializeIcon(boolean isLeaf) {
        
        this.panelNode.showIcon();
        this.panelNode.setIcon(this.getLeafIcon(isLeaf));
    }

    private void initializeProgress(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()) {
                
                ImageIcon animatedGIFPaused = new ImageOverlap(UiUtil.PATH_PROGRESSBAR, UiUtil.PATH_PAUSE);
                
                animatedGIFPaused.setImageObserver(
                    new ImageObserverAnimated(
                        MediatorHelper.treeDatabase(),
                        currentNode
                    )
                );
                
                this.panelNode.setLoaderIcon(animatedGIFPaused);
            }
        }
    }

    private void initializeLabel(final boolean isSelected, boolean hasFocus, boolean isEdited) {
        
        // Fix #90521: NullPointerException on setText()
        try {
            this.panelNode.getLabel().setText(UiStringUtil.detectUtf8Html(this.toString()));
        } catch (NullPointerException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
        
        this.panelNode.getLabel().setVisible(!isEdited);

        if (isSelected) {
            if (hasFocus) {
                
                this.panelNode.getLabel().setBackground(UiUtil.COLOR_FOCUS_GAINED);
                this.panelNode.getLabel().setBorder(UiUtil.BORDER_FOCUS_GAINED);
                
            } else {
                
                this.panelNode.getLabel().setBackground(UiUtil.COLOR_FOCUS_LOST);
                this.panelNode.getLabel().setBorder(UiUtil.BORDER_FOCUS_LOST);
            }
        } else {
            
            this.panelNode.getLabel().setBackground(Color.WHITE);
            this.panelNode.getLabel().setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
        }
    }

    private void initializeEditable(boolean isEdited) {
        
        if (StringUtil.isUtf8(this.getElementDatabase().toString())) {
            this.panelNode.getEditable().setFont(UiUtil.FONT_MONO_ASIAN);
        } else {
            this.panelNode.getEditable().setFont(UiUtil.FONT_NON_MONO);
        }
        
        this.panelNode.getEditable().setText(StringUtil.detectUtf8(this.getElementDatabase().toString()));
        this.panelNode.getEditable().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.getLabelCount() : 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 int getIndexProgress() {
        return this.indexProgress;
    }

    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 boolean isContainingSelection() {
        return this.isContainingSelection;
    }

    public void setContainingSelection(boolean isContainingSelection) {
        this.isContainingSelection = isContainingSelection;
    }

    public boolean isLoaded() {
        return this.isLoaded;
    }

    public void setLoaded(boolean isLoaded) {
        this.isLoaded = isLoaded;
    }

    public boolean isProgressing() {
        return this.isProgressing;
    }

    public void setProgressing(boolean isProgressing) {
        this.isProgressing = isProgressing;
    }

    public boolean isLoading() {
        return this.isLoading;
    }

    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;
    }
}