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-2003 Sun
 * Microsystems, Inc. All Rights Reserved.
 */ 

package org.openide.explorer.propertysheet;

import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyEditor;
import javax.swing.*;
import javax.swing.event.AncestorListener;
import javax.swing.plaf.ComboBoxUI;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.text.JTextComponent;
import org.openide.explorer.propertysheet.editors.EnhancedPropertyEditor;

/** A combo box inplace editor.  Does a couple of necessary things:
 * 1.  It does not allow the UI delegate to install a focus listener on
 * it - it will manage opening and closing the popup on its own - this
 * is to avoid a specific problem - that if the editor is moved to a 
 * different cell and updated, the focus lost event will arrive after 
 * it has been moved, and the UI delegate will try to close the popup
 * when it should be opening.  2.  Contains a replacement renderer for
 * use on GTK look and feel - on JDK 1.4.2, combo boxes do not respect
 * the value assigned by setBackground() (there is a fixme note about this
 * in SynthComboBoxUI, so presumably this will be fixed at some point).
 */
class ComboInplaceEditor extends JComboBox implements InplaceEditor, FocusListener, AncestorListener {
    protected PropertyEditor editor;
    protected PropertyEnv env;
    protected PropertyModel mdl;
    boolean inSetUI=false;
    private boolean tableUI;
    private boolean connecting=false;
    private boolean hasBeenEditable=false;
    private boolean needLayout=false;
    
