package org.openswing.swing.tree.client;


import java.lang.reflect.*;
import java.text.*;
import java.util.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import javax.swing.tree.*;

import org.openswing.swing.message.receive.java.*;
import org.openswing.swing.util.client.*;


/**
 * <p>Title: OpenSwing Framework</p>
 * <p>Description: grid that contains a tree in the first column.</p>
 * <p>Copyright: Copyright (C) 2006 Mauro Carniel</p>
 *
 * <p> This file is part of OpenSwing Framework.
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the (LGPL) Lesser General Public
 * License as published by the Free Software Foundation;
 *
 *                GNU LESSER GENERAL PUBLIC LICENSE
 *                 Version 2.1, February 1999
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *       The author may be contacted at:
 *           maurocarniel@tin.it</p>
 *
 * @author Mauro Carniel
 * @version 1.0
 */
public class TreeGrid extends JTable {

    /** a subclass of JTree. */
    protected TreeTableCellRenderer tree;

    private TreeGridNodeRenderer treeRenderer;

    /** grid columns alignments: collection of pairs <attribute name,alignment: Integer> */
    private Hashtable gridColumnAlignments = new Hashtable();

    /** attribute name that identifies the column containing the tree */
    private String attributeName;

    /** attribute names to used to show grid columns */
    private ArrayList gridColumns = new ArrayList();


    public TreeGrid(TreeTableModel treeTableModel,String attributeName,ArrayList gridColumns,ArrayList gridColumnSizes,Hashtable gridColumnAlignments,String folderIconName,String leavesImageName,Format formatter,boolean rootVisible) {
      super();
      this.attributeName = attributeName;
      this.gridColumns = gridColumns;
      this.gridColumnAlignments = gridColumnAlignments;

      treeRenderer = new TreeGridNodeRenderer(folderIconName,leavesImageName,formatter);

      // Create the tree. It will be used as a renderer and editor.
      tree = new TreeTableCellRenderer(treeTableModel);
      tree.setRootVisible(rootVisible);
      tree.setCellRenderer(treeRenderer);

      // Install a tableModel representing the visible rows in the tree.
      super.setModel(new TreeTableModelAdapter(treeTableModel, tree));

      // Force the JTable and JTree to share their row selection models.
      ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
      tree.setSelectionModel(selectionWrapper);
      setSelectionModel(selectionWrapper.getListSelectionModel());

      // Install the tree editor renderer and editor.
      setDefaultRenderer(TreeTableModel.class, tree);

      setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());

      // No grid.
      setShowGrid(false);

      // No intercell spacing
      setIntercellSpacing(new Dimension(0, 0));

      setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
      setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

      for(int i=0;i<gridColumnSizes.size();i++) {
        getColumnModel().getColumn(i).setPreferredWidth( ( (Integer)gridColumnSizes.get(i)).intValue());
        if (gridColumnAlignments.containsKey(gridColumns.get(i)) &&
            !gridColumns.get(i).equals(attributeName)) {
          DefaultTableCellRenderer rend = new DefaultTableCellRenderer();
          rend.setHorizontalAlignment( ((Integer)gridColumnAlignments.get(gridColumns.get(i))).intValue() );
          getColumnModel().getColumn(i).setCellRenderer(rend);
        }
      }

      JTableHeader th = this.getTableHeader();
      th.setPreferredSize(new Dimension(th.getPreferredSize().width,ClientSettings.HEADER_HEIGHT));

