/**
* GmailAssistant 2.0 (2008-09-07)
* Copyright 2008 Zach Scrivena
* zachscrivena@gmail.com
* http://gmailassistant.sourceforge.net/
*
* Notifier for multiple Gmail and Google Apps email 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 org.freeshell.zs.gmailassistant;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
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.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.NoSuchElementException;
import java.util.TreeSet;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumnModel;
import org.freeshell.zs.common.Debug;
import org.freeshell.zs.common.LoggerConsole;
import org.freeshell.zs.common.Downloader;
import org.freeshell.zs.common.ResourceManipulator;
import org.freeshell.zs.common.SimpleProperties;
import org.freeshell.zs.common.StringManipulator;
import org.freeshell.zs.common.SwingManipulator;
import org.freeshell.zs.common.TerminatingException;
/**
* Notifier for multiple Gmail and Google Apps email accounts.
* This is the main class of the program.
*/
final class GmailAssistant
extends JFrame
implements ListSelectionListener
{
/** default program properties file */
private static final String DEFAULT_PROGRAM_PROPERTIES =
"/org/freeshell/zs/gmailassistant/resources/program.properties.txt";
/** refresh interval, in milliseconds */
private static final long REFRESH_INTERVAL_MILLISECONDS = 100L;
/** program properties */
final SimpleProperties properties;
/** default account properties */
final SimpleProperties defaultAccountProperties;
/** program properties that can be saved and loaded */
final SimpleProperties savedProgramProperties;
/** account properties that can be saved and loaded */
final SimpleProperties savedAccountProperties;
/** program name */
final String name;
/** list view of email accounts */
private final List<Account> accountsList = new ArrayList<Account>();
/** navigable set view of email accounts */
private final NavigableSet<Account> accountsNavigableSet = new TreeSet<Account>();
/** mutex lock for <code>accounts</code> and <code>accountsNavigableSet</code> */
private final Object accountsLock = new Object();
/** table model for table of accounts */
private final AccountsTableModel accountsTableModel = new AccountsTableModel();
/** "Load Profile" form */
private ProfileLoader profileLoader = null;
/** "Save Profile" form */
private ProfileSaver profileSaver = null;
/** "Options" form */
Options options = null;
/** "Usage" dialog */
JDialog usage = null;
/** "About" form */
private About about = null;
/** tray icon for program */
final SimpleTrayIcon trayIcon;
/** popup alerter */
final DesktopPopup popup;
/** keyboard LED blinker */
final KeyboardLedBlinker led;
/** chime player */
final ChimePlayer chime;
/** last used account ID */
private int lastId;
/** mutex lock for this.lastId */
private final Object lastIdLock = new Object();
/** perform periodic refresh of the status label? */
volatile private boolean refreshStatus = true;
/** number of enabled accounts */
private volatile int numEnabledAccounts = 0;
/** number of unread mails */
private volatile int numUnreadMails = 0;
/** is debug mode on? */
final boolean debug;
/** logger console used when debug mode is on */
final LoggerConsole logger;
/** minimize the program on startup? */
private static boolean minimizeOnStartup = false;
/**
* Main entry point for the program.
*
* @param args
* command-line arguments
*/
public static void main(
final String[] args)
{
/************************************
* SCHEDULE GUI CREATION ON THE EDT *
************************************/
java.awt.EventQueue.invokeLater(new Runnable()
{
public void run()
{
try
{
/* use system look and feel if possible */
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
/* low-delay tooltips */
ToolTipManager.sharedInstance().setInitialDelay(50);
}
catch (Exception e)
{
/* ignore */
}
try
{
final GmailAssistant ga = new GmailAssistant(args);
if (minimizeOnStartup)
{
ga.minimizeProgram();
}
else
{
ga.setVisible(true);
}
}
catch (Exception e)
{
if (e instanceof TerminatingException)
{
SwingManipulator.showErrorDialog(
null,
"Initialization Error",
String.format("%s\nPlease file a bug report to help improve this program.\n\n%s\n\n%s",
e.getMessage(), Debug.getSystemInformationString(), Debug.getStackTraceString(e)));
}
else
{
SwingManipulator.showErrorDialog(
null,
"Initialization Error",
String.format("Failed to initialize program because of an unexpected error:\n%s" +
"\nPlease file a bug report to help improve this program.\n\n%s\n\n%s",
e.toString(), Debug.getSystemInformationString(), Debug.getStackTraceString(e)));
}
System.exit(1);
}
}
});
}
/**
* Constructor.
*
* @param args
* command-line arguments
*/
private GmailAssistant(
final String[] args)
{
/*************************
* INITIALIZE PROPERTIES *
*************************/
/* load default program properties */
try
{
properties = new SimpleProperties(ResourceManipulator.resourceAsString(GmailAssistant.DEFAULT_PROGRAM_PROPERTIES));
}
catch (Exception e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to load default program properties (%s).", e.toString()));
}
/* compile regex patterns, if any */
final List<String> patterns = new ArrayList<String>();
for (String k : properties.keySet())
{
if (k.endsWith(".pattern"))
{
patterns.add(k);
}
}
for (String k: patterns)
{
final String v = properties.getString(k);
try
{
properties.set(k + ".object", Pattern.compile(v));
}
catch (Exception e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to compile regex pattern \"%s\" (%s).", v, e.toString()));
}
}
/* load default account properties */
try
{
defaultAccountProperties = new SimpleProperties(ResourceManipulator.resourceAsString(properties.getString("account.properties")));
}
catch (Exception e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to load default account properties (%s).", e.toString()));
}
/* keys of program properties that can be saved/loaded in the profile */
try
{
savedProgramProperties = new SimpleProperties(ResourceManipulator.resourceAsString(properties.getString("saved.program.properties")));
}
catch (Exception e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to load saved program properties (%s).", e.toString()));
}
/* keys of account properties that should be saved/loaded in the profile */
try
{
savedAccountProperties = new SimpleProperties(ResourceManipulator.resourceAsString(properties.getString("saved.account.properties")));
}
catch (Exception e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to load saved account properties (%s).", e.toString()));
}
/* program name */
name = properties.getString("name");
/*********************
* INITIALIZE ALERTS *
*********************/
popup = new DesktopPopup(this);
chime = new ChimePlayer(this);
led = new KeyboardLedBlinker(this);
/******************************
* INITIALIZE FORM COMPONENTS *
******************************/
initComponents();
/*****************************
* CONFIGURE FORM COMPONENTS *
*****************************/
setTitle(name);
addWindowListener(new WindowAdapter()
{
@Override
public void windowIconified(WindowEvent e)
{
minimizeProgram();
}
@Override
public void windowDeiconified(WindowEvent e)
{
restoreProgram();
}
@Override
public void windowClosing(WindowEvent e)
{
closeProgram();
}
});
/* menu item: "Load Profiles..." */
loadItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (profileLoader == null)
{
profileLoader = new ProfileLoader(GmailAssistant.this);
}
profileLoader.showForm();
}
});
/* menu item: "Save Profile..." */
saveItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (profileSaver == null)
{
profileSaver = new ProfileSaver(GmailAssistant.this);
}
profileSaver.showForm();
}
});
/* menu item: "Exit" */
exitItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
closeProgram();
}
});
/* menu item: "Options..." */
optionsItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (options == null)
{
options = new Options(GmailAssistant.this);
}
options.showForm();
}
});
/* menu item: "Usage..." */
usageItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (usage == null)
{
try
{
usage = SwingManipulator.createModelessInfoDialog(
GmailAssistant.this,
String.format("Usage - %s", name),
String.format("Usage information for %s", name),
ResourceManipulator.resourceAsString(properties.getString("usage")),
10);
}
catch (Exception ex)
{
SwingManipulator.showErrorDialog(
GmailAssistant.this,
name,
String.format("(INTERNAL) Failed to load \"Usage\" text (%s).", ex.toString()));
}
}
usage.setVisible(true);
}
});
/* menu item: "About..." */
aboutItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if (about == null)
{
about = new About(GmailAssistant.this);
}
about.showForm();
}
});
/* menu item: "Check for Update" */
updateItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
new Thread(new Runnable()
{
public void run()
{
checkForUpdate(true, true);
}
}).start();
}
});
/* button: "Add" */
addButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
addNewAccount(null);
valueChanged(null);
}
});
/* button: "Remove" */
removeButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
removeButton.setEnabled(false);
removeAccounts();
valueChanged(null);
}
});
/* button: "Edit" */
editButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
editButton.setEnabled(false);
editAccounts();
valueChanged(null);
}
});
/* button: "Enable" */
enableButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
enableButton.setEnabled(false);
enableAccounts();
valueChanged(null);
}
});
/* button: "Disable" */
disableButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
disableButton.setEnabled(false);
disableAccounts();
valueChanged(null);
}
});
/* key binding: ESCAPE key */
accountsPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_CANCEL_BUTTON");
accountsPane.getActionMap().put("ESCAPE_CANCEL_BUTTON", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
minimizeProgram();
}
});
/* key binding: DELETE key */
accountsPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "DELETE_REMOVE_BUTTON");
accountsPane.getActionMap().put("DELETE_REMOVE_BUTTON", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
removeButton.doClick();
}
});
/* table of accounts */
accountsTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
final TableColumnModel colModel = accountsTable.getColumnModel();
colModel.getColumn(0).setMinWidth(0);
colModel.getColumn(1).setMinWidth(0);
colModel.getColumn(2).setMinWidth(0);
colModel.getColumn(3).setMinWidth(0);
colModel.getColumn(0).setPreferredWidth(5);
colModel.getColumn(1).setPreferredWidth(80);
colModel.getColumn(2).setPreferredWidth(40);
colModel.getColumn(3).setPreferredWidth(200);
accountsTable.setToolTipText("Mouse-over any row for more information");
accountsTable.getSelectionModel().addListSelectionListener(this);
accountsTable.setDefaultRenderer(TableCellContent.class, new AccountsTableCellRenderer(
accountsTable.getForeground(),
accountsTable.getBackground(),
accountsTable.getSelectionForeground(),
accountsTable.getSelectionBackground()));
/* accounts popup menu (add, remove, edit, enable, disable) */
final JPopupMenu accountsPopupMenu = new JPopupMenu();
/* accounts popup menu: "Add" */
final JMenuItem addMenuItem = new JMenuItem("Add", 'A');
addMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
addButton.doClick();
}
});
accountsPopupMenu.add(addMenuItem);
/* accounts popup menu: "Remove" */
final JMenuItem removeMenuItem = new JMenuItem("Remove", 'R');
removeMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
removeButton.doClick();
}
});
accountsPopupMenu.add(removeMenuItem);
/* accounts popup menu: "Edit" */
final JMenuItem editMenuItem = new JMenuItem("Edit", 't');
editMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
editButton.doClick();
}
});
accountsPopupMenu.add(editMenuItem);
/* accounts popup menu: "Enable" */
final JMenuItem enableMenuItem = new JMenuItem("Enable", 'E');
enableMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
enableButton.doClick();
}
});
accountsPopupMenu.add(enableMenuItem);
/* accounts popup menu: "Disable" */
final JMenuItem disableMenuItem = new JMenuItem("Disable", 'D');
disableMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
disableButton.doClick();
}
});
accountsPopupMenu.add(disableMenuItem);
accountsPopupMenu.addSeparator();
/* accounts popup menu: "Select All" */
final JMenuItem selectAllMenuItem = new JMenuItem("Select All", 'S');
selectAllMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
accountsTable.selectAll();
}
});
accountsPopupMenu.add(selectAllMenuItem);
/* accounts popup menu: "Clear All" */
final JMenuItem clearAllMenuItem = new JMenuItem("Clear All", 'C');
clearAllMenuItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
accountsTable.clearSelection();
}
});
accountsPopupMenu.add(clearAllMenuItem);
accountsTable.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())
{
/* display popup menu */
final int i = accountsTable.rowAtPoint(e.getPoint());
if ((i >= 0) && (!accountsTable.isRowSelected(i)))
{
accountsTable.setRowSelectionInterval(i, i);
}
accountsPopupMenu.show(e.getComponent(), e.getX(), e.getY());
}
else if (SwingUtilities.isLeftMouseButton(e))
{
final int i = accountsTable.rowAtPoint(e.getPoint());
if (e.getClickCount() >= 2)
{
/* open selected account for editing */
if (i >= 0)
{
accountsTable.setRowSelectionInterval(i, i);
editButton.doClick();
}
}
else
{
/* clear selection */
if (i < 0)
{
accountsTable.clearSelection();
}
}
}
}
});
/* center form on the screen */
setLocationRelativeTo(null);
/*************************************************
* SETUP PROGRAM ICON, TRAY ICON, AND POPUP MENU *
*************************************************/
/* setup program icon */
try
{
setIconImage(ImageIO.read(GmailAssistant.class.getResource("/org/freeshell/zs/gmailassistant/resources/ga_logo_64.png")));
}
catch (IOException e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to load program icon image (%s).", e.toString()));
}
/* create popup menu for system tray icon */
final PopupMenu trayMenu = new PopupMenu(name);
/* popup menu: "Check Mail Now" */
final MenuItem checkItem = new MenuItem("Check Mail Now");
checkItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
checkMailNow();
}
});
trayMenu.add(checkItem);
/* popup menu: "Tell me again..." */
final MenuItem againItem = new MenuItem("Tell me Again...");
againItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
popup.showAllMessages();
}
});
trayMenu.add(againItem);
/* popup menu: "Reset Alerts" */
final MenuItem resetItem = new MenuItem("Reset Alerts");
resetItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
resetAlerts();
}
});
trayMenu.add(resetItem);
trayMenu.addSeparator();
/* popup menu: "Restore" */
final MenuItem restoreItem = new MenuItem("Restore");
restoreItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
restoreProgram();
}
});
trayMenu.add(restoreItem);
/* popup menu: "Exit" */
final MenuItem exitTrayItem = new MenuItem("Exit");
exitTrayItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
closeProgram();
}
});
trayMenu.add(exitTrayItem);
try
{
trayIcon = new SimpleTrayIcon(this);
trayIcon.setPopupMenu(trayMenu);
trayIcon.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
restoreProgram();
}
});
SystemTray.getSystemTray().add(trayIcon);
}
catch (IOException e)
{
throw new TerminatingException(String.format("(INTERNAL) Failed to load system tray icon images (%s).", e.toString()));
}
catch (AWTException ex)
{
throw new TerminatingException("Failed to set system tray icon. If you are using a Linux operating system," +
" try disabling the advanced visual effects of the window manager.");
}
/*************************
* REFRESH PROGRAM MODES *
*************************/
refreshAlwaysOnTopMode();
refreshProxyMode();
valueChanged(null);
/**************************
* START CHECK FOR UPDATE *
**************************/
new Thread(new Runnable()
{
public void run()
{
checkForUpdate(false, false);
}
}).start();
/**********************************
* PROCESS COMMAND-LINE ARGUMENTS *
**********************************/
final List<File> profileFiles = new ArrayList<File>();
boolean debugSwitch = false;
boolean noloadSwitch = false;
for (String s : args)
{
try
{
if (s.startsWith("--load:"))
{
/* "load profile" */
final String a = s.substring(s.indexOf(':') + 1);
if (a.isEmpty())
{
throw new IllegalArgumentException("Empty --load parameter: A filename must be specified, e.g. --load:\"myprofile.ga\".");
}
profileFiles.add(new File(a));
}
else if ("--noload".equals(s))
{
noloadSwitch = true;
}
else if ("--debug".equals(s))
{
debugSwitch = true;
}
else if ("--minimize".equals(s))
{
minimizeOnStartup = true;
}
else
{
/* invalid switch */
throw new IllegalArgumentException(String.format("\"%s\" is not a valid command-line switch.", s));
}
}
catch (IllegalArgumentException e)
{
SwingManipulator.showErrorDialog(
null,
String.format("Initialization Error - %s", name),
String.format("%s\n%s will ignore this switch. Please see Help > Usage for more information.",
e.getMessage(), name));
}
}
debug = debugSwitch;
final boolean noload = noloadSwitch;
/* create debug console menu item if necessary */
if (debug)
{
logger = new LoggerConsole(String.format("Debug Console - %s", name));
final JMenuItem debugItem = new JMenuItem("Show Debug Console", 'd');
debugItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
logger.showForm();
}
});
helpMenu.add(debugItem);
}
else
{
logger = null;
}
/*****************
* LOAD PROFILES *
*****************/
new Thread(new Runnable()
{
public void run()
{
/* find "*.ga" profile files in the current directory, if necessary */
if ((!noload) && profileFiles.isEmpty())
{
File d = new File(".");
try
{
d = d.getCanonicalFile();
}
catch (Exception e)
{
/* ignore */
}
final File[] listFiles = d.listFiles();
if (listFiles != null)
{
for (File f : listFiles)
{
if (f.getName().endsWith("." + properties.getString("profile.default.extension")))
{
profileFiles.add(f);
}
}
}
/* sort alphabetically */
Collections.sort(profileFiles);
}
/* load profiles */
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
loadProfiles(profileFiles);
}
});
}
}).start();
/************************************************************
* START MONITORING NUMBER OF UNREAD MAILS & EMAIL ACCOUNTS *
************************************************************/
new Thread(new Runnable()
{
public void run()
{
while (true)
{
if (refreshStatus)
{
final int m = numUnreadMails;
final int a = numEnabledAccounts;
SwingManipulator.updateLabel(
status,
String.format(" %d unread %s (monitoring %d %s)",
m, (m == 1) ? "mail" : "mails", a, (a == 1) ? "account" : "accounts"));
}
Debug.sleep(10 * REFRESH_INTERVAL_MILLISECONDS);
}
}
}).start();
}
/**
* Check for update.
* This method should run on a dedicated worker thread, not the EDT.
*
* @param showPromptIfLatest
* should a user prompt be displayed even if the current program is the latest release?
* @param showPromptOnError
* should a user prompt be displayed if an error occurs?
*/
private void checkForUpdate(
final boolean showPromptIfLatest,
final boolean showPromptOnError)
{
refreshStatus = false;
SwingManipulator.updateLabel(status, " Checking for update...");
final SimpleProperties updateProperties = new SimpleProperties();
try
{
/* try to retrieve latest defintions from homepage */
final StringBuilder sb = new StringBuilder();
final Downloader d = new Downloader(
new URL(properties.getString("update.url")),
sb);
new Thread(d).start();
while (true)
{
if (d.isProgressUpdated())
{
final int percent = d.getProgressPercent();
SwingManipulator.updateLabel(
status,
String.format(" Checking for update: %s %s",
d.getProgressString(), (percent >= 0) ? String.format("(%d%%)", percent) : ""));
}
if (d.isCompleted())
{
d.waitUntilCompleted();
break;
}
Debug.sleep(REFRESH_INTERVAL_MILLISECONDS);
}
/* process update information */
SwingManipulator.updateLabel(status, " Checking for update...");
for (String s : sb.toString().split("[\n\r\u0085\u2028\u2028]++"))
{
if (s.isEmpty() || s.startsWith("#"))
{
/* ignore empty lines and comments */
}
else if (s.contains(":"))
{
/* process "key:value" pair */
final String[] kv = StringManipulator.parseKeyValueString(s);
updateProperties.setString(kv[0], kv[1]);
}
}
}
catch (Exception e)
{
if (showPromptOnError)
{
SwingManipulator.showErrorDialog(
this,
String.format("Check for Update - %s", name),
String.format("Failed to retrieve update information from the %s homepage (%s).\nPlease try again later. If the problem persists, please visit the homepage manually.",
name, e.toString()));
}
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
return;
}
/* show user prompt if an update is available */
try
{
final String version = updateProperties.getString("update.version");
final String date = updateProperties.getString("update.date");
final String download = updateProperties.getString("update.download");
final String comment = updateProperties.getString("update.comment");
if (properties.getString("date").compareTo(date) < 0)
{
/* new release is available */
final int choice = SwingManipulator.showModalOptionTextDialog(
this,
String.format("A new release of %s is available", name),
String.format("%s %s (%s) is available for download. %s\n\n%s",
name, version, date,
Double.parseDouble(properties.getString("version")) < Double.parseDouble(version) ?
"This is a major update and is strongly recommended." :
"This is a minor update and is recommended if you are currently experiencing problems.",
comment),
8,
String.format("Check for Update - %s", name),
JOptionPane.DEFAULT_OPTION,
JOptionPane.INFORMATION_MESSAGE,
null,
new String[] {"Download Now", "Cancel"},
0);
/* download now */
if (choice == 0)
{
try
{
Desktop.getDesktop().browse(new URI(download));
}
catch (Exception ex)
{
/* ignore */
}
}
}
else
{
/* this release is up-to-date */
if (showPromptIfLatest)
{
SwingManipulator.showInfoDialog(
this,
String.format("Check for Update - %s", name),
"No update found",
String.format("This release of %s %s (%s) is already up-to-date.",
name, properties.getString("version"), properties.getString("date")),
5);
}
}
}
catch (Exception e)
{
/* unable to parse */
if (showPromptOnError)
{
SwingManipulator.showWarningDialog(
this,
String.format("Check for Update - %s", name),
String.format("Failed to parse update information from the %s homepage (%s).\nPlease try again later. If the problem persists, please visit the homepage manually.",
name, e.toString()));
}
}
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
}
/**
* Respond to a list selection event on the table of accounts.
* Enables or disables the appropriate buttons on the form according to the current
* table selection.
* This method must run on the EDT.
*
* @param e
* list selection event
*/
@Override
public void valueChanged(
ListSelectionEvent e)
{
if (accountsTable.getSelectedRowCount() == 0)
{
removeButton.setEnabled(false);
editButton.setEnabled(false);
enableButton.setEnabled(false);
disableButton.setEnabled(false);
}
else
{
removeButton.setEnabled(true);
editButton.setEnabled(true);
enableButton.setEnabled(true);
disableButton.setEnabled(true);
}
}
/**
* Check for unread mails in all accounts now.
*/
private void checkMailNow()
{
synchronized (accountsLock)
{
for (Account ac : accountsList)
{
ac.checkMailNow();
}
}
}
/**
* Reset all alerts.
*/
private void resetAlerts()
{
trayIcon.setNormalIcon();
popup.cancelAllMessages();
chime.cancelAll();
chime.stopPeriodicBell();
led.cancelAll();
synchronized (accountsLock)
{
for (Account ac : accountsList)
{
ac.properties.setBoolean("new.unread.mails", false);
}
}
}
/**
* Load the specified profile files.
* This method must run on the EDT.
*
* @param files
* profile files to be loaded
*/
private void loadProfiles(
final List<File> files)
{
if (profileLoader == null)
{
profileLoader = new ProfileLoader(this);
}
profileLoader.loadProfiles(files);
}
/**
* Add a new account with the specified properties.
* The account form is made visible for editing, if necessary.
* This method can be called on any thread.
*
* @param accountProperties
* account properties for the new account; if null, the default properties
* will be applied
*/
void addNewAccount(
final SimpleProperties accountProperties)
{
new Thread(new Runnable()
{
public void run()
{
refreshStatus = false;
SwingManipulator.updateLabel(status, " Adding new account...");
/* increment last account ID used */
final int id;
synchronized (lastIdLock)
{
id = ++lastId;
}
final Account ac;
final boolean editAccount;
if (accountProperties == null)
{
editAccount = true;
ac = new Account(GmailAssistant.this, id, new SimpleProperties(defaultAccountProperties));
}
else
{
if (accountProperties.getBoolean("enabled") &&
accountProperties.getString("password").isEmpty())
{
editAccount = true;
accountProperties.setBoolean("enabled", false);
}
else
{
editAccount = false;
}
ac = new Account(GmailAssistant.this, id, accountProperties);
}
/* add newly created account to table of accounts */
synchronized (accountsLock)
{
accountsList.add(ac);
accountsNavigableSet.add(ac);
}
refreshTotalUnreadMailCount();
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
/* make account form visible for editing, if necessary */
if (editAccount)
{
ac.editAccount();
}
accountsTableModel.fireTableDataChanged();
}
});
}
}).start();
}
/**
* Remove the selected accounts.
* This method must run on the EDT.
*/
private void removeAccounts()
{
refreshStatus = false;
SwingManipulator.updateLabel(status, " Removing selected accounts...");
synchronized (accountsLock)
{
final List<Account> accountsToRemove = new ArrayList<Account>();
for (int i : accountsTable.getSelectedRows())
{
final Account ac = accountsList.get(accountsTable.convertRowIndexToModel(i));
accountsToRemove.add(ac);
}
for (Account ac : accountsToRemove)
{
accountsList.remove(ac);
accountsNavigableSet.remove(ac);
ac.removeAccount();
}
}
accountsTableModel.fireTableDataChanged();
refreshTotalUnreadMailCount();
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
}
/**
* Edit the selected accounts.
* This method must run on the EDT.
*/
private void editAccounts()
{
refreshStatus = false;
SwingManipulator.updateLabel(status, " Opening selected accounts for editing...");
synchronized (accountsLock)
{
for (int i : accountsTable.getSelectedRows())
{
final Account ac = accountsList.get(accountsTable.convertRowIndexToModel(i));
ac.editAccount();
}
}
refreshTotalUnreadMailCount();
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
}
/**
* Enable the selected accounts.
* This method must run on the EDT.
*/
private void enableAccounts()
{
refreshStatus = false;
SwingManipulator.updateLabel(status, " Enabling selected accounts...");
synchronized (accountsLock)
{
for (int i : accountsTable.getSelectedRows())
{
final Account ac = accountsList.get(accountsTable.convertRowIndexToModel(i));
ac.enableAccount();
}
}
refreshTotalUnreadMailCount();
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
}
/**
* Disable the selected accounts.
* This method must run on the EDT.
*/
private void disableAccounts()
{
refreshStatus = false;
SwingManipulator.updateLabel(status, " Disabling selected accounts...");
synchronized (accountsLock)
{
for (int i : accountsTable.getSelectedRows())
{
final Account ac = accountsList.get(accountsTable.convertRowIndexToModel(i));
ac.disableAccount();
}
}
refreshTotalUnreadMailCount();
SwingManipulator.updateLabel(status, " ");
refreshStatus = true;
}
/**
* Refresh the specified account on the table.
* This method can be called on any thread.
*
* @param ac
* account to be refreshed
*/
void refreshAccountOnTable(
final Account ac)
{
synchronized (accountsLock)
{
final int i = accountsList.indexOf(ac);
if (i >= 0)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
accountsTableModel.fireTableRowsUpdated(i, i);
valueChanged(null);
}
});
}
}
}
/**
* Refresh the total number of unread mails for all accounts.
* This method can be called on any thread.
*/
void refreshTotalUnreadMailCount()
{
int countUnreadMails = 0;
int countEnabledAccounts = 0;
boolean error = false;
boolean keepHotIcon = false;
boolean keepPopup = false;
boolean keepChime = false;
boolean keepPeriodicBell = false;
boolean keepLed = false;
synchronized (accountsLock)
{
for (Account ac : accountsList)
{
if (ac.properties.getBoolean("enabled"))
{
countEnabledAccounts++;
}
int unreadMails = ac.properties.getInt("unread.mails");
/* handle case of "N/A" unread mails */
if (unreadMails < 0)
{
unreadMails = 0;
}
/* update total number of unread mails */
countUnreadMails += unreadMails;
if (unreadMails == 0)
{
ac.properties.setBoolean("new.unread.mails", false);
}
else
{
/* keep "Popup" alerts if there are any unread mails */
keepPopup = true;
if (ac.properties.getBoolean("new.unread.mails"))
{
/* keep hot icon if there are NEW unread mails */
keepHotIcon = true;
/* keep "Chime", "Periodic Bell", and "LED" alerts if there are NEW unread mails */
if (ac.properties.getBoolean("alert.chime"))
{
keepChime = true;
}
if (ac.properties.getBoolean("alert.periodic.bell"))
{
keepPeriodicBell = true;
}
if (ac.properties.getBoolean("alert.led"))
{
keepLed = true;
}
}
}
/* has an error occured in any account? */
if (ac.properties.getBoolean("error"))
{
error = true;
}
}
}
numUnreadMails = countUnreadMails;
numEnabledAccounts = countEnabledAccounts;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
final int m = numUnreadMails;
trayIcon.setToolTip(String.format("%s (%d unread %s)",
properties.getString("name"), m, (m == 1) ? "mail" : "mails"));
}
});
if (!keepHotIcon)
{
trayIcon.setNormalIcon();
}
if (!keepPopup)
{
popup.cancelAllMessages();
}
if (!keepChime)
{
chime.cancelAll();
}
if (!keepPeriodicBell)
{
chime.stopPeriodicBell();
}
if (!keepLed)
{
led.cancelAll();
}
if (error)
{
trayIcon.setErrorIcon();
}
else
{
trayIcon.clearErrorIcon();
}
}
/**
* Close the program.
* This method can be called on any thread.
* This method calls System.exit() and never returns.
*/
private void closeProgram()
{
final int a = numEnabledAccounts;
if (a > 0)
{
/* prompt user about timers that are still running */
final int choice = JOptionPane.showConfirmDialog(
GmailAssistant.this,
String.format("%s is still monitoring %d %s. Exit now?",
name, a, (a == 1) ? "account" : "accounts"),
String.format("Confirm Exit - %s", name),
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (choice != JOptionPane.YES_OPTION)
{
return;
}
}
led.terminate();
System.exit(0);
}
/**
* Minimize the program to the tray.
* This method must run on the EDT.
*/
private void minimizeProgram()
{
setExtendedState(JFrame.ICONIFIED);
setVisible(false);
}
/**
* Restore the program from the tray.
* This method must run on the EDT.
*/
private void restoreProgram()
{
setVisible(true);
setExtendedState(JFrame.NORMAL);
toFront();
}
/**
* Get the first account.
* This method can be called on any thread.
*
* @return
* first account; null if there are no accounts
*/
Account getFirstAccount()
{
try
{
synchronized (accountsLock)
{
return accountsNavigableSet.first();
}
}
catch (NoSuchElementException e)
{
return null;
}
}
/**
* Get the last account.
* This method can be called on any thread.
*
* @return
* last account; null if there are no accounts
*/
Account getLastAccount()
{
try
{
synchronized (accountsLock)
{
return accountsNavigableSet.last();
}
}
catch (NoSuchElementException e)
{
return null;
}
}
/**
* Get the next account that comes after the specified account.
* This method can be called on any thread.
*
* @param ac
* account
* @return
* next account that comes after <code>ac</code>; null if there is none
*/
Account getNextAccount(
final Account ac)
{
synchronized (accountsLock)
{
return accountsNavigableSet.higher(ac);
}
}
/**
* Get the previous account that comes before the specified account.
* This method can be called on any thread.
*
* @param ac
* account
* @return
* previous account that comes before <code>ac</code>; null if there is none
*/
Account getPreviousAccount(
final Account ac)
{
synchronized (accountsLock)
{
return accountsNavigableSet.lower(ac);
}
}
/**
* Refresh the "Always on Top" mode of the program.
* This method must run on the EDT.
*/
void refreshAlwaysOnTopMode()
{
final boolean state = properties.getBoolean("always.on.top");
final JFrame[] frames = new JFrame[]
{
this,
profileLoader,
profileSaver,
options,
about
};
for (JFrame f : frames)
{
if (f != null)
{
try
{
f.setAlwaysOnTop(state);
}
catch (Exception ee)
{
/* ignore */
}
}
}
synchronized (accountsLock)
{
/* propagate mode to other forms */
for (Account ac : accountsList)
{
try
{
ac.setAlwaysOnTop(state);
}
catch (Exception ee)
{
/* ignore */
}
}
}
}
/**
* Refresh SOCKS proxy mode.
*/
void refreshProxyMode()
{
Debug.setSystemProperty("socksProxyHost", properties.getString("proxy.host"));
Debug.setSystemProperty("socksProxyPort", properties.getString("proxy.port"));
Debug.setSystemProperty("java.net.socks.username", properties.getString("proxy.username"));
Debug.setSystemProperty("java.net.socks.password", properties.getString("proxy.password"));
}
/**
* Save account properties as byte arrays in the given list.
*
* @param bytes
* list of byte arrays to be populated
* @param saveAccountPasswords
* should account passwords be saved?
* @param charset
* character set encoding
* @param newlineBytes
* byte array representing the new line character
* @param colonBytes
* byte array representing the colon character
* @param leftAngleBytes
* byte array representing the left angle character
* @param rightAngleBytes
* byte array representing the right angle character
* @param leftSquareBytes
* byte array representing the left square character
* @param rightSquareBytes
* byte array representing the right square character
* @throws java.io.UnsupportedEncodingException
* on any errors in character set encoding
*/
void saveAccountProperties(
final List<byte[]> bytes,
final boolean saveAccountPasswords,
final String charset,
final byte[] newlineBytes,
final byte[] colonBytes,
final byte[] leftAngleBytes,
final byte[] rightAngleBytes,
final byte[] leftSquareBytes,
final byte[] rightSquareBytes)
throws UnsupportedEncodingException
{
synchronized (accountsLock)
{
for (Account ac : accountsList)
{
/* "[username]" */
bytes.add(leftSquareBytes);
bytes.add(ac.properties.getAsString("username").getBytes(charset));
bytes.add(rightSquareBytes);
bytes.add(newlineBytes);
/* save password, if requested */
if (saveAccountPasswords)
{
bytes.add("password".getBytes(charset));
bytes.add(colonBytes);
bytes.add(ac.properties.getAsString("password").getBytes(charset));
bytes.add(newlineBytes);
}
for (String k : savedAccountProperties.keySet())
{
/* write "key:value" pair */
bytes.add(k.getBytes(charset));
bytes.add(colonBytes);
bytes.add(ac.properties.getAsString(k).getBytes(charset));
bytes.add(newlineBytes);
}
}
}
}
/**
* Provide a string description for a specified time duration.
*
* @param millis
* time duration in milliseconds
* @return
* string description for the specified time duration (e.g. "5 minutes", "1 hour")
*/
static String timeDurationString(
final long millis)
{
String s;
final long t = (millis == Long.MIN_VALUE) ? Long.MAX_VALUE : Math.abs(millis);
final long seconds = t / 1000L;
final long minutes = t / 60000L;
final long hours = t / 3600000L;
final long days = t / 86400000L;
if (days >= 2)
{
s = days + " days";
}
else if (hours >= 2)
{
s = hours + " hours";
}
else if (minutes >= 2)
{
s = minutes + " minutes";
}
else if (seconds >= 2)
{
s = seconds + " seconds";
}
else if (t > 0)
{
s = "1 second";
}
else
{
s = "0 seconds";
}
return s;
}
/******************
* NESTED CLASSES *
******************/
/**
* Represent the model for the table of accounts.
*/
private class AccountsTableModel
extends AbstractTableModel
{
/** name of each column (headers) */
private final String[] columnNames =
{
"",
"Username",
"Unread mails",
"Status"
};
/** class of each column */
private final Class[] columnClasses =
{
TableCellContent.class,
TableCellContent.class,
TableCellContent.class,
TableCellContent.class
};
/** cached copy of the table cell contents, for rendering */
private final Map<Account,TableCellContent[]> contents = new HashMap<Account,TableCellContent[]>();
@Override
public int getRowCount()
{
synchronized (accountsLock)
{
return accountsList.size();
}
}
@Override
public int getColumnCount()
{
return columnNames.length;
}
@Override
public String getColumnName(
int col)
{
return columnNames[col];
}
@Override
public Class getColumnClass(
int col)
{
return columnClasses[col];
}
@Override
public Object getValueAt(
int row,
int col)
{
Account ac;
synchronized (accountsLock)
{
ac = accountsList.get(row);
}
TableCellContent[] c;
synchronized (contents)
{
c = contents.get(ac);
if (c == null)
{
c = new TableCellContent[columnNames.length];
for (int i = 0; i < columnNames.length; i++)
{
c[i] = new TableCellContent(ac);
}
contents.put(ac, c);
/* initialize invariant properties of the cell contents */
/* color */
c[0].align = JLabel.CENTER;
/* username */
c[1].align = JLabel.CENTER;
/* unread mails */
c[2].text = "N/A";
c[2].align = JLabel.CENTER;
}
}
switch (col)
{
case 0:
/* color */
c[col].text = String.format(
"<html><span style='color:%1$s;background-color:%1$s'> M </span></html>",
ac.properties.getString("color.html"));
break;
case 1:
/* username */
final String username = ac.properties.getString("username");
c[col].text = username.isEmpty() ? ("New Account #" + ac.accountId) : username;
break;
case 2:
/* unread mails */
final int n = ac.properties.getInt("unread.mails");
c[col].value = n;
c[col].text = (n >= 0) ? Integer.toString(n) : "N/A";
break;
case 3:
/* status */
c[col].text = ac.properties.getString("status");
break;
default:
return null;
}
return c[col];
}
}
/***************************
* 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() {
accountsPane = new javax.swing.JScrollPane();
accountsTable = new javax.swing.JTable();
buttonsPanel = new javax.swing.JPanel();
addButton = new javax.swing.JButton();
removeButton = new javax.swing.JButton();
editButton = new javax.swing.JButton();
enableButton = new javax.swing.JButton();
disableButton = new javax.swing.JButton();
status = new javax.swing.JLabel();
menuBar = new javax.swing.JMenuBar();
fileMenu = new javax.swing.JMenu();
loadItem = new javax.swing.JMenuItem();
saveItem = new javax.swing.JMenuItem();
exitItem = new javax.swing.JMenuItem();
toolsMenu = new javax.swing.JMenu();
optionsItem = new javax.swing.JMenuItem();
helpMenu = new javax.swing.JMenu();
usageItem = new javax.swing.JMenuItem();
aboutItem = new javax.swing.JMenuItem();
updateItem = new javax.swing.JMenuItem();
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
accountsTable.setAutoCreateRowSorter(true);
accountsTable.setModel(this.accountsTableModel);
accountsTable.setFillsViewportHeight(true);
accountsPane.setViewportView(accountsTable);
accountsTable.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
buttonsPanel.setLayout(new java.awt.GridLayout(1, 0));
addButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/gmailassistant/resources/add.png"))); // NOI18N
addButton.setMnemonic('A');
addButton.setText("Add");
addButton.setToolTipText("Add a new account");
addButton.setIconTextGap(8);
addButton.setNextFocusableComponent(addButton);
buttonsPanel.add(addButton);
removeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/gmailassistant/resources/cross.png"))); // NOI18N
removeButton.setMnemonic('R');
removeButton.setText("Remove");
removeButton.setToolTipText("Remove selected accounts");
removeButton.setIconTextGap(8);
removeButton.setNextFocusableComponent(removeButton);
buttonsPanel.add(removeButton);
editButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/gmailassistant/resources/pencil.png"))); // NOI18N
editButton.setMnemonic('t');
editButton.setText("Edit");
editButton.setToolTipText("Edit selected accounts");
editButton.setIconTextGap(8);
editButton.setNextFocusableComponent(editButton);
buttonsPanel.add(editButton);
enableButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/gmailassistant/resources/lightbulb.png"))); // NOI18N
enableButton.setMnemonic('E');
enableButton.setText("Enable");
enableButton.setToolTipText("Enable selected accounts");
enableButton.setIconTextGap(8);
enableButton.setNextFocusableComponent(enableButton);
buttonsPanel.add(enableButton);
disableButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/freeshell/zs/gmailassistant/resources/lightbulb_off.png"))); // NOI18N
disableButton.setMnemonic('D');
disableButton.setText("Disable");
disableButton.setToolTipText("Disable selected accounts");
disableButton.setIconTextGap(8);
disableButton.setNextFocusableComponent(disableButton);
buttonsPanel.add(disableButton);
status.setText(" ");
fileMenu.setMnemonic('F');
fileMenu.setText("File");
loadItem.setMnemonic('L');
loadItem.setText("Load Profiles...");
fileMenu.add(loadItem);
saveItem.setMnemonic('S');
saveItem.setText("Save Profile...");
fileMenu.add(saveItem);
exitItem.setMnemonic('x');
exitItem.setText("Exit");
fileMenu.add(exitItem);
menuBar.add(fileMenu);
toolsMenu.setMnemonic('O');
toolsMenu.setText("Tools");
optionsItem.setMnemonic('o');
optionsItem.setText("Options...");
toolsMenu.add(optionsItem);
menuBar.add(toolsMenu);
helpMenu.setMnemonic('H');
helpMenu.setText("Help");
usageItem.setMnemonic('u');
usageItem.setText("Usage...");
helpMenu.add(usageItem);
aboutItem.setMnemonic('a');
aboutItem.setText("About...");
helpMenu.add(aboutItem);
updateItem.setMnemonic('c');
updateItem.setText("Check for Update");
helpMenu.add(updateItem);
menuBar.add(helpMenu);
setJMenuBar(menuBar);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(buttonsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 485, Short.MAX_VALUE)
.addComponent(accountsPane, javax.swing.GroupLayout.DEFAULT_SIZE, 485, Short.MAX_VALUE))
.addContainerGap())
.addComponent(status, javax.swing.GroupLayout.DEFAULT_SIZE, 505, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addComponent(accountsPane, javax.swing.GroupLayout.DEFAULT_SIZE, 128, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(buttonsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 35, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(status))
);
pack();
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JMenuItem aboutItem;
private javax.swing.JScrollPane accountsPane;
private javax.swing.JTable accountsTable;
private javax.swing.JButton addButton;
private javax.swing.JPanel buttonsPanel;
private javax.swing.JButton disableButton;
private javax.swing.JButton editButton;
private javax.swing.JButton enableButton;
private javax.swing.JMenuItem exitItem;
private javax.swing.JMenu fileMenu;
private javax.swing.JMenu helpMenu;
private javax.swing.JMenuItem loadItem;
private javax.swing.JMenuBar menuBar;
private javax.swing.JMenuItem optionsItem;
private javax.swing.JButton removeButton;
private javax.swing.JMenuItem saveItem;
private javax.swing.JLabel status;
private javax.swing.JMenu toolsMenu;
private javax.swing.JMenuItem updateItem;
private javax.swing.JMenuItem usageItem;
// End of variables declaration//GEN-END:variables
}