/* * Copyright © 2010 Martin Riedel * * This file is part of TransFile. * * TransFile 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. * * TransFile 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 TransFile. If not, see <http://www.gnu.org/licenses/>. */ package net.sourceforge.transfile.ui.swing; import static net.sourceforge.jenerics.i18n.Translator.createLocale; import static net.sourceforge.jenerics.i18n.Translator.getDefaultTranslator; import static net.sourceforge.jenerics.i18n.Translator.Helpers.translate; import static net.sourceforge.jenerics.Tools.getLoggerForThisMethod; import java.awt.Container; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.logging.Level; import javax.swing.BoxLayout; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; import net.sourceforge.jmacadapter.MacAdapterException; import net.sourceforge.jmacadapter.MacAdapterTools; import net.sourceforge.jmacadapter.eawtwrappers.Application; import net.sourceforge.jmacadapter.eawtwrappers.ApplicationAdapter; import net.sourceforge.jmacadapter.eawtwrappers.ApplicationEvent; import net.sourceforge.jenerics.i18n.Translator; import net.sourceforge.transfile.operations.ReceiveOperation; import net.sourceforge.transfile.operations.Session; import net.sourceforge.transfile.operations.SimpleSocketConnection; import net.sourceforge.transfile.settings.Settings; import net.sourceforge.transfile.settings.exceptions.IllegalConfigValueException; import net.sourceforge.transfile.ui.UserInterface; import net.sourceforge.transfile.ui.swing.StatusService.StatusMessage; import net.sourceforge.transfile.ui.swing.exceptions.NativeLookAndFeelException; /** * Main class of the Swing GUI for TransFile. Handles global events, aggregates GUI windows * and components and incorporates the application's main window. * * @author Martin Riedel * */ public class SwingGUI extends JFrame implements UserInterface { private static final long serialVersionUID = 3087671371254147452L; /* * Main window title */ private final static String title = "TransFile"; /* * Handles status messages */ private final StatusService statusService; /* * References to the TopLevelPanels */ private NetworkPanel networkPanel; private TransferPanel transferPanel; private StatusPanel statusPanel; /* * List containing all TopLevelPanels */ private List<TopLevelPanel> panels = new LinkedList<TopLevelPanel>(); /** * Object managing the creation of operations. */ private final Session session; /** * Constructs a new instance * */ public SwingGUI() { super(title); this.statusService = new StatusServiceProvider(); this.session = createSession(); this.setStartupLocale(); } /** * * @return * <br>A non-null value * <br>A shared value */ public final Session getSession() { return this.session; } /** * {@inheritDoc} */ @Override public void start() { SwingUtilities.invokeLater(new Runnable() { // suppressing the synthetic access warning for SwingGUI._start() in order // to not have to (visibly) expose the method to the entire package @SuppressWarnings("synthetic-access") @Override public void run() { SwingGUI.this.doStart(); } }); } /** * * @return the {@link StatusService} handling status messages for this window */ public StatusService getStatusService() { return this.statusService; } /** * Packs the window and uses the resulting size as a minimum size, so that the user cannot * make it any smaller. * */ @Override public void pack() { // unset minimum and maximum size so that pack can reduce window size if appropriate this.setMinimumSize(null); this.setMaximumSize(null); super.pack(); this.setMinimumSize(this.getSize()); this.setMaximumSize(new Dimension(Integer.MAX_VALUE, this.getSize().height)); } /** * Invoked when a connection to a peer has been established * */ final void onConnectSuccessful() { this.getStatusService().postStatusMessage(translate(new StatusMessage("status_connected"))); this.showTransferScreen(); } /** * Shows the "About" dialog */ void showAboutDialog() { //TODO implement } /** * Shows the preferences window */ void showPreferences() { new PreferencesFrame(this).setVisible(true); } /** * Quits the application * */ final void quit() { // inform all TopLevelPanels about the impending shutdown for (TopLevelPanel panel: this.panels) { panel.informQuit(); } this.getSession().getConnection().disconnect(); //TODO this.getSession().quit() ? } /** * Sets up and starts the Swing GUI. Should be invoked from the Swing event dispatch thread * */ private final void doStart() { GUITools.checkAWT(); try { this.setNativeLookAndFeel(); } catch (final NativeLookAndFeelException exception) { this.showErrorDialog(exception); } this.addWindowListener(this.new MainWindowListener()); this.setup(); this.showConnectScreen(); Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this.statusService)); this.statusService.postStatusMessage(translate(new StatusMessage("status_ready"))); this.pack(); this.enforceMaximumSize(); // Center the window on the screen and show it this.setLocationRelativeTo(null); this.setVisible(true); } /** * Sets the initial locale during startup * */ private void setStartupLocale() { Locale locale; try { // load locale from user settings file locale = loadLocale(); // if not present, use the host's default locale (and implicitly fall back to en_US if the host's // default locale is unsupported if (locale == null) locale = getLocale(); } catch (final IllegalConfigValueException e) { //TODO this code is never reached as IllegalConfigValueException is never thrown // user set an invalid locale, fall back to host's default locale locale = getLocale(); getLoggerForThisMethod().log(Level.WARNING, "failed to load user locale preference; illegal locale: " + e.getValue(), e); //TODO inform the user (i.e. dialog) } getDefaultTranslator().setLocale(locale); } /** * Loads the user's selected locale from the user-specific config file * @return * <br>A possibly null value * <br>A possibly new value * * @throws IllegalConfigValueException if the "locale" setting is present but invalid */ private static final Locale loadLocale() { //TODO userLocaleSetting can never be null. Hence, the host default locale is never used. final String userLocaleSetting = Settings.getPreferences().get("locale", Settings.LOCALE.toString()); if (userLocaleSetting == null || "".equals(userLocaleSetting)) { return null; } return createLocale(userLocaleSetting); } /** * Creates all GUI components * */ private void setup() { // Add a translator listener to update the display when the user changes the language getDefaultTranslator().addTranslatorListener(new Translator.Listener() { @Override public final void localeChanged(final Locale oldLocale, final Locale newLocale) { SwingGUI.this.repaint(); } }); this.setupMenuBar(); // set up content pane final Container pane = getContentPane(); pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS)); // "Network" panel this.networkPanel = new NetworkPanel(this); this.panels.add(this.networkPanel); pane.add(this.networkPanel); // "Transfer" panel this.transferPanel = new TransferPanel(this); this.panels.add(this.transferPanel); pane.add(this.transferPanel); // "Status" panel //TODO remove or redesign // this.statusPanel = new StatusPanel(this); // this.statusPanel.setPreferredSize(new Dimension(360, 28)); // this.panels.add(this.statusPanel); // pane.add(this.statusPanel); } /** * Creates the menu bar * */ private final void setupMenuBar() { final JMenuBar menuBar = new JMenuBar(); this.setJMenuBar(menuBar); final JMenu instanceMenu = translate(new JMenu("menu_instance")); { final JMenuItem newInstanceItem = translate(new JMenuItem("menu_item_new_instance")); newInstanceItem.addActionListener(new ActionListener() { @Override public final void actionPerformed(final ActionEvent event) { final SwingGUI newSwingGUI = new SwingGUI(); newSwingGUI.start(); SwingUtilities.invokeLater(new Runnable() { @Override public final void run() { newSwingGUI.getSession().getConnection().setRemotePeer(SwingGUI.this.getSession().getConnection().getLocalPeer()); newSwingGUI.getSession().getConnection().setLocalPeer(SwingGUI.this.getSession().getConnection().getRemotePeer()); } }); } }); instanceMenu.add(newInstanceItem); } final JMenu fileMenu = translate(new JMenu("menu_file")); // add the "Exit" item to the "File" menu, unless running on Mac OS (X) in which // case there is already a "Quit" item in the application menu if (!MacAdapterTools.isMacOSX()) { final JMenuItem exitItem = translate(new JMenuItem("menu_item_exit")); exitItem.addActionListener(new ActionListener() { @Override public final void actionPerformed(final ActionEvent event) { SwingGUI.this.quit(); } }); fileMenu.add(exitItem); } final JMenu settingsMenu = translate(new JMenu("menu_settings")); // add the "Preferences..." item to the "Settings" menu, unless running on Mac OS (X) in which // case there is already a "Preferences..." item in the application menu if (!MacAdapterTools.isMacOSX()) { final JMenuItem preferencesItem = translate(new JMenuItem("menu_item_preferences")); preferencesItem.addActionListener(new ActionListener() { @Override public final void actionPerformed(final ActionEvent event) { SwingGUI.this.showPreferences(); } }); settingsMenu.add(preferencesItem); } // show the "File" menu if it has at least one element addMenuIfNotEmpty(menuBar, fileMenu); // show the "Instance" menu if it has at least one element addMenuIfNotEmpty(menuBar, instanceMenu); // show the "Settings" menu if it has at least one element addMenuIfNotEmpty(menuBar, settingsMenu); } /** * Makes this Swing GUI look like a native application on the respective OS it's running on * * @throws NativeLookAndFeelException if a native look and feel cannot be loaded */ private final void setNativeLookAndFeel() throws NativeLookAndFeelException { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (final Exception exception) { throw new NativeLookAndFeelException(exception); } try { // Mac-specific adaptations if (MacAdapterTools.isMacOSX()) { MacAdapterTools.setUseScreenMenuBar(true); Application.getApplication().setEnabledAboutMenu(false); Application.getApplication().setEnabledPreferencesMenu(true); Application.getApplication().addApplicationListener(new MacOSXApplicationAdapter()); } } catch (final MacAdapterException e) { //TODO handle properly getLoggerForThisMethod().warning("Exception in Mac OS X adapter"); } } /** * Enforces the window's maximum size * */ private void enforceMaximumSize() { this.addComponentListener(new ComponentAdapter() { /** * {@inheritDoc} */ @Override public void componentResized(final ComponentEvent event) { // enforce maximum height if (SwingGUI.this.getSize().height > SwingGUI.this.getMaximumSize().height) { SwingGUI.this.setSize(SwingGUI.this.getSize().width, SwingGUI.this.getMaximumSize().height); } // enforce maximum width if (SwingGUI.this.getSize().width > SwingGUI.this.getMaximumSize().width) { SwingGUI.this.setSize(SwingGUI.this.getMaximumSize().width, SwingGUI.this.getSize().height); } } }); } /** * Changes the main window so that it represents the screen where the user * configures and initiates a connection. * */ private void showConnectScreen() { Set<TopLevelPanel> visiblePanels = new HashSet<TopLevelPanel>(2); visiblePanels.add(this.networkPanel); visiblePanels.add(this.statusPanel); setVisiblePanels(visiblePanels); } /** * Changes the main window so that it represents the screen where the user * sends and receives files through the previously established connection * */ private final void showTransferScreen() { final Set<TopLevelPanel> visiblePanels = new HashSet<TopLevelPanel>(3); visiblePanels.add(this.transferPanel); visiblePanels.add(this.statusPanel); this.setVisiblePanels(visiblePanels); } /** * Shows the panels contained in the provided set, hides all others * * @param visiblePanels the panels to show */ private final void setVisiblePanels(final Set<TopLevelPanel> visiblePanels) { for (TopLevelPanel panel : this.panels) { if (visiblePanels.contains(panel)) { panel.showPanel(); } else { panel.hidePanel(); } } // resize the main window to fit the currently active panels this.pack(); } /** * Shows a (more or less) human-readable, localized error dialog for the provided Throwable * * @param t the Throwable to visualise as an error dialog */ private void showErrorDialog(final Throwable t) { showErrorDialog(t.toString()); } /** * Shows an error dialog displaying the provided message * * @param errorMessage the error message to show */ private void showErrorDialog(final String errorMessage) { JOptionPane.showMessageDialog(this, errorMessage, translate("Error"), JOptionPane.ERROR_MESSAGE); } /** * Listens for when the user closes the window * * @author Martin Riedel * */ private class MainWindowListener extends WindowAdapter { /** * Constructs a new instance * */ public MainWindowListener() { // do nothing, just allow instantiation } /** * {@inheritDoc} */ @Override public void windowClosing(WindowEvent e) { quit(); } } /** * Adds {@code menu} to {@code menuBar} if {@code menu} contains at least one sub element. * @param menuBar * <br>Should not be null * @param menu * <br>Should not be null * <br>Reference parameter */ private static final void addMenuIfNotEmpty(final JMenuBar menuBar, final JMenu menu) { if (menu.getSubElements() != null && menu.getSubElements().length > 0) { menuBar.add(menu); } } /** * TODO doc * * @return * <br>Not null * <br>New */ static final Session createSession() { return new Session(new SimpleSocketConnection(), new DestinationFileProvider()); } /** * TODO doc * * @author codistmonk (creation 2010-06-13) * */ private static class DestinationFileProvider implements ReceiveOperation.DestinationFileProvider { /** * Package-private default constructor to suppress visibility warnings. */ public DestinationFileProvider() { // Do nothing } @Override public final File getDestinationFile(final String fileName) { final JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (JFileChooser.APPROVE_OPTION == fileChooser.showOpenDialog(null) && fileChooser.getSelectedFile() != null) { return new File(fileChooser.getSelectedFile(), fileName); } return null; } } /** * Mac OS X {@link ApplicationEvent} handler * * @author Martin Riedel * */ private class MacOSXApplicationAdapter extends ApplicationAdapter { /** * Constructs a new instance * */ public MacOSXApplicationAdapter() { // do nothing, just allow non-synthetic constructor access by SwingGUI } /** * {@inheritDoc} */ @Override protected void handleAbout(final ApplicationEvent event) { event.setHandled(true); SwingGUI.this.showAboutDialog(); } /** * {@inheritDoc} */ @Override protected void handlePreferences(final ApplicationEvent event) { event.setHandled(true); SwingGUI.this.showPreferences(); } /** * {@inheritDoc} */ @Override protected void handleQuit(final ApplicationEvent event) { event.setHandled(true); SwingGUI.this.quit(); } } }