Add and expose “Insert Project” actions.
[jSite2.git] / src / net / pterodactylus / jsite / gui / SwingInterface.java
1 /*
2  * jSite2 - SwingInterface.java -
3  * Copyright © 2008 David Roden
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 package net.pterodactylus.jsite.gui;
21
22 import java.awt.event.ActionEvent;
23 import java.beans.PropertyChangeEvent;
24 import java.beans.PropertyChangeListener;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.net.UnknownHostException;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Date;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Locale;
36 import java.util.Map;
37 import java.util.Properties;
38 import java.util.concurrent.Executor;
39 import java.util.concurrent.Executors;
40 import java.util.logging.Level;
41 import java.util.logging.LogRecord;
42 import java.util.logging.Logger;
43
44 import javax.swing.JOptionPane;
45 import javax.swing.UIManager;
46 import javax.swing.UnsupportedLookAndFeelException;
47
48 import net.pterodactylus.jsite.core.Core;
49 import net.pterodactylus.jsite.core.CoreListener;
50 import net.pterodactylus.jsite.core.JSiteException;
51 import net.pterodactylus.jsite.core.Node;
52 import net.pterodactylus.jsite.core.Project;
53 import net.pterodactylus.jsite.i18n.I18n;
54 import net.pterodactylus.jsite.i18n.gui.I18nAction;
55 import net.pterodactylus.util.image.IconLoader;
56 import net.pterodactylus.util.io.Closer;
57 import net.pterodactylus.util.logging.Logging;
58 import net.pterodactylus.util.logging.LoggingListener;
59
60 /**
61  * The Swing user interface.
62  *
63  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
64  */
65 public class SwingInterface implements CoreListener, LoggingListener, PropertyChangeListener {
66
67         /** The logger. */
68         private static final Logger logger = Logging.getLogger(SwingInterface.class.getName());
69
70         /** The application core. */
71         private final Core core;
72
73         /** The configuration directory. */
74         private final String configDirectory;
75
76         /** The main window. */
77         private MainWindow mainWindow;
78
79         /** Thread pool. */
80         private Executor threadPool = Executors.newCachedThreadPool();
81
82         /** The logger window. */
83         private LogWindow logWindow;
84
85         /** The “configure” action. */
86         private I18nAction configureAction;
87
88         /** The “import config” action. */
89         private I18nAction importConfigAction;
90
91         /** The “quit” action. */
92         private I18nAction quitAction;
93
94         /** The “add node” action. */
95         private I18nAction addNodeAction;
96
97         /** All node menu items. */
98         private Map<Node, I18nAction> nodeConnectActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
99
100         /** All node disconnect actions. */
101         private Map<Node, I18nAction> nodeDisconnectActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
102
103         /** All node edit actions. */
104         private Map<Node, I18nAction> nodeEditActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
105
106         /** All node removal actions. */
107         private Map<Node, I18nAction> nodeDeleteActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
108
109         /** All lanugage menu items. */
110         private List<I18nAction> languageActions = new ArrayList<I18nAction>();
111
112         /** The “about” action. */
113         private I18nAction helpAboutAction;
114
115         /** The “add project” action. */
116         private I18nAction addProjectAction;
117
118         /** The “insert project” actions. */
119         private Map<Project, I18nAction> insertProjectActions = new HashMap<Project, I18nAction>();
120
121         /** The “clone project” actions. */
122         private Map<Project, I18nAction> cloneProjectActions = new HashMap<Project, I18nAction>();
123
124         /** The “delete project” actions. */
125         private Map<Project, I18nAction> deleteProjectActions = new HashMap<Project, I18nAction>();
126
127         /** The “about” dialog. */
128         private AboutDialog aboutDialog;
129
130         /** The configuration dialog. */
131         private ConfigurationDialog configurationDialog;
132
133         /** The node editor dialog. */
134         private AddNodeDialog addNodeDialog;
135
136         /** The list of all defined nodes. */
137         private List<Node> nodeList = Collections.synchronizedList(new ArrayList<Node>());
138
139         /** The list of all projects. */
140         private List<Project> projectList = Collections.synchronizedList(new ArrayList<Project>());
141
142         //
143         // CONFIGURATION
144         //
145
146         /** The advanced mode. */
147         private boolean advancedMode;
148
149         /** Whether to antialias the GUI. */
150         private boolean antialias;
151
152         /** The control font. */
153         private String controlFont;
154
155         /** The user font. */
156         private String userFont;
157
158         /** The class name of the look and feel. */
159         private String lookAndFeel;
160
161         /** X coordinate of the main window. */
162         private int mainWindowX = -1;
163
164         /** Y coordinate of the main window. */
165         private int mainWindowY = -1;
166
167         /** Width of the main window. */
168         private int mainWindowWidth = -1;
169
170         /** Height of the main window. */
171         private int mainWindowHeight = -1;
172
173         /**
174          * Creates a new swing interface.
175          *
176          * @param core
177          *            The core to operate on
178          * @param configDirectory
179          *            The directory the configuration is stored in
180          */
181         public SwingInterface(Core core, String configDirectory) {
182                 this.core = core;
183                 this.configDirectory = configDirectory;
184                 I18n.setLocale(Locale.ENGLISH);
185                 loadConfig();
186                 if (lookAndFeel != null) {
187                         try {
188                                 UIManager.setLookAndFeel(lookAndFeel);
189                         } catch (ClassNotFoundException cnfe1) {
190                                 logger.log(Level.WARNING, "could not load look and feel", cnfe1);
191                         } catch (InstantiationException ie1) {
192                                 logger.log(Level.WARNING, "could not load look and feel", ie1);
193                         } catch (IllegalAccessException iae1) {
194                                 logger.log(Level.WARNING, "could not load look and feel", iae1);
195                         } catch (UnsupportedLookAndFeelException ulafe1) {
196                                 logger.log(Level.WARNING, "could not load look and feel", ulafe1);
197                         }
198                 }
199                 if (antialias) {
200                         System.setProperty("swing.aatext", "true");
201                 }
202                 if (controlFont != null) {
203                         System.setProperty("swing.plaf.metal.controlFont", controlFont);
204                 }
205                 if (userFont != null) {
206                         System.setProperty("swing.plaf.metal.userFont", userFont);
207                 }
208                 initActions();
209                 initDialogs();
210                 mainWindow = new MainWindow(this);
211                 mainWindow.setAdvancedMode(advancedMode);
212                 if ((mainWindowX != -1) && (mainWindowY != -1) && (mainWindowWidth != -1) && (mainWindowHeight != -1)) {
213                         mainWindow.setLocation(mainWindowX, mainWindowY);
214                         mainWindow.setSize(mainWindowWidth, mainWindowHeight);
215                 }
216                 logWindow = new LogWindow();
217         }
218
219         //
220         // ACCESSORS
221         //
222
223         /**
224          * Returns the core that is controlled by the Swing interface.
225          *
226          * @return The core
227          */
228         Core getCore() {
229                 return core;
230         }
231
232         /**
233          * Returns the main window of the Swing interface.
234          *
235          * @return The main window
236          */
237         MainWindow getMainWindow() {
238                 return mainWindow;
239         }
240
241         /**
242          * Returns whether the advanced mode is activated.
243          *
244          * @return <code>true</code> if the advanced mode is activated,
245          *         <code>false</code> if the simple mode is activated
246          */
247         boolean isAdvancedMode() {
248                 return advancedMode;
249         }
250
251         /**
252          * Returns the “configure” action.
253          *
254          * @return The “configure” action
255          */
256         I18nAction getConfigureAction() {
257                 return configureAction;
258         }
259
260         /**
261          * Returns the “import config” action.
262          *
263          * @return The “import config” action
264          */
265         I18nAction getImportConfigAction() {
266                 return importConfigAction;
267         }
268
269         /**
270          * Returns the “quit” action.
271          *
272          * @return The “quit” action
273          */
274         I18nAction getQuitAction() {
275                 return quitAction;
276         }
277
278         /**
279          * Returns the “add node” action.
280          *
281          * @return The “add node” action
282          */
283         I18nAction getAddNodeAction() {
284                 return addNodeAction;
285         }
286
287         /**
288          * Returns the “connect to node” action for the given node.
289          *
290          * @param node
291          *            The node go get the “connect” action for
292          * @return The “connect to node” action
293          */
294         I18nAction getNodeConnectAction(Node node) {
295                 return nodeConnectActions.get(node);
296         }
297
298         /**
299          * Returns the “disconnect from node” action for the given node.
300          *
301          * @param node
302          *            The node go get the “disconnect” action for
303          * @return The “disconnect from node” action
304          */
305         I18nAction getNodeDisconnectAction(Node node) {
306                 return nodeDisconnectActions.get(node);
307         }
308
309         /**
310          * Returns the “edit node” action for the given node.
311          *
312          * @param node
313          *            The node to edit
314          * @return The “edit node” action
315          */
316         I18nAction getNodeEditAction(Node node) {
317                 return nodeEditActions.get(node);
318         }
319
320         /**
321          * Returns the “delete node” action for the given node.
322          *
323          * @param node
324          *            The node to delete
325          * @return The “delete node” action
326          */
327         I18nAction getNodeDeleteAction(Node node) {
328                 return nodeDeleteActions.get(node);
329         }
330
331         /**
332          * Returns all language actions.
333          *
334          * @return All language actions
335          */
336         List<I18nAction> getLanguageActions() {
337                 return languageActions;
338         }
339
340         /**
341          * Returns the “about” action.
342          *
343          * @return The “about” action
344          */
345         I18nAction getHelpAboutAction() {
346                 return helpAboutAction;
347         }
348
349         /**
350          * Returns the “add project” action.
351          *
352          * @return The “add project” action
353          */
354         I18nAction getAddProjectAction() {
355                 return addProjectAction;
356         }
357
358         /**
359          * Returns the “insert project” action for the given project.
360          *
361          * @param project
362          *            The project to get the “insert project” action for
363          * @return The “insert project” action
364          */
365         I18nAction getInsertProjectAction(Project project) {
366                 return insertProjectActions.get(project);
367         }
368
369         /**
370          * Returns the “clone project” action for the given project.
371          *
372          * @param project
373          *            The project to get the “clone project” action for
374          * @return The “clone project” action
375          */
376         I18nAction getCloneProjectAction(Project project) {
377                 return cloneProjectActions.get(project);
378         }
379
380         /**
381          * Returns the “delete project” action for the given project.
382          *
383          * @param project
384          *            The project to get the “delete project” action for
385          * @return The “delete project” action
386          */
387         I18nAction getDeleteProjectAction(Project project) {
388                 return deleteProjectActions.get(project);
389         }
390
391         /**
392          * Returns all currently configured nodes.
393          *
394          * @return All configured nodes
395          */
396         List<Node> getNodes() {
397                 return nodeList;
398         }
399
400         /**
401          * Returns a list of all projects.
402          *
403          * @return All projects
404          */
405         List<Project> getProjects() {
406                 return projectList;
407         }
408
409         /**
410          * Returns the thread pool used for off-thread processes.
411          *
412          * @return The thread pool
413          */
414         Executor getThreadPool() {
415                 return threadPool;
416         }
417
418         //
419         // ACTIONS
420         //
421
422         //
423         // SERVICE METHODS
424         //
425
426         //
427         // PRIVATE METHODS
428         //
429
430         /**
431          * Loads the configuration of the interface.
432          */
433         private void loadConfig() {
434                 /* initialize default stuff. */
435                 antialias = false;
436                 /* now read config. */
437                 File configFile = new File(configDirectory, "swing-interface.properties");
438                 if (!configFile.exists() || !configFile.canRead() || !configFile.isFile()) {
439                         System.err.println("could not find “" + configFile.getAbsolutePath() + "”!");
440                         return;
441                 }
442                 Properties configProperties = new Properties();
443                 FileInputStream configInputStream = null;
444                 try {
445                         configInputStream = new FileInputStream(configFile);
446                         configProperties.load(configInputStream);
447                 } catch (IOException ioe1) {
448                         System.err.println("could not load config, " + ioe1.getMessage());
449                 } finally {
450                         Closer.close(configInputStream);
451                 }
452                 if (configProperties.containsKey("advancedMode")) {
453                         advancedMode = Boolean.valueOf(configProperties.getProperty("advancedMode"));
454                 }
455                 if (configProperties.containsKey("antialias")) {
456                         antialias = Boolean.valueOf(configProperties.getProperty("antialias"));
457                 }
458                 if (configProperties.containsKey("controlFont")) {
459                         controlFont = configProperties.getProperty("controlFont");
460                 }
461                 if (configProperties.containsKey("userFont")) {
462                         userFont = configProperties.getProperty("userFont");
463                 }
464                 if (configProperties.containsKey("lookAndFeel")) {
465                         lookAndFeel = configProperties.getProperty("lookAndFeel");
466                 }
467                 if (configProperties.containsKey("language")) {
468                         I18n.setLocale(new Locale(configProperties.getProperty("language")));
469                 }
470                 if (configProperties.containsKey("mainWindowX")) {
471                         mainWindowX = Integer.valueOf(configProperties.getProperty("mainWindowX"));
472                 }
473                 if (configProperties.containsKey("mainWindowY")) {
474                         mainWindowY = Integer.valueOf(configProperties.getProperty("mainWindowY"));
475                 }
476                 if (configProperties.containsKey("mainWindowWidth")) {
477                         mainWindowWidth = Integer.valueOf(configProperties.getProperty("mainWindowWidth"));
478                 }
479                 if (configProperties.containsKey("mainWindowHeight")) {
480                         mainWindowHeight = Integer.valueOf(configProperties.getProperty("mainWindowHeight"));
481                 }
482         }
483
484         /**
485          * Saves the configuration.
486          */
487         private void saveConfig() {
488                 File configDirectory = new File(this.configDirectory);
489                 if (!configDirectory.exists()) {
490                         if (!configDirectory.mkdirs()) {
491                                 System.err.println("could not create “" + this.configDirectory + "”!");
492                                 return;
493                         }
494                 }
495                 if (!configDirectory.exists() || !configDirectory.isDirectory() || !configDirectory.canWrite()) {
496                         System.err.println("can not access “" + this.configDirectory + "”!");
497                         return;
498                 }
499                 File configFile = new File(configDirectory, "swing-interface.properties");
500                 Properties configProperties = new Properties();
501                 configProperties.setProperty("advancedMode", String.valueOf(advancedMode));
502                 configProperties.setProperty("antialias", String.valueOf(antialias));
503                 if (controlFont != null) {
504                         configProperties.setProperty("controlFont", controlFont);
505                 }
506                 if (userFont != null) {
507                         configProperties.setProperty("userFont", userFont);
508                 }
509                 if (lookAndFeel != null) {
510                         configProperties.setProperty("lookAndFeel", lookAndFeel);
511                 }
512                 configProperties.setProperty("language", I18n.getLocale().getLanguage());
513                 configProperties.setProperty("mainWindowX", String.valueOf(mainWindowX));
514                 configProperties.setProperty("mainWindowY", String.valueOf(mainWindowY));
515                 configProperties.setProperty("mainWindowWidth", String.valueOf(mainWindowWidth));
516                 configProperties.setProperty("mainWindowHeight", String.valueOf(mainWindowHeight));
517                 FileOutputStream configOutputStream = null;
518                 try {
519                         configOutputStream = new FileOutputStream(configFile);
520                         configProperties.store(configOutputStream, "configuration of swing interface");
521                 } catch (IOException ioe1) {
522                         System.err.println("could not save config, " + ioe1.getMessage());
523                 } finally {
524                         Closer.close(configOutputStream);
525                 }
526         }
527
528         /**
529          * Initializes all actions.
530          */
531         private void initActions() {
532                 configureAction = new I18nAction("mainWindow.menu.jSite.configure", IconLoader.loadIcon("/preferences-system.png")) {
533
534                         /**
535                          * {@inheritDoc}
536                          */
537                         @SuppressWarnings("synthetic-access")
538                         public void actionPerformed(ActionEvent actionEvent) {
539                                 configure();
540                         }
541                 };
542                 importConfigAction = new I18nAction("mainWindow.menu.jSite.importConfig") {
543
544                         /**
545                          * {@inheritDoc}
546                          */
547                         @SuppressWarnings("synthetic-access")
548                         public void actionPerformed(ActionEvent actionEvent) {
549                                 importConfig();
550                         }
551                 };
552                 quitAction = new I18nAction("mainWindow.menu.jSite.quit", IconLoader.loadIcon("/system-log-out.png")) {
553
554                         /**
555                          * {@inheritDoc}
556                          */
557                         @SuppressWarnings("synthetic-access")
558                         public void actionPerformed(ActionEvent actionEvent) {
559                                 quit();
560                         }
561                 };
562                 List<Locale> availableLanguages = I18n.findAvailableLanguages();
563                 for (final Locale locale : availableLanguages) {
564                         String language = locale.getLanguage();
565                         I18nAction languageAction = new I18nAction("general.language." + language, IconLoader.loadIcon("/flag-" + language + ".png")) {
566
567                                 @SuppressWarnings("synthetic-access")
568                                 public void actionPerformed(ActionEvent e) {
569                                         changeLanguage(locale, this);
570                                 }
571
572                         };
573                         if (I18n.getLocale().getLanguage().equals(language)) {
574                                 languageAction.setEnabled(false);
575                         }
576                         languageActions.add(languageAction);
577                 }
578                 addNodeAction = new I18nAction("mainWindow.menu.node.item.addNode", IconLoader.loadIcon("/node-new.png")) {
579
580                         /**
581                          * {@inheritDoc}
582                          */
583                         @SuppressWarnings("synthetic-access")
584                         public void actionPerformed(ActionEvent actionEvent) {
585                                 addNode();
586                         }
587                 };
588                 helpAboutAction = new I18nAction("mainWindow.menu.help.item.about") {
589
590                         /**
591                          * {@inheritDoc}
592                          */
593                         @SuppressWarnings("synthetic-access")
594                         public void actionPerformed(ActionEvent actionEvent) {
595                                 helpAbout();
596                         }
597                 };
598                 addProjectAction = new I18nAction("mainWindow.button.addProject") {
599
600                         /**
601                          * {@inheritDoc}
602                          */
603                         @SuppressWarnings("synthetic-access")
604                         public void actionPerformed(ActionEvent actionEvent) {
605                                 addProject();
606                         }
607                 };
608         }
609
610         /**
611          * Initializes all child dialogs.
612          */
613         private void initDialogs() {
614                 aboutDialog = new AboutDialog(this);
615                 configurationDialog = new ConfigurationDialog(this);
616                 addNodeDialog = new AddNodeDialog(mainWindow);
617         }
618
619         //
620         // PRIVATE ACTIONS
621         //
622
623         /**
624          * Shows the configuration dialog.
625          */
626         private void configure() {
627                 configurationDialog.setAdvancedMode(advancedMode);
628                 configurationDialog.setAntialias(antialias);
629                 configurationDialog.setControlFont(controlFont);
630                 configurationDialog.setUserFont(userFont);
631                 configurationDialog.setLookAndFeel(lookAndFeel);
632                 configurationDialog.setVisible(true);
633                 if (!configurationDialog.wasCancelled()) {
634                         advancedMode = configurationDialog.isAdvancedMode();
635                         if (!advancedMode && (nodeList.size() > 1)) {
636                                 JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.warning.multipleNodesNotAdvancedMode.message"), I18n.get("mainWindow.warning.multipleNodesNotAdvancedMode.title"), JOptionPane.WARNING_MESSAGE);
637                         }
638                         mainWindow.setAdvancedMode(advancedMode);
639                         antialias = configurationDialog.isAntialias();
640                         controlFont = configurationDialog.getControlFont();
641                         userFont = configurationDialog.getUserFont();
642                         lookAndFeel = configurationDialog.getLookAndFeel();
643                         saveConfig();
644                 }
645         }
646
647         /**
648          * Imports old jSite configuration.
649          */
650         private void importConfig() {
651                 /* TODO */
652         }
653
654         /**
655          * Quits jSite.
656          */
657         private void quit() {
658                 /* TODO - ask */
659                 core.stop();
660                 mainWindowX = mainWindow.getX();
661                 mainWindowY = mainWindow.getY();
662                 mainWindowWidth = mainWindow.getWidth();
663                 mainWindowHeight = mainWindow.getHeight();
664                 saveConfig();
665                 System.exit(0);
666         }
667
668         /**
669          * Adds a node.
670          */
671         private void addNode() {
672                 addNodeDialog.setNodeName(I18n.get(nodeList.isEmpty() ? "general.defaultNode.name" : "general.newNode.name"));
673                 addNodeDialog.setNodeHostname("localhost");
674                 addNodeDialog.setNodePort(9481);
675                 addNodeDialog.setVisible(true);
676                 if (!addNodeDialog.wasCancelled()) {
677                         Node newNode = new Node();
678                         newNode.setName(addNodeDialog.getNodeName());
679                         newNode.setHostname(addNodeDialog.getNodeHostname());
680                         newNode.setPort(addNodeDialog.getNodePort());
681                         try {
682                                 core.addNode(newNode);
683                         } catch (UnknownHostException e) {
684                                 JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.hostnameUnresolvable.message"), I18n.get("mainWindow.error.hostnameUnresolvable.title"), JOptionPane.ERROR_MESSAGE);
685                         }
686                 }
687         }
688
689         /**
690          * Edits the given node.
691          *
692          * @param node
693          *            The node to edit
694          */
695         private void editNode(Node node) {
696                 addNodeDialog.setNodeName(node.getName());
697                 addNodeDialog.setNodeHostname(node.getHostname());
698                 addNodeDialog.setNodePort(node.getPort());
699                 addNodeDialog.setVisible(true);
700                 if (!addNodeDialog.wasCancelled()) {
701                         node.setName(addNodeDialog.getNodeName());
702                         node.setHostname(addNodeDialog.getNodeHostname());
703                         node.setPort(addNodeDialog.getNodePort());
704                 }
705         }
706
707         /**
708          * Deletes the given node.
709          *
710          * @param node
711          *            The node to delete
712          */
713         private void deleteNode(Node node) {
714                 int option = JOptionPane.showConfirmDialog(mainWindow, I18n.get("mainWindow.question.deleteNode.message", node.getName()), I18n.get("mainWindow.question.deleteNode.title"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
715                 if (option == JOptionPane.OK_OPTION) {
716                         core.removeNode(node);
717                 }
718         }
719
720         /**
721          * Connects to the node.
722          *
723          * @param node
724          *            The node to connect to
725          */
726         private void nodeConnect(final Node node) {
727                 threadPool.execute(new Runnable() {
728
729                         /**
730                          * {@inheritDoc}
731                          */
732                         @SuppressWarnings("synthetic-access")
733                         public void run() {
734                                 logger.log(Level.INFO, "connecting to node “" + node.getName() + "”…");
735                                 core.connectToNode(node);
736                         }
737                 });
738         }
739
740         /**
741          * Disconnects from the node.
742          *
743          * @param node
744          *            The node to disconnect from
745          */
746         private void nodeDisconnect(Node node) {
747                 logger.log(Level.INFO, "disconnecting from node “" + node.getName() + "”…");
748                 core.disconnectFromNode(node);
749         }
750
751         /**
752          * Changes the language of the interface. This method also disables the
753          * action for the newly set language and enables all others.
754          *
755          * @param newLocale
756          *            The new language
757          * @param languageAction
758          *            The action that triggered the change
759          */
760         private void changeLanguage(Locale newLocale, I18nAction languageAction) {
761                 for (I18nAction i18nAction : languageActions) {
762                         i18nAction.setEnabled(i18nAction != languageAction);
763                 }
764                 I18n.setLocale(newLocale);
765         }
766
767         /**
768          * Shows the “about” dialog.
769          */
770         private void helpAbout() {
771                 aboutDialog.setVisible(true);
772         }
773
774         /**
775          * Adds a project.
776          */
777         private void addProject() {
778                 try {
779                         core.createProject();
780                 } catch (JSiteException jse1) {
781                         JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.notConnected.message"), I18n.get("mainWindow.error.notConnected.title"), JOptionPane.ERROR_MESSAGE);
782                 } catch (IOException e) {
783                         /* TODO - add i18n */
784                         JOptionPane.showMessageDialog(mainWindow, I18n.get(""), I18n.get(""), JOptionPane.ERROR_MESSAGE);
785                 }
786         }
787
788         /**
789          * Clones a project.
790          *
791          * @param project
792          *            The project to clone
793          */
794         private void cloneProject(Project project) {
795                 core.cloneProject(project);
796         }
797
798         /**
799          * Deletes a project.
800          *
801          * @param project
802          *            The project to delete
803          */
804         private void deleteProject(Project project) {
805                 int choice = JOptionPane.showConfirmDialog(mainWindow, I18n.get("mainWindow.question.deleteProject.message", project.getName()), I18n.get("mainWindow.question.deleteProject.title"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
806                 if (choice == JOptionPane.NO_OPTION) {
807                         return;
808                 }
809                 core.removeProject(project);
810         }
811
812         //
813         // INTERFACE CoreListener
814         //
815
816         /**
817          * {@inheritDoc}
818          */
819         public void loadingProjectsDone(String directory) {
820                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.projectLoadingDone"));
821                 for (Project project : core.getProjects()) {
822                         projectAdded(project, false);
823                 }
824         }
825
826         /**
827          * {@inheritDoc}
828          */
829         public void loadingProjectsFailed(String directory, Throwable throwable) {
830                 JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.projectLoadingFailed.message", directory), I18n.get("mainWindow.error.projectLoadingFailed.title"), JOptionPane.ERROR_MESSAGE);
831         }
832
833         /**
834          * {@inheritDoc}
835          */
836         public void savingProjectsDone(String directory) {
837                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.projectSavingDone"));
838         }
839
840         /**
841          * {@inheritDoc}
842          */
843         public void savingProjectsFailed(String directory, Throwable throwabled) {
844                 /* TODO */
845         }
846
847         /**
848          * {@inheritDoc}
849          */
850         public void projectAdded(Project project) {
851                 project.setName(I18n.get("general.newProject.name"));
852                 project.setDescription(I18n.get("general.newProject.description", new Date()));
853                 projectAdded(project, true);
854         }
855
856         /**
857          * @param project
858          * @param switchToProject
859          */
860         private void projectAdded(final Project project, boolean switchToProject) {
861                 cloneProjectActions.put(project, new I18nAction("mainWindow.button.cloneProject") {
862
863                         /**
864                          * {@inheritDoc}
865                          */
866                         @SuppressWarnings("synthetic-access")
867                         public void actionPerformed(ActionEvent actionEvent) {
868                                 cloneProject(project);
869                         }
870                 });
871                 deleteProjectActions.put(project, new I18nAction("mainWindow.button.deleteProject") {
872
873                         /**
874                          * {@inheritDoc}
875                          */
876                         @SuppressWarnings("synthetic-access")
877                         public void actionPerformed(ActionEvent actionEvent) {
878                                 deleteProject(project);
879                         }
880                 });
881                 projectList.add(project);
882                 mainWindow.addProject(project, switchToProject);
883         }
884
885         /**
886          * {@inheritDoc}
887          */
888         public void projectCloned(Project clonedProject, Project projectClone) {
889                 projectAdded(projectClone, true);
890         }
891
892         /**
893          * {@inheritDoc}
894          */
895         public void projectRemoved(Project project) {
896                 mainWindow.removeProject(project);
897                 cloneProjectActions.remove(project);
898                 deleteProjectActions.remove(project);
899         }
900
901         /**
902          * {@inheritDoc}
903          */
904         public void loadingNodesDone(String directory) {
905                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.loadingNodesDone"));
906         }
907
908         /**
909          * {@inheritDoc}
910          */
911         public void loadingNodesFailed(String directory, Throwable throwable) {
912                 /* TODO */
913         }
914
915         /**
916          * {@inheritDoc}
917          */
918         public void savingNodesDone(String directory) {
919                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.savingNodesDone"));
920         }
921
922         /**
923          * {@inheritDoc}
924          */
925         public void savingNodesFailed(String directory, Throwable throwable) {
926                 /* TODO */
927         }
928
929         /**
930          * {@inheritDoc}
931          */
932         public void coreLoaded() {
933                 mainWindow.setVisible(true);
934                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.coreLoaded"));
935         }
936
937         /**
938          * {@inheritDoc}
939          */
940         public void coreStopped() {
941                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.coreStopped"));
942         }
943
944         /**
945          * {@inheritDoc}
946          */
947         public void nodeAdded(final Node node) {
948                 logger.log(Level.INFO, "node added: " + node);
949                 nodeList.add(node);
950                 node.addPropertyChangeListener(this);
951                 logger.log(Level.FINE, "nodeList.size(): " + nodeList.size());
952                 nodeConnectActions.put(node, new I18nAction("mainWindow.menu.node.item.connect") {
953
954                         /**
955                          * {@inheritDoc}
956                          */
957                         @SuppressWarnings("synthetic-access")
958                         public void actionPerformed(ActionEvent e) {
959                                 nodeConnect(node);
960                         }
961                 });
962                 nodeDisconnectActions.put(node, new I18nAction("mainWindow.menu.node.item.disconnect") {
963
964                         /**
965                          * {@inheritDoc}
966                          */
967                         @SuppressWarnings("synthetic-access")
968                         public void actionPerformed(ActionEvent e) {
969                                 nodeDisconnect(node);
970                         }
971                 });
972                 nodeDisconnectActions.get(node).setEnabled(false);
973                 nodeEditActions.put(node, new I18nAction("mainWindow.menu.node.item.edit") {
974
975                         /**
976                          * {@inheritDoc}
977                          */
978                         @SuppressWarnings("synthetic-access")
979                         public void actionPerformed(ActionEvent actionEvent) {
980                                 editNode(node);
981                         }
982                 });
983                 nodeDeleteActions.put(node, new I18nAction("mainWindow.menu.node.item.remove") {
984
985                         /**
986                          * {@inheritDoc}
987                          */
988                         @SuppressWarnings("synthetic-access")
989                         public void actionPerformed(ActionEvent actionEvent) {
990                                 deleteNode(node);
991                         }
992                 });
993                 mainWindow.addNode(node);
994         }
995
996         /**
997          * {@inheritDoc}
998          */
999         public void nodeRemoved(Node node) {
1000                 logger.log(Level.INFO, "node removed: " + node);
1001                 nodeList.remove(node);
1002                 node.removePropertyChangeListener(this);
1003                 nodeConnectActions.remove(node);
1004                 nodeDisconnectActions.remove(node);
1005                 nodeEditActions.remove(node);
1006                 nodeDeleteActions.remove(node);
1007                 mainWindow.removeNode(node);
1008         }
1009
1010         /**
1011          * {@inheritDoc}
1012          */
1013         public void nodeConnecting(Node node) {
1014                 nodeConnectActions.get(node).setEnabled(false);
1015                 nodeEditActions.get(node).setEnabled(false);
1016                 nodeDeleteActions.get(node).setEnabled(false);
1017                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.connectingToNode", node.getName(), node.getHostname(), node.getPort()));
1018         }
1019
1020         /**
1021          * {@inheritDoc}
1022          */
1023         public void nodeConnected(Node node) {
1024                 nodeDisconnectActions.get(node).setEnabled(true);
1025                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.connectedToNode", node.getName(), node.getHostname(), node.getPort()));
1026                 mainWindow.setOnline(node);
1027         }
1028
1029         /**
1030          * {@inheritDoc}
1031          */
1032         public void nodeConnectionFailed(Node node, Throwable cause) {
1033                 nodeConnectActions.get(node).setEnabled(true);
1034                 nodeEditActions.get(node).setEnabled(true);
1035                 nodeDeleteActions.get(node).setEnabled(true);
1036                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.connectionToNodeFailed", node.getName(), node.getHostname(), node.getPort(), (cause != null) ? cause.getMessage() : "no reason given"));
1037                 mainWindow.setError(node);
1038                 JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.nodeConnectionFailed.message", node.getName(), node.getHostname(), node.getPort(), (cause != null) ? cause.getMessage() : "no reason given"), I18n.get("mainWindow.error.nodeConnectionFailed.title"), JOptionPane.ERROR_MESSAGE);
1039         }
1040
1041         /**
1042          * {@inheritDoc}
1043          */
1044         public void nodeDisconnected(Node node, Throwable throwable) {
1045                 nodeDisconnectActions.get(node).setEnabled(false);
1046                 nodeConnectActions.get(node).setEnabled(true);
1047                 nodeEditActions.get(node).setEnabled(true);
1048                 nodeDeleteActions.get(node).setEnabled(true);
1049                 mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.disconnectedFromNode", node.getName(), node.getHostname(), node.getPort()));
1050                 mainWindow.setOffline(node);
1051         }
1052
1053         /**
1054          * @see net.pterodactylus.jsite.core.CoreListener#projectInsertStarted(net.pterodactylus.jsite.core.Project)
1055          */
1056         public void projectInsertStarted(Project project) {
1057                 mainWindow.projectInsertStarted(project);
1058         }
1059
1060         /**
1061          * @see net.pterodactylus.jsite.core.CoreListener#projectInsertProgressed(net.pterodactylus.jsite.core.Project,
1062          *      int, int, int, int, int, boolean)
1063          */
1064         public void projectInsertProgressed(Project project, int totalBlocks, int requiredBlocks, int successfulBlocks, int failedBlocks, int fatallyFailedBlocks, boolean finalizedTotal) {
1065                 mainWindow.projectInsertProgressed(project, totalBlocks, requiredBlocks, successfulBlocks, failedBlocks, fatallyFailedBlocks, finalizedTotal);
1066         }
1067
1068         /**
1069          * @see net.pterodactylus.jsite.core.CoreListener#projectInsertGeneratedURI(net.pterodactylus.jsite.core.Project,
1070          *      java.lang.String)
1071          */
1072         public void projectInsertGeneratedURI(Project project, String uri) {
1073                 mainWindow.projectInsertGeneratedURI(project);
1074         }
1075
1076         /**
1077          * @see net.pterodactylus.jsite.core.CoreListener#projectInsertFinished(net.pterodactylus.jsite.core.Project,
1078          *      boolean)
1079          */
1080         public void projectInsertFinished(Project project, boolean success) {
1081                 mainWindow.projectInsertFinished(project, success);
1082         }
1083
1084         //
1085         // INTERFACE LoggingListener
1086         //
1087
1088         /**
1089          * {@inheritDoc}
1090          */
1091         public void logged(LogRecord logRecord) {
1092                 logWindow.logged(logRecord);
1093         }
1094
1095         //
1096         // INTERFACE PropertyChangeListener
1097         //
1098
1099         /**
1100          * {@inheritDoc}
1101          */
1102         public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
1103                 /* do not react to anything (yet). */
1104         }
1105
1106 }