package org.csstudio.ui.util.widgets; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.HashSet; import java.util.Set; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; /** * Widget that display a range (currently only time, and only vertical) which can be modified * using a mouse drag. * * @author carcassi */ public class RangeWidget extends Canvas { private double distancePerPx = 0.5; private int startPosition = SWT.TOP; private double pxPerTick = 2.0; private boolean editable = true; // The tick sizes for the first few ticks (loop around after that) private int[] sizes = new int[] {20, 10, 10, 10, 10, 15, 10, 10, 10, 10}; private Set<RangeListener> listeners = new HashSet<RangeListener>(); /** * Adds a listener, notified if the range resolution changes. * * @param listener a new listener */ public void addRangeListener(RangeListener listener) { listeners.add(listener); } /** * Removes a listener. * * @param listener listener to be removed */ public void removeRangeListener(RangeListener listener) { listeners.remove(listener); } private void fireRangeChanged() { for (RangeListener listener : listeners) { listener.rangeChanged(); } } /** * Determines whether the range start at the top (and goes down) * or at the bottom (and goes up). * * @param startPosition SWT.TOP or SWT.BOTTOM */ public void setStartPosition(int startPosition) { this.startPosition = startPosition; redraw(); } /** * Whether the range starts at the top or at the bottom. * * @return SWT.TOP or SWT.BOTTOM */ public int getStartPosition() { return startPosition; } /** * Whether the use can use the mouse to change the resolution. * * @return true if user changes are allowed */ public boolean isEditable() { return editable; } /** * Changes whether the use can use the mouse to change the resolution. * * @param editable true if user changes are allowed */ public void setEditable(boolean editable) { this.editable = editable; } /** * A new range widget. * * @param parent parent component * @param style SWT style */ public RangeWidget(Composite parent, int style) { super(parent, style); // TODO Auto-generated constructor stub addPaintListener(paintListener); addMouseListener(mouseListener); addMouseMoveListener(mouseListener); } private NumberFormat numberFormat = new DecimalFormat("0.#"); private String calculateLabel(double distance) { // Calculate the label if (distance == 0) return "now"; if (distance >= 0.999) { return numberFormat.format(distance) + " s"; } else if (distance >= 0.000999) { return numberFormat.format(distance * 1000) + " ms"; } else if (distance >= 0.000000999) { return numberFormat.format(distance * 1000000) + " us"; } else { return numberFormat.format(distance * 1000000000) + " ns"; } } private final MouseRescale mouseListener = new MouseRescale(); // Listener that implements the re-scaling through a drag operation private class MouseRescale extends MouseAdapter implements MouseMoveListener { private double startY; private double startDistancePerPx; @Override public void mouseDown(MouseEvent e) { // Save the starting point startY = e.y; startDistancePerPx = distancePerPx; } @Override public void mouseMove(MouseEvent e) { // Only if editable and it is a left click drag if (editable && (e.stateMask & SWT.BUTTON1) != 0) { // Re-scale based on how much the mouse is dragged if ((startPosition & SWT.DOWN) != 0) { // Calculate the coordinates starting from the bottom int height = getClientArea().height; if (e.y < height) { setDistancePerPx(startDistancePerPx * (height - startY) / (height - e.y)); } } else { if (e.y > 0) { setDistancePerPx(startDistancePerPx * startY / e.y); } } } } } /** * Changes how much distance is represented by each pixel. For example, * 1 ms per pixel or 20 seconds per pixel. The distance is expressed in * seconds. * * @param distancePerPx seconds (or fraction) represented by each pixel */ public void setDistancePerPx(double distancePerPx) { this.distancePerPx = distancePerPx; // Calculate the distance in pixels between ticks. // The distance should between 2 to 20 pixels to make the range look // reasonable. Find the appropriate order of magnitute to get to that. this.pxPerTick = 1.0 / distancePerPx; if (pxPerTick > 0.0) { while (pxPerTick < 2.0 || pxPerTick > 20.0) { if (pxPerTick < 2.0) pxPerTick *= 10; if (pxPerTick > 20.0) pxPerTick /= 10; } } // New range: need to redraw and notify listeners redraw(); fireRangeChanged(); } /** * Distance represented by each pixel (e.g. 10 ms per pixel). * * @return seconds (or fraction) represented by each pixel */ public double getDistancePerPx() { return distancePerPx; } // Drawing function private PaintListener paintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { double height = getClientArea().height; int width = getClientArea().width; double currentPx = 0.0; int sizeIndex = 0; while (currentPx < height) { // Calculate new tick screen position (if from bottom, invert it) int tickPosition = (int) currentPx; if ((startPosition & SWT.BOTTOM) != 0) { tickPosition = getClientArea().height - tickPosition - 1; } // If we are at the beginning of a new ruler, we need // to print the label if (sizeIndex == 0) { // Invert screen position if start from bottom if ((startPosition & SWT.BOTTOM) != 0) { e.gc.drawText(calculateLabel(distancePerPx * currentPx), 0, tickPosition - e.gc.getFontMetrics().getHeight()); } else { e.gc.drawText(calculateLabel(distancePerPx * currentPx), 0, tickPosition); } } // Draw the line of appropriate size e.gc.drawLine(width - sizes[sizeIndex], tickPosition, width, tickPosition); // If there are more than 10 pixels between ticks, // draw another smaller tick between them if (pxPerTick >= 10.0) { tickPosition = (int) (currentPx + pxPerTick / 2.0); if ((startPosition & SWT.BOTTOM) != 0) { // Invert screen position if start from bottom tickPosition = getClientArea().height - tickPosition - 1; } e.gc.drawLine(width - 5, tickPosition, width, tickPosition); } // Increase screen position to next tick currentPx += pxPerTick; // Increment the pointer for the tick size, and look around if necessary sizeIndex++; if (sizeIndex == sizes.length) sizeIndex = 0; } } }; }