| /******************************************************************************* |
| * Copyright (c) 2004, 2020 Tasktop Technologies and others. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * https://www.eclipse.org/legal/epl-2.0 |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Benjamin Pasero - initial API and implementation |
| * Tasktop Technologies - initial API and implementation |
| * SAP SE - port to platform.ui |
| *******************************************************************************/ |
| package org.eclipse.jface.notifications; |
| |
| import org.eclipse.core.runtime.IProduct; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jface.notifications.internal.AnimationUtil; |
| import org.eclipse.jface.notifications.internal.AnimationUtil.FadeJob; |
| import org.eclipse.jface.notifications.internal.CommonImages; |
| import org.eclipse.jface.notifications.internal.Messages; |
| import org.eclipse.jface.resource.JFaceResources; |
| import org.eclipse.jface.resource.LocalResourceManager; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.MouseAdapter; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseTrackAdapter; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Label; |
| import org.eclipse.swt.widgets.Monitor; |
| import org.eclipse.swt.widgets.Shell; |
| |
| /** |
| * @since 0.2 |
| */ |
| public abstract class AbstractNotificationPopup extends Window { |
| |
| private static final int TITLE_HEIGHT = 24; |
| |
| private static final String LABEL_NOTIFICATION = Messages.AbstractNotificationPopup_Label; |
| |
| private static final String LABEL_JOB_CLOSE = Messages.AbstractNotificationPopup_CloseJobTitle; |
| |
| private static final int MAX_WIDTH = 400; |
| |
| private static final int MIN_HEIGHT = 100; |
| |
| private static final long DEFAULT_DELAY_CLOSE = 8 * 1000; |
| |
| private static final int PADDING_EDGE = 5; |
| |
| private long delayClose = DEFAULT_DELAY_CLOSE; |
| |
| protected LocalResourceManager resources; |
| |
| private final Display display; |
| |
| private Shell shell; |
| |
| private final Job closeJob = new Job(LABEL_JOB_CLOSE) { |
| |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| if (!AbstractNotificationPopup.this.display.isDisposed()) { |
| AbstractNotificationPopup.this.display.asyncExec(() -> { |
| Shell shell = AbstractNotificationPopup.this.getShell(); |
| if (shell == null || shell.isDisposed()) { |
| return; |
| } |
| |
| if (isMouseOver(shell)) { |
| scheduleAutoClose(); |
| return; |
| } |
| |
| AbstractNotificationPopup.this.closeFade(); |
| }); |
| } |
| if (monitor.isCanceled()) { |
| return Status.CANCEL_STATUS; |
| } |
| |
| return Status.OK_STATUS; |
| } |
| }; |
| |
| private FadeJob fadeJob; |
| |
| private boolean fadingEnabled; |
| |
| public AbstractNotificationPopup(Display display) { |
| this(display, SWT.NO_TRIM | SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); |
| } |
| |
| public AbstractNotificationPopup(Display display, int style) { |
| super((Shell) null); |
| setShellStyle(style); |
| |
| this.display = display; |
| this.resources = new LocalResourceManager(JFaceResources.getResources()); |
| |
| this.closeJob.setSystem(true); |
| } |
| |
| public boolean isFadingEnabled() { |
| return this.fadingEnabled; |
| } |
| |
| public void setFadingEnabled(boolean fadingEnabled) { |
| this.fadingEnabled = fadingEnabled; |
| } |
| |
| @Override |
| public void create() { |
| super.create(); |
| } |
| |
| @Override |
| public int open() { |
| if (this.shell == null || this.shell.isDisposed()) { |
| this.shell = null; |
| create(); |
| } |
| |
| constrainShellSize(); |
| |
| if (isFadingEnabled()) { |
| this.shell.setAlpha(0); |
| } |
| this.shell.setVisible(true); |
| this.fadeJob = AnimationUtil.fadeIn(this.shell, (shell, alpha) -> { |
| if (shell.isDisposed()) { |
| return; |
| } |
| |
| if (alpha == 255) { |
| scheduleAutoClose(); |
| } |
| }); |
| |
| return Window.OK; |
| } |
| |
| @Override |
| public boolean close() { |
| this.resources.dispose(); |
| return super.close(); |
| } |
| |
| public long getDelayClose() { |
| return this.delayClose; |
| } |
| |
| public void setDelayClose(long delayClose) { |
| this.delayClose = delayClose; |
| } |
| |
| public void closeFade() { |
| if (this.fadeJob != null) { |
| this.fadeJob.cancelAndWait(false); |
| } |
| this.fadeJob = AnimationUtil.fadeOut(getShell(), (shell, alpha) -> { |
| if (!shell.isDisposed()) { |
| if (alpha == 0) { |
| shell.close(); |
| } else if (isMouseOver(shell)) { |
| if (AbstractNotificationPopup.this.fadeJob != null) { |
| AbstractNotificationPopup.this.fadeJob.cancelAndWait(false); |
| } |
| AbstractNotificationPopup.this.fadeJob = AnimationUtil.fastFadeIn(shell, (shell1, alpha1) -> { |
| if (shell1.isDisposed()) { |
| return; |
| } |
| |
| if (alpha1 == 255) { |
| scheduleAutoClose(); |
| } |
| }); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Override to return a customized name. Default is to return the name of the product, specified by the -name (e.g. |
| * "Eclipse SDK") command line parameter that's associated with the product ID (e.g. "org.eclipse.sdk.ide"). Strips |
| * the trailing "SDK" for any name, since this part of the label is considered visual noise. |
| * |
| * @return the name to be used in the title of the popup. |
| */ |
| protected String getPopupShellTitle() { |
| String productName = getProductName(); |
| if (productName != null) { |
| return productName + " " + LABEL_NOTIFICATION; //$NON-NLS-1$ |
| } else { |
| return LABEL_NOTIFICATION; |
| } |
| } |
| |
| protected Image getPopupShellImage(int maximumHeight) { |
| return null; |
| } |
| |
| /** |
| * Override to populate with notifications. |
| * |
| * @param parent Parent for this component. |
| */ |
| protected void createContentArea(Composite parent) { |
| // empty by default |
| } |
| |
| /** |
| * Override to customize the title bar |
| */ |
| protected void createTitleArea(Composite parent) { |
| ((GridData) parent.getLayoutData()).heightHint = TITLE_HEIGHT; |
| |
| Label titleImageLabel = new Label(parent, SWT.NONE); |
| titleImageLabel.setImage(getPopupShellImage(TITLE_HEIGHT)); |
| |
| Label titleTextLabel = new Label(parent, SWT.NONE); |
| titleTextLabel.setText(getPopupShellTitle()); |
| titleTextLabel.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT)); |
| titleTextLabel.setForeground(getTitleForeground()); |
| titleTextLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); |
| titleTextLabel.setCursor(parent.getDisplay().getSystemCursor(SWT.CURSOR_HAND)); |
| |
| final Label button = new Label(parent, SWT.NONE); |
| button.setImage(CommonImages.getImage(CommonImages.NOTIFICATION_CLOSE)); |
| button.addMouseTrackListener(new MouseTrackAdapter() { |
| @Override |
| public void mouseEnter(MouseEvent e) { |
| button.setImage(CommonImages.getImage(CommonImages.NOTIFICATION_CLOSE_HOVER)); |
| } |
| |
| @Override |
| public void mouseExit(MouseEvent e) { |
| button.setImage(CommonImages.getImage(CommonImages.NOTIFICATION_CLOSE)); |
| } |
| }); |
| button.addMouseListener(new MouseAdapter() { |
| |
| @Override |
| public void mouseUp(MouseEvent e) { |
| close(); |
| setReturnCode(CANCEL); |
| } |
| |
| }); |
| } |
| |
| protected Color getTitleForeground() { |
| return display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW); |
| } |
| |
| @Override |
| protected void configureShell(Shell newShell) { |
| super.configureShell(newShell); |
| this.shell = newShell; |
| } |
| |
| protected void scheduleAutoClose() { |
| if (this.delayClose > 0) { |
| this.closeJob.schedule(this.delayClose); |
| } |
| } |
| |
| @Override |
| protected Control createContents(Composite parent) { |
| ((GridLayout) parent.getLayout()).marginWidth = 1; |
| ((GridLayout) parent.getLayout()).marginHeight = 1; |
| |
| /* Outer Composite holding the controls */ |
| final Composite outerCircle = new Composite(parent, SWT.NO_FOCUS); |
| outerCircle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| GridLayout layout = new GridLayout(1, false); |
| layout.marginWidth = 0; |
| layout.marginHeight = 0; |
| layout.verticalSpacing = 0; |
| |
| outerCircle.setLayout(layout); |
| |
| /* Title area containing label and close button */ |
| final Composite titleCircle = new Composite(outerCircle, SWT.NO_FOCUS); |
| titleCircle.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); |
| |
| layout = new GridLayout(4, false); |
| layout.marginWidth = 3; |
| layout.marginHeight = 0; |
| layout.verticalSpacing = 5; |
| layout.horizontalSpacing = 3; |
| |
| titleCircle.setLayout(layout); |
| |
| /* Create Title Area */ |
| createTitleArea(titleCircle); |
| |
| /* Outer composite to hold content controlls */ |
| Composite outerContentCircle = new Composite(outerCircle, SWT.NONE); |
| |
| layout = new GridLayout(1, false); |
| layout.marginWidth = 0; |
| layout.marginHeight = 0; |
| |
| outerContentCircle.setLayout(layout); |
| outerContentCircle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| /* Middle composite to show a 1px black line around the content controls */ |
| Composite middleContentCircle = new Composite(outerContentCircle, SWT.NO_FOCUS); |
| |
| layout = new GridLayout(1, false); |
| layout.marginWidth = 0; |
| layout.marginHeight = 0; |
| layout.marginTop = 1; |
| |
| middleContentCircle.setLayout(layout); |
| middleContentCircle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| /* Inner composite containing the content controls */ |
| Composite innerContent = new Composite(middleContentCircle, SWT.NO_FOCUS); |
| innerContent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| layout = new GridLayout(1, false); |
| layout.marginWidth = 0; |
| layout.marginHeight = 5; |
| layout.marginLeft = 5; |
| layout.marginRight = 5; |
| innerContent.setLayout(layout); |
| |
| innerContent.setBackground(this.shell.getDisplay().getSystemColor(SWT.COLOR_WHITE)); |
| |
| /* Content Area */ |
| createContentArea(innerContent); |
| |
| return outerCircle; |
| } |
| |
| @Override |
| protected void initializeBounds() { |
| Rectangle clArea = getPrimaryClientArea(); |
| Point initialSize = this.shell.computeSize(SWT.DEFAULT, SWT.DEFAULT); |
| int height = Math.max(initialSize.y, MIN_HEIGHT); |
| int width = Math.min(initialSize.x, MAX_WIDTH); |
| |
| Point size = new Point(width, height); |
| this.shell.setLocation(clArea.width + clArea.x - size.x - PADDING_EDGE, clArea.height + clArea.y - size.y |
| - PADDING_EDGE); |
| this.shell.setSize(size); |
| } |
| |
| private static String getProductName() { |
| IProduct product = Platform.getProduct(); |
| if (product != null) { |
| String productName = product.getName(); |
| if (productName != null) { |
| String LABEL_SDK = "SDK"; //$NON-NLS-1$ |
| if (productName.endsWith(LABEL_SDK)) { |
| productName = productName.substring(0, productName.length() - LABEL_SDK.length()).trim(); |
| } |
| return productName; |
| } |
| } |
| return null; |
| } |
| |
| private boolean isMouseOver(Shell shell) { |
| if (this.display.isDisposed()) { |
| return false; |
| } |
| return shell.getBounds().contains(this.display.getCursorLocation()); |
| } |
| |
| private Rectangle getPrimaryClientArea() { |
| Shell parentShell = getParentShell(); |
| if (parentShell != null) { |
| // calculate client area in display-relative coordinates |
| // (i.e. without window border / decorations) |
| Rectangle bounds = parentShell.getBounds(); |
| Rectangle trim = parentShell.computeTrim(0, 0, 0, 0); |
| return new Rectangle(bounds.x - trim.x, bounds.y - trim.y, bounds.width - trim.width, |
| bounds.height - trim.height); |
| } |
| // else display on primary monitor |
| Monitor primaryMonitor = this.shell.getDisplay().getPrimaryMonitor(); |
| return (primaryMonitor != null) ? primaryMonitor.getClientArea() : this.shell.getDisplay().getClientArea(); |
| } |
| |
| } |