From cdbf8a2a4d8f09f726470746027f9a6cfae88865 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 23 Aug 2017 11:08:22 +0200 Subject: [PATCH] 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. --- src/dorkbox/notify/AsDialog.java | 36 ++- src/dorkbox/notify/AsFrame.java | 39 ++- src/dorkbox/notify/LookAndFeel.java | 432 ++++++++----------------- src/dorkbox/notify/Notify.java | 25 +- src/dorkbox/notify/NotifyAccessor.java | 10 +- src/dorkbox/notify/NotifyCanvas.java | 189 +++++++++++ src/dorkbox/notify/Theme.java | 55 ++++ test/NotifyTest.java | 5 +- 8 files changed, 455 insertions(+), 336 deletions(-) create mode 100644 src/dorkbox/notify/NotifyCanvas.java create mode 100644 src/dorkbox/notify/Theme.java diff --git a/src/dorkbox/notify/AsDialog.java b/src/dorkbox/notify/AsDialog.java index 5d14300..602493b 100644 --- a/src/dorkbox/notify/AsDialog.java +++ b/src/dorkbox/notify/AsDialog.java @@ -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 diff --git a/src/dorkbox/notify/AsFrame.java b/src/dorkbox/notify/AsFrame.java index c4312e5..ba14616 100644 --- a/src/dorkbox/notify/AsFrame.java +++ b/src/dorkbox/notify/AsFrame.java @@ -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 diff --git a/src/dorkbox/notify/LookAndFeel.java b/src/dorkbox/notify/LookAndFeel.java index 889ee1c..05a6d46 100644 --- a/src/dorkbox/notify/LookAndFeel.java +++ b/src/dorkbox/notify/LookAndFeel.java @@ -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> popups = new HashMap>(); + 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 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() { @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, "", 0, 7)) { - text.append(notificationText); - text.delete(text.length() - 7, text.length()); - - length -= 7; - } - else { - text.append(""); - text.append(notificationText); - } - - // make sure the text is the correct length - if (length > textLengthLimit) { - text.delete(6 + textLengthLimit, text.length()); - text.append("..."); - } - text.append(""); - - 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); + } + } } diff --git a/src/dorkbox/notify/Notify.java b/src/dorkbox/notify/Notify.java index 8a1c2eb..3042a27 100644 --- a/src/dorkbox/notify/Notify.java +++ b/src/dorkbox/notify/Notify.java @@ -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); diff --git a/src/dorkbox/notify/NotifyAccessor.java b/src/dorkbox/notify/NotifyAccessor.java index a9c928b..3a1a30f 100644 --- a/src/dorkbox/notify/NotifyAccessor.java +++ b/src/dorkbox/notify/NotifyAccessor.java @@ -32,11 +32,11 @@ class NotifyAccessor implements TweenAccessor { 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 { 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]); diff --git a/src/dorkbox/notify/NotifyCanvas.java b/src/dorkbox/notify/NotifyCanvas.java new file mode 100644 index 0000000..adb1e4b --- /dev/null +++ b/src/dorkbox/notify/NotifyCanvas.java @@ -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, "", 0, 7)) { + text.append(notificationText); + text.delete(text.length() - 7, text.length()); + + length -= 7; + } + else { + text.append(""); + text.append(notificationText); + } + + // make sure the text is the correct length + if (length > textLengthLimit) { + text.delete(6 + textLengthLimit, text.length()); + text.append("..."); + } + text.append(""); + + 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; + } +} diff --git a/src/dorkbox/notify/Theme.java b/src/dorkbox/notify/Theme.java new file mode 100644 index 0000000..8d8b82f --- /dev/null +++ b/src/dorkbox/notify/Theme.java @@ -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; + } +} diff --git a/test/NotifyTest.java b/test/NotifyTest.java index 171a4bb..421c104 100644 --- a/test/NotifyTest.java +++ b/test/NotifyTest.java @@ -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()