Issues surrounding buggy rendering behavior fixed via frame/dialog
changed to use the AWT canvas specifically for drawing, updated Active Rendering components to support AWT canvas. Added custom theme support.
This commit is contained in:
parent
14fb42b69e
commit
cdbf8a2a4d
|
@ -16,8 +16,8 @@
|
|||
package dorkbox.notify;
|
||||
|
||||
import java.awt.Dialog;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ComponentEvent;
|
||||
|
@ -43,22 +43,29 @@ class AsDialog extends JDialog implements INotify {
|
|||
|
||||
// this is on the swing EDT
|
||||
@SuppressWarnings("NumericCastThatLosesPrecision")
|
||||
AsDialog(final Notify notification, final Image image, final ImageIcon imageIcon, final Window container) {
|
||||
AsDialog(final Notify notification, final Image image, final ImageIcon imageIcon, final Window container, final Theme theme) {
|
||||
super(container, Dialog.ModalityType.MODELESS);
|
||||
this.notification = notification;
|
||||
|
||||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
setUndecorated(true);
|
||||
setLayout(null);
|
||||
|
||||
setSize(LookAndFeel.WIDTH, LookAndFeel.HEIGHT);
|
||||
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);
|
||||
|
||||
look = new LookAndFeel(this, notification, image, imageIcon, container.getBounds());
|
||||
NotifyCanvas notifyCanvas = new NotifyCanvas(notification, imageIcon, theme);
|
||||
add(notifyCanvas);
|
||||
|
||||
look = new LookAndFeel(this, notifyCanvas, notification, image, container.getBounds());
|
||||
|
||||
// this makes sure that our dialogs follow the parent window (if it's hidden/shown/moved/etc)
|
||||
parentListener = new ComponentListener() {
|
||||
@Override
|
||||
public
|
||||
|
@ -104,12 +111,6 @@ class AsDialog extends JDialog implements INotify {
|
|||
container.addComponentListener(parentListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void paint(Graphics g) {
|
||||
look.paint(g);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void onClick(final int x, final int y) {
|
||||
|
@ -130,15 +131,20 @@ class AsDialog extends JDialog implements INotify {
|
|||
|
||||
@Override
|
||||
public
|
||||
void setVisible(final boolean b) {
|
||||
void setVisible(final boolean visible) {
|
||||
// was it already visible?
|
||||
if (b == isVisible()) {
|
||||
if (visible == isVisible()) {
|
||||
// prevent "double setting" visible state
|
||||
return;
|
||||
}
|
||||
|
||||
super.setVisible(b);
|
||||
look.setVisible(b);
|
||||
// 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
package dorkbox.notify;
|
||||
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GraphicsDevice;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.Image;
|
||||
|
@ -42,15 +42,19 @@ class AsFrame extends JFrame implements INotify {
|
|||
|
||||
// this is on the swing EDT
|
||||
@SuppressWarnings("NumericCastThatLosesPrecision")
|
||||
AsFrame(final Notify notification, final Image image, final ImageIcon imageIcon) {
|
||||
AsFrame(final Notify notification, final Image image, final ImageIcon imageIcon, final Theme theme) {
|
||||
this.notification = notification;
|
||||
|
||||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
setUndecorated(true);
|
||||
setAlwaysOnTop(true);
|
||||
setLayout(null);
|
||||
// setLayout(null);
|
||||
|
||||
setSize(LookAndFeel.WIDTH, LookAndFeel.HEIGHT);
|
||||
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);
|
||||
|
@ -85,13 +89,11 @@ class AsFrame extends JFrame implements INotify {
|
|||
bounds = device.getDefaultConfiguration()
|
||||
.getBounds();
|
||||
|
||||
look = new LookAndFeel(this, notification, image, imageIcon, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
void paint(Graphics g) {
|
||||
look.paint(g);
|
||||
NotifyCanvas notifyCanvas = new NotifyCanvas(notification, imageIcon, theme);
|
||||
getContentPane().add(notifyCanvas);
|
||||
|
||||
look = new LookAndFeel(this, notifyCanvas, notification, image, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -114,15 +116,24 @@ class AsFrame extends JFrame implements INotify {
|
|||
|
||||
@Override
|
||||
public
|
||||
void setVisible(final boolean b) {
|
||||
void setVisible(final boolean visible) {
|
||||
// was it already visible?
|
||||
if (b == isVisible()) {
|
||||
if (visible == isVisible()) {
|
||||
// prevent "double setting" visible state
|
||||
return;
|
||||
}
|
||||
|
||||
super.setVisible(b);
|
||||
look.setVisible(b);
|
||||
// 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);
|
||||
|
||||
if (visible) {
|
||||
this.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,35 +15,23 @@
|
|||
*/
|
||||
package dorkbox.notify;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.image.BufferedImage;
|
||||
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 javax.swing.JLabel;
|
||||
|
||||
import dorkbox.tweenengine.BaseTween;
|
||||
import dorkbox.tweenengine.Tween;
|
||||
import dorkbox.tweenengine.TweenCallback;
|
||||
import dorkbox.tweenengine.TweenEngine;
|
||||
import dorkbox.tweenengine.TweenEquations;
|
||||
import dorkbox.tweenengine.TweenManager;
|
||||
import dorkbox.util.ActionHandler;
|
||||
import dorkbox.util.ActionHandlerLong;
|
||||
import dorkbox.util.FontUtil;
|
||||
import dorkbox.util.SwingUtil;
|
||||
import dorkbox.util.swing.SwingActiveRender;
|
||||
|
||||
|
@ -51,58 +39,42 @@ import dorkbox.util.swing.SwingActiveRender;
|
|||
class LookAndFeel {
|
||||
private static final Map<String, ArrayList<LookAndFeel>> popups = new HashMap<String, ArrayList<LookAndFeel>>();
|
||||
|
||||
static final TweenEngine animation = TweenEngine.create()
|
||||
.unsafe() // access is only from a single thread ever, so unsafe is preferred.
|
||||
.build();
|
||||
|
||||
static final NotifyAccessor accessor = new NotifyAccessor();
|
||||
static final TweenManager tweenManager = new TweenManager();
|
||||
private static final ActionHandlerLong frameStartHandler;
|
||||
|
||||
|
||||
|
||||
private static final java.awt.event.WindowAdapter windowListener = new WindowAdapter();
|
||||
private static final MouseAdapter mouseListener = new ClickAdapter();
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
LookAndFeel.animation.update(deltaInNanos);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static final int PADDING = 40;
|
||||
|
||||
private static final java.awt.event.WindowAdapter windowListener = new WindowAdapter();
|
||||
private static final MouseAdapter mouseListener = new ClickAdapter();
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final float MOVE_DURATION = Notify.MOVE_DURATION;
|
||||
|
||||
|
||||
|
||||
private volatile int anchorX;
|
||||
private volatile int anchorY;
|
||||
|
||||
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 boolean showCloseButton;
|
||||
private final BufferedImage cachedImage;
|
||||
|
||||
private final Window parent;
|
||||
private final NotifyCanvas notifyCanvas;
|
||||
|
||||
private final float hideAfterDurationInSeconds;
|
||||
private final Pos position;
|
||||
|
@ -114,42 +86,24 @@ class LookAndFeel {
|
|||
private volatile Tween tween = null;
|
||||
private volatile 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 ActionHandler<Notify> onCloseAction;
|
||||
|
||||
LookAndFeel(final Window parent, final Notify notification, final Image image, final ImageIcon imageIcon, final Rectangle parentBounds) {
|
||||
LookAndFeel(final Window parent,
|
||||
final NotifyCanvas notifyCanvas,
|
||||
final Notify notification,
|
||||
final Image image,
|
||||
final Rectangle parentBounds) {
|
||||
|
||||
this.parent = parent;
|
||||
this.notifyCanvas = notifyCanvas;
|
||||
|
||||
|
||||
parent.addWindowListener(windowListener);
|
||||
parent.addMouseListener(mouseListener);
|
||||
|
||||
if (notification.isDark) {
|
||||
panel_BG = Color.DARK_GRAY;
|
||||
titleText_FG = Color.GRAY;
|
||||
mainText_FG = Color.LIGHT_GRAY;
|
||||
closeX_FG = Color.GRAY;
|
||||
progress_FG = Color.gray;
|
||||
}
|
||||
else {
|
||||
panel_BG = Color.WHITE;
|
||||
titleText_FG = Color.GRAY.darker();
|
||||
mainText_FG = Color.GRAY;
|
||||
closeX_FG = Color.LIGHT_GRAY;
|
||||
progress_FG = new Color(0x42A5F5);
|
||||
}
|
||||
|
||||
hideAfterDurationInSeconds = notification.hideAfterDurationInMillis / 1000.0F;
|
||||
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) {
|
||||
onCloseAction = new ActionHandler<Notify>() {
|
||||
@Override
|
||||
|
@ -168,7 +122,6 @@ class LookAndFeel {
|
|||
anchorX = getAnchorX(position, parentBounds);
|
||||
anchorY = getAnchorY(position, parentBounds);
|
||||
|
||||
parent.setBackground(panel_BG);
|
||||
if (image != null) {
|
||||
parent.setIconImage(image);
|
||||
}
|
||||
|
@ -177,61 +130,10 @@ class LookAndFeel {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
g2.setColor(progress_FG);
|
||||
g2.fillRect(0, PROGRESS_HEIGHT, progress, 2);
|
||||
} finally {
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (hideTween != null) {
|
||||
hideTween.cancel();
|
||||
hideTween = null;
|
||||
}
|
||||
|
||||
if (tween != null) {
|
||||
tween.cancel();
|
||||
tween = null;
|
||||
}
|
||||
|
||||
parent.removeWindowListener(windowListener);
|
||||
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) {
|
||||
if (notifyCanvas.isCloseButton(x, y)) {
|
||||
// reasonable position for detecting mouse over
|
||||
((INotify)parent).close();
|
||||
}
|
||||
|
@ -257,15 +159,30 @@ class LookAndFeel {
|
|||
|
||||
int changedY;
|
||||
if (showFromTop) {
|
||||
changedY = anchorY + (popupIndex * (HEIGHT + 10));
|
||||
changedY = anchorY + (popupIndex * (NotifyCanvas.HEIGHT + 10));
|
||||
}
|
||||
else {
|
||||
changedY = anchorY - (popupIndex * (HEIGHT + 10));
|
||||
changedY = anchorY - (popupIndex * (NotifyCanvas.HEIGHT + 10));
|
||||
}
|
||||
|
||||
parent.setLocation(anchorX, changedY);
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (hideTween != null) {
|
||||
hideTween.cancel();
|
||||
hideTween = null;
|
||||
}
|
||||
|
||||
if (tween != null) {
|
||||
tween.cancel();
|
||||
tween = null;
|
||||
}
|
||||
|
||||
parent.removeWindowListener(windowListener);
|
||||
parent.removeMouseListener(mouseListener);
|
||||
}
|
||||
|
||||
void shake(final int durationInMillis, final int amplitude) {
|
||||
int i1 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude;
|
||||
int i2 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude;
|
||||
|
@ -294,47 +211,25 @@ class LookAndFeel {
|
|||
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);
|
||||
animation.to(this, NotifyAccessor.X_Y_POS, accessor, 0.05F)
|
||||
.targetRelative(i1, i2)
|
||||
.repeatAutoReverse(count, 0)
|
||||
.ease(TweenEquations.Linear)
|
||||
.start();
|
||||
}
|
||||
|
||||
void setY(final int y) {
|
||||
void setParentY(final int y) {
|
||||
parent.setLocation(parent.getX(), y);
|
||||
}
|
||||
|
||||
void setProgress(final int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
int getY() {
|
||||
int getParentY() {
|
||||
return parent.getY();
|
||||
}
|
||||
|
||||
int getX() {
|
||||
int getParentX() {
|
||||
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.
|
||||
|
@ -349,11 +244,11 @@ class LookAndFeel {
|
|||
return startX + PADDING;
|
||||
|
||||
case CENTER:
|
||||
return startX + (screenWidth / 2) - WIDTH / 2 - PADDING / 2;
|
||||
return startX + (screenWidth / 2) - NotifyCanvas.WIDTH / 2 - PADDING / 2;
|
||||
|
||||
case TOP_RIGHT:
|
||||
case BOTTOM_RIGHT:
|
||||
return startX + screenWidth - WIDTH - PADDING;
|
||||
return startX + screenWidth - NotifyCanvas.WIDTH - PADDING;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unknown position. '" + position + "'");
|
||||
|
@ -372,99 +267,22 @@ class LookAndFeel {
|
|||
return PADDING + startY;
|
||||
|
||||
case CENTER:
|
||||
return startY + (screenHeight / 2) - HEIGHT / 2 - PADDING / 2;
|
||||
return startY + (screenHeight / 2) - NotifyCanvas.HEIGHT / 2 - PADDING / 2;
|
||||
|
||||
case BOTTOM_LEFT:
|
||||
case BOTTOM_RIGHT:
|
||||
return startY + screenHeight - HEIGHT - PADDING;
|
||||
return startY + screenHeight - NotifyCanvas.HEIGHT - PADDING;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unknown position. '" + position + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
BufferedImage renderBackgroundInfo(final String title,
|
||||
final String notificationText,
|
||||
final Color titleText_FG,
|
||||
final Color mainText_FG,
|
||||
final Color panel_BG,
|
||||
final ImageIcon imageIcon) {
|
||||
|
||||
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = image.createGraphics();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
|
||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
|
||||
try {
|
||||
g2.setColor(panel_BG);
|
||||
g2.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
// Draw the title text
|
||||
java.awt.Font titleTextFont = FontUtil.parseFont(Notify.TITLE_TEXT_FONT);
|
||||
g2.setColor(titleText_FG);
|
||||
g2.setFont(titleTextFont);
|
||||
g2.drawString(title, 5, 20);
|
||||
|
||||
|
||||
int posX = 10;
|
||||
int posY = -8;
|
||||
int textLengthLimit = 108;
|
||||
|
||||
// ICON
|
||||
if (imageIcon != null) {
|
||||
textLengthLimit = 88;
|
||||
posX = 60;
|
||||
// Draw the image
|
||||
imageIcon.paintIcon(null, g2, 5, 30);
|
||||
}
|
||||
|
||||
// Draw the main text
|
||||
java.awt.Font mainTextFont = FontUtil.parseFont(Notify.MAIN_TEXT_FONT);
|
||||
int length = notificationText.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 && notificationText.regionMatches(true, length - 7, "</html>", 0, 7)) {
|
||||
text.append(notificationText);
|
||||
text.delete(text.length() - 7, text.length());
|
||||
|
||||
length -= 7;
|
||||
}
|
||||
else {
|
||||
text.append("<html>");
|
||||
text.append(notificationText);
|
||||
}
|
||||
|
||||
// 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());
|
||||
mainTextLabel.setBounds(0, 0, WIDTH - posX - 2, HEIGHT);
|
||||
|
||||
g2.translate(posX, posY);
|
||||
mainTextLabel.paint(g2);
|
||||
g2.translate(-posX, -posY);
|
||||
} finally {
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
return image;
|
||||
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) {
|
||||
|
@ -486,48 +304,37 @@ class LookAndFeel {
|
|||
int anchorY = sourceLook.anchorY;
|
||||
|
||||
if (isShowFromTop(sourceLook)) {
|
||||
targetY = anchorY + (popupIndex * (HEIGHT + 10));
|
||||
targetY = anchorY + (popupIndex * (NotifyCanvas.HEIGHT + 10));
|
||||
}
|
||||
else {
|
||||
targetY = anchorY - (popupIndex * (HEIGHT + 10));
|
||||
targetY = anchorY - (popupIndex * (NotifyCanvas.HEIGHT + 10));
|
||||
}
|
||||
|
||||
looks.add(sourceLook);
|
||||
sourceLook.setLocation(anchorX, targetY);
|
||||
sourceLook.setParentLocation(anchorX, targetY);
|
||||
|
||||
if (sourceLook.hideAfterDurationInSeconds > 0 && sourceLook.hideTween == null) {
|
||||
// begin a timeline to get rid of the popup (default is 5 seconds)
|
||||
Tween hideTween = Tween.to(sourceLook, NotifyAccessor.PROGRESS, accessor, sourceLook.hideAfterDurationInSeconds)
|
||||
.target(WIDTH)
|
||||
.ease(TweenEquations.Linear)
|
||||
.addCallback(new TweenCallback() {
|
||||
@Override
|
||||
public
|
||||
void onEvent(final int type, final BaseTween<?> source) {
|
||||
if (type == Events.COMPLETE) {
|
||||
((INotify)sourceLook.parent).close();
|
||||
}
|
||||
}
|
||||
});
|
||||
tweenManager.add(hideTween);
|
||||
animation.to(sourceLook, NotifyAccessor.PROGRESS, accessor, sourceLook.hideAfterDurationInSeconds)
|
||||
.target(NotifyCanvas.WIDTH)
|
||||
.ease(TweenEquations.Linear)
|
||||
.addCallback(new TweenCallback() {
|
||||
@Override
|
||||
public
|
||||
void onEvent(final int type, final BaseTween<?> source) {
|
||||
if (type == Events.COMPLETE) {
|
||||
((INotify) sourceLook.parent).close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
// start if we have stopped the timer
|
||||
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 removePopupFromMap(final LookAndFeel sourceLook) {
|
||||
boolean showFromTop = isShowFromTop(sourceLook);
|
||||
boolean popupsAreEmpty;
|
||||
|
||||
|
@ -565,41 +372,31 @@ class LookAndFeel {
|
|||
// popups at TOP grow down, popups at BOTTOM grow up
|
||||
int changedY;
|
||||
if (showFromTop) {
|
||||
changedY = look.anchorY + (look.popupIndex * (HEIGHT + 10));
|
||||
changedY = look.anchorY + (look.popupIndex * (NotifyCanvas.HEIGHT + 10));
|
||||
}
|
||||
else {
|
||||
changedY = look.anchorY - (look.popupIndex * (HEIGHT + 10));
|
||||
changedY = look.anchorY - (look.popupIndex * (NotifyCanvas.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);
|
||||
look.tween = animation.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;
|
||||
}
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
return popupsAreEmpty;
|
||||
}
|
||||
|
||||
private static
|
||||
|
@ -613,4 +410,45 @@ class LookAndFeel {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setProgress(final int progress) {
|
||||
notifyCanvas.setProgress(progress);
|
||||
}
|
||||
|
||||
int getProgress() {
|
||||
return notifyCanvas.getProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* we have to remove the active renderer BEFORE we set the visibility status.
|
||||
*/
|
||||
void updatePositionsPre(final boolean visible) {
|
||||
if (!visible) {
|
||||
boolean popupsAreEmpty = LookAndFeel.removePopupFromMap(this);
|
||||
SwingActiveRender.removeActiveRender(notifyCanvas);
|
||||
|
||||
if (popupsAreEmpty) {
|
||||
// if there's nothing left, stop the timer.
|
||||
SwingActiveRender.removeActiveRenderFrameStart(frameStartHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* when using active rendering, we have to add it AFTER we have set the visibility status
|
||||
*/
|
||||
void updatePositionsPost(final boolean visible) {
|
||||
if (visible) {
|
||||
|
||||
SwingActiveRender.addActiveRender(notifyCanvas);
|
||||
|
||||
// start if we have previously stopped the timer
|
||||
if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) {
|
||||
LookAndFeel.animation.resetUpdateTime();
|
||||
SwingActiveRender.addActiveRenderFrameStart(frameStartHandler);
|
||||
}
|
||||
|
||||
LookAndFeel.addPopupToMap(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,6 +150,8 @@ class Notify {
|
|||
String title;
|
||||
String text;
|
||||
|
||||
Theme theme;
|
||||
|
||||
Pos position = Pos.BOTTOM_RIGHT;
|
||||
int hideAfterDurationInMillis = 0;
|
||||
|
||||
|
@ -238,6 +240,15 @@ class Notify {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies what the theme should be, if other than the default. This will always take precedence over the defaults.
|
||||
*/
|
||||
public
|
||||
Notify text(Theme theme) {
|
||||
this.theme = theme;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the close button in the top-right corner of the notification should not be shown.
|
||||
*/
|
||||
|
@ -320,10 +331,18 @@ class Notify {
|
|||
}
|
||||
}
|
||||
|
||||
if (window == null) {
|
||||
notifyPopup = new AsFrame(notify, graphic, imageIcon);
|
||||
Theme theme;
|
||||
if (notify.theme != null) {
|
||||
// use custom theme.
|
||||
theme = notify.theme;
|
||||
} else {
|
||||
notifyPopup = new AsDialog(notify, graphic, imageIcon, window);
|
||||
theme = new Theme(Notify.TITLE_TEXT_FONT, Notify.MAIN_TEXT_FONT, notify.isDark);
|
||||
}
|
||||
|
||||
if (window == null) {
|
||||
notifyPopup = new AsFrame(notify, graphic, imageIcon, theme);
|
||||
} else {
|
||||
notifyPopup = new AsDialog(notify, graphic, imageIcon, window, theme);
|
||||
}
|
||||
|
||||
notifyPopup.setVisible(true);
|
||||
|
|
|
@ -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.getY();
|
||||
returnValues[0] = (float) target.getParentY();
|
||||
return 1;
|
||||
case X_Y_POS:
|
||||
returnValues[0] = (float) target.getX();
|
||||
returnValues[1] = (float) target.getY();
|
||||
returnValues[0] = (float) target.getParentX();
|
||||
returnValues[1] = (float) target.getParentY();
|
||||
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.setY((int) newValues[0]);
|
||||
target.setParentY((int) newValues[0]);
|
||||
return;
|
||||
case X_Y_POS:
|
||||
target.setLocation((int) newValues[0], (int) newValues[1]);
|
||||
target.setParentLocation((int) newValues[0], (int) newValues[1]);
|
||||
return;
|
||||
case PROGRESS:
|
||||
target.setProgress((int) newValues[0]);
|
||||
|
|
189
src/dorkbox/notify/NotifyCanvas.java
Normal file
189
src/dorkbox/notify/NotifyCanvas.java
Normal file
|
@ -0,0 +1,189 @@
|
|||
package dorkbox.notify;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Canvas;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
class NotifyCanvas extends Canvas {
|
||||
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;
|
||||
|
||||
static final int WIDTH = 300;
|
||||
static final int HEIGHT = 87;
|
||||
private static final int PROGRESS_HEIGHT = HEIGHT - 2;
|
||||
|
||||
private final boolean showCloseButton;
|
||||
private final BufferedImage cachedImage;
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
NotifyCanvas(final Notify notification, final ImageIcon imageIcon, final Theme theme) {
|
||||
this.theme = theme;
|
||||
|
||||
final Dimension preferredSize = new Dimension(WIDTH, HEIGHT);
|
||||
setPreferredSize(preferredSize);
|
||||
setMaximumSize(preferredSize);
|
||||
setMinimumSize(preferredSize);
|
||||
setSize(WIDTH, HEIGHT);
|
||||
|
||||
setFocusable(false);
|
||||
|
||||
setBackground(this.theme.panel_BG);
|
||||
showCloseButton = !notification.hideCloseButton;
|
||||
|
||||
// now we setup the rendering of the image
|
||||
cachedImage = renderBackgroundInfo(notification.title, notification.text, this.theme, imageIcon);
|
||||
}
|
||||
|
||||
void setProgress(final int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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(theme.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(theme.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(theme.progress_FG);
|
||||
g2.fillRect(0, PROGRESS_HEIGHT, progress, 2);
|
||||
} finally {
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TRUE if we were over the 'X' or FALSE if the click was in the general area (and not over the 'X').
|
||||
*/
|
||||
boolean isCloseButton(final int x, final int y) {
|
||||
return showCloseButton && x >= 280 && y <= 20;
|
||||
}
|
||||
|
||||
private static
|
||||
BufferedImage renderBackgroundInfo(final String title,
|
||||
final String notificationText,
|
||||
final Theme theme,
|
||||
final ImageIcon imageIcon) {
|
||||
|
||||
|
||||
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = image.createGraphics();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
|
||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
|
||||
try {
|
||||
g2.setColor(theme.panel_BG);
|
||||
g2.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
// Draw the title text
|
||||
g2.setColor(theme.titleText_FG);
|
||||
g2.setFont(theme.titleTextFont);
|
||||
g2.drawString(title, 5, 20);
|
||||
|
||||
|
||||
int posX = 10;
|
||||
int posY = -8;
|
||||
int textLengthLimit = 108;
|
||||
|
||||
// ICON
|
||||
if (imageIcon != null) {
|
||||
textLengthLimit = 88;
|
||||
posX = 60;
|
||||
// Draw the image
|
||||
imageIcon.paintIcon(null, g2, 5, 30);
|
||||
}
|
||||
|
||||
// Draw the main text
|
||||
int length = notificationText.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 && notificationText.regionMatches(true, length - 7, "</html>", 0, 7)) {
|
||||
text.append(notificationText);
|
||||
text.delete(text.length() - 7, text.length());
|
||||
|
||||
length -= 7;
|
||||
}
|
||||
else {
|
||||
text.append("<html>");
|
||||
text.append(notificationText);
|
||||
}
|
||||
|
||||
// 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(theme.mainText_FG);
|
||||
mainTextLabel.setFont(theme.mainTextFont);
|
||||
mainTextLabel.setText(text.toString());
|
||||
mainTextLabel.setBounds(0, 0, WIDTH - posX - 2, HEIGHT);
|
||||
|
||||
g2.translate(posX, posY);
|
||||
mainTextLabel.paint(g2);
|
||||
g2.translate(-posX, -posY);
|
||||
} finally {
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
55
src/dorkbox/notify/Theme.java
Normal file
55
src/dorkbox/notify/Theme.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
package dorkbox.notify;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
|
||||
import dorkbox.util.FontUtil;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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;
|
||||
|
||||
public final Font titleTextFont;
|
||||
public final Font mainTextFont;
|
||||
|
||||
|
||||
Theme(final String titleTextFont, final String mainTextFont, boolean isDarkTheme) {
|
||||
this.titleTextFont = FontUtil.parseFont(titleTextFont);
|
||||
this.mainTextFont = FontUtil.parseFont(mainTextFont);
|
||||
|
||||
if (isDarkTheme) {
|
||||
panel_BG = Color.DARK_GRAY;
|
||||
titleText_FG = Color.GRAY;
|
||||
mainText_FG = Color.LIGHT_GRAY;
|
||||
closeX_FG = Color.GRAY;
|
||||
progress_FG = Color.gray;
|
||||
}
|
||||
else {
|
||||
panel_BG = Color.WHITE;
|
||||
titleText_FG = Color.GRAY.darker();
|
||||
mainText_FG = Color.GRAY;
|
||||
closeX_FG = Color.LIGHT_GRAY;
|
||||
progress_FG = new Color(0x42A5F5);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this.titleTextFont = FontUtil.parseFont(titleTextFont);
|
||||
this.mainTextFont = FontUtil.parseFont(mainTextFont);
|
||||
|
||||
this.panel_BG = panel_BG;
|
||||
this.titleText_FG = titleText_FG;
|
||||
this.mainText_FG = mainText_FG;
|
||||
this.closeX_FG = closeX_FG;
|
||||
this.progress_FG = progress_FG;
|
||||
}
|
||||
}
|
|
@ -64,7 +64,8 @@ class NotifyTest {
|
|||
.text("This is a notification " + i + " popup message This is a notification popup message This is a " +
|
||||
"notification popup message")
|
||||
.hideAfter(13000)
|
||||
.position(Pos.CENTER)
|
||||
.position(Pos.BOTTOM_RIGHT)
|
||||
// .position(Pos.CENTER)
|
||||
// .setScreen(0)
|
||||
.darkStyle()
|
||||
// .shake(1300, 4)
|
||||
|
@ -93,7 +94,7 @@ 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(3000)
|
||||
.position(Pos.TOP_RIGHT)
|
||||
// .setScreen(0)
|
||||
.darkStyle()
|
||||
|
|
Loading…
Reference in New Issue
Block a user