alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2004 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.swing.tabcontrol.plaf;

import org.netbeans.swing.tabcontrol.TabData;
import org.netbeans.swing.tabcontrol.TabbedContainer;
import org.netbeans.swing.tabcontrol.TabDisplayer;

import org.openide.awt.HtmlRenderer;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.EventObject;
import java.util.List;

/**
 * A panel that displays a drop down list of items, in several columns if
 * needed, associated with a JTabbedPane
 *
 * @author Tim Boudreau
 */

final class TabListPopup extends JTable implements MouseMotionListener,
        MouseListener {
    /** Reference to the focus owner when addNotify was called.  This is the
     * component that received the mouse event, so it's what we need to listen
     * on to update the selected cell as the user drags the mouse */
    private Component invokingComponent = null;
    /** Cached preferred size value */
    private Dimension prefSize = null;
    /** Flag indicating that the fixed row height has not yet been calculated - 
     * this is for fontsize support */
    boolean needCalcRowHeight = true;

    /** Reference to container for which we display quicklist */
    private TabDisplayer displayer = null;

    /** Reference to the popup object currently showing the default instance,
     * if it is visible */
    private static Popup currentPopup=null;
    /** AWTEventListener which is attached when the popup is shown to ensure
     * that it is closed when it should be.  It is removed after the popup
     * has been hidden. */
    private static AWTEventListener blistener = null;
    /** Reference to the default shared instance */
    private static Reference instance=null;
    /** Time of invocation, used to determine if a mouse release is
     * delayed long enough from a mouse press that it should close
     * the popup, instead of assuming the user wants move-and-click
     * behavior instead of drag-and-click behavior */
    long invocationTime = -1;

    private static final Border rendererBorder = 
        BorderFactory.createEmptyBorder (2, 3, 0, 3);

    private static HtmlRenderer.Renderer renderer = null;

    /** Creates a new instance of TabListPanel */
    private TabListPopup() {
        super (new TabListPopupTableModel());
        //Set up a line border around the edges
        setBorder (
            BorderFactory.createLineBorder(getForeground()));        
        setShowHorizontalLines(false);
        setBackground (UIManager.getColor("ComboBox.background")); //NOI18N
        if (renderer == null) {
            renderer = HtmlRenderer.createRenderer();
        }
        setDefaultRenderer(Object.class, renderer);
    }

    /**
     * Maps tab selected in quicklist to tab index in displayer to select
     * correct tab
     */
    private void setSelectedTab(int row, int col) {
        //Find corresponding index in displayer
        Object o = getTTModel().getValueAt(row, col);
        if (o instanceof TabData) {
            TabData td = (TabData) o;
            List l = displayer.getModel().getTabs();
            int ind = -1;
            for (int i = 0; i < l.size(); i++) {
                if (td.equals(l.get(i))) {
                    ind = i;
                    break;
                }
            }
            if (ind != -1) {
                int old = displayer.getSelectionModel().getSelectedIndex();
                displayer.getSelectionModel().setSelectedIndex(ind);
                //#40665 fix start
                if (displayer.getType() == TabbedContainer.TYPE_EDITOR && ind >= 0 && ind
                        == old) {
                    displayer.getUI().makeTabVisible(ind);
                }
                //#40665 fix end
            }
        }
    }

    public void updateUI() {
        needCalcRowHeight = true;
        super.updateUI();
    }

    public void setFont(Font f) {
        needCalcRowHeight = true;
        super.setFont(f);
    }

    public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
        //Find our TabData object
        Object value = getTTModel().getValueAt(row, column);
        
        //Set up font, selection, icon, colors, borders
        
        //Under very peculiar circumstances, displayer can be null - this happens on the
        //mac if the popup has been displayed, and then focus is shifted to another
        //application before it has had a chance to paint.  You need swapping or a big
        //hefty garbage collection after the popup is displayed but before it paints 
        //to make it happen
        int selIdx = displayer != null ? displayer.getSelectionModel().getSelectedIndex() : -1;
        boolean isSelTab = selIdx != -1 ? 
            value == displayer.getModel().getTab(selIdx) 
            : false;

        boolean isMouseOver = row == getSelectedRow() && 
            column == getSelectedColumn() && value != null;
            
        JComponent result = /*(JComponent)
            super.prepareRenderer (renderer, row, column); */
            (JComponent)
            renderer.getTableCellRendererComponent(this, value, 
                isMouseOver, isMouseOver, row, column);

        if (value == null) {
            //it's a filler space, we're done
            result.setOpaque(false);
            return result;
        }
        
        if (isSelTab) {
            result.setFont (getFont().deriveFont (Font.BOLD));
        }

        Icon icon = ((TabData) value).getIcon();

        HtmlRenderer.Renderer ren = (HtmlRenderer.Renderer) result;
        ren.setIcon(icon);
        
        if (icon.getIconWidth() > 0) {
            //Max annotated icon width is 24, so to have all the text and all
            //the icons come out aligned, set the icon text gap to the difference
            //plus a two pixel margin
            ren.setIconTextGap (26 - icon.getIconWidth());
        } else {
            //If the icon width is 0, fill the space and add in
            //the extra two pixels so the node names are aligned (btw, this
            //does seem to waste a frightful amount of horizontal space in
            //a tree that can use all it can get)
            ren.setIndent (26);
        }
        
        //The table may not really have focus, but it should always use the focus
        //color for the selection, not controlShadow
        ((HtmlRenderer.Renderer) result).setParentFocused(true);
        result.setBorder (rendererBorder);
        result.setOpaque(true);
        if (isMouseOver) {
            result.setBackground (getSelectionBackground());
            result.setForeground (getSelectionForeground());
        } else {
            result.setBackground (getBackground());
            result.setForeground (getForeground());
        }
 
        return result;
    }


    /**
     * Calculate the height of rows based on the current font.  This is done
     * when the first paint occurs, to ensure that a valid Graphics object is
     * available.
     *
     * @since 1.25
     */
    private void calcRowHeight(Graphics g) {
        Font f = getFont();
        FontMetrics fm = g.getFontMetrics(f);
        //As icons are displayed use maximum from font and icon height
        int rowHeight = Math.max(fm.getHeight(), 16) + 4;
        needCalcRowHeight = false;
        setRowHeight(rowHeight);
    }

    public void attach(TabDisplayer cont) {
        prefSize = null;
        displayer = cont;
        //Calc row height here so that TableModel can adjust number of columns.
        calcRowHeight(getOffscreenGraphics());
        getTTModel().setRowHeight(getRowHeight());
        getTTModel().attach(cont);
        synchronizeColumns(getTTModel().getColumnCount());
        getSelectionModel().clearSelection();
        getSelectionModel().setAnchorSelectionIndex(-1);
        getSelectionModel().setLeadSelectionIndex(-1);
    }

    static SoftReference ctx = null;

    /**
     * Provides an offscreen graphics context so that widths based on character
     * size can be calculated correctly before the component is shown
     */
    public static Graphics2D getOffscreenGraphics() {
        BufferedImage result = null;
        //XXX multi-monitors w/ different resolution may have problems;
        //Better to call Toolkit to create a screen graphics
        if (ctx != null) {
            result = (BufferedImage) ctx.get();
        }
        if (result == null) {
            result = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
            ctx = new SoftReference(result);
        }
        return (Graphics2D) result.getGraphics();
    }

    private void synchronizeColumns(int count) {
        TableColumnModel mdl = getColumnModel();
        int currCt = mdl.getColumnCount();
        if (currCt < count) {
            for (int i = currCt; i < count; i++) {
                mdl.addColumn(new TableColumn(i, 75, 
                    renderer, null));            }
        } else if (currCt > count) {
            for (int i = currCt - 1; i >= count; i--) {
                mdl.removeColumn(mdl.getColumn(i));
            }
        }
    }

    /** Overridden to calculate a preferred size based on the current optimal
     * number of columns, and set up the preferred width for each column based
     * on the maximum width tab name & icon displayed in it */
    public Dimension getPreferredSize() {
        if (prefSize == null) {
            Insets ins = getInsets();
            
            prefSize = new Dimension(ins.left + ins.top, ins.right + ins.bottom);
            int cols = getColumnCount();
            int rows = getRowCount();
            
            //Iterate the columns
            for (int i=0; i < cols; i++) {
                int columnWidth = 0;
                //For each column, iterate the rows
                for (int j=0; j < rows; j++) {
                    TableCellRenderer ren = getCellRenderer(j,i);
                    Component c = prepareRenderer (ren, j, i);
                    //find the widest cell
                    columnWidth = Math.max (c.getPreferredSize().width, 
                        columnWidth);
                }
                //Add in the max width needed for this column to the total
                //width
                prefSize.width += columnWidth;
                //Store it in the column model so it will be displayed with
                //the right width
                getColumnModel().getColumn(i).setPreferredWidth(columnWidth);
            }
            //Rows will be fixed height, so just multiply it out
            prefSize.height += rows * getRowHeight();
        }
        return prefSize;
    }

    private final TabListPopupTableModel getTTModel() {
        return (TabListPopupTableModel) getModel();
    }

    public boolean isAttached() {
        return displayer != null;
    }

    public void detach() {
        displayer = null;
        getTTModel().detach();
    }

    public void addNotify() {
        super.addNotify();
        addMouseListener(this);
        addMouseMotionListener(this);
        //Set initial selection if there is any field in table
        if ((getRowCount() > 0) && (getColumnCount() > 0)) {
            changeSelection(-1, -1, false, false);
        }
        EventObject eo = EventQueue.getCurrentEvent();
        if (eo != null) {
            if (eo.getSource() instanceof Component) {
                invokingComponent = (Component) eo.getSource();
            }
        }

        if (invokingComponent != null) {
            invokingComponent.addMouseListener(this);
            invokingComponent.addMouseMotionListener(this);
        }
        invocationTime = System.currentTimeMillis();
    }

    public void removeNotify() {
        super.removeNotify();
        removeMouseListener(this);
        removeMouseMotionListener(this);
        if (invokingComponent != null) {
            invokingComponent.removeMouseListener(this);
            invokingComponent.removeMouseMotionListener(this);
            invokingComponent = null;
        }
        detach();
    }

    public void paint(Graphics g) {
        if (needCalcRowHeight) {
            calcRowHeight(g);
        }
        super.paint(g);
    }

    int convertIndex(int row, int column) {
        return column * getRowCount() + row;
    }

    public void mouseClicked(MouseEvent e) {
        e.consume();
    }

    public void mousePressed(MouseEvent e) {
        Point p = e.getPoint();
        p = SwingUtilities.convertPoint((Component) e.getSource(), p, this);
        if (contains(p)) {
            //Otherwise the AWT listener will handle hiding the popup
            int row = getSelectedRow();
            int col = getSelectedColumn();
            setSelectedTab(row, col);
            //Hide window
            hideCurrentPopup();
        }
        e.consume();
    }

    public void mouseReleased(MouseEvent e) {
        if (e.getSource() == invokingComponent) {
            long time = System.currentTimeMillis();
            if (time - invocationTime > 500) {
                mousePressed(e);
            }
        }
        e.consume();
    }

    public void mouseEntered(MouseEvent e) {
        e.consume();
    }

    public void mouseExited(MouseEvent e) {
        clearSelection();
        e.consume();
    }

    //MouseMotionListener
    public void mouseDragged(MouseEvent e) {
        mouseMoved(e);
        e.consume();
    }

    public void mouseMoved(MouseEvent e) {
        Point p = e.getPoint();
        //It may have occured on the button that invoked the tabtable
        if (e.getSource() != this) {
            p = SwingUtilities.convertPoint((Component) e.getSource(), p,
                                            this);
        }

        if (this.contains(p)) {
            int row = rowAtPoint(p);
            int col = columnAtPoint(p);
            changeSelection(row, col, false, false);
        } else {
            clearSelection();
        }
        e.consume();
    }

    public static synchronized void invoke(final TabDisplayer c,
                                           int screenX, int screenY) {
        if (currentPopup != null) {
            hideCurrentPopup();
            return;
        }
        if (c.getModel().size() == 0) {
            return;
        }
        //Get our singleton soft-cached instance
        final TabListPopup tt = sharedInstance();
        //Aim it at the tabbed displayer in question
        tt.attach(c);
        //Get a popup object for the right coordinates.  Offset it to the 
        //left so it appears with its upper right corner under the button
        maybeHackPopupForAqua();
        currentPopup = PopupFactory.getSharedInstance().getPopup(c, tt, screenX - tt.getPreferredSize()
                                                                                  .width,
                                                                 screenY);

        //show it
        currentPopup.show();
        //Use an AWT listener to hide it in certain circumstances
        blistener = new BackupListener(tt);
    }

    /**
     * Focus changes are occasionally missed if the editor has focus while the
     * tab table is active.  This listener ensures that any mouse event on it
     * will indeed close the component.
     */
    private static class BackupListener implements AWTEventListener {
        //XXX could consolidate the property change listener above into this
        //and just have one listener class.
        private TabListPopup tt;

        public BackupListener(TabListPopup tt) {
            this.tt = tt;
            Toolkit.getDefaultToolkit().addAWTEventListener(this,
                                                            AWTEvent.MOUSE_EVENT_MASK
                                                            | AWTEvent.KEY_EVENT_MASK);
        }

        private boolean onTabTable(MouseEvent e) {
            Point p = e.getPoint();
            p = SwingUtilities.convertPoint((Component) e.getSource(), p, tt);
            return tt.contains(p);
        }

        public void eventDispatched(AWTEvent event) {
            if (event.getSource() == tt) {
                return;
            }
            if (event instanceof MouseEvent) {
                if (event.getID() == MouseEvent.MOUSE_RELEASED) {
                    long time = System.currentTimeMillis();
                    if (time - tt.invocationTime > 500) {
                        if (!onTabTable((MouseEvent) event)) {
                            //Don't take any chances
                            Toolkit.getDefaultToolkit()
                                    .removeAWTEventListener(this);
                            hideCurrentPopup();
                        }
                    }
                } else if (event.getID() == MouseEvent.MOUSE_PRESSED) {
                    if (!onTabTable((MouseEvent) event)) {
                        //Don't take any chances
                        if (event.getSource() != tt.invokingComponent) {
                            //If it's the invoker, don't do anything - it
                            //will generate another call to invoke(), which will
                            //do the hiding - if we do it here, it will get
                            //shown again when the button processes the event
                            Toolkit.getDefaultToolkit()
                                    .removeAWTEventListener(this);
                            hideCurrentPopup();
                        }
                    }
                }
            } else if (event instanceof KeyEvent) {
                if (event.getID() == KeyEvent.KEY_PRESSED) {
                    Toolkit.getDefaultToolkit().removeAWTEventListener(this);
                    hideCurrentPopup();
                }
            }
        }

    }

    public synchronized static void hideCurrentPopup() {
        if (currentPopup != null) {
            //Issue 41121 - use invokeLater to allow any pending
            //event processing against the popup contents to run
            //before the popup is hidden
            SwingUtilities.invokeLater (new PopupHider(currentPopup));
            currentPopup = null;
        }
        if (blistener != null) {
            Toolkit.getDefaultToolkit().removeAWTEventListener(blistener);
        }
    }
    
    /** Runnable which hides the popup in a subsequent event queue
     * loop.  This is to avoid problems with BasicToolbarUI, which
     * will try to process events on the component after it has been
     * hidden and throw exceptions.
     * @see http://www.netbeans.org/issues/show_bug.cgi?id=41121
     */
    private static class PopupHider implements Runnable {
        private Popup toHide;
        public PopupHider (Popup popup) {
            toHide = popup;
        }
        
        public void run() {
            toHide.hide();
         }
     }     

    private static TabListPopup sharedInstance() {
        TabListPopup result = null;
        if (instance != null) {
            result = (TabListPopup) instance.get();
        }
        if (result == null) {
            result = new TabListPopup();
            instance = new SoftReference(result);
        }
        return result;
    }

    private static void maybeHackPopupForAqua() {
        //Workaround for JDK bug NNN - a note in the 1.4.2 source for 
        //PopupFactory says:
        
             // MacOSX we want to change the default for pupus to be heavyweight!
            // was: private int popupType = LIGHT_WEIGHT_POPUP;

            // reverting out the change because we need better support in AWT
            // for heavyweights!
            // Steve will put this back in when AWT handles it better (after DP6)
            //private int popupType = HEAVY_WEIGHT_POPUP;        
        
        //but the fix for Apple is actually still commented out, so lightweight
        //popups do not work correctly.  We correct this here via reflection:
        try {
            String osName = System.getProperty ("os.name");
            if ("Mac OS X".equals (osName) || osName.startsWith ("Darwin")) {
                Field toSet = PopupFactory.class.getDeclaredField("popupType");
                toSet.setAccessible(true);
                toSet.set(PopupFactory.getSharedInstance(), new Integer(2));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.