/* ******************************************************************************
* 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.dialogs;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.xmind.ui.resources.ColorUtils;
import org.xmind.ui.util.UITimer;
import org.xmind.ui.viewers.ImageButton;
/**
* @author Frank Shaka
*/
public class SmoothPopupDialog extends Window {
private static Map<String, PopupGroup> groups = new HashMap<String, PopupGroup>();
private static class PopupGroup {
private Point initBottomRight = null;
private Point bottomRight = null;
private int width = 0;
private List<SmoothPopupDialog> dialogs = new ArrayList<SmoothPopupDialog>();
public Point getBottomRight() {
return bottomRight;
}
public void setBottomRight(int right, int bottom) {
this.initBottomRight = new Point(right, bottom);
this.bottomRight = new Point(right, bottom);
}
public void add(SmoothPopupDialog dialog, int height, int width) {
if (bottomRight == null)
throw new IllegalStateException();
dialogs.add(dialog);
int top = bottomRight.y - height;
if (top < Display.getCurrent().getClientArea().y) {
this.bottomRight.x -= this.width;
this.bottomRight.y = this.initBottomRight.y;
} else {
this.width = Math.max(this.width, width);
this.bottomRight.y -= height;
}
}
public void remove(SmoothPopupDialog dialog) {
dialogs.remove(dialog);
if (dialogs.isEmpty()) {
initBottomRight = null;
bottomRight = null;
}
}
}
private final class BorderFillLayout extends Layout {
private int borderWidth;
private int margin;
public BorderFillLayout(int borderWidth) {
this.borderWidth = borderWidth;
this.margin = borderWidth + borderWidth;
}
protected Point computeSize(Composite composite, int wHint, int hHint,
boolean flushCache) {
Point size = computeContentSize();
return new Point(size.x + margin, size.y + margin);
}
private Point computeContentSize() {
if (targetSize != null) {
return targetSize;
} else if (getContents() != null) {
return getContents().computeSize(DEFAULT_TARGET_SIZE.x,
DEFAULT_TARGET_SIZE.y);
}
return DEFAULT_TARGET_SIZE;
}
protected void layout(Composite composite, boolean flushCache) {
int x = borderWidth;
int y = borderWidth;
Rectangle clientArea = composite.getClientArea();
if (targetSize != null) {
clientArea.width = targetSize.x;
clientArea.height = targetSize.y;
}
int width = clientArea.width;
int height = clientArea.height;
Control[] children = composite.getChildren();
for (Control c : children) {
c.setBounds(x, y, width - 2 * borderWidth,
height - 2 * borderWidth);
}
}
}
protected class PullDownTask implements Runnable {
Display display;
boolean canceled = false;
public PullDownTask(Display display) {
this.display = display;
}
public void cancel() {
this.canceled = true;
}
public boolean isCanceled() {
return this.canceled;
}
public void run() {
if (canceled || display.isDisposed())
return;
pullDown();
}
}
protected static final String DEFAULT_BACKGROUDCOLOR_VALUE = "#e9e8e9"; //$NON-NLS-1$
private static final int VERTICAL_SPEED = 3;
private static final int ANIM_INTERVALS = 8;
private static final GridDataFactory LAYOUTDATA_GRAB_BOTH = GridDataFactory
.fillDefaults().grab(true, true);
private static final GridDataFactory LAYOUTDATA_GRAB_HORIZONTAL = GridDataFactory
.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, false);
private static final GridDataFactory LAYOUTDATA_ALIGN_RIGHT = GridDataFactory
.fillDefaults().align(SWT.END, SWT.CENTER);
private static final GridLayoutFactory LAYOUT_CONTENTS = GridLayoutFactory
.fillDefaults().numColumns(1).margins(1, 1)
.extendedMargins(0, 0, 0, 0).spacing(1, 1);
/**
* Margin width (in pixels) to be used in layouts inside popup dialogs
* (value is 0).
*/
public final static int POPUP_MARGINWIDTH = 0;
/**
* Margin height (in pixels) to be used in layouts inside popup dialogs
* (value is 0).
*/
public final static int POPUP_MARGINHEIGHT = 0;
/**
* Vertical spacing (in pixels) between cells in the layouts inside popup
* dialogs (value is 1).
*/
public final static int POPUP_VERTICALSPACING = 1;
/**
* Vertical spacing (in pixels) between cells in the layouts inside popup
* dialogs (value is 1).
*/
public final static int POPUP_HORIZONTALSPACING = 1;
/**
*
*/
private static final GridLayoutFactory POPUP_LAYOUT_FACTORY = GridLayoutFactory
.fillDefaults().margins(POPUP_MARGINWIDTH, POPUP_MARGINHEIGHT)
.spacing(POPUP_HORIZONTALSPACING, POPUP_VERTICALSPACING);
private static final int POPUP_GAP = 3;
/**
* Border thickness in pixels.
*/
private static final int BORDER_THICKNESS = 1;
private static int STAY_DURATION = 5000;
protected static ImageDescriptor IMG_CLOSE_NORMAL = null;
private Point DEFAULT_TARGET_SIZE;
private boolean showCloseButton = false;
private Point targetSize = null;
private String titleText = null;
private Control dialogArea = null;
private Point startingBottomRight = null;
private boolean centerPopUp = false;
private boolean popup = false;
private UITimer timer = null;
private int targetWidth = 0;
private int targetHeight = 0;
private int currentHeight = 0;
private PullDownTask pullDownTask = null;
private Control sourceControl = null;
private Listener sourceControlMoveListener = null;
private PopupGroup group = null;
private int duration = STAY_DURATION;
public SmoothPopupDialog(Shell parent, boolean showCloseButton,
String titleText) {
super(parent);
setShellStyle(SWT.TOOL);
setBlockOnOpen(false);
this.showCloseButton = showCloseButton;
this.titleText = titleText;
Point size = parent.getSize();
DEFAULT_TARGET_SIZE = new Point(size.x * 70 / 100, -1);
}
/**
*
* @param stayDuration
* the duration this dialog will stay on the screen, in
* milliseconds
*/
public void setDuration(int stayDuration) {
this.duration = stayDuration;
}
public int getDuration() {
return this.duration;
}
protected void configureShell(Shell shell) {
Display display = shell.getDisplay();
int border = getBorderThickness();
shell.setLayout(new BorderFillLayout(border));
if (border > 0) {
Color borderColor = getBorderColor(display);
if (borderColor == null)
borderColor = display.getSystemColor(SWT.COLOR_GRAY);
shell.setBackground(borderColor);
}
shell.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent event) {
handleDispose();
}
});
}
private int getBorderThickness() {
return ((getShellStyle() & SWT.NO_TRIM) == 0) ? 0 : BORDER_THICKNESS;
}
protected Control createContents(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
LAYOUT_CONTENTS.applyTo(composite);
if (hasTitleArea()) {
Control titleArea = createTitleArea(composite);
LAYOUTDATA_GRAB_HORIZONTAL.applyTo(titleArea);
}
dialogArea = createDialogArea(composite);
if (dialogArea.getLayoutData() == null) {
LAYOUTDATA_GRAB_BOTH.applyTo(dialogArea);
}
applyColors(composite);
return composite;
}
/**
* Creates and returns the contents of the dialog (the area below the title
* area and above the info text area.
* <p>
* The <code>PopupDialog</code> implementation of this framework method
* creates and returns a new <code>Composite</code> with standard margins
* and spacing.
* <p>
* The returned control's layout data must be an instance of
* <code>GridData</code>. This method must not modify the parent's layout.
* <p>
* Subclasses must override this method but may call <code>super</code> as
* in the following example:
*
* <pre>
* Composite composite = (Composite) super.createDialogArea(parent);
* //add controls to composite as necessary
* return composite;
* </pre>
*
* @param parent
* the parent composite to contain the dialog area
* @return the dialog area control
*/
protected Control createDialogArea(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
POPUP_LAYOUT_FACTORY.applyTo(composite);
return composite;
}
protected boolean hasTitleArea() {
return titleText != null || showCloseButton;
}
protected Control createTitleArea(Composite parent) {
Composite titleAreaComposite = new Composite(parent, SWT.NONE);
boolean hasTitle = titleText != null;
GridLayoutFactory.fillDefaults()
.numColumns(hasTitle && showCloseButton ? 2 : 1)
.applyTo(titleAreaComposite);
if (hasTitle) {
Control title = createTitleControl(titleAreaComposite);
LAYOUTDATA_GRAB_HORIZONTAL.applyTo(title);
}
if (showCloseButton) {
Control closeButton = createCloseButton(titleAreaComposite);
LAYOUTDATA_ALIGN_RIGHT.grab(!hasTitle, false).applyTo(closeButton);
}
return titleAreaComposite;
}
protected Control createCloseButton(Composite parent) {
ImageButton closeButton = new ImageButton(parent, SWT.NONE);
closeButton.setNormalImageDescriptor(getCloseButtonNormalImage());
closeButton.setDisabledImageDescriptor(getDisabledCloseButtonImage());
closeButton.setHoveredImageDescriptor(getHoverCloseButtonImage());
closeButton.setPressedImageDescriptor(getPressedCloseButtonImage());
closeButton.addOpenListener(new IOpenListener() {
public void open(OpenEvent event) {
close();
}
});
return closeButton.getControl();
}
public static void setDefaultCloseButtonNormalImage(ImageDescriptor img) {
IMG_CLOSE_NORMAL = img;
}
protected ImageDescriptor getCloseButtonNormalImage() {
if (IMG_CLOSE_NORMAL == null) {
IMG_CLOSE_NORMAL = createDefaultCloseButtonImage();
}
return IMG_CLOSE_NORMAL;
}
private static ImageDescriptor createDefaultCloseButtonImage() {
Display display = Display.getCurrent();
Image img = new Image(display, 16, 16);
GC gc = new GC(img);
gc.setForeground(display.getSystemColor(SWT.COLOR_GRAY));
gc.setBackground(ColorUtils.getColor(DEFAULT_BACKGROUDCOLOR_VALUE));
gc.fillRectangle(0, 0, 16, 16);
gc.setLineWidth(2);
gc.drawLine(4, 4, 11, 11);
gc.drawLine(4, 11, 11, 4);
gc.dispose();
ImageData data = img.getImageData();
img.dispose();
return ImageDescriptor.createFromImageData(data);
}
protected ImageDescriptor getHoverCloseButtonImage() {
return null;
}
protected ImageDescriptor getDisabledCloseButtonImage() {
return null;
}
protected ImageDescriptor getPressedCloseButtonImage() {
return null;
}
protected Control createTitleControl(Composite parent) {
Composite titleContainer = new Composite(parent, SWT.NONE);
GridLayoutFactory.fillDefaults().margins(5, 2).spacing(0, 0)
.numColumns(1).applyTo(titleContainer);
Label title = new Label(titleContainer, SWT.NONE);
if (titleText != null)
title.setText(titleText);
LAYOUTDATA_GRAB_HORIZONTAL.applyTo(title);
return titleContainer;
}
/**
* Apply any desired color to the specified composite and its children.
*
* @param composite
* the contents composite
* @param display
*/
private void applyColors(Composite composite) {
Display display = getShell().getDisplay();
applyForegroundColor(display.getSystemColor(SWT.COLOR_DARK_GRAY),
composite, getForegroundColorExclusions());
applyBackgroundColor(ColorUtils.getColor(DEFAULT_BACKGROUDCOLOR_VALUE),
composite, getBackgroundColorExclusions());
}
/**
* Set the specified foreground color for the specified control and all of
* its children, except for those specified in the list of exclusions.
*
* @param color
* the color to use as the foreground color
* @param control
* the control whose color is to be changed
* @param exclusions
* a list of controls who are to be excluded from getting their
* color assigned
*/
private void applyForegroundColor(Color color, Control control,
List exclusions) {
if (exclusions.contains(control))
return;
control.setForeground(color);
if (control instanceof Composite) {
Control[] children = ((Composite) control).getChildren();
for (int i = 0; i < children.length; i++) {
applyForegroundColor(color, children[i], exclusions);
}
}
}
/**
* Set the specified background color for the specified control and all of
* its children.
*
* @param color
* the color to use as the background color
* @param control
* the control whose color is to be changed
* @param exclusions
* a list of controls who are to be excluded from getting their
* color assigned
*/
private void applyBackgroundColor(Color color, Control control,
List exclusions) {
if (exclusions.contains(control))
return;
control.setBackground(color);
if (control instanceof Composite) {
Control[] children = ((Composite) control).getChildren();
for (int i = 0; i < children.length; i++) {
applyBackgroundColor(color, children[i], exclusions);
}
}
}
/**
* Set the specified foreground color for the specified control and all of
* its children. Subclasses may override this method, but typically do not.
* If a subclass wishes to exclude a particular control in its contents from
* getting the specified foreground color, it may instead override
* <code>PopupDialog.getForegroundColorExclusions</code>.
*
* @param color
* the color to use as the background color
* @param control
* the control whose color is to be changed
* @see PopupDialog#getBackgroundColorExclusions()
*/
protected void applyForegroundColor(Color color, Control control) {
applyForegroundColor(color, control, getForegroundColorExclusions());
}
/**
* Set the specified background color for the specified control and all of
* its children. Subclasses may override this method, but typically do not.
* If a subclass wishes to exclude a particular control in its contents from
* getting the specified background color, it may instead override
* <code>PopupDialog.getBackgroundColorExclusions</code>.
*
* @param color
* the color to use as the background color
* @param control
* the control whose color is to be changed
* @see PopupDialog#getBackgroundColorExclusions()
*/
protected void applyBackgroundColor(Color color, Control control) {
applyBackgroundColor(color, control, getBackgroundColorExclusions());
}
/**
* Return a list of controls which should never have their foreground color
* reset. Subclasses may extend this method (should always call
* <code>super.getForegroundColorExclusions</code> to aggregate the list.
*
* @return the List of controls
*/
protected List getForegroundColorExclusions() {
List list = new ArrayList(3);
return list;
}
/**
* Return a list of controls which should never have their background color
* reset. Subclasses may extend this method (should always call
* <code>super.getBackgroundColorExclusions</code> to aggregate the list.
*
* @return the List of controls
*/
protected List getBackgroundColorExclusions() {
List list = new ArrayList(2);
return list;
}
protected Color getBorderColor(Display display) {
return null;
}
protected Point getTargetSize() {
return targetSize;
}
protected void setTargetSize(Point targetSize) {
this.targetSize = targetSize;
}
protected void setCenterPopUp(boolean centerPopUp) {
this.centerPopUp = centerPopUp;
}
public void setGroupId(String groupId) {
if (groupId == null) {
if (group != null) {
group.remove(this);
group = null;
}
} else {
group = groups.get(groupId);
if (group == null) {
group = new PopupGroup();
groups.put(groupId, group);
}
}
}
public void popUp() {
Display display = Display.getCurrent();
Shell shell = null;
if (PlatformUI.isWorkbenchRunning()) {
IWorkbenchWindow window = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow();
if (window != null) {
shell = window.getShell();
}
}
if (shell == null)
shell = display.getActiveShell();
if (shell != null && !shell.isDisposed()) {
popUp(shell);
} else {
Rectangle area = display.getClientArea();
popUp(area.x + area.width - 50, area.y + area.height - 50);
}
}
public void popUp(Control sourceControl) {
open(sourceControl, true);
}
public void popUp(int right, int bottom) {
open(right, bottom, true);
}
public void open(Control sourceControl) {
open(sourceControl, false);
}
public void open(int right, int bottom) {
open(right, bottom, false);
}
protected void open(Control sourceControl, boolean popup) {
if (sourceControlMoveListener != null) {
sourceControl.removeListener(SWT.Move, sourceControlMoveListener);
if (this.sourceControl != null) {
this.sourceControl.removeListener(SWT.Move,
sourceControlMoveListener);
}
}
this.sourceControl = sourceControl;
if (sourceControlMoveListener == null) {
sourceControlMoveListener = new Listener() {
public void handleEvent(Event event) {
group = null;
updateShellBounds(currentHeight);
}
};
}
this.sourceControl.addListener(SWT.Move, sourceControlMoveListener);
Point bottomRight = getBottomRight(sourceControl);
open(bottomRight.x, bottomRight.y, popup);
}
private Point getBottomRight(Control sourceControl) {
Rectangle bounds = getSourceArea(sourceControl);
Point loc = sourceControl.toDisplay(bounds.x, bounds.y);
int pointX = loc.x + bounds.width - POPUP_GAP;
if (centerPopUp) {
pointX = pointX - bounds.width / 2 + POPUP_GAP;
if (targetSize != null)
pointX += targetSize.x / 2;
else
pointX += DEFAULT_TARGET_SIZE.x / 2;
}
return new Point(pointX, loc.y + bounds.height - POPUP_GAP);
}
private Rectangle getSourceArea(Control sourceControl) {
Rectangle bounds = sourceControl.getBounds();
if (sourceControl instanceof Composite) {
Rectangle computedSize = ((Composite) sourceControl)
.getClientArea();
return computedSize;
}
return bounds;
}
protected void open(int right, int bottom, boolean popup) {
this.startingBottomRight = getStartingBottomRight(right, bottom);
this.popup = popup;
open();
}
private Point getStartingBottomRight(int right, int bottom) {
if (group != null) {
Point bottomRight = group.getBottomRight();
if (bottomRight == null) {
group.setBottomRight(right, bottom);
bottomRight = group.getBottomRight();
}
return new Point(bottomRight.x, bottomRight.y);
}
return new Point(right, bottom);
}
public int open() {
stop();
Shell shell = showShell();
if (group != null) {
group.add(this, targetHeight, targetWidth);
}
if (shell != null && !shell.isDisposed()) {
if (popup && startingBottomRight != null) {
doPopUp(shell);
} else {
postOpen();
}
popup = false;
}
return OK;
}
private Shell showShell() {
Shell shell = getShell();
if (shell == null || shell.isDisposed()) {
shell = null;
// create the window
create();
shell = getShell();
}
initializeBounds();
// limit the shell size to the display size
constrainShellSize();
shell.setVisible(true);
return shell;
}
private void doPopUp(Shell shell) {
currentHeight = shell.getSize().y;
timer = new UITimer(0, ANIM_INTERVALS, new SafeRunnable() {
public void run() {
currentHeight += VERTICAL_SPEED;
if (currentHeight > targetHeight) {
stop();
postOpen();
} else {
updateShellBounds(currentHeight);
}
}
});
timer.run();
}
protected void postOpen() {
updateShellBounds(targetHeight);
if (getDuration() > 0) {
Display display = getShell().getDisplay();
pullDownTask = new PullDownTask(display);
display.timerExec(getDuration(), pullDownTask);
}
currentHeight = targetHeight;
}
protected void updateShellBounds(int height) {
Shell shell = getShell();
if (shell != null && !shell.isDisposed()) {
shell.setRedraw(false);
shell.setBounds(getCurrentBounds(height, shell));
shell.setRedraw(true);
}
}
protected Rectangle getCurrentBounds(int height, Shell shell) {
Point bottomRight;
Point start;
if (sourceControl != null && group == null) {
start = getBottomRight(sourceControl);
} else {
start = startingBottomRight;
}
if (start != null) {
bottomRight = new Point(start.x, start.y);
} else {
Rectangle bounds = shell.getBounds();
bottomRight = new Point(bounds.x + bounds.width,
bounds.y + bounds.height);
}
int x = bottomRight.x - targetWidth;
int y = bottomRight.y - height;
return new Rectangle(x, y, targetWidth, height);
}
private void stop() {
if (timer != null) {
timer.cancel();
timer = null;
}
if (pullDownTask != null) {
pullDownTask.cancel();
pullDownTask = null;
}
}
protected Point getInitialSize() {
Point size = super.getInitialSize();
targetWidth = size.x;
targetHeight = size.y;
if (!popup)
return size;
return new Point(size.x, 1);
}
protected Point getInitialLocation(Point initialSize) {
if (startingBottomRight == null)
return super.getInitialLocation(initialSize);
return new Point(startingBottomRight.x - initialSize.x,
startingBottomRight.y - initialSize.y);
}
public static int getStayDuration() {
return STAY_DURATION;
}
public static void setStayDuration(int duration) {
STAY_DURATION = duration;
}
public boolean isShowing() {
return getShell() != null && !getShell().isDisposed();
}
public boolean close() {
stop();
currentHeight = 0;
if (group != null) {
group.remove(this);
}
return super.close();
}
public void pullDown() {
stop();
Shell shell = getShell();
if (shell != null && !shell.isDisposed()) {
targetHeight = 0;
doPullDown(shell);
}
}
private void doPullDown(Shell shell) {
Rectangle bounds = shell.getBounds();
currentHeight = bounds.height;
timer = new UITimer(0, ANIM_INTERVALS, new SafeRunnable() {
public void run() {
currentHeight -= VERTICAL_SPEED;
if (currentHeight <= 0) {
close();
} else {
updateShellBounds(currentHeight);
}
}
});
timer.run();
}
protected void handleDispose() {
stop();
if (sourceControl != null && !sourceControl.isDisposed()) {
if (sourceControlMoveListener != null) {
sourceControl.removeListener(SWT.Move,
sourceControlMoveListener);
}
sourceControl = null;
}
}
}