Changed how properties act. Changed the default for showing from 5 seconds (to hide), to never hide. Added smooth animation (via active rendering swing components). Added progress-bar to show when popup will be hidden. Made "shake" move the popup in a random direction instead of all in the same direction.

This commit is contained in:
nathan 2015-11-20 11:14:38 +01:00
parent 8d814f02ab
commit 4288dd46e5
7 changed files with 335 additions and 166 deletions

View File

@ -4,6 +4,8 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/libs" />
</content>

View File

@ -49,8 +49,6 @@ import java.util.Map;
public final
class Growl {
public static final int FOREVER = 0;
/**
* Location of the dialog image resources. By default they must be in the 'resources' directory relative to the application
*/
@ -119,7 +117,7 @@ class Growl {
String title;
String text;
Pos position = Pos.BOTTOM_RIGHT;
int hideAfterDurationInMillis = 5000;
int hideAfterDurationInMillis = 0;
boolean hideCloseButton;
boolean isDark = false;
int screenNumber = Short.MIN_VALUE;
@ -171,7 +169,8 @@ class Growl {
}
/**
* Specifies the duration that the notification should show, after which it will be hidden. 0 means to show forever.
* Specifies the duration that the notification should show, after which it will be hidden. 0 means to show forever. By default it
* will show forever
*/
public
Growl hideAfter(int durationInMillis) {
@ -294,10 +293,13 @@ class Growl {
}
/**
* "shakes" the notification, to bring user attention
* "shakes" the notification, to bring user attention to it.
*
* @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) {
Growl shake(final int durationInMillis, final int amplitude) {
this.shakeDurationInMillis = durationInMillis;
this.shakeAmplitude = amplitude;
@ -312,6 +314,8 @@ class Growl {
}
});
}
return this;
}
/**

View File

@ -15,67 +15,68 @@
*/
package dorkbox.util.growl;
import dorkbox.util.ActionHandlerLong;
import dorkbox.util.OS;
import dorkbox.util.Property;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
import dorkbox.util.SystemProps;
import dorkbox.util.swing.SwingActiveRender;
import dorkbox.util.tweenengine.BaseTween;
import dorkbox.util.tweenengine.Tween;
import dorkbox.util.tweenengine.TweenCallback;
import dorkbox.util.tweenengine.TweenEquations;
import dorkbox.util.tweenengine.TweenManager;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
// 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")
@SuppressWarnings({"Duplicates", "FieldCanBeLocal"})
public
class GrowlPopup extends JFrame {
private static final int padding = 40;
@Property
/** title font used by growl */
public static String TITLE_TEXT_FONT = "Source Code Pro BOLD 16";
public static final float FADE_DURATION = 1.5F;
@Property
/** main text font used by growl */
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 final float MOVE_DURATION = 1.0F;
private static final int padding = 40;
private static final Map<String, ArrayList<GrowlPopup>> popups = new HashMap<String, ArrayList<GrowlPopup>>();
private static final GrowlPopupAccessor accessor = new GrowlPopupAccessor();
private static final TweenManager tweenManager = new TweenManager();
private static ActionHandlerLong frameStartHandler;
private static Timer timer;
private static WindowUtil opacity_compat;
static {
// this timer is on the EDT (this is not the java.util timer)
// 30 times a second
//noinspection Convert2Lambda
timer = new Timer(1000/30, new ActionListener() {
// this is for updating the tween engine during active-rendering
frameStartHandler = new ActionHandlerLong() {
@Override
public
void actionPerformed(final ActionEvent e) {
tweenManager.update();
void handle(final long deltaInNanos) {
GrowlPopup.tweenManager.update(deltaInNanos);
}
});
timer.setRepeats(true);
};
if (OS.javaVersion == 6) {
opacity_compat = new WindowUtil_Java6();
@ -85,8 +86,23 @@ class GrowlPopup extends JFrame {
}
private static final int WIDTH = 300;
private static final int HEIGHT = 90;
private static final int HEIGHT = 87;
private static final int PROGRESS_HEIGHT = HEIGHT - 1;
private static final Stroke stroke = new BasicStroke(2);
private static final int closeX = 282;
private static final int closeY = 2;
private static final int Y_1 = closeY + 5;
private static final int X_1 = closeX + 5;
private static final int Y_2 = closeY + 11;
private static final int X_2 = closeX + 11;
private final Color panel_BG;
private final Color titleText_FG;
private final Color mainText_FG;
private final Color closeX_FG;
private final Color progress_FG;
private final int anchorX;
@ -96,6 +112,7 @@ class GrowlPopup extends JFrame {
private final MouseAdapter mouseListener;
private final Growl notification;
private final ImageIcon imageIcon;
// 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;
@ -105,11 +122,21 @@ class GrowlPopup extends JFrame {
private Tween tween = null;
private Tween hideTween = null;
// 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 boolean showCloseButton;
private BufferedImage cachedImage;
private static final Random RANDOM = new Random();
// this is on the swing EDT
@SuppressWarnings("NumericCastThatLosesPrecision")
GrowlPopup(Growl notification, Image image, ImageIcon imageIcon) {
this.notification = notification;
this.imageIcon = imageIcon;
windowListener = new GrowlPopupWindowAdapter();
mouseListener = new GrowlPopupClickAdapter();
@ -117,7 +144,9 @@ class GrowlPopup extends JFrame {
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
setOpacity_Compat(1.0F);
setAlwaysOnTop(false);
setAlwaysOnTop(true);
setLayout(null);
setSize(WIDTH, HEIGHT);
setLocation(Short.MIN_VALUE, Short.MIN_VALUE);
@ -136,26 +165,23 @@ class GrowlPopup extends JFrame {
addMouseListener(mouseListener);
final Color text_BG;
final Color titleText_FG;
final Color mainText_FG;
final Color closeX_FG;
if (notification.isDark) {
text_BG = Color.DARK_GRAY;
panel_BG = Color.DARK_GRAY;
titleText_FG = Color.GRAY;
mainText_FG = Color.LIGHT_GRAY;
closeX_FG = Color.GRAY;
progress_FG = Color.gray;
}
else {
text_BG = Color.WHITE;
titleText_FG = Color.DARK_GRAY;
panel_BG = Color.WHITE;
titleText_FG = Color.GRAY.darker();
mainText_FG = Color.GRAY;
closeX_FG = Color.LIGHT_GRAY;
progress_FG = new Color(0x42A5F5);
}
setBackground(text_BG);
setBackground(panel_BG);
showCloseButton = !notification.hideCloseButton;
GraphicsDevice device;
if (notification.screenNumber == Short.MIN_VALUE) {
@ -192,105 +218,6 @@ class GrowlPopup extends JFrame {
final int screenWidth = (int) screenBounds.getWidth();
final int screenHeight = (int) screenBounds.getHeight();
// makes sure everything is spaced nicely
final JPanel contentPane = new JPanel();
contentPane.setBackground(text_BG);
contentPane.setBorder(new EmptyBorder(0, 10, 10, 5));
contentPane.setLayout(new BorderLayout(10, 5));
setContentPane(contentPane);
// closebutton is the 'x' button, but it is really just a font
Font closeButtonFont = SwingUtil.getFontFromProperty(SystemProps.growl_closeButtonFontName, "Source Code Pro", Font.BOLD, 12);
Font mainTextFont = SwingUtil.getFontFromProperty(SystemProps.growl_titleTextFontName, "Source Code Pro", Font.BOLD, 14);
Font titleTextFont = SwingUtil.getFontFromProperty(SystemProps.growl_mainTextFontName, "Source Code Pro", Font.BOLD, 16);
// TITLE AND CLOSE BUTTON
{
Box box = new Box(BoxLayout.X_AXIS);
box.setBackground(text_BG);
box.setAlignmentX(Component.CENTER_ALIGNMENT);
{
Box textBox = new Box(BoxLayout.X_AXIS);
textBox.setAlignmentX(Component.LEFT_ALIGNMENT);
final JLabel titleLabel = new JLabel();
titleLabel.setForeground(titleText_FG);
titleLabel.setFont(titleTextFont);
titleLabel.setText(notification.title);
textBox.add(titleLabel);
textBox.add(Box.createHorizontalGlue());
box.add(textBox);
if (!notification.hideCloseButton) {
// can specify to hide the close button
Box closeBox = new Box(BoxLayout.X_AXIS);
closeBox.setBorder(new EmptyBorder(4, 4, 4, 4));
closeBox.setAlignmentX(Component.RIGHT_ALIGNMENT);
final JLabel closeButton = new JLabel();
closeButton.setForeground(closeX_FG);
closeButton.setFont(closeButtonFont);
closeButton.setText("x");
closeButton.setVerticalTextPosition(SwingConstants.TOP);
closeBox.addMouseListener(new GrowlCloseAdapter(this));
closeBox.add(closeButton);
box.add(closeBox);
}
}
contentPane.add(box, BorderLayout.NORTH);
}
int textLengthLimit = 98;
// ICON
if (imageIcon != null) {
textLengthLimit = 76;
JLabel iconLabel = new JLabel(imageIcon);
contentPane.add(iconLabel, BorderLayout.WEST);
}
// MAIN TEXT
{
String notText = notification.text;
int length = notText.length();
StringBuilder text = new StringBuilder(length);
// are we "html" already? just check for the starting tag and strip off END html tag
if (length >= 13 && notText.regionMatches(true, length-7, "</html>", 0, 7)) {
text.append(notText);
text.delete(text.length() - 7, text.length());
length -= 7;
}
else {
text.append("<html>");
text.append(notText);
}
// make sure the text is the correct length
if (length > textLengthLimit) {
text.delete(6 + textLengthLimit, text.length());
text.append("...");
}
text.append("</html>");
JLabel mainTextLabel = new JLabel();
mainTextLabel.setForeground(mainText_FG);
mainTextLabel.setFont(mainTextFont);
mainTextLabel.setText(text.toString());
contentPane.add(mainTextLabel, BorderLayout.CENTER);
}
// determine location for the popup
final Pos position = notification.position;
@ -336,9 +263,156 @@ class GrowlPopup extends JFrame {
}
}
public void close() {
removePopupFromMap();
@Override
public
void paint(Graphics g) {
// we cache the text + image (to another image), and then always render the close + progressbar
int width = getWidth();
int height = getHeight();
if (width <= 0 || height <= 0) {
return;
}
if (cachedImage == null) {
cachedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g2 = cachedImage.createGraphics();
try {
g2.setColor(panel_BG);
g2.fillRect(0, 0, WIDTH, HEIGHT);
// Draw the title text
Font titleTextFont = SwingUtil.parseFont(TITLE_TEXT_FONT);
g2.setColor(titleText_FG);
g2.setFont(titleTextFont);
g2.drawString(notification.title, 5, 20);
int posX = 10;
int textLengthLimit = 108;
// ICON
if (imageIcon != null) {
textLengthLimit = 88;
posX = 60;
// Draw the image
imageIcon.paintIcon(this, g2, 5, 30);
}
// Draw the main text
Font mainTextFont = SwingUtil.parseFont(MAIN_TEXT_FONT);
String notText = notification.text;
int length = notText.length();
StringBuilder text = new StringBuilder(length);
// are we "html" already? just check for the starting tag and strip off END html tag
if (length >= 13 && notText.regionMatches(true, length-7, "</html>", 0, 7)) {
text.append(notText);
text.delete(text.length() - 7, text.length());
length -= 7;
}
else {
text.append("<html>");
text.append(notText);
}
// make sure the text is the correct length
if (length > textLengthLimit) {
text.delete(6 + textLengthLimit, text.length());
text.append("...");
}
text.append("</html>");
JLabel mainTextLabel = new JLabel();
mainTextLabel.setForeground(mainText_FG);
mainTextLabel.setFont(mainTextFont);
mainTextLabel.setText(text.toString());
int posY = -8;
mainTextLabel.setBounds(0, 0, WIDTH - posX - 2, HEIGHT);
g2.translate(posX, posY);
mainTextLabel.paint(g2);
g2.translate(-posX, -posY);
} finally {
g2.dispose();
}
g.drawImage(cachedImage, 0, 0, null);
}
else {
// use our cached image, so we don't have to re-render text
g.drawImage(cachedImage, getX(), getY(), null);
// the progress bar and close button are the only things that can change, so we always draw them
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 {
notification.onClick();
close();
}
}
@Override
public
void setVisible(final boolean b) {
// necessary for active rendering
setIgnoreRepaint(true);
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() {
WindowEvent winClosingEvent = new WindowEvent(this, WindowEvent.WINDOW_CLOSING);
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(winClosingEvent);
@ -383,9 +457,11 @@ class GrowlPopup extends JFrame {
if (notification.hideAfterDurationInMillis > 0 && hideTween == null) {
// begin a timeline to get rid of the popup (default is 5 seconds)
hideTween = Tween.set(this, GrowlPopupAccessor.OPACITY, accessor)
.delay(FADE_DURATION + (notification.hideAfterDurationInMillis / 1000.0F))
.target(0)
final float durationInSeconds = notification.hideAfterDurationInMillis / 1000.0F;
hideTween = Tween.to(this, GrowlPopupAccessor.PROGRESS, accessor, durationInSeconds)
.target(WIDTH)
.ease(TweenEquations.Linear)
.addCallback(new TweenCallback() {
@Override
public
@ -395,8 +471,9 @@ class GrowlPopup extends JFrame {
});
tweenManager.add(hideTween);
if (!timer.isRunning()) {
timer.start();
if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) {
tweenManager.resetUpdateTime();
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler);
}
}
}
@ -428,8 +505,8 @@ class GrowlPopup extends JFrame {
}
// if there's nothing left, stop the timer.
if (copies.isEmpty()) {
timer.stop();
if (popups.isEmpty()) {
SwingActiveRender.removeActiveRenderFrameStart(frameStartHandler);
}
return;
}
@ -478,12 +555,9 @@ class GrowlPopup extends JFrame {
popups.put(idAndPosition, copies);
// if there's nothing left, stop the timer.
if (copies.isEmpty()) {
timer.stop();
}
else if (!timer.isRunning()) {
if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) {
tweenManager.resetUpdateTime();
timer.start();
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler);
}
}
}
@ -505,19 +579,45 @@ class GrowlPopup extends JFrame {
setLocation(getX(), newY);
}
public
void onClick() {
notification.onClick();
close();
}
/**
* 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) {
System.err.println("shake");
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, GrowlPopupAccessor.X_Y_POS, accessor, 0.05F)
.targetRelative(amplitude, amplitude)
.repeatAutoReverse(durationInMillis / 50, 0)
.targetRelative(i1, i2)
.repeatAutoReverse(count, 0)
.ease(TweenEquations.Linear);
tweenManager.add(tween);
}
@ -531,4 +631,14 @@ class GrowlPopup extends JFrame {
float getOpacity_Compat() {
return opacity_compat.getOpacity(this);
}
public
int getProgress() {
return progress;
}
public
void setProgress(final int progress) {
this.progress = progress;
}
}

View File

@ -22,6 +22,7 @@ class GrowlPopupAccessor implements TweenAccessor<GrowlPopup> {
static final int OPACITY = 0;
static final int Y_POS = 1;
static final int X_Y_POS = 2;
static final int PROGRESS = 3;
GrowlPopupAccessor() {
@ -41,10 +42,14 @@ class GrowlPopupAccessor implements TweenAccessor<GrowlPopup> {
returnValues[0] = (float) target.getX();
returnValues[1] = (float) target.getY();
return 2;
case PROGRESS:
returnValues[0] = (float) target.getProgress();
return 1;
}
return 1;
}
@SuppressWarnings({"NumericCastThatLosesPrecision", "UnnecessaryReturnStatement"})
@Override
public
void setValues(final GrowlPopup target, final int tweenType, final float[] newValues) {
@ -53,12 +58,14 @@ class GrowlPopupAccessor implements TweenAccessor<GrowlPopup> {
target.setOpacity_Compat(newValues[0]);
return;
case Y_POS:
//noinspection NumericCastThatLosesPrecision
target.setY((int) newValues[0]);
return;
case X_Y_POS:
//noinspection NumericCastThatLosesPrecision
target.setLocation((int) newValues[0], (int) newValues[1]);
return;
case PROGRESS:
target.setProgress((int) newValues[0]);
return;
}
}
}

View File

@ -28,6 +28,6 @@ class GrowlPopupClickAdapter extends MouseAdapter {
public
void mouseReleased(final MouseEvent e) {
GrowlPopup source = (GrowlPopup) e.getSource();
source.onClick();
source.onClick(e.getX(), e.getY());
}
}

View File

@ -20,8 +20,14 @@ import java.awt.event.WindowEvent;
class GrowlPopupWindowAdapter extends WindowAdapter {
public
void windowOpened(WindowEvent e) {
GrowlPopup source = (GrowlPopup) e.getSource();
source.addPopupToMap();
void windowLostFocus(WindowEvent e) {
if (e.getNewState() != WindowEvent.WINDOW_CLOSED) {
GrowlPopup source = (GrowlPopup) e.getSource();
//toFront();
//requestFocus();
source.setAlwaysOnTop(false);
source.setAlwaysOnTop(true);
//requestFocusInWindow();
}
}
}

40
test/GrowlTest.java Normal file
View File

@ -0,0 +1,40 @@
import dorkbox.util.ActionHandler;
import dorkbox.util.growl.Growl;
import dorkbox.util.growl.Pos;
/**
*
*/
public
class GrowlTest {
public static
void main(String[] args) {
Growl growl;
int count = 3;
for (int i = 0; i < count; i++) {
growl = Growl.create()
.title("Growl title " + i)
.text("This is a growl notification popup message This is a growl notification popup message This is a growl notification popup message")
.hideAfter(50000)
.position(Pos.TOP_RIGHT)
// .setScreen(0)
.darkStyle()
.shake(1300, 4)
// .shake(1300, 10)
// .hideCloseButton()
.onAction(new ActionHandler<Growl>() {
@Override
public
void handle(final Growl arg0) {
System.out.println("Notification clicked on!");
}
});
growl.showWarning();
// growl.show();
}
}
}