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
---------
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.
@ -75,7 +76,7 @@ Maven Info
<dependency>
<groupId>com.dorkbox</groupId>
<artifactId>Notify</artifactId>
<version>3.4</version>
<version>3.5</version>
</dependency>
</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");
* you may not use this file except in compliance with the License.
@ -15,99 +15,107 @@
*/
package dorkbox.notify;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import dorkbox.util.SwingUtil;
// this is a child to a Jframe/window (instead of globally to the screen)
@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"})
public
class AsDialog extends JDialog implements INotify {
private static final long serialVersionUID = 1L;
class AsApplication implements INotify {
private final LookAndFeel look;
private final Notify notification;
private final NotifyCanvas notifyCanvas;
private final JFrame appWindow;
private final ComponentListener parentListener;
private final WindowStateListener windowStateListener;
private static final String glassPanePrefix = "dorkbox.notify";
private JPanel glassPane;
// this is on the swing EDT
@SuppressWarnings("NumericCastThatLosesPrecision")
AsDialog(final Notify notification, final ImageIcon image, final Window container, final Theme theme) {
super(container, Dialog.ModalityType.MODELESS);
AsApplication(final Notify notification, final ImageIcon image, final JFrame appWindow, final Theme theme) {
this.notification = notification;
this.notifyCanvas = new NotifyCanvas(this, notification, image, theme);
this.appWindow = appWindow;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
look = new LookAndFeel(this, appWindow, notifyCanvas, notification, appWindow.getBounds(), false);
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT);
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)
// this makes sure that our notify canvas stay anchored to the parent window (if it's hidden/shown/moved/etc)
parentListener = new ComponentListener() {
@Override
public
void componentShown(final ComponentEvent e) {
AsDialog.this.setVisible(true);
look.reLayout(container.getBounds());
look.reLayout(appWindow.getBounds());
}
@Override
public
void componentHidden(final ComponentEvent e) {
AsDialog.this.setVisible(false);
}
@Override
public
void componentResized(final ComponentEvent e) {
look.reLayout(container.getBounds());
look.reLayout(appWindow.getBounds());
}
@Override
public
void componentMoved(final ComponentEvent e) {
look.reLayout(container.getBounds());
}
};
container.addWindowStateListener(new WindowStateListener() {
windowStateListener = new WindowStateListener() {
@Override
public
void windowStateChanged(WindowEvent e) {
int state = e.getNewState();
if ((state & Frame.ICONIFIED) != 0) {
setVisible(false);
}
else {
setVisible(true);
look.reLayout(container.getBounds());
if ((state & Frame.ICONIFIED) == 0) {
look.reLayout(appWindow.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
@ -131,18 +139,8 @@ class AsDialog extends JDialog implements INotify {
@Override
public
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.
look.updatePositionsPre(visible);
super.setVisible(visible);
// this is because the order of operations are different based upon visibility.
look.updatePositionsPost(visible);
}
@ -154,19 +152,26 @@ class AsDialog extends JDialog implements INotify {
@Override
public
void run() {
// set it off screen (which is what the close method also does)
if (isVisible()) {
setVisible(false);
}
look.close();
if (parentListener != null) {
removeComponentListener(parentListener);
glassPane.remove(notifyCanvas);
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();
}

View File

@ -23,16 +23,16 @@ import java.awt.Point;
import java.awt.Rectangle;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JWindow;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
// 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"})
public
class AsFrame extends JFrame implements INotify {
class AsDesktop extends JWindow implements INotify {
private static final long serialVersionUID = 1L;
private final LookAndFeel look;
@ -41,13 +41,10 @@ class AsFrame extends JFrame implements INotify {
// this is on the swing EDT
@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;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
setAlwaysOnTop(true);
// setLayout(null);
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT);
setPreferredSize(preferredSize);
@ -56,9 +53,6 @@ class AsFrame extends JFrame implements INotify {
setSize(NotifyCanvas.WIDTH, NotifyCanvas.HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
setTitle(notification.title);
setResizable(false);
Rectangle bounds;
GraphicsDevice device;
@ -89,10 +83,10 @@ class AsFrame extends JFrame implements INotify {
.getBounds();
NotifyCanvas notifyCanvas = new NotifyCanvas(notification, image, theme);
NotifyCanvas notifyCanvas = new NotifyCanvas(this, notification, image, theme);
getContentPane().add(notifyCanvas);
look = new LookAndFeel(this, notifyCanvas, notification, image, bounds);
look = new LookAndFeel(this, this, notifyCanvas, notification, bounds, true);
}
@Override
@ -135,6 +129,11 @@ class AsFrame extends JFrame implements INotify {
}
}
// setVisible(false) with any extra logic
void doHide() {
super.setVisible(false);
}
@Override
public
void close() {
@ -143,14 +142,9 @@ class AsFrame extends JFrame implements INotify {
@Override
public
void run() {
// set it off screen (which is what the close method also does)
if (isVisible()) {
setVisible(false);
}
doHide();
look.close();
setIconImage(null);
removeAll();
dispose();

View File

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

View File

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

View File

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

View File

@ -32,11 +32,11 @@ class NotifyAccessor implements TweenAccessor<LookAndFeel> {
int getValues(final LookAndFeel target, final int tweenType, final float[] returnValues) {
switch (tweenType) {
case Y_POS:
returnValues[0] = (float) target.getParentY();
returnValues[0] = (float) target.getY();
return 1;
case X_Y_POS:
returnValues[0] = (float) target.getParentX();
returnValues[1] = (float) target.getParentY();
returnValues[0] = (float) target.getX();
returnValues[1] = (float) target.getY();
return 2;
case PROGRESS:
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) {
switch (tweenType) {
case Y_POS:
target.setParentY((int) newValues[0]);
target.setY((int) newValues[0]);
return;
case X_Y_POS:
target.setParentLocation((int) newValues[0], (int) newValues[1]);
target.setLocation((int) newValues[0], (int) newValues[1]);
return;
case PROGRESS:
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;
import java.awt.BasicStroke;
@ -30,15 +45,22 @@ class NotifyCanvas extends Canvas {
private static final int PROGRESS_HEIGHT = HEIGHT - 2;
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
// non-volatile because it's always accessed in the active render thread
private int progress = 0;
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;
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT);
@ -53,7 +75,7 @@ class NotifyCanvas extends Canvas {
showCloseButton = !notification.hideCloseButton;
// 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) {
@ -64,12 +86,38 @@ class NotifyCanvas extends Canvas {
return progress;
}
@Override
public
void paint(final Graphics g) {
// 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
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
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;
import java.awt.Color;
@ -6,18 +21,18 @@ import java.awt.Font;
import dorkbox.util.FontUtil;
/**
*
* Settings available to change the theme
*/
public
class Theme {
public final Color panel_BG;
public final Color titleText_FG;
public final Color mainText_FG;
public final Color closeX_FG;
public final Color progress_FG;
final Color panel_BG;
final Color titleText_FG;
final Color mainText_FG;
final Color closeX_FG;
final Color progress_FG;
public final Font titleTextFont;
public final Font mainTextFont;
final Font titleTextFont;
final Font mainTextFont;
Theme(final String titleTextFont, final String mainTextFont, boolean isDarkTheme) {
@ -42,7 +57,8 @@ class Theme {
public
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.mainTextFont = FontUtil.parseFont(mainTextFont);

View File

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

View File

@ -47,7 +47,7 @@ class NotifyTest {
panel.add(button);
frame.add(panel);
frame.setSize(600, 600);
frame.setSize(900, 600);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
@ -69,7 +69,7 @@ class NotifyTest {
// .setScreen(0)
.darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
.shake(1300, 10)
.attach(frame)
.hideCloseButton()
.onAction(new ActionHandler<Notify>() {
@ -81,6 +81,37 @@ class NotifyTest {
});
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 {
Thread.sleep(1000);
} catch (InterruptedException e) {
@ -88,6 +119,9 @@ class NotifyTest {
}
}
for (int i = 0; i < count; i++) {
final int finalI = i;
notify = Notify.create()
@ -99,7 +133,7 @@ class NotifyTest {
// .setScreen(0)
.darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
.shake(1300, 10)
.hideCloseButton()
.onAction(new ActionHandler<Notify>() {
@Override
@ -123,13 +157,13 @@ class NotifyTest {
.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)
// .hideAfter(13000)
.position(Pos.BOTTOM_LEFT)
// .setScreen(0)
// .darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
.hideCloseButton()
// .hideCloseButton()
.onAction(new ActionHandler<Notify>() {
@Override
public