1/*
2 * Copyright (c) 1998, 2015, 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 */
25package com.sun.hotspot.igv.util;
26
27import com.sun.hotspot.igv.data.ChangedListener;
28import java.awt.*;
29import java.awt.geom.*;
30import java.awt.event.MouseEvent;
31import java.awt.event.MouseListener;
32import java.awt.event.MouseMotionListener;
33import java.util.List;
34import javax.swing.*;
35
36/**
37 *
38 * @author Thomas Wuerthinger
39 */
40public class RangeSlider extends JComponent implements ChangedListener<RangeSliderModel>, MouseListener, MouseMotionListener, Scrollable {
41
42    public static final int HEIGHT = 40;
43    public static final float BAR_HEIGHT = 22;
44    public static final float BAR_SELECTION_ENDING_HEIGHT = 16;
45    public static final float BAR_SELECTION_HEIGHT = 10;
46    public static final float BAR_THICKNESS = 2;
47    public static final float BAR_CIRCLE_SIZE = 9;
48    public static final float BAR_CIRCLE_CONNECTOR_SIZE = 6;
49    public static final int MOUSE_ENDING_OFFSET = 3;
50    public static final Color BACKGROUND_COLOR = Color.white;
51    public static final Color BAR_COLOR = Color.black;
52    public static final Color BAR_SELECTION_COLOR = new Color(255, 0, 0, 120);
53    public static final Color BAR_SELECTION_COLOR_ROLLOVER = new Color(255, 0, 255, 120);
54    public static final Color BAR_SELECTION_COLOR_DRAG = new Color(0, 0, 255, 120);
55    private RangeSliderModel model;
56    private State state;
57    private Point startPoint;
58    private RangeSliderModel tempModel;
59    private boolean isOverBar;
60
61    private enum State {
62
63        Initial,
64        DragBar,
65        DragFirstPosition,
66        DragSecondPosition
67    }
68
69    public RangeSlider() {
70        state = State.Initial;
71        this.addMouseMotionListener(this);
72        this.addMouseListener(this);
73    }
74
75    public void setModel(RangeSliderModel newModel) {
76        if (model != null) {
77            model.getChangedEvent().removeListener(this);
78            model.getColorChangedEvent().removeListener(this);
79        }
80        if (newModel != null) {
81            newModel.getChangedEvent().addListener(this);
82            newModel.getColorChangedEvent().addListener(this);
83        }
84        this.model = newModel;
85        update();
86    }
87
88    private RangeSliderModel getPaintingModel() {
89        if (tempModel != null) {
90            return tempModel;
91        }
92        return model;
93    }
94
95    /**
96     * Returns the preferred size of the viewport for a view component.
97     * For example, the preferred size of a <code>JList</code> component
98     * is the size required to accommodate all of the cells in its list.
99     * However, the value of <code>preferredScrollableViewportSize</code>
100     * is the size required for <code>JList.getVisibleRowCount</code> rows.
101     * A component without any properties that would affect the viewport
102     * size should just return <code>getPreferredSize</code> here.
103     *
104     * @return the preferredSize of a <code>JViewport</code> whose view
105     *    is this <code>Scrollable</code>
106     * @see JViewport#getPreferredSize
107     */
108    public Dimension getPreferredScrollableViewportSize() {
109        return getPreferredSize();
110    }
111
112    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
113        if (orientation == SwingConstants.VERTICAL) {
114            return 1;
115        }
116
117        return (int)(BAR_CIRCLE_SIZE + BAR_CIRCLE_CONNECTOR_SIZE);
118    }
119
120    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
121        return orientation == SwingConstants.VERTICAL ? visibleRect.height / 2 : visibleRect.width / 2;
122    }
123
124    public boolean getScrollableTracksViewportWidth() {
125        return false;
126    }
127
128    public boolean getScrollableTracksViewportHeight() {
129        return true;
130    }
131
132    @Override
133    public Dimension getPreferredSize() {
134        Dimension d = super.getPreferredSize();
135        d.height = HEIGHT;
136        d.width = Math.max(d.width, (int)(2 * BAR_CIRCLE_CONNECTOR_SIZE + getPaintingModel().getPositions().size() * (BAR_CIRCLE_SIZE + BAR_CIRCLE_CONNECTOR_SIZE)));
137        return d;
138    }
139
140    @Override
141    public void changed(RangeSliderModel source) {
142        revalidate();
143
144        float barStartY = getBarStartY();
145        int circleCenterY = (int)(barStartY + BAR_HEIGHT / 2);
146        int startX = (int)getStartXPosition(model.getFirstPosition());
147        int endX = (int)getEndXPosition(model.getSecondPosition());
148        Rectangle r = new Rectangle(startX, circleCenterY, endX - startX, 1);
149        scrollRectToVisible(r);
150        update();
151    }
152
153    private void update() {
154        this.repaint();
155    }
156
157    private float getXPosition(int index) {
158        assert index >= 0 && index < getPaintingModel().getPositions().size();
159        return getXOffset() * (index + 1);
160    }
161
162    private float getXOffset() {
163        int size = getPaintingModel().getPositions().size();
164        float width = (float)getWidth();
165        return (width / (size + 1));
166    }
167
168    private float getEndXPosition(int index) {
169        return getXPosition(index) + getXOffset() / 2;
170    }
171
172    private float getStartXPosition(int index) {
173        return getXPosition(index) - getXOffset() / 2;
174    }
175
176    @Override
177    public void paint(Graphics g) {
178        super.paint(g);
179        Graphics2D g2 = (Graphics2D) g;
180        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
181                RenderingHints.VALUE_ANTIALIAS_ON);
182        int width = getWidth();
183        int height = getHeight();
184
185        g2.setColor(BACKGROUND_COLOR);
186        g2.fill(new Rectangle2D.Float(0, 0, width, height));
187
188        // Nothing to paint?
189        if (getPaintingModel() == null || getPaintingModel().getPositions().size() == 0) {
190            return;
191        }
192
193        int firstPos = getPaintingModel().getFirstPosition();
194        int secondPos = getPaintingModel().getSecondPosition();
195
196        paintSelected(g2, firstPos, secondPos);
197        paintBar(g2);
198
199    }
200
201    private float getBarStartY() {
202        return getHeight() / 2 - BAR_HEIGHT / 2;
203    }
204
205    private void paintBar(Graphics2D g) {
206        List<String> list = getPaintingModel().getPositions();
207        float barStartY = getBarStartY();
208
209        g.setColor(BAR_COLOR);
210        g.fill(new Rectangle2D.Float(getXPosition(0), barStartY + BAR_HEIGHT / 2 - BAR_THICKNESS / 2, getXPosition(list.size() - 1) - getXPosition(0), BAR_THICKNESS));
211
212        float circleCenterY = barStartY + BAR_HEIGHT / 2;
213        for (int i = 0; i < list.size(); i++) {
214            float curX = getXPosition(i);
215            g.setColor(getPaintingModel().getColors().get(i));
216            g.fill(new Ellipse2D.Float(curX - BAR_CIRCLE_SIZE / 2, circleCenterY - BAR_CIRCLE_SIZE / 2, BAR_CIRCLE_SIZE, BAR_CIRCLE_SIZE));
217            g.setColor(Color.black);
218            g.draw(new Ellipse2D.Float(curX - BAR_CIRCLE_SIZE / 2, circleCenterY - BAR_CIRCLE_SIZE / 2, BAR_CIRCLE_SIZE, BAR_CIRCLE_SIZE));
219
220
221            String curS = list.get(i);
222            if (curS != null && curS.length() > 0) {
223                float startX = getStartXPosition(i);
224                float endX = getEndXPosition(i);
225                FontMetrics metrics = g.getFontMetrics();
226                Rectangle bounds = metrics.getStringBounds(curS, g).getBounds();
227                if (bounds.width < endX - startX && bounds.height < barStartY) {
228                    g.setColor(Color.black);
229                    g.drawString(curS, startX + (endX - startX) / 2 - bounds.width / 2, barStartY / 2 + bounds.height / 2);
230                }
231            }
232        }
233
234    }
235
236    private void paintSelected(Graphics2D g, int start, int end) {
237
238        float startX = getStartXPosition(start);
239        float endX = getEndXPosition(end);
240        float barStartY = getBarStartY();
241        float barSelectionEndingStartY = barStartY + BAR_HEIGHT / 2 - BAR_SELECTION_ENDING_HEIGHT / 2;
242        paintSelectedEnding(g, startX, barSelectionEndingStartY);
243        paintSelectedEnding(g, endX, barSelectionEndingStartY);
244
245        g.setColor(BAR_SELECTION_COLOR);
246        if (state == State.DragBar) {
247            g.setColor(BAR_SELECTION_COLOR_DRAG);
248        } else if (isOverBar) {
249            g.setColor(BAR_SELECTION_COLOR_ROLLOVER);
250        }
251        g.fill(new Rectangle2D.Float(startX, barStartY + BAR_HEIGHT / 2 - BAR_SELECTION_HEIGHT / 2, endX - startX, BAR_SELECTION_HEIGHT));
252    }
253
254    private void paintSelectedEnding(Graphics2D g, float x, float y) {
255        g.setColor(BAR_COLOR);
256        g.fill(new Rectangle2D.Float(x - BAR_THICKNESS / 2, y, BAR_THICKNESS, BAR_SELECTION_ENDING_HEIGHT));
257    }
258
259    private boolean isOverSecondPosition(Point p) {
260        if (p.y >= getBarStartY()) {
261            float destX = getEndXPosition(getPaintingModel().getSecondPosition());
262            float off = Math.abs(destX - p.x);
263            return off <= MOUSE_ENDING_OFFSET;
264        }
265        return false;
266    }
267
268    private boolean isOverFirstPosition(Point p) {
269        if (p.y >= getBarStartY()) {
270            float destX = getStartXPosition(getPaintingModel().getFirstPosition());
271            float off = Math.abs(destX - p.x);
272            return off <= MOUSE_ENDING_OFFSET;
273        }
274        return false;
275    }
276
277    private boolean isOverSelection(Point p) {
278        if (p.y >= getBarStartY() && !isOverFirstPosition(p) && !isOverSecondPosition(p)) {
279            return p.x > getStartXPosition(getPaintingModel().getFirstPosition()) && p.x < getEndXPosition(getPaintingModel().getSecondPosition());
280        }
281        return false;
282    }
283
284    @Override
285    public void mouseDragged(MouseEvent e) {
286        Rectangle r = new Rectangle(e.getX(), e.getY(), 1, 1);
287        scrollRectToVisible(r);
288
289        if (state == State.DragBar) {
290            float firstX = this.getStartXPosition(model.getFirstPosition());
291            float newFirstX = firstX + e.getPoint().x - startPoint.x;
292            int newIndex = getIndexFromPosition(newFirstX) + 1;
293            if (newIndex + model.getSecondPosition() - model.getFirstPosition() >= model.getPositions().size()) {
294                newIndex = model.getPositions().size() - (model.getSecondPosition() - model.getFirstPosition()) - 1;
295            }
296            int secondPosition = newIndex + model.getSecondPosition() - model.getFirstPosition();
297            tempModel.setPositions(newIndex, secondPosition);
298            update();
299        } else if (state == State.DragFirstPosition) {
300            int firstPosition = getIndexFromPosition(e.getPoint().x) + 1;
301            int secondPosition = model.getSecondPosition();
302            if (firstPosition > secondPosition) {
303                firstPosition--;
304            }
305            tempModel.setPositions(firstPosition, secondPosition);
306            update();
307        } else if (state == State.DragSecondPosition) {
308            int firstPosition = model.getFirstPosition();
309            int secondPosition = getIndexFromPosition(e.getPoint().x);
310            if (secondPosition < firstPosition) {
311                secondPosition++;
312            }
313            tempModel.setPositions(firstPosition, secondPosition);
314            update();
315        }
316    }
317
318    private int getIndexFromPosition(float x) {
319        if (x < getXPosition(0)) {
320            return -1;
321        }
322        for (int i = 0; i < getPaintingModel().getPositions().size() - 1; i++) {
323            float startX = getXPosition(i);
324            float endX = getXPosition(i + 1);
325            if (x >= startX && x <= endX) {
326                return i;
327            }
328        }
329        return getPaintingModel().getPositions().size() - 1;
330    }
331
332    private int getCircleIndexFromPosition(int x) {
333        int result = 0;
334        for (int i = 1; i < getPaintingModel().getPositions().size(); i++) {
335            if (x > getStartXPosition(i)) {
336                result = i;
337            }
338        }
339        return result;
340    }
341
342    @Override
343    public void mouseMoved(MouseEvent e) {
344        isOverBar = false;
345        if (model == null) {
346            return;
347        }
348
349
350        Point p = e.getPoint();
351        if (isOverFirstPosition(p) || isOverSecondPosition(p)) {
352            setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
353        } else if (isOverSelection(p)) {
354            isOverBar = true;
355            setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
356        } else {
357            this.setCursor(Cursor.getDefaultCursor());
358        }
359        repaint();
360    }
361
362    @Override
363    public void mouseClicked(MouseEvent e) {
364        if (e.getClickCount() > 1) {
365            // Double click
366            int index = getCircleIndexFromPosition(e.getPoint().x);
367            model.setPositions(index, index);
368        }
369    }
370
371    @Override
372    public void mousePressed(MouseEvent e) {
373        if (model == null) {
374            return;
375        }
376
377        Point p = e.getPoint();
378        if (isOverFirstPosition(p)) {
379            state = State.DragFirstPosition;
380        } else if (isOverSecondPosition(p)) {
381            state = State.DragSecondPosition;
382        } else if (isOverSelection(p)) {
383            state = State.DragBar;
384        } else {
385            return;
386        }
387
388        startPoint = e.getPoint();
389        tempModel = model.copy();
390    }
391
392    @Override
393    public void mouseReleased(MouseEvent e) {
394        if (model == null || tempModel == null) {
395            return;
396        }
397        state = State.Initial;
398        model.setPositions(tempModel.getFirstPosition(), tempModel.getSecondPosition());
399        tempModel = null;
400    }
401
402    @Override
403    public void mouseEntered(MouseEvent e) {
404    }
405
406    @Override
407    public void mouseExited(MouseEvent e) {
408        isOverBar = false;
409        repaint();
410    }
411}
412