BasicRadioButtonUI.java revision 12345:fbf897c33625
1123682Sache/*
2123682Sache * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
3123682Sache * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4123682Sache *
5123682Sache * This code is free software; you can redistribute it and/or modify it
6123682Sache * under the terms of the GNU General Public License version 2 only, as
7123682Sache * published by the Free Software Foundation.  Oracle designates this
8123682Sache * particular file as subject to the "Classpath" exception as provided
9123682Sache * by Oracle in the LICENSE file that accompanied this code.
10123682Sache *
11123682Sache * This code is distributed in the hope that it will be useful, but WITHOUT
12123682Sache * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13123682Sache * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14123682Sache * version 2 for more details (a copy is included in the LICENSE file that
15123682Sache * accompanied this code).
16123682Sache *
17123682Sache * You should have received a copy of the GNU General Public License version
18123682Sache * 2 along with this work; if not, write to the Free Software Foundation,
19123682Sache * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20123682Sache *
21123682Sache * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22123682Sache * or visit www.oracle.com if you need additional information or have any
23123682Sache * questions.
24123682Sache */
25123682Sache
26123682Sachepackage javax.swing.plaf.basic;
27123682Sache
28123682Sacheimport java.awt.*;
29123682Sacheimport java.awt.event.*;
30123682Sacheimport javax.swing.*;
31123682Sacheimport javax.swing.border.*;
32123682Sacheimport javax.swing.plaf.*;
33123682Sacheimport javax.swing.text.View;
34123682Sacheimport sun.swing.SwingUtilities2;
35123682Sacheimport sun.awt.AppContext;
36123682Sacheimport java.util.Enumeration;
37123682Sacheimport java.util.HashSet;
38123682Sacheimport java.util.Set;
39123682Sache
40123682Sache/**
41123682Sache * RadioButtonUI implementation for BasicRadioButtonUI
42123682Sache *
43123682Sache * @author Jeff Dinkins
44123682Sache */
45123682Sachepublic class BasicRadioButtonUI extends BasicToggleButtonUI
46123682Sache{
47123682Sache    private static final Object BASIC_RADIO_BUTTON_UI_KEY = new Object();
48123682Sache
49123682Sache    /**
50123682Sache     * The icon.
51123682Sache     */
52123682Sache    protected Icon icon;
53123682Sache
54123682Sache    private boolean defaults_initialized = false;
55123682Sache
56123682Sache    private final static String propertyPrefix = "RadioButton" + ".";
57123682Sache
58123682Sache    private KeyListener keyListener = null;
59123682Sache
60123682Sache    // ********************************
61123682Sache    //        Create PLAF
62123682Sache    // ********************************
63123682Sache
64123682Sache    /**
65123682Sache     * Returns an instance of {@code BasicRadioButtonUI}.
66123682Sache     *
67123682Sache     * @param b a component
68123682Sache     * @return an instance of {@code BasicRadioButtonUI}
69123682Sache     */
70123682Sache    public static ComponentUI createUI(JComponent b) {
71123682Sache        AppContext appContext = AppContext.getAppContext();
72123682Sache        BasicRadioButtonUI radioButtonUI =
73123682Sache                (BasicRadioButtonUI) appContext.get(BASIC_RADIO_BUTTON_UI_KEY);
74123682Sache        if (radioButtonUI == null) {
75123682Sache            radioButtonUI = new BasicRadioButtonUI();
76123682Sache            appContext.put(BASIC_RADIO_BUTTON_UI_KEY, radioButtonUI);
77123682Sache        }
78123682Sache        return radioButtonUI;
79123682Sache    }
80123682Sache
81123682Sache    @Override
82123682Sache    protected String getPropertyPrefix() {
83123682Sache        return propertyPrefix;
84123682Sache    }
85123682Sache
86123682Sache    // ********************************
87123682Sache    //        Install PLAF
88123682Sache    // ********************************
89123682Sache    @Override
90123682Sache    protected void installDefaults(AbstractButton b) {
91123682Sache        super.installDefaults(b);
92123682Sache        if(!defaults_initialized) {
93123682Sache            icon = UIManager.getIcon(getPropertyPrefix() + "icon");
94123682Sache            defaults_initialized = true;
95123682Sache        }
96123682Sache    }
97
98    // ********************************
99    //        Uninstall PLAF
100    // ********************************
101    @Override
102    protected void uninstallDefaults(AbstractButton b) {
103        super.uninstallDefaults(b);
104        defaults_initialized = false;
105    }
106
107    /**
108     * Returns the default icon.
109     *
110     * @return the default icon
111     */
112    public Icon getDefaultIcon() {
113        return icon;
114    }
115
116    // ********************************
117    //        Install Listeners
118    // ********************************
119    @Override
120    protected void installListeners(AbstractButton button) {
121        super.installListeners(button);
122
123        // Only for JRadioButton
124        if (!(button instanceof JRadioButton))
125            return;
126
127        keyListener = createKeyListener();
128        button.addKeyListener(keyListener);
129
130        // Need to get traversal key event
131        button.setFocusTraversalKeysEnabled(false);
132
133        // Map actions to the arrow keys
134        button.getActionMap().put("Previous", new SelectPreviousBtn());
135        button.getActionMap().put("Next", new SelectNextBtn());
136
137        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
138            put(KeyStroke.getKeyStroke("UP"), "Previous");
139        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
140            put(KeyStroke.getKeyStroke("DOWN"), "Next");
141        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
142            put(KeyStroke.getKeyStroke("LEFT"), "Previous");
143        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
144            put(KeyStroke.getKeyStroke("RIGHT"), "Next");
145    }
146
147    // ********************************
148    //        UnInstall Listeners
149    // ********************************
150    @Override
151    protected void uninstallListeners(AbstractButton button) {
152        super.uninstallListeners(button);
153
154        // Only for JRadioButton
155        if (!(button instanceof JRadioButton))
156            return;
157
158        // Unmap actions from the arrow keys
159        button.getActionMap().remove("Previous");
160        button.getActionMap().remove("Next");
161        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
162                    .remove(KeyStroke.getKeyStroke("UP"));
163        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
164                    .remove(KeyStroke.getKeyStroke("DOWN"));
165        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
166                    .remove(KeyStroke.getKeyStroke("LEFT"));
167        button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
168                    .remove(KeyStroke.getKeyStroke("RIGHT"));
169
170        if (keyListener != null) {
171            button.removeKeyListener(keyListener);
172            keyListener = null;
173        }
174    }
175
176    /* These Dimensions/Rectangles are allocated once for all
177     * RadioButtonUI.paint() calls.  Re-using rectangles
178     * rather than allocating them in each paint call substantially
179     * reduced the time it took paint to run.  Obviously, this
180     * method can't be re-entered.
181     */
182    private static Dimension size = new Dimension();
183    private static Rectangle viewRect = new Rectangle();
184    private static Rectangle iconRect = new Rectangle();
185    private static Rectangle textRect = new Rectangle();
186
187    /**
188     * paint the radio button
189     */
190    @Override
191    public synchronized void paint(Graphics g, JComponent c) {
192        AbstractButton b = (AbstractButton) c;
193        ButtonModel model = b.getModel();
194
195        Font f = c.getFont();
196        g.setFont(f);
197        FontMetrics fm = SwingUtilities2.getFontMetrics(c, g, f);
198
199        Insets i = c.getInsets();
200        size = b.getSize(size);
201        viewRect.x = i.left;
202        viewRect.y = i.top;
203        viewRect.width = size.width - (i.right + viewRect.x);
204        viewRect.height = size.height - (i.bottom + viewRect.y);
205        iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
206        textRect.x = textRect.y = textRect.width = textRect.height = 0;
207
208        Icon altIcon = b.getIcon();
209        Icon selectedIcon = null;
210        Icon disabledIcon = null;
211
212        String text = SwingUtilities.layoutCompoundLabel(
213            c, fm, b.getText(), altIcon != null ? altIcon : getDefaultIcon(),
214            b.getVerticalAlignment(), b.getHorizontalAlignment(),
215            b.getVerticalTextPosition(), b.getHorizontalTextPosition(),
216            viewRect, iconRect, textRect,
217            b.getText() == null ? 0 : b.getIconTextGap());
218
219        // fill background
220        if(c.isOpaque()) {
221            g.setColor(b.getBackground());
222            g.fillRect(0,0, size.width, size.height);
223        }
224
225
226        // Paint the radio button
227        if(altIcon != null) {
228
229            if(!model.isEnabled()) {
230                if(model.isSelected()) {
231                   altIcon = b.getDisabledSelectedIcon();
232                } else {
233                   altIcon = b.getDisabledIcon();
234                }
235            } else if(model.isPressed() && model.isArmed()) {
236                altIcon = b.getPressedIcon();
237                if(altIcon == null) {
238                    // Use selected icon
239                    altIcon = b.getSelectedIcon();
240                }
241            } else if(model.isSelected()) {
242                if(b.isRolloverEnabled() && model.isRollover()) {
243                        altIcon = b.getRolloverSelectedIcon();
244                        if (altIcon == null) {
245                                altIcon = b.getSelectedIcon();
246                        }
247                } else {
248                        altIcon = b.getSelectedIcon();
249                }
250            } else if(b.isRolloverEnabled() && model.isRollover()) {
251                altIcon = b.getRolloverIcon();
252            }
253
254            if(altIcon == null) {
255                altIcon = b.getIcon();
256            }
257
258            altIcon.paintIcon(c, g, iconRect.x, iconRect.y);
259
260        } else {
261            getDefaultIcon().paintIcon(c, g, iconRect.x, iconRect.y);
262        }
263
264
265        // Draw the Text
266        if(text != null) {
267            View v = (View) c.getClientProperty(BasicHTML.propertyKey);
268            if (v != null) {
269                v.paint(g, textRect);
270            } else {
271                paintText(g, b, textRect, text);
272            }
273            if(b.hasFocus() && b.isFocusPainted() &&
274               textRect.width > 0 && textRect.height > 0 ) {
275                paintFocus(g, textRect, size);
276            }
277        }
278    }
279
280    /**
281     * Paints focused radio button.
282     *
283     * @param g an instance of {@code Graphics}
284     * @param textRect bounds
285     * @param size the size of radio button
286     */
287    protected void paintFocus(Graphics g, Rectangle textRect, Dimension size) {
288    }
289
290
291    /* These Insets/Rectangles are allocated once for all
292     * RadioButtonUI.getPreferredSize() calls.  Re-using rectangles
293     * rather than allocating them in each call substantially
294     * reduced the time it took getPreferredSize() to run.  Obviously,
295     * this method can't be re-entered.
296     */
297    private static Rectangle prefViewRect = new Rectangle();
298    private static Rectangle prefIconRect = new Rectangle();
299    private static Rectangle prefTextRect = new Rectangle();
300    private static Insets prefInsets = new Insets(0, 0, 0, 0);
301
302    /**
303     * The preferred size of the radio button
304     */
305    @Override
306    public Dimension getPreferredSize(JComponent c) {
307        if(c.getComponentCount() > 0) {
308            return null;
309        }
310
311        AbstractButton b = (AbstractButton) c;
312
313        String text = b.getText();
314
315        Icon buttonIcon = b.getIcon();
316        if(buttonIcon == null) {
317            buttonIcon = getDefaultIcon();
318        }
319
320        Font font = b.getFont();
321        FontMetrics fm = b.getFontMetrics(font);
322
323        prefViewRect.x = prefViewRect.y = 0;
324        prefViewRect.width = Short.MAX_VALUE;
325        prefViewRect.height = Short.MAX_VALUE;
326        prefIconRect.x = prefIconRect.y = prefIconRect.width = prefIconRect.height = 0;
327        prefTextRect.x = prefTextRect.y = prefTextRect.width = prefTextRect.height = 0;
328
329        SwingUtilities.layoutCompoundLabel(
330            c, fm, text, buttonIcon,
331            b.getVerticalAlignment(), b.getHorizontalAlignment(),
332            b.getVerticalTextPosition(), b.getHorizontalTextPosition(),
333            prefViewRect, prefIconRect, prefTextRect,
334            text == null ? 0 : b.getIconTextGap());
335
336        // find the union of the icon and text rects (from Rectangle.java)
337        int x1 = Math.min(prefIconRect.x, prefTextRect.x);
338        int x2 = Math.max(prefIconRect.x + prefIconRect.width,
339                          prefTextRect.x + prefTextRect.width);
340        int y1 = Math.min(prefIconRect.y, prefTextRect.y);
341        int y2 = Math.max(prefIconRect.y + prefIconRect.height,
342                          prefTextRect.y + prefTextRect.height);
343        int width = x2 - x1;
344        int height = y2 - y1;
345
346        prefInsets = b.getInsets(prefInsets);
347        width += prefInsets.left + prefInsets.right;
348        height += prefInsets.top + prefInsets.bottom;
349        return new Dimension(width, height);
350    }
351
352    /////////////////////////// Private functions ////////////////////////
353    /**
354     * Creates the key listener to handle tab navigation in JRadioButton Group.
355     */
356    private KeyListener createKeyListener() {
357         if (keyListener == null) {
358            keyListener = new KeyHandler();
359        }
360        return keyListener;
361    }
362
363
364    private boolean isValidRadioButtonObj(Object obj) {
365        return ((obj instanceof JRadioButton) &&
366                    ((JRadioButton) obj).isVisible() &&
367                    ((JRadioButton) obj).isEnabled());
368    }
369
370    /**
371     * Select radio button based on "Previous" or "Next" operation
372     *
373     * @param event, the event object.
374     * @param next, indicate if it's next one
375     */
376    private void selectRadioButton(ActionEvent event, boolean next) {
377        // Get the source of the event.
378        Object eventSrc = event.getSource();
379
380        // Check whether the source is JRadioButton, it so, whether it is visible
381        if (!isValidRadioButtonObj(eventSrc))
382            return;
383
384        ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc);
385        btnGroupInfo.selectNewButton(next);
386    }
387
388    /////////////////////////// Inner Classes ////////////////////////
389    @SuppressWarnings("serial")
390    private class SelectPreviousBtn extends AbstractAction {
391        public SelectPreviousBtn() {
392            super("Previous");
393        }
394
395        public void actionPerformed(ActionEvent e) {
396           BasicRadioButtonUI.this.selectRadioButton(e, false);
397        }
398    }
399
400    @SuppressWarnings("serial")
401    private class SelectNextBtn extends AbstractAction{
402        public SelectNextBtn() {
403            super("Next");
404        }
405
406        public void actionPerformed(ActionEvent e) {
407            BasicRadioButtonUI.this.selectRadioButton(e, true);
408        }
409    }
410
411    /**
412     * ButtonGroupInfo, used to get related info in button group
413     * for given radio button
414     */
415    private class ButtonGroupInfo {
416
417        JRadioButton activeBtn = null;
418
419        JRadioButton firstBtn = null;
420        JRadioButton lastBtn = null;
421
422        JRadioButton previousBtn = null;
423        JRadioButton nextBtn = null;
424
425        HashSet<JRadioButton> btnsInGroup = null;
426
427        boolean srcFound = false;
428        public ButtonGroupInfo(JRadioButton btn) {
429            activeBtn = btn;
430            btnsInGroup = new HashSet<JRadioButton>();
431        }
432
433        // Check if given object is in the button group
434        boolean containsInGroup(Object obj){
435           return btnsInGroup.contains(obj);
436        }
437
438        // Check if the next object to gain focus belongs
439        // to the button group or not
440        Component getFocusTransferBaseComponent(boolean next){
441            Component focusBaseComp = activeBtn;
442            Container container = focusBaseComp.getFocusCycleRootAncestor();
443            if (container != null) {
444                FocusTraversalPolicy policy = container.getFocusTraversalPolicy();
445                Component comp = next ? policy.getComponentAfter(container, activeBtn)
446                                      : policy.getComponentBefore(container, activeBtn);
447
448                // If next component in the button group, use last/first button as base focus
449                // otherwise, use the activeBtn as the base focus
450                if (containsInGroup(comp)) {
451                    focusBaseComp = next ? lastBtn : firstBtn;
452                }
453            }
454
455            return focusBaseComp;
456        }
457
458        boolean getButtonGroupInfo() {
459            if (activeBtn == null)
460                return false;
461
462            btnsInGroup.clear();
463
464            // Get the button model from the source.
465            ButtonModel model = activeBtn.getModel();
466            if (!(model instanceof DefaultButtonModel))
467                return false;
468
469            // If the button model is DefaultButtonModel, and use it, otherwise return.
470            DefaultButtonModel bm = (DefaultButtonModel) model;
471
472            // get the ButtonGroup of the button from the button model
473            ButtonGroup group = bm.getGroup();
474            if (group == null)
475                return false;
476
477            // Get all the buttons in the group
478            Enumeration<AbstractButton> e = group.getElements();
479            if (e == null)
480                return false;
481
482            while (e.hasMoreElements()) {
483                AbstractButton curElement = e.nextElement();
484                if (!isValidRadioButtonObj(curElement))
485                    continue;
486
487                btnsInGroup.add((JRadioButton) curElement);
488
489                // If firstBtn is not set yet, curElement is that first button
490                if (null == firstBtn)
491                    firstBtn = (JRadioButton) curElement;
492
493                if (activeBtn == curElement)
494                    srcFound = true;
495                else if (!srcFound) {
496                    // The source has not been yet found and the current element
497                    // is the last previousBtn
498                    previousBtn = (JRadioButton) curElement;
499                } else if (nextBtn == null) {
500                    // The source has been found and the current element
501                    // is the next valid button of the list
502                    nextBtn = (JRadioButton) curElement;
503                }
504
505                // Set new last "valid" JRadioButton of the list
506                lastBtn = (JRadioButton) curElement;
507            }
508
509            return true;
510        }
511
512        /**
513          * Find the new radio button that focus needs to be
514          * moved to in the group, select the button
515          *
516          * @param next, indicate if it's arrow up/left or down/right
517          */
518        void selectNewButton(boolean next) {
519            if (!getButtonGroupInfo())
520                return;
521
522            if (srcFound) {
523                JRadioButton newSelectedBtn = null;
524                if (next) {
525                    // Select Next button. Cycle to the first button if the source
526                    // button is the last of the group.
527                    newSelectedBtn = (null == nextBtn) ? firstBtn : nextBtn;
528                } else {
529                    // Select previous button. Cycle to the last button if the source
530                    // button is the first button of the group.
531                    newSelectedBtn = (null == previousBtn) ? lastBtn : previousBtn;
532                }
533                if (newSelectedBtn != null &&
534                    (newSelectedBtn != activeBtn)) {
535                    newSelectedBtn.requestFocusInWindow();
536                    newSelectedBtn.setSelected(true);
537                }
538            }
539        }
540
541        /**
542          * Find the button group the passed in JRadioButton belongs to, and
543          * move focus to next component of the last button in the group
544          * or previous component of first button
545          *
546          * @param next, indicate if jump to next component or previous
547          */
548        void jumpToNextComponent(boolean next) {
549            if (!getButtonGroupInfo()){
550                // In case the button does not belong to any group, it needs
551                // to be treated as a component
552                if (activeBtn != null){
553                    lastBtn = activeBtn;
554                    firstBtn = activeBtn;
555                }
556                else
557                    return;
558            }
559
560            // Update the component we will use as base to transfer
561            // focus from
562            JComponent compTransferFocusFrom = activeBtn;
563
564            // If next component in the parent window is not in
565            // the button group, current active button will be
566            // base, otherwise, the base will be first or last
567            // button in the button group
568            Component focusBase = getFocusTransferBaseComponent(next);
569            if (focusBase != null){
570                if (next) {
571                    KeyboardFocusManager.
572                        getCurrentKeyboardFocusManager().focusNextComponent(focusBase);
573                } else {
574                    KeyboardFocusManager.
575                        getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase);
576                }
577            }
578        }
579    }
580
581    /**
582     * Radiobutton KeyListener
583     */
584    private class KeyHandler implements KeyListener {
585
586        // This listener checks if the key event is a focus traversal key event
587        // on a radio button, consume the event if so and move the focus
588        // to next/previous component
589        public void keyPressed(KeyEvent e) {
590            AWTKeyStroke stroke = AWTKeyStroke.getAWTKeyStrokeForEvent(e);
591            if (stroke != null && e.getSource() instanceof JRadioButton) {
592                JRadioButton source = (JRadioButton) e.getSource();
593                boolean next = isFocusTraversalKey(source,
594                        KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
595                        stroke);
596                if (next || isFocusTraversalKey(source,
597                        KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
598                        stroke)) {
599                    e.consume();
600                    ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo(source);
601                    btnGroupInfo.jumpToNextComponent(next);
602                }
603            }
604        }
605
606        private boolean isFocusTraversalKey(JComponent c, int id,
607                                            AWTKeyStroke stroke) {
608            Set<AWTKeyStroke> keys = c.getFocusTraversalKeys(id);
609            return keys != null && keys.contains(stroke);
610        }
611
612        public void keyReleased(KeyEvent e) {
613        }
614
615        public void keyTyped(KeyEvent e) {
616        }
617    }
618}
619