package com.readytalk.swt.widgets.notifications; import com.readytalk.swt.effects.FadeEffect; import com.readytalk.swt.effects.FadeEffect.Fadeable; import com.readytalk.swt.effects.InvalidEffectArgumentException; import com.readytalk.swt.helpers.AncestryHelper; import com.readytalk.swt.helpers.WidgetHelper; import com.readytalk.swt.util.ColorFactory; import com.readytalk.swt.util.DisplaySafe; import com.readytalk.swt.widgets.CustomElementDataProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Region; import org.eclipse.swt.layout.FillLayout; 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.Shell; import org.eclipse.swt.widgets.Widget; import java.util.logging.Logger; /** * PopOverShell provides a simple interface for popping a Shell on top of any Object that subclasses * <code>Control</code> or implements <code>CustomElementDataProvider</code> */ public abstract class PopOverShell extends Widget implements Fadeable { private static final Logger LOG = Logger.getLogger(PopOverShell.class.getName()); private static final RGB BACKGROUND_COLOR = new RGB(74, 74, 74); private static final int FADE_OUT_TIME = 200; //milliseconds private static final int FULLY_VISIBLE_ALPHA = 255; //fully opaque private static final int FULLY_HIDDEN_ALPHA = 0; //fully transparent static final VerticalLocation DEFAULT_DISPLAY_LOCATION = VerticalLocation.BELOW; static final CenteringEdge DEFAULT_EDGE_CENTERED = CenteringEdge.LEFT; VerticalLocation popOverAboveOrBelowParent = DEFAULT_DISPLAY_LOCATION; CenteringEdge popOverEdgeCenteredOnParent = DEFAULT_EDGE_CENTERED; private Object fadeLock = new Object(); protected final Control parentControl; protected Shell popOverShell; private Shell parentShell; private PoppedOverItem poppedOverItem; private Listener popOverListener; private Listener parentListener; private Color backgroundColor; private Region popOverRegion; private boolean positionRelativeParent = false; private boolean fadeEffectInProgress = false; private DisplaySafe displaySafe; /** * Provides the backbone for Custom Widgets that need a <code>Shell</code> popped over a <code>Control</code> or * <code>CustomElementDataProvider</code>. If you're using a <code>CustomElementDataProvider</code>, pass the * <code>CustomElementDataProvider.getPaintedElement()</code> as the parentControl. * @param parentControl The control you want the PopOverShell to appear above. In the case of * <code>CustomElementDataProvider</code>, pass * <code>CustomElementDataProvider.getPaintedElement()</code>. * @param customElementDataProvider The <code>CustomElementDataProvider</code> you want the PopOverShell to appear * above (or null if you're using a Control) */ public PopOverShell(Control parentControl, CustomElementDataProvider customElementDataProvider) { super(parentControl, SWT.NONE); displaySafe = new DisplaySafe(); if (customElementDataProvider != null) { poppedOverItem = new PoppedOverItem(customElementDataProvider); } else { poppedOverItem = new PoppedOverItem(parentControl); } this.parentControl = parentControl; parentShell = AncestryHelper.getShellFromControl(poppedOverItem.getControl()); backgroundColor = ColorFactory.getColor(getDisplay(), BACKGROUND_COLOR); // SWT.TOOL adds a drop shadow on supported platforms popOverShell = new Shell(parentShell, SWT.NO_TRIM | SWT.TOOL); popOverShell.setBackground(backgroundColor); popOverShell.setLayout(new FillLayout()); attachListeners(); } /** * Shows the PopOverShell in a suitable location relative to the parent component. Classes extending PopOverShell will * provide the <code>Region</code> via the abstract <code>getAppropriatePopOverRegion()</code> method. */ public void show() { runBeforeShowPopOverShell(); Point popOverShellSize = getAppropriatePopOverSize(); popOverRegion = new Region(); popOverRegion.add(new Rectangle(0, 0, popOverShellSize.x, popOverShellSize.y)); Point location = getPopOverShellLocation(parentShell, poppedOverItem, popOverRegion); popOverShell.setRegion(popOverRegion); popOverShell.setSize(popOverRegion.getBounds().width, popOverRegion.getBounds().height); popOverShell.setLocation(location); popOverShell.setAlpha(FULLY_VISIBLE_ALPHA); popOverShell.setVisible(true); } /** * Toggles visibility of the PopOverShell. If the PopOverShell is visible, it will fade it from the screen, otherwise * it will pop it up. */ public void toggle() { if (isVisible() && !getIsFadeEffectInProgress()) { fadeOut(); } else { show(); } } public PopOverShell setPositionRelativeParent(boolean positionRelativeParent) { this.positionRelativeParent = positionRelativeParent; return this; } /** * Implementers of this method return a Point describing the width and height the PopOverShell should be. * @return A Point object describing the appropriate PopOverSize. The x is the width and y is the height. */ abstract Point getAppropriatePopOverSize(); /** * Implementers of this method run any logic that needs to be executed before the PopOverShell is shown to * the user. */ abstract void runBeforeShowPopOverShell(); /** * Implementers of this method should do any clean-up needed to reset the widget to its default state. */ abstract void resetWidget(); /** * Called when the parent <code>PopOverShell</code> is disposed. Make sure you clean up any leftover elements * that need to be disposed. See https://github.com/ReadyTalk/swt-bling/wiki/Finding-SWT-Resource-Leaks-with-Sleak * for more information on detecting leaks with Sleak. */ abstract void widgetDispose(); PoppedOverItem getPoppedOverItem() { return poppedOverItem; } Shell getPopOverShell() { return popOverShell; } public void checkSubclass() { //no-op } private void attachListeners() { popOverListener = new Listener() { public void handleEvent(Event event) { switch (event.type) { case SWT.Dispose: onDispose(event); break; } } }; addListener(SWT.Dispose, popOverListener); parentListener = new Listener() { public void handleEvent(Event event) { dispose(); } }; parentControl.addListener(SWT.Dispose, parentListener); } private void onDispose(Event event) { widgetDispose(); parentControl.removeListener(SWT.Dispose, parentListener); removeListener(SWT.Dispose, parentListener); event.type = SWT.None; popOverShell.dispose(); popOverShell = null; if (popOverRegion != null) { popOverRegion.dispose(); } popOverRegion = null; } private Point getPopOverShellLocation(Shell parentShell, PoppedOverItem poppedOverItem, Region popOverRegion) { Point location; Rectangle displayBounds = null; try { Display display = displaySafe.getLatestDisplay(); displayBounds = display.getBounds(); } catch (DisplaySafe.NullDisplayException nde) { LOG.warning("Could not find display"); } Rectangle popOverBounds = popOverRegion.getBounds(); Point poppedOverItemLocationRelativeToDisplay = getPoppedOverItemRelativeLocation(poppedOverItem); // Guess on the location first location = getPopOverDisplayPoint(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverEdgeCenteredOnParent, popOverAboveOrBelowParent); // Adjust as needed if (popOverAboveOrBelowParent == VerticalLocation.BELOW) { if (isBottomCutOff(displayBounds, location, popOverBounds)) { popOverAboveOrBelowParent = VerticalLocation.ABOVE; location.y = getPopOverYLocation(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverAboveOrBelowParent); } } else { if (isTopCutOff(location)) { popOverAboveOrBelowParent = VerticalLocation.BELOW; location.y = getPopOverYLocation(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverAboveOrBelowParent); } } if (popOverEdgeCenteredOnParent == CenteringEdge.LEFT) { if (isRightCutOff(displayBounds, location, popOverBounds)) { popOverEdgeCenteredOnParent = CenteringEdge.RIGHT; location.x = getPopOverXLocation(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverEdgeCenteredOnParent); } } else { if (isLeftCutOff(location)) { popOverEdgeCenteredOnParent = CenteringEdge.LEFT; location.x = getPopOverXLocation(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverEdgeCenteredOnParent); } } if (isStillOffScreen(displayBounds, location, popOverBounds)) { location = getPopOverLocationControlOffscreen(displayBounds, popOverRegion, poppedOverItemLocationRelativeToDisplay, location); } return location; } boolean isBottomCutOff(Rectangle displayBounds, Point locationRelativeToDisplay, Rectangle popOverBounds) { boolean isBottomCutOff = false; int lowestYPosition = locationRelativeToDisplay.y + popOverBounds.height; if (displayBounds != null && !displayBounds.contains(new Point(0, lowestYPosition))) { isBottomCutOff = true; } return isBottomCutOff; } boolean isTopCutOff(Point locationRelativeToDisplay) { return locationRelativeToDisplay.y >= 0 ? false : true; } boolean isRightCutOff(Rectangle displayBounds, Point locationRelativeToDisplay, Rectangle popOverBounds) { boolean isRightCutOff = false; int farthestXPosition = locationRelativeToDisplay.x + popOverBounds.width; if (displayBounds != null && !displayBounds.contains(new Point(farthestXPosition, 0))) { popOverEdgeCenteredOnParent = CenteringEdge.RIGHT; isRightCutOff = true; } return isRightCutOff; } boolean isLeftCutOff(Point locationRelativeToDisplay) { return locationRelativeToDisplay.x >= 0 ? false : true; } boolean isStillOffScreen(Rectangle displayBounds, Point locationRelativeToDisplay, Rectangle popOverBounds) { boolean isStillOffScreen = false; Point currentPosition = new Point (locationRelativeToDisplay.x + popOverBounds.width, locationRelativeToDisplay.y + popOverBounds.height); if (!displayBounds.contains(currentPosition)) { isStillOffScreen = true; } return isStillOffScreen; } Point getPoppedOverItemRelativeLocation(PoppedOverItem poppedOverItem) { Point location = null; if (positionRelativeParent == false) { Display display = null; try { display = displaySafe.getLatestDisplay(); } catch (DisplaySafe.NullDisplayException nde) { LOG.warning("Could not find active display."); } if(display != null) { location = display.map(parentShell, null, poppedOverItem.getLocation()); } } else { location = parentControl.toDisplay(poppedOverItem.getLocation()); } return location; } private Point getPopOverDisplayPoint(Rectangle popOverBounds, PoppedOverItem poppedOverItem, Point poppedOverItemLocationRelativeToDisplay, CenteringEdge popOverCornerCenteredOnParent, VerticalLocation popOverAboveOrBelowParent) { Point location = new Point(0, 0); location.x = getPopOverXLocation(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverCornerCenteredOnParent); location.y = getPopOverYLocation(popOverBounds, poppedOverItem, poppedOverItemLocationRelativeToDisplay, popOverAboveOrBelowParent); return location; } private int getPopOverXLocation(Rectangle popOverBounds, PoppedOverItem poppedOverItem, Point poppedOverItemLocationRelativeToDisplay, CenteringEdge popOverCornerCenteredOnParent) { int popOverX = 0; switch(popOverCornerCenteredOnParent) { case LEFT: popOverX = poppedOverItemLocationRelativeToDisplay.x + (poppedOverItem.getSize().x / 2); break; case RIGHT: popOverX = poppedOverItemLocationRelativeToDisplay.x - popOverBounds.width + (poppedOverItem.getSize().x / 2); break; } return popOverX; } private int getPopOverYLocation(Rectangle popOverBounds, PoppedOverItem poppedOverItem, Point poppedOverItemLocationRelativeToDisplay, VerticalLocation aboveOrBelow) { int popOverY = 0; switch (aboveOrBelow) { case ABOVE: popOverY = poppedOverItemLocationRelativeToDisplay.y - popOverBounds.height; break; case BELOW: popOverY = poppedOverItemLocationRelativeToDisplay.y + poppedOverItem.getSize().y; break; } return popOverY; } private Point getPopOverLocationControlOffscreen(Rectangle displayBounds, Region popOverRegion, Point poppedOverItemLocationRelativeToDisplay, Point popOverOffscreenLocation) { Point appropriateDisplayLocation = popOverOffscreenLocation; Rectangle popOverRegionBounds = popOverRegion.getBounds(); if (!displayBounds.contains(new Point(poppedOverItemLocationRelativeToDisplay.x + popOverRegionBounds.width, 0))) { appropriateDisplayLocation.x = displayBounds.width - popOverRegionBounds.width; } if (!displayBounds.contains(new Point(0, poppedOverItemLocationRelativeToDisplay.y + popOverRegionBounds.height))) { appropriateDisplayLocation.y = displayBounds.height - popOverRegionBounds.height; } return appropriateDisplayLocation; } /** * Returns whether the PopOverShell is currently visible on screen. * Note: If you utilize <code>PopOverShell.fadeOut()</code>, this method will return true while it's fading. * To determine if it's fading out, call <code>PopOverShell.getIsFadeEffectInProgress</code> * @return Visibility state of the PopOverShell */ public boolean isVisible() { boolean isVisible = false; if (WidgetHelper.isWidgetSafe(popOverShell)){ isVisible = popOverShell.isVisible(); } return isVisible; } /** * Fades the <code>PopOverShell</code> off the screen. */ public void fadeOut() { if (fadeEffectInProgress) { return; } try { fadeEffectInProgress = true; FadeEffect fade = new FadeEffect.FadeEffectBuilder(). setFadeable(this). setFadeCallback(new PopOverShellFadeCallback()). setFadeTimeInMilliseconds(FADE_OUT_TIME). setCurrentAlpha(FULLY_VISIBLE_ALPHA). setTargetAlpha(FULLY_HIDDEN_ALPHA).build(); fade.startEffect(); } catch (InvalidEffectArgumentException e) { LOG.warning("Invalid argument provided to FadeEffect."); } } /** * Returns whether the PopOverShell is currently fading from the screen. * Calls to <code>PopOverShell.isVisible()</code> will return true while the PopOverShell is dismissing. * @return Whether or not the PopOverShell is currently fading from the screen */ public boolean getIsFadeEffectInProgress() { return fadeEffectInProgress; } /** * Implemented as part of Fadeable. <br/> * Users should not interact directly invoke this method. */ public boolean fadeComplete(int targetAlpha) { synchronized (fadeLock) { boolean isFadeComplete = false; if (popOverShell == null || popOverShell.isDisposed() || popOverShell.getAlpha() == targetAlpha) { isFadeComplete = true; } return isFadeComplete; } } /** * Implemented as part of Fadeable. <br/> * Users should not interact directly invoke this method. */ public void fade(int alpha) { synchronized (fadeLock) { popOverShell.setAlpha(alpha); } } void hide() { popOverShell.setVisible(false); resetState(); resetWidget(); } private void resetState() { popOverAboveOrBelowParent = DEFAULT_DISPLAY_LOCATION; popOverEdgeCenteredOnParent = DEFAULT_EDGE_CENTERED; fadeEffectInProgress = false; } private class PopOverShellFadeCallback implements FadeEffect.FadeCallback { public void fadeComplete() { hide(); } } /** * A convenience structure for PopOverShell. We could be interacting with a <code>Control</code> (or descendant), * or we could be interacting with a {@link CustomElementDataProvider}. This wrapper helps to provide some * abstraction. */ public class PoppedOverItem { private Control control; private CustomElementDataProvider customElementDataProvider; public PoppedOverItem(Control control) { this.control = control; } public PoppedOverItem(CustomElementDataProvider customElementDataProvider) { this.customElementDataProvider = customElementDataProvider; } Point getSize() { if (control != null) { return control.getSize(); } else { return customElementDataProvider.getSize(); } } Point getLocation() { if (control != null) { return control.getLocation(); } else { return customElementDataProvider.getLocation(); } } Control getControl() { if (control != null) { return control; } else { return customElementDataProvider.getPaintedElement(); } } Object getControlOrCustomElement() { if (customElementDataProvider != null) { return customElementDataProvider; } else { return control; } } CustomElementDataProvider getCustomElementDataProvider() { return customElementDataProvider; } } }