package chatty.gui.notifications; import chatty.gui.notifications.Notification.HideMethod; import chatty.util.ActivityTracker; import java.awt.Component; import java.awt.Dimension; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import javax.swing.SwingUtilities; /** * Create and position notifications. * * @author tduva * @param <T> The type of the data associated with a notification */ public class NotificationManager<T> { // Margin between notifications and the border of the screen private static final int VERTICAL_MARGIN = 3; private static final int HORIZONTAL_MARGIN = 4; private static final int SECOND = 1000; private static final int MINUTE = 60*SECOND; // List of displayed notifications. private final LinkedList<Notification> displayed = new LinkedList<>(); // List of notifications queued for display. private final LinkedList<Notification> queue = new LinkedList<>(); private final Map<Notification,T> notificationData = new HashMap<>(); /** * Settings */ private int screen; private int position; // Defined based on position private int verticalMoveDirection; private int horizontalMoveDirection; // Regular display time private int displayTime = 10*SECOND; // How long to display a notification at the most private int maxDisplayTime = 30*MINUTE; // Shorter max display time, in case the queue is full private int shortMaxDisplayTime = 2*SECOND; // How long the notification should be shown as new private int expireTime = 5*MINUTE; private int activityTime = -1; // Maximum number of items to display at once private int maxItems = 4; // Maximum number of items in the queue private int maxQueueSize = 4; private HideMethod hideMethod = HideMethod.FADE_OUT; // Used to choose a screen for the notification to tbe displayed on private final Component parent; private final NotificationListener listener; private NotificationActionListener<T> actionListener; /** * * @param parent */ public NotificationManager(Component parent) { this.parent = parent; setPosition(0); setScreen(0); this.listener = new NotificationListener() { @Override public void notificationRemoved(Notification source) { NotificationManager.this.notificationRemoved(source); } @Override public void notificationAction(Notification source) { if (actionListener != null) { actionListener.notificationAction(notificationData.get(source)); } } }; } /** * Create a notification with the given message and either show or queue it. * * @param title * @param message * @param data */ public void showMessage(String title, String message, T data) { Notification n = new Notification(title, message, listener, expireTime); n.setHideMethod(hideMethod); //n.setTimeout(displayTime); n.setFallbackTimeout(maxDisplayTime); n.setActivityTime(activityTime); if (displayed.size() < maxItems) { showNotification(n); } else { queue.add(n); checkQueueSize(); } notificationData.put(n, data); } public void showMessage(String title, String message) { showMessage(title, message, null); } /** * Removes any queued notifications and closes displayed ones. */ public void clearAll() { queue.clear(); clearAllShown(); } /** * Closes all displayed notifications. */ public void clearAllShown() { // Create a copy of the list so it can iterate over all elements while // the original list is modified. for (Notification n : new LinkedList<>(displayed)) { n.close(); } } /** * Set in which corner of the screen the notification should appear in. * 0 - Top Left (default for invalid values) * 1 - Top Right * 2 - Bottom Left * 3 - Bottom Right * * Only changes the location if no notifications are currently displayed. * * @param position */ public final void setPosition(int position) { if (isClear()) { this.position = position; updateVariables(); } } /** * Set on which screen the notification should appear. Values smaller than * 0 mean the screen should be determined another way (from the parent * JFrame if set or just the default one). * * Only changes the screen if no notifications are currently displayed. * * @param screen */ public final void setScreen(int screen) { if (isClear()) { this.screen = screen; } } public final void setDisplayTime(int displayTime) { this.displayTime = displayTime * SECOND; } public final void setMaxDisplayTime(int maxDisplayTime) { this.maxDisplayTime = maxDisplayTime * SECOND; } public final void setShortMaxDisplayTime(int shortDisplayTime) { this.shortMaxDisplayTime = shortDisplayTime; } public final void setMaxQueueSize(int size) { this.maxQueueSize = size; } public final void setMaxDisplayItems(int count) { this.maxItems = count; } public final void setExpireTime(int time) { this.expireTime = time; } /** * Sets the time in seconds in which activity should have been detected to * be considered active. Automatically starts tracking if it's greater than * 0. Values <= 0 disable this feature (however tracking may still be * active). * * @param time */ public final void setActivityTime(int time) { this.activityTime = time * SECOND; if (activityTime > 0) { ActivityTracker.startTracking(); } } public final void setHideMethod(HideMethod hideMethod) { this.hideMethod = hideMethod; } public final void setNotificationActionListener(NotificationActionListener<T> listener) { this.actionListener = listener; } /** * Checks if no notifications are displayed or queued. * * @return */ private boolean isClear() { if (displayed.isEmpty() && queue.isEmpty()) { return true; } return false; } /** * Update the variables that depend on the position. */ private void updateVariables() { if (position == 2 || position == 3) { verticalMoveDirection = 1; } else { verticalMoveDirection = -1; } if (position == 1 || position == 3) { horizontalMoveDirection = 1; } else { horizontalMoveDirection = -1; } } /** * A notification was removed, reposition any other ones if necessary and * remove it from the displayed-list. * * @param removed The notification that was removed. */ private void notificationRemoved(Notification removed) { int offset = 0; Iterator<Notification> it = displayed.iterator(); while (it.hasNext()) { Notification n = it.next(); if (removed == n) { offset = n.getHeight() + VERTICAL_MARGIN; it.remove(); } else if (offset > 0) { // Move any notifcations after the removed one n.moveVertical(verticalMoveDirection * offset); } } // Show next notification if possible and if one is queued. Iterator<Notification> itQueue = queue.iterator(); while (itQueue.hasNext() && displayed.size() < maxItems) { Notification next = itQueue.next(); itQueue.remove(); showNotification(next); } } /** * If the queue size exceeds the maxQueueSize and any notifications are * displayed, then make the oldest one disappear faster. */ private void checkQueueSize() { if (queue.size() > maxQueueSize && displayed.size() > 0) { displayed.getFirst().setFallbackTimeout(shortMaxDisplayTime); } } /** * Actually shows a notification after it was created. Positions the * notification appropriately and checks if the queue is full and acts * appropriately. * * @param n */ private void showNotification(Notification n) { checkQueueSize(); GraphicsConfiguration config = getGraphicsConfig(); Point location = calculateLocation(position, getSafeBounds(config), n.getSize(), getCurrentOffset()); n.setLocation(location); n.setTimeout(displayTime + (displayTime/4 * displayed.size())); if (queue.size() > maxQueueSize && displayed.size() == 0) { n.setFallbackTimeout(shortMaxDisplayTime); } displayed.add(n); n.show(); } /** * Calculate the current offset based on the displayed notifications. * * @return */ private int getCurrentOffset() { int offset = VERTICAL_MARGIN; for (Notification n : displayed) { offset += n.getHeight() + VERTICAL_MARGIN; } return offset; } /** * Get a GraphicsConfiguration based on the settings. Either from a defined * screen, from the parent or the default one. * * @return */ private GraphicsConfiguration getGraphicsConfig() { GraphicsDevice[] devices = GraphicsEnvironment .getLocalGraphicsEnvironment().getScreenDevices(); if (screen >= 0 && devices.length - 1 >= screen) { return devices[screen].getDefaultConfiguration(); } if (parent != null) { GraphicsConfiguration g = parent.getGraphicsConfiguration(); if (g != null) { return g; } } return GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration(); } /** * Calculates the location within the bounds to display something with the * given size, so it appears at the given position, which is in one of the * four corners (0: top-left, 1: top-right, 2: bottom-left, 3: bottom-right). * * @param position * @param bounds * @param size * @return */ private Point calculateLocation(int position, Rectangle bounds, Dimension size, int offset) { Point location = new Point(); location.x = bounds.x - horizontalMoveDirection*HORIZONTAL_MARGIN; location.y = bounds.y; if (position == 1 || position == 3) { location.x += (bounds.width - size.width); } if (position == 2 || position == 3) { location.y += (bounds.height - size.height); offset = -offset; } location.y += offset; return location; } /** * Calculates the safe bounds (without taskbar) of the given * GraphicsConfiguration. * * @param config * @return */ private static Rectangle getSafeBounds(GraphicsConfiguration config) { Rectangle bounds = new Rectangle(config.getBounds()); Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(config); bounds.x += insets.left; bounds.y += insets.top; bounds.width -= (insets.left + insets.right); bounds.height -= (insets.top + insets.bottom); return bounds; } public static final void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { NotificationManager m = new NotificationManager(null); m.setPosition(0); m.setScreen(1); m.showMessage("Test", "Test message with some text."); } }); } }