      setRowHeight(ClientSettings.CELL_HEIGHT);
      setBackground(ClientSettings.GRID_CELL_BACKGROUND);
      setForeground(ClientSettings.GRID_CELL_FOREGROUND);
      setSelectionForeground(ClientSettings.GRID_SELECTION_FOREGROUND);
      setSelectionBackground(ClientSettings.GRID_SELECTION_BACKGROUND);

    }


    /**
     * Overridden to message super and forward the method to the tree.
     * Since the tree is not actually in the component hieachy it will
     * never receive this unless we forward it in this manner.
     */
    public void updateUI() {
        super.updateUI();
        if(tree != null) {
            tree.updateUI();
        }
        // Use the tree's default foreground and background colors in the
        // table.
        LookAndFeel.installColorsAndFont(this, "Tree.background", "Tree.foreground", "Tree.font");
    }

    /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
     * paint the editor. The UI currently uses different techniques to
     * paint the renderers and editors and overriding setBounds() below
     * is not the right thing to do for an editor. Returning -1 for the
     * editing row in this case, ensures the editor is never painted.
     */
    public int getEditingRow() {
        return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1 :
                editingRow;
    }

    /**
     * Overridden to pass the new rowHeight to the tree.
     */
    public void setRowHeight(int rowHeight) {
        super.setRowHeight(rowHeight);
        if (tree != null && tree.getRowHeight() != rowHeight) {
            tree.setRowHeight(getRowHeight());
        }
    }

    /**
     * Returns the tree that is being shared between the model.
     */
    public JTree getTree() {
        return tree;
    }


    /**
     * A TreeCellRenderer that displays a JTree.
     */
    public class TreeTableCellRenderer extends JTree implements TableCellRenderer {

        /** last table/tree row asked to renderer. */
        protected int visibleRow;

        public TreeTableCellRenderer(TreeModel model) {
            super(model);
        }

        /**
         * updateUI is overridden to set the colors of the Tree's renderer
         * to match that of the table.
         */
        public void updateUI() {
            super.updateUI();
            // Make the tree's cell renderer use the table's cell selection
            // colors.
            TreeCellRenderer tcr = getCellRenderer();
            if (tcr instanceof DefaultTreeCellRenderer) {
                DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr);
                // For 1.1 uncomment this, 1.2 has a bug that will cause an
                // exception to be thrown if the border selection color is
                // null.
                // dtcr.setBorderSelectionColor(null);
                dtcr.setTextSelectionColor(UIManager.getColor("Table.selectionForeground"));
                dtcr.setBackgroundSelectionColor(UIManager.getColor("Table.selectionBackground"));
            }
        }

        /**
         * Sets the row height of the tree, and forwards the row height to
         * the table.
         */
        public void setRowHeight(int rowHeight) {
            if (rowHeight > 0) {
                super.setRowHeight(rowHeight);
                if (TreeGrid.this != null &&
                    TreeGrid.this.getRowHeight() != rowHeight) {
                    TreeGrid.this.setRowHeight(getRowHeight());
                }
            }
        }

        /**
         * This is overridden to set the height to match that of the JTable.
         */
        public void setBounds(int x, int y, int w, int h) {
            super.setBounds(x, 0, w, TreeGrid.this.getHeight());
        }

        /**
         * Sublcassed to translate the graphics such that the last visible
         * row will be drawn at 0,0.
         */
        public void paint(Graphics g) {
            g.translate(0, -visibleRow * getRowHeight());
            super.paint(g);
        }

        /**
         * TreeCellRenderer method. Overridden to update the visible row.
         */
        public Component getTableCellRendererComponent(JTable table,
                                                       Object value,
                                                       boolean isSelected,
                                                       boolean hasFocus,
                                                       int row, int column) {
            if(isSelected)
                setBackground(table.getSelectionBackground());
            else
                setBackground(table.getBackground());

            visibleRow = row;
            return this;
        }
    }









    /**
     * <p>Title: OpenSwing Framework</p>
     * <p>Description: Inner class used to render the tree cells.</p>
     * <p>Copyright: Copyright (C) 2006 Mauro Carniel</p>
     * <p> </p>
     * @author Mauro Carniel
     * @version 1.0
     */
    public class TreeGridNodeRenderer extends DefaultTreeCellRenderer {

      ImageIcon folderIcon = null;
      ImageIcon leafIcon = null;
      TreePanel treePanel;

      /** formatter to use for this column (optional, may be null) */
      private Format formatter;


      /**
       * Costructor.
       * @param tree node container
       */
      public TreeGridNodeRenderer(String folderIconName,String leavesImageName,Format formatter) {
        try {
          this.treePanel = treePanel;
          this.formatter = formatter;
          folderIcon = new ImageIcon(ClientUtils.getImage(folderIconName));
          leafIcon = new ImageIcon(ClientUtils.getImage(leavesImageName));
          this.setOpaque(false);
          this.setBackgroundNonSelectionColor(new java.awt.Color(0,0,0,0));

        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }


      public Component getTreeCellRendererComponent(JTree tree,
                                                    Object value,
                                                    boolean sel,
                                                    boolean expanded,
                                                    boolean leaf,
                                                    int row,
                                                    boolean hasFocus) {
        try {
          super.getTreeCellRendererComponent(tree, value, sel,expanded, leaf, row,hasFocus);
          if (leaf)
            setIcon(leafIcon);
          else
            setIcon(folderIcon);

        } catch (Exception ex) {
          ex.printStackTrace();
        }
        JLabel l = (JLabel)this;
//    setBounds(0,0,350,l.getHeight());

        try {
          DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
          ValueObject vo = (ValueObject) node.getUserObject();
          if (vo!=null) {

            Method getter = vo.getClass().getMethod("get"+attributeName.substring(0,1).toUpperCase()+attributeName.substring(1),new Class[0]);
            value = getter.invoke(vo,new Object[0]);
            if (value!=null) {
              if (formatter!=null)
                value = formatter.format(value);
              l.setText(value.toString());

              if (gridColumnAlignments.get(attributeName)!=null) {
                ((JLabel)this).setHorizontalAlignment( ((Integer)gridColumnAlignments.get(attributeName)).intValue() );
              }


            }
          }

        }
        catch (Throwable ex1) {
          ex1.printStackTrace();
        }

        return l;
      }


    } // end inner-class


























    /**
     * TreeTableCellEditor implementation. Component returned is the
     * JTree.
     */
    public class TreeTableCellEditor extends AbstractCellEditor implements TableCellEditor {
        public Component getTableCellEditorComponent(JTable table,
                                                     Object value,
                                                     boolean isSelected,
                                                     int r, int c) {
            return tree;
        }

        /**
         * Overridden to return false, and if the event is a mouse event
         * it is forwarded to the tree.<p>
         * The behavior for this is debatable, and should really be offered
         * as a property. By returning false, all keyboard actions are
         * implemented in terms of the table. By returning true, the
         * tree would get a chance to do something with the keyboard
         * events. For the most part this is ok. But for certain keys,
         * such as left/right, the tree will expand/collapse where as
         * the table focus should really move to a different column. Page
         * up/down should also be implemented in terms of the table.
         * By returning false this also has the added benefit that clicking
         * outside of the bounds of the tree node, but still in the tree
         * column will select the row, whereas if this returned true
         * that wouldn't be the case.
         * <p>By returning false we are also enforcing the policy that
         * the tree will never be editable (at least by a key sequence).
         */
        public boolean isCellEditable(EventObject e) {
            if (e instanceof MouseEvent) {
                for (int counter = getColumnCount() - 1; counter >= 0;
                     counter--) {
                    if (getColumnClass(counter) == TreeTableModel.class) {
                        MouseEvent me = (MouseEvent)e;
                        MouseEvent newME = new MouseEvent(tree, me.getID(),
                                   me.getWhen(), me.getModifiers(),
                                   me.getX() - getCellRect(0, counter, true).x,
                                   me.getY(), me.getClickCount(),
                                   me.isPopupTrigger());
                        tree.dispatchEvent(newME);
                        break;
                    }
                }
            }
            return false;
        }
    }


    /**
     * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
     * to listen for changes in the ListSelectionModel it maintains. Once
     * a change in the ListSelectionModel happens, the paths are updated
     * in the DefaultTreeSelectionModel.
     */
    class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
        /** Set to true when we are updating the ListSelectionModel. */
        protected boolean         updatingListSelectionModel;

        public ListToTreeSelectionModelWrapper() {
            super();
            getListSelectionModel().addListSelectionListener
                                    (createListSelectionListener());
        }

        /**
         * Returns the list selection model. ListToTreeSelectionModelWrapper
         * listens for changes to this model and updates the selected paths
         * accordingly.
         */
        ListSelectionModel getListSelectionModel() {
            return listSelectionModel;
        }

        /**
         * This is overridden to set <code>updatingListSelectionModel</code>
         * and message super. This is the only place DefaultTreeSelectionModel
         * alters the ListSelectionModel.
         */
        public void resetRowSelection() {
            if(!updatingListSelectionModel) {
                updatingListSelectionModel = true;
                try {
                    super.resetRowSelection();
                }
                finally {
                    updatingListSelectionModel = false;
                }
            }
            // Notice how we don't message super if
            // updatingListSelectionModel is true. If
            // updatingListSelectionModel is true, it implies the
            // ListSelectionModel has already been updated and the
            // paths are the only thing that needs to be updated.
        }

        /**
         * Creates and returns an instance of ListSelectionHandler.
         */
        protected ListSelectionListener createListSelectionListener() {
            return new ListSelectionHandler();
        }

        /**
         * If <code>updatingListSelectionModel</code> is false, this will
         * reset the selected paths from the selected rows in the list
         * selection model.
         */
        protected void updateSelectedPathsFromSelectedRows() {
            if(!updatingListSelectionModel) {
                updatingListSelectionModel = true;
                try {
                    // This is way expensive, ListSelectionModel needs an
                    // enumerator for iterating.
                    int        min = listSelectionModel.getMinSelectionIndex();
                    int        max = listSelectionModel.getMaxSelectionIndex();

                    clearSelection();
                    if(min != -1 && max != -1) {
                        for(int counter = min; counter <= max; counter++) {
                            if(listSelectionModel.isSelectedIndex(counter)) {
                                TreePath selPath = tree.getPathForRow(counter);

                                if(selPath != null) {
                                    addSelectionPath(selPath);
                                }
                            }
                        }
                    }
                }
                finally {
                    updatingListSelectionModel = false;
                }
            }
        }

        /**
         * Class responsible for calling updateSelectedPathsFromSelectedRows
         * when the selection of the list changse.
         */
        class ListSelectionHandler implements ListSelectionListener {
            public void valueChanged(ListSelectionEvent e) {
                updateSelectedPathsFromSelectedRows();
            }
        }
    }
}
