1/*
2 * Copyright (c) 1997, 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 com.sun.java.swing.plaf.windows;
27
28import java.beans.PropertyChangeListener;
29import java.beans.PropertyChangeEvent;
30import javax.swing.plaf.basic.*;
31import javax.swing.plaf.*;
32import javax.swing.border.*;
33import javax.swing.*;
34import java.awt.event.*;
35import java.awt.*;
36
37import static com.sun.java.swing.plaf.windows.TMSchema.Part;
38import static com.sun.java.swing.plaf.windows.TMSchema.State;
39import static com.sun.java.swing.plaf.windows.XPStyle.Skin;
40
41import sun.swing.DefaultLookup;
42import sun.swing.StringUIClientPropertyKey;
43
44import com.sun.java.swing.plaf.windows.WindowsBorders.DashedBorder;
45
46/**
47 * Windows combo box.
48 * <p>
49 * <strong>Warning:</strong>
50 * Serialized objects of this class will not be compatible with
51 * future Swing releases.  The current serialization support is appropriate
52 * for short term storage or RMI between applications running the same
53 * version of Swing.  A future release of Swing will provide support for
54 * long term persistence.
55 *
56 * @author Tom Santos
57 * @author Igor Kushnirskiy
58 */
59
60public class WindowsComboBoxUI extends BasicComboBoxUI {
61
62    private static final MouseListener rolloverListener =
63        new MouseAdapter() {
64            private void handleRollover(MouseEvent e, boolean isRollover) {
65                JComboBox<?> comboBox = getComboBox(e);
66                WindowsComboBoxUI comboBoxUI = getWindowsComboBoxUI(e);
67                if (comboBox == null || comboBoxUI == null) {
68                    return;
69                }
70                if (! comboBox.isEditable()) {
71                    //mouse over editable ComboBox does not switch rollover
72                    //for the arrow button
73                    ButtonModel m = null;
74                    if (comboBoxUI.arrowButton != null) {
75                        m = comboBoxUI.arrowButton.getModel();
76                    }
77                    if (m != null ) {
78                        m.setRollover(isRollover);
79                    }
80                }
81                comboBoxUI.isRollover = isRollover;
82                comboBox.repaint();
83            }
84
85            public void mouseEntered(MouseEvent e) {
86                handleRollover(e, true);
87            }
88
89            public void mouseExited(MouseEvent e) {
90                handleRollover(e, false);
91            }
92
93            private JComboBox<?> getComboBox(MouseEvent event) {
94                Object source = event.getSource();
95                JComboBox<?> rv = null;
96                if (source instanceof JComboBox) {
97                    rv = (JComboBox) source;
98                } else if (source instanceof XPComboBoxButton) {
99                    rv = ((XPComboBoxButton) source)
100                        .getWindowsComboBoxUI().comboBox;
101                } else if (source instanceof JTextField &&
102                        ((JTextField) source).getParent() instanceof JComboBox) {
103                    rv = (JComboBox) ((JTextField) source).getParent();
104                }
105                return rv;
106            }
107
108            private WindowsComboBoxUI getWindowsComboBoxUI(MouseEvent event) {
109                JComboBox<?> comboBox = getComboBox(event);
110                WindowsComboBoxUI rv = null;
111                if (comboBox != null
112                    && comboBox.getUI() instanceof WindowsComboBoxUI) {
113                    rv = (WindowsComboBoxUI) comboBox.getUI();
114                }
115                return rv;
116            }
117
118        };
119    private boolean isRollover = false;
120
121    private static final PropertyChangeListener componentOrientationListener =
122        new PropertyChangeListener() {
123            public void propertyChange(PropertyChangeEvent e) {
124                String propertyName = e.getPropertyName();
125                Object source = null;
126                if ("componentOrientation" == propertyName
127                    && (source = e.getSource()) instanceof JComboBox
128                    && ((JComboBox) source).getUI() instanceof
129                      WindowsComboBoxUI) {
130                    JComboBox<?> comboBox = (JComboBox) source;
131                    WindowsComboBoxUI comboBoxUI = (WindowsComboBoxUI) comboBox.getUI();
132                    if (comboBoxUI.arrowButton instanceof XPComboBoxButton) {
133                        ((XPComboBoxButton) comboBoxUI.arrowButton).setPart(
134                                    (comboBox.getComponentOrientation() ==
135                                       ComponentOrientation.RIGHT_TO_LEFT)
136                                    ? Part.CP_DROPDOWNBUTTONLEFT
137                                    : Part.CP_DROPDOWNBUTTONRIGHT);
138                            }
139                        }
140                    }
141                };
142
143    public static ComponentUI createUI(JComponent c) {
144        return new WindowsComboBoxUI();
145    }
146
147    public void installUI( JComponent c ) {
148        super.installUI( c );
149        isRollover = false;
150        comboBox.setRequestFocusEnabled( true );
151        if (XPStyle.getXP() != null && arrowButton != null) {
152            //we can not do it in installListeners because arrowButton
153            //is initialized after installListeners is invoked
154            comboBox.addMouseListener(rolloverListener);
155            arrowButton.addMouseListener(rolloverListener);
156            // set empty border as default to see vista animated border
157            comboBox.setBorder(new EmptyBorder(0,0,0,0));
158        }
159    }
160
161    public void uninstallUI(JComponent c ) {
162        comboBox.removeMouseListener(rolloverListener);
163        if(arrowButton != null) {
164            arrowButton.removeMouseListener(rolloverListener);
165        }
166        super.uninstallUI( c );
167    }
168
169    /**
170     * {@inheritDoc}
171     * @since 1.6
172     */
173    @Override
174    protected void installListeners() {
175        super.installListeners();
176        XPStyle xp = XPStyle.getXP();
177        //button glyph for LTR and RTL combobox might differ
178        if (xp != null
179              && xp.isSkinDefined(comboBox, Part.CP_DROPDOWNBUTTONRIGHT)) {
180            comboBox.addPropertyChangeListener("componentOrientation",
181                                               componentOrientationListener);
182        }
183    }
184
185    /**
186     * {@inheritDoc}
187     * @since 1.6
188     */
189    @Override
190    protected void uninstallListeners() {
191        super.uninstallListeners();
192        comboBox.removePropertyChangeListener("componentOrientation",
193                                              componentOrientationListener);
194    }
195
196    /**
197     * {@inheritDoc}
198     * @since 1.6
199     */
200    protected void configureEditor() {
201        super.configureEditor();
202        if (XPStyle.getXP() != null) {
203            editor.addMouseListener(rolloverListener);
204        }
205    }
206
207    /**
208     * {@inheritDoc}
209     * @since 1.6
210     */
211    protected void unconfigureEditor() {
212        super.unconfigureEditor();
213        editor.removeMouseListener(rolloverListener);
214    }
215
216    /**
217     * {@inheritDoc}
218     * @since 1.6
219     */
220    public void paint(Graphics g, JComponent c) {
221        if (XPStyle.getXP() != null) {
222            paintXPComboBoxBackground(g, c);
223        }
224        super.paint(g, c);
225    }
226
227    State getXPComboBoxState(JComponent c) {
228        State state = State.NORMAL;
229        if (!c.isEnabled()) {
230            state = State.DISABLED;
231        } else if (isPopupVisible(comboBox)) {
232            state = State.PRESSED;
233        } else if (comboBox.isEditable()
234                && comboBox.getEditor().getEditorComponent().isFocusOwner()) {
235            state = State.PRESSED;
236        } else if (isRollover) {
237            state = State.HOT;
238        }
239        return state;
240    }
241
242    private void paintXPComboBoxBackground(Graphics g, JComponent c) {
243        XPStyle xp = XPStyle.getXP();
244        if (xp == null) {
245            return;
246        }
247        State state = getXPComboBoxState(c);
248        Skin skin = null;
249        if (! comboBox.isEditable()
250              && xp.isSkinDefined(c, Part.CP_READONLY)) {
251            skin = xp.getSkin(c, Part.CP_READONLY);
252        }
253        if (skin == null) {
254            skin = xp.getSkin(c, Part.CP_BORDER);
255        }
256        skin.paintSkin(g, 0, 0, c.getWidth(), c.getHeight(), state);
257    }
258
259    /**
260     * If necessary paints the currently selected item.
261     *
262     * @param g Graphics to paint to
263     * @param bounds Region to paint current value to
264     * @param hasFocus whether or not the JComboBox has focus
265     * @throws NullPointerException if any of the arguments are null.
266     * @since 1.5
267     */
268    public void paintCurrentValue(Graphics g, Rectangle bounds,
269                                  boolean hasFocus) {
270        XPStyle xp = XPStyle.getXP();
271        if ( xp != null) {
272            bounds.x += 2;
273            bounds.y += 2;
274            bounds.width -= 4;
275            bounds.height -= 4;
276        } else {
277            bounds.x += 1;
278            bounds.y += 1;
279            bounds.width -= 2;
280            bounds.height -= 2;
281        }
282        if (! comboBox.isEditable()
283            && xp != null
284            && xp.isSkinDefined(comboBox, Part.CP_READONLY)) {
285            // On vista for READNLY ComboBox
286            // color for currentValue is the same as for any other item
287
288            // mostly copied from javax.swing.plaf.basic.BasicComboBoxUI.paintCurrentValue
289            ListCellRenderer<Object> renderer = comboBox.getRenderer();
290            Component c;
291            if ( hasFocus && !isPopupVisible(comboBox) ) {
292                c = renderer.getListCellRendererComponent(
293                        listBox,
294                        comboBox.getSelectedItem(),
295                        -1,
296                        true,
297                        false );
298            } else {
299                c = renderer.getListCellRendererComponent(
300                        listBox,
301                        comboBox.getSelectedItem(),
302                        -1,
303                        false,
304                        false );
305            }
306            c.setFont(comboBox.getFont());
307            if ( comboBox.isEnabled() ) {
308                c.setForeground(comboBox.getForeground());
309                c.setBackground(comboBox.getBackground());
310            } else {
311                c.setForeground(DefaultLookup.getColor(
312                         comboBox, this, "ComboBox.disabledForeground", null));
313                c.setBackground(DefaultLookup.getColor(
314                         comboBox, this, "ComboBox.disabledBackground", null));
315            }
316            boolean shouldValidate = false;
317            if (c instanceof JPanel)  {
318                shouldValidate = true;
319            }
320            currentValuePane.paintComponent(g, c, comboBox, bounds.x, bounds.y,
321                                            bounds.width, bounds.height, shouldValidate);
322
323        } else {
324            super.paintCurrentValue(g, bounds, hasFocus);
325        }
326    }
327
328    /**
329     * {@inheritDoc}
330     * @since 1.6
331     */
332    public void paintCurrentValueBackground(Graphics g, Rectangle bounds,
333                                            boolean hasFocus) {
334        if (XPStyle.getXP() == null) {
335            super.paintCurrentValueBackground(g, bounds, hasFocus);
336        }
337    }
338
339    public Dimension getMinimumSize( JComponent c ) {
340        Dimension d = super.getMinimumSize(c);
341        if (XPStyle.getXP() != null) {
342            d.width += 7;
343            boolean isEditable = false;
344            if (c instanceof JComboBox) {
345                isEditable = ((JComboBox) c).isEditable();
346            }
347            d.height += isEditable ? 4 : 6;
348        } else {
349            d.width += 4;
350            d.height += 2;
351        }
352        return d;
353    }
354
355    /**
356     * Creates a layout manager for managing the components which make up the
357     * combo box.
358     *
359     * @return an instance of a layout manager
360     */
361    protected LayoutManager createLayoutManager() {
362        return new BasicComboBoxUI.ComboBoxLayoutManager() {
363            public void layoutContainer(Container parent) {
364                super.layoutContainer(parent);
365
366                if (XPStyle.getXP() != null && arrowButton != null) {
367                    Dimension d = parent.getSize();
368                    Insets insets = getInsets();
369                    int buttonWidth = arrowButton.getPreferredSize().width;
370                    arrowButton.setBounds(WindowsGraphicsUtils.isLeftToRight((JComboBox)parent)
371                                          ? (d.width - insets.right - buttonWidth)
372                                          : insets.left,
373                                          insets.top,
374                                          buttonWidth, d.height - insets.top - insets.bottom);
375                }
376            }
377        };
378    }
379
380    protected void installKeyboardActions() {
381        super.installKeyboardActions();
382    }
383
384    protected ComboPopup createPopup() {
385        return new WinComboPopUp(comboBox);
386    }
387
388    /**
389     * Creates the default editor that will be used in editable combo boxes.
390     * A default editor will be used only if an editor has not been
391     * explicitly set with <code>setEditor</code>.
392     *
393     * @return a <code>ComboBoxEditor</code> used for the combo box
394     * @see javax.swing.JComboBox#setEditor
395     */
396    protected ComboBoxEditor createEditor() {
397        return new WindowsComboBoxEditor();
398    }
399
400    /**
401     * {@inheritDoc}
402     * @since 1.6
403     */
404    @Override
405    protected ListCellRenderer<Object> createRenderer() {
406        XPStyle xp = XPStyle.getXP();
407        if (xp != null && xp.isSkinDefined(comboBox, Part.CP_READONLY)) {
408            return new WindowsComboBoxRenderer();
409        } else {
410            return super.createRenderer();
411        }
412    }
413
414    /**
415     * Creates an button which will be used as the control to show or hide
416     * the popup portion of the combo box.
417     *
418     * @return a button which represents the popup control
419     */
420    protected JButton createArrowButton() {
421        XPStyle xp = XPStyle.getXP();
422        if (xp != null) {
423            return new XPComboBoxButton(xp);
424        } else {
425            return super.createArrowButton();
426        }
427    }
428
429    @SuppressWarnings("serial") // Superclass is not serializable across versions
430    private class XPComboBoxButton extends XPStyle.GlyphButton {
431        private State prevState = null;
432
433        public XPComboBoxButton(XPStyle xp) {
434            super(comboBox,
435                  (! xp.isSkinDefined(comboBox, Part.CP_DROPDOWNBUTTONRIGHT))
436                   ? Part.CP_DROPDOWNBUTTON
437                   : (comboBox.getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT)
438                     ? Part.CP_DROPDOWNBUTTONLEFT
439                     : Part.CP_DROPDOWNBUTTONRIGHT
440                  );
441            setRequestFocusEnabled(false);
442        }
443
444        @Override
445        protected State getState() {
446            State rv;
447
448            getModel().setPressed(comboBox.isPopupVisible());
449
450            rv = super.getState();
451            XPStyle xp = XPStyle.getXP();
452            if (rv != State.DISABLED
453                    && comboBox != null && ! comboBox.isEditable()
454                    && xp != null && xp.isSkinDefined(comboBox,
455                            Part.CP_DROPDOWNBUTTONRIGHT)) {
456                /*
457                 * for non editable ComboBoxes Vista seems to have the
458                 * same glyph for all non DISABLED states
459                 */
460                rv = State.NORMAL;
461            }
462            if (rv == State.NORMAL && (prevState == State.HOT || prevState == State.PRESSED)) {
463                /*
464                 * State NORMAL of combobox button cannot overpaint states HOT or PRESSED
465                 * Therefore HOT state must be painted from alpha 1 to 0 and not as usual that
466                 * NORMAL state is painted from alpha 0 to alpha 1.
467                 */
468                skin.switchStates(true);
469            }
470            if (rv != prevState) {
471                prevState = rv;
472            }
473
474            return rv;
475        }
476
477        public Dimension getPreferredSize() {
478            return new Dimension(17, 21);
479        }
480
481        void setPart(Part part) {
482            setPart(comboBox, part);
483        }
484
485        WindowsComboBoxUI getWindowsComboBoxUI() {
486            return WindowsComboBoxUI.this;
487        }
488    }
489
490
491    /**
492     * Subclassed to add Windows specific Key Bindings.
493     * This class is now obsolete and doesn't do anything.
494     * Only included for backwards API compatibility.
495     * Do not call or override.
496     *
497     * @deprecated As of Java 2 platform v1.4.
498     */
499    @Deprecated
500    @SuppressWarnings("serial") // Superclass is not serializable across versions
501    protected class WindowsComboPopup extends BasicComboPopup {
502
503        public WindowsComboPopup( JComboBox<Object> cBox ) {
504            super( cBox );
505        }
506
507        protected KeyListener createKeyListener() {
508            return new InvocationKeyHandler();
509        }
510
511        protected class InvocationKeyHandler extends BasicComboPopup.InvocationKeyHandler {
512            protected InvocationKeyHandler() {
513                WindowsComboPopup.this.super();
514            }
515        }
516    }
517
518    @SuppressWarnings("serial") // Same-version serialization only
519    protected class WinComboPopUp extends BasicComboPopup {
520        private Skin listBoxBorder = null;
521        private XPStyle xp;
522
523        public WinComboPopUp(JComboBox<Object> combo) {
524            super(combo);
525            xp = XPStyle.getXP();
526            if (xp != null && xp.isSkinDefined(combo, Part.LBCP_BORDER_NOSCROLL)) {
527                this.listBoxBorder = new Skin(combo, Part.LBCP_BORDER_NOSCROLL);
528                this.setBorder(new EmptyBorder(1,1,1,1));
529            }
530        }
531
532        protected KeyListener createKeyListener() {
533            return new InvocationKeyHandler();
534        }
535
536        protected class InvocationKeyHandler extends BasicComboPopup.InvocationKeyHandler {
537            protected InvocationKeyHandler() {
538                WinComboPopUp.this.super();
539            }
540        }
541
542        protected void paintComponent(Graphics g) {
543            super.paintComponent(g);
544            if (this.listBoxBorder != null) {
545                this.listBoxBorder.paintSkinRaw(g, this.getX(), this.getY(),
546                        this.getWidth(), this.getHeight(), State.HOT);
547            }
548        }
549    }
550
551
552    /**
553     * Subclassed to highlight selected item in an editable combo box.
554     */
555    public static class WindowsComboBoxEditor
556        extends BasicComboBoxEditor.UIResource {
557
558        /**
559         * {@inheritDoc}
560         * @since 1.6
561         */
562        protected JTextField createEditorComponent() {
563            JTextField editor = super.createEditorComponent();
564            Border border = (Border)UIManager.get("ComboBox.editorBorder");
565
566            if (border != null) {
567                editor.setBorder(border);
568            }
569            editor.setOpaque(false);
570            return editor;
571        }
572
573        public void setItem(Object item) {
574            super.setItem(item);
575            Object focus = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
576            if ((focus == editor) || (focus == editor.getParent())) {
577                editor.selectAll();
578            }
579        }
580    }
581
582    /**
583     * Subclassed to set opacity {@code false} on the renderer
584     * and to show border for focused cells.
585     */
586    @SuppressWarnings("serial") // Superclass is not serializable across versions
587    private static class WindowsComboBoxRenderer
588          extends BasicComboBoxRenderer.UIResource {
589        private static final Object BORDER_KEY
590            = new StringUIClientPropertyKey("BORDER_KEY");
591        private static final Border NULL_BORDER = new EmptyBorder(0, 0, 0, 0);
592
593        // Create own version of DashedBorder with more space on left side
594        private class WindowsComboBoxDashedBorder extends DashedBorder {
595
596            public WindowsComboBoxDashedBorder(Color color, int thickness) {
597                super(color, thickness);
598            }
599
600            public WindowsComboBoxDashedBorder(Color color) {
601                super(color);
602            }
603
604            @Override
605            public Insets getBorderInsets(Component c, Insets i) {
606                return new Insets(0,2,0,0);
607            }
608        }
609
610        public WindowsComboBoxRenderer() {
611            super();
612
613            // correct space on the left side of text items in the combo popup list
614            Insets i = getBorder().getBorderInsets(this);
615            setBorder(new EmptyBorder(0, 2, 0, i.right));
616        }
617        /**
618         * {@inheritDoc}
619         */
620        @Override
621        public Component getListCellRendererComponent(
622                                                 JList<?> list,
623                                                 Object value,
624                                                 int index,
625                                                 boolean isSelected,
626                                                 boolean cellHasFocus) {
627            Component rv =
628                super.getListCellRendererComponent(list, value, index,
629                                                   isSelected, cellHasFocus);
630            if (rv instanceof JComponent) {
631                JComponent component = (JComponent) rv;
632                if (index == -1 && isSelected) {
633                    Border border = component.getBorder();
634                    Border dashedBorder =
635                        new WindowsComboBoxDashedBorder(list.getForeground());
636                    component.setBorder(dashedBorder);
637                    //store current border in client property if needed
638                    if (component.getClientProperty(BORDER_KEY) == null) {
639                        component.putClientProperty(BORDER_KEY,
640                                       (border == null) ? NULL_BORDER : border);
641                    }
642                } else {
643                    if (component.getBorder() instanceof
644                          WindowsBorders.DashedBorder) {
645                        Object storedBorder = component.getClientProperty(BORDER_KEY);
646                        if (storedBorder instanceof Border) {
647                            component.setBorder(
648                                (storedBorder == NULL_BORDER) ? null
649                                    : (Border) storedBorder);
650                        }
651                        component.putClientProperty(BORDER_KEY, null);
652                    }
653                }
654                if (index == -1) {
655                    component.setOpaque(false);
656                    component.setForeground(list.getForeground());
657                } else {
658                    component.setOpaque(true);
659                }
660            }
661            return rv;
662        }
663
664    }
665}
666