    /*Keystrokes this inplace editor wants to consume */
    static final KeyStroke[] cbKeyStrokes =
        new KeyStroke[] {KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0,false), 
                         KeyStroke.getKeyStroke(KeyEvent.VK_UP,0,false),
                         KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0,true), 
                         KeyStroke.getKeyStroke(KeyEvent.VK_UP,0,true),                         
                         KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_DOWN,0,false), 
                         KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_UP,0,false),
                         KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_DOWN,0,true), 
                         KeyStroke.getKeyStroke (KeyEvent.VK_PAGE_UP,0,true)
                        };

    /** Create a ComboInplaceEditor - the tableUI flag will tell it to use
     * less borders & such */                        
    public ComboInplaceEditor(boolean tableUI) {
        if (tableUI) {
            putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE); //NOI18N
        }
        if (Boolean.getBoolean("netbeans.ps.combohack")) { //NOI18N
            setLightWeightPopupEnabled(false);
        }
        if (getClass() == ComboInplaceEditor.class) {
            enableEvents(AWTEvent.FOCUS_EVENT_MASK);
        }
        this.tableUI = tableUI;
        if (tableUI) {
            updateUI();
        }
    }   
    
    /** Overridden to add a listener to the editor if necessary, since the
     * UI won't do that for us without a focus listener */
    public void addNotify() {
        super.addNotify();
        if (isEditable() && getClass() == ComboInplaceEditor.class) {
            getEditor().getEditorComponent().addFocusListener(this);
        }
        getLayout().layoutContainer(this);
    }
    
    public void setEditable(boolean val) {
        boolean hadBeenEditable = hasBeenEditable;
        hasBeenEditable |= val;
        super.setEditable(val);
        if (hadBeenEditable != hasBeenEditable) {
            log ("Combo editor for " + editor + " setEditable (" + val + ")");
            needLayout=true;
        }
    }
    
    /** Overridden to hide the popup and remove any listeners from the 
     * combo editor */
    public void removeNotify() {
        log ("Combo editor for " + editor + " removeNotify forcing popup close");
        setPopupVisible(false);
        super.removeNotify();
        getEditor().getEditorComponent().removeFocusListener(this);
    }
    
    public Insets getInsets() {
        if ("Aqua".equals(UIManager.getLookAndFeel().getID())) {
            return new Insets(0,0,0,0);
        } else {
            return super.getInsets();
        }
    }
    
    public void clear() {
        editor = null;
        env = null;
    }
    
    public void connect(PropertyEditor pe, PropertyEnv env) {
        connecting = true;
        try {
            log ("Combo editor connect to " + pe + " env=" + env);
            
            this.env = env;
            this.editor = pe;
            setModel(new DefaultComboBoxModel(pe.getTags()));
            boolean editable = (editor instanceof EnhancedPropertyEditor) ?
                ((EnhancedPropertyEditor) editor).supportsEditingTaggedValues() :
                env != null &&
                Boolean.TRUE.equals(
                    env.getFeatureDescriptor().getValue("canEditAsText")); //NOI18N

            setEditable (editable);
            setActionCommand(COMMAND_SUCCESS);
            reset();
            
        } finally {
            connecting = false;
        }
    }
    
    private void log (String s) {
        if (PropUtils.isLoggable(ComboInplaceEditor.class) && getClass() == ComboInplaceEditor.class) {
            PropUtils.log (ComboInplaceEditor.class, s); //NOI18N
        }
    }
    
    public void setSelectedItem(Object o) {
        //Some property editors (i.e. IMT's choice editor) treat
        //null as 0.  Probably not the right way to do it, but needs to
        //be handled.
        if (o == null && editor.getTags() != null && editor.getTags().length > 0) {
            o = editor.getTags()[0];
        }
        if (o != null) {
            super.setSelectedItem(o);
        }
    }
    
    /** Overridden to not fire changes is an event is called inside the
     * connect method */
    public void fireActionEvent() {
        if (connecting || editor == null) {
            return;
        } else {
            if (editor == null) {
                return;
            }
            if ("comboBoxEdited".equals(getActionCommand())) {
                log ("Translating comboBoxEdited action command to COMMAND_SUCCESS");
                setActionCommand(COMMAND_SUCCESS);
            }
            log ("Combo editor firing ActionPerformed command=" + getActionCommand());
            super.fireActionEvent();
        }
    }

    public void reset() {
        String targetValue = null;
        if (editor != null) {
            log ("Combo editor reset setting selected item to " + editor.getAsText());
            targetValue = editor.getAsText();

            //issue 26367, form editor needs ability to set a custom value
            //when editing is initiated (event handler combos, part of them
            //cleaning up their EnhancedPropertyEditors).  
        }
        if (getClass() == ComboInplaceEditor.class && env != null && 
            env.getFeatureDescriptor() != null) {

            String initialEditValue = (String) env.getFeatureDescriptor().
                getValue("initialEditValue"); //NOI18N

            if (initialEditValue != null) {
                targetValue = initialEditValue;
            }

        }
        setSelectedItem(targetValue);
    }
    
    public Object getValue() {
        if (isEditable()) {
            return getEditor().getItem();
        } else {
            return getSelectedItem();
        }
    }
    
    public PropertyEditor getPropertyEditor() {
        return editor;
    }
    
    public PropertyModel getPropertyModel() {
        return mdl;
    }
    
    public void setPropertyModel(PropertyModel pm) {
        log ("Combo editor set property model to " + pm);
        this.mdl=pm;
    }
    
    public JComponent getComponent() {
        return this;
    }
    
    public KeyStroke[] getKeyStrokes() {
        return cbKeyStrokes;
    }
    
    public void handleInitialInputEvent(InputEvent e) {
        //do nothing, this should get deprecated in InplaceEditor
    }
    
    /** Overridden to use CleanComboUI on Metal L&F to avoid extra borders */
    public void updateUI() {
        LookAndFeel lf = UIManager.getLookAndFeel();
        String id = lf.getID();
        boolean useClean = tableUI &&
            (lf instanceof MetalLookAndFeel || 
            "GTK".equals(id) || "Kunststoff".equals(id)); //NOI18N
        
        if (useClean) {
            super.setUI (PropUtils.createComboUI(this, tableUI));
        } else {
            super.updateUI();
        }
        if (tableUI & getEditor().getEditorComponent() instanceof JComponent) {
            ((JComponent) getEditor().getEditorComponent()).setBorder(null);
        }
    }

    /** Overridden to set a flag used to block the UI from adding a focus
     * listener, and to use an alternate renderer class on GTK look and feel
     * to work around a painting bug in SynthComboUI (colors not set correctly)*/
    public void setUI(ComboBoxUI ui) {
        inSetUI=true;
        try {
            super.setUI(ui);
        } finally {
            inSetUI=false;
        }
    }
    
    /** Overridden to handle a corner case - an NPE if the UI tries to display
     * the popup, but the combo box is removed from the parent before that can
     * happen - only happens on very rapid clicks between popups */
    public void showPopup() {
        try {
            log (" Combo editor show popup");
            super.showPopup();
        } catch (NullPointerException e) {
            //An inevitable consequence - the look and feel will queue display
            //of the popup, but it can be processed after the combo box is
            //offscreen
            log (" Combo editor show popup later due to npe");
            
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    ComboInplaceEditor.super.showPopup();
                }
            });
        }
    }
    
    private void prepareEditor() {
        Component c = getEditor().getEditorComponent();
        if (c instanceof JTextComponent) {
            JTextComponent jtc = (JTextComponent) c;
            String s = jtc.getText();
            if (s != null && s.length() > 0){
                jtc.setSelectionStart(0);
                jtc.setSelectionEnd(s.length());
            }
            if (tableUI) {
                jtc.setBackground(getBackground());
            } else {
                jtc.setBackground(PropUtils.getTextFieldBackground());
            }
        }
        if (getLayout() != null) {
            getLayout().layoutContainer(this);
        }
        repaint();
    }

    /** Overridden to do the focus-popup handling that would normally be done
     * by the look and feel */
    public void processFocusEvent(FocusEvent fe) {
        super.processFocusEvent(fe);
        if (PropUtils.isLoggable(ComboInplaceEditor.class)) {
            PropUtils.log(ComboInplaceEditor.class, "Focus event on combo " +
                "editor"); //NOI18N
            PropUtils.log(ComboInplaceEditor.class, fe);
        }
        Component focusOwner = KeyboardFocusManager.
            getCurrentKeyboardFocusManager().getFocusOwner();

        if (isDisplayable() && fe.getID()==fe.FOCUS_GAINED && focusOwner==this 
            && !isPopupVisible()) {
            if (isEditable()) {
                prepareEditor();
//                showPopup();
            } else {
                if (tableUI) {
                    showPopup();
                    //Try to beat the event mis-ordering at its own game
                    SwingUtilities.invokeLater(new PopupChecker());
                }
            }
            repaint();
        } else if (fe.getID() == fe.FOCUS_LOST && isPopupVisible() && !isDisplayable()) {
            if (!PropUtils.psCommitOnFocusLoss) {
                setActionCommand(COMMAND_FAILURE);
                fireActionEvent();
            }
            //We were removed, but we may be immediately added. See if that's the
            //case after other queued events run
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    if (!isDisplayable()) {
                        hidePopup();
                    }
                }
            });
        }
        repaint();
    }
    
    public boolean isKnownComponent(Component c) {
        return (c == getEditor().getEditorComponent());
    }
    
    public void setValue(Object o) {
        setSelectedItem(o);
    }
    
    /** Returns true if the combo box is editable */
    public boolean supportsTextEntry() {
        return isEditable();
    }
    
    /** Overridden to install an ancestor listener which will ensure the
     * popup is always opened correctly */
    protected void installAncestorListener() {
        //Use a replacement which will check to ensure the popup is 
        //displayed
        if (tableUI) {
            addAncestorListener(this);
        } else {
            super.installAncestorListener();
        }
    }
    
    /** Overridden to block the UI from adding its own focus listener, which
     * will close the popup at the wrong times.  We will manage focus 
     * ourselves instead */
    public void addFocusListener(FocusListener fl) {
        if (!inSetUI || !tableUI) {
            super.addFocusListener(fl);
        }
    }    
    
    public void focusGained(FocusEvent e) {
        //do nothing
        prepareEditor();
    }    

    /** If the editor loses focus, we're done editing - fire COMMAND_FAILURE */
    public void focusLost(FocusEvent e) {
        Component c = e.getOppositeComponent();
        if (!isAncestorOf(c) && c != getEditor().getEditorComponent()) {
            if (c == this || (c instanceof SheetTable && 
                ((SheetTable)c).isAncestorOf(this))) {
                //workaround for issue 38029 - editable combo editor can lose focus to ...itself
                return;
            }
            setActionCommand(COMMAND_FAILURE);
            log (" Combo editor lost focus - setting action command to " + COMMAND_FAILURE);
            getEditor().getEditorComponent().removeFocusListener(this);
            if (checker == null) {
                log ("No active popup checker, firing action event");
                fireActionEvent();
            }
        }
    }
    
    /** Overridden to ensure the editor gets focus if editable */
    public void firePopupMenuCanceled() {
        super.firePopupMenuCanceled();
        if (isEditable()) {
            Component focus = KeyboardFocusManager.
                getCurrentKeyboardFocusManager().getFocusOwner();
            if (isDisplayable() && focus == this) {
                log ("combo editor popup menu canceled.  Requesting focus on editor component");
                getEditor().getEditorComponent().requestFocus();
            }
        }
    }

    /** Overridden to fire COMMAND_FAILURE on Escape */
    public void processKeyEvent(KeyEvent ke) {
        super.processKeyEvent(ke);
        if (ke.getID() == ke.KEY_PRESSED && ke.getKeyCode() == ke.VK_ESCAPE) {
            setActionCommand(COMMAND_FAILURE);
            fireActionEvent();
        }
    }
    
    private static PopupChecker checker = null;
    public void ancestorAdded(javax.swing.event.AncestorEvent event) {
        //This is where we typically have a problem with popups not showing,
        //and below is the cure... Problem is that the popup is hidden
        //because the combo's ancestor is changed (even though we blocked
        //the normal ancestor listener from being added)
        checker = new PopupChecker();
        SwingUtilities.invokeLater(checker);
    }
    
    public void ancestorMoved(javax.swing.event.AncestorEvent event) {
        //do nothing
        if (needLayout && getLayout() != null) {
            getLayout().layoutContainer(this);
        }
    }
    
    public void ancestorRemoved(javax.swing.event.AncestorEvent event) {
        //do nothing
    }

    /** A handy runnable which will ensure the popup is really displayed */
    private class PopupChecker implements Runnable {
        public void run() {
            Window w = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
            if (w.isAncestorOf(ComboInplaceEditor.this)) {
                if (isShowing() && !isPopupVisible()) {
                    log ("Popup checker ensuring editor prepared or popup visible");
                    if (isEditable()) {
                        prepareEditor();
                    } else {
                        showPopup();
                    }
                }
                checker = null;
            }
        }
    }
    
    public void paintChildren(Graphics g) {
        if (editor != null && !hasFocus() && editor.isPaintable()) {
            return;
        } else {
            super.paintChildren(g);
        }
    }

    public void paintComponent (Graphics g) {
        //For property panel usage, allow the editor to paint
        if (editor != null && !hasFocus() && editor.isPaintable()) {
            Insets ins = getInsets();
            Color c = g.getColor();
            try {
                g.setColor(getBackground());
                g.fillRect(0,0,getWidth(),getHeight());
            } finally {
                g.setColor(c);
            }
            ins.left += PropUtils.getTextMargin();
            editor.paintValue(g, new Rectangle(ins.left, ins.top, getWidth() - 
                (ins.right + ins.left), getHeight() - (ins.top + ins.bottom)));
        } else {
            super.paintComponent(g);
        }
    }
    
    /* Replacement renderer class to hack around bug in SynthComboUI - will
    * only be used on GTK look & feel.  GTK does not set background/highlight
    * colors correctly */
    private class Renderer extends DefaultListCellRenderer {
        private boolean sel=false;
        /** Overridden to return the combo box's background color if selected
         * and focused - in GTK L&F combo boxes are always white (there's even
         * a "fixme" note in the code. */
        public Color getBackground() {
            //This method can be called in the superclass constructor, thanks
            //to updateUI().  At that time, this==null, so an NPE would happen
            //if we tried tor reference the outer class
            if (ComboInplaceEditor.this == null) {
                return null;
            }
            
            if (!sel && (getText() != null && getSelectedItem() != null && 
                getText().equals(getSelectedItem()))) {
                return ComboInplaceEditor.this.getBackground();
            } else {
                return super.getBackground();
            } 
        }
        
        public Component getListCellRendererComponent(JList list,Object value,
                                                       int index,
                                                       boolean isSelected,
                                                       boolean cellHasFocus) {
            sel = isSelected;
            return super.getListCellRendererComponent(list,
                value, index, isSelected, cellHasFocus);
        }        
        
    }
}
... 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.