Added support for notifications on a JFrame/Window. Refactored

LookAndFeel of the notification icon. Fixed various bugs surrounding
animation and rendering of notifications.
This commit is contained in:
nathan 2017-07-28 22:07:01 +02:00
parent 04cfd7d91b
commit f8866c9aeb
9 changed files with 852 additions and 421 deletions

View File

@ -0,0 +1,170 @@
/*
* 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.Dialog;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
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 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;
private final LookAndFeel look;
private final Notify notification;
private final ComponentListener parentListener;
// this is on the swing EDT
@SuppressWarnings("NumericCastThatLosesPrecision")
AsDialog(final Notify notification, final Image image, final ImageIcon imageIcon, final Window container) {
super(container, Dialog.ModalityType.MODELESS);
this.notification = notification;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
setLayout(null);
setSize(LookAndFeel.WIDTH, LookAndFeel.HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
setTitle(notification.title);
setResizable(false);
look = new LookAndFeel(this, notification, image, imageIcon, container.getBounds());
parentListener = new ComponentListener() {
@Override
public
void componentShown(final ComponentEvent e) {
AsDialog.this.setVisible(true);
look.reLayout(container.getBounds());
}
@Override
public
void componentHidden(final ComponentEvent e) {
AsDialog.this.setVisible(false);
}
@Override
public
void componentResized(final ComponentEvent e) {
look.reLayout(container.getBounds());
}
@Override
public
void componentMoved(final ComponentEvent e) {
look.reLayout(container.getBounds());
}
};
container.addWindowStateListener(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());
}
}
});
container.addComponentListener(parentListener);
}
@Override
public
void paint(Graphics g) {
look.paint(g);
}
@Override
public
void onClick(final int x, final int y) {
look.onClick(x, y);
}
/**
* Shakes the popup
*
* @param durationInMillis now long it will shake
* @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot.
*/
@Override
public
void shake(final int durationInMillis, final int amplitude) {
look.shake(durationInMillis, amplitude);
}
@Override
public
void setVisible(final boolean b) {
// was it already visible?
if (b == isVisible()) {
// prevent "double setting" visible state
return;
}
super.setVisible(b);
look.setVisible(b);
}
@Override
public
void close() {
// this must happen in the Swing EDT. This is usually called by the active renderer
SwingUtil.invokeLater(new Runnable() {
@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);
}
setIconImage(null);
removeAll();
dispose();
notification.onClose();
}
});
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
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
@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"})
public
class AsFrame extends JFrame implements INotify {
private static final long serialVersionUID = 1L;
private final LookAndFeel look;
private final Notify notification;
// this is on the swing EDT
@SuppressWarnings("NumericCastThatLosesPrecision")
AsFrame(final Notify notification, final Image image, final ImageIcon imageIcon) {
this.notification = notification;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
setAlwaysOnTop(true);
setLayout(null);
setSize(LookAndFeel.WIDTH, LookAndFeel.HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
setTitle(notification.title);
setResizable(false);
Rectangle bounds;
GraphicsDevice device;
if (notification.screenNumber == Short.MIN_VALUE) {
// set screen position based on mouse
Point mouseLocation = MouseInfo.getPointerInfo()
.getLocation();
device = ScreenUtil.getGraphicsDeviceAt(mouseLocation);
}
else {
// set screen position based on specified screen
int screenNumber = notification.screenNumber;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice screenDevices[] = ge.getScreenDevices();
if (screenNumber < 0) {
screenNumber = 0;
}
else if (screenNumber > screenDevices.length - 1) {
screenNumber = screenDevices.length - 1;
}
device = screenDevices[screenNumber];
}
bounds = device.getDefaultConfiguration()
.getBounds();
look = new LookAndFeel(this, notification, image, imageIcon, bounds);
}
@Override
public
void paint(Graphics g) {
look.paint(g);
}
@Override
public
void onClick(final int x, final int y) {
look.onClick(x, y);
}
/**
* Shakes the popup
*
* @param durationInMillis now long it will shake
* @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot.
*/
@Override
public
void shake(final int durationInMillis, final int amplitude) {
look.shake(durationInMillis, amplitude);
}
@Override
public
void setVisible(final boolean b) {
// was it already visible?
if (b == isVisible()) {
// prevent "double setting" visible state
return;
}
super.setVisible(b);
look.setVisible(b);
}
@Override
public
void close() {
// this must happen in the Swing EDT. This is usually called by the active renderer
SwingUtil.invokeLater(new Runnable() {
@Override
public
void run() {
// set it off screen (which is what the close method also does)
if (isVisible()) {
setVisible(false);
}
look.close();
setIconImage(null);
removeAll();
dispose();
notification.onClose();
}
});
}
}

View File

@ -18,16 +18,15 @@ package dorkbox.notify;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
class NotifyPopupClickAdapter extends MouseAdapter { class ClickAdapter extends MouseAdapter {
public ClickAdapter() {
NotifyPopupClickAdapter() {
} }
@Override @Override
public public
void mouseReleased(final MouseEvent e) { void mouseReleased(final MouseEvent e) {
NotifyPopup source = (NotifyPopup) e.getSource(); INotify source = (INotify) e.getSource();
source.onClick(e.getX(), e.getY()); source.onClick(e.getX(), e.getY());
} }
} }

View File

@ -0,0 +1,27 @@
/*
* 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;
public
interface INotify {
void close();
void shake(int durationInMillis, int amplitude);
void setVisible(boolean b);
void onClick(int x, int y);
}

View File

@ -19,16 +19,13 @@ import java.awt.BasicStroke;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image; import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point; import java.awt.Point;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Stroke; import java.awt.Stroke;
import java.awt.Window;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.WindowAdapter;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -37,7 +34,6 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel; import javax.swing.JLabel;
import dorkbox.tweenengine.BaseTween; import dorkbox.tweenengine.BaseTween;
@ -48,52 +44,21 @@ import dorkbox.tweenengine.TweenManager;
import dorkbox.util.ActionHandler; import dorkbox.util.ActionHandler;
import dorkbox.util.ActionHandlerLong; import dorkbox.util.ActionHandlerLong;
import dorkbox.util.FontUtil; import dorkbox.util.FontUtil;
import dorkbox.util.Property;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil; import dorkbox.util.SwingUtil;
import dorkbox.util.swing.SwingActiveRender; import dorkbox.util.swing.SwingActiveRender;
// we can't use regular popup, because if we have no owner, it won't work! @SuppressWarnings({"FieldCanBeLocal"})
// instead, we just create a JFrame and use it to hold our content class LookAndFeel {
@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"}) private static final Map<String, ArrayList<LookAndFeel>> popups = new HashMap<String, ArrayList<LookAndFeel>>();
public
class NotifyPopup extends JFrame {
private static final long serialVersionUID = 1L;
@Property static final NotifyAccessor accessor = new NotifyAccessor();
/** This is the title font used by a notification. */ static final TweenManager tweenManager = new TweenManager();
public static String TITLE_TEXT_FONT = "Source Code Pro BOLD 16";
@Property
/** This is the main text font used by a notification. */
public static String MAIN_TEXT_FONT = "Source Code Pro BOLD 12";
@Property
/** How long we want it to take for the popups to relocate when one is closed */
public static float MOVE_DURATION = 1.0F;
private static final int padding = 40;
private static final Map<String, ArrayList<NotifyPopup>> popups = new HashMap<String, ArrayList<NotifyPopup>>();
private static final NotifyPopupAccessor accessor = new NotifyPopupAccessor();
private static final TweenManager tweenManager = new TweenManager();
private static final ActionHandlerLong frameStartHandler; private static final ActionHandlerLong frameStartHandler;
static {
// this is for updating the tween engine during active-rendering
frameStartHandler = new ActionHandlerLong() {
@Override
public
void handle(final long deltaInNanos) {
NotifyPopup.tweenManager.update(deltaInNanos);
}
};
}
private static final int WIDTH = 300;
private static final int HEIGHT = 87; private static final java.awt.event.WindowAdapter windowListener = new WindowAdapter();
private static final int PROGRESS_HEIGHT = HEIGHT - 1; private static final MouseAdapter mouseListener = new ClickAdapter();
private static final Stroke stroke = new BasicStroke(2); private static final Stroke stroke = new BasicStroke(2);
private static final int closeX = 282; private static final int closeX = 282;
@ -104,28 +69,46 @@ class NotifyPopup extends JFrame {
private static final int Y_2 = closeY + 11; private static final int Y_2 = closeY + 11;
private static final int X_2 = closeX + 11; private static final int X_2 = closeX + 11;
static final int WIDTH = 300;
static final int HEIGHT = 87;
private static final int PROGRESS_HEIGHT = HEIGHT - 2;
private static final int PADDING = 40;
private static final Random RANDOM = new Random();
private static final float MOVE_DURATION = Notify.MOVE_DURATION;
static {
// this is for updating the tween engine during active-rendering
frameStartHandler = new ActionHandlerLong() {
@Override
public
void handle(final long deltaInNanos) {
LookAndFeel.tweenManager.update(deltaInNanos);
}
};
}
private volatile int anchorX;
private volatile int anchorY;
private final Color panel_BG; private final Color panel_BG;
private final Color titleText_FG; private final Color titleText_FG;
private final Color mainText_FG; private final Color mainText_FG;
private final Color closeX_FG; private final Color closeX_FG;
private final Color progress_FG; private final Color progress_FG;
private final boolean showCloseButton;
private final BufferedImage cachedImage;
private final int anchorX; private final Window parent;
private final int anchorY;
private static final WindowAdapter windowListener = new NotifyPopupWindowAdapter();
private static final MouseAdapter mouseListener = new NotifyPopupClickAdapter();
private final Notify notification;
private final float hideAfterDurationInSeconds; private final float hideAfterDurationInSeconds;
private final Pos position; private final Pos position;
private final ActionHandler<Notify> onCloseAction;
// this is used in combination with position, so that we can track which screen and what position a popup is in // this is used in combination with position, so that we can track which screen and what position a popup is in
private final String idAndPosition; private final String idAndPosition;
private int popupIndex; private int popupIndex;
private volatile Tween tween = null; private volatile Tween tween = null;
@ -135,39 +118,13 @@ class NotifyPopup extends JFrame {
// 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 boolean showCloseButton; private final ActionHandler<Notify> onCloseAction;
private final BufferedImage cachedImage;
private static final Random RANDOM = new Random();
LookAndFeel(final Window parent, final Notify notification, final Image image, final ImageIcon imageIcon, final Rectangle parentBounds) {
this.parent = parent;
parent.addWindowListener(windowListener);
// this is on the swing EDT parent.addMouseListener(mouseListener);
@SuppressWarnings("NumericCastThatLosesPrecision")
NotifyPopup(final Notify notification, final Image image, final ImageIcon imageIcon) {
this.notification = notification;
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
setAlwaysOnTop(false);
setAlwaysOnTop(true);
setLayout(null);
setSize(WIDTH, HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
setTitle(notification.title);
setResizable(false);
if (image != null) {
setIconImage(image);
} else {
setIconImage(SwingUtil.BLANK_ICON);
}
addWindowListener(windowListener);
addMouseListener(mouseListener);
if (notification.isDark) { if (notification.isDark) {
panel_BG = Color.DARK_GRAY; panel_BG = Color.DARK_GRAY;
@ -184,11 +141,15 @@ class NotifyPopup extends JFrame {
progress_FG = new Color(0x42A5F5); progress_FG = new Color(0x42A5F5);
} }
setBackground(panel_BG);
showCloseButton = !notification.hideCloseButton;
hideAfterDurationInSeconds = notification.hideAfterDurationInMillis / 1000.0F; hideAfterDurationInSeconds = notification.hideAfterDurationInMillis / 1000.0F;
position = notification.position; position = notification.position;
showCloseButton = !notification.hideCloseButton;
// now we setup the rendering of the image
cachedImage = renderBackgroundInfo(notification.title, notification.text, titleText_FG, mainText_FG, panel_BG, imageIcon);
if (notification.onCloseAction != null) { if (notification.onCloseAction != null) {
onCloseAction = new ActionHandler<Notify>() { onCloseAction = new ActionHandler<Notify>() {
@Override @Override
@ -197,91 +158,229 @@ class NotifyPopup extends JFrame {
notification.onCloseAction.handle(notification); notification.onCloseAction.handle(notification);
} }
}; };
} else { }
else {
onCloseAction = null; onCloseAction = null;
} }
GraphicsDevice device; idAndPosition = parentBounds.x + ":" + parentBounds.y + ":" + parentBounds.width + ":" + parentBounds.height + ":" + position;
if (notification.screenNumber == Short.MIN_VALUE) {
// set screen position based on mouse
Point mouseLocation = MouseInfo.getPointerInfo()
.getLocation();
device = ScreenUtil.getGraphicsDeviceAt(mouseLocation); anchorX = getAnchorX(position, parentBounds);
anchorY = getAnchorY(position, parentBounds);
parent.setBackground(panel_BG);
if (image != null) {
parent.setIconImage(image);
} }
else { else {
// set screen position based on specified screen parent.setIconImage(SwingUtil.BLANK_ICON);
int screenNumber = notification.screenNumber; }
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); }
GraphicsDevice screenDevices[] = ge.getScreenDevices();
if (screenNumber < 0) { void paint(final Graphics g) {
screenNumber = 0; // we cache the text + image (to another image), and then always render the close + progressbar
}
else if (screenNumber > screenDevices.length - 1) { // use our cached image, so we don't have to re-render text/background/etc
screenNumber = screenDevices.length - 1; g.drawImage(cachedImage, 0, 0, null);
// 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();
try {
if (showCloseButton) {
Graphics2D g3 = (Graphics2D) g.create();
g3.setColor(panel_BG);
g3.setStroke(stroke);
final Point p = parent.getMousePosition();
// reasonable position for detecting mouse over
if (p != null && p.getX() >= 280 && p.getY() <= 20) {
g3.setColor(Color.RED);
}
else {
g3.setColor(closeX_FG);
}
// draw the X
g3.drawLine(X_1, Y_1, X_2, Y_2);
g3.drawLine(X_2, Y_1, X_1, Y_2);
} }
device = screenDevices[screenNumber]; g2.setColor(progress_FG);
g2.fillRect(0, PROGRESS_HEIGHT, progress, 2);
} finally {
g2.dispose();
}
}
void close() {
if (hideTween != null) {
hideTween.cancel();
hideTween = null;
} }
idAndPosition = device.getIDstring() + notification.position; if (tween != null) {
tween.cancel();
tween = null;
}
Rectangle screenBounds = device.getDefaultConfiguration() parent.removeWindowListener(windowListener);
.getBounds(); parent.removeMouseListener(mouseListener);
}
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 (showCloseButton && x >= 280 && y <= 20) {
// reasonable position for detecting mouse over
((INotify)parent).close();
}
else {
if (onCloseAction != null) {
onCloseAction.handle(null);
}
((INotify) parent).close();
}
}
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);
boolean showFromTop = isShowFromTop(this);
if (tween != null) {
tween.cancel(); // cancel does it's thing on the next tick of animation cycle
tween = null;
}
int changedY;
if (showFromTop) {
changedY = anchorY + (popupIndex * (HEIGHT + 10));
}
else {
changedY = anchorY - (popupIndex * (HEIGHT + 10));
}
parent.setLocation(anchorX, changedY);
}
void shake(final int durationInMillis, final int amplitude) {
int i1 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude;
int i2 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude;
i1 = i1 >> 2;
i2 = i2 >> 2;
// make sure it always moves by some amount
if (i1 < 0) {
i1 -= amplitude >> 2;
}
else {
i1 += amplitude >> 2;
}
if (i2 < 0) {
i2 -= amplitude >> 2;
}
else {
i2 += amplitude >> 2;
}
int count = durationInMillis / 50;
// make sure we always end the animation where we start
if ((count & 1) == 0) {
count++;
}
Tween tween = Tween.to(this, NotifyAccessor.X_Y_POS, accessor, 0.05F)
.targetRelative(i1, i2)
.repeatAutoReverse(count, 0)
.ease(TweenEquations.Linear);
tweenManager.add(tween);
}
void setY(final int y) {
parent.setLocation(parent.getX(), y);
}
void setProgress(final int progress) {
this.progress = progress;
}
int getProgress() {
return progress;
}
int getY() {
return parent.getY();
}
int getX() {
return parent.getX();
}
void setVisible(final boolean visible) {
if (visible) {
parent.toFront();
// set this jframe to use active rendering
SwingActiveRender.addActiveRender(parent);
addPopupToMap(this);
}
else {
removePopupFromMap(this);
SwingActiveRender.removeActiveRender(parent);
}
}
private static
int getAnchorX(final Pos position, final Rectangle bounds) {
// we use the screen that the mouse is currently on. // we use the screen that the mouse is currently on.
final int startX = (int) screenBounds.getX(); final int startX = (int) bounds.getX();
final int startY = (int) screenBounds.getY(); final int screenWidth = (int) bounds.getWidth();
final int screenWidth = (int) screenBounds.getWidth();
final int screenHeight = (int) screenBounds.getHeight();
// determine location for the popup // determine location for the popup
final Pos position = notification.position;
// get anchorX // get anchorX
switch (position) { switch (position) {
case TOP_LEFT: case TOP_LEFT:
case BOTTOM_LEFT: case BOTTOM_LEFT:
anchorX = startX + padding; return startX + PADDING;
break;
case CENTER: case CENTER:
anchorX = startX + (screenWidth / 2) - WIDTH / 2 - padding / 2; return startX + (screenWidth / 2) - WIDTH / 2 - PADDING / 2;
break;
case TOP_RIGHT: case TOP_RIGHT:
case BOTTOM_RIGHT: case BOTTOM_RIGHT:
anchorX = startX + screenWidth - WIDTH - padding; return startX + screenWidth - WIDTH - PADDING;
break;
default: default:
throw new RuntimeException("Unknown position. '" + position + "'"); throw new RuntimeException("Unknown position. '" + position + "'");
} }
}
private static
int getAnchorY(final Pos position, final Rectangle bounds) {
final int startY = (int) bounds.getY();
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:
anchorY = padding + startY; return PADDING + startY;
break;
case CENTER: case CENTER:
anchorY = startY + (screenHeight / 2) - HEIGHT / 2 - padding / 2; return startY + (screenHeight / 2) - HEIGHT / 2 - PADDING / 2;
break;
case BOTTOM_LEFT: case BOTTOM_LEFT:
case BOTTOM_RIGHT: case BOTTOM_RIGHT:
anchorY = startY + screenHeight - HEIGHT - padding; return startY + screenHeight - HEIGHT - PADDING;
break;
default: default:
throw new RuntimeException("Unknown position. '" + position + "'"); throw new RuntimeException("Unknown position. '" + position + "'");
} }
// now we setup the rendering of the image
cachedImage = renderBackgroundInfo(notification.title, notification.text, titleText_FG, mainText_FG, panel_BG, imageIcon);
} }
private static private static
@ -303,14 +402,12 @@ class NotifyPopup extends JFrame {
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
// g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
try { try {
g2.setColor(panel_BG); g2.setColor(panel_BG);
g2.fillRect(0, 0, WIDTH, HEIGHT); g2.fillRect(0, 0, WIDTH, HEIGHT);
// Draw the title text // Draw the title text
java.awt.Font titleTextFont = FontUtil.parseFont(TITLE_TEXT_FONT); java.awt.Font titleTextFont = FontUtil.parseFont(Notify.TITLE_TEXT_FONT);
g2.setColor(titleText_FG); g2.setColor(titleText_FG);
g2.setFont(titleTextFont); g2.setFont(titleTextFont);
g2.drawString(title, 5, 20); g2.drawString(title, 5, 20);
@ -329,12 +426,12 @@ class NotifyPopup extends JFrame {
} }
// Draw the main text // Draw the main text
java.awt.Font mainTextFont = FontUtil.parseFont(MAIN_TEXT_FONT); java.awt.Font mainTextFont = FontUtil.parseFont(Notify.MAIN_TEXT_FONT);
int length = notificationText.length(); int length = notificationText.length();
StringBuilder text = new StringBuilder(length); StringBuilder text = new StringBuilder(length);
// are we "html" already? just check for the starting tag and strip off END html tag // are we "html" already? just check for the starting tag and strip off END html tag
if (length >= 13 && notificationText.regionMatches(true, length-7, "</html>", 0, 7)) { if (length >= 13 && notificationText.regionMatches(true, length - 7, "</html>", 0, 7)) {
text.append(notificationText); text.append(notificationText);
text.delete(text.length() - 7, text.length()); text.delete(text.length() - 7, text.length());
@ -368,235 +465,146 @@ class NotifyPopup extends JFrame {
return image; return image;
} }
@Override
public
void paint(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);
// 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();
try {
if (showCloseButton) {
Graphics2D g3 = (Graphics2D) g.create();
g3.setColor(panel_BG);
g3.setStroke(stroke);
final Point p = getMousePosition();
// reasonable position for detecting mouse over
if (p != null && p.getX() >= 280 && p.getY() <= 20) {
g3.setColor(Color.RED);
} else {
g3.setColor(closeX_FG);
}
// draw the X
g3.drawLine(X_1, Y_1, X_2, Y_2);
g3.drawLine(X_2, Y_1, X_1, Y_2);
}
g2.setColor(progress_FG);
g2.fillRect(0, PROGRESS_HEIGHT, progress, 1);
} finally {
g2.dispose();
}
}
public
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 (showCloseButton && x >= 280 && y <= 20) {
// reasonable position for detecting mouse over
close();
}
else {
if (onCloseAction != null) {
onCloseAction.handle(null);
}
close();
}
}
@Override
public
void setVisible(final boolean b) {
// was it already visible?
if (b == isVisible()) {
// prevent "double setting" visible state
return;
}
super.setVisible(b);
if (b) {
toFront();
// set this jframe to use active rendering
SwingActiveRender.addActiveRender(this);
addPopupToMap();
}
else {
removePopupFromMap();
SwingActiveRender.removeActiveRender(this);
}
}
public
void close() {
// this must happen in the Swing EDT. This is usually called by the active renderer
SwingUtil.invokeLater(new Runnable() {
@Override
public
void run() {
// set it off screen (which is what the close method also does)
if (isVisible()) {
setVisible(false);
}
removeAll();
removeWindowListener(windowListener);
removeMouseListener(mouseListener);
setIconImage(null);
dispose();
notification.onClose();
}
});
}
// only called on the swing EDT thread // only called on the swing EDT thread
void addPopupToMap() { private static
void addPopupToMap(final LookAndFeel sourceLook) {
synchronized (popups) { synchronized (popups) {
ArrayList<NotifyPopup> notifyPopups = popups.get(idAndPosition); String id = sourceLook.idAndPosition;
if (notifyPopups == null) {
notifyPopups = new ArrayList<NotifyPopup>(4); ArrayList<LookAndFeel> looks = popups.get(id);
popups.put(idAndPosition, notifyPopups); if (looks == null) {
looks = new ArrayList<LookAndFeel>(4);
popups.put(id, looks);
} }
final int popupIndex = notifyPopups.size(); final int popupIndex = looks.size();
this.popupIndex = popupIndex; sourceLook.popupIndex = popupIndex;
// 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;
if (isShowFromTop(position)) { int anchorX = sourceLook.anchorX;
int anchorY = sourceLook.anchorY;
if (isShowFromTop(sourceLook)) {
targetY = anchorY + (popupIndex * (HEIGHT + 10)); targetY = anchorY + (popupIndex * (HEIGHT + 10));
} }
else { else {
targetY = anchorY - (popupIndex * (HEIGHT + 10)); targetY = anchorY - (popupIndex * (HEIGHT + 10));
} }
notifyPopups.add(this); looks.add(sourceLook);
setLocation(anchorX, targetY); sourceLook.setLocation(anchorX, targetY);
if (hideAfterDurationInSeconds > 0 && 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)
hideTween = Tween.to(this, NotifyPopupAccessor.PROGRESS, accessor, hideAfterDurationInSeconds) Tween hideTween = Tween.to(sourceLook, NotifyAccessor.PROGRESS, accessor, sourceLook.hideAfterDurationInSeconds)
.target(WIDTH) .target(WIDTH)
.ease(TweenEquations.Linear)
.addCallback(new TweenCallback() {
@Override
public
void onEvent(final int type, final BaseTween<?> source) {
if (type == Events.END) {
close();
}
}
});
tweenManager.add(hideTween);
// start if we have stopped the timer
if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) {
tweenManager.resetUpdateTime();
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler);
}
}
}
}
// only called on the swing app or SwingActiveRender thread
private
void removePopupFromMap() {
boolean showFromTop = isShowFromTop(position);
synchronized (popups) {
final ArrayList<NotifyPopup> notifyPopups = popups.get(idAndPosition);
// there are two loops because it is necessary to kill + remove all tweens BEFORE adding new ones.
for (final NotifyPopup popup : notifyPopups) {
if (popup.tween != null) {
popup.tween.kill(); // kill does it's thing on the next tick of animation cycle
popup.tween = null;
}
if (popup == this && popup.hideTween != null) {
popup.hideTween.kill();
}
}
boolean adjustPopupPosition = false;
for (Iterator<NotifyPopup> iterator = notifyPopups.iterator(); iterator.hasNext(); ) {
final NotifyPopup popup = iterator.next();
if (popup == this) {
adjustPopupPosition = true;
iterator.remove();
}
else if (adjustPopupPosition) {
int index = popup.popupIndex - 1;
popup.popupIndex = index;
// the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up
int changedY;
if (showFromTop) {
changedY = popup.anchorY + (index * (HEIGHT + 10));
}
else {
changedY = popup.anchorY - (index * (HEIGHT + 10));
}
// now animate that popup to it's new location
Tween tween = Tween.to(popup, NotifyPopupAccessor.Y_POS, accessor, MOVE_DURATION)
.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.END) { if (type == Events.COMPLETE) {
// make sure to remove the tween once it's done, otherwise .kill can do weird things. ((INotify)sourceLook.parent).close();
popup.hideTween = null; }
// }
} }
}); });
tweenManager.add(hideTween);
}
}
popup.tween = tween; // start if we have stopped the timer
tweenManager.add(tween); if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) {
tweenManager.resetUpdateTime();
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler);
}
}
void setLocation(final int x, final int y) {
parent.setLocation(x, y);
}
// only called on the swing app or SwingActiveRender thread
private static
void removePopupFromMap(final LookAndFeel sourceLook) {
boolean showFromTop = isShowFromTop(sourceLook);
boolean popupsAreEmpty;
synchronized (popups) {
popupsAreEmpty = popups.isEmpty();
final ArrayList<LookAndFeel> 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;
for (Iterator<LookAndFeel> iterator = allLooks.iterator(); iterator.hasNext(); ) {
final LookAndFeel look = iterator.next();
if (look.tween != null) {
look.tween.cancel(); // cancel does it's thing on the next tick of animation cycle
look.tween = null;
}
if (look == sourceLook) {
if (look.hideTween != null) {
look.hideTween.cancel();
look.hideTween = null;
}
adjustPopupPosition = true;
iterator.remove();
}
if (adjustPopupPosition) {
look.popupIndex--;
} }
} }
// if there's nothing left, stop the timer. for (final LookAndFeel look : allLooks) {
if (popups.isEmpty()) { // the popups are ALL the same size!
SwingActiveRender.removeActiveRenderFrameStart(frameStartHandler); // popups at TOP grow down, popups at BOTTOM grow up
} int changedY;
// start if we have stopped the timer if (showFromTop) {
else if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) { changedY = look.anchorY + (look.popupIndex * (HEIGHT + 10));
tweenManager.resetUpdateTime(); }
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler); else {
changedY = look.anchorY - (look.popupIndex * (HEIGHT + 10));
}
// now animate that popup to it's new location
Tween tween = Tween.to(look, NotifyAccessor.Y_POS, accessor, MOVE_DURATION)
.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;
}
}
});
look.tween = tween;
tweenManager.add(tween);
} }
} }
// if there's nothing left, stop the timer.
if (popupsAreEmpty) {
SwingActiveRender.removeActiveRenderFrameStart(frameStartHandler);
}
// start if we have previously stopped the timer
else if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) {
tweenManager.resetUpdateTime();
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler);
}
} }
private static private static
boolean isShowFromTop(final Pos p) { boolean isShowFromTop(final LookAndFeel look) {
switch (p) { switch (look.position) {
case TOP_LEFT: case TOP_LEFT:
case TOP_RIGHT: case TOP_RIGHT:
case CENTER: // center grows down case CENTER: // center grows down
@ -605,60 +613,4 @@ class NotifyPopup extends JFrame {
return false; return false;
} }
} }
public
void setY(final int newY) {
setLocation(getX(), newY);
}
/**
* Shakes the popup
*
* @param durationInMillis now long it will shake
* @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot.
*/
public
void shake(final int durationInMillis, final int amplitude) {
int i1 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude;
int i2 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude;
i1 = i1 >> 2;
i2 = i2 >> 2;
// make sure it always moves by some amount
if (i1 < 0) {
i1 -= amplitude >> 2;
}
else {
i1 += amplitude >> 2;
}
if (i2 < 0) {
i2 -= amplitude >> 2;
}
else {
i2 += amplitude >> 2;
}
int count = durationInMillis / 50;
// make sure we always end the animation where we start
if ((count & 1) == 0) {
count++;
}
Tween tween = Tween.to(this, NotifyPopupAccessor.X_Y_POS, accessor, 0.05F)
.targetRelative(i1, i2)
.repeatAutoReverse(count, 0)
.ease(TweenEquations.Linear);
tweenManager.add(tween);
}
int getProgress() {
return progress;
}
void setProgress(final int progress) {
this.progress = progress;
}
} }

