/**
* GmailAssistant 1.1 (2008-03-16)
* Copyright 2008 Zach Scrivena
* zachscrivena@gmail.com
* http://gmailassistant.sourceforge.net/
*
* Notifier for multiple Gmail accounts.
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gmailassistant;
import java.awt.Color;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
/**
* Display popup messages on the desktop.
*/
class DesktopPopup
extends JPanel
{
/** timer refresh interval */
private static final long TIMER_REFRESH_INTERVAL_MILLISECONDS = 200L;
/** popup duration for first popup message (5 seconds) */
private static final long FIRST_POPUP_DURATION_MILLISECONDS = 5000L;
/** popup duration for subsequent popup messages (2 seconds) */
private static final long POPUP_DURATION_MILLISECONDS = 2000L;
/** additional popup duration for the last popup message (1 second) */
private static final long LAST_POPUP_DURATION_MILLISECONDS = 1000L;
/** default border color */
private static final Color DEFAULT_BORDER_COLOR = (Color) Account.Property.COLOR.initialVal;
/** tooltip text for the popup */
private static final String POPUP_TOOLTIP = "<html>Click on the icon to skip to the next message.<br />Click on the text to dismiss messages.</html>";
/** window that will contain the popup */
private final JWindow window;
/** popup data corresponding to each account */
private final NavigableMap<Account,AccountPopupData> popupData =
new TreeMap<Account,AccountPopupData>();
/** timer for displaying popup messages */
private final Timer timer;
/** is the timer cancelled? */
private volatile boolean timerCancelled = false;
/** queued actions to be performed by the timer */
private final Deque<ActionType> actions = new ArrayDeque<ActionType>();
/**
* Constructor.
*
* @param parent
* parent GmailAssistant object
*/
DesktopPopup(
final GmailAssistant parent)
{
/*******************************
* INITIALIZE PANEL COMPONENTS *
*******************************/
initComponents();
/******************************
* CONFIGURE PANEL COMPONENTS *
******************************/
setToolTipText(DesktopPopup.POPUP_TOOLTIP);
this.icon.setToolTipText(DesktopPopup.POPUP_TOOLTIP);
this.icon.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
skipMessage();
}
});
this.text.setToolTipText(DesktopPopup.POPUP_TOOLTIP);
this.text.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
cancelMessage();
}
});
this.window = new JWindow();
this.window.getContentPane().add(this);
this.window.setAlwaysOnTop(true);
this.window.pack();
this.window.setFocusable(false);
this.window.setVisible(false);
/***************************
* INITIALIZE TIMER THREAD *
***************************/
this.timer = new Timer("Desktop-Popup-Timer", true);
this.timer.schedule(new TimerTask()
{
SlidingAnimator animator;
int width;
int height;
int X;
int Y;
@Override
public void run()
{
ActionType action;
ActionType interrupt;
synchronized (DesktopPopup.this.actions)
{
action = DesktopPopup.this.actions.pollFirst();
}
if ((action == ActionType.ALL) ||
(action == ActionType.RECENT))
{
Entry<Account,AccountPopupData> entry;
Account ac = null;
AccountPopupData data;
boolean firstMessage = true;
boolean autoAdvance = true;
while (true)
{
/* get next account */
synchronized (DesktopPopup.this.popupData)
{
if (ac == null)
{
/* get first account */
entry = DesktopPopup.this.popupData.firstEntry();
}
else
{
/* get next account */
entry = DesktopPopup.this.popupData.higherEntry(ac);
if ((!autoAdvance) && (entry == null))
{
/* reached last account; loop-around to first account again */
entry = DesktopPopup.this.popupData.firstEntry();
action = ActionType.ALL;
}
}
if (entry == null)
{
/* no more accounts left */
ac = null;
data = null;
}
else
{
ac = entry.getKey();
data = entry.getValue();
}
}
if (ac == null)
{
if (((Boolean) parent.getProperty(GmailAssistant.Property.ALERT_POPUP_PERSISTENT_MESSAGES)) &&
(!firstMessage))
{
/* display persistent popup messages */
while (true)
{
interrupt = sleepTillInterrupted(DesktopPopup.POPUP_DURATION_MILLISECONDS);
if (interrupt == ActionType.SKIP)
{
autoAdvance = false;
break;
}
else if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
}
/* loop-around to the first account */
continue;
}
else
{
/* exit loop */
break;
}
}
final int n = data.mails.length;
final Color borderColor = data.color;
for (int i = 0; i < n; i++)
{
final Mail m = data.mails[i];
final int k = i + 1;
interrupt = sleepTillInterrupted(0L);
if (interrupt == ActionType.SKIP)
{
autoAdvance = false;
}
else if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
boolean recent;
synchronized (data.lastIdLock)
{
recent = (m.sequenceNumber > data.lastId);
}
if ((action == ActionType.ALL) ||
((action == ActionType.RECENT) && recent))
{
final long duration = System.currentTimeMillis() - m.date.getTime();
final String popupMessageText =
"<html><div style='white-space:nowrap'>" +
"<b>" + k + " of " + n + "</b> unread " +
((n == 1) ? "mail" : "mails") +
" for <b>" + data.username + "</b><br/>" +
m.from + " (" + GmailAssistant.timeDurationString(duration) +
((duration >= 0) ? " ago" : " in the future") + ")<br />" +
"<b>" + m.subject + "</b><br />" +
"<i>" + m.snippet + "</i></div></html>";
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
DesktopPopup.this.setBorder(BorderFactory.createLineBorder(borderColor, 4));
DesktopPopup.this.text.setText(popupMessageText);
DesktopPopup.this.window.pack();
width = DesktopPopup.this.window.getWidth();
height = DesktopPopup.this.window.getHeight();
final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
X = r.x + r.width - width;
Y = r.y + r.height - height;
DesktopPopup.this.window.setLocation(X, Y);
}
});
}
catch (Exception e)
{
/* ignore */
}
if (firstMessage)
{
/* animate entry of popup message */
setVisiblePopupMessage(false);
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
animator = new SlidingAnimator(
DesktopPopup.this.window,
width,
height,
X,
Y,
10,
SlidingAnimator.SlidingDirection.UP_IN);
}
});
}
catch (Exception e)
{
/* ignore */
}
animator.animate();
}
setVisiblePopupMessage(true);
firstMessage = false;
synchronized (data.lastIdLock)
{
if (m.sequenceNumber > data.lastId)
{
data.lastId = m.sequenceNumber;
}
}
/* pause before the next message */
while (true)
{
interrupt = sleepTillInterrupted(firstMessage ?
DesktopPopup.FIRST_POPUP_DURATION_MILLISECONDS :
DesktopPopup.POPUP_DURATION_MILLISECONDS);
if (interrupt == ActionType.SKIP)
{
autoAdvance = false;
break;
}
else if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
if (autoAdvance)
{
break;
}
}
}
}
}
if (!firstMessage)
{
/* at least one popup message has been shown; animate exit */
interrupt = sleepTillInterrupted(DesktopPopup.LAST_POPUP_DURATION_MILLISECONDS);
if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
final int width = DesktopPopup.this.window.getWidth();
final int height = DesktopPopup.this.window.getHeight();
final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
final int X = r.x + r.width - width;
final int Y = r.y + r.height - height;
animator = new SlidingAnimator(
DesktopPopup.this.window,
width,
height,
X,
Y,
10,
SlidingAnimator.SlidingDirection.DOWN_OUT);
}
});
}
catch (Exception e)
{
/* ignore */
}
animator.animate();
setVisiblePopupMessage(false);
}
}
else if (action == ActionType.TEST)
{
/* display a test popup */
interrupt = sleepTillInterrupted(0L);
if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
final String popupMessageText = "<html>This is a test message.</html>";
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
DesktopPopup.this.setBorder(BorderFactory.createLineBorder(DesktopPopup.DEFAULT_BORDER_COLOR, 4));
DesktopPopup.this.text.setText(popupMessageText);
DesktopPopup.this.window.pack();
width = DesktopPopup.this.window.getWidth();
height = DesktopPopup.this.window.getHeight();
final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
X = r.x + r.width - width;
Y = r.y + r.height - height;
DesktopPopup.this.window.setLocation(X, Y);
}
});
}
catch (Exception e)
{
/* ignore */
}
/* animate entry of popup message */
setVisiblePopupMessage(false);
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
animator = new SlidingAnimator(
DesktopPopup.this.window,
width,
height,
X,
Y,
10,
SlidingAnimator.SlidingDirection.UP_IN);
}
});
}
catch (Exception e)
{
/* ignore */
}
animator.animate();
setVisiblePopupMessage(true);
interrupt = sleepTillInterrupted(DesktopPopup.FIRST_POPUP_DURATION_MILLISECONDS);
if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
/* animate exit */
interrupt = sleepTillInterrupted(DesktopPopup.LAST_POPUP_DURATION_MILLISECONDS);
if (interrupt == ActionType.CANCEL)
{
setVisiblePopupMessage(false);
return;
}
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
final int width = DesktopPopup.this.window.getWidth();
final int height = DesktopPopup.this.window.getHeight();
final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
final int X = r.x + r.width - width;
final int Y = r.y + r.height - height;
animator = new SlidingAnimator(
DesktopPopup.this.window,
width,
height,
X,
Y,
10,
SlidingAnimator.SlidingDirection.DOWN_OUT);
}
});
}
catch (Exception e)
{
/* ignore */
}
animator.animate();
setVisiblePopupMessage(false);
}
else if (action == ActionType.TERMINATE)
{
DesktopPopup.this.timer.cancel();
DesktopPopup.this.timerCancelled = true;
}
}
}, 0L, DesktopPopup.TIMER_REFRESH_INTERVAL_MILLISECONDS);
}
/**
* Set visibility of the popup message.
*
* @param visible
* true to show popup message; false otherwise
*/
private void setVisiblePopupMessage(
final boolean visible)
{
try
{
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
DesktopPopup.this.window.setVisible(visible);
}
});
}
catch (Exception e)
{
/* ignore */
}
}
/**
* Update popup messages for the specified account.
*
* @param ac
* account to be updated
* @param username
* username for the account
* @param color
* color for the account
* @param mails
* unread mails for the account
* @param showPopups
* true to show popup messages; false otherwise
*/
void updateMessages(
final Account ac,
final String username,
final Color color,
final Mail[] mails,
final boolean showPopups)
{
AccountPopupData data;
synchronized (this.popupData)
{
data = this.popupData.get(ac);
if (data == null)
{
data = new AccountPopupData();
this.popupData.put(ac, data);
}
}
data.username = username;
data.color = color;
data.mails = mails;
Arrays.sort(data.mails);
if (showPopups)
{
synchronized (this.actions)
{
this.actions.addLast(ActionType.RECENT);
}
}
else
{
int maxId = 0;
for (Mail m : data.mails)
{
if (m.sequenceNumber > maxId)
{
maxId = m.sequenceNumber;
}
}
synchronized (data.lastIdLock)
{
if (maxId > data.lastId)
{
data.lastId = maxId;
}
}
}
}
/**
* Skip to the next popup message.
*/
void skipMessage()
{
synchronized (this.actions)
{
this.actions.addFirst(ActionType.SKIP);
}
}
/**
* Cancel the current popup message.
*/
void cancelMessage()
{
synchronized (this.actions)
{
this.actions.addFirst(ActionType.CANCEL);
}
}
/**
* Cancel display of all popup messages.
*/
void cancelAllMessages()
{
synchronized (this.actions)
{
this.actions.clear();
this.actions.addLast(ActionType.CANCEL);
}
}
/**
* Show all popup messages.
*/
void showAllMessages()
{
synchronized (this.actions)
{
this.actions.clear();
this.actions.addLast(ActionType.CANCEL);
this.actions.addLast(ActionType.ALL);
}
}
/**
* Show a test popup message.
*/
void test()
{
synchronized (this.actions)
{
this.actions.addLast(ActionType.TEST);
}
}
/**
* Terminate the timer thread.
* This method blocks until the thread is cancelled.
*/
void terminate()
{
synchronized (this.actions)
{
this.actions.clear();
}
do
{
synchronized (this.actions)
{
this.actions.addLast(ActionType.CANCEL);
this.actions.addLast(ActionType.TERMINATE);
}
if (this.timerCancelled)
{
return;
}
try
{
Thread.sleep(DesktopPopup.TIMER_REFRESH_INTERVAL_MILLISECONDS / 10);
}
catch (Exception e)
{
/* ignore */
}
}
while (true);
}
/**
* Remove popup messages for the specified account.
*
* @param ac
* account for which to remove popup messages
*/
void removeMessages(
final Account ac)
{
synchronized (this.popupData)
{
this.popupData.remove(ac);
}
}
/**
* Sleep for the specified duration, or until interrupted.
*
* @pa