/* ******************************************************************************
* 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.internal.mindmap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.draw2d.AbstractBackground;
import org.eclipse.draw2d.FreeformFigure;
import org.eclipse.draw2d.FreeformLayer;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Util;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.xmind.gef.GEF;
import org.xmind.gef.IGraphicalViewer;
import org.xmind.gef.ISourceProvider;
import org.xmind.gef.command.Command;
import org.xmind.gef.command.CommandStackEvent;
import org.xmind.gef.command.ICommandStack;
import org.xmind.gef.command.ICommandStackListener;
import org.xmind.gef.draw2d.ITransparentableFigure;
import org.xmind.gef.draw2d.PathFigure;
import org.xmind.gef.draw2d.RotatableWrapLabel;
import org.xmind.gef.draw2d.graphics.Path;
import org.xmind.gef.part.IGraphicalPart;
import org.xmind.gef.part.IPart;
import org.xmind.gef.service.GraphicalViewerService;
import org.xmind.ui.internal.MindMapMessages;
import org.xmind.ui.mindmap.MindMapUI;
import org.xmind.ui.resources.FontUtils;
import org.xmind.ui.util.Cancelable;
import org.xmind.ui.util.ICancelable;
public class UndoRedoTipsService extends GraphicalViewerService implements
ICommandStackListener {
// private static class FrameFigure extends PathFigure {
// private int alpha = 0xff;
//
// public int getAlpha() {
// return alpha;
// }
//
// public void setAlpha(int alpha) {
// if (alpha == this.alpha)
// return;
// this.alpha = alpha;
// repaint();
// }
//
// public void paintFigure(Graphics graphics) {
// graphics.setAlpha(getAlpha());
// super.paintFigure(graphics);
// }
// }
/**
*
* @author MANGOSOFT this class is used to paint the UndoRedo tips'
* background
*/
private static class TitleBackground extends AbstractBackground {
private int r;
private int d;
private Insets ins;
private Color color;
public TitleBackground(int r, Color color) {
this.r = r;
this.color = color;
this.d = r * 2;
this.ins = new Insets(0, r, 0, r);
}
@Override
public Insets getInsets(IFigure figure) {
return ins;
}
@Override
public void paintBackground(IFigure figure, Graphics graphics,
Insets insets) {
Rectangle rect = getPaintRectangle(figure, insets);
Rectangle inner = rect.getShrinked(getInsets(figure));
Path p = new Path(Display.getCurrent());
p.addArc(inner.x - r, inner.y, d, d, 90, 180);
p.addRectangle(inner);
p.addArc(inner.right() - r, inner.y, d, d, 270, 180);
graphics.setFillRule(SWT.FILL_WINDING);
graphics.setBackgroundColor(color);
graphics.fillPath(p);
p.dispose();
}
}
private class Tip {
private IGraphicalPart source;
private String label;
private PathFigure frame;
private ITransparentableFigure title;
public Tip(IGraphicalPart source, String label) {
this.source = source;
this.label = label;
}
public void setAlpha(int alpha) {
if (frame == null)
frame = createFrameFigure();
if (title == null)
title = createTitleFigure();
if (frame != null)
frame.setAlpha(alpha);
if (title != null) {
title.setMainAlpha(alpha);
title.setSubAlpha(alpha);
}
}
private PathFigure createFrameFigure() {
if (getFrameLayer() == null)
return null;
IFigure srcFigure = source.getFigure();
if (srcFigure instanceof FreeformFigure)
return null;
Rectangle frameBounds = srcFigure.getBounds().getExpanded(4, 4);
PathFigure figure = new PathFigure();
getFrameLayer().add(figure);
figure.setFill(false);
figure.setOutline(true);
figure.setBackgroundColor(getFrameColor());
figure.setForegroundColor(getFrameColor());
figure.setLineStyle(SWT.LINE_SOLID);
figure.setLineWidth(4);
Path framePath = new Path(Display.getCurrent());
framePath.addRoundedRectangle(frameBounds, 4);
figure.setPath(framePath);
// figure.setBounds(figure.getPreferredBounds());
return figure;
}
private ITransparentableFigure createTitleFigure() {
if (getTitleLayer() == null)
return null;
RotatableWrapLabel figure = new RotatableWrapLabel(label);
getTitleLayer().add(figure);
figure.setFont(FontUtils.getBoldRelative(
JFaceResources.DEFAULT_FONT, Util.isMac() ? 2 : 1));
figure.setForegroundColor(Display.getCurrent().getSystemColor(
SWT.COLOR_WHITE));
figure.setBackgroundColor(getFrameColor());
Dimension size = figure.getPreferredSize();
TitleBackground titleBg = new TitleBackground(size.height / 2,
getFrameColor());
figure.setBorder(titleBg);
IFigure srcFigure = source.getFigure();
Point titleLoc;
if (srcFigure instanceof FreeformFigure) {
Rectangle extent = ((FreeformFigure) srcFigure)
.getFreeformExtent();
titleLoc = new Point(extent.x + extent.width / 2, extent.y - 20);
} else {
Rectangle frameBounds = srcFigure.getBounds().getExpanded(4, 4);
titleLoc = new Point(frameBounds.x + frameBounds.width / 2,
frameBounds.y - 2);
}
size = figure.getPreferredSize();
int x = titleLoc.x - size.width / 2;
int y = titleLoc.y - size.height;
Rectangle titleBounds = new Rectangle(x, y, size.width, size.height);
figure.setBounds(titleBounds);
return figure;
}
private Color getFrameColor() {
return Display.getCurrent().getSystemColor(SWT.COLOR_GRAY);
}
public void dispose() {
if (frame != null) {
if (frame.getParent() != null) {
frame.getParent().remove(frame);
}
org.eclipse.swt.graphics.Path path = frame.getPath();
frame.setPath(null);
if (path != null)
path.dispose();
frame = null;
}
if (title != null) {
if (title.getParent() != null) {
title.getParent().remove(title);
}
title = null;
}
}
}
private class TipsUpdater extends Cancelable {
private Command command;
private List<Tip> tips;
private double alpha = -1;
private boolean showingOrHiding = true;
private long shownTime = -1;
public TipsUpdater(Command command, List<IGraphicalPart> parts,
String label) {
this.command = command;
tips = new ArrayList<Tip>(parts.size());
for (IGraphicalPart part : parts) {
tips.add(new Tip(part, label));
}
}
public void cancel() {
super.cancel();
disposeTips();
removeSubTask(command);
}
private void disposeTips() {
if (tips != null) {
for (Tip tip : tips) {
tip.dispose();
}
tips = null;
}
}
private double getRealAlphaStep(double step) {
return step * DEFAULT_DURATION / getDuration();
}
protected void doJob() {
if (MindMapUI.isAnimationEnabled()) {
if (showingOrHiding) {
if (alpha < 0) {
alpha = 0;
} else {
alpha += getRealAlphaStep(ALPHA_STEP);
if (alpha > 0xff) {
alpha = 0xff;
showingOrHiding = false;
}
}
} else if (alpha > 0) {
if (alpha > SPEEDING_UP_ALPHA) {
alpha -= getRealAlphaStep(ALPHA_STEP2);
} else {
alpha -= getRealAlphaStep(ALPHA_STEP3);
}
}
} else {
if (showingOrHiding) {
alpha = 0xff;
showingOrHiding = false;
if (shownTime < 0) {
shownTime = System.currentTimeMillis();
}
} else {
if (shownTime > 0) {
if (System.currentTimeMillis() > shownTime
+ getDuration()) {
cancel();
} else {
return;
}
} else {
cancel();
}
}
}
if (alpha <= 0) {
if (!showingOrHiding)
cancel();
} else if (tips != null) {
for (Tip tip : tips) {
tip.setAlpha((int) alpha);
}
}
}
}
private class MainTask implements Runnable {
private Display display;
public MainTask(Display display) {
this.display = display;
}
public void run() {
if (subTasks == null || subTasks.isEmpty()) {
cancel();
return;
}
if (Thread.currentThread() == display.getThread()) {
for (Object o : subTasks.values().toArray()) {
((ICancelable) o).run();
}
} else {
display.syncExec(new Runnable() {
public void run() {
for (Object o : subTasks.values().toArray()) {
((ICancelable) o).run();
}
}
});
}
if (subTasks == null || subTasks.isEmpty()) {
cancel();
return;
}
display.timerExec(UPDATE_INTERVALS, this);
}
private void cancel() {
mainTaskEnded();
}
}
/**
* The total duration of showing the tips.
*/
public static final int DEFAULT_DURATION = 800;
/**
* Intervals between two update task: 30 (milliseconds)
*/
private static final int UPDATE_INTERVALS = 20;
private static final double ALPHA_STEP = 30;
private static final double ALPHA_STEP2 = 2;
private static final double ALPHA_STEP3 = 8;
private static final int SPEEDING_UP_ALPHA = 0xc0;
private ICommandStack commandStack;
private IFigure layer;
private IFigure frameLayer;
private IFigure titleLayer;
private Runnable mainTask;
private Map<Command, ICancelable> subTasks;
private int duration = DEFAULT_DURATION;
public UndoRedoTipsService(IGraphicalViewer viewer) {
super(viewer);
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = Math.max(1, duration);
}
public IFigure getLayer() {
return layer;
}
public void setLayer(IFigure layer) {
boolean hadLayers = hasLayers();
this.layer = layer;
boolean hasLayers = hasLayers();
if (isActive()) {
if (hadLayers && !hasLayers) {
stopListenToCommandChange();
} else if (!hadLayers && hasLayers) {
startListenToCommandChange();
}
}
}
private IFigure getFrameLayer() {
ensureLayers();
return frameLayer;
}
private IFigure getTitleLayer() {
ensureLayers();
return titleLayer;
}
private void ensureLayers() {
if (layer == null)
return;
if (frameLayer != null && titleLayer != null)
return;
if (frameLayer == null) {
frameLayer = new FreeformLayer();
if (titleLayer != null) {
int index = layer.getChildren().indexOf(titleLayer);
layer.add(frameLayer, index);
} else {
layer.add(frameLayer);
}
}
if (titleLayer == null) {
titleLayer = new FreeformLayer();
layer.add(titleLayer);
}
}
private boolean hasLayers() {
return layer != null;
}
protected void activate() {
commandStack = getViewer().getEditDomain().getCommandStack();
if (hasLayers()) {
startListenToCommandChange();
}
}
protected void deactivate() {
if (subTasks != null) {
for (Object o : subTasks.keySet().toArray()) {
removeSubTask((Command) o);
}
subTasks = null;
}
stopListenToCommandChange();
commandStack = null;
mainTask = null;
}
private void startListenToCommandChange() {
if (commandStack != null)
commandStack.addCSListener(this);
}
private void stopListenToCommandChange() {
if (commandStack != null)
commandStack.removeCSListener(this);
}
public void handleCommandStackEvent(CommandStackEvent event) {
Command command = event.getCommand();
if (command == null)
return;
int commandType = command.getType();
if (event.getStatus() == GEF.CS_PRE_UNDO
&& commandType != GEF.CMD_DELETE) {
showTipsFor(command, true, commandType != GEF.CMD_CREATE);
} else if (event.getStatus() == GEF.CS_POST_UNDO
&& commandType == GEF.CMD_DELETE) {
showTipsFor(command, true, true);
} else if (event.getStatus() == GEF.CS_PRE_REDO
&& commandType != GEF.CMD_CREATE) {
showTipsFor(command, false, commandType != GEF.CMD_DELETE);
} else if (event.getStatus() == GEF.CS_POST_REDO
&& commandType == GEF.CMD_CREATE) {
showTipsFor(command, false, true);
}
}
private void showTipsFor(Command command, boolean undoOrRedo, boolean async) {
if (!(command instanceof ISourceProvider))
return;
Object[] elements = ((ISourceProvider) command).getSources().toArray();
List<IGraphicalPart> parts = new ArrayList<IGraphicalPart>(
elements.length);
for (Object element : elements) {
IPart part = getViewer().findPart(element);
if (part instanceof IGraphicalPart) {
parts.add((IGraphicalPart) part);
}
}
if (parts.isEmpty())
return;
showTipsFor(command, parts, getLabel(command, undoOrRedo), async);
}
private void showTipsFor(final Command command,
final List<IGraphicalPart> parts, final String label, boolean async) {
removeSubTask(command);
final TipsUpdater task = new TipsUpdater(command, parts, label);
if (async) {
Display.getCurrent().asyncExec(new Runnable() {
public void run() {
getViewer().getCanvas().getLightweightSystem()
.getUpdateManager().runWithUpdate(new Runnable() {
public void run() {
Display.getCurrent().asyncExec(
new Runnable() {
public void run() {
addSubTask(command, task);
}
});
}
});
}
});
} else {
addSubTask(command, task);
}
}
private void addSubTask(Command command, ICancelable task) {
if (subTasks == null)
subTasks = new HashMap<Command, ICancelable>();
subTasks.put(command, task);
ensureMainTaskStarted();
}
private void removeSubTask(Command command) {
if (subTasks == null || subTasks.isEmpty())
return;
ICancelable task = subTasks.remove(command);
if (task != null) {
task.cancel();
}
}
private void ensureMainTaskStarted() {
if (mainTask != null)
return;
mainTask = new MainTask(Display.getCurrent());
mainTask.run();
}
private void mainTaskEnded() {
mainTask = null;
subTasks = null;
}
private String getLabel(Command command, boolean undoOrRedo) {
String label = command.getLabel();
if (label == null)
label = ""; //$NON-NLS-1$
if (undoOrRedo)
return NLS.bind(MindMapMessages.UndoRedoTipsService_Undo, label);
return NLS.bind(MindMapMessages.UndoRedoTipsService_Redo, label);
}
}