diff --git a/src/dorkbox/notify/AsDialog.java b/src/dorkbox/notify/AsDialog.java new file mode 100644 index 0000000..5d14300 --- /dev/null +++ b/src/dorkbox/notify/AsDialog.java @@ -0,0 +1,170 @@ +/* + * Copyright 2015 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.notify; + +import java.awt.Dialog; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Window; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowStateListener; + +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.JFrame; + +import dorkbox.util.SwingUtil; + +// this is a child to a Jframe/window (instead of globally to the screen) +@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"}) +public +class AsDialog extends JDialog implements INotify { + private static final long serialVersionUID = 1L; + + private final LookAndFeel look; + private final Notify notification; + private final ComponentListener parentListener; + + // this is on the swing EDT + @SuppressWarnings("NumericCastThatLosesPrecision") + AsDialog(final Notify notification, final Image image, final ImageIcon imageIcon, final Window container) { + super(container, Dialog.ModalityType.MODELESS); + this.notification = notification; + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + setUndecorated(true); + setLayout(null); + + setSize(LookAndFeel.WIDTH, LookAndFeel.HEIGHT); + setLocation(Short.MIN_VALUE, Short.MIN_VALUE); + + setTitle(notification.title); + setResizable(false); + + look = new LookAndFeel(this, notification, image, imageIcon, container.getBounds()); + + parentListener = new ComponentListener() { + @Override + public + void componentShown(final ComponentEvent e) { + AsDialog.this.setVisible(true); + look.reLayout(container.getBounds()); + } + + @Override + public + void componentHidden(final ComponentEvent e) { + AsDialog.this.setVisible(false); + } + + @Override + public + void componentResized(final ComponentEvent e) { + look.reLayout(container.getBounds()); + } + + @Override + public + void componentMoved(final ComponentEvent e) { + look.reLayout(container.getBounds()); + } + }; + + container.addWindowStateListener(new WindowStateListener() { + @Override + public + void windowStateChanged(WindowEvent e) { + int state = e.getNewState(); + if ((state & Frame.ICONIFIED) != 0) { + setVisible(false); + } + else { + setVisible(true); + look.reLayout(container.getBounds()); + } + } + }); + + container.addComponentListener(parentListener); + } + + @Override + public + void paint(Graphics g) { + look.paint(g); + } + + @Override + public + void onClick(final int x, final int y) { + look.onClick(x, y); + } + + /** + * Shakes the popup + * + * @param durationInMillis now long it will shake + * @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot. + */ + @Override + public + void shake(final int durationInMillis, final int amplitude) { + look.shake(durationInMillis, amplitude); + } + + @Override + public + void setVisible(final boolean b) { + // was it already visible? + if (b == isVisible()) { + // prevent "double setting" visible state + return; + } + + super.setVisible(b); + look.setVisible(b); + } + + @Override + public + void close() { + // this must happen in the Swing EDT. This is usually called by the active renderer + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + // set it off screen (which is what the close method also does) + if (isVisible()) { + setVisible(false); + } + + look.close(); + + if (parentListener != null) { + removeComponentListener(parentListener); + } + setIconImage(null); + removeAll(); + dispose(); + + notification.onClose(); + } + }); + } +} diff --git a/src/dorkbox/notify/AsFrame.java b/src/dorkbox/notify/AsFrame.java new file mode 100644 index 0000000..90e31c0 --- /dev/null +++ b/src/dorkbox/notify/AsFrame.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.notify; + +import java.awt.Graphics; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Rectangle; + +import javax.swing.ImageIcon; +import javax.swing.JFrame; + +import dorkbox.util.ScreenUtil; +import dorkbox.util.SwingUtil; + +// we can't use regular popup, because if we have no owner, it won't work! +// instead, we just create a JFrame and use it to hold our content +@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"}) +public +class AsFrame extends JFrame implements INotify { + private static final long serialVersionUID = 1L; + + private final LookAndFeel look; + private final Notify notification; + + + // this is on the swing EDT + @SuppressWarnings("NumericCastThatLosesPrecision") + AsFrame(final Notify notification, final Image image, final ImageIcon imageIcon) { + this.notification = notification; + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + setUndecorated(true); + setAlwaysOnTop(true); + setLayout(null); + + setSize(LookAndFeel.WIDTH, LookAndFeel.HEIGHT); + setLocation(Short.MIN_VALUE, Short.MIN_VALUE); + + setTitle(notification.title); + setResizable(false); + + Rectangle bounds; + GraphicsDevice device; + + if (notification.screenNumber == Short.MIN_VALUE) { + // set screen position based on mouse + Point mouseLocation = MouseInfo.getPointerInfo() + .getLocation(); + + device = ScreenUtil.getGraphicsDeviceAt(mouseLocation); + } + else { + // set screen position based on specified screen + int screenNumber = notification.screenNumber; + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice screenDevices[] = ge.getScreenDevices(); + + if (screenNumber < 0) { + screenNumber = 0; + } + else if (screenNumber > screenDevices.length - 1) { + screenNumber = screenDevices.length - 1; + } + + device = screenDevices[screenNumber]; + } + + bounds = device.getDefaultConfiguration() + .getBounds(); + + look = new LookAndFeel(this, notification, image, imageIcon, bounds); + } + + @Override + public + void paint(Graphics g) { + look.paint(g); + } + + @Override + public + void onClick(final int x, final int y) { + look.onClick(x, y); + } + + /** + * Shakes the popup + * + * @param durationInMillis now long it will shake + * @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot. + */ + @Override + public + void shake(final int durationInMillis, final int amplitude) { + look.shake(durationInMillis, amplitude); + } + + @Override + public + void setVisible(final boolean b) { + // was it already visible? + if (b == isVisible()) { + // prevent "double setting" visible state + return; + } + + super.setVisible(b); + look.setVisible(b); + } + + @Override + public + void close() { + // this must happen in the Swing EDT. This is usually called by the active renderer + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + // set it off screen (which is what the close method also does) + if (isVisible()) { + setVisible(false); + } + + look.close(); + + setIconImage(null); + removeAll(); + dispose(); + + notification.onClose(); + } + }); + } +} diff --git a/src/dorkbox/notify/NotifyPopupClickAdapter.java b/src/dorkbox/notify/ClickAdapter.java similarity index 84% rename from src/dorkbox/notify/NotifyPopupClickAdapter.java rename to src/dorkbox/notify/ClickAdapter.java index 07250d8..8799991 100644 --- a/src/dorkbox/notify/NotifyPopupClickAdapter.java +++ b/src/dorkbox/notify/ClickAdapter.java @@ -18,16 +18,15 @@ package dorkbox.notify; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -class NotifyPopupClickAdapter extends MouseAdapter { +class ClickAdapter extends MouseAdapter { - public - NotifyPopupClickAdapter() { + ClickAdapter() { } @Override public void mouseReleased(final MouseEvent e) { - NotifyPopup source = (NotifyPopup) e.getSource(); + INotify source = (INotify) e.getSource(); source.onClick(e.getX(), e.getY()); } } diff --git a/src/dorkbox/notify/INotify.java b/src/dorkbox/notify/INotify.java new file mode 100644 index 0000000..5632984 --- /dev/null +++ b/src/dorkbox/notify/INotify.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.notify; + +public +interface INotify { + void close(); + + void shake(int durationInMillis, int amplitude); + + void setVisible(boolean b); + + void onClick(int x, int y); +} diff --git a/src/dorkbox/notify/NotifyPopup.java b/src/dorkbox/notify/LookAndFeel.java similarity index 55% rename from src/dorkbox/notify/NotifyPopup.java rename to src/dorkbox/notify/LookAndFeel.java index 6a875a6..889ee1c 100644 --- a/src/dorkbox/notify/NotifyPopup.java +++ b/src/dorkbox/notify/LookAndFeel.java @@ -19,16 +19,13 @@ import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; import java.awt.Image; -import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; +import java.awt.Window; import java.awt.event.MouseAdapter; -import java.awt.event.WindowAdapter; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.HashMap; @@ -37,7 +34,6 @@ import java.util.Map; import java.util.Random; import javax.swing.ImageIcon; -import javax.swing.JFrame; import javax.swing.JLabel; import dorkbox.tweenengine.BaseTween; @@ -48,52 +44,21 @@ import dorkbox.tweenengine.TweenManager; import dorkbox.util.ActionHandler; import dorkbox.util.ActionHandlerLong; import dorkbox.util.FontUtil; -import dorkbox.util.Property; -import dorkbox.util.ScreenUtil; import dorkbox.util.SwingUtil; import dorkbox.util.swing.SwingActiveRender; -// we can't use regular popup, because if we have no owner, it won't work! -// instead, we just create a JFrame and use it to hold our content -@SuppressWarnings({"Duplicates", "FieldCanBeLocal", "WeakerAccess", "DanglingJavadoc"}) -public -class NotifyPopup extends JFrame { - private static final long serialVersionUID = 1L; +@SuppressWarnings({"FieldCanBeLocal"}) +class LookAndFeel { + private static final Map> popups = new HashMap>(); - @Property - /** This is the title font used by a notification. */ - public static String TITLE_TEXT_FONT = "Source Code Pro BOLD 16"; - - @Property - /** This is the main text font used by a notification. */ - public static String MAIN_TEXT_FONT = "Source Code Pro BOLD 12"; - - @Property - /** How long we want it to take for the popups to relocate when one is closed */ - public static float MOVE_DURATION = 1.0F; - - private static final int padding = 40; - - private static final Map> popups = new HashMap>(); - - private static final NotifyPopupAccessor accessor = new NotifyPopupAccessor(); - private static final TweenManager tweenManager = new TweenManager(); + static final NotifyAccessor accessor = new NotifyAccessor(); + static final TweenManager tweenManager = new TweenManager(); private static final ActionHandlerLong frameStartHandler; - static { - // this is for updating the tween engine during active-rendering - frameStartHandler = new ActionHandlerLong() { - @Override - public - void handle(final long deltaInNanos) { - NotifyPopup.tweenManager.update(deltaInNanos); - } - }; - } - private static final int WIDTH = 300; - private static final int HEIGHT = 87; - private static final int PROGRESS_HEIGHT = HEIGHT - 1; + + 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; @@ -104,28 +69,46 @@ class NotifyPopup extends JFrame { 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); + } + }; + } + + 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 int anchorX; - private final int anchorY; - - private static final WindowAdapter windowListener = new NotifyPopupWindowAdapter(); - private static final MouseAdapter mouseListener = new NotifyPopupClickAdapter(); - - private final Notify notification; + private final Window parent; private final float hideAfterDurationInSeconds; private final Pos position; - private final ActionHandler onCloseAction; // this is used in combination with position, so that we can track which screen and what position a popup is in private final String idAndPosition; - private int popupIndex; private volatile Tween tween = null; @@ -135,39 +118,13 @@ class NotifyPopup extends JFrame { // non-volatile because it's always accessed in the active render thread private int progress = 0; - private final boolean showCloseButton; - private final BufferedImage cachedImage; - private static final Random RANDOM = new Random(); + private final ActionHandler onCloseAction; + LookAndFeel(final Window parent, final Notify notification, final Image image, final ImageIcon imageIcon, final Rectangle parentBounds) { + this.parent = parent; - - // this is on the swing EDT - @SuppressWarnings("NumericCastThatLosesPrecision") - NotifyPopup(final Notify notification, final Image image, final ImageIcon imageIcon) { - this.notification = notification; - - setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - setUndecorated(true); - setAlwaysOnTop(false); - setAlwaysOnTop(true); - setLayout(null); - - setSize(WIDTH, HEIGHT); - setLocation(Short.MIN_VALUE, Short.MIN_VALUE); - - setTitle(notification.title); - setResizable(false); - - if (image != null) { - setIconImage(image); - } else { - setIconImage(SwingUtil.BLANK_ICON); - } - - - addWindowListener(windowListener); - addMouseListener(mouseListener); - + parent.addWindowListener(windowListener); + parent.addMouseListener(mouseListener); if (notification.isDark) { panel_BG = Color.DARK_GRAY; @@ -184,11 +141,15 @@ class NotifyPopup extends JFrame { progress_FG = new Color(0x42A5F5); } - setBackground(panel_BG); - showCloseButton = !notification.hideCloseButton; 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 @@ -197,91 +158,229 @@ class NotifyPopup extends JFrame { notification.onCloseAction.handle(notification); } }; - } else { + } + else { onCloseAction = null; } - GraphicsDevice device; - if (notification.screenNumber == Short.MIN_VALUE) { - // set screen position based on mouse - Point mouseLocation = MouseInfo.getPointerInfo() - .getLocation(); + idAndPosition = parentBounds.x + ":" + parentBounds.y + ":" + parentBounds.width + ":" + parentBounds.height + ":" + position; - device = ScreenUtil.getGraphicsDeviceAt(mouseLocation); + anchorX = getAnchorX(position, parentBounds); + anchorY = getAnchorY(position, parentBounds); + + parent.setBackground(panel_BG); + if (image != null) { + parent.setIconImage(image); } else { - // set screen position based on specified screen - int screenNumber = notification.screenNumber; - GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice screenDevices[] = ge.getScreenDevices(); + parent.setIconImage(SwingUtil.BLANK_ICON); + } + } - if (screenNumber < 0) { - screenNumber = 0; - } - else if (screenNumber > screenDevices.length - 1) { - screenNumber = screenDevices.length - 1; + 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); } - device = screenDevices[screenNumber]; + g2.setColor(progress_FG); + g2.fillRect(0, PROGRESS_HEIGHT, progress, 2); + } finally { + g2.dispose(); + } + } + + void close() { + if (hideTween != null) { + hideTween.cancel(); + hideTween = null; } - idAndPosition = device.getIDstring() + notification.position; + if (tween != null) { + tween.cancel(); + tween = null; + } - Rectangle screenBounds = device.getDefaultConfiguration() - .getBounds(); + 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) { + // reasonable position for detecting mouse over + ((INotify)parent).close(); + } + else { + if (onCloseAction != null) { + onCloseAction.handle(null); + } + ((INotify) parent).close(); + } + } + + void reLayout(final Rectangle bounds) { + // when the parent window moves, we stop all animation and snap the popup into place. This simplifies logic greatly + anchorX = getAnchorX(position, bounds); + anchorY = getAnchorY(position, bounds); + + boolean showFromTop = isShowFromTop(this); + + if (tween != null) { + tween.cancel(); // cancel does it's thing on the next tick of animation cycle + tween = null; + } + + int changedY; + if (showFromTop) { + changedY = anchorY + (popupIndex * (HEIGHT + 10)); + } + else { + changedY = anchorY - (popupIndex * (HEIGHT + 10)); + } + + parent.setLocation(anchorX, changedY); + } + + void shake(final int durationInMillis, final int amplitude) { + int i1 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude; + int i2 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude; + + i1 = i1 >> 2; + i2 = i2 >> 2; + + // make sure it always moves by some amount + if (i1 < 0) { + i1 -= amplitude >> 2; + } + else { + i1 += amplitude >> 2; + } + + if (i2 < 0) { + i2 -= amplitude >> 2; + } + else { + i2 += amplitude >> 2; + } + + int count = durationInMillis / 50; + // make sure we always end the animation where we start + if ((count & 1) == 0) { + count++; + } + + Tween tween = Tween.to(this, NotifyAccessor.X_Y_POS, accessor, 0.05F) + .targetRelative(i1, i2) + .repeatAutoReverse(count, 0) + .ease(TweenEquations.Linear); + tweenManager.add(tween); + } + + void setY(final int y) { + parent.setLocation(parent.getX(), y); + } + + void setProgress(final int progress) { + this.progress = progress; + } + + int getProgress() { + return progress; + } + + int getY() { + return parent.getY(); + } + + int getX() { + return parent.getX(); + } + + void setVisible(final boolean visible) { + if (visible) { + parent.toFront(); + + // set this jframe to use active rendering + SwingActiveRender.addActiveRender(parent); + addPopupToMap(this); + } + else { + removePopupFromMap(this); + SwingActiveRender.removeActiveRender(parent); + } + } + + private static + int getAnchorX(final Pos position, final Rectangle bounds) { // we use the screen that the mouse is currently on. - final int startX = (int) screenBounds.getX(); - final int startY = (int) screenBounds.getY(); - final int screenWidth = (int) screenBounds.getWidth(); - final int screenHeight = (int) screenBounds.getHeight(); - + final int startX = (int) bounds.getX(); + final int screenWidth = (int) bounds.getWidth(); // determine location for the popup - final Pos position = notification.position; - // get anchorX switch (position) { case TOP_LEFT: case BOTTOM_LEFT: - anchorX = startX + padding; - break; + return startX + PADDING; case CENTER: - anchorX = startX + (screenWidth / 2) - WIDTH / 2 - padding / 2; - break; + return startX + (screenWidth / 2) - WIDTH / 2 - PADDING / 2; case TOP_RIGHT: case BOTTOM_RIGHT: - anchorX = startX + screenWidth - WIDTH - padding; - break; + return startX + screenWidth - WIDTH - PADDING; default: throw new RuntimeException("Unknown position. '" + position + "'"); } + } + + private static + int getAnchorY(final Pos position, final Rectangle bounds) { + final int startY = (int) bounds.getY(); + final int screenHeight = (int) bounds.getHeight(); // get anchorY switch (position) { case TOP_LEFT: case TOP_RIGHT: - anchorY = padding + startY; - break; + return PADDING + startY; case CENTER: - anchorY = startY + (screenHeight / 2) - HEIGHT / 2 - padding / 2; - break; + return startY + (screenHeight / 2) - HEIGHT / 2 - PADDING / 2; case BOTTOM_LEFT: case BOTTOM_RIGHT: - anchorY = startY + screenHeight - HEIGHT - padding; - break; + return startY + screenHeight - HEIGHT - PADDING; default: throw new RuntimeException("Unknown position. '" + position + "'"); } - - // now we setup the rendering of the image - cachedImage = renderBackgroundInfo(notification.title, notification.text, titleText_FG, mainText_FG, panel_BG, imageIcon); } private static @@ -303,14 +402,12 @@ class NotifyPopup extends JFrame { g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); - // g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); - try { g2.setColor(panel_BG); g2.fillRect(0, 0, WIDTH, HEIGHT); // Draw the title text - java.awt.Font titleTextFont = FontUtil.parseFont(TITLE_TEXT_FONT); + java.awt.Font titleTextFont = FontUtil.parseFont(Notify.TITLE_TEXT_FONT); g2.setColor(titleText_FG); g2.setFont(titleTextFont); g2.drawString(title, 5, 20); @@ -329,12 +426,12 @@ class NotifyPopup extends JFrame { } // Draw the main text - java.awt.Font mainTextFont = FontUtil.parseFont(MAIN_TEXT_FONT); + java.awt.Font mainTextFont = FontUtil.parseFont(Notify.MAIN_TEXT_FONT); int length = notificationText.length(); 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)) { + if (length >= 13 && notificationText.regionMatches(true, length - 7, "", 0, 7)) { text.append(notificationText); text.delete(text.length() - 7, text.length()); @@ -368,235 +465,146 @@ class NotifyPopup extends JFrame { return image; } - @Override - public - void paint(Graphics g) { - // we cache the text + image (to another image), and then always render the close + progressbar - - // use our cached image, so we don't have to re-render text/background/etc - g.drawImage(cachedImage, 0, 0, null); - - // the progress bar and close button are the only things that can change, so we always draw them every time - Graphics2D g2 = (Graphics2D) g.create(); - try { - if (showCloseButton) { - Graphics2D g3 = (Graphics2D) g.create(); - - g3.setColor(panel_BG); - g3.setStroke(stroke); - - final Point p = getMousePosition(); - // reasonable position for detecting mouse over - if (p != null && p.getX() >= 280 && p.getY() <= 20) { - g3.setColor(Color.RED); - } else { - g3.setColor(closeX_FG); - } - - // draw the X - g3.drawLine(X_1, Y_1, X_2, Y_2); - g3.drawLine(X_2, Y_1, X_1, Y_2); - } - - g2.setColor(progress_FG); - g2.fillRect(0, PROGRESS_HEIGHT, progress, 1); - } finally { - g2.dispose(); - } - } - - public - void onClick(final int x, final int y) { - // Check - we were over the 'X' (and thus no notify), or was it in the general area? - - if (showCloseButton && x >= 280 && y <= 20) { - // reasonable position for detecting mouse over - close(); - } - else { - if (onCloseAction != null) { - onCloseAction.handle(null); - } - close(); - } - } - - @Override - public - void setVisible(final boolean b) { - // was it already visible? - if (b == isVisible()) { - // prevent "double setting" visible state - return; - } - - super.setVisible(b); - - if (b) { - toFront(); - - // set this jframe to use active rendering - SwingActiveRender.addActiveRender(this); - addPopupToMap(); - } - else { - removePopupFromMap(); - SwingActiveRender.removeActiveRender(this); - } - } - - - public - void close() { - // this must happen in the Swing EDT. This is usually called by the active renderer - SwingUtil.invokeLater(new Runnable() { - @Override - public - void run() { - // set it off screen (which is what the close method also does) - if (isVisible()) { - setVisible(false); - } - - removeAll(); - removeWindowListener(windowListener); - removeMouseListener(mouseListener); - setIconImage(null); - dispose(); - - notification.onClose(); - } - }); - } - - // only called on the swing EDT thread - void addPopupToMap() { + private static + void addPopupToMap(final LookAndFeel sourceLook) { synchronized (popups) { - ArrayList notifyPopups = popups.get(idAndPosition); - if (notifyPopups == null) { - notifyPopups = new ArrayList(4); - popups.put(idAndPosition, notifyPopups); + String id = sourceLook.idAndPosition; + + ArrayList looks = popups.get(id); + if (looks == null) { + looks = new ArrayList(4); + popups.put(id, looks); } - final int popupIndex = notifyPopups.size(); - this.popupIndex = popupIndex; + final int popupIndex = looks.size(); + sourceLook.popupIndex = popupIndex; // the popups are ALL the same size! // popups at TOP grow down, popups at BOTTOM grow up - int targetY; - if (isShowFromTop(position)) { + int anchorX = sourceLook.anchorX; + int anchorY = sourceLook.anchorY; + + if (isShowFromTop(sourceLook)) { targetY = anchorY + (popupIndex * (HEIGHT + 10)); } else { targetY = anchorY - (popupIndex * (HEIGHT + 10)); } - notifyPopups.add(this); - setLocation(anchorX, targetY); + looks.add(sourceLook); + sourceLook.setLocation(anchorX, targetY); - if (hideAfterDurationInSeconds > 0 && hideTween == null) { + if (sourceLook.hideAfterDurationInSeconds > 0 && sourceLook.hideTween == null) { // begin a timeline to get rid of the popup (default is 5 seconds) - hideTween = Tween.to(this, NotifyPopupAccessor.PROGRESS, accessor, hideAfterDurationInSeconds) - .target(WIDTH) - .ease(TweenEquations.Linear) - .addCallback(new TweenCallback() { - @Override - public - void onEvent(final int type, final BaseTween source) { - if (type == Events.END) { - close(); - } - } - }); - tweenManager.add(hideTween); - - // start if we have stopped the timer - if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) { - tweenManager.resetUpdateTime(); - SwingActiveRender.addActiveRenderFrameStart(frameStartHandler); - } - } - } - } - - - // only called on the swing app or SwingActiveRender thread - private - void removePopupFromMap() { - boolean showFromTop = isShowFromTop(position); - synchronized (popups) { - final ArrayList notifyPopups = popups.get(idAndPosition); - - // there are two loops because it is necessary to kill + remove all tweens BEFORE adding new ones. - for (final NotifyPopup popup : notifyPopups) { - if (popup.tween != null) { - popup.tween.kill(); // kill does it's thing on the next tick of animation cycle - popup.tween = null; - } - - if (popup == this && popup.hideTween != null) { - popup.hideTween.kill(); - } - } - - boolean adjustPopupPosition = false; - for (Iterator iterator = notifyPopups.iterator(); iterator.hasNext(); ) { - final NotifyPopup popup = iterator.next(); - - if (popup == this) { - adjustPopupPosition = true; - iterator.remove(); - } - else if (adjustPopupPosition) { - int index = popup.popupIndex - 1; - popup.popupIndex = index; - - // the popups are ALL the same size! - // popups at TOP grow down, popups at BOTTOM grow up - int changedY; - if (showFromTop) { - changedY = popup.anchorY + (index * (HEIGHT + 10)); - } - else { - changedY = popup.anchorY - (index * (HEIGHT + 10)); - } - - // now animate that popup to it's new location - Tween tween = Tween.to(popup, NotifyPopupAccessor.Y_POS, accessor, MOVE_DURATION) - .target((float) changedY) + 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.END) { - // make sure to remove the tween once it's done, otherwise .kill can do weird things. - popup.hideTween = null; - // } + if (type == Events.COMPLETE) { + ((INotify)sourceLook.parent).close(); + } } }); + tweenManager.add(hideTween); + } + } - popup.tween = tween; - tweenManager.add(tween); + // 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 showFromTop = isShowFromTop(sourceLook); + boolean popupsAreEmpty; + + synchronized (popups) { + popupsAreEmpty = popups.isEmpty(); + final ArrayList allLooks = popups.get(sourceLook.idAndPosition); + + // there are two loops because it is necessary to cancel + remove all tweens BEFORE adding new ones. + boolean adjustPopupPosition = false; + for (Iterator iterator = allLooks.iterator(); iterator.hasNext(); ) { + final LookAndFeel look = iterator.next(); + + if (look.tween != null) { + look.tween.cancel(); // cancel does it's thing on the next tick of animation cycle + look.tween = null; + } + + if (look == sourceLook) { + if (look.hideTween != null) { + look.hideTween.cancel(); + look.hideTween = null; + } + + adjustPopupPosition = true; + iterator.remove(); + } + + if (adjustPopupPosition) { + look.popupIndex--; } } - // if there's nothing left, stop the timer. - if (popups.isEmpty()) { - SwingActiveRender.removeActiveRenderFrameStart(frameStartHandler); - } - // start if we have stopped the timer - else if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) { - tweenManager.resetUpdateTime(); - SwingActiveRender.addActiveRenderFrameStart(frameStartHandler); + for (final LookAndFeel look : allLooks) { + // the popups are ALL the same size! + // popups at TOP grow down, popups at BOTTOM grow up + int changedY; + if (showFromTop) { + changedY = look.anchorY + (look.popupIndex * (HEIGHT + 10)); + } + else { + changedY = look.anchorY - (look.popupIndex * (HEIGHT + 10)); + } + + // now animate that popup to it's new location + Tween tween = Tween.to(look, NotifyAccessor.Y_POS, accessor, MOVE_DURATION) + .target((float) changedY) + .ease(TweenEquations.Linear) + .addCallback(new TweenCallback() { + @Override + public + void onEvent(final int type, final BaseTween source) { + if (type == Events.COMPLETE) { + // make sure to remove the tween once it's done, otherwise .kill can do weird things. + look.tween = null; + } + } + }); + + look.tween = tween; + tweenManager.add(tween); } } + + // if there's nothing left, stop the timer. + if (popupsAreEmpty) { + SwingActiveRender.removeActiveRenderFrameStart(frameStartHandler); + } + // start if we have previously stopped the timer + else if (!SwingActiveRender.containsActiveRenderFrameStart(frameStartHandler)) { + tweenManager.resetUpdateTime(); + SwingActiveRender.addActiveRenderFrameStart(frameStartHandler); + } } private static - boolean isShowFromTop(final Pos p) { - switch (p) { + boolean isShowFromTop(final LookAndFeel look) { + switch (look.position) { case TOP_LEFT: case TOP_RIGHT: case CENTER: // center grows down @@ -605,60 +613,4 @@ class NotifyPopup extends JFrame { return false; } } - - public - void setY(final int newY) { - setLocation(getX(), newY); - } - - - /** - * Shakes the popup - * - * @param durationInMillis now long it will shake - * @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot. - */ - public - void shake(final int durationInMillis, final int amplitude) { - int i1 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude; - int i2 = RANDOM.nextInt((amplitude << 2) + 1) - amplitude; - - i1 = i1 >> 2; - i2 = i2 >> 2; - - // make sure it always moves by some amount - if (i1 < 0) { - i1 -= amplitude >> 2; - } - else { - i1 += amplitude >> 2; - } - - if (i2 < 0) { - i2 -= amplitude >> 2; - } - else { - i2 += amplitude >> 2; - } - - int count = durationInMillis / 50; - // make sure we always end the animation where we start - if ((count & 1) == 0) { - count++; - } - - Tween tween = Tween.to(this, NotifyPopupAccessor.X_Y_POS, accessor, 0.05F) - .targetRelative(i1, i2) - .repeatAutoReverse(count, 0) - .ease(TweenEquations.Linear); - tweenManager.add(tween); - } - - int getProgress() { - return progress; - } - - void setProgress(final int progress) { - this.progress = progress; - } } diff --git a/src/dorkbox/notify/Notify.java b/src/dorkbox/notify/Notify.java index 7ccff46..8340e11 100644 --- a/src/dorkbox/notify/Notify.java +++ b/src/dorkbox/notify/Notify.java @@ -16,6 +16,7 @@ package dorkbox.notify; import java.awt.Image; +import java.awt.Window; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -54,6 +55,24 @@ import dorkbox.util.Version; public final class Notify { + /** + * This is the title font used by a notification. + */ + @Property + public static String TITLE_TEXT_FONT = "Source Code Pro BOLD 16"; + + /** + * This is the main text font used by a notification. + */ + @Property + public static String MAIN_TEXT_FONT = "Source Code Pro BOLD 12"; + + /** + * How long we want it to take for the popups to relocate when one is closed + */ + @Property + public static float MOVE_DURATION = 1.0F; + /** * Location of the dialog image resources. By default they must be in the 'resources' directory relative to the application */ @@ -140,11 +159,12 @@ class Notify { private Image graphic; ActionHandler onCloseAction; - private NotifyPopup notifyPopup; + private INotify notifyPopup; private String name; private int shakeDurationInMillis = 0; private int shakeAmplitude = 0; + private Window window; private Notify() { @@ -268,7 +288,7 @@ class Notify { } /** - * Shows the notification + * Shows the notification. If the Notification is assigned to a screen, but shown in a JFrame, the screen number will be ignored. */ public void show() { @@ -281,12 +301,9 @@ class Notify { final Notify notify = Notify.this; final Image graphic = notify.graphic; - if (graphic == null) { - notifyPopup = new NotifyPopup(notify, null, null); - } - else { - // we ONLY cache our own icons - ImageIcon imageIcon; + // we ONLY cache our own icons + ImageIcon imageIcon = null; + if (graphic != null) { if (name != null) { imageIcon = imageIconCache.get(name); if (imageIcon == null) { @@ -301,8 +318,12 @@ class Notify { else { imageIcon = new ImageIcon(graphic); } + } - notifyPopup = new NotifyPopup(notify, graphic, imageIcon); + if (window == null) { + notifyPopup = new AsFrame(notify, graphic, imageIcon); + } else { + notifyPopup = new AsDialog(notify, graphic, imageIcon, window); } notifyPopup.setVisible(true); @@ -312,6 +333,9 @@ class Notify { } } }); + + // don't need to hang onto these. + graphic = null; } /** @@ -369,9 +393,19 @@ class Notify { return this; } + /** + * Attaches this notification to a specific JFrame/Window, instead of having a global notification + */ + public + Notify attach(final Window frame) { + this.window = frame; + return this; + } + + + // called when this notification is closed. void onClose() { notifyPopup = null; - graphic = null; } } diff --git a/src/dorkbox/notify/NotifyPopupAccessor.java b/src/dorkbox/notify/NotifyAccessor.java similarity index 89% rename from src/dorkbox/notify/NotifyPopupAccessor.java rename to src/dorkbox/notify/NotifyAccessor.java index 0d5a2bc..a9c928b 100644 --- a/src/dorkbox/notify/NotifyPopupAccessor.java +++ b/src/dorkbox/notify/NotifyAccessor.java @@ -17,19 +17,19 @@ package dorkbox.notify; import dorkbox.tweenengine.TweenAccessor; -class NotifyPopupAccessor implements TweenAccessor { +class NotifyAccessor implements TweenAccessor { static final int Y_POS = 1; static final int X_Y_POS = 2; static final int PROGRESS = 3; - NotifyPopupAccessor() { + NotifyAccessor() { } @Override public - int getValues(final NotifyPopup target, final int tweenType, final float[] returnValues) { + int getValues(final LookAndFeel target, final int tweenType, final float[] returnValues) { switch (tweenType) { case Y_POS: returnValues[0] = (float) target.getY(); @@ -48,7 +48,7 @@ class NotifyPopupAccessor implements TweenAccessor { @SuppressWarnings({"NumericCastThatLosesPrecision", "UnnecessaryReturnStatement"}) @Override public - void setValues(final NotifyPopup target, final int tweenType, final float[] newValues) { + void setValues(final LookAndFeel target, final int tweenType, final float[] newValues) { switch (tweenType) { case Y_POS: target.setY((int) newValues[0]); diff --git a/src/dorkbox/notify/NotifyPopupWindowAdapter.java b/src/dorkbox/notify/WindowAdapter.java similarity index 84% rename from src/dorkbox/notify/NotifyPopupWindowAdapter.java rename to src/dorkbox/notify/WindowAdapter.java index 1b94dc0..a704ce1 100644 --- a/src/dorkbox/notify/NotifyPopupWindowAdapter.java +++ b/src/dorkbox/notify/WindowAdapter.java @@ -15,22 +15,23 @@ */ package dorkbox.notify; -import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -class NotifyPopupWindowAdapter extends WindowAdapter { +class WindowAdapter extends java.awt.event.WindowAdapter { + @Override public void windowClosing(WindowEvent e) { if (e.getNewState() != WindowEvent.WINDOW_CLOSED) { - NotifyPopup source = (NotifyPopup) e.getSource(); + AsFrame source = (AsFrame) e.getSource(); source.close(); } } + @Override public void windowLostFocus(WindowEvent e) { if (e.getNewState() != WindowEvent.WINDOW_CLOSED) { - NotifyPopup source = (NotifyPopup) e.getSource(); + AsFrame source = (AsFrame) e.getSource(); // these don't work //toFront(); //requestFocus(); diff --git a/test/NotifyTest.java b/test/NotifyTest.java index d02f186..171a4bb 100644 --- a/test/NotifyTest.java +++ b/test/NotifyTest.java @@ -13,9 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import java.awt.FlowLayout; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + import dorkbox.notify.Notify; import dorkbox.notify.Pos; import dorkbox.util.ActionHandler; +import dorkbox.util.ScreenUtil; public class NotifyTest { @@ -24,28 +32,117 @@ class NotifyTest { void main(String[] args) { Notify notify; - int count = 3; + + JFrame frame = new JFrame("Test"); + + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout()); + + JLabel label = new JLabel("This is a label!"); + + JButton button = new JButton(); + button.setText("Press me"); + + panel.add(label); + panel.add(button); + + frame.add(panel); + frame.setSize(600, 600); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + + ScreenUtil.showOnSameScreenAsMouse_Center(frame); + + + + int count = 2; for (int i = 0; i < count; i++) { + final int finalI = i; notify = Notify.create() .title("Notify title " + i) .text("This is a notification " + i + " popup message This is a notification popup message This is a " + "notification popup message") - .hideAfter(50000) + .hideAfter(13000) + .position(Pos.CENTER) + // .setScreen(0) + .darkStyle() + // .shake(1300, 4) + // .shake(1300, 10) + .attach(frame) + .hideCloseButton() + .onAction(new ActionHandler() { + @Override + public + void handle(final Notify arg0) { + System.err.println("Notification " + finalI + " clicked on!"); + } + }); + notify.showWarning(); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + for (int i = 0; i < count; i++) { + final int finalI = i; + notify = Notify.create() + .title("Notify title " + i) + .text("This is a notification " + i + " popup message This is a notification popup message This is a " + + "notification popup message") + .hideAfter(13000) .position(Pos.TOP_RIGHT) // .setScreen(0) .darkStyle() - .shake(1300, 4) -// .shake(1300, 10) -// .hideCloseButton() + // .shake(1300, 4) + // .shake(1300, 10) + .hideCloseButton() .onAction(new ActionHandler() { - @Override - public - void handle(final Notify arg0) { - System.out.println("Notification clicked on!"); - } - }); - notify.showWarning(); + @Override + public + void handle(final Notify arg0) { + System.err.println("Notification " + finalI + " clicked on!"); + } + }); + notify.showConfirm(); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + for (int i = 0; i < count; i++) { + final int finalI = i; + notify = Notify.create() + .title("Notify title " + i) + .text("This is a notification " + i + " popup message This is a notification popup message This is a " + + "notification popup message") + .hideAfter(13000) + .position(Pos.BOTTOM_LEFT) +// .setScreen(0) + .darkStyle() + // .shake(1300, 4) + // .shake(1300, 10) + .hideCloseButton() + .onAction(new ActionHandler() { + @Override + public + void handle(final Notify arg0) { + System.err.println("Notification " + finalI + " clicked on!"); + } + }); + notify.show(); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } } } }