1/*
2 * Copyright (c) 2003, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.awt.X11;
27
28import java.awt.*;
29import java.awt.event.MouseEvent;
30import java.awt.event.MouseWheelEvent;
31import java.awt.event.AdjustmentEvent;
32import java.util.ArrayList;
33import java.util.Iterator;
34import sun.util.logging.PlatformLogger;
35
36// FIXME: implement multi-select
37/*
38 * Class to paint a list of items, possibly with scrollbars
39 * This class paints all items with the same font
40 * For now, this class manages the list of items and painting thereof, but not
41 * posting of Item or ActionEvents
42 */
43final class ListHelper implements XScrollbarClient {
44    private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.X11.ListHelper");
45
46    private final int FOCUS_INSET = 1;
47
48    private final int BORDER_WIDTH; // Width of border drawn around the list
49                                    // of items
50    private final int ITEM_MARGIN;  // Margin between the border of the list
51                                    // of items and item's bg, and between
52                                    // items
53    private final int TEXT_SPACE;   // Space between the edge of an item and
54                                    // the text
55
56    private final int SCROLLBAR_WIDTH;  // Width of a scrollbar
57
58    private java.util.List<String> items;        // List of items
59
60    // TODO: maybe this would be better as a simple int[]
61    private java.util.List<Integer> selected;     // List of selected items
62    private boolean multiSelect;         // Can multiple items be selected
63                                         // at once?
64    private int focusedIndex;
65
66    private int maxVisItems;             // # items visible without a vsb
67    private XVerticalScrollbar vsb;      // null if unsupported
68    private boolean vsbVis;
69    private XHorizontalScrollbar hsb;    // null if unsupported
70    private boolean hsbVis;
71
72    private Font font;
73    private FontMetrics fm;
74
75    private XWindow peer;   // So far, only needed for painting
76                            // on notifyValue()
77    private Color[] colors; // Passed in for painting on notifyValue()
78
79    // Holds the true if mouse is dragging outside of the area of the list
80    // The flag is used at the moment of the dragging and releasing mouse
81    // See 6243382 for more information
82    private boolean mouseDraggedOutVertically = false;
83    private volatile boolean vsbVisibilityChanged = false;
84
85    /*
86     * Comment
87     */
88    ListHelper(XWindow peer, Color[] colors, int initialSize,
89               boolean multiSelect, boolean scrollVert, boolean scrollHoriz,
90               Font font, int maxVisItems, int SPACE, int MARGIN, int BORDER,
91               int SCROLLBAR) {
92        this.peer = peer;
93        this.colors = colors;
94        this.multiSelect = multiSelect;
95        items = new ArrayList<>(initialSize);
96        selected = new ArrayList<>(1);
97        selected.add(Integer.valueOf(-1));
98
99        this.maxVisItems = maxVisItems;
100        if (scrollVert) {
101            vsb = new XVerticalScrollbar(this);
102            vsb.setValues(0, 0, 0, 0, 1, maxVisItems - 1);
103        }
104        if (scrollHoriz) {
105            hsb = new XHorizontalScrollbar(this);
106            hsb.setValues(0, 0, 0, 0, 1, 1);
107        }
108
109        setFont(font);
110        TEXT_SPACE = SPACE;
111        ITEM_MARGIN = MARGIN;
112        BORDER_WIDTH = BORDER;
113        SCROLLBAR_WIDTH = SCROLLBAR;
114    }
115
116    @Override
117    public Component getEventSource() {
118        return peer.getEventSource();
119    }
120
121    /**********************************************************************/
122    /* List management methods                                            */
123    /**********************************************************************/
124
125    void add(String item) {
126        items.add(item);
127        updateScrollbars();
128    }
129
130    void add(String item, int index) {
131        items.add(index, item);
132        updateScrollbars();
133    }
134
135    void remove(String item) {
136        // FIXME: need to clean up select list, too?
137        items.remove(item);
138        updateScrollbars();
139        // Is vsb visible now?
140    }
141
142    void remove(int index) {
143        // FIXME: need to clean up select list, too?
144        items.remove(index);
145        updateScrollbars();
146        // Is vsb visible now?
147    }
148
149    void removeAll() {
150        items.removeAll(items);
151        updateScrollbars();
152    }
153
154    void setMultiSelect(boolean ms) {
155        multiSelect = ms;
156    }
157
158    /*
159     * docs.....definitely docs
160     * merely keeps internal track of which items are selected for painting
161     * dealing with target Components happens elsewhere
162     */
163    void select(int index) {
164        if (index > getItemCount() - 1) {
165            index = (isEmpty() ? -1 : 0);
166        }
167        if (multiSelect) {
168            assert false : "Implement ListHelper.select() for multiselect";
169        }
170        else if (getSelectedIndex() != index) {
171            selected.remove(0);
172            selected.add(Integer.valueOf(index));
173            makeVisible(index);
174        }
175    }
176
177    /* docs */
178    void deselect(int index) {
179        assert(false);
180    }
181
182    /* docs */
183    /* if called for multiselect, return -1 */
184    int getSelectedIndex() {
185        if (!multiSelect) {
186            Integer val = selected.get(0);
187            return val.intValue();
188        }
189        return -1;
190    }
191
192    int[] getSelectedIndexes() { assert(false); return null;}
193
194    /*
195     * A getter method for XChoicePeer.
196     * Returns vsbVisiblityChanged value and sets it to false.
197     */
198    boolean checkVsbVisibilityChangedAndReset(){
199        boolean returnVal = vsbVisibilityChanged;
200        vsbVisibilityChanged = false;
201        return returnVal;
202    }
203
204    boolean isEmpty() {
205        return items.isEmpty();
206    }
207
208    int getItemCount() {
209        return items.size();
210    }
211
212    String getItem(int index) {
213        return items.get(index);
214    }
215
216    /**********************************************************************/
217    /* GUI-related methods                                                */
218    /**********************************************************************/
219
220    void setFocusedIndex(int index) {
221        focusedIndex = index;
222    }
223
224    private boolean isFocusedIndex(int index) {
225        return index == focusedIndex;
226    }
227
228    @SuppressWarnings("deprecation")
229    void setFont(Font newFont) {
230        if (newFont != font) {
231            font = newFont;
232            fm = Toolkit.getDefaultToolkit().getFontMetrics(font);
233            // Also cache stuff like fontHeight?
234        }
235    }
236
237    /*
238     * Returns width of the text of the longest item
239     */
240    int getMaxItemWidth() {
241        int m = 0;
242        int end = getItemCount();
243        for(int i = 0 ; i < end ; i++) {
244            int l = fm.stringWidth(getItem(i));
245            m = Math.max(m, l);
246        }
247        return m;
248    }
249
250    /*
251     * Height of an item (this doesn't include ITEM_MARGIN)
252     */
253    int getItemHeight() {
254        return fm.getHeight() + (2*TEXT_SPACE);
255    }
256
257    int y2index(int y) {
258        if (log.isLoggable(PlatformLogger.Level.FINE)) {
259            log.fine("y=" + y +", firstIdx=" + firstDisplayedIndex() +", itemHeight=" + getItemHeight()
260                     + ",item_margin=" + ITEM_MARGIN);
261        }
262        // See 6243382 for more information
263        int newIdx = firstDisplayedIndex() + ((y - 2*ITEM_MARGIN) / (getItemHeight() + 2*ITEM_MARGIN));
264        return newIdx;
265    }
266
267    /* write these
268    int index2y(int);
269    public int numItemsDisplayed() {}
270    */
271
272    int firstDisplayedIndex() {
273        if (vsbVis) {
274            return vsb.getValue();
275        }
276        return 0;
277    }
278
279    int lastDisplayedIndex() {
280        // FIXME: need to account for horiz scroll bar
281        if (hsbVis) {
282            assert false : "Implement for horiz scroll bar";
283        }
284
285        return vsbVis ? vsb.getValue() + maxVisItems - 1: getItemCount() - 1;
286    }
287
288    /*
289     * If the given index is not visible in the List, scroll so that it is.
290     */
291    private void makeVisible(int index) {
292        if (vsbVis) {
293            if (index < firstDisplayedIndex()) {
294                vsb.setValue(index);
295            }
296            else if (index > lastDisplayedIndex()) {
297                vsb.setValue(index - maxVisItems + 1);
298            }
299        }
300    }
301
302    // FIXME: multi-select needs separate focused index
303    void up() {
304        int curIdx = getSelectedIndex();
305        int numItems = getItemCount();
306        int newIdx;
307
308        assert curIdx >= 0;
309
310        if (curIdx == 0) {
311            newIdx = numItems - 1;
312        }
313        else {
314            newIdx = --curIdx;
315        }
316        // focus(newIdx);
317        select(newIdx);
318    }
319
320    void down() {
321        int newIdx = (getSelectedIndex() + 1) % getItemCount();
322        select(newIdx);
323    }
324
325    void pageUp() {
326        // FIXME: for multi-select, move the focused item, not the selected item
327        if (vsbVis && firstDisplayedIndex() > 0) {
328            if (multiSelect) {
329                assert false : "Implement pageUp() for multiSelect";
330            }
331            else {
332                int selectionOffset = getSelectedIndex() - firstDisplayedIndex();
333                // the vsb does bounds checking
334                int newIdx = firstDisplayedIndex() - vsb.getBlockIncrement();
335                vsb.setValue(newIdx);
336                select(firstDisplayedIndex() + selectionOffset);
337            }
338        }
339    }
340    void pageDown() {
341        if (vsbVis && lastDisplayedIndex() < getItemCount() - 1) {
342            if (multiSelect) {
343                assert false : "Implement pageDown() for multiSelect";
344            }
345            else {
346                int selectionOffset = getSelectedIndex() - firstDisplayedIndex();
347                // the vsb does bounds checking
348                int newIdx = lastDisplayedIndex();
349                vsb.setValue(newIdx);
350                select(firstDisplayedIndex() + selectionOffset);
351            }
352        }
353    }
354    void home() {}
355    void end() {}
356
357
358    boolean isVSBVisible() { return vsbVis; }
359    boolean isHSBVisible() { return hsbVis; }
360
361    XVerticalScrollbar getVSB() { return vsb; }
362    XHorizontalScrollbar getHSB() { return hsb; }
363
364    boolean isInVertSB(Rectangle bounds, int x, int y) {
365        if (vsbVis) {
366            assert vsb != null : "Vert scrollbar is visible, yet is null?";
367            int sbHeight = hsbVis ? bounds.height - SCROLLBAR_WIDTH : bounds.height;
368            return (x <= bounds.width) &&
369                   (x >= bounds.width - SCROLLBAR_WIDTH) &&
370                   (y >= 0) &&
371                   (y <= sbHeight);
372        }
373        return false;
374    }
375
376    boolean isInHorizSB(Rectangle bounds, int x, int y) {
377        if (hsbVis) {
378            assert hsb != null : "Horiz scrollbar is visible, yet is null?";
379
380            int sbWidth = vsbVis ? bounds.width - SCROLLBAR_WIDTH : bounds.width;
381            return (x <= sbWidth) &&
382                   (x >= 0) &&
383                   (y >= bounds.height - SCROLLBAR_WIDTH) &&
384                   (y <= bounds.height);
385        }
386        return false;
387    }
388    @SuppressWarnings("deprecation")
389    void handleVSBEvent(MouseEvent e, Rectangle bounds, int x, int y) {
390        int sbHeight = hsbVis ? bounds.height - SCROLLBAR_WIDTH : bounds.height;
391
392        vsb.handleMouseEvent(e.getID(),
393                             e.getModifiers(),
394                             x - (bounds.width - SCROLLBAR_WIDTH),
395                             y);
396    }
397
398    /*
399     * Called when items are added/removed.
400     * Update whether the scrollbar is visible or not, scrollbar values
401     */
402    private void updateScrollbars() {
403        boolean oldVsbVis = vsbVis;
404        vsbVis = vsb != null && items.size() > maxVisItems;
405        if (vsbVis) {
406            vsb.setValues(vsb.getValue(), getNumItemsDisplayed(),
407                          vsb.getMinimum(), items.size());
408        }
409
410        // 6405689. If Vert Scrollbar gets disappeared from the dropdown menu we should repaint whole dropdown even if
411        // no actual resize gets invoked. This is needed because some painting artifacts remained between dropdown items
412        // but draw3DRect doesn't clear the area inside. Instead it just paints lines as borders.
413        vsbVisibilityChanged = (vsbVis != oldVsbVis);
414        // FIXME: check if added item makes a hsb necessary (if supported, that of course)
415    }
416
417    private int getNumItemsDisplayed() {
418        return items.size() > maxVisItems ? maxVisItems : items.size();
419    }
420
421    @Override
422    public void repaintScrollbarRequest(XScrollbar sb) {
423        Graphics g = peer.getGraphics();
424        Rectangle bounds = peer.getBounds();
425        if ((sb == vsb) && vsbVis) {
426            paintVSB(g, XComponentPeer.getSystemColors(), bounds);
427        }
428        else if ((sb == hsb) && hsbVis) {
429            paintHSB(g, XComponentPeer.getSystemColors(), bounds);
430        }
431        g.dispose();
432    }
433
434    @Override
435    public void notifyValue(XScrollbar obj, int type, int v, boolean isAdjusting) {
436        if (obj == vsb) {
437            int oldScrollValue = vsb.getValue();
438            vsb.setValue(v);
439            boolean needRepaint = (oldScrollValue != vsb.getValue());
440            // See 6243382 for more information
441            if (mouseDraggedOutVertically){
442                int oldItemValue = getSelectedIndex();
443                int newItemValue = getSelectedIndex() + v - oldScrollValue;
444                select(newItemValue);
445                needRepaint = needRepaint || (getSelectedIndex() != oldItemValue);
446            }
447
448            // FIXME: how are we going to paint!?
449            Graphics g = peer.getGraphics();
450            Rectangle bounds = peer.getBounds();
451            int first = v;
452            int last = Math.min(getItemCount() - 1,
453                                v + maxVisItems);
454            if (needRepaint) {
455                paintItems(g, colors, bounds, first, last);
456            }
457            g.dispose();
458
459        }
460        else if ((XHorizontalScrollbar)obj == hsb) {
461            hsb.setValue(v);
462            // FIXME: how are we going to paint!?
463        }
464    }
465
466    void updateColors(Color[] newColors) {
467        colors = newColors;
468    }
469
470    /*
471    public void paintItems(Graphics g,
472                           Color[] colors,
473                           Rectangle bounds,
474                           Font font,
475                           int first,
476                           int last,
477                           XVerticalScrollbar vsb,
478                           XHorizontalScrollbar hsb) {
479    */
480    void paintItems(Graphics g,
481                           Color[] colors,
482                           Rectangle bounds) {
483        // paint border
484        // paint items
485        // paint scrollbars
486        // paint focus?
487
488    }
489    void paintAllItems(Graphics g,
490                           Color[] colors,
491                           Rectangle bounds) {
492        paintItems(g, colors, bounds,
493                   firstDisplayedIndex(), lastDisplayedIndex());
494    }
495    private void paintItems(Graphics g, Color[] colors, Rectangle bounds,
496                            int first, int last) {
497        peer.flush();
498        int x = BORDER_WIDTH + ITEM_MARGIN;
499        int width = bounds.width - 2*ITEM_MARGIN - 2*BORDER_WIDTH - (vsbVis ? SCROLLBAR_WIDTH : 0);
500        int height = getItemHeight();
501        int y = BORDER_WIDTH + ITEM_MARGIN;
502
503        for (int i = first; i <= last ; i++) {
504            paintItem(g, colors, getItem(i),
505                      x, y, width, height,
506                      isItemSelected(i),
507                      isFocusedIndex(i));
508            y += height + 2*ITEM_MARGIN;
509        }
510
511        if (vsbVis) {
512            paintVSB(g, XComponentPeer.getSystemColors(), bounds);
513        }
514        if (hsbVis) {
515            paintHSB(g, XComponentPeer.getSystemColors(), bounds);
516        }
517        peer.flush();
518        // FIXME: if none of the items were focused, paint focus around the
519        // entire list.  This is how java.awt.List should work.
520    }
521
522    /*
523     * comment about what is painted (i.e. the focus rect
524     */
525    private void paintItem(Graphics g, Color[] colors, String string, int x,
526                           int y, int width, int height, boolean selected,
527                           boolean focused) {
528        //System.out.println("LP.pI(): x="+x+" y="+y+" w="+width+" h="+height);
529        //g.setColor(colors[BACKGROUND_COLOR]);
530
531        // FIXME: items shouldn't draw into the scrollbar
532
533        if (selected) {
534            g.setColor(colors[XComponentPeer.FOREGROUND_COLOR]);
535        }
536        else {
537            g.setColor(colors[XComponentPeer.BACKGROUND_COLOR]);
538        }
539        g.fillRect(x, y, width, height);
540
541        if (focused) {
542            //g.setColor(colors[XComponentPeer.FOREGROUND_COLOR]);
543            g.setColor(Color.BLACK);
544            g.drawRect(x + FOCUS_INSET,
545                       y + FOCUS_INSET,
546                       width - 2*FOCUS_INSET,
547                       height - 2*FOCUS_INSET);
548        }
549
550        if (selected) {
551            g.setColor(colors[XComponentPeer.BACKGROUND_COLOR]);
552        }
553        else {
554            g.setColor(colors[XComponentPeer.FOREGROUND_COLOR]);
555        }
556        g.setFont(font);
557        //Rectangle clip = g.getClipBounds();
558        //g.clipRect(x, y, width, height);
559        //g.drawString(string, x + TEXT_SPACE, y + TEXT_SPACE + ITEM_MARGIN);
560
561        int fontAscent = fm.getAscent();
562        int fontDescent = fm.getDescent();
563
564        g.drawString(string, x + TEXT_SPACE, y + (height + fm.getMaxAscent() - fm.getMaxDescent())/2);
565        //g.clipRect(clip.x, clip.y, clip.width, clip.height);
566    }
567
568    private boolean isItemSelected(int index) {
569        Iterator<Integer> itr = selected.iterator();
570        while (itr.hasNext()) {
571            Integer val = itr.next();
572            if (val.intValue() == index) {
573                return true;
574            }
575        }
576        return false;
577    }
578
579    private void paintVSB(Graphics g, Color colors[], Rectangle bounds) {
580        int height = bounds.height - 2*BORDER_WIDTH - (hsbVis ? (SCROLLBAR_WIDTH-2) : 0);
581        Graphics ng = g.create();
582
583        g.setColor(colors[XComponentPeer.BACKGROUND_COLOR]);
584        try {
585            ng.translate(bounds.width - BORDER_WIDTH - SCROLLBAR_WIDTH,
586                         BORDER_WIDTH);
587            // Update scrollbar's size
588            vsb.setSize(SCROLLBAR_WIDTH, bounds.height);
589            vsb.paint(ng, colors, true);
590        } finally {
591            ng.dispose();
592        }
593    }
594
595    private void paintHSB(Graphics g, Color colors[], Rectangle bounds) {
596
597    }
598
599    /*
600     * Helper method for Components with integrated scrollbars.
601     * Pass in the vertical and horizontal scroll bar (or null for none/hidden)
602     * and the MouseWheelEvent, and the appropriate scrollbar will be scrolled
603     * correctly.
604     * Returns whether or not scrolling actually took place.  This will indicate
605     * whether or not repainting is required.
606     */
607    static boolean doWheelScroll(XVerticalScrollbar vsb,
608                                     XHorizontalScrollbar hsb,
609                                     MouseWheelEvent e) {
610        XScrollbar scroll = null;
611        int wheelRotation;
612
613        // Determine which, if any, sb to scroll
614        if (vsb != null) {
615            scroll = vsb;
616        }
617        else if (hsb != null) {
618            scroll = hsb;
619        }
620        else { // Neither scrollbar is showing
621            return false;
622        }
623
624        wheelRotation = e.getWheelRotation();
625
626        // Check if scroll is necessary
627        if ((wheelRotation < 0 && scroll.getValue() > scroll.getMinimum()) ||
628            (wheelRotation > 0 && scroll.getValue() < scroll.getMaximum()) ||
629            wheelRotation != 0) {
630
631            int type = e.getScrollType();
632            int incr;
633            if (type == MouseWheelEvent.WHEEL_BLOCK_SCROLL) {
634                incr = wheelRotation * scroll.getBlockIncrement();
635            }
636            else { // type is WHEEL_UNIT_SCROLL
637                incr = e.getUnitsToScroll() * scroll.getUnitIncrement();
638            }
639            scroll.setValue(scroll.getValue() + incr);
640            return true;
641        }
642        return false;
643    }
644
645    /*
646     * Helper method for XChoicePeer with integrated vertical scrollbar.
647     * Start or stop vertical scrolling when mouse dragged in / out the area of the list if it's required
648     * Restoring Motif behavior
649     * See 6243382 for more information
650     */
651    void trackMouseDraggedScroll(int mouseX, int mouseY, int listWidth, int listHeight){
652
653        if (!mouseDraggedOutVertically){
654            if (vsb.beforeThumb(mouseX, mouseY)) {
655                vsb.setMode(AdjustmentEvent.UNIT_DECREMENT);
656            } else {
657                vsb.setMode(AdjustmentEvent.UNIT_INCREMENT);
658            }
659        }
660
661        if(!mouseDraggedOutVertically && (mouseY < 0 || mouseY >= listHeight)){
662            mouseDraggedOutVertically = true;
663            vsb.startScrollingInstance();
664        }
665
666        if (mouseDraggedOutVertically && mouseY >= 0 && mouseY < listHeight && mouseX >= 0 && mouseX < listWidth){
667            mouseDraggedOutVertically = false;
668            vsb.stopScrollingInstance();
669        }
670    }
671
672    /*
673     * Helper method for XChoicePeer with integrated vertical scrollbar.
674     * Stop vertical scrolling when mouse released in / out the area of the list if it's required
675     * Restoring Motif behavior
676     * see 6243382 for more information
677     */
678    void trackMouseReleasedScroll(){
679
680        if (mouseDraggedOutVertically){
681            mouseDraggedOutVertically = false;
682            vsb.stopScrollingInstance();
683        }
684
685    }
686}
687