1/*
2 * Copyright (c) 1997, 2016, 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.basic;
27
28import java.awt.*;
29import java.awt.event.*;
30import javax.swing.*;
31import javax.swing.border.*;
32import javax.swing.plaf.*;
33import javax.swing.text.View;
34import sun.swing.SwingUtilities2;
35import sun.awt.AppContext;
36import java.util.Enumeration;
37import java.util.HashSet;
38import java.util.Set;
39
40/**
41 * RadioButtonUI implementation for BasicRadioButtonUI
42 *
43 * @author Jeff Dinkins
44 */
45public class BasicRadioButtonUI extends BasicToggleButtonUI
46{
47    private static final Object BASIC_RADIO_BUTTON_UI_KEY = new Object();
48
49    /**
50     * The icon.
51     */
52    protected Icon icon;
53
54    private boolean defaults_initialized = false;
55
56    private static final String propertyPrefix = "RadioButton" + ".";
57
58    private KeyListener keyListener = null;
59
60    // ********************************
61    //        Create PLAF
62    // ********************************
63
64    /**
65     * Returns an instance of {@code BasicRadioButtonUI}.
66     *
67     * @param b a component
68     * @return an instance of {@code BasicRadioButtonUI}
69     */
70    public static ComponentUI createUI(JComponent b) {
71        AppContext appContext = AppContext.getAppContext();
72        BasicRadioButtonUI radioButtonUI =
73                (BasicRadioButtonUI) appContext.get(BASIC_RADIO_BUTTON_UI_KEY);
74        if (radioButtonUI == null) {
75            radioButtonUI = new BasicRadioButtonUI();
76            appContext.put(BASIC_RADIO_BUTTON_UI_KEY, radioButtonUI);
77        }
78        return radioButtonUI;
79    }
80
81    @Override
82    protected String getPropertyPrefix() {
83        return propertyPrefix;
84    }
85
86    // ********************************
87    //        Install PLAF
88    // ********************************
89    @Override
90    protected void installDefaults(AbstractButton b) {
91        super.installDefaults(b);
92        if(!defaults_initialized) {
93            icon = UIManager.getIcon(getPropertyPrefix() + "icon");
94            defaults_initialized = true;
95        }
96    }
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            return firstBtn;
442        }
443
444        boolean getButtonGroupInfo() {
445            if (activeBtn == null)
446                return false;
447
448            btnsInGroup.clear();
449
450            // Get the button model from the source.
451            ButtonModel model = activeBtn.getModel();
452            if (!(model instanceof DefaultButtonModel))
453                return false;
454
455            // If the button model is DefaultButtonModel, and use it, otherwise return.
456            DefaultButtonModel bm = (DefaultButtonModel) model;
457
458            // get the ButtonGroup of the button from the button model
459            ButtonGroup group = bm.getGroup();
460            if (group == null)
461                return false;
462
463            // Get all the buttons in the group
464            Enumeration<AbstractButton> e = group.getElements();
465            if (e == null)
466                return false;
467
468            while (e.hasMoreElements()) {
469                AbstractButton curElement = e.nextElement();
470                if (!isValidRadioButtonObj(curElement))
471                    continue;
472
473                btnsInGroup.add((JRadioButton) curElement);
474
475                // If firstBtn is not set yet, curElement is that first button
476                if (null == firstBtn)
477                    firstBtn = (JRadioButton) curElement;
478
479                if (activeBtn == curElement)
480                    srcFound = true;
481                else if (!srcFound) {
482                    // The source has not been yet found and the current element
483                    // is the last previousBtn
484                    previousBtn = (JRadioButton) curElement;
485                } else if (nextBtn == null) {
486                    // The source has been found and the current element
487                    // is the next valid button of the list
488                    nextBtn = (JRadioButton) curElement;
489                }
490
491                // Set new last "valid" JRadioButton of the list
492                lastBtn = (JRadioButton) curElement;
493            }
494
495            return true;
496        }
497
498        /**
499          * Find the new radio button that focus needs to be
500          * moved to in the group, select the button
501          *
502          * @param next, indicate if it's arrow up/left or down/right
503          */
504        void selectNewButton(boolean next) {
505            if (!getButtonGroupInfo())
506                return;
507
508            if (srcFound) {
509                JRadioButton newSelectedBtn = null;
510                if (next) {
511                    // Select Next button. Cycle to the first button if the source
512                    // button is the last of the group.
513                    newSelectedBtn = (null == nextBtn) ? firstBtn : nextBtn;
514                } else {
515                    // Select previous button. Cycle to the last button if the source
516                    // button is the first button of the group.
517                    newSelectedBtn = (null == previousBtn) ? lastBtn : previousBtn;
518                }
519                if (newSelectedBtn != null &&
520                    (newSelectedBtn != activeBtn)) {
521                    newSelectedBtn.requestFocusInWindow();
522                    newSelectedBtn.setSelected(true);
523                }
524            }
525        }
526
527        /**
528          * Find the button group the passed in JRadioButton belongs to, and
529          * move focus to next component of the last button in the group
530          * or previous component of first button
531          *
532          * @param next, indicate if jump to next component or previous
533          */
534        void jumpToNextComponent(boolean next) {
535            if (!getButtonGroupInfo()){
536                // In case the button does not belong to any group, it needs
537                // to be treated as a component
538                if (activeBtn != null){
539                    lastBtn = activeBtn;
540                    firstBtn = activeBtn;
541                }
542                else
543                    return;
544            }
545
546            // Update the component we will use as base to transfer
547            // focus from
548            JComponent compTransferFocusFrom = activeBtn;
549
550            // If next component in the parent window is not in
551            // the button group, current active button will be
552            // base, otherwise, the base will be first or last
553            // button in the button group
554            Component focusBase = getFocusTransferBaseComponent(next);
555            if (focusBase != null){
556                if (next) {
557                    KeyboardFocusManager.
558                        getCurrentKeyboardFocusManager().focusNextComponent(focusBase);
559                } else {
560                    KeyboardFocusManager.
561                        getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase);
562                }
563            }
564        }
565    }
566
567    /**
568     * Radiobutton KeyListener
569     */
570    private class KeyHandler implements KeyListener {
571
572        // This listener checks if the key event is a focus traversal key event
573        // on a radio button, consume the event if so and move the focus
574        // to next/previous component
575        public void keyPressed(KeyEvent e) {
576            AWTKeyStroke stroke = AWTKeyStroke.getAWTKeyStrokeForEvent(e);
577            if (stroke != null && e.getSource() instanceof JRadioButton) {
578                JRadioButton source = (JRadioButton) e.getSource();
579                boolean next = isFocusTraversalKey(source,
580                        KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
581                        stroke);
582                if (next || isFocusTraversalKey(source,
583                        KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
584                        stroke)) {
585                    e.consume();
586                    ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo(source);
587                    btnGroupInfo.jumpToNextComponent(next);
588                }
589            }
590        }
591
592        private boolean isFocusTraversalKey(JComponent c, int id,
593                                            AWTKeyStroke stroke) {
594            Set<AWTKeyStroke> keys = c.getFocusTraversalKeys(id);
595            return keys != null && keys.contains(stroke);
596        }
597
598        public void keyReleased(KeyEvent e) {
599        }
600
601        public void keyTyped(KeyEvent e) {
602        }
603    }
604}
605