/* ****************************************************************************** * 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.core.runtime.Assert; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.util.Util; 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.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Region; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; 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.Monitor; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.events.IHyperlinkListener; import org.xmind.ui.resources.ColorUtils; import org.xmind.ui.resources.FontUtils; public class NotificationWindow extends Window { private static final int DEFAULT_WIDTH = 260; private static final int MARGIN = 15; private static final int TWO_MARGINS = MARGIN + MARGIN; private static final int CORNER = 19; private static final int SPACING = 15; private static final int DEFAULT_DURATION = 30000; private static final int FINAL_ALPHA = 200; // private static final int FADE_IN_DURATION = 1200; // private static final int FADE_OUT_DURATION = 260; private static final int AUTO_FADE_OUT_DURATION = 4000; private static final int HOVER_FADE_IN_DURATION = 600; private static final Map<Monitor, List<NotificationWindow>> TRAILING_DIALOGS = new HashMap<Monitor, List<NotificationWindow>>( 2); private Shell sourceShell; private String title; private IAction action; private IAction moreAction; private int duration; private Region shape = null; private long startTime = 0; private boolean showing = false; private boolean closing = false; private boolean hovered = false; private Control closeButton = null; private boolean closeOnMoreLink = false; public NotificationWindow(Shell sourceShell, String title, IAction action, IAction moreAction, int duration) { super((Shell) null); this.title = title; Assert.isNotNull(action); this.sourceShell = sourceShell; this.action = action; this.moreAction = moreAction; if (duration < 0) { this.duration = DEFAULT_DURATION; } else { this.duration = duration; } setBlockOnOpen(false); setShellStyle(SWT.NO_TRIM | SWT.ON_TOP); } public NotificationWindow setCloseOnMoreLink(boolean closeOnMoreLink) { this.closeOnMoreLink = closeOnMoreLink; return this; } @Override protected void constrainShellSize() { Monitor monitor; if (sourceShell == null || sourceShell.isDisposed()) { monitor = Display.getCurrent().getPrimaryMonitor(); } else { monitor = sourceShell.getMonitor(); } Rectangle clientArea = monitor.getClientArea(); final Shell shell = getShell(); Point contentsSize = getContents().computeSize(DEFAULT_WIDTH, SWT.DEFAULT, true); Rectangle result = shell.computeTrim(0, 0, contentsSize.x + TWO_MARGINS, contentsSize.y + TWO_MARGINS); List<NotificationWindow> dialogs = TRAILING_DIALOGS.get(monitor); if (dialogs == null) { dialogs = new ArrayList<NotificationWindow>(10); TRAILING_DIALOGS.put(monitor, dialogs); } Shell trailingShell = findLastShell(dialogs); if (trailingShell != null && !trailingShell.isDisposed()) { result = computeNewLocation(clientArea, result, trailingShell.getBounds()); } else { result = computeNewLocation(clientArea, result); } shell.setBounds(result); dialogs.remove(this); dialogs.add(this); // Set shell shape: if (shape == null || shape.isDisposed()) { shape = createShape(shell, result); } shell.setRegion(shape); } private Shell findLastShell(List<NotificationWindow> dialogs) { for (int i = dialogs.size() - 1; i >= 0; i--) { NotificationWindow dialog = dialogs.get(i); Shell lastShell = dialog.getShell(); if (lastShell == null || lastShell.isDisposed()) { dialogs.remove(i); } else { return lastShell; } } return null; } private Rectangle computeNewLocation(Rectangle clientArea, Rectangle size, Rectangle lastBounds) { int newY = lastBounds.y + lastBounds.height + SPACING; if (newY + size.height < clientArea.y + clientArea.height) { return new Rectangle(lastBounds.x, newY, size.width, size.height); } int newX = lastBounds.x - SPACING - size.width; if (newX > clientArea.x) { return new Rectangle(newX, clientArea.y + SPACING, size.width, size.height); } return computeNewLocation(clientArea, size); } private Rectangle computeNewLocation(Rectangle clientArea, Rectangle size) { return new Rectangle(clientArea.x + clientArea.width - SPACING - size.width, clientArea.y + SPACING, size.width, size.height); } private Region createShape(Shell shell, Rectangle r) { int left = 0; int top = 0; int right = left + r.width; int bottom = top + r.height; Polygon polygon = new Polygon(250); polygon.lineTo(left + CORNER, top); polygon.lineTo(right, top); polygon.lineTo(right, bottom - CORNER); polygon.roundCornerTo(right, bottom, right - CORNER, bottom); polygon.lineTo(left, bottom); polygon.lineTo(left, top + CORNER); polygon.roundCornerTo(left, top, left + CORNER, top); final Region s = new Region(shell.getDisplay()); s.add(polygon.toPointList()); shell.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { s.dispose(); } }); return s; } @Override protected Layout getLayout() { GridLayout layout = new GridLayout(); layout.marginWidth = MARGIN; layout.marginHeight = MARGIN; layout.horizontalSpacing = 0; layout.verticalSpacing = 0; return layout; } @Override protected Control createContents(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); composite.setBackground(parent.getBackground()); composite.setForeground(parent.getForeground()); composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); GridLayout layout = new GridLayout(1, false); layout.marginWidth = 0; layout.marginHeight = 0; layout.verticalSpacing = 9; layout.horizontalSpacing = 10; composite.setLayout(layout); createIconAndActions(composite); createCloseButton(parent); return composite; } private void createIconAndActions(Composite parent) { Image image = null; final Image imageToDispose; ImageDescriptor icon = action.getImageDescriptor(); if (icon != null) { image = icon.createImage(false); imageToDispose = image; } else { imageToDispose = null; } if (image == null && sourceShell != null && !sourceShell.isDisposed()) { image = findBrandingImage(sourceShell.getImage(), sourceShell.getImages()); } if (image != null) { ((GridLayout) parent.getLayout()).numColumns = 2; Label iconLabel = new Label(parent, SWT.NONE); iconLabel.setBackground(parent.getBackground()); iconLabel.setForeground(parent.getForeground()); iconLabel.setImage(image); iconLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.BEGINNING, false, true)); Composite composite = new Composite(parent, SWT.NONE); composite.setBackground(parent.getBackground()); composite.setForeground(parent.getForeground()); GridLayout layout = new GridLayout(1, false); layout.marginWidth = 0; layout.marginHeight = 0; layout.verticalSpacing = 9; layout.horizontalSpacing = 0; composite.setLayout(layout); composite .setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); if (imageToDispose != null) { iconLabel.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { imageToDispose.dispose(); } }); } parent = composite; } createActions(parent); } private Image findBrandingImage(Image mainImage, Image[] images) { Image best = null; int scale = -1; Rectangle r; int s; if (mainImage != null) { r = mainImage.getBounds(); s = Math.abs(r.width - 48) * Math.abs(r.height - 48); if (scale < 0 || s < scale) { best = mainImage; scale = s; } } for (Image img : images) { r = img.getBounds(); s = Math.abs(r.width - 48) * Math.abs(r.height - 48); if (scale < 0 || s < scale) { best = img; scale = s; } } return best; } private void createActions(Composite parent) { createTitle(parent); createActionLink(parent); if (moreAction != null) { createMoreLink(parent); } } private void createTitle(Composite parent) { String text = Display.getAppName(); if (text == null && title == null) return; if (text == null) { text = title; } else if (title != null) { text = text + " - " + title; //$NON-NLS-1$ } Label label = new Label(parent, SWT.WRAP); label.setText(text); label.setBackground(parent.getBackground()); label.setForeground(parent.getForeground()); label.setFont(FontUtils.getBoldRelative(JFaceResources.DEFAULT_FONT, 1)); label.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); } private void createActionLink(Composite parent) { StyledLink link; String content = action.getText(); if (content.indexOf("<form>") >= 0) { //$NON-NLS-1$ link = new StyledLink(parent, SWT.NONE); } else { link = new StyledLink(parent, SWT.SIMPLE); } link.setText(content); link.setBackground(parent.getBackground()); link.setForeground(parent.getForeground()); link.setEnabled(action.isEnabled()); link.setFont(FontUtils.getRelativeHeight(JFaceResources.DEFAULT_FONT, Util.isMac() ? -2 : -1)); link.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); final IAction theAction = this.action; link.addHyperlinkListener(new IHyperlinkListener() { public void linkExited(HyperlinkEvent e) { } public void linkEntered(HyperlinkEvent e) { } public void linkActivated(HyperlinkEvent e) { Display.getCurrent().asyncExec(new Runnable() { public void run() { close(); theAction.run(); } }); } }); } private void createMoreLink(Composite parent) { String moreText = moreAction.getText(); if (moreText == null) { moreText = Messages.NotificationDialog_MoreLink_defaultText; } StyledLink link = new StyledLink(parent, SWT.SIMPLE); link.setText(moreText); link.setFont(FontUtils.getRelativeHeight(JFaceResources.DEFAULT_FONT, -2)); link.setBackground(parent.getBackground()); link.setForeground(ColorUtils.getColor(80, 100, 250)); link.setEnabled(moreAction.isEnabled()); link.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); final IAction theMoreAction = this.moreAction; link.addHyperlinkListener(new IHyperlinkListener() { public void linkExited(HyperlinkEvent e) { } public void linkEntered(HyperlinkEvent e) { } public void linkActivated(HyperlinkEvent e) { Display.getCurrent().asyncExec(new Runnable() { public void run() { if (closeOnMoreLink) { close(); } theMoreAction.run(); } }); } }); } private void createCloseButton(final Composite parent) { final Label label = new Label(parent, SWT.NONE); closeButton = label; label.setBackground(ColorUtils.getColor(8, 8, 8)); label.setForeground(parent.getForeground()); GridData layoutData = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); layoutData.exclude = true; label.setLayoutData(layoutData); label.setSize(32, 32); label.setLocation(0, 0); label.setCursor(parent.getDisplay().getSystemCursor(SWT.CURSOR_HAND)); label.moveAbove(null); label.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { GC gc = e.gc; gc.setAntialias(SWT.ON); gc.setLineStyle(SWT.LINE_SOLID); gc.setLineJoin(SWT.JOIN_ROUND); gc.setLineWidth(1); gc.fillRectangle(0, 0, 32, 32); gc.setLineWidth(3); gc.drawArc(5, 5, 21, 21, 0, 360); int a = 11, b = 20; gc.drawLine(a, a, b, b); gc.drawLine(a, b, b, a); } }); Polygon polygon = new Polygon(250); int x1, x2, x3, y1, y2, y3; x1 = y1 = 4; x2 = y2 = 16; x3 = y3 = 28; polygon.lineTo(x2, y1); polygon.roundCornerTo(x3, y1, x3, y2); polygon.roundCornerTo(x3, y3, x2, y3); polygon.roundCornerTo(x1, y3, x1, y2); polygon.roundCornerTo(x1, y1, x2, y1); final Region region = new Region(parent.getDisplay()); region.add(polygon.toPointList()); label.setRegion(region); label.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { region.dispose(); } }); label.setVisible(false); label.addListener(SWT.MouseDown, new Listener() { public void handleEvent(Event event) { event.display.asyncExec(new Runnable() { public void run() { close(); } }); } }); } private void toggleCloseButton(boolean visible) { if (closeButton != null && !closeButton.isDisposed()) { closeButton.setVisible(visible); } } @Override protected void configureShell(final Shell newShell) { super.configureShell(newShell); final Display display = newShell.getDisplay(); newShell.setBackground(display.getSystemColor(SWT.COLOR_BLACK)); newShell.setForeground(display.getSystemColor(SWT.COLOR_WHITE)); newShell.setAlpha(FINAL_ALPHA); } @Override public void create() { super.create(); final Shell shell = getShell(); final Listener mouseTracker = new Listener() { public void handleEvent(Event event) { if (shell.isDisposed()) return; Point p = ((Control) event.widget).toDisplay(event.x, event.y); if (shell.getBounds().contains(p)) { if (closing) return; hovered = true; toggleCloseButton(true); if (!showing && shell.getAlpha() < FINAL_ALPHA) { showing = true; fadeTo(FINAL_ALPHA, HOVER_FADE_IN_DURATION, new Runnable() { public void run() { showing = false; countDown(5000); } }); } } else { hovered = false; toggleCloseButton(false); } } }; trackMouseMove(shell, mouseTracker); } private void trackMouseMove(Control c, Listener mouseTracker) { c.addListener(SWT.MouseExit, mouseTracker); c.addListener(SWT.MouseEnter, mouseTracker); if (c instanceof Composite) { for (Control child : ((Composite) c).getChildren()) { trackMouseMove(child, mouseTracker); } } } private void fadeTo(final int finalAlpha, int defaultDuration, final Runnable callback) { final Shell shell = getShell(); if (shell == null || shell.isDisposed()) return; final int startAlpha = shell.getAlpha(); final int t = defaultDuration * 10 / FINAL_ALPHA; final int minAlpha = Math.min(startAlpha, finalAlpha); final int maxAlpha = Math.max(startAlpha, finalAlpha); final int[] step = new int[1]; step[0] = 0; final long start = System.currentTimeMillis(); startTime = start; Display.getCurrent().timerExec(1, new Runnable() { public void run() { if (startTime != start) return; if (shell == null || shell.isDisposed()) return; if (finalAlpha > startAlpha) step[0] += 10; else step[0] -= 10; int newAlpha = startAlpha + (int) (step[0] / t); if (newAlpha > maxAlpha || newAlpha < minAlpha) { if (callback != null) callback.run(); } else { if (newAlpha != shell.getAlpha()) { shell.setAlpha(newAlpha); } Display.getCurrent().timerExec(1, this); } } }); } public int open() { if (getShell() == null || getShell().isDisposed()) { // create the window create(); } // limit the shell size to the display size constrainShellSize(); // open the window getShell().setVisible(true); showing = true; closing = false; Display.getCurrent().asyncExec(new Runnable() { public void run() { countDown(duration); } }); // fadeTo(FINAL_ALPHA, FADE_IN_DURATION, new Runnable() { // public void run() { // showing = false; // countDown(duration); // } // }); return OK; } public boolean close() { closing = true; hovered = false; showing = false; hardClose(); // fadeTo(0, FADE_OUT_DURATION, new Runnable() { // public void run() { // hardClose(); // } // }); return true; } private void hardClose() { super.close(); Shell shell = getShell(); if (shell != null && !shell.isDisposed()) { shell.dispose(); } } private void countDown(int duration) { if (duration == 0) return; final long countDownTime = System.currentTimeMillis(); startTime = countDownTime; Display.getCurrent().timerExec(duration, new Runnable() { public void run() { if (startTime != countDownTime) return; if (hovered) { Display.getCurrent().timerExec(1000, this); } else { fadeTo(0, AUTO_FADE_OUT_DURATION, new Runnable() { public void run() { hardClose(); } }); } } }); } public static void main(String[] args) { Display.setAppName("Main"); //$NON-NLS-1$ Display display = new Display(); try { final IAction action = new Action() { @Override public void run() { super.run(); System.out.println("Action clicked."); //$NON-NLS-1$ } }; action.setText("<form><p>Test <b>test</b> test test\n test test test test waf awoijf oa&wfoaw <b><i>foaawefwfewfawj</i></b> foaw fo awof aw.</p><p>"WOfjsiowf" s fowe 文字 ofwjfosj</p></form>"); //$NON-NLS-1$ action.setImageDescriptor(ImageDescriptor.createFromImage(display .getSystemImage(SWT.ICON_INFORMATION))); action.setEnabled(true); final IAction action2 = new Action() { public void run() { System.out.println("Simple action clicked."); //$NON-NLS-1$ }; }; action2.setText("File has been downloaded:\nC:\\Users\\USER\\Download\\test.xmind\r\nXMind test test test aowif waof awiof jawiofaj wiofaowfjawoifjaw ofawoifjaowf aowe jfoaw."); //$NON-NLS-1$ final IAction moreAction = new Action() { @Override public void run() { super.run(); System.out.println("More clicked."); //$NON-NLS-1$ } }; moreAction.setText("Details..."); //$NON-NLS-1$ new NotificationWindow(null, "XMind", action, null, SWT.DEFAULT) //$NON-NLS-1$ .open(); new NotificationWindow(null, "Test Notification", action, //$NON-NLS-1$ moreAction, SWT.DEFAULT).open(); new NotificationWindow(null, null, action, null, 4000).open(); new NotificationWindow(null, null, action2, moreAction, SWT.DEFAULT) .open(); new NotificationWindow(null, null, action2, null, 10000).open(); Shell shell = new Shell(display); shell.setSize(400, 300); Button addButton = new Button(shell, SWT.PUSH); addButton.setText("Add"); //$NON-NLS-1$ addButton.setSize(100, addButton.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y); addButton.setLocation(10, 10); addButton.addListener(SWT.Selection, new Listener() { public void handleEvent(Event event) { new NotificationWindow(null, null, action, moreAction, SWT.DEFAULT).open(); } }); shell.open(); while (!shell.isDisposed() && !display.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } shell.dispose(); } finally { display.dispose(); } } }