Project: ceres
/*
 * Copyright (C) 2010 Brockmann Consult GmbH ([email protected]
 * 
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU General Public License as published by the Free 
 * Software Foundation; either version 3 of the License, or (at your option) 
 * any later version. 
 * This program 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 General Public License for 
 * more details. 
 * 
 * You should have received a copy of the GNU General Public License along 
 * with this program; if not, see http://www.gnu.org/licenses/ 
 */
 
package com.bc.ceres.swing; 
 
import javax.swing.JPanel; 
import javax.swing.JTree; 
import javax.swing.JViewport; 
import javax.swing.JWindow; 
import javax.swing.SwingUtilities; 
import javax.swing.border.AbstractBorder; 
import javax.swing.tree.TreeCellRenderer; 
import javax.swing.tree.TreePath; 
import java.awt.BorderLayout; 
import java.awt.Color; 
import java.awt.Component; 
import java.awt.Graphics; 
import java.awt.Insets; 
import java.awt.Point; 
import java.awt.Rectangle; 
import java.awt.event.MouseAdapter; 
import java.awt.event.MouseEvent; 
 
public class TreeCellExtender { 
    public final JTree tree; 
    private JWindow cellExtender; 
    private int offset; 
    private int row; 
    private boolean active; 
    private final MouseHandler mouseHandler; 
 
    public static TreeCellExtender equip(JTree tree) { 
        final TreeCellExtender extender = new TreeCellExtender(tree); 
        extender.setActive(true); 
        return extender; 
    } 
 
    public TreeCellExtender(JTree tree) { 
        this.tree = tree; 
        unsetRow(); 
        this.mouseHandler = new MouseHandler(); 
        this.active = false
    } 
 
    public JTree getTree() { 
        return tree; 
    } 
 
    public boolean isActive() { 
        return active; 
    } 
 
    public void setActive(boolean active) { 
        final boolean oldValue = this.active; 
        if (oldValue) { 
            tree.removeMouseListener(mouseHandler); 
            tree.removeMouseMotionListener(mouseHandler); 
            disposeCellExtender(); 
            unsetRow(); 
        } 
        this.active = active; 
        if (this.active) { 
            tree.addMouseListener(mouseHandler); 
            tree.addMouseMotionListener(mouseHandler); 
            hideCellExtender(); 
            unsetRow(); 
        } 
    } 
 
    private void showOrHideCellExtender(Point point) { 
        final int oldRow = row; 
        row = tree.getRowForLocation(point.x, point.y); 
        if (row == oldRow) { 
            return
        } 
 
        hideCellExtender(); 
        if (row < 0) { 
            return
        } 
 
        TreePath path = tree.getPathForRow(row); 
        Rectangle rowRect = tree.getPathBounds(path); 
        Rectangle viewRect; 
        if (tree.getParent() instanceof JViewport) { 
            viewRect = ((JViewport) tree.getParent()).getViewRect(); 
        } else { 
            viewRect = tree.getBounds(); 
        } 
 
        int rx1 = rowRect.x; 
        int ry1 = rowRect.y; 
        int rx2 = rowRect.x + rowRect.width - 1
        int ry2 = rowRect.y + rowRect.height - 1
 
        // int vx1 = viewRect.x; 
        int vy1 = viewRect.y; 
        int vx2 = viewRect.x + viewRect.width - 1
        int vy2 = viewRect.y + viewRect.height - 1
 
        boolean cellExtenderVisible = rx2 > vx2 && ry1 >= vy1 && ry2 <= vy2; 
        if (!cellExtenderVisible) { 
            return
        } 
 
        offset = vx2 - rx1; 
 
        int wx = rx1 < vx2 ? vx2 : rx1; 
        int wy = rowRect.y - 1
        int ww = rx2 - wx + 2
        int wh = rowRect.height + 2
        Rectangle windowRect = new Rectangle(wx, wy, ww, wh); 
        if (windowRect.isEmpty()) { 
            return
        } 
 
        showCellExtender(windowRect); 
    } 
 