View File

@ -16,6 +16,7 @@
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;
@ -54,6 +55,24 @@ import dorkbox.util.Version;
public final public final
class Notify { class Notify {
/**
* This is the title font used by a notification.
*/
@Property
public static String TITLE_TEXT_FONT = "Source Code Pro BOLD 16";
/**
* This is the main text font used by a notification.
*/
@Property
public static String MAIN_TEXT_FONT = "Source Code Pro BOLD 12";
/**
* How long we want it to take for the popups to relocate when one is closed
*/
@Property
public static float MOVE_DURATION = 1.0F;
/** /**
* Location of the dialog image resources. By default they must be in the 'resources' directory relative to the application * Location of the dialog image resources. By default they must be in the 'resources' directory relative to the application
*/ */
@ -140,11 +159,12 @@ class Notify {
private Image graphic; private Image graphic;
ActionHandler<Notify> onCloseAction; ActionHandler<Notify> onCloseAction;
private NotifyPopup 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 private
Notify() { Notify() {
@ -268,7 +288,7 @@ class Notify {
} }
/** /**
* Shows the notification * Shows the notification. If the Notification is assigned to a screen, but shown in a JFrame, the screen number will be ignored.
*/ */
public public
void show() { void show() {
@ -281,12 +301,9 @@ class Notify {
final Notify notify = Notify.this; final Notify notify = Notify.this;
final Image graphic = notify.graphic; final Image graphic = notify.graphic;
if (graphic == null) { // we ONLY cache our own icons
notifyPopup = new NotifyPopup(notify, null, null); ImageIcon imageIcon = null;
} if (graphic != null) {
else {
// we ONLY cache our own icons
ImageIcon imageIcon;
if (name != null) { if (name != null) {
imageIcon = imageIconCache.get(name); imageIcon = imageIconCache.get(name);
if (imageIcon == null) { if (imageIcon == null) {
@ -301,8 +318,12 @@ class Notify {
else { else {
imageIcon = new ImageIcon(graphic); imageIcon = new ImageIcon(graphic);
} }
}
notifyPopup = new NotifyPopup(notify, graphic, imageIcon); if (window == null) {
notifyPopup = new AsFrame(notify, graphic, imageIcon);
} else {
notifyPopup = new AsDialog(notify, graphic, imageIcon, window);
} }
notifyPopup.setVisible(true); notifyPopup.setVisible(true);
@ -312,6 +333,9 @@ class Notify {
} }
} }
}); });
// don't need to hang onto these.
graphic = null;
} }
/** /**
@ -369,9 +393,19 @@ class Notify {
return this; return this;
} }
/**
* Attaches this notification to a specific JFrame/Window, instead of having a global notification
*/
public
Notify attach(final Window frame) {
this.window = frame;
return this;
}
// called when this notification is closed.
void onClose() { void onClose() {
notifyPopup = null; notifyPopup = null;
graphic = null;
} }
} }

View File

@ -17,19 +17,19 @@ package dorkbox.notify;
import dorkbox.tweenengine.TweenAccessor; import dorkbox.tweenengine.TweenAccessor;
class NotifyPopupAccessor implements TweenAccessor<NotifyPopup> { class NotifyAccessor implements TweenAccessor<LookAndFeel> {
static final int Y_POS = 1; static final int Y_POS = 1;
static final int X_Y_POS = 2; static final int X_Y_POS = 2;
static final int PROGRESS = 3; static final int PROGRESS = 3;
NotifyPopupAccessor() { NotifyAccessor() {
} }
@Override @Override
public public
int getValues(final NotifyPopup 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.getY(); returnValues[0] = (float) target.getY();
@ -48,7 +48,7 @@ class NotifyPopupAccessor implements TweenAccessor<NotifyPopup> {
@SuppressWarnings({"NumericCastThatLosesPrecision", "UnnecessaryReturnStatement"}) @SuppressWarnings({"NumericCastThatLosesPrecision", "UnnecessaryReturnStatement"})
@Override @Override
public public
void setValues(final NotifyPopup 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.setY((int) newValues[0]); target.setY((int) newValues[0]);

View File

@ -15,22 +15,23 @@
*/ */
package dorkbox.notify; package dorkbox.notify;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
class NotifyPopupWindowAdapter extends WindowAdapter { class WindowAdapter extends java.awt.event.WindowAdapter {
@Override
public public
void windowClosing(WindowEvent e) { void windowClosing(WindowEvent e) {
if (e.getNewState() != WindowEvent.WINDOW_CLOSED) { if (e.getNewState() != WindowEvent.WINDOW_CLOSED) {
NotifyPopup source = (NotifyPopup) e.getSource(); AsFrame source = (AsFrame) e.getSource();
source.close(); source.close();
} }
} }
@Override
public public
void windowLostFocus(WindowEvent e) { void windowLostFocus(WindowEvent e) {
if (e.getNewState() != WindowEvent.WINDOW_CLOSED) { if (e.getNewState() != WindowEvent.WINDOW_CLOSED) {
NotifyPopup source = (NotifyPopup) e.getSource(); AsFrame source = (AsFrame) e.getSource();
// these don't work // these don't work
//toFront(); //toFront();
//requestFocus(); //requestFocus();

View File

@ -13,9 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import dorkbox.notify.Notify; import dorkbox.notify.Notify;
import dorkbox.notify.Pos; import dorkbox.notify.Pos;
import dorkbox.util.ActionHandler; import dorkbox.util.ActionHandler;
import dorkbox.util.ScreenUtil;
public public
class NotifyTest { class NotifyTest {
@ -24,28 +32,117 @@ class NotifyTest {
void main(String[] args) { void main(String[] args) {
Notify notify; Notify notify;
int count = 3;
JFrame frame = new JFrame("Test");
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
JLabel label = new JLabel("This is a label!");
JButton button = new JButton();
button.setText("Press me");
panel.add(label);
panel.add(button);
frame.add(panel);
frame.setSize(600, 600);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
ScreenUtil.showOnSameScreenAsMouse_Center(frame);
int count = 2;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
final int finalI = i;
notify = Notify.create() notify = Notify.create()
.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(50000) .hideAfter(13000)
.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) {
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_RIGHT) .position(Pos.TOP_RIGHT)
// .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
void handle(final Notify arg0) { void handle(final Notify arg0) {
System.out.println("Notification clicked on!"); System.err.println("Notification " + finalI + " clicked on!");
} }
}); });
notify.showWarning(); notify.showConfirm();
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.BOTTOM_LEFT)
// .setScreen(0)
.darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
.hideCloseButton()
.onAction(new ActionHandler<Notify>() {
@Override
public
void handle(final Notify arg0) {
System.err.println("Notification " + finalI + " clicked on!");
}
});
notify.show();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
} }
} }