/******************************************************************************* * Copyright (c) 2009 the CHISEL group and contributors. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Del Myers - initial API and implementation *******************************************************************************/ package ca.uvic.chisel.widgets; import java.util.LinkedList; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Item; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.TypedListener; /** * A simple range slider * @author Del Myers * */ public class RangeSlider extends Composite { private Canvas canvas; private long min; private long max; private long rangeMin; private long rangeMax; private double scale; private int visualHigh; private int visualLow; private LinkedList<RangeAnnotation> items; private DisposeListener itemDisposedListener; private RangeAnnotation selectedAnnotation; static final int HANDLE_SIZE = 3; /** * @param parent * @param style */ public RangeSlider(Composite parent, int style) { super(parent, style); items = new LinkedList<RangeAnnotation>(); setLayout(new FillLayout()); this.canvas = new Canvas(this, SWT.DOUBLE_BUFFERED | SWT.NO_BACKGROUND); canvas.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { paintCanvas(e); } }); canvas.addControlListener(new ControlListener() { public void controlResized(ControlEvent e) { resetScale(); } public void controlMoved(ControlEvent e) {} }); itemDisposedListener = new DisposeListener() { public void widgetDisposed(DisposeEvent e) { items.remove(e.widget); redraw(); } }; new RangeSliderHelper(this); } /** * */ protected void resetScale() { Rectangle bounds = getCanvas().getClientArea(); int visualRangeSize = bounds.width-HANDLE_SIZE*2; if (visualRangeSize < HANDLE_SIZE*2) { scale = 0; } scale = ((double)visualRangeSize)/(getMaximum()-getMinimum()); setVisualLow(toVisualValue(getSelectedMinimum())); setVisualHigh(toVisualValue(getSelectedMaximum())); } /** * @param e */ protected void paintCanvas(PaintEvent e) { Rectangle bounds = canvas.getClientArea(); Color black = getDisplay().getSystemColor(SWT.COLOR_BLACK); GC gc = e.gc; gc.setBackground(getBackground()); gc.fillRectangle(bounds); gc.setAntialias(SWT.OFF); //paint the edges the same colour as the parent, so that it //is apparent that they don't belong to the range of this slider. gc.setBackground(getParent().getBackground()); gc.fillRectangle(bounds.x, bounds.y, HANDLE_SIZE, bounds.height); gc.fillRectangle(bounds.x + bounds.width-HANDLE_SIZE, bounds.y, HANDLE_SIZE, bounds.height); //the actual area for the range will actually be a few pixels in from the //sides so that there will be room for the small arrow handles int visualLow = getVisualLow(); int visualHigh = getVisualHigh(); gc.setBackground(getForeground()); paintRanges(gc, bounds); gc.setAlpha(100); gc.setForeground(getForeground()); gc.setBackground(getForeground()); //draw a rectangle for the selected range gc.fillRectangle(visualLow+bounds.x, bounds.y, visualHigh-visualLow, bounds.height); gc.setForeground(black); gc.setBackground(black); //draw triangle handles; gc.setAlpha(255); //the minimum handles gc.fillPolygon(new int[]{ visualLow + bounds.x - HANDLE_SIZE, bounds.y, visualLow + bounds.x, bounds.y, visualLow + bounds.x, bounds.y+HANDLE_SIZE }); gc.fillPolygon(new int[]{ visualLow + bounds.x - HANDLE_SIZE, bounds.y+bounds.height, visualLow + bounds.x, bounds.y+bounds.height, visualLow + bounds.x, bounds.y+bounds.height-HANDLE_SIZE }); //the max handles gc.fillPolygon(new int[]{ visualHigh + bounds.x + HANDLE_SIZE, bounds.y, visualHigh + bounds.x, bounds.y, visualHigh + bounds.x, bounds.y+HANDLE_SIZE+1 }); gc.fillPolygon(new int[]{ visualHigh + bounds.x + HANDLE_SIZE, bounds.y+bounds.height, visualHigh + bounds.x, bounds.y+bounds.height, visualHigh + bounds.x, bounds.y+bounds.height-HANDLE_SIZE-1 }); //draw separater lines gc.drawLine(visualLow, bounds.y, visualLow, bounds.y+bounds.height); gc.drawLine(visualHigh-1, bounds.y, visualHigh-1, bounds.y+bounds.height); } /** * @param gc */ private void paintRanges(GC gc, Rectangle bounds) { //draw a rectangle for each range gc.setAlpha(200); for (RangeAnnotation a : items) { if (a == selectedAnnotation) { continue; } if (a.isDisposed()) continue; int visualLow = toVisualValue(a.getOffset()) + bounds.x; int visualHigh = toVisualValue(a.getLength()) + visualLow; if (visualLow < bounds.x + HANDLE_SIZE) { visualLow = bounds.x + HANDLE_SIZE; } if (visualHigh > bounds.x + bounds.width - HANDLE_SIZE) { visualHigh = bounds.x + bounds.width - HANDLE_SIZE; } Color fg = a.getForeground(); if (a == selectedAnnotation) { fg = gc.getDevice().getSystemColor(SWT.COLOR_WHITE); } if (fg == null) { fg = getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); } Color bg = a.getBackground(); if (bg == null) { bg = getDisplay().getSystemColor(SWT.COLOR_GRAY); } gc.setForeground(fg); gc.setBackground(bg); gc.fillRectangle(visualLow, bounds.y, visualHigh-visualLow, bounds.height-1); gc.drawRectangle(visualLow, bounds.y, visualHigh-visualLow-1, bounds.height-1); } //paint the selected annotation if (selectedAnnotation != null) { RangeAnnotation a = selectedAnnotation; if (a.isDisposed()) return; int visualLow = toVisualValue(a.getOffset()) + bounds.x; int visualHigh = toVisualValue(a.getLength()) + visualLow; if (visualLow < bounds.x + HANDLE_SIZE) { visualLow = bounds.x + HANDLE_SIZE; } if (visualHigh > bounds.x + bounds.width - HANDLE_SIZE) { visualHigh = bounds.x + bounds.width - HANDLE_SIZE; } Color fg = gc.getDevice().getSystemColor(SWT.COLOR_WHITE); if (fg == null) { fg = getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); } Color bg = a.getBackground(); if (bg == null) { bg = getDisplay().getSystemColor(SWT.COLOR_GRAY); } gc.setForeground(fg); gc.setBackground(bg); gc.fillRectangle(visualLow, bounds.y, visualHigh-visualLow, bounds.height-1); gc.drawRectangle(visualLow, bounds.y, visualHigh-visualLow-1, bounds.height-1); } } /** * Returns the item under the given point. The point is in display coordinates, relative * to this composite. * @param p the point query. * @return an annotation under that point, or null if none. */ public RangeAnnotation itemAt(Point p) { checkWidget(); RangeAnnotation winner = null; //first, look for all the ones that are in the pixel range RangeAnnotation[] currentRanges = items.toArray(new RangeAnnotation[items.size()]); LinkedList<RangeAnnotation> annotationsToScore = new LinkedList<RangeAnnotation>(); for (RangeAnnotation a : currentRanges) { int low = toVisualValue(a.getOffset()); int high = toVisualValue(a.getLength()+a.getOffset()); if (low == high) { high++; } if (low <= p.x && high >= p.x) { annotationsToScore.add(a); } } long score = -1; long rangeValue = toRangeValue(p.x); for (RangeAnnotation a : annotationsToScore) { long localScore = 0; long leftDiff = Math.abs(rangeValue - a.getOffset()); long rightDiff = Math.abs((a.getOffset() + a.getLength()) - rangeValue); //normalize to 0 because the scaling might be huge localScore = rightDiff + leftDiff; if (score == -1 || localScore < score) { winner = a; score = localScore; } } return winner; } public RangeAnnotation[] getRanges() { return items.toArray(new RangeAnnotation[items.size()]); } /** * @return */ int getVisualHigh() { return visualHigh; } /** * @return */ int getVisualLow() { return visualLow; } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Control#setBackground(org.eclipse.swt.graphics.Color) */ @Override public void setBackground(Color color) { super.setBackground(color); } /** * @return the minimum value for the range */ public long getMinimum() { return min; } /** * Sets the minimum value of the range. * @param l the min to set */ public void setMinimum(long l) { checkWidget(); this.min = l; resetScale(); setVisualHigh(toVisualValue(getMaximum())); setVisualLow(toVisualValue(getMinimum())); canvas.redraw(); } /** * @return the maximum value of the range */ public long getMaximum() { return max; } /** * Sets the maximum value of the range. * @param max the new maximum */ public void setMaximum(long max) { checkWidget(); this.max = max; resetScale(); setVisualHigh(toVisualValue(getMaximum())); setVisualLow(toVisualValue(getMinimum())); canvas.redraw(); } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Control#redraw(int, int, int, int, boolean) */ @Override public void redraw(int x, int y, int width, int height, boolean all) { super.redraw(x, y, width, height, all); if (!all) { //make sure that the canvas is redrawn as well canvas.redraw(); } } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Control#redraw() */ @Override public void redraw() { super.redraw(); canvas.redraw(); } /** * Sets the minimum selected value for the range. * @param rangeMin the new selected value; */ public void setSelectedMinimum(long rangeMin) { checkWidget(); if (rangeMin < min) { rangeMin = min; } else if (rangeMin > this.rangeMax) { rangeMin = this.rangeMax; } this.rangeMin = rangeMin; setVisualLow(toVisualValue(rangeMin)); canvas.redraw(); } /** * @param visualValue */ private void setVisualLow(int visualValue) { Rectangle bounds = canvas.getClientArea(); int limit = bounds.x + HANDLE_SIZE; if (visualValue < limit) { visualValue = limit; } visualLow = visualValue; } /** * Sets the current maximum selected value for the range. * @param rangeMax the new selected value. */ public void setSelectedMaximum(long rangeMax) { checkWidget(); if (rangeMax > max) { rangeMax = max; } else if (rangeMax < this.rangeMin) { rangeMax = this.rangeMin; } this.rangeMax = rangeMax; setVisualHigh(toVisualValue(rangeMax)); canvas.redraw(); } /** * @param rangeMax2 * @return */ private int toVisualValue(long rangeValue) { if (scale == 0.0) { return 0; } long highOffset = rangeValue - getMinimum(); return (int)(Math.round(scale*highOffset) + canvas.getClientArea().x+HANDLE_SIZE); } private void setVisualHigh(int visualValue) { Rectangle bounds = canvas.getClientArea(); int limit = bounds.x + bounds.width - HANDLE_SIZE; if (visualValue > limit) { visualValue = limit; } this.visualHigh = visualValue; } /** * @return the selected maximum value for the range */ public long getSelectedMaximum() { return rangeMax; } /** * @return the selected minumum value for the range */ public long getSelectedMinimum() { return rangeMin; } Canvas getCanvas() { return canvas; } /** * Converts the given * @param x * @return */ public long toRangeValue(int x) { // if (x <= getVisualLow()) { // x -= HANDLE_SIZE; // } else if (x >= getVisualHigh()) { // x -= HANDLE_SIZE*2; // } Rectangle bounds = getCanvas().getClientArea(); if (scale == 0.0) { return 0; } return Math.round((x-bounds.x+HANDLE_SIZE)/scale) + getMinimum(); } /** * Adds the given listener to listen for when the minimum and maximum values change, or * when an annotation is selected in the receiver. Clients can tell the difference by * querying the width and height values of the selection event. If they are negative, then * the selected annotation has changed. Otherwise, the selected range has changed. * On the selection change event, the bounds will contain the rectangle bounds that * the minimum and maximum values are visualized in. In order to get the actual values, * clients should cast the event's widget to a RangeSlider and use * {@link #getSelectedMaximum()} and {@link #getSelectedMinimum()}. * @param listener */ public void addSelectionListener(SelectionListener listener) { checkWidget(); if (listener == null) SWT.error (SWT.ERROR_NULL_ARGUMENT); TypedListener tl = new TypedListener(listener); addListener (SWT.Selection,tl); addListener (SWT.DefaultSelection,tl); } /** * @param rangeValue */ void internalSetSelectedMinimum(long min) { setSelectedMinimum(min); fireSelectionChanged(); } void internalSetVisualMinimum(int min) { setVisualLow(min); rangeMin = toRangeValue(getVisualLow()); fireSelectionChanged(); canvas.redraw(); } /** * @param rangeValue */ public void internalSetSelectedMaximum(long max) { setSelectedMaximum(max); fireSelectionChanged(); } void internalSetVisualMaximum(int max) { setVisualHigh(max); rangeMax = toRangeValue(getVisualHigh()); fireSelectionChanged(); canvas.redraw(); } /** * */ private void fireSelectionChanged() { Event event = new Event(); event.button = 1; Rectangle bounds = getCanvas().getBounds(); int visualLow = getVisualLow(); int visualHigh = getVisualHigh(); event.x = visualLow+bounds.x; event.y = bounds.y; event.width = visualHigh-visualLow; event.height = bounds.height; event.item = null; notifyListeners(SWT.Selection, event); } private void fireItemSelectionChanged(int button) { Event event = new Event(); event.button = button; event.x = 0; event.y =0; event.width = -1; event.height = -1; event.item = selectedAnnotation; event.data = (selectedAnnotation !=null) ? selectedAnnotation.getData() : null; notifyListeners(SWT.Selection, event); } /** * Creates a new annotation for this item * @param rangeAnnotation */ void createItem(RangeAnnotation rangeAnnotation) { items.add(rangeAnnotation); rangeAnnotation.addDisposeListener(itemDisposedListener); } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Composite#layout(boolean, boolean) */ @Override public void layout(boolean changed, boolean all) { super.layout(changed, all); resetScale(); } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Composite#layout(org.eclipse.swt.widgets.Control[]) */ @Override public void layout(Control[] changed) { super.layout(changed); resetScale(); } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Control#setMenu(org.eclipse.swt.widgets.Menu) */ @Override public void setMenu(Menu menu) { canvas.setMenu(menu); } /** * Updates the selection for the given mouse event. * @param e */ void updateSelection(MouseEvent e) { Item item = itemAt(new Point(e.x, e.y)); if (item != selectedAnnotation) { if (item instanceof RangeAnnotation) { selectedAnnotation = (RangeAnnotation) item; } else { selectedAnnotation = null; } redraw(); fireItemSelectionChanged(e.button); } } /** * @return the index of the selected range item, or -1 if none selected. */ public int getSelectionIndex() { checkWidget(); return getIndex(selectedAnnotation); } /** * Returns the index at which the given annotation exists in the range. * -1 if the item is disposed, or doesn't exist in this slider. * @param item * @return the index at which the given annotation exists in the range. * -1 if the item is disposed, or doesn't exist in this slider. */ public int getIndex(RangeAnnotation item) { checkWidget(); if (item == null || item.isDisposed()) { return -1; } return (items.indexOf(item)); } /** * Returns the annotation at the given index. Null is returned if the index * is out of range. * @param index the index of the annotation to return. * @return the annotation at the given index. Null is returned if the index * is out of range. */ public RangeAnnotation getItem(int index) { if (index < 0 || index >= items.size()) { return null; } return items.get(index); } /** * Sets the selected annotation to the annotation at the given index. * Returns true if the annotation could be selected, or false otherwise. * @param index the index of the annotation to select. * @return true if the annotation could be selected, or false otherwise. */ public boolean setSelectionIndex(int index) { checkWidget(); if (index < 0 || index >= items.size()) { return false; } RangeAnnotation newSelection = items.get(index); if (newSelection == selectedAnnotation) return true; selectedAnnotation = newSelection; redraw(); fireItemSelectionChanged(1); return true; } }