1/*
2 * Copyright (c) 2003, 2017, 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 sun.util.logging.PlatformLogger;
32
33// FIXME: tab traversal should be disabled when mouse is captured (4816336)
34
35// FIXME: key and mouse events should not be delivered to listeners when the Choice is unfurled.  Must override handleNativeKey/MouseEvent (4816336)
36
37// FIXME: test programmatic add/remove/clear/etc
38
39// FIXME: account for unfurling at the edge of the screen
40// Note: can't set x,y on layout(), 'cause moving the top-level to the
41// edge of the screen won't call layout().  Just do it on paint, I guess
42
43// TODO: make painting more efficient (i.e. when down arrow is pressed, only two items should need to be repainted.
44
45public final class XChoicePeer extends XComponentPeer implements ChoicePeer, ToplevelStateListener {
46    private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.X11.XChoicePeer");
47
48    private static final int MAX_UNFURLED_ITEMS = 10;  // Maximum number of
49    // items to be displayed
50    // at a time in an
51    // unfurled Choice
52    // Description of these constants in ListHelper
53    public static final int TEXT_SPACE = 1;
54    public static final int BORDER_WIDTH = 1;
55    public static final int ITEM_MARGIN = 1;
56    public static final int SCROLLBAR_WIDTH = 15;
57
58
59    // SHARE THESE!
60    private static final Insets focusInsets = new Insets(0,0,0,0);
61
62
63    static final int WIDGET_OFFSET = 18;
64
65    // Stolen from Tiny
66    static final int            TEXT_XPAD = 8;
67    static final int            TEXT_YPAD = 6;
68
69    // FIXME: Motif uses a different focus color for the item within
70    // the unfurled Choice list and for when the Choice itself is focused and
71    // popped up.
72    static final Color focusColor = Color.black;
73
74    // TODO: there is a time value that the mouse is held down.  If short
75    // enough,  the Choice stays popped down.  If long enough, Choice
76    // is furled when the mouse is released
77
78    private boolean unfurled = false;        // Choice list is popped down
79
80    private boolean dragging = false;        // Mouse was pressed and is being
81                                             // dragged over the (unfurled)
82                                             // Choice
83
84    private boolean mouseInSB = false;       // Mouse is interacting with the
85                                             // scrollbar
86
87    private boolean firstPress = false;      // mouse was pressed on
88                                             // furled Choice so we
89                                             // not need to furl the
90                                             // Choice when MOUSE_RELEASED occurred
91
92    // 6425067. Mouse was pressed on furled choice and dropdown list appeared over Choice itself
93    // and then there were no mouse movements until MOUSE_RELEASE.
94    // This scenario leads to ItemStateChanged as the choice logic uses
95    // MouseReleased event to send ItemStateChanged. To prevent it we should
96    // use a combination of firstPress and wasDragged variables.
97    // The only difference in dragging and wasDragged is: last one will not
98    // set to false on mouse ungrab. It become false after MouseRelased() finishes.
99    private boolean wasDragged = false;
100    private ListHelper helper;
101    private UnfurledChoice unfurledChoice;
102
103    // TODO: Choice remembers where it was scrolled to when unfurled - it's not
104    // always to the currently selected item.
105
106    // Indicates whether or not to paint selected item in the choice.
107    // Default is to paint
108    private boolean drawSelectedItem = true;
109
110    // If set, indicates components under which choice popup should be showed.
111    // The choice's popup width and location should be adjust to appear
112    // under both choice and alignUnder component.
113    private Component alignUnder;
114
115    // If cursor is outside of an unfurled Choice when the mouse is
116    // released, Choice item should NOT be updated.  Remember the proper index.
117    private int dragStartIdx = -1;
118
119    // Holds the listener (XFileDialogPeer) which the processing events from the choice
120    // See 6240074 for more information
121    private XChoicePeerListener choiceListener;
122
123    XChoicePeer(Choice target) {
124        super(target);
125    }
126
127    void preInit(XCreateWindowParams params) {
128        super.preInit(params);
129        Choice target = (Choice)this.target;
130        int numItems = target.getItemCount();
131        unfurledChoice = new UnfurledChoice(target);
132        getToplevelXWindow().addToplevelStateListener(this);
133        helper = new ListHelper(unfurledChoice,
134                                getGUIcolors(),
135                                numItems,
136                                false,
137                                true,
138                                false,
139                                target.getFont(),
140                                MAX_UNFURLED_ITEMS,
141                                TEXT_SPACE,
142                                ITEM_MARGIN,
143                                BORDER_WIDTH,
144                                SCROLLBAR_WIDTH);
145    }
146
147    void postInit(XCreateWindowParams params) {
148        super.postInit(params);
149        Choice target = (Choice)this.target;
150        int numItems = target.getItemCount();
151
152        // Add all items
153        for (int i = 0; i < numItems; i++) {
154            helper.add(target.getItem(i));
155        }
156        if (!helper.isEmpty()) {
157            helper.select(target.getSelectedIndex());
158            helper.setFocusedIndex(target.getSelectedIndex());
159        }
160        helper.updateColors(getGUIcolors());
161        updateMotifColors(getPeerBackground());
162    }
163
164    public boolean isFocusable() { return true; }
165
166    // 6399679. check if super.setBounds() actually changes the size of the
167    // component and then compare current Choice size with a new one. If
168    // they differs then hide dropdown menu
169    public void setBounds(int x, int y, int width, int height, int op) {
170        int oldX = this.x;
171        int oldY = this.y;
172        int oldWidth = this.width;
173        int oldHeight = this.height;
174        super.setBounds(x, y, width, height, op);
175        if (unfurled && (oldX != this.x || oldY != this.y || oldWidth != this.width || oldHeight != this.height) ) {
176            hidePopdownMenu();
177        }
178    }
179
180    public void focusGained(FocusEvent e) {
181        // TODO: only need to paint the focus bit
182        super.focusGained(e);
183        repaint();
184    }
185
186    /*
187     * Fix for 6246503 : Disabling a choice after selection locks keyboard, mouse and makes the system unusable, Xtoolkit
188     * if setEnabled(false) invoked we should close opened choice in
189     * order to prevent keyboard/mouse lock.
190     */
191    public void setEnabled(boolean value) {
192        super.setEnabled(value);
193        helper.updateColors(getGUIcolors());
194        if (!value && unfurled){
195            hidePopdownMenu();
196        }
197    }
198
199    public void focusLost(FocusEvent e) {
200        // TODO: only need to paint the focus bit?
201        super.focusLost(e);
202        repaint();
203    }
204
205    void ungrabInputImpl() {
206        if (unfurled) {
207            unfurled = false;
208            dragging = false;
209            mouseInSB = false;
210            unfurledChoice.setVisible(false);
211        }
212
213        super.ungrabInputImpl();
214    }
215
216    void handleJavaKeyEvent(KeyEvent e) {
217        if (e.getID() == KeyEvent.KEY_PRESSED) {
218            keyPressed(e);
219        }
220    }
221
222    public void keyPressed(KeyEvent e) {
223        switch(e.getKeyCode()) {
224            // UP & DOWN are same if furled or unfurled
225          case KeyEvent.VK_DOWN:
226          case KeyEvent.VK_KP_DOWN: {
227              if (helper.getItemCount() > 1) {
228                  helper.down();
229                  int newIdx = helper.getSelectedIndex();
230
231                  ((Choice)target).select(newIdx);
232                  postEvent(new ItemEvent((Choice)target,
233                                          ItemEvent.ITEM_STATE_CHANGED,
234                                          ((Choice)target).getItem(newIdx),
235                                          ItemEvent.SELECTED));
236                  repaint();
237              }
238              break;
239          }
240          case KeyEvent.VK_UP:
241          case KeyEvent.VK_KP_UP: {
242              if (helper.getItemCount() > 1) {
243                  helper.up();
244                  int newIdx = helper.getSelectedIndex();
245
246                  ((Choice)target).select(newIdx);
247                  postEvent(new ItemEvent((Choice)target,
248                                          ItemEvent.ITEM_STATE_CHANGED,
249                                          ((Choice)target).getItem(newIdx),
250                                          ItemEvent.SELECTED));
251                  repaint();
252              }
253              break;
254          }
255          case KeyEvent.VK_PAGE_DOWN:
256              if (unfurled && !dragging) {
257                  int oldIdx = helper.getSelectedIndex();
258                  helper.pageDown();
259                  int newIdx = helper.getSelectedIndex();
260                  if (oldIdx != newIdx) {
261                      ((Choice)target).select(newIdx);
262                      postEvent(new ItemEvent((Choice)target,
263                                              ItemEvent.ITEM_STATE_CHANGED,
264                                              ((Choice)target).getItem(newIdx),
265                                              ItemEvent.SELECTED));
266                      repaint();
267                  }
268              }
269              break;
270          case KeyEvent.VK_PAGE_UP:
271              if (unfurled && !dragging) {
272                  int oldIdx = helper.getSelectedIndex();
273                  helper.pageUp();
274                  int newIdx = helper.getSelectedIndex();
275                  if (oldIdx != newIdx) {
276                      ((Choice)target).select(newIdx);
277                      postEvent(new ItemEvent((Choice)target,
278                                              ItemEvent.ITEM_STATE_CHANGED,
279                                              ((Choice)target).getItem(newIdx),
280                                              ItemEvent.SELECTED));
281                      repaint();
282                  }
283              }
284              break;
285          case KeyEvent.VK_ESCAPE:
286          case KeyEvent.VK_ENTER:
287              if (unfurled) {
288                  if (dragging){
289                      if (e.getKeyCode() == KeyEvent.VK_ESCAPE){
290                          //This also happens on
291                          // - MouseButton2,3, etc. press
292                          // - ENTER press
293                          helper.select(dragStartIdx);
294                      } else { //KeyEvent.VK_ENTER:
295                          int newIdx = helper.getSelectedIndex();
296                          ((Choice)target).select(newIdx);
297                          postEvent(new ItemEvent((Choice)target,
298                                                  ItemEvent.ITEM_STATE_CHANGED,
299                                                  ((Choice)target).getItem(newIdx),
300                                                  ItemEvent.SELECTED));
301                      }
302                  }
303                  hidePopdownMenu();
304                  dragging = false;
305                  wasDragged = false;
306                  mouseInSB = false;
307
308                  // See 6240074 for more information
309                  if (choiceListener != null){
310                      choiceListener.unfurledChoiceClosing();
311                  }
312              }
313              break;
314          default:
315              if (unfurled) {
316                  Toolkit.getDefaultToolkit().beep();
317              }
318              break;
319        }
320    }
321
322    public boolean handlesWheelScrolling() { return true; }
323
324    void handleJavaMouseWheelEvent(MouseWheelEvent e) {
325        if (unfurled && helper.isVSBVisible()) {
326            if (ListHelper.doWheelScroll(helper.getVSB(), null, e)) {
327                repaint();
328            }
329        }
330    }
331
332    void handleJavaMouseEvent(MouseEvent e) {
333        super.handleJavaMouseEvent(e);
334        int i = e.getID();
335        switch (i) {
336          case MouseEvent.MOUSE_PRESSED:
337              mousePressed(e);
338              break;
339          case MouseEvent.MOUSE_RELEASED:
340              mouseReleased(e);
341              break;
342          case MouseEvent.MOUSE_DRAGGED:
343              mouseDragged(e);
344              break;
345        }
346    }
347
348    public void mousePressed(MouseEvent e) {
349        /*
350         * fix for 5003166: a Choice on XAWT shouldn't react to any
351         * mouse button presses except left. This involves presses on
352         * Choice but not on opened part of choice.
353         */
354        if (e.getButton() == MouseEvent.BUTTON1){
355            dragStartIdx = helper.getSelectedIndex();
356            if (unfurled) {
357                //fix 6259328: PIT: Choice scrolls when dragging the parent frame while drop-down is active, XToolkit
358                if (! (isMouseEventInChoice(e) ||
359                       unfurledChoice.isMouseEventInside(e)))
360                {
361                    hidePopdownMenu();
362                }
363                // Press on unfurled Choice.  Highlight the item under the cursor,
364                // but don't send item event or set the text on the button yet
365                unfurledChoice.trackMouse(e);
366            }
367            else {
368                // Choice is up - unfurl it
369                grabInput();
370                unfurledChoice.toFront();
371                firstPress = true;
372                wasDragged = false;
373                unfurled = true;
374            }
375        }
376    }
377
378    /*
379     * helper method for mouseReleased routine
380     */
381    void hidePopdownMenu(){
382        ungrabInput();
383        unfurledChoice.setVisible(false);
384        unfurled = false;
385    }
386
387    public void mouseReleased(MouseEvent e) {
388        if (unfurled) {
389            if (mouseInSB) {
390                unfurledChoice.trackMouse(e);
391            }
392            else {
393                // We pressed and dragged onto the Choice, or, this is the
394                // second release after clicking to make the Choice "stick"
395                // unfurled.
396                // This release should ungrab/furl, and set the new item if
397                // release was over the unfurled Choice.
398
399                // Fix for 6239944 : Choice shouldn't close its
400                // pop-down menu if user presses Mouse on Choice's Scrollbar
401                // some additional cases like releasing mouse outside
402                // of Choice are considered too
403                boolean isMouseEventInside = unfurledChoice.isMouseEventInside( e );
404                boolean isMouseInListArea = unfurledChoice.isMouseInListArea( e );
405
406                // Fixed 6318746: REG: File Selection is failing
407                // We shouldn't restore the selected item
408                // if the mouse was dragged outside the drop-down choice area
409                if (!helper.isEmpty() && !isMouseInListArea && dragging) {
410                    // Set the selected item back how it was.
411                    ((Choice)target).select(dragStartIdx);
412                }
413
414                // Choice must be closed if user releases mouse on
415                // pop-down menu on the second click
416                if ( !firstPress && isMouseInListArea) {
417                    hidePopdownMenu();
418                }
419                // Choice must be closed if user releases mouse
420                // outside of Choice's pop-down menu  on the second click
421                if ( !firstPress && !isMouseEventInside) {
422                    hidePopdownMenu();
423                }
424                //if user drags Mouse on pop-down menu, Scrollbar or
425                // outside the Choice
426                if ( firstPress && dragging) {
427                    hidePopdownMenu();
428                }
429                /* this could happen when user has opened a Choice and
430                 * released mouse button. Then he drags mouse on the
431                 * Scrollbar and releases mouse again.
432                 */
433                if ( !firstPress && !isMouseInListArea &&
434                     isMouseEventInside && dragging)
435                {
436                    hidePopdownMenu();
437                }
438
439                if (!helper.isEmpty()) {
440                    // Only update the Choice if the mouse button is released
441                    // over the list of items.
442                    if (unfurledChoice.isMouseInListArea(e)) {
443                        int newIdx = helper.getSelectedIndex();
444                        if (newIdx >= 0) {
445                            // Update the selected item in the target now that
446                            // the mouse selection is complete.
447                            if (newIdx != dragStartIdx) {
448                                ((Choice)target).select(newIdx);
449                                // NOTE: We get a repaint when Choice.select()
450                                // calls our peer.select().
451                            }
452                            if (wasDragged && e.getButton() != MouseEvent.BUTTON1){
453                                ((Choice)target).select(dragStartIdx);
454                            }
455
456                            /*fix for 6239941 : Choice triggers ItemEvent when selecting an item with right mouse button, Xtoolkit
457                            * We should generate ItemEvent if only
458                            * LeftMouseButton used */
459                            if (e.getButton() == MouseEvent.BUTTON1 &&
460                                (!firstPress || wasDragged ))
461                            {
462                                postEvent(new ItemEvent((Choice)target,
463                                                        ItemEvent.ITEM_STATE_CHANGED,
464                                                        ((Choice)target).getItem(newIdx),
465                                                        ItemEvent.SELECTED));
466                            }
467
468                            // see 6240074 for more information
469                            if (choiceListener != null) {
470                                choiceListener.unfurledChoiceClosing();
471                            }
472                        }
473                    }
474                }
475                // See 6243382 for more information
476                unfurledChoice.trackMouse(e);
477            }
478        }
479
480        dragging = false;
481        wasDragged = false;
482        firstPress = false;
483        dragStartIdx = -1;
484    }
485    @SuppressWarnings("deprecation")
486    public void mouseDragged(MouseEvent e) {
487        /*
488         * fix for 5003166. On Motif user are unable to drag
489         * mouse inside opened Choice if he drags the mouse with
490         * different from LEFT mouse button ( e.g. RIGHT or MIDDLE).
491         * This fix make impossible to drag mouse inside opened choice
492         * with other mouse buttons rather then LEFT one.
493         */
494        if ( e.getModifiers() == MouseEvent.BUTTON1_MASK ){
495            dragging = true;
496            wasDragged = true;
497            unfurledChoice.trackMouse(e);
498        }
499    }
500
501    // Stolen from TinyChoicePeer
502    @SuppressWarnings("deprecation")
503    public Dimension getMinimumSize() {
504        // TODO: move this impl into ListHelper?
505        FontMetrics fm = getFontMetrics(target.getFont());
506        Choice c = (Choice)target;
507        int w = 0;
508        for (int i = c.countItems() ; i-- > 0 ;) {
509            w = Math.max(fm.stringWidth(c.getItem(i)), w);
510        }
511        return new Dimension(w + TEXT_XPAD + WIDGET_OFFSET,
512                             fm.getMaxAscent() + fm.getMaxDescent() + TEXT_YPAD);
513    }
514
515    /*
516     * Layout the...
517     */
518    public void layout() {
519        /*
520          Dimension size = target.getSize();
521          Font f = target.getFont();
522          FontMetrics fm = target.getFontMetrics(f);
523          String text = ((Choice)target).getLabel();
524
525          textRect.height = fm.getHeight();
526
527          checkBoxSize = getChoiceSize(fm);
528
529          // Note - Motif appears to use an left inset that is slightly
530          // scaled to the checkbox/font size.
531          cbX = borderInsets.left + checkBoxInsetFromText;
532          cbY = size.height / 2 - checkBoxSize / 2;
533          int minTextX = borderInsets.left + 2 * checkBoxInsetFromText + checkBoxSize;
534          // FIXME: will need to account for alignment?
535          // FIXME: call layout() on alignment changes
536          //textRect.width = fm.stringWidth(text);
537          textRect.width = fm.stringWidth(text == null ? "" : text);
538          textRect.x = Math.max(minTextX, size.width / 2 - textRect.width / 2);
539          textRect.y = size.height / 2 - textRect.height / 2 + borderInsets.top;
540
541          focusRect.x = focusInsets.left;
542          focusRect.y = focusInsets.top;
543          focusRect.width = size.width-(focusInsets.left+focusInsets.right)-1;
544          focusRect.height = size.height-(focusInsets.top+focusInsets.bottom)-1;
545
546          myCheckMark = AffineTransform.getScaleInstance((double)target.getFont().getSize() / MASTER_SIZE, (double)target.getFont().getSize() / MASTER_SIZE).createTransformedShape(MASTER_CHECKMARK);
547        */
548
549    }
550
551    /**
552     * Paint the choice
553     */
554    @Override
555    void paintPeer(final Graphics g) {
556        flush();
557        Dimension size = getPeerSize();
558        // TODO: when mouse is down over button, widget should be drawn depressed
559        g.setColor(getPeerBackground());
560        g.fillRect(0, 0, width, height);
561
562        drawMotif3DRect(g, 1, 1, width-2, height-2, false);
563        drawMotif3DRect(g, width - WIDGET_OFFSET, (height / 2) - 3, 12, 6, false);
564
565        if (!helper.isEmpty() && helper.getSelectedIndex() != -1) {
566            g.setFont(getPeerFont());
567            FontMetrics fm = g.getFontMetrics();
568            String lbl = helper.getItem(helper.getSelectedIndex());
569            if (lbl != null && drawSelectedItem) {
570                g.setClip(1, 1, width - WIDGET_OFFSET - 2, height);
571                if (isEnabled()) {
572                    g.setColor(getPeerForeground());
573                    g.drawString(lbl, 5, (height + fm.getMaxAscent()-fm.getMaxDescent())/2);
574                }
575                else {
576                    g.setColor(getPeerBackground().brighter());
577                    g.drawString(lbl, 5, (height + fm.getMaxAscent()-fm.getMaxDescent())/2);
578                    g.setColor(getPeerBackground().darker());
579                    g.drawString(lbl, 4, ((height + fm.getMaxAscent()-fm.getMaxDescent())/2)-1);
580                }
581                g.setClip(0, 0, width, height);
582            }
583        }
584        if (hasFocus()) {
585            paintFocus(g,focusInsets.left,focusInsets.top,size.width-(focusInsets.left+focusInsets.right)-1,size.height-(focusInsets.top+focusInsets.bottom)-1);
586        }
587        if (unfurled) {
588            unfurledChoice.repaint();
589        }
590        flush();
591    }
592
593    protected void paintFocus(Graphics g,
594                              int x, int y, int w, int h) {
595        g.setColor(focusColor);
596        g.drawRect(x,y,w,h);
597    }
598
599
600
601    /*
602     * ChoicePeer methods stolen from TinyChoicePeer
603     */
604
605    public void select(int index) {
606        helper.select(index);
607        helper.setFocusedIndex(index);
608        repaint();
609    }
610
611    public void add(String item, int index) {
612        helper.add(item, index);
613        repaint();
614    }
615
616    public void remove(int index) {
617        boolean selected = (index == helper.getSelectedIndex());
618        boolean visibled = (index >= helper.firstDisplayedIndex() && index <= helper.lastDisplayedIndex());
619        helper.remove(index);
620        if (selected) {
621            if (helper.isEmpty()) {
622                helper.select(-1);
623            }
624            else {
625                helper.select(0);
626            }
627        }
628        /*
629         * Fix for 6248016
630         * After removing the item of the choice we need to reshape unfurled choice
631         * in order to keep actual bounds of the choice
632         */
633
634        /*
635         * condition added only for performance
636         */
637        if (!unfurled) {
638            // Fix 6292186: PIT: Choice is not refreshed properly when the last item gets removed, XToolkit
639            // We should take into account that there is no 'select' invoking (hence 'repaint')
640            // if the choice is empty (see Choice.java method removeNoInvalidate())
641            // The condition isn't 'visibled' since it would be cause of the twice repainting
642            if (helper.isEmpty()) {
643                repaint();
644            }
645            return;
646        }
647
648        /*
649         * condition added only for performance
650         * the count of the visible items changed
651         */
652        if (visibled){
653            Rectangle r = unfurledChoice.placeOnScreen();
654            unfurledChoice.reshape(r.x, r.y, r.width, r.height);
655            return;
656        }
657
658        /*
659         * condition added only for performance
660         * the structure of visible items changed
661         * if removable item is non visible and non selected then there is no repaint
662         */
663        if (visibled || selected){
664            repaint();
665        }
666    }
667
668    public void removeAll() {
669        helper.removeAll();
670        helper.select(-1);
671        /*
672         * Fix for 6248016
673         * After removing the item of the choice we need to reshape unfurled choice
674         * in order to keep actual bounds of the choice
675         */
676        Rectangle r = unfurledChoice.placeOnScreen();
677        unfurledChoice.reshape(r.x, r.y, r.width, r.height);
678        repaint();
679    }
680
681    /**
682     * DEPRECATED: Replaced by add(String, int).
683     */
684    public void addItem(String item, int index) {
685        add(item, index);
686    }
687
688    public void setFont(Font font) {
689        super.setFont(font);
690        helper.setFont(this.font);
691    }
692
693    public void setForeground(Color c) {
694        super.setForeground(c);
695        helper.updateColors(getGUIcolors());
696    }
697
698    public void setBackground(Color c) {
699        super.setBackground(c);
700        unfurledChoice.setBackground(c);
701        helper.updateColors(getGUIcolors());
702        updateMotifColors(c);
703    }
704
705    public void setDrawSelectedItem(boolean value) {
706        drawSelectedItem = value;
707    }
708
709    public void setAlignUnder(Component comp) {
710        alignUnder = comp;
711    }
712
713    // see 6240074 for more information
714    public void addXChoicePeerListener(XChoicePeerListener l){
715        choiceListener = l;
716    }
717
718    // see 6240074 for more information
719    public void removeXChoicePeerListener(){
720        choiceListener = null;
721    }
722
723    public boolean isUnfurled(){
724        return unfurled;
725    }
726
727    /* fix for 6261352. We should detect if current parent Window (containing a Choice) become iconified and hide pop-down menu with grab release.
728     * In this case we should hide pop-down menu.
729     */
730    //calls from XWindowPeer. Could accept X-styled state events
731    public void stateChangedICCCM(int oldState, int newState) {
732        if (unfurled && oldState != newState){
733                hidePopdownMenu();
734        }
735    }
736
737    //calls from XFramePeer. Could accept Frame's states.
738    public void stateChangedJava(int oldState, int newState) {
739        if (unfurled && oldState != newState){
740            hidePopdownMenu();
741        }
742    }
743
744    @Override
745    protected void initGraphicsConfiguration() {
746        super.initGraphicsConfiguration();
747        // The popup have the same graphic config, so update it at the same time
748        if (unfurledChoice != null) {
749            unfurledChoice.initGraphicsConfiguration();
750            unfurledChoice.doValidateSurface();
751        }
752    }
753
754    /**************************************************************************/
755    /* Common functionality between List & Choice
756       /**************************************************************************/
757
758    /**
759     * Inner class for the unfurled Choice list
760     * Much, much more docs
761     */
762    final class UnfurledChoice extends XWindow /*implements XScrollbarClient*/ {
763
764        // First try - use Choice as the target
765
766        public UnfurledChoice(Component target) {
767            super(target);
768        }
769
770        // Override so we can do our own create()
771        public void preInit(XCreateWindowParams params) {
772            // A parent of this window is the target, at this point: wrong.
773            // Remove parent window; in the following preInit() call we'll calculate as a default
774            // a correct root window which is the proper parent for override redirect.
775            params.delete(PARENT_WINDOW);
776            super.preInit(params);
777            // Reset bounds(we'll set them later), set overrideRedirect
778            params.remove(BOUNDS);
779            params.add(OVERRIDE_REDIRECT, Boolean.TRUE);
780        }
781
782        // Generally, bounds should be:
783        //  x = target.x
784        //  y = target.y + target.height
785        //  w = Max(target.width, getLongestItemWidth) + possible vertScrollbar
786        //  h = Min(MAX_UNFURLED_ITEMS, target.getItemCount()) * itemHeight
787        Rectangle placeOnScreen() {
788            int numItemsDisplayed;
789            // Motif paints an empty Choice the same size as a single item
790            if (helper.isEmpty()) {
791                numItemsDisplayed = 1;
792            }
793            else {
794                int numItems = helper.getItemCount();
795                numItemsDisplayed = Math.min(MAX_UNFURLED_ITEMS, numItems);
796            }
797            Point global = XChoicePeer.this.toGlobal(0,0);
798            Rectangle screenBounds = graphicsConfig.getBounds();
799
800            if (alignUnder != null) {
801                Rectangle choiceRec = XChoicePeer.this.getBounds();
802                choiceRec.setLocation(0, 0);
803                choiceRec = XChoicePeer.this.toGlobal(choiceRec);
804                Rectangle alignUnderRec = new Rectangle(alignUnder.getLocationOnScreen(), alignUnder.getSize()); // TODO: Security?
805                Rectangle result = choiceRec.union(alignUnderRec);
806                // we've got the left and width, calculate top and height
807                width = result.width;
808                x = result.x;
809                y = result.y + result.height;
810                height = 2*BORDER_WIDTH +
811                    numItemsDisplayed*(helper.getItemHeight()+2*ITEM_MARGIN);
812            } else {
813                x = global.x;
814                y = global.y + XChoicePeer.this.height;
815                width = Math.max(XChoicePeer.this.width,
816                                 helper.getMaxItemWidth() + 2 * (BORDER_WIDTH + ITEM_MARGIN + TEXT_SPACE) + (helper.isVSBVisible() ? SCROLLBAR_WIDTH : 0));
817                height = 2*BORDER_WIDTH +
818                    numItemsDisplayed*(helper.getItemHeight()+2*ITEM_MARGIN);
819            }
820            // Don't run off the edge of the screenBounds
821            if (x < screenBounds.x) {
822                x = screenBounds.x;
823            }
824            else if (x + width > screenBounds.x + screenBounds.width) {
825                x = screenBounds.x + screenBounds.width - width;
826            }
827
828            if (y + height > screenBounds.y + screenBounds.height) {
829                y = global.y - height;
830            }
831            if (y < screenBounds.y) {
832                y = screenBounds.y;
833            }
834            return new Rectangle(x, y, width, height);
835        }
836
837        public void toFront() {
838            // see 6240074 for more information
839            if (choiceListener != null)
840                choiceListener.unfurledChoiceOpening(helper);
841
842            Rectangle r = placeOnScreen();
843            reshape(r.x, r.y, r.width, r.height);
844            super.toFront();
845            setVisible(true);
846        }
847
848        /*
849         * Track a MouseEvent (either a drag or a press) and paint a new
850         * selected item, if necessary.
851         */
852        // FIXME: first unfurl after move is not at edge of the screen  onto second monitor doesn't
853        // track mouse correctly.  Problem is w/ UnfurledChoice coords
854        public void trackMouse(MouseEvent e) {
855            // Event coords are relative to the button, so translate a bit
856            Point local = toLocalCoords(e);
857
858            // If x,y is over unfurled Choice,
859            // highlight item under cursor
860
861            switch (e.getID()) {
862              case MouseEvent.MOUSE_PRESSED:
863                  // FIXME: If the Choice is unfurled and the mouse is pressed
864                  // outside of the Choice, the mouse should ungrab on the
865                  // the press, not the release
866                  if (helper.isInVertSB(getBounds(), local.x, local.y)) {
867                      mouseInSB = true;
868                      helper.handleVSBEvent(e, getBounds(), local.x, local.y);
869                  }
870                  else {
871                      trackSelection(local.x, local.y);
872                  }
873                  break;
874              case MouseEvent.MOUSE_RELEASED:
875                  if (mouseInSB) {
876                      mouseInSB = false;
877                      helper.handleVSBEvent(e, getBounds(), local.x, local.y);
878                  }else{
879                      // See 6243382 for more information
880                      helper.trackMouseReleasedScroll();
881                  }
882                  /*
883                    else {
884                    trackSelection(local.x, local.y);
885                    }
886                  */
887                  break;
888              case MouseEvent.MOUSE_DRAGGED:
889                  if (mouseInSB) {
890                      helper.handleVSBEvent(e, getBounds(), local.x, local.y);
891                  }
892                  else {
893                      // See 6243382 for more information
894                      helper.trackMouseDraggedScroll(local.x, local.y, width, height);
895                      trackSelection(local.x, local.y);
896                  }
897                  break;
898            }
899        }
900
901        private void trackSelection(int transX, int transY) {
902            if (!helper.isEmpty()) {
903                if (transX > 0 && transX < width &&
904                    transY > 0 && transY < height) {
905                    int newIdx = helper.y2index(transY);
906                    if (log.isLoggable(PlatformLogger.Level.FINE)) {
907                        log.fine("transX=" + transX + ", transY=" + transY
908                                 + ",width=" + width + ", height=" + height
909                                 + ", newIdx=" + newIdx + " on " + target);
910                    }
911                    if ((newIdx >=0) && (newIdx < helper.getItemCount())
912                        && (newIdx != helper.getSelectedIndex()))
913                    {
914                        helper.select(newIdx);
915                        unfurledChoice.repaint();
916                    }
917                }
918            }
919            // FIXME: If dragged off top or bottom, scroll if there's a vsb
920            // (ICK - we'll need a timer or our own event or something)
921        }
922
923        /*
924         * fillRect with current Background color on the whole dropdown list.
925         */
926        public void paintBackground() {
927            final Graphics g = getGraphics();
928            if (g != null) {
929                try {
930                    g.setColor(getPeerBackground());
931                    g.fillRect(0, 0, width, height);
932                } finally {
933                    g.dispose();
934                }
935            }
936        }
937        /*
938         * 6405689. In some cases we should erase background to eliminate painting
939         * artefacts.
940         */
941        @Override
942        public void repaint() {
943            if (!isVisible()) {
944                return;
945            }
946            if (helper.checkVsbVisibilityChangedAndReset()){
947                paintBackground();
948            }
949            super.repaint();
950        }
951        @Override
952        public void paintPeer(Graphics g) {
953            //System.out.println("UC.paint()");
954            Choice choice = (Choice)target;
955            Color colors[] = XChoicePeer.this.getGUIcolors();
956            draw3DRect(g, getSystemColors(), 0, 0, width - 1, height - 1, true);
957            draw3DRect(g, getSystemColors(), 1, 1, width - 3, height - 3, true);
958
959            helper.paintAllItems(g,
960                                 colors,
961                                 getBounds());
962        }
963
964        public void setVisible(boolean vis) {
965            xSetVisible(vis);
966
967            if (!vis && alignUnder != null) {
968                alignUnder.requestFocusInWindow();
969            }
970        }
971
972        /**
973         * Return a MouseEvent's Point in coordinates relative to the
974         * UnfurledChoice.
975         */
976        private Point toLocalCoords(MouseEvent e) {
977            // Event coords are relative to the button, so translate a bit
978            Point global = e.getLocationOnScreen();
979
980            global.x -= x;
981            global.y -= y;
982            return global;
983        }
984
985        /* Returns true if the MouseEvent coords (which are based on the Choice)
986         * are inside of the UnfurledChoice.
987         */
988        private boolean isMouseEventInside(MouseEvent e) {
989            Point local = toLocalCoords(e);
990            if (local.x > 0 && local.x < width &&
991                local.y > 0 && local.y < height) {
992                return true;
993            }
994            return false;
995        }
996
997        /**
998         * Tests if the mouse cursor is in the Unfurled Choice, yet not
999         * in the vertical scrollbar
1000         */
1001        private boolean isMouseInListArea(MouseEvent e) {
1002            if (isMouseEventInside(e)) {
1003                Point local = toLocalCoords(e);
1004                Rectangle bounds = getBounds();
1005                if (!helper.isInVertSB(bounds, local.x, local.y)) {
1006                    return true;
1007                }
1008            }
1009            return false;
1010        }
1011
1012        /*
1013         * Overridden from XWindow() because we don't want to send
1014         * ComponentEvents
1015         */
1016        public void handleConfigureNotifyEvent(XEvent xev) {}
1017        public void handleMapNotifyEvent(XEvent xev) {}
1018        public void handleUnmapNotifyEvent(XEvent xev) {}
1019    } //UnfurledChoice
1020
1021    public void dispose() {
1022        if (unfurledChoice != null) {
1023            unfurledChoice.destroy();
1024        }
1025        super.dispose();
1026    }
1027
1028    /*
1029     * fix for 6239938 : Choice drop-down does not disappear when it loses
1030     * focus, on XToolkit
1031     * We are able to handle all _Key_ events received by Choice when
1032     * it is in opened state without sending it to EventQueue.
1033     * If Choice is in closed state we should behave like before: send
1034     * all events to EventQueue.
1035     * To be compatible with Motif we should handle all KeyEvents in
1036     * Choice if it is opened. KeyEvents should be sent into Java if Choice is not opened.
1037     */
1038    boolean prePostEvent(final AWTEvent e) {
1039        if (unfurled){
1040            // fix for 6253211: PIT: MouseWheel events not triggered for Choice drop down in XAWT
1041            if (e instanceof MouseWheelEvent){
1042                return super.prePostEvent(e);
1043            }
1044            //fix 6252982: PIT: Keyboard FocusTraversal not working when choice's drop-down is visible, on XToolkit
1045            if (e instanceof KeyEvent){
1046                // notify XWindow that this event had been already handled and no need to post it again
1047                InvocationEvent ev = new InvocationEvent(target, new Runnable() {
1048                    public void run() {
1049                        if(target.isFocusable() &&
1050                                getParentTopLevel().isFocusableWindow() )
1051                        {
1052                            handleJavaKeyEvent((KeyEvent)e);
1053                        }
1054                    }
1055                });
1056                postEvent(ev);
1057
1058                return true;
1059            } else {
1060                if (e instanceof MouseEvent){
1061                    // Fix for 6240046 : REG:Choice's Drop-down does not disappear when clicking somewhere, after popup menu is disposed
1062                    // if user presses Right Mouse Button on opened (unfurled)
1063                    // Choice then we mustn't open a popup menu. We could filter
1064                    // Mouse Events and handle them in XChoicePeer if Choice
1065                    // currently in opened state.
1066                    MouseEvent me = (MouseEvent)e;
1067                    int eventId = e.getID();
1068                    // fix 6251983: PIT: MouseDragged events not triggered
1069                    // fix 6251988: PIT: Choice consumes MouseReleased, MouseClicked events when clicking it with left button,
1070                    if ((unfurledChoice.isMouseEventInside(me) ||
1071                         (!firstPress && eventId == MouseEvent.MOUSE_DRAGGED)))
1072                    {
1073                        return handleMouseEventByChoice(me);
1074                    }
1075                    // MouseMoved events should be fired in Choice's comp if it's not opened
1076                    // Shouldn't generate Moved Events. CR : 6251995
1077                    if (eventId == MouseEvent.MOUSE_MOVED){
1078                        return handleMouseEventByChoice(me);
1079                    }
1080                    //fix for 6272965: PIT: Choice triggers MousePressed when pressing mouse outside comp while drop-down is active, XTkt
1081                    if (  !firstPress && !( isMouseEventInChoice(me) ||
1082                             unfurledChoice.isMouseEventInside(me)) &&
1083                             ( eventId == MouseEvent.MOUSE_PRESSED ||
1084                               eventId == MouseEvent.MOUSE_RELEASED ||
1085                               eventId == MouseEvent.MOUSE_CLICKED )
1086                          )
1087                    {
1088                        return handleMouseEventByChoice(me);
1089                    }
1090                }
1091            }//else KeyEvent
1092        }//if unfurled
1093        return super.prePostEvent(e);
1094    }
1095
1096    //convenient method
1097    //do not generate this kind of Events
1098    public boolean handleMouseEventByChoice(final MouseEvent me){
1099        InvocationEvent ev = new InvocationEvent(target, new Runnable() {
1100            public void run() {
1101                handleJavaMouseEvent(me);
1102            }
1103        });
1104        postEvent(ev);
1105
1106        return true;
1107    }
1108
1109    /* Returns true if the MouseEvent coords
1110     * are inside of the Choice itself (it doesnt's depends on
1111     * if this choice opened or not).
1112     */
1113    private boolean isMouseEventInChoice(MouseEvent e) {
1114        int x = e.getX();
1115        int y = e.getY();
1116        Rectangle choiceRect = getBounds();
1117
1118        if (x < 0 || x > choiceRect.width ||
1119            y < 0 || y > choiceRect.height)
1120        {
1121            return false;
1122        }
1123        return true;
1124    }
1125}
1126