/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.ui.viewers;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FigureCanvas;
import org.eclipse.draw2d.FreeformLayer;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.ImageFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.LayoutListener;
import org.eclipse.draw2d.SWTEventDispatcher;
import org.eclipse.draw2d.Viewport;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.graphics.Pattern;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Scale;
import org.xmind.ui.internal.ToolkitImages;
/**
* A slider viewer displays linear values in a native
* {@link org.eclipse.swt.widgets.Scale} widget (on Mac OS X) or a custom
* {@link org.eclipse.draw2d.FigureCanvas} (for other platforms). The custom
* canvas draws a draggable handle along and above a straight slot to mimic the
* look and feel of the native Mac scale widget.
*
* <p>
* A {@link ISliderContentProvider} is used to provide major tick values
* including the minimum and maximum values and the algorithm to convert between
* widget selections and selectable values. The default content provider is an
* implementation that simply converts between double values and their
* <code>Double</code> object representation.
* </p>
*
* <p>
* A {@link ILabelProvider} is used to provide the tool-tip text for the
* selected value.
* </p>
*
* <p>
* A {@link StructionSelection} is used as the viewer's selection, which always
* contains the selected value as its only element.
* </p>
*
* <p>
* The user interaction is guaranteed to behave the same for either native Scale
* widget or custom canvas:
* <ul>
* <li>Press the left mouse button down on anywhere along the slot to trigger a
* <i>selection</i> event;</li>
* <li>Drag the mouse while pressing down the left button along the slot to
* trigger <i>selection</i> events sequentially;</li>
* <li>Release the mouse button to trigger a <i>post selection</i> event;</li>
* <li>Double click on the slot to trigger an <i>open</i> event.</li>
* </ul>
* </p>
*
* <dl>
* <dt><b>Styles:</b></dt>
* <dd>HORIZONTAL, VERTICAL</dd>
* </dl>
* <p>
* Note: Only one of the styles HORIZONTAL and VERTICAL may be specified.
* </p>
*
* @author Frank Shaka
*
*/
public class SliderViewer extends ContentViewer implements
IPostSelectionProvider {
protected static int DEFAULT_WIDTH = 200;
protected static int DEFAULT_HEIGHT = 15;
private static final int SLOT_HEIGHT = 4;
private final class SliderFigureCanvas extends FigureCanvas {
private SliderFigureCanvas(int style, Composite parent) {
super(style, parent);
}
public Point computeSize(int wHint, int hHint, boolean changed) {
int w = (wHint != SWT.DEFAULT) ? wHint : (vertical ? DEFAULT_HEIGHT
: DEFAULT_WIDTH);
int h = (hHint != SWT.DEFAULT) ? hHint : (vertical ? DEFAULT_WIDTH
: DEFAULT_HEIGHT);
org.eclipse.swt.graphics.Rectangle trim = computeTrim(0, 0, w, h);
return new Point(trim.width, trim.height);
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
redraw();
}
}
private final class BlockFigure extends ImageFigure {
protected void paintFigure(Graphics graphics) {
graphics.setAntialias(SWT.ON);
if (control != null && !control.isDisposed() && control.isEnabled()) {
graphics.setAlpha(0xff);
} else {
graphics.setAlpha(0x80);
}
super.paintFigure(graphics);
}
}
private class SlotFigure extends Figure {
public boolean containsPoint(int x, int y) {
Rectangle r = getBounds();
return y >= r.y - 5 && //
y < r.y + r.height + 5 && //
x >= r.x && //
x < r.x + r.width;
}
protected void paintFigure(Graphics graphics) {
int alpha = 0x60;
graphics.setAntialias(SWT.ON);
if (control != null && !control.isDisposed() && control.isEnabled()) {
graphics.setAlpha(0xff);
} else {
graphics.setAlpha(0x90);
}
Rectangle r = getBounds();
Path shape = new Path(Display.getCurrent());
float corner = Math.max(2, (vertical ? r.width : r.height) / 2);
SWTUtils.addRoundedRectangle(shape, r.x, r.y, r.width - 1,
r.height - 1, corner);
Pattern pattern = new Pattern(Display.getCurrent(), //
r.x, r.y, //
vertical ? r.right() - 1 : r.x, //
vertical ? r.y : r.bottom() - 1,//
ColorConstants.gray, alpha,//
ColorConstants.lightGray, alpha);
graphics.setBackgroundPattern(pattern);
graphics.fillPath(shape);
graphics.setBackgroundPattern(null);
pattern.dispose();
graphics.setAlpha(alpha);
graphics.setForegroundColor(ColorConstants.gray);
graphics.drawPath(shape);
shape.dispose();
}
}
private class SliderEventDispatcher extends SWTEventDispatcher {
protected void updateFigureUnderCursor(MouseEvent me) {
super.updateFigureUnderCursor(me);
if (getCursorTarget() == blockFigure
&& (me.stateMask & SWT.BUTTON_MASK) != 0) {
getToolTipHelper().updateToolTip(null, null, 0, 0);
updateHoverSource(me);
}
}
}
private static class DefaultSliderContentProvider implements
ISliderContentProvider {
public double getRatio(Object input, Object value) {
return ((Double) value).doubleValue();
}
public Object getValue(Object input, double ratio) {
return Double.valueOf(ratio);
}
public void dispose() {
}
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
}
}
private Control control;
private IFigure slotFigure;
private IFigure blockFigure;
private double selectionRatio = 0;
private Object selectionValue = null;
private boolean vertical;
private List<IOpenListener> openListeners = null;
private List<ISelectionChangedListener> postSelectionChangedListeners = null;
/**
* @see SWT#HORIZONTAL
* @see SWT#VERTICAL
* @param parent
* @param style
*/
public SliderViewer(Composite parent, int style) {
this.vertical = (style & SWT.VERTICAL) != 0;
this.control = createControl(parent, style);
configureControl(control);
hookControl(control);
setContentProvider(new DefaultSliderContentProvider());
}
protected Control createControl(Composite parent, int style) {
int canvasStyle = SWT.NO_REDRAW_RESIZE | SWT.V_SCROLL | SWT.H_SCROLL
| SWT.DOUBLE_BUFFERED;
FigureCanvas fc = new SliderFigureCanvas(canvasStyle, parent);
return fc;
}
protected void configureControl(Control control) {
if (control instanceof Scale) {
Scale scale = (Scale) control;
scale.setMinimum(0);
scale.setMaximum(10000);
scale.setSelection(0);
return;
} else if (control instanceof FigureCanvas) {
FigureCanvas fc = (FigureCanvas) control;
fc.setScrollBarVisibility(FigureCanvas.NEVER);
fc.getLightweightSystem().setEventDispatcher(
new SliderEventDispatcher());
fc.setViewport(createViewport(fc));
IFigure contents = createContents(fc);
fc.setContents(contents);
createSlotFigure(contents);
createBlockFigure(contents);
}
}
protected Viewport createViewport(FigureCanvas fc) {
return new Viewport(true);
}
protected IFigure createContents(final FigureCanvas fc) {
FreeformLayer contents = new FreeformLayer();
contents.addLayoutListener(new LayoutListener.Stub() {
public boolean layout(IFigure container) {
layoutFigures(fc);
return true;
}
});
return contents;
}
protected void createBlockFigure(IFigure contents) {
blockFigure = createBlockFigure();
contents.add(blockFigure);
}
protected void createSlotFigure(IFigure contents) {
slotFigure = createSlotFigure();
contents.add(slotFigure);
}
protected IFigure createBlockFigure() {
final BlockFigure figure = new BlockFigure();
ImageDescriptor descriptor = ToolkitImages
.get(ToolkitImages.SLIDER_HANDLE);
final Image image = descriptor == null ? null : descriptor
.createImage(control.getDisplay());
figure.setImage(image);
figure.setSize(figure.getPreferredSize());
control.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
if (image != null) {
image.dispose();
}
figure.setImage(null);
}
});
return figure;
}
protected IFigure createSlotFigure() {
return new SlotFigure();
}
protected void hookControl(final Control control) {
super.hookControl(control);
if (control instanceof Scale) {
Listener listener = new Listener() {
private boolean mouseDown = false;
private boolean mouseDrag = false;
private boolean selectionChangedDuringDragging = false;
public void handleEvent(Event event) {
switch (event.type) {
case SWT.Selection:
boolean selectionChanged = handleScaleSelection((Scale) event.widget);
if (selectionChanged && !selectionChangedDuringDragging
&& (mouseDrag || mouseDown)) {
selectionChangedDuringDragging = true;
}
// if (!selectionChangedDuringDragging) {
// firePostSelectionChanged();
// }
break;
case SWT.MouseDoubleClick:
mouseDown = false;
mouseDrag = false;
selectionChangedDuringDragging = false;
fireOpen(new OpenEvent(SliderViewer.this,
getSelection()));
break;
case SWT.MouseDown:
mouseDown = true;
mouseDrag = false;
selectionChangedDuringDragging = false;
break;
case SWT.MouseMove:
if (!mouseDrag && mouseDown) {
mouseDrag = true;
}
break;
case SWT.MouseUp:
boolean postSelected = selectionChangedDuringDragging;
mouseDown = false;
mouseDrag = false;
selectionChangedDuringDragging = false;
if (postSelected) {
firePostSelectionChanged();
}
}
}
};
control.addListener(SWT.Selection, listener);
control.addListener(SWT.MouseDoubleClick, listener);
control.addListener(SWT.MouseDown, listener);
control.addListener(SWT.MouseMove, listener);
control.addListener(SWT.MouseUp, listener);
return;
} else if (control instanceof FigureCanvas) {
Listener listener = new Listener() {
private boolean mouseDown = false;
private boolean mouseDrag = false;
private boolean selectionChangedDuringDragging = false;
public void handleEvent(Event event) {
switch (event.type) {
case SWT.Resize:
handleResize();
break;
case SWT.MouseMove:
if (!mouseDrag && mouseDown) {
mouseDrag = true;
}
if (mouseDrag && (event.stateMask & SWT.BUTTON1) != 0) {
if (handleCanvasSelection(event.x, event.y)) {
selectionChangedDuringDragging = true;
}
}
break;
case SWT.MouseDown:
if (receives(event.x, event.y)) {
mouseDown = true;
mouseDrag = false;
if (event.button == 1) {
if (handleCanvasSelection(event.x, event.y)) {
selectionChangedDuringDragging = true;
}
}
}
break;
case SWT.MouseUp:
boolean postSelected = selectionChangedDuringDragging;
mouseDown = false;
mouseDrag = false;
selectionChangedDuringDragging = false;
if (postSelected) {
firePostSelectionChanged();
}
break;
case SWT.MouseDoubleClick:
mouseDown = false;
mouseDrag = false;
fireOpen(new OpenEvent(SliderViewer.this,
getSelection()));
break;
}
}
};
control.addListener(SWT.MouseMove, listener);
control.addListener(SWT.MouseDown, listener);
control.addListener(SWT.MouseUp, listener);
control.addListener(SWT.Resize, listener);
control.addListener(SWT.MouseDoubleClick, listener);
}
}
protected boolean handleScaleSelection(Scale scale) {
int min = scale.getMinimum();
double portion = (scale.getSelection() - min) * 1.0d
/ (scale.getMaximum() - min);
boolean selectionChanged = internalSetSelection(portion, false);
refresh();
return selectionChanged;
}
protected boolean handleCanvasSelection(int x, int y) {
double newPortion = calcNewPortionCanvas(x, y);
newPortion = Math.max(0, Math.min(1, newPortion));
return internalSetSelection(newPortion, true);
}
protected double calcNewPortionCanvas(int x, int y) {
Rectangle r = slotFigure.getBounds();
if (vertical) {
return ((r.y + r.height - y) * 1.0d) / r.height;
} else {
return ((x - r.x) * 1.0d) / r.width;
}
}
protected boolean isVertical() {
return vertical;
}
protected void layoutFigures(FigureCanvas fc) {
Rectangle r = new Rectangle(fc.getViewport().getClientArea());
if (vertical)
transpose(r);
Dimension size = blockFigure.getPreferredSize();
Rectangle b = new Rectangle(r.x + size.width / 2, r.y
+ (r.height - SLOT_HEIGHT) / 2, r.width - size.width,
SLOT_HEIGHT);
int x = (int) (b.x + b.width * selectionRatio);
int y = b.y + b.height - b.height / 2;
Rectangle b2 = new Rectangle(x - size.width / 2, y - size.height / 2,
size.width, size.height);
if (vertical) {
transpose(b);
transpose(b2);
}
slotFigure.setBounds(b);
blockFigure.setBounds(b2);
}
private static void transpose(Rectangle r) {
int temp = r.x;
r.x = -r.y - r.height;
r.y = -temp - r.width;
temp = r.width;
r.width = r.height;
r.height = temp;
}
protected boolean receives(int x, int y) {
return slotFigure.containsPoint(x, y)
|| blockFigure.containsPoint(x, y);
}
protected IFigure getSlotFigure() {
return slotFigure;
}
protected IFigure getBlockFigure() {
return blockFigure;
}
protected double getSelectionPortion() {
return selectionRatio;
}
public Control getControl() {
return control;
}
public Object getSelectionValue() {
if (selectionValue == null) {
if (getContentProvider() instanceof ISliderContentProvider) {
selectionValue = ((ISliderContentProvider) getContentProvider())
.getValue(getInput(), 0);
}
if (selectionValue == null) {
selectionValue = Double.valueOf(0);
}
}
return selectionValue;
}
public ISelection getSelection() {
return new StructuredSelection(getSelectionValue());
}
public void refresh() {
selectionRatio = calcSelectionRatio();
if (control instanceof Scale) {
refreshScale((Scale) control);
} else if (control instanceof FigureCanvas) {
refreshCanvas((FigureCanvas) control);
}
}
protected void refreshScale(Scale scale) {
int min = scale.getMinimum();
int sel = (int) Math.round((scale.getMaximum() - min) * selectionRatio
+ min);
scale.setSelection(sel);
scale.setToolTipText(getSelectionText());
}
private String getSelectionText() {
Object value = getSelectionValue();
if (value == null)
return null;
if (getLabelProvider() instanceof ILabelProvider) {
return ((ILabelProvider) getLabelProvider()).getText(value);
}
return null;
}
protected void refreshCanvas(FigureCanvas fc) {
fc.getContents().revalidate();
fc.getContents().repaint();
String text = getSelectionText();
if (text == null) {
blockFigure.setToolTip(null);
} else {
blockFigure.setToolTip(new Label(text));
}
}
protected double calcSelectionRatio() {
Object value = getSelectionValue();
if (value != null) {
if (getContentProvider() instanceof ISliderContentProvider) {
return ((ISliderContentProvider) getContentProvider())
.getRatio(getInput(), value);
}
if (value instanceof Double)
return ((Double) value).doubleValue();
}
return 0;
}
public void setSelection(ISelection selection, boolean reveal) {
if (selection instanceof IStructuredSelection) {
IStructuredSelection ss = (IStructuredSelection) selection;
Object value = ss.getFirstElement();
internalSetSelection(value, true);
}
}
protected boolean internalSetSelection(double newPortion,
boolean needRefresh) {
return internalSetSelection(calcSelectionValue(newPortion), needRefresh);
}
protected Object calcSelectionValue(double portion) {
if (portion >= 0 && portion <= 1) {
if (getContentProvider() instanceof ISliderContentProvider) {
return ((ISliderContentProvider) getContentProvider())
.getValue(getInput(), portion);
}
}
return Double.valueOf(portion);
}
protected boolean internalSetSelection(Object newValue, boolean needRefresh) {
if (newValue == selectionValue
|| (selectionValue != null && selectionValue.equals(newValue)))
return false;
this.selectionValue = newValue;
if (needRefresh)
refresh();
fireSelectionChanged(new SelectionChangedEvent(this, getSelection()));
return true;
}
protected void handleResize() {
slotFigure.revalidate();
}
public void addOpenListener(IOpenListener listener) {
if (openListeners == null)
openListeners = new ArrayList<IOpenListener>();
openListeners.add(listener);
}
public void removeOpenListener(IOpenListener listener) {
if (openListeners == null)
return;
openListeners.remove(listener);
}
protected void fireOpen(final OpenEvent event) {
if (openListeners == null)
return;
for (final Object l : openListeners.toArray()) {
SafeRunner.run(new SafeRunnable() {
public void run() throws Exception {
((IOpenListener) l).open(event);
}
});
}
}
public void addPostSelectionChangedListener(
ISelectionChangedListener listener) {
if (postSelectionChangedListeners == null)
postSelectionChangedListeners = new ArrayList<ISelectionChangedListener>();
postSelectionChangedListeners.add(listener);
}
public void removePostSelectionChangedListener(
ISelectionChangedListener listener) {
if (postSelectionChangedListeners == null)
return;
postSelectionChangedListeners.remove(listener);
}
protected void firePostSelectionChanged(final SelectionChangedEvent event) {
if (postSelectionChangedListeners == null)
return;
for (final Object l : postSelectionChangedListeners.toArray()) {
SafeRunner.run(new SafeRunnable() {
public void run() throws Exception {
((ISelectionChangedListener) l).selectionChanged(event);
}
});
}
}
protected void firePostSelectionChanged() {
if (getControl().isDisposed())
return;
getControl().getDisplay().asyncExec(new Runnable() {
public void run() {
if (getControl().isDisposed())
return;
firePostSelectionChanged(new SelectionChangedEvent(
SliderViewer.this, getSelection()));
}
});
}
}