/**
* ThisTime 2.0 (2008-03-30)
* Copyright 2007 Zach Scrivena
* zachscrivena@gmail.com
* http://thistime.sourceforge.net/
*
* Simple clock and timer program.
*
* 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 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 thistime;
import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Timer;
import java.util.TimerTask;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
/**
* Timer unit.
*/
class TimerUnit
extends JFrame
{
/** timer refresh interval */
private static final long TIMER_REFRESH_INTERVAL_MILLISECONDS = 200L;
/** default "always on top" mode */
private static final boolean DEFAULT_ALWAYS_ON_TOP_MODE = true;
/** tooltip for timer display when "set" mode is ON */
private static final String DISPLAY_TOOLTIP_SET_ON =
"<html>To increase a number, scroll up or click above the dotted line;<br />to decrease it, scroll down or click below the dotted line.</html>";
/** tooltip for timer display when "set" mode is OFF */
private static final String DISPLAY_TOOLTIP_SET_OFF = "Right-click for timer options";
/** parent ThisTime object */
private final ThisTime parent;
/** timer display for this timer unit */
private final TimerDisplay display;
/** "normalized" timer value to be displayed */
private final TimerValue value;
/** accumulated elapsed time in milliseconds, for finer-resolution time-keeping */
private long accumulatedElapsedMilliseconds = 0L;
/** timer for refreshing timer value */
private final Timer timer;
/** true for count up timer; false for count down timer */
private volatile boolean countUp;
/** is the timer alarm enabled? (default is true) */
private volatile boolean alarmEnabled = true;
/** "minimize to tray" mode (default is true) */
private boolean minimizeToTray = true;
/** is the timer running? (initially false) */
private volatile boolean isRunning = false;
/** is the timer display in "set" mode? */
private boolean setMode = false;
/** time marker used for computing elapsed time, in milliseconds since the epoch */
private volatile long timeMarker = System.currentTimeMillis();
/** alarm audio clip */
private AudioClip alarm;
/** is the alarm currently playing? */
private boolean alarmPlaying = false;
/** tray icon for this timer unit */
private TrayIcon trayIcon;
/**
* Constructor.
* This method must run on the EDT.
*
* @param parent
* parent ThisTime object
* @param title
* title of this timer unit
* @param countUp
* true for count up timer; false for count down timer
*/
TimerUnit(
final ThisTime parent,
final String title,
final boolean countUp)
{
/*********************
* INITIALIZE FIELDS *
*********************/
this.parent = parent;
this.countUp = countUp;
value = new TimerValue();
display = new TimerDisplay(value);
display.setSetMode(setMode);
/******************************
* INITIALIZE FORM COMPONENTS *
******************************/
initComponents();
/*****************************
* CONFIGURE FORM COMPONENTS *
*****************************/
setTitle(title);
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(WindowEvent e)
{
closeTimer();
}
@Override
public void windowDeiconified(WindowEvent e)
{
restoreTimer();
}
@Override
public void windowIconified(WindowEvent e)
{
minimizeTimer();
}
});
/* set "always on top" mode */
try
{
setAlwaysOnTop(DEFAULT_ALWAYS_ON_TOP_MODE);
}
catch (Exception e)
{
/* ignore */
}
/* set frame icon */
final Image[] frameIcons = new Image[2];
final int countUpIconIndex = 0;
final int countDownIconIndex = 1;
try
{
frameIcons[countUpIconIndex] = ImageIO.read(TimerUnit.class.getResource("/thistime/resources/clock_bullet_go_up.png"));
frameIcons[countDownIconIndex] = ImageIO.read(TimerUnit.class.getResource("/thistime/resources/clock_bullet_go_down.png"));
setIconImage(frameIcons[this.countUp ? countUpIconIndex : countDownIconIndex]);
}
catch (Exception e)
{
SwingManipulator.showErrorDialog(
null,
"Initialization Error - " + getTitle(),
"Failed to load timer icons (" + e + ").\n" +
"This timer will proceed to run without displaying the timer icons.");
}
/* load alarm audio clip */
try
{
alarm = Applet.newAudioClip(TimerUnit.class.getResource("/thistime/resources/timeralarm.wav"));
}
catch (Exception e)
{
alarm = null;
SwingManipulator.showErrorDialog(
null,
"Initialization Error - " + getTitle(),
"Failed to load alarm audio clip (" + e + ").\n" +
"This timer will proceed to run without playing the alarm audio clip.");
}
/* prepare tray icon */
try
{
trayIcon = new TrayIcon(
frameIcons[this.countUp ? countUpIconIndex : countDownIconIndex],
getTitle());
trayIcon.setImageAutoSize(true);
trayIcon.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
restoreTimer();
}
});
}
catch (Exception e)
{
trayIcon = null;
SwingManipulator.showErrorDialog(
null,
"Initialization Error - " + getTitle(),
"Failed to load timer tray icon (" + e + ").\n" +
"This timer will proceed to run without displaying the timer tray icon.");
}
/* button: "Start" */
startButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
if (!isRunning)
{
timeMarker = System.currentTimeMillis();
isRunning = true;
}
stopAlarm();
}
});
/* button: "Stop" */
stopButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
if (isRunning)
{
isRunning = false;
}
stopAlarm();
}
});
/* button: "Reset" */
resetButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
synchronized (value)
{
value.negative = false;
value.h = 0L;
value.m = 0;
value.s = 0;
accumulatedElapsedMilliseconds = 0L;
}
display.repaint();
stopAlarm();
}
});
/* button: "Switch" */
switchButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
TimerUnit.this.countUp = !TimerUnit.this.countUp;
try
{
setIconImage(frameIcons[TimerUnit.this.countUp ? countUpIconIndex : countDownIconIndex]);
if (trayIcon != null)
{
trayIcon.setImage(frameIcons[TimerUnit.this.countUp ? countUpIconIndex : countDownIconIndex]);
}
}
catch (Exception ex)
{
/* ignore */
}
stopAlarm();
}
});
/* button: "Set/Lock" */
final Icon[] setButtonIcons = new Icon[2];
final int setIconIndex = 0;
final int lockIconIndex = 1;
setButtonIcons[setIconIndex] = new ImageIcon(TimerUnit.class.getResource("/thistime/resources/control_equalizer_blue.png"));
setButtonIcons[lockIconIndex] = new ImageIcon(TimerUnit.class.getResource("/thistime/resources/control_equalizer_blue_cancel.png"));
setButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
setMode = !setMode;
display.setSetMode(setMode);
if (setMode)
{
setButton.setIcon(setButtonIcons[lockIconIndex]);
display.setToolTipText(DISPLAY_TOOLTIP_SET_ON);
resetButton.setEnabled(true);
switchButton.setEnabled(true);
}
else
{
setButton.setIcon(setButtonIcons[setIconIndex]);
display.setToolTipText(DISPLAY_TOOLTIP_SET_OFF);
resetButton.setEnabled(false);
switchButton.setEnabled(false);
}
display.repaint();
stopAlarm();
}
});
/* set initial button states */
if (setMode)
{
setButton.setIcon(setButtonIcons[lockIconIndex]);
display.setToolTipText(DISPLAY_TOOLTIP_SET_ON);
resetButton.setEnabled(true);
switchButton.setEnabled(true);
}
else
{
setButton.setIcon(setButtonIcons[setIconIndex]);
display.setToolTipText(DISPLAY_TOOLTIP_SET_OFF);
resetButton.setEnabled(false);
switchButton.setEnabled(false);
}
/* popup menu (Set Title, Enable Alarm, Show Buttons, Always on Top, Minimize to Tray) */
final JPopupMenu popupMenu = new JPopupMenu();
/* popup menu: "Set Title..." */
final JMenuItem titleMenuItem = new JMenuItem("Set Title...", 't');
titleMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
String newTitle = JOptionPane.showInputDialog(
TimerUnit.this,
"Timer title:",
"Set Title - " + getTitle(),
JOptionPane.QUESTION_MESSAGE);
if (newTitle == null)
{
return;
}
newTitle = newTitle.trim();
if (newTitle.isEmpty())
{
newTitle = title;
}
setTitle(newTitle);
if (trayIcon != null)
{
trayIcon.setToolTip(newTitle);
}
}
});
popupMenu.add(titleMenuItem);
/* popup menu: "Enable Alarm" */
final JMenuItem alarmMenuItem = new JCheckBoxMenuItem("Enable Alarm", alarmEnabled);
alarmMenuItem.setMnemonic('a');
alarmMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
alarmEnabled = alarmMenuItem.isSelected();
if (!alarmEnabled)
{
stopAlarm();
}
}
});
popupMenu.add(alarmMenuItem);
/* popup menu: "Show Buttons" */
final JMenuItem buttonsMenuItem = new JCheckBoxMenuItem("Show Buttons", true);
buttonsMenuItem.setMnemonic('b');
buttonsMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
final Container c = getContentPane();
c.removeAll();
c.add(display, BorderLayout.CENTER);
if (buttonsMenuItem.isSelected())
{
c.add(buttonsPanel, BorderLayout.SOUTH);
}
validate();
}
});
popupMenu.add(buttonsMenuItem);
popupMenu.addSeparator();
/* popup menu: "Always on Top" */
final JMenuItem topMenuItem = new JCheckBoxMenuItem("Always on Top", isAlwaysOnTop());
topMenuItem.setMnemonic('t');
topMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
setAlwaysOnTop(topMenuItem.isSelected());
}
});
popupMenu.add(topMenuItem);
/* popup menu: "Minimize to Tray" */
final JMenuItem trayMenuItem = new JCheckBoxMenuItem("Minimize to Tray", minimizeToTray);
trayMenuItem.setMnemonic('t');
trayMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
minimizeToTray = trayMenuItem.isSelected();
}
});
popupMenu.add(trayMenuItem);
/* add popup menu to timer display */
display.addMouseListener(new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent e)
{
processMouseEvent(e);
}
@Override
public void mouseReleased(MouseEvent e)
{
processMouseEvent(e);
}
private void processMouseEvent(
final MouseEvent e)
{
if (e.isPopupTrigger())
{
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
});
/* key binding: ESCAPE key */
for (JComponent c : new JComponent[] {display, buttonsPanel})
{
c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_MINIMIZE");
c.getActionMap().put("ESCAPE_MINIMIZE", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
minimizeTimer();
}
});
}
/* layout frame components */
setLayout(new BorderLayout());
final Container c = getContentPane();
c.add(display, BorderLayout.CENTER);
if (buttonsMenuItem.isSelected())
{
c.add(buttonsPanel, BorderLayout.SOUTH);
}
setPreferredSize(new Dimension(350, 200));
pack();
/* center form on the screen */
setLocationRelativeTo(null);
/***************************
* INITIALIZE TIMER THREAD *
***************************/
timer = new Timer("Timer-Unit-Timer", true);
timer.schedule(new TimerTask()
{
/**
* Refresh timer value.
*/
@Override
public void run()
{
if (isRunning)
{
/* update time marker */
final long currentTime = System.currentTimeMillis();
final long newElapsedMilliseconds = currentTime - timeMarker;
timeMarker = currentTime;
synchronized (value)
{
accumulatedElapsedMilliseconds += newElapsedMilliseconds;
if ((accumulatedElapsedMilliseconds >= 1000L) ||
(accumulatedElapsedMilliseconds <= -1000L))
{
/* accumulated enough elapsed time to trigger a change in displayed value */
final long accumulatedElapsedSeconds = accumulatedElapsedMilliseconds / 1000;
accumulatedElapsedMilliseconds -= (accumulatedElapsedSeconds * 1000);
/* timer value in seconds */
long seconds = value.s;
seconds += (value.m * 60);
seconds += (value.h * 60 * 60);
if (value.negative)
{
seconds = -seconds;
}
/* add elapsed time */
final long prevSeconds = seconds;
if (TimerUnit.this.countUp)
{
seconds += accumulatedElapsedSeconds;
if ((prevSeconds < 0L) && (seconds >= 0L))
{
startAlarm();
}
}
else
{
seconds -= accumulatedElapsedSeconds;
if ((prevSeconds > 0L) && (seconds <= 0L))
{
startAlarm();
}
}
/* compute "normalized" timer value for display */
if (seconds < 0L)
{
/* negative value */
value.negative = true;
value.h = seconds / (-60 * 60);
seconds = -(seconds + value.h * 60 * 60);
}
else
{
/* nonnegative value */
value.negative = false;
value.h = seconds / (60 * 60);
seconds -= value.h * 60 * 60;
}
value.m = (int) seconds / 60;
value.s = (int) seconds - value.m * 60;
display.repaint();
}
}
}
}
}, 0L, TIMER_REFRESH_INTERVAL_MILLISECONDS);
}
/**
* Start playing the alarm.
*/
private void startAlarm()
{
if ((alarm == null) || (!alarmEnabled))
{
return;
}
synchronized (alarm)
{
if (!alarmPlaying)
{
alarmPlaying = true;
alarm.loop();
if (trayIcon != null)
{
trayIcon.displayMessage(getTitle(), "Alarm Alert", MessageType.INFO);
}
}
}
}
/**
* Stop playing the alarm.
*/
private void stopAlarm()
{
if (alarm == null)
{
return;
}
synchronized (alarm)
{
if (alarmPlaying)
{
alarmPlaying = false;
alarm.stop();
}
}
}
/**
* Close this timer unit.
*/
private void closeTimer()
{
if (isRunning)
{
/* prompt user about running timer */
final int choice = JOptionPane.showConfirmDialog(
this,
"This timer is still running. Close this timer now?",
"Confirm Close - " + getTitle(),
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (choice != JOptionPane.YES_OPTION)
{
return;
}
}
parent.removeTimer(this);
setVisible(false);
timer.cancel();
stopAlarm();
dispose();
}
/**
* Minimize this timer unit.
*/
void minimizeTimer()
{
setExtendedState(JFrame.ICONIFIED);
if (minimizeToTray && (trayIcon != null))
{
try
{
final SystemTray systemTray = SystemTray.getSystemTray();
boolean alreadyInTray = false;
for (TrayIcon t : systemTray.getTrayIcons())
{
if (t.equals(trayIcon))
{
alreadyInTray = true;
break;
}
}
if (!alreadyInTray)
{
systemTray.add(trayIcon);
}
setVisible(false);
}
catch (Exception e)
{
SwingManipulator.showErrorDialog(
this,
"Error - " + getTitle(),
"Failed to minimize timer to tray (" + e + ").\n" +
"This timer will continue to stay on the desktop.");
}
}
}
/**
* Restore this timer unit.
*/
void restoreTimer()
{
setVisible(true);
setExtendedState(JFrame.NORMAL);
toFront();
if (trayIcon != null)
{
try
{
SystemTray.getSystemTray().remove(trayIcon);
}
catch (Exception e)
{
/* ignore */
}
}
}
/***************************
* NETBEANS-GENERATED CODE *
***************************/
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents()
{
buttonsPanel = new javax.swing.JPanel();
startButton = new javax.swing.JButton();
stopButton = new javax.swing.JButton();
resetButton = new javax.swing.JButton();
switchButton = new javax.swing.JButton();
setButton = new javax.swing.JButton();
buttonsPanel.setLayout(new java.awt.GridLayout(1, 0));
startButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_play_blue.png"))); // NOI18N
startButton.setMnemonic('a');
startButton.setToolTipText("<html>St<u>a</u>rt timer</html>");
buttonsPanel.add(startButton);
stopButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_stop_blue.png"))); // NOI18N
stopButton.setMnemonic('s');
stopButton.setToolTipText("<html><u>S</u>top timer</html>");
buttonsPanel.add(stopButton);
resetButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_eject_blue.png"))); // NOI18N
resetButton.setMnemonic('r');
resetButton.setToolTipText("<html><u>R</u>eset timer value to zero</html>");
buttonsPanel.add(resetButton);
switchButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_fastforward_blue.png"))); // NOI18N
switchButton.setMnemonic('w');
switchButton.setToolTipText("<html>S<u>w</u>itch between counting up and down</html>");
buttonsPanel.add(switchButton);
setButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/thistime/resources/control_equalizer_blue.png"))); // NOI18N
setButton.setMnemonic('e');
setButton.setToolTipText("<html>S<u>e</u>t/Lock timer value</html>");
buttonsPanel.add(setButton);
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 0, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 0, Short.MAX_VALUE)
);
pack();
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JPanel buttonsPanel;
private javax.swing.JButton resetButton;
private javax.swing.JButton setButton;
private javax.swing.JButton startButton;
private javax.swing.JButton stopButton;
private javax.swing.JButton switchButton;
// End of variables declaration//GEN-END:variables
}