    private void showCellExtender(Rectangle windowRect) { 
        final Rectangle screenBounds = convertToScreen(tree, windowRect); 
        if (cellExtender == null) { 
            cellExtender = new JWindow(SwingUtilities.getWindowAncestor(tree)); 
            cellExtender.getContentPane().add(new CellExtenderPanel(), BorderLayout.CENTER); 
        } 
        cellExtender.setBounds(screenBounds); 
        cellExtender.setVisible(true); 
    } 
 
    private void hideCellExtender() { 
        if (cellExtender != null) { 
            cellExtender.setVisible(false); 
        } 
    } 
 
    private void disposeCellExtender() { 
        if (cellExtender != null) { 
            cellExtender.dispose(); 
            cellExtender = null
        } 
    } 
 
    private void unsetRow() { 
        row = -1
    } 
 
    private static Rectangle convertToScreen(Component c, Rectangle r) { 
        Point p = new Point(r.getLocation()); 
        SwingUtilities.convertPointToScreen(p, c); 
        return new Rectangle(p.x, p.y, r.width, r.height); 
    } 
 
    private class MouseHandler extends MouseAdapter { 
        private Point point; 
 
        @Override 
        public void mouseMoved(MouseEvent e) { 
            Point point = e.getPoint(); 
            if (!point.equals(this.point)) { 
                this.point = point; 
                showOrHideCellExtender(this.point); 
            } 
        } 
 
        @Override 
        public void mouseDragged(MouseEvent e) { 
            hideCellExtender(); 
            unsetRow(); 
        } 
 
        @Override 
        public void mouseEntered(MouseEvent e) { 
            // Check, because mouseEntered is also called if cellExtender becomes invisible 
            // and cursor is over both, tree and cellExtender. 
            if (e.getPoint().equals(this.point)) { 
                hideCellExtender(); 
                unsetRow(); 
            } 
        } 
 
        @Override 
        public void mouseExited(MouseEvent e) { 
            // Check, because mouseEntered is also called if cellExtender becomes visible 
            // and cursor is over both, tree and cellExtender. 
            if (e.getPoint().equals(this.point)) { 
                disposeCellExtender(); 
                unsetRow(); 
            } 
        } 
    } 
 
    private class CellExtenderPanel extends JPanel { 
 
        public CellExtenderPanel() { 
            setBorder(new CellExtenderBorder()); 
            setBackground(tree.getBackground()); 
        } 
 
        @Override 
        protected void paintComponent(Graphics g) { 
            final int width = getWidth(); 
            final int height = getHeight(); 
            if (width <= 0 || height <= 0) { 
                return
            } 
 
            Rectangle bounds = getBounds(); 
            Color color = g.getColor(); 
            g.setColor(getBackground()); 
            g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); 
            g.setColor(color); 
 
            TreePath path = tree.getPathForRow(row); 
            TreeCellRenderer renderer = tree.getCellRenderer(); 
            Component rendererComponent = renderer.getTreeCellRendererComponent(tree, 
                                                                                path.getLastPathComponent(), 
                                                                                tree.isPathSelected(path), 
                                                                                tree.isExpanded(path), 
                                                                                true// todo - leaf ? 
                                                                                row, 
                                                                                false); // has focus ? 
            rendererComponent.setSize(1024, height); 
 
            Graphics g2 = g.create(00, width, height); 
            g2.translate(-offset, 0); 
            rendererComponent.paint(g2); 
            g2.dispose(); 
        } 
    } 
 
    private static class CellExtenderBorder extends AbstractBorder { 
        @Override 
        public Insets getBorderInsets(Component c) { 
            return new Insets(1011); 
        } 
 
        @Override 
        public Insets getBorderInsets(Component c, Insets insets) { 
            insets.left = 0
            insets.top = insets.right = insets.bottom = 1
            return insets; 
        } 
 
        @Override 
        public void paintBorder(Component c, Graphics g, int x1, int y1, int width, int height) { 
            final int x2 = x1 + width - 1
            final int y2 = y1 + height - 1
            final Color color = g.getColor(); 
            g.setColor(Color.DARK_GRAY); 
            g.drawLine(x1, y1, x2, y1); 
            g.drawLine(x1, y2, x2, y2); 
            g.drawLine(x2, y1, x2, y2); 
            g.setColor(color); 
        } 
    } 
 
}