1/*
2 * Copyright (c) 2002, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package javax.swing.plaf.synth;
27
28import java.awt.*;
29import java.awt.event.*;
30import javax.swing.*;
31import javax.swing.plaf.*;
32import javax.swing.event.*;
33import javax.swing.plaf.basic.*;
34import java.beans.PropertyChangeListener;
35import java.beans.PropertyChangeEvent;
36
37/**
38 * Provides the Synth L&F UI delegate for
39 * {@link javax.swing.JComboBox}.
40 *
41 * @author Scott Violet
42 * @since 1.7
43 */
44public class SynthComboBoxUI extends BasicComboBoxUI implements
45                              PropertyChangeListener, SynthUI {
46    private SynthStyle style;
47    private boolean useListColors;
48
49    /**
50     * Used to adjust the location and size of the popup. Very useful for
51     * situations such as we find in Nimbus where part of the border is used
52     * to paint the focus. In such cases, the border is empty space, and not
53     * part of the "visual" border, and in these cases, you'd like the popup
54     * to be adjusted such that it looks as if it were next to the visual border.
55     * You may want to use negative insets to get the right look.
56     */
57    Insets popupInsets;
58
59    /**
60     * This flag may be set via UIDefaults. By default, it is false, to
61     * preserve backwards compatibility. If true, then the combo will
62     * "act as a button" when it is not editable.
63     */
64    private boolean buttonWhenNotEditable;
65
66    /**
67     * A flag to indicate that the combo box and combo box button should
68     * remain in the PRESSED state while the combo popup is visible.
69     */
70    private boolean pressedWhenPopupVisible;
71
72    /**
73     * When buttonWhenNotEditable is true, this field is used to help make
74     * the combo box appear and function as a button when the combo box is
75     * not editable. In such a state, you can click anywhere on the button
76     * to get it to open the popup. Also, anywhere you hover over the combo
77     * will cause the entire combo to go into "rollover" state, and anywhere
78     * you press will go into "pressed" state. This also keeps in sync the
79     * state of the combo and the arrowButton.
80     */
81    private ButtonHandler buttonHandler;
82
83    /**
84     * Handler for repainting combo when editor component gains/looses focus
85     */
86    private EditorFocusHandler editorFocusHandler;
87
88    /**
89     * If true, then the cell renderer will be forced to be non-opaque when
90     * used for rendering the selected item in the combo box (not in the list),
91     * and forced to opaque after rendering the selected value.
92     */
93    private boolean forceOpaque = false;
94
95    /**
96     * Creates a new UI object for the given component.
97     *
98     * @param c component to create UI object for
99     * @return the UI object
100     */
101    public static ComponentUI createUI(JComponent c) {
102        return new SynthComboBoxUI();
103    }
104
105    /**
106     * {@inheritDoc}
107     *
108     * Overridden to ensure that ButtonHandler is created prior to any of
109     * the other installXXX methods, since several of them reference
110     * buttonHandler.
111     */
112    @Override
113    public void installUI(JComponent c) {
114        buttonHandler = new ButtonHandler();
115        super.installUI(c);
116    }
117
118    @Override
119    protected void installDefaults() {
120        updateStyle(comboBox);
121    }
122
123    private void updateStyle(JComboBox<?> comboBox) {
124        SynthStyle oldStyle = style;
125        SynthContext context = getContext(comboBox, ENABLED);
126
127        style = SynthLookAndFeel.updateStyle(context, this);
128        if (style != oldStyle) {
129            padding = (Insets) style.get(context, "ComboBox.padding");
130            popupInsets = (Insets)style.get(context, "ComboBox.popupInsets");
131            useListColors = style.getBoolean(context,
132                    "ComboBox.rendererUseListColors", true);
133            buttonWhenNotEditable = style.getBoolean(context,
134                    "ComboBox.buttonWhenNotEditable", false);
135            pressedWhenPopupVisible = style.getBoolean(context,
136                    "ComboBox.pressedWhenPopupVisible", false);
137            squareButton = style.getBoolean(context,
138                    "ComboBox.squareButton", true);
139
140            if (oldStyle != null) {
141                uninstallKeyboardActions();
142                installKeyboardActions();
143            }
144            forceOpaque = style.getBoolean(context,
145                    "ComboBox.forceOpaque", false);
146        }
147
148        if(listBox != null) {
149            SynthLookAndFeel.updateStyles(listBox);
150        }
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    protected void installListeners() {
158        comboBox.addPropertyChangeListener(this);
159        comboBox.addMouseListener(buttonHandler);
160        editorFocusHandler = new EditorFocusHandler(comboBox);
161        super.installListeners();
162    }
163
164    /**
165     * {@inheritDoc}
166     */
167    @Override
168    public void uninstallUI(JComponent c) {
169        if (popup instanceof SynthComboPopup) {
170            ((SynthComboPopup)popup).removePopupMenuListener(buttonHandler);
171        }
172        super.uninstallUI(c);
173        buttonHandler = null;
174    }
175
176    /**
177     * {@inheritDoc}
178     */
179    @Override
180    protected void uninstallDefaults() {
181        SynthContext context = getContext(comboBox, ENABLED);
182
183        style.uninstallDefaults(context);
184        style = null;
185    }
186
187    /**
188     * {@inheritDoc}
189     */
190    @Override
191    protected void uninstallListeners() {
192        editorFocusHandler.unregister();
193        comboBox.removePropertyChangeListener(this);
194        comboBox.removeMouseListener(buttonHandler);
195        buttonHandler.pressed = false;
196        buttonHandler.over = false;
197        super.uninstallListeners();
198    }
199
200    /**
201     * {@inheritDoc}
202     */
203    @Override
204    public SynthContext getContext(JComponent c) {
205        return getContext(c, getComponentState(c));
206    }
207
208    private SynthContext getContext(JComponent c, int state) {
209        return SynthContext.getContext(c, style, state);
210    }
211
212    private int getComponentState(JComponent c) {
213        // currently we have a broken situation where if a developer
214        // takes the border from a JComboBox and sets it on a JTextField
215        // then the codepath will eventually lead back to this method
216        // but pass in a JTextField instead of JComboBox! In case this
217        // happens, we just return the normal synth state for the component
218        // instead of doing anything special
219        if (!(c instanceof JComboBox)) return SynthLookAndFeel.getComponentState(c);
220
221        JComboBox<?> box = (JComboBox)c;
222        if (shouldActLikeButton()) {
223            int state = ENABLED;
224            if ((!c.isEnabled())) {
225                state = DISABLED;
226            }
227            if (buttonHandler.isPressed()) {
228                state |= PRESSED;
229            }
230            if (buttonHandler.isRollover()) {
231                state |= MOUSE_OVER;
232            }
233            if (box.isFocusOwner()) {
234                state |= FOCUSED;
235            }
236            return state;
237        } else {
238            // for editable combos the editor component has the focus not the
239            // combo box its self, so we should make the combo paint focused
240            // when its editor has focus
241            int basicState = SynthLookAndFeel.getComponentState(c);
242            if (box.isEditable() &&
243                     box.getEditor().getEditorComponent().isFocusOwner()) {
244                basicState |= FOCUSED;
245            }
246            return basicState;
247        }
248    }
249
250    /**
251     * {@inheritDoc}
252     */
253    @Override
254    protected ComboPopup createPopup() {
255        SynthComboPopup p = new SynthComboPopup(comboBox);
256        p.addPopupMenuListener(buttonHandler);
257        return p;
258    }
259
260    /**
261     * {@inheritDoc}
262     */
263    @Override
264    protected ListCellRenderer<Object> createRenderer() {
265        return new SynthComboBoxRenderer();
266    }
267
268    /**
269     * {@inheritDoc}
270     */
271    @Override
272    protected ComboBoxEditor createEditor() {
273        return new SynthComboBoxEditor();
274    }
275
276    //
277    // end UI Initialization
278    //======================
279
280    /**
281     * {@inheritDoc}
282     */
283    @Override
284    public void propertyChange(PropertyChangeEvent e) {
285        if (SynthLookAndFeel.shouldUpdateStyle(e)) {
286            updateStyle(comboBox);
287        }
288    }
289
290    /**
291     * {@inheritDoc}
292     */
293    @Override
294    protected JButton createArrowButton() {
295        SynthArrowButton button = new SynthArrowButton(SwingConstants.SOUTH);
296        button.setName("ComboBox.arrowButton");
297        button.setModel(buttonHandler);
298        return button;
299    }
300
301    //=================================
302    // begin ComponentUI Implementation
303
304    /**
305     * Notifies this UI delegate to repaint the specified component.
306     * This method paints the component background, then calls
307     * the {@link #paint(SynthContext,Graphics)} method.
308     *
309     * <p>In general, this method does not need to be overridden by subclasses.
310     * All Look and Feel rendering code should reside in the {@code paint} method.
311     *
312     * @param g the {@code Graphics} object used for painting
313     * @param c the component being painted
314     * @see #paint(SynthContext,Graphics)
315     */
316    @Override
317    public void update(Graphics g, JComponent c) {
318        SynthContext context = getContext(c);
319
320        SynthLookAndFeel.update(context, g);
321        context.getPainter().paintComboBoxBackground(context, g, 0, 0,
322                                                  c.getWidth(), c.getHeight());
323        paint(context, g);
324    }
325
326    /**
327     * Paints the specified component according to the Look and Feel.
328     * <p>This method is not used by Synth Look and Feel.
329     * Painting is handled by the {@link #paint(SynthContext,Graphics)} method.
330     *
331     * @param g the {@code Graphics} object used for painting
332     * @param c the component being painted
333     * @see #paint(SynthContext,Graphics)
334     */
335    @Override
336    public void paint(Graphics g, JComponent c) {
337        SynthContext context = getContext(c);
338
339        paint(context, g);
340    }
341
342    /**
343     * Paints the specified component.
344     *
345     * @param context context for the component being painted
346     * @param g the {@code Graphics} object used for painting
347     * @see #update(Graphics,JComponent)
348     */
349    protected void paint(SynthContext context, Graphics g) {
350        hasFocus = comboBox.hasFocus();
351        if ( !comboBox.isEditable() ) {
352            Rectangle r = rectangleForCurrentValue();
353            paintCurrentValue(g,r,hasFocus);
354        }
355    }
356
357    /**
358     * {@inheritDoc}
359     */
360    @Override
361    public void paintBorder(SynthContext context, Graphics g, int x,
362                            int y, int w, int h) {
363        context.getPainter().paintComboBoxBorder(context, g, x, y, w, h);
364    }
365
366    /**
367     * Paints the currently selected item.
368     */
369    @Override
370    public void paintCurrentValue(Graphics g,Rectangle bounds,boolean hasFocus) {
371        ListCellRenderer<Object> renderer = comboBox.getRenderer();
372        Component c;
373
374        c = renderer.getListCellRendererComponent(
375                listBox, comboBox.getSelectedItem(), -1, false, false );
376
377        // Fix for 4238829: should lay out the JPanel.
378        boolean shouldValidate = false;
379        if (c instanceof JPanel)  {
380            shouldValidate = true;
381        }
382
383        if (c instanceof UIResource) {
384            c.setName("ComboBox.renderer");
385        }
386
387        boolean force = forceOpaque && c instanceof JComponent;
388        if (force) {
389            ((JComponent)c).setOpaque(false);
390        }
391
392        int x = bounds.x, y = bounds.y, w = bounds.width, h = bounds.height;
393        if (padding != null) {
394            x = bounds.x + padding.left;
395            y = bounds.y + padding.top;
396            w = bounds.width - (padding.left + padding.right);
397            h = bounds.height - (padding.top + padding.bottom);
398        }
399
400        currentValuePane.paintComponent(g, c, comboBox, x, y, w, h, shouldValidate);
401
402        if (force) {
403            ((JComponent)c).setOpaque(true);
404        }
405    }
406
407    /**
408     * @return true if this combo box should act as one big button. Typically
409     * only happens when buttonWhenNotEditable is true, and comboBox.isEditable
410     * is false.
411     */
412    private boolean shouldActLikeButton() {
413        return buttonWhenNotEditable && !comboBox.isEditable();
414    }
415
416    /**
417     * Returns the default size of an empty display area of the combo box using
418     * the current renderer and font.
419     *
420     * This method was overridden to use SynthComboBoxRenderer instead of
421     * DefaultListCellRenderer as the default renderer when calculating the
422     * size of the combo box. This is used in the case of the combo not having
423     * any data.
424     *
425     * @return the size of an empty display area
426     * @see #getDisplaySize
427     */
428    @Override
429    protected Dimension getDefaultSize() {
430        SynthComboBoxRenderer r = new SynthComboBoxRenderer();
431        Dimension d = getSizeForComponent(r.getListCellRendererComponent(listBox, " ", -1, false, false));
432        return new Dimension(d.width, d.height);
433    }
434
435    /**
436     * From BasicComboBoxRenderer v 1.18.
437     *
438     * Be aware that SynthFileChooserUIImpl relies on the fact that the default
439     * renderer installed on a Synth combo box is a JLabel. If this is changed,
440     * then an assert will fail in SynthFileChooserUIImpl
441     */
442    @SuppressWarnings("serial") // Superclass is not serializable across versions
443    private class SynthComboBoxRenderer extends JLabel implements ListCellRenderer<Object>, UIResource {
444        public SynthComboBoxRenderer() {
445            super();
446            setText(" ");
447        }
448
449        @Override
450        public String getName() {
451            // SynthComboBoxRenderer should have installed Name while constructor is working.
452            // The setName invocation in the SynthComboBoxRenderer() constructor doesn't work
453            // because of the opaque property is installed in the constructor based on the
454            // component name (see GTKStyle.isOpaque())
455            String name = super.getName();
456
457            return name == null ? "ComboBox.renderer" : name;
458        }
459
460        @Override
461        public Component getListCellRendererComponent(JList<?> list, Object value,
462                         int index, boolean isSelected, boolean cellHasFocus) {
463            setName("ComboBox.listRenderer");
464            SynthLookAndFeel.resetSelectedUI();
465            if (isSelected) {
466                setBackground(list.getSelectionBackground());
467                setForeground(list.getSelectionForeground());
468                if (!useListColors) {
469                    SynthLookAndFeel.setSelectedUI(
470                         (SynthLabelUI)SynthLookAndFeel.getUIOfType(getUI(),
471                         SynthLabelUI.class), isSelected, cellHasFocus,
472                         list.isEnabled(), false);
473                }
474            } else {
475                setBackground(list.getBackground());
476                setForeground(list.getForeground());
477            }
478
479            setFont(list.getFont());
480
481            if (value instanceof Icon) {
482                setIcon((Icon)value);
483                setText("");
484            } else {
485                String text = (value == null) ? " " : value.toString();
486
487                if ("".equals(text)) {
488                    text = " ";
489                }
490                setText(text);
491            }
492
493            // The renderer component should inherit the enabled and
494            // orientation state of its parent combobox.  This is
495            // especially needed for GTK comboboxes, where the
496            // ListCellRenderer's state determines the visual state
497            // of the combobox.
498            if (comboBox != null){
499                setEnabled(comboBox.isEnabled());
500                setComponentOrientation(comboBox.getComponentOrientation());
501            }
502
503            return this;
504        }
505
506        @Override
507        public void paint(Graphics g) {
508            super.paint(g);
509            SynthLookAndFeel.resetSelectedUI();
510        }
511    }
512
513
514    private static class SynthComboBoxEditor
515            extends BasicComboBoxEditor.UIResource {
516
517        @Override public JTextField createEditorComponent() {
518            JTextField f = new JTextField("", 9);
519            f.setName("ComboBox.textField");
520            return f;
521        }
522    }
523
524
525    /**
526     * Handles all the logic for treating the combo as a button when it is
527     * not editable, and when shouldActLikeButton() is true. This class is a
528     * special ButtonModel, and installed on the arrowButton when appropriate.
529     * It also is installed as a mouse listener and mouse motion listener on
530     * the combo box. In this way, the state between the button and combo
531     * are in sync. Whenever one is "over" both are. Whenever one is pressed,
532     * both are.
533     */
534    @SuppressWarnings("serial") // Superclass is not serializable across versions
535    private final class ButtonHandler extends DefaultButtonModel
536            implements MouseListener, PopupMenuListener {
537        /**
538         * Indicates that the mouse is over the combo or the arrow button.
539         * This field only has meaning if buttonWhenNotEnabled is true.
540         */
541        private boolean over;
542        /**
543         * Indicates that the combo or arrow button has been pressed. This
544         * field only has meaning if buttonWhenNotEnabled is true.
545         */
546        private boolean pressed;
547
548        //------------------------------------------------------------------
549        // State Methods
550        //------------------------------------------------------------------
551
552        /**
553         * <p>Updates the internal "pressed" state. If shouldActLikeButton()
554         * is true, and if this method call will change the internal state,
555         * then the combo and button will be repainted.</p>
556         *
557         * <p>Note that this method is called either when a press event
558         * occurs on the combo box, or on the arrow button.</p>
559         */
560        private void updatePressed(boolean p) {
561            this.pressed = p && isEnabled();
562            if (shouldActLikeButton()) {
563                comboBox.repaint();
564            }
565        }
566
567        /**
568         * <p>Updates the internal "over" state. If shouldActLikeButton()
569         * is true, and if this method call will change the internal state,
570         * then the combo and button will be repainted.</p>
571         *
572         * <p>Note that this method is called either when a mouseover/mouseoff event
573         * occurs on the combo box, or on the arrow button.</p>
574         */
575        private void updateOver(boolean o) {
576            boolean old = isRollover();
577            this.over = o && isEnabled();
578            boolean newo = isRollover();
579            if (shouldActLikeButton() && old != newo) {
580                comboBox.repaint();
581            }
582        }
583
584        //------------------------------------------------------------------
585        // DefaultButtonModel Methods
586        //------------------------------------------------------------------
587
588        /**
589         * @inheritDoc
590         *
591         * Ensures that isPressed() will return true if the combo is pressed,
592         * or the arrowButton is pressed, <em>or</em> if the combo popup is
593         * visible. This is the case because a combo box looks pressed when
594         * the popup is visible, and so should the arrow button.
595         */
596        @Override
597        public boolean isPressed() {
598            boolean b = shouldActLikeButton() ? pressed : super.isPressed();
599            return b || (pressedWhenPopupVisible && comboBox.isPopupVisible());
600        }
601
602        /**
603         * @inheritDoc
604         *
605         * Ensures that the armed state is in sync with the pressed state
606         * if shouldActLikeButton is true. Without this method, the arrow
607         * button will not look pressed when the popup is open, regardless
608         * of the result of isPressed() alone.
609         */
610        @Override
611        public boolean isArmed() {
612            boolean b = shouldActLikeButton() ||
613                        (pressedWhenPopupVisible && comboBox.isPopupVisible());
614            return b ? isPressed() : super.isArmed();
615        }
616
617        /**
618         * @inheritDoc
619         *
620         * Ensures that isRollover() will return true if the combo is
621         * rolled over, or the arrowButton is rolled over.
622         */
623        @Override
624        public boolean isRollover() {
625            return shouldActLikeButton() ? over : super.isRollover();
626        }
627
628        /**
629         * @inheritDoc
630         *
631         * Forwards pressed states to the internal "pressed" field
632         */
633        @Override
634        public void setPressed(boolean b) {
635            super.setPressed(b);
636            updatePressed(b);
637        }
638
639        /**
640         * @inheritDoc
641         *
642         * Forwards rollover states to the internal "over" field
643         */
644        @Override
645        public void setRollover(boolean b) {
646            super.setRollover(b);
647            updateOver(b);
648        }
649
650        //------------------------------------------------------------------
651        // MouseListener/MouseMotionListener Methods
652        //------------------------------------------------------------------
653
654        @Override
655        public void mouseEntered(MouseEvent mouseEvent) {
656            updateOver(true);
657        }
658
659        @Override
660        public void mouseExited(MouseEvent mouseEvent) {
661            updateOver(false);
662        }
663
664        @Override
665        public void mousePressed(MouseEvent mouseEvent) {
666            updatePressed(true);
667        }
668
669        @Override
670        public void mouseReleased(MouseEvent mouseEvent) {
671            updatePressed(false);
672        }
673
674        @Override
675        public void mouseClicked(MouseEvent e) {}
676
677        //------------------------------------------------------------------
678        // PopupMenuListener Methods
679        //------------------------------------------------------------------
680
681        /**
682         * @inheritDoc
683         *
684         * Ensures that the combo box is repainted when the popup is closed.
685         * This avoids a bug where clicking off the combo wasn't causing a repaint,
686         * and thus the combo box still looked pressed even when it was not.
687         *
688         * This bug was only noticed when acting as a button, but may be generally
689         * present. If so, remove the if() block
690         */
691        @Override
692        public void popupMenuCanceled(PopupMenuEvent e) {
693            if (shouldActLikeButton() || pressedWhenPopupVisible) {
694                comboBox.repaint();
695            }
696        }
697
698        @Override
699        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {}
700        @Override
701        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {}
702    }
703
704    /**
705     * Handler for repainting combo when editor component gains/looses focus
706     */
707    private static class EditorFocusHandler implements FocusListener,
708            PropertyChangeListener {
709        private JComboBox<?> comboBox;
710        private ComboBoxEditor editor = null;
711        private Component editorComponent = null;
712
713        private EditorFocusHandler(JComboBox<?> comboBox) {
714            this.comboBox = comboBox;
715            editor = comboBox.getEditor();
716            if (editor != null){
717                editorComponent = editor.getEditorComponent();
718                if (editorComponent != null){
719                    editorComponent.addFocusListener(this);
720                }
721            }
722            comboBox.addPropertyChangeListener("editor",this);
723        }
724
725        public void unregister(){
726            comboBox.removePropertyChangeListener(this);
727            if (editorComponent!=null){
728                editorComponent.removeFocusListener(this);
729            }
730        }
731
732        /** Invoked when a component gains the keyboard focus. */
733        public void focusGained(FocusEvent e) {
734            // repaint whole combo on focus gain
735            comboBox.repaint();
736        }
737
738        /** Invoked when a component loses the keyboard focus. */
739        public void focusLost(FocusEvent e) {
740            // repaint whole combo on focus loss
741            comboBox.repaint();
742        }
743
744        /**
745         * Called when the combos editor changes
746         *
747         * @param evt A PropertyChangeEvent object describing the event source and
748         *            the property that has changed.
749         */
750        public void propertyChange(PropertyChangeEvent evt) {
751            ComboBoxEditor newEditor = comboBox.getEditor();
752            if (editor != newEditor){
753                if (editorComponent!=null){
754                    editorComponent.removeFocusListener(this);
755                }
756                editor = newEditor;
757                if (editor != null){
758                    editorComponent = editor.getEditorComponent();
759                    if (editorComponent != null){
760                        editorComponent.addFocusListener(this);
761                    }
762                }
763            }
764        }
765    }
766}
767