/*-
* Copyright © 2011 Diamond Light Source Ltd.
*
* This file is part of GDA.
*
* GDA is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License version 3 as published by the Free
* Software Foundation.
*
* GDA is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with GDA. If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.gda.ui.components;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FigureCanvas;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.ImageFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.MouseEvent;
import org.eclipse.draw2d.MouseListener;
import org.eclipse.draw2d.Panel;
import org.eclipse.draw2d.Polyline;
import org.eclipse.draw2d.RectangleFigure;
import org.eclipse.draw2d.XYLayout;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Spinner;
import uk.ac.gda.common.rcp.CommonRCPActivator;
import uk.ac.gda.common.rcp.ImageConstants;
/**
* Composite that works like a stepper. This stepper displays a slider which displays label underneath it. Also displays
* a stepper text box which can used along with the slider. For usage, please refer to
* "uk.ac.gda.ui.components.StepperTest"
*
* @author rsr31645
*/
public class Stepper extends Canvas {
private static final int LABEL_WIDTH = 60;
private org.eclipse.swt.widgets.Label actualValue;
private ArrayList<IStepperSelectionListener> listeners;
private static final String TEXT_SMALL_7 = "TEXT_SMALL_6";
private int steps = 1;
private boolean moved;
private boolean showActualValueLabel;
private FigureCanvas figCanvas;
private Polyline lineAcross;
private RectangleFigure btnContainer;
private boolean refreshMarkers;
private List<MarkerFigure> markers = Collections.emptyList();
private FontRegistry fontRegistry;
private IFigure rootFigure;
private Spinner spinner;
private int markerCurrentPosition = 0;
private org.eclipse.swt.widgets.Label stepperLabel;
private double[] indexValues;
private boolean notifyWhenDragged = false;
/**
* Switch to request Stepper to notify caller when the stepper is dragged.
*
* @param notifyWhenDragged
*/
public void setNotifyWhenDragged(boolean notifyWhenDragged) {
this.notifyWhenDragged = notifyWhenDragged;
}
/**
* Set the max number of steps
*
* @param steps
*/
public void setSteps(int steps) {
setSteps(steps, null);
}
/**
* @param steps
* - the number of steps
* @param indexValues
* - the labels for each of the steps
*/
public void setSteps(int steps, double[] indexValues) {
this.indexValues = indexValues;
this.steps = steps;
spinner.setMinimum(0);
spinner.setMaximum(steps - 1);
clearMarkerFigures();
this.layout(true, true);
}
private void clearMarkerFigures() {
if (rootFigure != null) {
for (MarkerFigure mf : markers) {
rootFigure.remove(mf);
}
}
markers.clear();
rootFigure.getLayoutManager().layout(rootFigure);
}
private SelectionListener spinnerSelectionListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (e.getSource() instanceof Spinner) {
Spinner spinner = (Spinner) e.getSource();
int selection = spinner.getSelection();
//
moveToStep(selection);
if (showActualValueLabel) {
actualValue.setText(getDisplayVal(selection));
}
fireNotifyChanged();
}
}
};
private void moveToStep(int selection) {
//
if (markers.size() == steps) {
if (steps > 1) {
moveToMarkerClosestTo(markers.get(selection).getLocation());
}
} else {
for (int i = 0; i < markers.size(); i++) {
MarkerFigure mf = markers.get(i);
if (mf.getMarkerIndex() == selection) {
moveToMarkerClosestTo(mf.getLocation());
break;
}
if (i == markers.size() - 1) {
// if reached the end of the slider.
moveToStep(steps - 1);
} else if (mf.getMarkerIndex() < selection && markers.get(i + 1).getMarkerIndex() > selection) {
// if it is in between any of the two markers.
int locToMove = 0;
int numStepsBetweenMarkers = markers.get(i + 1).getMarkerIndex() - mf.getMarkerIndex() - 1;
int numPixelsBetweenMarkers = markers.get(i + 1).getLocation().x - mf.getLocation().x;
if (numPixelsBetweenMarkers >= numStepsBetweenMarkers) {
// there are more pixels than steps between markers.
double pixelsForStep = numPixelsBetweenMarkers / (numStepsBetweenMarkers + 1);
int index = selection - mf.getMarkerIndex();
locToMove = mf.getLocation().x + (int) (index * pixelsForStep);
} else {
double stepsPerPixel = numStepsBetweenMarkers / ((double)numPixelsBetweenMarkers - 8);
int stepsFromMarker = selection - mf.getMarkerIndex();
int pixelsToMove = (int) (stepsFromMarker / stepsPerPixel);
locToMove = mf.getLocation().x + pixelsToMove;
}
Rectangle b = btnContainer.getBounds();
btnContainer.setLocation(new Point(locToMove - b.width / 2, b.y));
moved = true;
break;
}
}
}
}
@Override
public void setBounds(int x, int y, int width, int height) {
refreshMarkers = true;
super.setBounds(x, y, width, height);
}
public Stepper(Composite parent, int style) {
this(parent, style, true);
}
public Stepper(Composite parent, int style, boolean showActualValueLabel) {
this(parent, style, showActualValueLabel, null);
}
public Stepper(Composite parent, int style, Image sliderImage) {
this(parent, style, true, sliderImage);
}
public Stepper(Composite parent, int style, boolean showActualValueLabel, Image sliderImage) {
super(parent, style);
this.setBackground(ColorConstants.white);
this.showActualValueLabel = showActualValueLabel;
GridLayout layout = new GridLayout(3, false);
layout.marginWidth = 1;
layout.marginHeight = 1;
layout.horizontalSpacing = 1;
layout.verticalSpacing = 1;
this.setLayout(layout);
fontRegistry = new FontRegistry(getDisplay());
if (sliderImage == null) {
setSliderImage(CommonRCPActivator.getDefault().getImageRegistry().get(ImageConstants.IMG_SLIDER));
} else {
setSliderImage(sliderImage);
}
if (Display.getCurrent() != null) {
fontRegistry = new FontRegistry(Display.getCurrent());
String fontName = Display.getCurrent().getSystemFont().getFontData()[0].getName();
fontRegistry.put(TEXT_SMALL_7, new FontData[] { new FontData(fontName, 7, SWT.BOLD) });
}
stepperLabel = new org.eclipse.swt.widgets.Label(this, SWT.None);
stepperLabel.setBackground(ColorConstants.white);
stepperLabel.setLayoutData(new org.eclipse.swt.layout.GridData());
figCanvas = new FigureCanvas(this);
figCanvas.setLayoutData(new org.eclipse.swt.layout.GridData(org.eclipse.swt.layout.GridData.FILL_BOTH));
figCanvas.setLayout(new FillLayout());
figCanvas.setBackground(ColorConstants.white);
figCanvas.setHorizontalScrollBarVisibility(FigureCanvas.NEVER);
figCanvas.setVerticalScrollBarVisibility(FigureCanvas.NEVER);
figCanvas.setContents(getContents());
figCanvas.getViewport().setContentsTracksHeight(true);
figCanvas.getViewport().setContentsTracksWidth(true);
figCanvas.addListener(SWT.Resize, new Listener() {
@Override
public void handleEvent(Event event) {
rootFigure.setSize(figCanvas.getSize().x, figCanvas.getSize().y);
refreshMarkers = true;
}
});
Composite spinnerGroup = new Composite(this, SWT.None);
layout = new GridLayout();
layout.marginWidth = 1;
layout.marginHeight = 1;
layout.horizontalSpacing = 1;
layout.verticalSpacing = 1;
spinnerGroup.setLayout(layout);
spinnerGroup.setLayoutData(new org.eclipse.swt.layout.GridData());
spinner = new Spinner(spinnerGroup, SWT.BORDER);
spinner.setLayoutData(new org.eclipse.swt.layout.GridData());
spinner.addSelectionListener(spinnerSelectionListener);
spinner.setMaximum(0);
if (showActualValueLabel) {
actualValue = new org.eclipse.swt.widgets.Label(spinnerGroup, SWT.None);
actualValue.setBackground(ColorConstants.white);
actualValue.setLayoutData(new org.eclipse.swt.layout.GridData(GridData.FILL_BOTH));
}
listeners = new ArrayList<IStepperSelectionListener>();
}
public void addStepperSelectionListener(IStepperSelectionListener listener) {
listeners.add(listener);
}
public void removeStepperSelectionListener(IStepperSelectionListener listener) {
listeners.remove(listener);
}
private MouseListener panelListener = new MouseListener() {
private boolean mousePressed = false;
@Override
public void mousePressed(MouseEvent me) {
mousePressed = true;
}
@Override
public void mouseReleased(MouseEvent me) {
if (mousePressed) {
mousePressed = false;
}
}
@Override
public void mouseDoubleClicked(MouseEvent me) {
if (mousePressed) {
Point location = me.getLocation();
int oldPosition = markerCurrentPosition;
moveToMarkerClosestTo(location);
int newPosition = markerCurrentPosition;
if (newPosition != oldPosition) {
MarkerFigure markerFigure = markers.get(markerCurrentPosition);
int markerIndex = markerFigure.getMarkerIndex();
spinner.setSelection(markerIndex);
if (showActualValueLabel) {
actualValue.setText(getDisplayVal(markerIndex));
}
fireNotifyChanged();
}
mousePressed = false;
}
}
};
private Image sliderImage;
@SuppressWarnings("unused")
protected IFigure getContents() {
rootFigure = new Panel();
rootFigure.setLayoutManager(new StepperLayout());
lineAcross = new Polyline();
lineAcross.setLineWidth(2);
lineAcross.setPoints(new PointList(new int[] { 0, 20, 10, 20 }));
rootFigure.add(lineAcross);
ImageFigure imgFigure = new ImageFigure(getSliderImage());
imgFigure.setOpaque(true);
btnContainer = new RectangleFigure();
btnContainer.setLayoutManager(new XYLayout() {
@SuppressWarnings("rawtypes")
@Override
public void layout(IFigure parent) {
super.layout(parent);
List children = parent.getChildren();
int maxWidth = 0;
for (Object child : children) {
int width = ((IFigure) child).getSize().width;
if (width > maxWidth) {
maxWidth = width;
}
}
parent.setSize(maxWidth + 10, parent.getSize().height);
}
});
rootFigure.addMouseListener(panelListener);
btnContainer.setBackgroundColor(ColorConstants.lightGray);
btnContainer.add(imgFigure, new Rectangle(5, 0, -1, -1));
rootFigure.add(btnContainer, new Rectangle(0, 0, -1, -1));
new Dragger(btnContainer);
return rootFigure;
}
private Image getSliderImage() {
return sliderImage;
}
public void setSliderImage(Image sliderImage) {
this.sliderImage = sliderImage;
}
protected void moveToMarkerClosestTo(Point relPoint) {
int xLoc = relPoint.x;
int minDist = Integer.MAX_VALUE;
int xDestination = -1;
int index = -1;
int controlIndex = index;
for (index = 0; index < markers.size(); index++) {
MarkerFigure m = markers.get(index);
int px = m.getLocation().x;
int dist = Math.abs(px - xLoc);
if (dist < minDist) {
minDist = dist;
xDestination = px;
controlIndex = index;
}
}
markerCurrentPosition = controlIndex;
Rectangle b = btnContainer.getBounds();
btnContainer.setLocation(new Point(xDestination - b.width / 2, b.y));
moved = true;
}
public void setSelection(final int index) {
getDisplay().syncExec(new Runnable() {
@Override
public void run() {
spinner.setSelection(index);
if (showActualValueLabel) {
actualValue.setText(getDisplayVal(index));
}
if (spinner.getMaximum() > 2 && markers.size() < 1) {
rootFigure.invalidate();
}
moveToStep(index);
}
});
}
private String getDisplayVal(int index) {
String displayVal = null;
if (indexValues != null) {
if (index == indexValues.length) {
displayVal = String.format("%.02f", indexValues[indexValues.length - 1]);
} else {
displayVal = String.format("%.02f", indexValues[index]);
}
} else {
displayVal = Integer.toString(index);
}
return displayVal;
}
private class MarkerFigure extends Figure {
private final int index;
public MarkerFigure(int index) {
this.index = index;
polyline = new Polyline();
polyline.setForegroundColor(ColorConstants.blue);
label = new Label(getDisplayVal(index));
label.setFont(fontRegistry.get(TEXT_SMALL_7));
polyline.setLineWidth(2);
setLayoutManager(new XYLayout() {
@Override
public void layout(IFigure parent) {
int px = parent.getBounds().x;
int py = parent.getBounds().y;
polyline.setPoints(new PointList(new int[] { px + 1, py, px + 1, py + 10 }));
label.setBounds(new Rectangle(px - 10, py + 2, LABEL_WIDTH, 10));
}
});
add(polyline);
add(label);
}
protected int getMarkerIndex() {
return index;
}
@Override
public void setSize(int w, int h) {
super.setSize(w, h);
}
private Polyline polyline;
private Label label;
public void setCoordinates(int px, int yStart) {
this.setBounds(new Rectangle(px, yStart, LABEL_WIDTH, 20));
this.layout();
}
}
private class StepperLayout extends XYLayout {
@Override
public void layout(IFigure parent) {
super.layout(parent);
Dimension parentSize = parent.getSize();
lineAcross.setPoints(new PointList(new int[] { 0, parentSize.height / 2, parentSize.width - 1,
parentSize.height / 2 }));
Dimension btnContainerSize = btnContainer.getSize();
/*
* Reducing the button container width to account for the front and end part of the button.
*/
if (steps > 0) {
if (lineAcross.getSize().width > 0) {
int numMarkers = calculateNumberOfMarkers(lineAcross.getSize().width, steps);
if (numMarkers > 0) {
double stepWidth = 0;
if (!markers.isEmpty()) {
// in case the number of steps is the same as the
// number of markers
if (steps == numMarkers) {
if (markers.size() != numMarkers) {
clearMarkerFigures();
}
stepWidth = lineAcross.getSize().width / numMarkers;
} else {
// in case the number of steps is greater than
// the number of markers.
if (markers.size() != numMarkers + 1) {
clearMarkerFigures();
}
stepWidth = lineAcross.getSize().width / (numMarkers + 1);
}
}
if (markers.equals(Collections.emptyList())) {
markers = new ArrayList<MarkerFigure>();
int yStart = parentSize.height / 2;
Rectangle bounds = btnContainer.getBounds();
// need to remove the btn container and then add it
// so that it appears above the markers. Markers
// added dynamically.
parent.remove(btnContainer);
int stepSkipped = steps / numMarkers;
for (int i = 0; i < numMarkers; i++) {
MarkerFigure m = new MarkerFigure(i * stepSkipped);
setMarkerFigureCoordinates(stepWidth, yStart, i, m);
parent.add(m);
markers.add(m);
}
// // need to add the last marker if it isn't there
// already.
if (numMarkers != steps) {
// add an additional marker to state the end of
// the steps which would essentially be (n-1)
MarkerFigure m = new MarkerFigure(steps - 1);
setMarkerFigureCoordinates(stepWidth, yStart, numMarkers + 1, m);
parent.add(m);
markers.add(m);
}
parent.add(btnContainer, bounds);
refreshMarkers = true;
} else if (refreshMarkers) {
int yStart = parentSize.height / 2;
for (int i = 0; i < numMarkers; i++) {
MarkerFigure m = markers.get(i);
setMarkerFigureCoordinates(stepWidth, yStart, i, m);
}
// // need to add the last marker if it isn't there
// already.
if (numMarkers != steps) {
// add an additional marker to state the end of
// the steps which would essentially be (n-1)
MarkerFigure m = markers.get(numMarkers);
setMarkerFigureCoordinates(stepWidth, yStart, numMarkers, m);
}
refreshMarkers = false;
}
}
}
btnContainer.setLocation(new Point(btnContainer.getLocation().x, parentSize.height / 2
- btnContainerSize.height / 2));
if (!moved) {
btnContainer.setLocation(new Point(btnContainer.getLocation().x, parentSize.height / 2
- btnContainerSize.height / 2));
} else {
setSelection(spinner.getSelection());
}
}
}
private int calculateNumberOfMarkers(int numberOfPixels, int steps) {
if (numberOfPixels / LABEL_WIDTH < steps) {
int maxNumMarkers = numberOfPixels / LABEL_WIDTH;
if (maxNumMarkers > 2) {
int numMarkers = getClosestRoundedMarker(maxNumMarkers, steps);
if (numMarkers < 1) {
// which means steps is prime
numMarkers = getClosestRoundedMarker(maxNumMarkers, steps - 1);
}
return numMarkers;
}
return maxNumMarkers;
}
return steps;
}
private int getClosestRoundedMarker(int maxNumMarkers, int steps) {
int num = maxNumMarkers;
while (num != 0 && steps % num != 0) {
num = num - 1;
}
while (num < 2 && num > 0) {
return getClosestRoundedMarker(maxNumMarkers - 1, steps);
}
return num;
}
private void setMarkerFigureCoordinates(double stepWidth, int yStart, int i, MarkerFigure m) {
int px = (int) (i * stepWidth) + btnContainer.getSize().width / 2;
m.setCoordinates(px, yStart);
}
}
class Dragger extends org.eclipse.draw2d.MouseMotionListener.Stub implements MouseListener {
private Point movedPoint;
private final IFigure figure;
private int spinnerValNotified = -1;
private long lastTime;
public Dragger(IFigure figure) {
this.figure = figure;
figure.addMouseMotionListener(this);
figure.addMouseListener(this);
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getSource().equals(figure)) {
btnContainer.setBackgroundColor(ColorConstants.lightGray);
int marker0Loc = markers.get(0).getLocation().x;
int finalMarkerXLoc = markers.get(markers.size() - 1).getLocation().x;
int wby2 = btnContainer.getSize().width / 2;
if (btnContainer.getLocation().x > finalMarkerXLoc) {
btnContainer.setLocation(new Point(finalMarkerXLoc - wby2, btnContainer.getLocation().y));
}
if (btnContainer.getLocation().x < marker0Loc) {
btnContainer.setLocation(new Point(marker0Loc - wby2, btnContainer.getLocation().y));
}
moveToStep(spinner.getSelection());
if (spinner.getSelection() != spinnerValNotified) {
fireNotifyChanged();
}
}
}
@Override
public void mouseDoubleClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
movedPoint = e.getLocation();
btnContainer.setBackgroundColor(ColorConstants.gray);
e.consume();
}
@Override
public void mouseDragged(MouseEvent e) {
if (movedPoint != null) {
Point p = e.getLocation();
Dimension delta = p.getDifference(movedPoint);
Figure f = ((Figure) e.getSource());
// Restricted drag movement for the triangle
int parentWidth = btnContainer.getParent().getSize().width;
Rectangle translated = f.getBounds().getTranslated(delta.width, 0);
if (translated.x < 0) {
translated.x = 0;
} else if (translated.x + translated.width > parentWidth) {
translated.x = parentWidth - btnContainer.getSize().width;
}
f.setBounds(translated);
moveToLocation(f.getBounds().x);
if (notifyWhenDragged) {
long currentTimeMillis = System.currentTimeMillis();
// fires an update only if the previous update hadn't been fired within a timespan of 500 mill
// seconds.
// This was added to avoid the UI thread being held up by listeners.
if (currentTimeMillis - lastTime > 500) {
spinnerValNotified = spinner.getSelection();
fireNotifyChanged();
lastTime = currentTimeMillis;
}
}
movedPoint = p;
}
}
}
public void moveToLocation(int x) {
if (steps > 1) {
int x0Loc = markers.get(0).getLocation().x;
int xEndLoc = markers.get(markers.size() - 1).getLocation().x;
int totalPixels = xEndLoc - x0Loc;
int pixelStep = (x * steps) / totalPixels;
if (pixelStep > steps - 1) {
pixelStep = steps - 1;
}
markerCurrentPosition = pixelStep;
if (pixelStep < 0) {
pixelStep = 0;
}
spinner.setSelection(pixelStep);
if (showActualValueLabel) {
actualValue.setText(getDisplayVal(pixelStep));
}
moved = true;
}
}
public void fireNotifyChanged() {
if (!markers.isEmpty()) {
StepperChangedEvent event = new StepperChangedEvent(this, spinner.getSelection());
for (IStepperSelectionListener l : listeners) {
l.stepperChanged(event);
}
}
}
public int getSelection() {
return spinner.getSelection();
}
@Override
public void dispose() {
listeners.clear();
}
public void setText(String lblText) {
stepperLabel.setText(lblText);
stepperLabel.setToolTipText(lblText);
stepperLabel.pack(true);
this.layout();
}
public String getText() {
return stepperLabel.getText();
}
public int getSteps() {
return steps;
}
public double getSelectedValue() {
if (indexValues != null) {
return indexValues[getSelection()];
}
return getSelection();
}
public double[] getIndexValues() {
return indexValues;
}
}