Fixed issues with close button not working. Fixed issues with adding

notifications to an application. Fixed issues when a toolbar changes the
 size of the desktop (and so offsets windows).
This commit is contained in:
nathan 2017-09-17 14:32:52 +02:00
parent abc7966f19
commit ce758c4c17
12 changed files with 456 additions and 209 deletions

View File

@ -61,6 +61,7 @@ Notify.MOVE_DURATION (type float, default value '1.0F')
Release Notes Release Notes
--------- ---------
It is important to note that notifications for an application use the [glassPane](https://docs.oracle.com/javase/tutorial/uiswing/components/rootpane.html#glasspane) and sets it's ````layoutManager```` to ````null````. This can cause problems with some applications, and you'll need to work around this limitation.
This project includes some utility classes that are a small subset of a much larger library. These classes are **kept in sync** with the main utilities library, so "jar hell" is not an issue, and the latest release will always include the same version of utility files as all of the other projects in the dorkbox repository at that time. This project includes some utility classes that are a small subset of a much larger library. These classes are **kept in sync** with the main utilities library, so "jar hell" is not an issue, and the latest release will always include the same version of utility files as all of the other projects in the dorkbox repository at that time.
@ -75,7 +76,7 @@ Maven Info
<dependency> <dependency>
<groupId>com.dorkbox</groupId> <groupId>com.dorkbox</groupId>
<artifactId>Notify</artifactId> <artifactId>Notify</artifactId>
<version>3.4</version> <version>3.5</version>
</dependency> </dependency>
</dependencies> </dependencies>
``` ```

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015 dorkbox, llc * Copyright 2017 dorkbox, llc
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,99 +15,107 @@
*/ */
package dorkbox.notify; package dorkbox.notify;
import java.awt.Dialog; import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame; import java.awt.Frame;
import java.awt.Window;
import java.awt.event.ComponentEvent; import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener; import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener; import java.awt.event.WindowStateListener;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.JPanel;
import dorkbox.util.SwingUtil; import dorkbox.util.SwingUtil;
// this is a child to a Jframe/window (instead of globally to the screen) // this is a child to a Jframe/window (instead of globally to the screen)
@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"}) @SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"})
public public
class AsDialog extends JDialog implements INotify { class AsApplication implements INotify {
private static final long serialVersionUID = 1L;
private final LookAndFeel look; private final LookAndFeel look;
private final Notify notification; private final Notify notification;
private final NotifyCanvas notifyCanvas;
private final JFrame appWindow;
private final ComponentListener parentListener; private final ComponentListener parentListener;
private final WindowStateListener windowStateListener;
private static final String glassPanePrefix = "dorkbox.notify";
private JPanel glassPane;
// this is on the swing EDT // this is on the swing EDT
@SuppressWarnings("NumericCastThatLosesPrecision") @SuppressWarnings("NumericCastThatLosesPrecision")
AsDialog(final Notify notification, final ImageIcon image, final Window container, final Theme theme) { AsApplication(final Notify notification, final ImageIcon image, final JFrame appWindow, final Theme theme) {
super(container, Dialog.ModalityType.MODELESS);
this.notification = notification; this.notification = notification;
this.notifyCanvas = new NotifyCanvas(this, notification, image, theme);
this.appWindow = appWindow;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); look = new LookAndFeel(this, appWindow, notifyCanvas, notification, appWindow.getBounds(), false);
setUndecorated(true);
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT); // this makes sure that our notify canvas stay anchored to the parent window (if it's hidden/shown/moved/etc)
setPreferredSize(preferredSize);
setMaximumSize(preferredSize);
setMinimumSize(preferredSize);
setSize(NotifyCanvas.WIDTH, NotifyCanvas.HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
setTitle(notification.title);
setResizable(false);
NotifyCanvas notifyCanvas = new NotifyCanvas(notification, image, theme);
add(notifyCanvas);
look = new LookAndFeel(this, notifyCanvas, notification, image, container.getBounds());
// this makes sure that our dialogs follow the parent window (if it's hidden/shown/moved/etc)
parentListener = new ComponentListener() { parentListener = new ComponentListener() {
@Override @Override
public public
void componentShown(final ComponentEvent e) { void componentShown(final ComponentEvent e) {
AsDialog.this.setVisible(true); look.reLayout(appWindow.getBounds());
look.reLayout(container.getBounds());
} }
@Override @Override
public public
void componentHidden(final ComponentEvent e) { void componentHidden(final ComponentEvent e) {
AsDialog.this.setVisible(false);
} }
@Override @Override
public public
void componentResized(final ComponentEvent e) { void componentResized(final ComponentEvent e) {
look.reLayout(container.getBounds()); look.reLayout(appWindow.getBounds());
} }
@Override @Override
public public
void componentMoved(final ComponentEvent e) { void componentMoved(final ComponentEvent e) {
look.reLayout(container.getBounds());
} }
}; };
container.addWindowStateListener(new WindowStateListener() { windowStateListener = new WindowStateListener() {
@Override @Override
public public
void windowStateChanged(WindowEvent e) { void windowStateChanged(WindowEvent e) {
int state = e.getNewState(); int state = e.getNewState();
if ((state & Frame.ICONIFIED) != 0) { if ((state & Frame.ICONIFIED) == 0) {
setVisible(false); look.reLayout(appWindow.getBounds());
}
else {
setVisible(true);
look.reLayout(container.getBounds());
} }
} }
}); };
container.addComponentListener(parentListener);
appWindow.addWindowStateListener(windowStateListener);
appWindow.addComponentListener(parentListener);
Component glassPane_ = appWindow.getGlassPane();
if (glassPane_ instanceof JPanel) {
glassPane = (JPanel) glassPane_;
String name = glassPane.getName();
if (!name.equals(glassPanePrefix)) {
// We just tweak the already existing glassPane, instead of replacing it with our own
// glassPane = new JPanel();
glassPane.setLayout(null);
glassPane.setName(glassPanePrefix);
// glassPane.setSize(appWindow.getSize());
// glassPane.setOpaque(false);
// appWindow.setGlassPane(glassPane);
}
glassPane.add(notifyCanvas);
if (!glassPane.isVisible()) {
glassPane.setVisible(true);
}
} else {
System.err.println("Not able to add notification to custom glassPane");
}
} }
@Override @Override
@ -131,18 +139,8 @@ class AsDialog extends JDialog implements INotify {
@Override @Override
public public
void setVisible(final boolean visible) { void setVisible(final boolean visible) {
// was it already visible?
if (visible == isVisible()) {
// prevent "double setting" visible state
return;
}
// this is because the order of operations are different based upon visibility. // this is because the order of operations are different based upon visibility.
look.updatePositionsPre(visible); look.updatePositionsPre(visible);
super.setVisible(visible);
// this is because the order of operations are different based upon visibility.
look.updatePositionsPost(visible); look.updatePositionsPost(visible);
} }
@ -154,19 +152,26 @@ class AsDialog extends JDialog implements INotify {
@Override @Override
public public
void run() { void run() {
// set it off screen (which is what the close method also does)
if (isVisible()) {
setVisible(false);
}
look.close(); look.close();
if (parentListener != null) { glassPane.remove(notifyCanvas);
removeComponentListener(parentListener);
appWindow.removeWindowStateListener(windowStateListener);
appWindow.removeComponentListener(parentListener);
boolean found = false;
Component[] components = glassPane.getComponents();
for (Component component : components) {
if (component instanceof NotifyCanvas) {
found = true;
break;
}
}
if (!found) {
// hide the glass pane if there are no more notifications on it.
glassPane.setVisible(false);
} }
setIconImage(null);
removeAll();
dispose();
notification.onClose(); notification.onClose();
} }

View File

@ -23,16 +23,16 @@ import java.awt.Point;
import java.awt.Rectangle; import java.awt.Rectangle;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JFrame; import javax.swing.JWindow;
import dorkbox.util.ScreenUtil; import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil; import dorkbox.util.SwingUtil;
// we can't use regular popup, because if we have no owner, it won't work! // we can't use regular popup, because if we have no owner, it won't work!
// instead, we just create a JFrame and use it to hold our content // instead, we just create a JWindow and use it to hold our content
@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"}) @SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"})
public public
class AsFrame extends JFrame implements INotify { class AsDesktop extends JWindow implements INotify {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final LookAndFeel look; private final LookAndFeel look;
@ -41,13 +41,10 @@ class AsFrame extends JFrame implements INotify {
// this is on the swing EDT // this is on the swing EDT
@SuppressWarnings("NumericCastThatLosesPrecision") @SuppressWarnings("NumericCastThatLosesPrecision")
AsFrame(final Notify notification, final ImageIcon image, final Theme theme) { AsDesktop(final Notify notification, final ImageIcon image, final Theme theme) {
this.notification = notification; this.notification = notification;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
setAlwaysOnTop(true); setAlwaysOnTop(true);
// setLayout(null);
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT); final Dimension preferredSize = new Dimension(WIDTH, HEIGHT);
setPreferredSize(preferredSize); setPreferredSize(preferredSize);
@ -56,9 +53,6 @@ class AsFrame extends JFrame implements INotify {
setSize(NotifyCanvas.WIDTH, NotifyCanvas.HEIGHT); setSize(NotifyCanvas.WIDTH, NotifyCanvas.HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE); setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
setTitle(notification.title);
setResizable(false);
Rectangle bounds; Rectangle bounds;
GraphicsDevice device; GraphicsDevice device;
@ -89,10 +83,10 @@ class AsFrame extends JFrame implements INotify {
.getBounds(); .getBounds();
NotifyCanvas notifyCanvas = new NotifyCanvas(notification, image, theme); NotifyCanvas notifyCanvas = new NotifyCanvas(this, notification, image, theme);
getContentPane().add(notifyCanvas); getContentPane().add(notifyCanvas);
look = new LookAndFeel(this, notifyCanvas, notification, image, bounds); look = new LookAndFeel(this, this, notifyCanvas, notification, bounds, true);
} }
@Override @Override
@ -135,6 +129,11 @@ class AsFrame extends JFrame implements INotify {
} }
} }
// setVisible(false) with any extra logic
void doHide() {
super.setVisible(false);
}
@Override @Override
public public
void close() { void close() {
@ -143,14 +142,9 @@ class AsFrame extends JFrame implements INotify {
@Override @Override
public public
void run() { void run() {
// set it off screen (which is what the close method also does) doHide();
if (isVisible()) {
setVisible(false);
}
look.close(); look.close();
setIconImage(null);
removeAll(); removeAll();
dispose(); dispose();

View File

@ -26,7 +26,7 @@ class ClickAdapter extends MouseAdapter {
@Override @Override
public public
void mouseReleased(final MouseEvent e) { void mouseReleased(final MouseEvent e) {
INotify source = (INotify) e.getSource(); INotify parent = ((NotifyCanvas) e.getSource()).parent;
source.onClick(e.getX(), e.getY()); parent.onClick(e.getX(), e.getY());
} }
} }

View File

@ -15,17 +15,15 @@
*/ */
package dorkbox.notify; package dorkbox.notify;
import java.awt.Point;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.Window; import java.awt.Window;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import javax.swing.ImageIcon;
import dorkbox.tweenengine.BaseTween; import dorkbox.tweenengine.BaseTween;
import dorkbox.tweenengine.Tween; import dorkbox.tweenengine.Tween;
import dorkbox.tweenengine.TweenCallback; import dorkbox.tweenengine.TweenCallback;
@ -33,12 +31,12 @@ import dorkbox.tweenengine.TweenEngine;
import dorkbox.tweenengine.TweenEquations; import dorkbox.tweenengine.TweenEquations;
import dorkbox.util.ActionHandler; import dorkbox.util.ActionHandler;
import dorkbox.util.ActionHandlerLong; import dorkbox.util.ActionHandlerLong;
import dorkbox.util.SwingUtil; import dorkbox.util.ScreenUtil;
import dorkbox.util.swing.SwingActiveRender; import dorkbox.util.swing.SwingActiveRender;
@SuppressWarnings({"FieldCanBeLocal"}) @SuppressWarnings({"FieldCanBeLocal"})
class LookAndFeel { class LookAndFeel {
private static final Map<String, ArrayList<LookAndFeel>> popups = new HashMap<String, ArrayList<LookAndFeel>>(); private static final Map<String, PopupList> popups = new HashMap<String, PopupList>();
static final TweenEngine animation = TweenEngine.create() static final TweenEngine animation = TweenEngine.create()
.unsafe() // access is only from a single thread ever, so unsafe is preferred. .unsafe() // access is only from a single thread ever, so unsafe is preferred.
@ -47,6 +45,7 @@ class LookAndFeel {
static final NotifyAccessor accessor = new NotifyAccessor(); static final NotifyAccessor accessor = new NotifyAccessor();
private static final ActionHandlerLong frameStartHandler; private static final ActionHandlerLong frameStartHandler;
static { static {
// this is for updating the tween engine during active-rendering // this is for updating the tween engine during active-rendering
frameStartHandler = new ActionHandlerLong() { frameStartHandler = new ActionHandlerLong() {
@ -58,8 +57,8 @@ class LookAndFeel {
}; };
} }
static final int SPACER = 10;
private static final int PADDING = 40; static final int MARGIN = 20;
private static final java.awt.event.WindowAdapter windowListener = new WindowAdapter(); private static final java.awt.event.WindowAdapter windowListener = new WindowAdapter();
private static final MouseAdapter mouseListener = new ClickAdapter(); private static final MouseAdapter mouseListener = new ClickAdapter();
@ -67,6 +66,7 @@ class LookAndFeel {
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private static final float MOVE_DURATION = Notify.MOVE_DURATION; private static final float MOVE_DURATION = Notify.MOVE_DURATION;
private final boolean isDesktopNotification;
@ -74,6 +74,7 @@ class LookAndFeel {
private volatile int anchorY; private volatile int anchorY;
private final INotify notify;
private final Window parent; private final Window parent;
private final NotifyCanvas notifyCanvas; private final NotifyCanvas notifyCanvas;
@ -87,69 +88,73 @@ class LookAndFeel {
private volatile Tween tween = null; private volatile Tween tween = null;
private volatile Tween hideTween = null; private volatile Tween hideTween = null;
private final ActionHandler<Notify> onCloseAction; private final ActionHandler<Notify> onGeneralAreaClickAction;
LookAndFeel(final Window parent, LookAndFeel(final INotify notify, final Window parent,
final NotifyCanvas notifyCanvas, final NotifyCanvas notifyCanvas,
final Notify notification, final Notify notification,
final ImageIcon image, final Rectangle parentBounds,
final Rectangle parentBounds) { final boolean isDesktopNotification) {
this.notify = notify;
this.parent = parent; this.parent = parent;
this.notifyCanvas = notifyCanvas; this.notifyCanvas = notifyCanvas;
this.isDesktopNotification = isDesktopNotification;
parent.addWindowListener(windowListener); if (isDesktopNotification) {
parent.addMouseListener(mouseListener); parent.addWindowListener(windowListener);
}
notifyCanvas.addMouseListener(mouseListener);
hideAfterDurationInSeconds = notification.hideAfterDurationInMillis / 1000.0F; hideAfterDurationInSeconds = notification.hideAfterDurationInMillis / 1000.0F;
position = notification.position; position = notification.position;
if (notification.onCloseAction != null) { if (notification.onGeneralAreaClickAction != null) {
onCloseAction = new ActionHandler<Notify>() { onGeneralAreaClickAction = new ActionHandler<Notify>() {
@Override @Override
public public
void handle(final Notify value) { void handle(final Notify value) {
notification.onCloseAction.handle(notification); notification.onGeneralAreaClickAction.handle(notification);
} }
}; };
} }
else { else {
onCloseAction = null; onGeneralAreaClickAction = null;
} }
idAndPosition = parentBounds.x + ":" + parentBounds.y + ":" + parentBounds.width + ":" + parentBounds.height + ":" + position; if (isDesktopNotification) {
Point point = new Point((int) parentBounds.getX(), ((int) parentBounds.getY()));
anchorX = getAnchorX(position, parentBounds); idAndPosition = ScreenUtil.getMonitorNumberAtLocation(point) + ":" + position;
anchorY = getAnchorY(position, parentBounds); } else {
idAndPosition = parent.getName() + ":" + position;
if (image != null) {
parent.setIconImage(image.getImage());
}
else {
parent.setIconImage(SwingUtil.BLANK_ICON);
} }
anchorX = getAnchorX(position, parentBounds, isDesktopNotification);
anchorY = getAnchorY(position, parentBounds, isDesktopNotification);
} }
void onClick(final int x, final int y) { void onClick(final int x, final int y) {
// Check - we were over the 'X' (and thus no notify), or was it in the general area? // Check - we were over the 'X' (and thus no notify), or was it in the general area?
if (notifyCanvas.isCloseButton(x, y)) { // reasonable position for detecting mouse over
// reasonable position for detecting mouse over if (!notifyCanvas.isCloseButton(x, y)) {
((INotify)parent).close(); // only call the general click handler IF we click in the general area!
} if (onGeneralAreaClickAction != null) {
else { onGeneralAreaClickAction.handle(null);
if (onCloseAction != null) {
onCloseAction.handle(null);
} }
((INotify) parent).close();
} }
// we always close the notification popup
notify.close();
} }
// only called from an application
void reLayout(final Rectangle bounds) { void reLayout(final Rectangle bounds) {
// when the parent window moves, we stop all animation and snap the popup into place. This simplifies logic greatly // when the parent window moves, we stop all animation and snap the popup into place. This simplifies logic greatly
anchorX = getAnchorX(position, bounds); anchorX = getAnchorX(position, bounds, isDesktopNotification);
anchorY = getAnchorY(position, bounds); anchorY = getAnchorY(position, bounds, isDesktopNotification);
boolean showFromTop = isShowFromTop(this); boolean showFromTop = isShowFromTop(this);
@ -159,14 +164,29 @@ class LookAndFeel {
} }
int changedY; int changedY;
if (showFromTop) { if (popupIndex == 0) {
changedY = anchorY + (popupIndex * (NotifyCanvas.HEIGHT + 10)); changedY = anchorY;
} }
else { else {
changedY = anchorY - (popupIndex * (NotifyCanvas.HEIGHT + 10)); synchronized (popups) {
String id = idAndPosition;
PopupList looks = popups.get(id);
if (looks != null) {
if (showFromTop) {
changedY = anchorY + (popupIndex * (NotifyCanvas.HEIGHT + SPACER));
}
else {
changedY = anchorY - (popupIndex * (NotifyCanvas.HEIGHT + SPACER));
}
}
else {
changedY = anchorY;
}
}
} }
parent.setLocation(anchorX, changedY); setLocation(anchorX, changedY);
} }
void close() { void close() {
@ -180,8 +200,13 @@ class LookAndFeel {
tween = null; tween = null;
} }
parent.removeWindowListener(windowListener); if (isDesktopNotification) {
parent.removeWindowListener(windowListener);
}
parent.removeMouseListener(mouseListener); parent.removeMouseListener(mouseListener);
updatePositionsPre(false);
updatePositionsPost(false);
} }
void shake(final int durationInMillis, final int amplitude) { void shake(final int durationInMillis, final int amplitude) {
@ -219,22 +244,52 @@ class LookAndFeel {
.start(); .start();
} }
void setParentY(final int y) { void setY(final int y) {
parent.setLocation(parent.getX(), y); if (isDesktopNotification) {
parent.setLocation(parent.getX(), y);
}
else {
notifyCanvas.setLocation(notifyCanvas.getX(), y);
}
} }
int getParentY() { int getY() {
return parent.getY(); if (isDesktopNotification) {
return parent.getY();
}
else {
return notifyCanvas.getY();
}
} }
int getParentX() { int getX() {
return parent.getX(); if (isDesktopNotification) {
return parent.getX();
}
else {
return notifyCanvas.getX();
}
}
void setLocation(final int x, final int y) {
if (isDesktopNotification) {
parent.setLocation(x, y);
}
else {
notifyCanvas.setLocation(x, y);
}
} }
private static private static
int getAnchorX(final Pos position, final Rectangle bounds) { int getAnchorX(final Pos position, final Rectangle bounds, boolean isDesktop) {
// we use the screen that the mouse is currently on. // we use the screen that the mouse is currently on.
final int startX = (int) bounds.getX(); final int startX;
if (isDesktop) {
startX = (int) bounds.getX();
} else {
startX = 0;
}
final int screenWidth = (int) bounds.getWidth(); final int screenWidth = (int) bounds.getWidth();
// determine location for the popup // determine location for the popup
@ -242,14 +297,14 @@ class LookAndFeel {
switch (position) { switch (position) {
case TOP_LEFT: case TOP_LEFT:
case BOTTOM_LEFT: case BOTTOM_LEFT:
return startX + PADDING; return MARGIN + startX;
case CENTER: case CENTER:
return startX + (screenWidth / 2) - NotifyCanvas.WIDTH / 2 - PADDING / 2; return startX + (screenWidth / 2) - NotifyCanvas.WIDTH / 2 - MARGIN / 2;
case TOP_RIGHT: case TOP_RIGHT:
case BOTTOM_RIGHT: case BOTTOM_RIGHT:
return startX + screenWidth - NotifyCanvas.WIDTH - PADDING; return startX + screenWidth - NotifyCanvas.WIDTH - MARGIN;
default: default:
throw new RuntimeException("Unknown position. '" + position + "'"); throw new RuntimeException("Unknown position. '" + position + "'");
@ -257,70 +312,80 @@ class LookAndFeel {
} }
private static private static
int getAnchorY(final Pos position, final Rectangle bounds) { int getAnchorY(final Pos position, final Rectangle bounds, final boolean isDesktop) {
final int startY = (int) bounds.getY(); final int startY;
if (isDesktop) {
startY = (int) bounds.getY();
}
else {
startY = 0;
}
final int screenHeight = (int) bounds.getHeight(); final int screenHeight = (int) bounds.getHeight();
// get anchorY // get anchorY
switch (position) { switch (position) {
case TOP_LEFT: case TOP_LEFT:
case TOP_RIGHT: case TOP_RIGHT:
return PADDING + startY; return startY + MARGIN;
case CENTER: case CENTER:
return startY + (screenHeight / 2) - NotifyCanvas.HEIGHT / 2 - PADDING / 2; return startY + (screenHeight / 2) - NotifyCanvas.HEIGHT / 2 - MARGIN / 2 - SPACER;
case BOTTOM_LEFT: case BOTTOM_LEFT:
case BOTTOM_RIGHT: case BOTTOM_RIGHT:
return startY + screenHeight - NotifyCanvas.HEIGHT - PADDING; if (isDesktop) {
return startY + screenHeight - NotifyCanvas.HEIGHT - MARGIN;
} else {
return startY + screenHeight - NotifyCanvas.HEIGHT - MARGIN - SPACER * 2;
}
default: default:
throw new RuntimeException("Unknown position. '" + position + "'"); throw new RuntimeException("Unknown position. '" + position + "'");
} }
} }
void setParentLocation(final int x, final int y) {
parent.setLocation(x, y);
}
// only called on the swing EDT thread // only called on the swing EDT thread
private static private static
void addPopupToMap(final LookAndFeel sourceLook) { void addPopupToMap(final LookAndFeel sourceLook) {
synchronized (popups) { synchronized (popups) {
String id = sourceLook.idAndPosition; String id = sourceLook.idAndPosition;
ArrayList<LookAndFeel> looks = popups.get(id); PopupList looks = popups.get(id);
if (looks == null) { if (looks == null) {
looks = new ArrayList<LookAndFeel>(4); looks = new PopupList();
popups.put(id, looks); popups.put(id, looks);
} }
final int popupIndex = looks.size(); final int index = looks.size();
sourceLook.popupIndex = popupIndex; sourceLook.popupIndex = index;
// the popups are ALL the same size! // the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up // popups at TOP grow down, popups at BOTTOM grow up
int targetY; int targetY;
int prevTargetY; // this is to determine if there is an offset as a result of a toolbar/etc
int anchorX = sourceLook.anchorX; int anchorX = sourceLook.anchorX;
int anchorY = sourceLook.anchorY; int anchorY = sourceLook.anchorY;
if (index == 0) {
if (popupIndex == 0) {
targetY = anchorY; targetY = anchorY;
} else { } else {
int previousY = looks.get(popupIndex - 1).getParentY(); boolean showFromTop = isShowFromTop(sourceLook);
if (isShowFromTop(sourceLook)) { if (sourceLook.isDesktopNotification && index == 1) {
targetY = previousY + (NotifyCanvas.HEIGHT + 10); // have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
// this is only done when the 2nd popup is added to the list
looks.calculateOffset(showFromTop, anchorX, anchorY);
}
if (showFromTop) {
targetY = anchorY + (index * (NotifyCanvas.HEIGHT + SPACER)) + looks.getOffsetY();
} }
else { else {
targetY = previousY - (NotifyCanvas.HEIGHT + 10); targetY = anchorY - (index * (NotifyCanvas.HEIGHT + SPACER)) + looks.getOffsetY();
} }
} }
looks.add(sourceLook); looks.add(sourceLook);
sourceLook.setParentLocation(anchorX, targetY); sourceLook.setLocation(anchorX, targetY);
if (sourceLook.hideAfterDurationInSeconds > 0 && sourceLook.hideTween == null) { if (sourceLook.hideAfterDurationInSeconds > 0 && sourceLook.hideTween == null) {
// begin a timeline to get rid of the popup (default is 5 seconds) // begin a timeline to get rid of the popup (default is 5 seconds)
@ -332,7 +397,7 @@ class LookAndFeel {
public public
void onEvent(final int type, final BaseTween<?> source) { void onEvent(final int type, final BaseTween<?> source) {
if (type == Events.COMPLETE) { if (type == Events.COMPLETE) {
((INotify) sourceLook.parent).close(); sourceLook.notify.close();
} }
} }
}) })
@ -349,7 +414,7 @@ class LookAndFeel {
synchronized (popups) { synchronized (popups) {
popupsAreEmpty = popups.isEmpty(); popupsAreEmpty = popups.isEmpty();
final ArrayList<LookAndFeel> allLooks = popups.get(sourceLook.idAndPosition); final PopupList allLooks = popups.get(sourceLook.idAndPosition);
// there are two loops because it is necessary to cancel + remove all tweens BEFORE adding new ones. // there are two loops because it is necessary to cancel + remove all tweens BEFORE adding new ones.
boolean adjustPopupPosition = false; boolean adjustPopupPosition = false;
@ -376,15 +441,20 @@ class LookAndFeel {
} }
} }
for (final LookAndFeel look : allLooks) { // have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
int offsetY = allLooks.getOffsetY();
for (int index = 0; index < allLooks.size(); index++) {
final LookAndFeel look = allLooks.get(index);
// the popups are ALL the same size! // the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up // popups at TOP grow down, popups at BOTTOM grow up
int changedY; int changedY;
if (showFromTop) { if (showFromTop) {
changedY = look.anchorY + (look.popupIndex * (NotifyCanvas.HEIGHT + 10)); changedY = look.anchorY + (look.popupIndex * (NotifyCanvas.HEIGHT + SPACER) + offsetY);
} }
else { else {
changedY = look.anchorY - (look.popupIndex * (NotifyCanvas.HEIGHT + 10)); changedY = look.anchorY - (look.popupIndex * (NotifyCanvas.HEIGHT + SPACER) + offsetY);
} }
// now animate that popup to it's new location // now animate that popup to it's new location
@ -392,15 +462,15 @@ class LookAndFeel {
.target((float) changedY) .target((float) changedY)
.ease(TweenEquations.Linear) .ease(TweenEquations.Linear)
.addCallback(new TweenCallback() { .addCallback(new TweenCallback() {
@Override @Override
public public
void onEvent(final int type, final BaseTween<?> source) { void onEvent(final int type, final BaseTween<?> source) {
if (type == Events.COMPLETE) { if (type == Events.COMPLETE) {
// make sure to remove the tween once it's done, otherwise .kill can do weird things. // make sure to remove the tween once it's done, otherwise .kill can do weird things.
look.tween = null; look.tween = null;
} }
} }
}) })
.start(); .start();
} }
} }
@ -448,7 +518,6 @@ class LookAndFeel {
*/ */
void updatePositionsPost(final boolean visible) { void updatePositionsPost(final boolean visible) {
if (visible) { if (visible) {
SwingActiveRender.addActiveRender(notifyCanvas); SwingActiveRender.addActiveRender(notifyCanvas);
// start if we have previously stopped the timer // start if we have previously stopped the timer

View File

@ -16,7 +16,6 @@
package dorkbox.notify; package dorkbox.notify;
import java.awt.Image; import java.awt.Image;
import java.awt.Window;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -27,6 +26,7 @@ import java.util.Map;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JFrame;
import dorkbox.util.ActionHandler; import dorkbox.util.ActionHandler;
import dorkbox.util.ImageUtil; import dorkbox.util.ImageUtil;
@ -93,7 +93,7 @@ class Notify {
*/ */
public static public static
String getVersion() { String getVersion() {
return "3.4"; return "3.5";
} }
/** /**
@ -197,13 +197,13 @@ class Notify {
int screenNumber = Short.MIN_VALUE; int screenNumber = Short.MIN_VALUE;
private ImageIcon icon; private ImageIcon icon;
ActionHandler<Notify> onCloseAction; ActionHandler<Notify> onGeneralAreaClickAction;
private INotify notifyPopup; private INotify notifyPopup;
private String name; private String name;
private int shakeDurationInMillis = 0; private int shakeDurationInMillis = 0;
private int shakeAmplitude = 0; private int shakeAmplitude = 0;
private Window window; private JFrame appWindow;
private private
Notify() { Notify() {
@ -277,7 +277,7 @@ class Notify {
*/ */
public public
Notify onAction(ActionHandler<Notify> onAction) { Notify onAction(ActionHandler<Notify> onAction) {
this.onCloseAction = onAction; this.onGeneralAreaClickAction = onAction;
return this; return this;
} }
@ -371,10 +371,10 @@ class Notify {
theme = new Theme(Notify.TITLE_TEXT_FONT, Notify.MAIN_TEXT_FONT, notify.isDark); theme = new Theme(Notify.TITLE_TEXT_FONT, Notify.MAIN_TEXT_FONT, notify.isDark);
} }
if (window == null) { if (appWindow == null) {
notifyPopup = new AsFrame(notify, image, theme); notifyPopup = new AsDesktop(notify, image, theme);
} else { } else {
notifyPopup = new AsDialog(notify, image, window, theme); notifyPopup = new AsApplication(notify, image, appWindow, theme);
} }
notifyPopup.setVisible(true); notifyPopup.setVisible(true);
@ -445,11 +445,11 @@ class Notify {
} }
/** /**
* Attaches this notification to a specific JFrame/Window, instead of having a global notification * Attaches this notification to a specific JFrame, instead of having a global notification
*/ */
public public
Notify attach(final Window frame) { Notify attach(final JFrame frame) {
this.window = frame; this.appWindow = frame;
return this; return this;
} }

View File

@ -32,11 +32,11 @@ class NotifyAccessor implements TweenAccessor<LookAndFeel> {
int getValues(final LookAndFeel target, final int tweenType, final float[] returnValues) { int getValues(final LookAndFeel target, final int tweenType, final float[] returnValues) {
switch (tweenType) { switch (tweenType) {
case Y_POS: case Y_POS:
returnValues[0] = (float) target.getParentY(); returnValues[0] = (float) target.getY();
return 1; return 1;
case X_Y_POS: case X_Y_POS:
returnValues[0] = (float) target.getParentX(); returnValues[0] = (float) target.getX();
returnValues[1] = (float) target.getParentY(); returnValues[1] = (float) target.getY();
return 2; return 2;
case PROGRESS: case PROGRESS:
returnValues[0] = (float) target.getProgress(); returnValues[0] = (float) target.getProgress();
@ -51,10 +51,10 @@ class NotifyAccessor implements TweenAccessor<LookAndFeel> {
void setValues(final LookAndFeel target, final int tweenType, final float[] newValues) { void setValues(final LookAndFeel target, final int tweenType, final float[] newValues) {
switch (tweenType) { switch (tweenType) {
case Y_POS: case Y_POS:
target.setParentY((int) newValues[0]); target.setY((int) newValues[0]);
return; return;
case X_Y_POS: case X_Y_POS:
target.setParentLocation((int) newValues[0], (int) newValues[1]); target.setLocation((int) newValues[0], (int) newValues[1]);
return; return;
case PROGRESS: case PROGRESS:
target.setProgress((int) newValues[0]); target.setProgress((int) newValues[0]);

View File

@ -1,3 +1,18 @@
/*
* Copyright 2015 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.notify; package dorkbox.notify;
import java.awt.BasicStroke; import java.awt.BasicStroke;
@ -30,15 +45,22 @@ class NotifyCanvas extends Canvas {
private static final int PROGRESS_HEIGHT = HEIGHT - 2; private static final int PROGRESS_HEIGHT = HEIGHT - 2;
private final boolean showCloseButton; private final boolean showCloseButton;
private final BufferedImage cachedImage; private BufferedImage cachedImage;
private final Notify notification;
private final ImageIcon imageIcon;
// for the progress bar. we directly draw this onscreen // for the progress bar. we directly draw this onscreen
// non-volatile because it's always accessed in the active render thread // non-volatile because it's always accessed in the active render thread
private int progress = 0; private int progress = 0;
private final Theme theme; private final Theme theme;
final INotify parent;
NotifyCanvas(final Notify notification, final ImageIcon imageIcon, final Theme theme) { NotifyCanvas(final INotify parent, final Notify notification, final ImageIcon imageIcon, final Theme theme) {
this.parent = parent;
this.notification = notification;
this.imageIcon = imageIcon;
this.theme = theme; this.theme = theme;
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT); final Dimension preferredSize = new Dimension(WIDTH, HEIGHT);
@ -53,7 +75,7 @@ class NotifyCanvas extends Canvas {
showCloseButton = !notification.hideCloseButton; showCloseButton = !notification.hideCloseButton;
// now we setup the rendering of the image // now we setup the rendering of the image
cachedImage = renderBackgroundInfo(notification.title, notification.text, this.theme, imageIcon); cachedImage = renderBackgroundInfo(notification.title, notification.text, this.theme, this.imageIcon);
} }
void setProgress(final int progress) { void setProgress(final int progress) {
@ -64,12 +86,38 @@ class NotifyCanvas extends Canvas {
return progress; return progress;
} }
@Override
public public
void paint(final Graphics g) { void paint(final Graphics g) {
// we cache the text + image (to another image), and then always render the close + progressbar // we cache the text + image (to another image), and then always render the close + progressbar
// use our cached image, so we don't have to re-render text/background/etc // use our cached image, so we don't have to re-render text/background/etc
g.drawImage(cachedImage, 0, 0, null); try {
g.drawImage(cachedImage, 0, 0, null);
} catch (Exception ignored) {
// have also seen (happened after screen/PC was "woken up", in Xubuntu 16.04):
// java.lang.ClassCastException:sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:148)
// at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:356)
// at sun.java2d.SurfaceDataProxy.updateSurfaceData(SurfaceDataProxy.java:498)
// at sun.java2d.SurfaceDataProxy.replaceData(SurfaceDataProxy.java:455)
// at sun.java2d.SurfaceData.getSourceSurfaceData(SurfaceData.java:233)
// at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:566)
// at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
// at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014)
// at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186)
// at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318)
// at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296)
// at dorkbox.notify.NotifyCanvas.paint(NotifyCanvas.java:92)
// redo the image
cachedImage = renderBackgroundInfo(notification.title, notification.text, this.theme, imageIcon);
// try to draw again
try {
g.drawImage(cachedImage, 0, 0, null);
} catch (Exception ignored2) {
}
}
// the progress bar and close button are the only things that can change, so we always draw them every time // the progress bar and close button are the only things that can change, so we always draw them every time
Graphics2D g2 = (Graphics2D) g.create(); Graphics2D g2 = (Graphics2D) g.create();

View File

@ -0,0 +1,80 @@
/*
* Copyright 2017 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.notify;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.Iterator;
import dorkbox.util.ScreenUtil;
/**
* Contains a list of notification popups + the Y offset (if any)
*/
class PopupList {
private int offsetY = 0;
private ArrayList<LookAndFeel> popups = new ArrayList<LookAndFeel>(4);
/**
* have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
*
* this is only done on the 2nd popup is added to the list
*/
void calculateOffset(final boolean showFromTop, final int anchorX, final int anchorY) {
if (offsetY == 0) {
Point point = new Point(anchorX, anchorY);
GraphicsConfiguration gc = ScreenUtil.getMonitorAtLocation(point)
.getDefaultConfiguration();
Insets screenInsets = Toolkit.getDefaultToolkit()
.getScreenInsets(gc);
if (showFromTop) {
if (screenInsets.top > 0) {
offsetY = screenInsets.top - LookAndFeel.MARGIN;
}
} else {
if (screenInsets.bottom > 0) {
offsetY = screenInsets.bottom + LookAndFeel.MARGIN;
}
}
}
}
int getOffsetY() {
return offsetY;
}
int size() {
return popups.size();
}
void add(final LookAndFeel lookAndFeel) {
popups.add(lookAndFeel);
}
Iterator<LookAndFeel> iterator() {
return popups.iterator();
}
LookAndFeel get(final int index) {
return popups.get(index);
}
}

View File

@ -1,3 +1,18 @@
/*
* Copyright 2017 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.notify; package dorkbox.notify;
import java.awt.Color; import java.awt.Color;
@ -6,18 +21,18 @@ import java.awt.Font;
import dorkbox.util.FontUtil; import dorkbox.util.FontUtil;
/** /**
* * Settings available to change the theme
*/ */
public public
class Theme { class Theme {
public final Color panel_BG; final Color panel_BG;
public final Color titleText_FG; final Color titleText_FG;
public final Color mainText_FG; final Color mainText_FG;
public final Color closeX_FG; final Color closeX_FG;
public final Color progress_FG; final Color progress_FG;
public final Font titleTextFont; final Font titleTextFont;
public final Font mainTextFont; final Font mainTextFont;
Theme(final String titleTextFont, final String mainTextFont, boolean isDarkTheme) { Theme(final String titleTextFont, final String mainTextFont, boolean isDarkTheme) {
@ -42,7 +57,8 @@ class Theme {
public public
Theme(final String titleTextFont, final String mainTextFont, Theme(final String titleTextFont, final String mainTextFont,
final Color panel_BG, final Color titleText_FG, final Color mainText_FG, final Color closeX_FG, final Color progress_FG) { final Color panel_BG, final Color titleText_FG, final Color mainText_FG,
final Color closeX_FG, final Color progress_FG) {
this.titleTextFont = FontUtil.parseFont(titleTextFont); this.titleTextFont = FontUtil.parseFont(titleTextFont);
this.mainTextFont = FontUtil.parseFont(mainTextFont); this.mainTextFont = FontUtil.parseFont(mainTextFont);

View File

@ -22,7 +22,7 @@ class WindowAdapter extends java.awt.event.WindowAdapter {
public public
void windowClosing(WindowEvent e) { void windowClosing(WindowEvent e) {
if (e.getNewState() != WindowEvent.WINDOW_CLOSED) { if (e.getNewState() != WindowEvent.WINDOW_CLOSED) {
AsFrame source = (AsFrame) e.getSource(); AsDesktop source = (AsDesktop) e.getSource();
source.close(); source.close();
} }
} }
@ -31,7 +31,7 @@ class WindowAdapter extends java.awt.event.WindowAdapter {
public public
void windowLostFocus(WindowEvent e) { void windowLostFocus(WindowEvent e) {
if (e.getNewState() != WindowEvent.WINDOW_CLOSED) { if (e.getNewState() != WindowEvent.WINDOW_CLOSED) {
AsFrame source = (AsFrame) e.getSource(); AsDesktop source = (AsDesktop) e.getSource();
// these don't work // these don't work
//toFront(); //toFront();
//requestFocus(); //requestFocus();

View File

@ -47,7 +47,7 @@ class NotifyTest {
panel.add(button); panel.add(button);
frame.add(panel); frame.add(panel);
frame.setSize(600, 600); frame.setSize(900, 600);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true); frame.setVisible(true);
@ -69,7 +69,7 @@ class NotifyTest {
// .setScreen(0) // .setScreen(0)
.darkStyle() .darkStyle()
// .shake(1300, 4) // .shake(1300, 4)
// .shake(1300, 10) .shake(1300, 10)
.attach(frame) .attach(frame)
.hideCloseButton() .hideCloseButton()
.onAction(new ActionHandler<Notify>() { .onAction(new ActionHandler<Notify>() {
@ -81,6 +81,37 @@ class NotifyTest {
}); });
notify.showWarning(); notify.showWarning();
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
for (int i = 0; i < count; i++) {
final int finalI = i;
notify = Notify.create()
.title("Notify title " + i)
.text("This is a notification " + i + " popup message This is a notification popup message This is a " +
"notification popup message")
.hideAfter(13000)
.position(Pos.TOP_LEFT)
// .position(Pos.CENTER)
// .setScreen(0)
// .darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
.attach(frame)
// .hideCloseButton()
.onAction(new ActionHandler<Notify>() {
@Override
public
void handle(final Notify arg0) {
System.err.println("Notification " + finalI + " clicked on!");
}
});
notify.showWarning();
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -88,6 +119,9 @@ class NotifyTest {
} }
} }
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
final int finalI = i; final int finalI = i;
notify = Notify.create() notify = Notify.create()
@ -99,7 +133,7 @@ class NotifyTest {
// .setScreen(0) // .setScreen(0)
.darkStyle() .darkStyle()
// .shake(1300, 4) // .shake(1300, 4)
// .shake(1300, 10) .shake(1300, 10)
.hideCloseButton() .hideCloseButton()
.onAction(new ActionHandler<Notify>() { .onAction(new ActionHandler<Notify>() {
@Override @Override
@ -123,13 +157,13 @@ class NotifyTest {
.title("Notify title " + i) .title("Notify title " + i)
.text("This is a notification " + i + " popup message This is a notification popup message This is a " + .text("This is a notification " + i + " popup message This is a notification popup message This is a " +
"notification popup message") "notification popup message")
.hideAfter(13000) // .hideAfter(13000)
.position(Pos.BOTTOM_LEFT) .position(Pos.BOTTOM_LEFT)
// .setScreen(0) // .setScreen(0)
// .darkStyle() // .darkStyle()
// .shake(1300, 4) // .shake(1300, 4)
// .shake(1300, 10) // .shake(1300, 10)
.hideCloseButton() // .hideCloseButton()
.onAction(new ActionHandler<Notify>() { .onAction(new ActionHandler<Notify>() {
@Override @Override
public public