1/*
2 * Copyright (c) 2002, 2013, 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 sun.awt.X11;
27
28import java.awt.*;
29import java.awt.peer.*;
30import java.awt.event.*;
31import java.awt.image.BufferedImage;
32import javax.swing.plaf.basic.BasicGraphicsUtils;
33import java.awt.geom.AffineTransform;
34import java.util.Objects;
35
36import sun.util.logging.PlatformLogger;
37
38class XCheckboxPeer extends XComponentPeer implements CheckboxPeer {
39
40    private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.X11.XCheckboxPeer");
41
42    private static final Insets focusInsets = new Insets(0,0,0,0);
43    private static final Insets borderInsets = new Insets(2,2,2,2);
44    private static final int checkBoxInsetFromText = 2;
45
46    //The check mark is less common than a plain "depressed" button,
47    //so don't use the checkmark.
48    // The checkmark shape:
49    private static final double MASTER_SIZE = 128.0;
50    private static final Polygon MASTER_CHECKMARK = new Polygon(
51        new int[] {1, 25,56,124,124,85, 64},  // X-coords
52        new int[] {59,35,67,  0, 12,66,123},  // Y-coords
53      7);
54
55    private Shape myCheckMark;
56
57    private Color focusColor = SystemColor.windowText;
58
59    private boolean pressed;
60    private boolean armed;
61    private boolean selected;
62
63    private Rectangle textRect;
64    private Rectangle focusRect;
65    private int checkBoxSize;
66    private int cbX;
67    private int cbY;
68
69    String label;
70    CheckboxGroup checkBoxGroup;
71
72    XCheckboxPeer(Checkbox target) {
73        super(target);
74        pressed = false;
75        armed = false;
76        selected = target.getState();
77        label = target.getLabel();
78        if ( label == null ) {
79            label = "";
80        }
81        checkBoxGroup = target.getCheckboxGroup();
82        updateMotifColors(getPeerBackground());
83    }
84
85    public void preInit(XCreateWindowParams params) {
86        // Put this here so it is executed before layout() is called from
87        // setFont() in XComponent.postInit()
88        textRect = new Rectangle();
89        focusRect = new Rectangle();
90        super.preInit(params);
91    }
92
93    public boolean isFocusable() { return true; }
94
95    public void focusGained(FocusEvent e) {
96        // TODO: only need to paint the focus bit
97        super.focusGained(e);
98        repaint();
99    }
100
101    public void focusLost(FocusEvent e) {
102        // TODO: only need to paint the focus bit?
103        super.focusLost(e);
104        repaint();
105    }
106
107
108    void handleJavaKeyEvent(KeyEvent e) {
109        int i = e.getID();
110        switch (i) {
111          case KeyEvent.KEY_PRESSED:
112              keyPressed(e);
113              break;
114          case KeyEvent.KEY_RELEASED:
115              keyReleased(e);
116              break;
117          case KeyEvent.KEY_TYPED:
118              keyTyped(e);
119              break;
120        }
121    }
122
123    public void keyTyped(KeyEvent e) {}
124
125    public void keyPressed(KeyEvent e) {
126        if (e.getKeyCode() == KeyEvent.VK_SPACE)
127        {
128            //pressed=true;
129            //armed=true;
130            //selected=!selected;
131            action(!selected);
132            //repaint();  // Gets the repaint from action()
133        }
134
135    }
136
137    public void keyReleased(KeyEvent e) {}
138
139    @Override
140    public void setLabel(String label) {
141        if (label == null) {
142            label = "";
143        }
144        if (!label.equals(this.label)) {
145            this.label = label;
146            layout();
147            repaint();
148        }
149    }
150
151    void handleJavaMouseEvent(MouseEvent e) {
152        super.handleJavaMouseEvent(e);
153        int i = e.getID();
154        switch (i) {
155          case MouseEvent.MOUSE_PRESSED:
156              mousePressed(e);
157              break;
158          case MouseEvent.MOUSE_RELEASED:
159              mouseReleased(e);
160              break;
161          case MouseEvent.MOUSE_ENTERED:
162              mouseEntered(e);
163              break;
164          case MouseEvent.MOUSE_EXITED:
165              mouseExited(e);
166              break;
167          case MouseEvent.MOUSE_CLICKED:
168              mouseClicked(e);
169              break;
170        }
171    }
172
173    public void mousePressed(MouseEvent e) {
174        if (XToolkit.isLeftMouseButton(e)) {
175            Checkbox cb = (Checkbox) e.getSource();
176
177            if (cb.contains(e.getX(), e.getY())) {
178                if (log.isLoggable(PlatformLogger.Level.FINER)) {
179                    log.finer("mousePressed() on " + target.getName() + " : armed = " + armed + ", pressed = " + pressed
180                              + ", selected = " + selected + ", enabled = " + isEnabled());
181                }
182                if (!isEnabled()) {
183                    // Disabled buttons ignore all input...
184                    return;
185                }
186                if (!armed) {
187                    armed = true;
188                }
189                pressed = true;
190                repaint();
191            }
192        }
193    }
194
195    public void mouseReleased(MouseEvent e) {
196        if (log.isLoggable(PlatformLogger.Level.FINER)) {
197            log.finer("mouseReleased() on " + target.getName() + ": armed = " + armed + ", pressed = " + pressed
198                      + ", selected = " + selected + ", enabled = " + isEnabled());
199        }
200        boolean sendEvent = false;
201        if (XToolkit.isLeftMouseButton(e)) {
202            // TODO: Multiclick Threshold? - see BasicButtonListener.java
203            if (armed) {
204                //selected = !selected;
205                // send action event
206                //action(e.getWhen(),e.getModifiers());
207                sendEvent = true;
208            }
209            pressed = false;
210            armed = false;
211            if (sendEvent) {
212                action(!selected);  // Also gets repaint in action()
213            }
214            else {
215                repaint();
216            }
217        }
218    }
219
220    public void mouseEntered(MouseEvent e) {
221        if (log.isLoggable(PlatformLogger.Level.FINER)) {
222            log.finer("mouseEntered() on " + target.getName() + ": armed = " + armed + ", pressed = " + pressed
223                      + ", selected = " + selected + ", enabled = " + isEnabled());
224        }
225        if (pressed) {
226            armed = true;
227            repaint();
228        }
229    }
230
231    public void mouseExited(MouseEvent e) {
232        if (log.isLoggable(PlatformLogger.Level.FINER)) {
233            log.finer("mouseExited() on " + target.getName() + ": armed = " + armed + ", pressed = " + pressed
234                      + ", selected = " + selected + ", enabled = " + isEnabled());
235        }
236        if (armed) {
237            armed = false;
238            repaint();
239        }
240    }
241
242    public void mouseClicked(MouseEvent e) {}
243
244    public Dimension getMinimumSize() {
245        /*
246         * Spacing (number of pixels between check mark and label text) is
247         * currently set to 0, but in case it ever changes we have to add
248         * it. 8 is a heuristic number. Indicator size depends on font
249         * height, so we don't need to include it in checkbox's height
250         * calculation.
251         */
252        FontMetrics fm = getFontMetrics(getPeerFont());
253
254        int wdth = fm.stringWidth(label) + getCheckboxSize(fm) + (2 * checkBoxInsetFromText) + 8;
255        int hght = Math.max(fm.getHeight() + 8, 15);
256
257        return new Dimension(wdth, hght);
258    }
259
260    private int getCheckboxSize(FontMetrics fm) {
261        // the motif way of sizing is a bit inscutible, but this
262        // is a fair approximation
263        return (fm.getHeight() * 76 / 100) - 1;
264    }
265
266    public void setBackground(Color c) {
267        updateMotifColors(c);
268        super.setBackground(c);
269    }
270
271    /*
272     * Layout the checkbox/radio button and text label
273     */
274    public void layout() {
275        Dimension size = getPeerSize();
276        Font f = getPeerFont();
277        FontMetrics fm = getFontMetrics(f);
278        String text = label;
279
280        checkBoxSize = getCheckboxSize(fm);
281
282        // Note - Motif appears to use an left inset that is slightly
283        // scaled to the checkbox/font size.
284        cbX = borderInsets.left + checkBoxInsetFromText;
285        cbY = size.height / 2 - checkBoxSize / 2;
286        int minTextX = borderInsets.left + 2 * checkBoxInsetFromText + checkBoxSize;
287        // FIXME: will need to account for alignment?
288        // FIXME: call layout() on alignment changes
289        //textRect.width = fm.stringWidth(text);
290        textRect.width = fm.stringWidth(text == null ? "" : text);
291        textRect.height = fm.getHeight();
292
293        textRect.x = Math.max(minTextX, size.width / 2 - textRect.width / 2);
294        textRect.y = (size.height - textRect.height) / 2;
295
296        focusRect.x = focusInsets.left;
297        focusRect.y = focusInsets.top;
298        focusRect.width = size.width-(focusInsets.left+focusInsets.right)-1;
299        focusRect.height = size.height-(focusInsets.top+focusInsets.bottom)-1;
300
301        double fsize = (double) checkBoxSize;
302        myCheckMark = AffineTransform.getScaleInstance(fsize / MASTER_SIZE, fsize / MASTER_SIZE).createTransformedShape(MASTER_CHECKMARK);
303    }
304    @Override
305    void paintPeer(final Graphics g) {
306        //layout();
307        Dimension size = getPeerSize();
308        Font f = getPeerFont();
309        flush();
310        g.setColor(getPeerBackground());   // erase the existing button
311        g.fillRect(0,0, size.width, size.height);
312        if (label != null) {
313            g.setFont(f);
314            paintText(g, textRect, label);
315        }
316
317        if (hasFocus()) {
318            paintFocus(g,
319                       focusRect.x,
320                       focusRect.y,
321                       focusRect.width,
322                       focusRect.height);
323        }
324        // Paint the checkbox or radio button
325        if (checkBoxGroup == null) {
326            paintCheckbox(g, cbX, cbY, checkBoxSize, checkBoxSize);
327        }
328        else {
329            paintRadioButton(g, cbX, cbY, checkBoxSize, checkBoxSize);
330        }
331        flush();
332    }
333
334    // You'll note this looks suspiciously like paintBorder
335    public void paintCheckbox(Graphics g,
336                              int x, int y, int w, int h) {
337        boolean useBufferedImage = false;
338        BufferedImage buffer = null;
339        Graphics2D g2 = null;
340        int rx = x;
341        int ry = y;
342        if (!(g instanceof Graphics2D)) {
343            // Fix for 5045936. While printing, g is an instance of
344            //   sun.print.ProxyPrintGraphics which extends Graphics. So
345            //   we use a separate buffered image and its graphics is
346            //   always Graphics2D instance
347            buffer = graphicsConfig.createCompatibleImage(w, h);
348            g2 = buffer.createGraphics();
349            useBufferedImage = true;
350            rx = 0;
351            ry = 0;
352        }
353        else {
354            g2 = (Graphics2D)g;
355        }
356        try {
357            drawMotif3DRect(g2, rx, ry, w-1, h-1, armed | selected);
358
359            // then paint the check
360            g2.setColor((armed | selected) ? selectColor : getPeerBackground());
361            g2.fillRect(rx+1, ry+1, w-2, h-2);
362
363            if (armed | selected) {
364                //Paint the check
365
366                // FIXME: is this the right color?
367                g2.setColor(getPeerForeground());
368
369                AffineTransform af = g2.getTransform();
370                g2.setTransform(AffineTransform.getTranslateInstance(rx,ry));
371                g2.fill(myCheckMark);
372                g2.setTransform(af);
373            }
374        } finally {
375            if (useBufferedImage) {
376                g2.dispose();
377            }
378        }
379        if (useBufferedImage) {
380            g.drawImage(buffer, x, y, null);
381        }
382    }
383
384    public void paintRadioButton(Graphics g, int x, int y, int w, int h) {
385
386        g.setColor((armed | selected) ? darkShadow : lightShadow);
387        g.drawArc(x-1, y-1, w+2, h+2, 45, 180);
388
389        g.setColor((armed | selected) ? lightShadow : darkShadow);
390        g.drawArc(x-1, y-1, w+2, h+2, 45, -180);
391
392        if (armed | selected) {
393            g.setColor(selectColor);
394            g.fillArc(x+1, y+1, w-1, h-1, 0, 360);
395        }
396    }
397
398    protected void paintText(Graphics g, Rectangle textRect, String text) {
399        FontMetrics fm = g.getFontMetrics();
400
401        int mnemonicIndex = -1;
402
403        if(isEnabled()) {
404            /*** paint the text normally */
405            g.setColor(getPeerForeground());
406            BasicGraphicsUtils.drawStringUnderlineCharAt(g,text,mnemonicIndex , textRect.x , textRect.y + fm.getAscent() );
407        }
408        else {
409            /*** paint the text disabled ***/
410            g.setColor(getPeerBackground().brighter());
411
412            BasicGraphicsUtils.drawStringUnderlineCharAt(g,text, mnemonicIndex,
413                                                         textRect.x, textRect.y + fm.getAscent());
414            g.setColor(getPeerBackground().darker());
415            BasicGraphicsUtils.drawStringUnderlineCharAt(g,text, mnemonicIndex,
416                                                         textRect.x - 1, textRect.y + fm.getAscent() - 1);
417        }
418    }
419
420    // TODO: copied directly from XButtonPeer.  Should probabaly be shared
421    protected void paintFocus(Graphics g, int x, int y, int w, int h) {
422        g.setColor(focusColor);
423        g.drawRect(x,y,w,h);
424    }
425
426    @Override
427    public void setState(boolean state) {
428        if (selected != state) {
429            selected = state;
430            repaint();
431        }
432    }
433
434    @Override
435    public void setCheckboxGroup(final CheckboxGroup g) {
436        if (!Objects.equals(g, checkBoxGroup)) {
437            // If changed from grouped/ungrouped, need to repaint()
438            checkBoxGroup = g;
439            repaint();
440        }
441    }
442
443    // NOTE: This method is called by privileged threads.
444    //       DO NOT INVOKE CLIENT CODE ON THIS THREAD!
445    // From MCheckboxPeer
446    void action(boolean state) {
447        final Checkbox cb = (Checkbox)target;
448        final boolean newState = state;
449        XToolkit.executeOnEventHandlerThread(cb, new Runnable() {
450                public void run() {
451                    CheckboxGroup cbg = checkBoxGroup;
452                    // Bugid 4039594. If this is the current Checkbox in
453                    // a CheckboxGroup, then return to prevent deselection.
454                    // Otherwise, it's logical state will be turned off,
455                    // but it will appear on.
456                    if ((cbg != null) && (cbg.getSelectedCheckbox() == cb) &&
457                        cb.getState()) {
458                        //inUpCall = false;
459                        cb.setState(true);
460                        return;
461                    }
462                    // All clear - set the new state
463                    cb.setState(newState);
464                    notifyStateChanged(newState);
465                }
466            });
467    }
468
469    void notifyStateChanged(boolean state) {
470        Checkbox cb = (Checkbox) target;
471        ItemEvent e = new ItemEvent(cb,
472                                    ItemEvent.ITEM_STATE_CHANGED,
473                                    cb.getLabel(),
474                                    state ? ItemEvent.SELECTED : ItemEvent.DESELECTED);
475        postEvent(e);
476    }
477}
478