Emit warning and abort insert if the selected node is not connected.
[jSite2.git] / src / net / pterodactylus / jsite / gui / SwingInterface.java
index 1f41af1..3df6402 100644 (file)
 package net.pterodactylus.jsite.gui;
 
 import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
 import javax.swing.JOptionPane;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
 
 import net.pterodactylus.jsite.core.Core;
 import net.pterodactylus.jsite.core.CoreListener;
+import net.pterodactylus.jsite.core.JSiteException;
 import net.pterodactylus.jsite.core.Node;
+import net.pterodactylus.jsite.core.Project;
 import net.pterodactylus.jsite.i18n.I18n;
 import net.pterodactylus.jsite.i18n.gui.I18nAction;
+import net.pterodactylus.util.image.IconLoader;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.logging.LoggingListener;
 
 /**
- * TODO
- * 
+ * The Swing user interface.
+ *
  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
- * @version $Id$
  */
-public class SwingInterface implements CoreListener {
+public class SwingInterface implements CoreListener, LoggingListener, PropertyChangeListener {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(SwingInterface.class.getName());
 
        /** The application core. */
        private final Core core;
 
+       /** The configuration directory. */
+       private final String configDirectory;
+
        /** The main window. */
        private MainWindow mainWindow;
 
+       /** Thread pool. */
+       private Executor threadPool = Executors.newCachedThreadPool();
+
+       /** The logger window. */
+       private LogWindow logWindow;
+
        /** The “configure” action. */
        private I18nAction configureAction;
 
@@ -55,17 +93,20 @@ public class SwingInterface implements CoreListener {
        /** The “quit” action. */
        private I18nAction quitAction;
 
-       /** The “manage nodes” action. */
-       private I18nAction manageNodesAction;
+       /** The “add node” action. */
+       private I18nAction addNodeAction;
+
+       /** All node menu items. */
+       private Map<Node, I18nAction> nodeConnectActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
 
-       /** The “connect to node” action. */
-       private I18nAction nodeConnectAction;
+       /** All node disconnect actions. */
+       private Map<Node, I18nAction> nodeDisconnectActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
 
-       /** The “disconnect from node” action. */
-       private I18nAction nodeDisconnectAction;
+       /** All node edit actions. */
+       private Map<Node, I18nAction> nodeEditActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
 
-       /** The node manager dialog. */
-       private ManageNodesDialog manageNodesDialog;
+       /** All node removal actions. */
+       private Map<Node, I18nAction> nodeDeleteActions = Collections.synchronizedMap(new HashMap<Node, I18nAction>());
 
        /** All lanugage menu items. */
        private List<I18nAction> languageActions = new ArrayList<I18nAction>();
@@ -76,39 +117,105 @@ public class SwingInterface implements CoreListener {
        /** The “add project” action. */
        private I18nAction addProjectAction;
 
+       /** The “insert project” actions. */
+       private Map<Project, I18nAction> insertProjectActions = new HashMap<Project, I18nAction>();
+
+       /** The “clone project” actions. */
+       private Map<Project, I18nAction> cloneProjectActions = new HashMap<Project, I18nAction>();
+
+       /** The “delete project” actions. */
+       private Map<Project, I18nAction> deleteProjectActions = new HashMap<Project, I18nAction>();
+
        /** The “about” dialog. */
        private AboutDialog aboutDialog;
 
        /** The configuration dialog. */
        private ConfigurationDialog configurationDialog;
 
+       /** The node editor dialog. */
+       private AddNodeDialog addNodeDialog;
+
        /** The list of all defined nodes. */
-       private List<Node> nodeList;
+       private List<Node> nodeList = Collections.synchronizedList(new ArrayList<Node>());
+
+       /** The list of all projects. */
+       private List<Project> projectList = Collections.synchronizedList(new ArrayList<Project>());
 
        //
        // CONFIGURATION
        //
 
-       /** Whether to beautify the GUI. */
-       private boolean beautify;
+       /** The advanced mode. */
+       private boolean advancedMode;
+
+       /** Whether to antialias the GUI. */
+       private boolean antialias;
+
+       /** The control font. */
+       private String controlFont;
+
+       /** The user font. */
+       private String userFont;
+
+       /** The class name of the look and feel. */
+       private String lookAndFeel;
+
+       /** X coordinate of the main window. */
+       private int mainWindowX = -1;
+
+       /** Y coordinate of the main window. */
+       private int mainWindowY = -1;
+
+       /** Width of the main window. */
+       private int mainWindowWidth = -1;
+
+       /** Height of the main window. */
+       private int mainWindowHeight = -1;
 
        /**
         * Creates a new swing interface.
-        * 
+        *
         * @param core
         *            The core to operate on
+        * @param configDirectory
+        *            The directory the configuration is stored in
         */
-       public SwingInterface(Core core) {
+       public SwingInterface(Core core, String configDirectory) {
                this.core = core;
-               I18n.setLocale(Locale.ENGLISH); /* TODO - load config */
+               this.configDirectory = configDirectory;
+               I18n.setLocale(Locale.ENGLISH);
                loadConfig();
-               if (beautify) {
+               if (lookAndFeel != null) {
+                       try {
+                               UIManager.setLookAndFeel(lookAndFeel);
+                       } catch (ClassNotFoundException cnfe1) {
+                               logger.log(Level.WARNING, "could not load look and feel", cnfe1);
+                       } catch (InstantiationException ie1) {
+                               logger.log(Level.WARNING, "could not load look and feel", ie1);
+                       } catch (IllegalAccessException iae1) {
+                               logger.log(Level.WARNING, "could not load look and feel", iae1);
+                       } catch (UnsupportedLookAndFeelException ulafe1) {
+                               logger.log(Level.WARNING, "could not load look and feel", ulafe1);
+                       }
+               }
+               if (antialias) {
                        System.setProperty("swing.aatext", "true");
-                       System.setProperty("swing.plaf.metal.controlFont", "Tahoma");
-                       System.setProperty("swing.plaf.metal.userFont", "Tahoma");
+               }
+               if (controlFont != null) {
+                       System.setProperty("swing.plaf.metal.controlFont", controlFont);
+               }
+               if (userFont != null) {
+                       System.setProperty("swing.plaf.metal.userFont", userFont);
                }
                initActions();
                initDialogs();
+               mainWindow = new MainWindow(this);
+               mainWindow.setAdvancedMode(advancedMode);
+               if ((mainWindowX != -1) && (mainWindowY != -1) && (mainWindowWidth != -1) && (mainWindowHeight != -1)) {
+                       mainWindow.setLocation(mainWindowX, mainWindowY);
+                       mainWindow.setSize(mainWindowWidth, mainWindowHeight);
+               }
+               logWindow = new LogWindow();
        }
 
        //
@@ -117,7 +224,7 @@ public class SwingInterface implements CoreListener {
 
        /**
         * Returns the core that is controlled by the Swing interface.
-        * 
+        *
         * @return The core
         */
        Core getCore() {
@@ -126,7 +233,7 @@ public class SwingInterface implements CoreListener {
 
        /**
         * Returns the main window of the Swing interface.
-        * 
+        *
         * @return The main window
         */
        MainWindow getMainWindow() {
@@ -134,8 +241,18 @@ public class SwingInterface implements CoreListener {
        }
 
        /**
+        * Returns whether the advanced mode is activated.
+        *
+        * @return <code>true</code> if the advanced mode is activated,
+        *         <code>false</code> if the simple mode is activated
+        */
+       boolean isAdvancedMode() {
+               return advancedMode;
+       }
+
+       /**
         * Returns the “configure” action.
-        * 
+        *
         * @return The “configure” action
         */
        I18nAction getConfigureAction() {
@@ -144,7 +261,7 @@ public class SwingInterface implements CoreListener {
 
        /**
         * Returns the “import config” action.
-        * 
+        *
         * @return The “import config” action
         */
        I18nAction getImportConfigAction() {
@@ -153,7 +270,7 @@ public class SwingInterface implements CoreListener {
 
        /**
         * Returns the “quit” action.
-        * 
+        *
         * @return The “quit” action
         */
        I18nAction getQuitAction() {
@@ -161,35 +278,61 @@ public class SwingInterface implements CoreListener {
        }
 
        /**
-        * Returns the “manage nodes” action.
-        * 
-        * @return The “manage nodes” action
+        * Returns the “add node” action.
+        *
+        * @return The “add node” action
         */
-       I18nAction getManageNodesAction() {
-               return manageNodesAction;
+       I18nAction getAddNodeAction() {
+               return addNodeAction;
        }
 
        /**
-        * Returns the “connect to node” action.
-        * 
+        * Returns the “connect to node” action for the given node.
+        *
+        * @param node
+        *            The node go get the “connect” action for
         * @return The “connect to node” action
         */
-       I18nAction getNodeConnectAction() {
-               return nodeConnectAction;
+       I18nAction getNodeConnectAction(Node node) {
+               return nodeConnectActions.get(node);
        }
 
        /**
-        * Returns the “disconnect from node” action.
-        * 
+        * Returns the “disconnect from node” action for the given node.
+        *
+        * @param node
+        *            The node go get the “disconnect” action for
         * @return The “disconnect from node” action
         */
-       I18nAction getNodeDisconnectAction() {
-               return nodeDisconnectAction;
+       I18nAction getNodeDisconnectAction(Node node) {
+               return nodeDisconnectActions.get(node);
+       }
+
+       /**
+        * Returns the “edit node” action for the given node.
+        *
+        * @param node
+        *            The node to edit
+        * @return The “edit node” action
+        */
+       I18nAction getNodeEditAction(Node node) {
+               return nodeEditActions.get(node);
+       }
+
+       /**
+        * Returns the “delete node” action for the given node.
+        *
+        * @param node
+        *            The node to delete
+        * @return The “delete node” action
+        */
+       I18nAction getNodeDeleteAction(Node node) {
+               return nodeDeleteActions.get(node);
        }
 
        /**
         * Returns all language actions.
-        * 
+        *
         * @return All language actions
         */
        List<I18nAction> getLanguageActions() {
@@ -198,7 +341,7 @@ public class SwingInterface implements CoreListener {
 
        /**
         * Returns the “about” action.
-        * 
+        *
         * @return The “about” action
         */
        I18nAction getHelpAboutAction() {
@@ -207,13 +350,73 @@ public class SwingInterface implements CoreListener {
 
        /**
         * Returns the “add project” action.
-        * 
+        *
         * @return The “add project” action
         */
        I18nAction getAddProjectAction() {
                return addProjectAction;
        }
 
+       /**
+        * Returns the “insert project” action for the given project.
+        *
+        * @param project
+        *            The project to get the “insert project” action for
+        * @return The “insert project” action
+        */
+       I18nAction getInsertProjectAction(Project project) {
+               return insertProjectActions.get(project);
+       }
+
+       /**
+        * Returns the “clone project” action for the given project.
+        *
+        * @param project
+        *            The project to get the “clone project” action for
+        * @return The “clone project” action
+        */
+       I18nAction getCloneProjectAction(Project project) {
+               return cloneProjectActions.get(project);
+       }
+
+       /**
+        * Returns the “delete project” action for the given project.
+        *
+        * @param project
+        *            The project to get the “delete project” action for
+        * @return The “delete project” action
+        */
+       I18nAction getDeleteProjectAction(Project project) {
+               return deleteProjectActions.get(project);
+       }
+
+       /**
+        * Returns all currently configured nodes.
+        *
+        * @return All configured nodes
+        */
+       List<Node> getNodes() {
+               return nodeList;
+       }
+
+       /**
+        * Returns a list of all projects.
+        *
+        * @return All projects
+        */
+       List<Project> getProjects() {
+               return projectList;
+       }
+
+       /**
+        * Returns the thread pool used for off-thread processes.
+        *
+        * @return The thread pool
+        */
+       Executor getThreadPool() {
+               return threadPool;
+       }
+
        //
        // ACTIONS
        //
@@ -222,13 +425,6 @@ public class SwingInterface implements CoreListener {
        // SERVICE METHODS
        //
 
-       /**
-        * Starts the interface.
-        */
-       public void start() {
-               mainWindow = new MainWindow(this);
-       }
-
        //
        // PRIVATE METHODS
        //
@@ -237,14 +433,105 @@ public class SwingInterface implements CoreListener {
         * Loads the configuration of the interface.
         */
        private void loadConfig() {
-               beautify = true;
+               /* initialize default stuff. */
+               antialias = false;
+               /* now read config. */
+               File configFile = new File(configDirectory, "swing-interface.properties");
+               if (!configFile.exists() || !configFile.canRead() || !configFile.isFile()) {
+                       System.err.println("could not find “" + configFile.getAbsolutePath() + "”!");
+                       return;
+               }
+               Properties configProperties = new Properties();
+               FileInputStream configInputStream = null;
+               try {
+                       configInputStream = new FileInputStream(configFile);
+                       configProperties.load(configInputStream);
+               } catch (IOException ioe1) {
+                       System.err.println("could not load config, " + ioe1.getMessage());
+               } finally {
+                       Closer.close(configInputStream);
+               }
+               if (configProperties.containsKey("advancedMode")) {
+                       advancedMode = Boolean.valueOf(configProperties.getProperty("advancedMode"));
+               }
+               if (configProperties.containsKey("antialias")) {
+                       antialias = Boolean.valueOf(configProperties.getProperty("antialias"));
+               }
+               if (configProperties.containsKey("controlFont")) {
+                       controlFont = configProperties.getProperty("controlFont");
+               }
+               if (configProperties.containsKey("userFont")) {
+                       userFont = configProperties.getProperty("userFont");
+               }
+               if (configProperties.containsKey("lookAndFeel")) {
+                       lookAndFeel = configProperties.getProperty("lookAndFeel");
+               }
+               if (configProperties.containsKey("language")) {
+                       I18n.setLocale(new Locale(configProperties.getProperty("language")));
+               }
+               if (configProperties.containsKey("mainWindowX")) {
+                       mainWindowX = Integer.valueOf(configProperties.getProperty("mainWindowX"));
+               }
+               if (configProperties.containsKey("mainWindowY")) {
+                       mainWindowY = Integer.valueOf(configProperties.getProperty("mainWindowY"));
+               }
+               if (configProperties.containsKey("mainWindowWidth")) {
+                       mainWindowWidth = Integer.valueOf(configProperties.getProperty("mainWindowWidth"));
+               }
+               if (configProperties.containsKey("mainWindowHeight")) {
+                       mainWindowHeight = Integer.valueOf(configProperties.getProperty("mainWindowHeight"));
+               }
+       }
+
+       /**
+        * Saves the configuration.
+        */
+       private void saveConfig() {
+               File configDirectory = new File(this.configDirectory);
+               if (!configDirectory.exists()) {
+                       if (!configDirectory.mkdirs()) {
+                               System.err.println("could not create “" + this.configDirectory + "”!");
+                               return;
+                       }
+               }
+               if (!configDirectory.exists() || !configDirectory.isDirectory() || !configDirectory.canWrite()) {
+                       System.err.println("can not access “" + this.configDirectory + "”!");
+                       return;
+               }
+               File configFile = new File(configDirectory, "swing-interface.properties");
+               Properties configProperties = new Properties();
+               configProperties.setProperty("advancedMode", String.valueOf(advancedMode));
+               configProperties.setProperty("antialias", String.valueOf(antialias));
+               if (controlFont != null) {
+                       configProperties.setProperty("controlFont", controlFont);
+               }
+               if (userFont != null) {
+                       configProperties.setProperty("userFont", userFont);
+               }
+               if (lookAndFeel != null) {
+                       configProperties.setProperty("lookAndFeel", lookAndFeel);
+               }
+               configProperties.setProperty("language", I18n.getLocale().getLanguage());
+               configProperties.setProperty("mainWindowX", String.valueOf(mainWindowX));
+               configProperties.setProperty("mainWindowY", String.valueOf(mainWindowY));
+               configProperties.setProperty("mainWindowWidth", String.valueOf(mainWindowWidth));
+               configProperties.setProperty("mainWindowHeight", String.valueOf(mainWindowHeight));
+               FileOutputStream configOutputStream = null;
+               try {
+                       configOutputStream = new FileOutputStream(configFile);
+                       configProperties.store(configOutputStream, "configuration of swing interface");
+               } catch (IOException ioe1) {
+                       System.err.println("could not save config, " + ioe1.getMessage());
+               } finally {
+                       Closer.close(configOutputStream);
+               }
        }
 
        /**
         * Initializes all actions.
         */
        private void initActions() {
-               configureAction = new I18nAction("mainWindow.menu.jSite.configure") {
+               configureAction = new I18nAction("mainWindow.menu.jSite.configure", IconLoader.loadIcon("/preferences-system.png")) {
 
                        /**
                         * {@inheritDoc}
@@ -264,7 +551,7 @@ public class SwingInterface implements CoreListener {
                                importConfig();
                        }
                };
-               quitAction = new I18nAction("mainWindow.menu.jSite.quit") {
+               quitAction = new I18nAction("mainWindow.menu.jSite.quit", IconLoader.loadIcon("/system-log-out.png")) {
 
                        /**
                         * {@inheritDoc}
@@ -274,37 +561,10 @@ public class SwingInterface implements CoreListener {
                                quit();
                        }
                };
-               manageNodesAction = new I18nAction("mainWindow.menu.node.item.manageNodes") {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @SuppressWarnings("synthetic-access")
-                       public void actionPerformed(ActionEvent actionEvent) {
-                               manageNodes();
-                       }
-               };
-               nodeConnectAction = new I18nAction("mainWindow.menu.node.item.connect", false) {
-
-                       @SuppressWarnings("synthetic-access")
-                       public void actionPerformed(ActionEvent actionEvent) {
-                               nodeConnect();
-                       }
-
-               };
-               nodeDisconnectAction = new I18nAction("mainWindow.menu.node.item.disconnect", false) {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @SuppressWarnings("synthetic-access")
-                       public void actionPerformed(ActionEvent e) {
-                               nodeDisconnect();
-                       }
-               };
                List<Locale> availableLanguages = I18n.findAvailableLanguages();
-               for (final Locale locale: availableLanguages) {
-                       I18nAction languageAction = new I18nAction("general.language." + locale.getLanguage()) {
+               for (final Locale locale : availableLanguages) {
+                       String language = locale.getLanguage();
+                       I18nAction languageAction = new I18nAction("general.language." + language, IconLoader.loadIcon("/flag-" + language + ".png")) {
 
                                @SuppressWarnings("synthetic-access")
                                public void actionPerformed(ActionEvent e) {
@@ -312,11 +572,21 @@ public class SwingInterface implements CoreListener {
                                }
 
                        };
-                       if (I18n.getLocale().getLanguage().equals(locale.getLanguage())) {
+                       if (I18n.getLocale().getLanguage().equals(language)) {
                                languageAction.setEnabled(false);
                        }
                        languageActions.add(languageAction);
                }
+               addNodeAction = new I18nAction("mainWindow.menu.node.item.addNode", IconLoader.loadIcon("/node-new.png")) {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               addNode();
+                       }
+               };
                helpAboutAction = new I18nAction("mainWindow.menu.help.item.about") {
 
                        /**
@@ -343,9 +613,9 @@ public class SwingInterface implements CoreListener {
         * Initializes all child dialogs.
         */
        private void initDialogs() {
-               manageNodesDialog = new ManageNodesDialog(this);
                aboutDialog = new AboutDialog(this);
                configurationDialog = new ConfigurationDialog(this);
+               addNodeDialog = new AddNodeDialog(mainWindow);
        }
 
        //
@@ -356,10 +626,23 @@ public class SwingInterface implements CoreListener {
         * Shows the configuration dialog.
         */
        private void configure() {
-               configurationDialog.setBeautify(beautify);
+               configurationDialog.setAdvancedMode(advancedMode);
+               configurationDialog.setAntialias(antialias);
+               configurationDialog.setControlFont(controlFont);
+               configurationDialog.setUserFont(userFont);
+               configurationDialog.setLookAndFeel(lookAndFeel);
                configurationDialog.setVisible(true);
                if (!configurationDialog.wasCancelled()) {
-                       beautify = configurationDialog.getBeautify();
+                       advancedMode = configurationDialog.isAdvancedMode();
+                       if (!advancedMode && (nodeList.size() > 1)) {
+                               JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.warning.multipleNodesNotAdvancedMode.message"), I18n.get("mainWindow.warning.multipleNodesNotAdvancedMode.title"), JOptionPane.WARNING_MESSAGE);
+                       }
+                       mainWindow.setAdvancedMode(advancedMode);
+                       antialias = configurationDialog.isAntialias();
+                       controlFont = configurationDialog.getControlFont();
+                       userFont = configurationDialog.getUserFont();
+                       lookAndFeel = configurationDialog.getLookAndFeel();
+                       saveConfig();
                }
        }
 
@@ -367,47 +650,117 @@ public class SwingInterface implements CoreListener {
         * Imports old jSite configuration.
         */
        private void importConfig() {
+               /* TODO */
        }
 
        /**
         * Quits jSite.
         */
        private void quit() {
+               /* TODO - ask */
+               core.stop();
+               mainWindowX = mainWindow.getX();
+               mainWindowY = mainWindow.getY();
+               mainWindowWidth = mainWindow.getWidth();
+               mainWindowHeight = mainWindow.getHeight();
+               saveConfig();
                System.exit(0);
        }
 
        /**
-        * Pops up the “manage nodes” dialog.
+        * Adds a node.
+        */
+       private void addNode() {
+               addNodeDialog.setNodeName(I18n.get(nodeList.isEmpty() ? "general.defaultNode.name" : "general.newNode.name"));
+               addNodeDialog.setNodeHostname("localhost");
+               addNodeDialog.setNodePort(9481);
+               addNodeDialog.setVisible(true);
+               if (!addNodeDialog.wasCancelled()) {
+                       Node newNode = new Node();
+                       newNode.setName(addNodeDialog.getNodeName());
+                       newNode.setHostname(addNodeDialog.getNodeHostname());
+                       newNode.setPort(addNodeDialog.getNodePort());
+                       try {
+                               core.addNode(newNode);
+                       } catch (UnknownHostException e) {
+                               JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.hostnameUnresolvable.message"), I18n.get("mainWindow.error.hostnameUnresolvable.title"), JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       }
+
+       /**
+        * Edits the given node.
+        *
+        * @param node
+        *            The node to edit
+        */
+       private void editNode(Node node) {
+               addNodeDialog.setNodeName(node.getName());
+               addNodeDialog.setNodeHostname(node.getHostname());
+               addNodeDialog.setNodePort(node.getPort());
+               addNodeDialog.setVisible(true);
+               if (!addNodeDialog.wasCancelled()) {
+                       node.setName(addNodeDialog.getNodeName());
+                       node.setHostname(addNodeDialog.getNodeHostname());
+                       node.setPort(addNodeDialog.getNodePort());
+               }
+       }
+
+       /**
+        * Deletes the given node.
+        *
+        * @param node
+        *            The node to delete
         */
-       private void manageNodes() {
-               manageNodesDialog.setNodeList(nodeList);
-               manageNodesDialog.setVisible(true);
-               nodeList = manageNodesDialog.getNodeList();
+       private void deleteNode(Node node) {
+               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);
+               if (option == JOptionPane.OK_OPTION) {
+                       core.removeNode(node);
+               }
        }
 
        /**
         * Connects to the node.
+        *
+        * @param node
+        *            The node to connect to
         */
-       private void nodeConnect() {
+       private void nodeConnect(final Node node) {
+               threadPool.execute(new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               logger.log(Level.INFO, "connecting to node “" + node.getName() + "”…");
+                               core.connectToNode(node);
+                       }
+               });
        }
 
        /**
         * Disconnects from the node.
+        *
+        * @param node
+        *            The node to disconnect from
         */
-       private void nodeDisconnect() {
+       private void nodeDisconnect(Node node) {
+               logger.log(Level.INFO, "disconnecting from node “" + node.getName() + "”…");
+               core.disconnectFromNode(node);
        }
 
        /**
         * Changes the language of the interface. This method also disables the
         * action for the newly set language and enables all others.
-        * 
+        *
         * @param newLocale
         *            The new language
         * @param languageAction
         *            The action that triggered the change
         */
        private void changeLanguage(Locale newLocale, I18nAction languageAction) {
-               for (I18nAction i18nAction: languageActions) {
+               for (I18nAction i18nAction : languageActions) {
                        i18nAction.setEnabled(i18nAction != languageAction);
                }
                I18n.setLocale(newLocale);
@@ -424,6 +777,67 @@ public class SwingInterface implements CoreListener {
         * Adds a project.
         */
        private void addProject() {
+               try {
+                       core.createProject();
+               } catch (JSiteException jse1) {
+                       JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.notConnected.message"), I18n.get("mainWindow.error.notConnected.title"), JOptionPane.ERROR_MESSAGE);
+               } catch (IOException e) {
+                       /* TODO - add i18n */
+                       JOptionPane.showMessageDialog(mainWindow, I18n.get(""), I18n.get(""), JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+       /**
+        * Inserts the given project.
+        *
+        * @param project
+        *            The project to insert
+        */
+       private void insertProject(Project project) {
+               Node targetNode = project.getNode();
+               if (targetNode == null) {
+                       JComboBox nodeComboBox = new JComboBox();
+                       for (Node node : nodeList) {
+                               ((DefaultComboBoxModel) nodeComboBox.getModel()).addElement(node.getName() + " (" + node.getHostname() + ":" + node.getPort() + ")");
+                       }
+                       int selectedOption = JOptionPane.showConfirmDialog(mainWindow, new Object[] { I18n.get("nodeSelectionDialog.selectNode.text"), nodeComboBox }, I18n.get("nodeSelectionDialog.noNodeSelected.text"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
+                       if (selectedOption == JOptionPane.CANCEL_OPTION) {
+                               return;
+                       }
+                       int selectedNodeIndex = nodeComboBox.getSelectedIndex();
+                       logger.log(Level.FINE, "selected node index: " + selectedNodeIndex);
+                       targetNode = nodeList.get(selectedNodeIndex);
+               }
+               logger.log(Level.INFO, "Inserting project “" + project.getName() + "” to node “" + targetNode.getName() + "”...");
+               if (!core.isNodeConnected(targetNode)) {
+                       JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.nodeNotConnected.message", targetNode.getName()), I18n.get("mainWindow.error.nodeNotConnected.title", targetNode.getName()), JOptionPane.ERROR_MESSAGE);
+                       return;
+               }
+               core.insertProject(targetNode, project);
+       }
+
+       /**
+        * Clones a project.
+        *
+        * @param project
+        *            The project to clone
+        */
+       private void cloneProject(Project project) {
+               core.cloneProject(project);
+       }
+
+       /**
+        * Deletes a project.
+        *
+        * @param project
+        *            The project to delete
+        */
+       private void deleteProject(Project project) {
+               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);
+               if (choice == JOptionPane.NO_OPTION) {
+                       return;
+               }
+               core.removeProject(project);
        }
 
        //
@@ -433,36 +847,301 @@ public class SwingInterface implements CoreListener {
        /**
         * {@inheritDoc}
         */
-       public void loadingProjectsFailed(String directory) {
+       public void loadingProjectsDone(String directory) {
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.projectLoadingDone"));
+               for (Project project : core.getProjects()) {
+                       projectAdded(project, false);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void loadingProjectsFailed(String directory, Throwable throwable) {
                JOptionPane.showMessageDialog(mainWindow, I18n.get("mainWindow.error.projectLoadingFailed.message", directory), I18n.get("mainWindow.error.projectLoadingFailed.title"), JOptionPane.ERROR_MESSAGE);
        }
 
        /**
         * {@inheritDoc}
         */
+       public void savingProjectsDone(String directory) {
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.projectSavingDone"));
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void savingProjectsFailed(String directory, Throwable throwabled) {
+               /* TODO */
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void projectAdded(Project project) {
+               project.setName(I18n.get("general.newProject.name"));
+               project.setDescription(I18n.get("general.newProject.description", new Date()));
+               projectAdded(project, true);
+       }
+
+       /**
+        * @param project
+        * @param switchToProject
+        */
+       private void projectAdded(final Project project, boolean switchToProject) {
+               insertProjectActions.put(project, new I18nAction("mainWindow.button.insertProject") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               insertProject(project);
+                       }
+               });
+               cloneProjectActions.put(project, new I18nAction("mainWindow.button.cloneProject") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               cloneProject(project);
+                       }
+               });
+               deleteProjectActions.put(project, new I18nAction("mainWindow.button.deleteProject") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               deleteProject(project);
+                       }
+               });
+               projectList.add(project);
+               mainWindow.addProject(project, switchToProject);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void projectCloned(Project clonedProject, Project projectClone) {
+               projectAdded(projectClone, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void projectRemoved(Project project) {
+               mainWindow.removeProject(project);
+               cloneProjectActions.remove(project);
+               deleteProjectActions.remove(project);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void loadingNodesDone(String directory) {
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.loadingNodesDone"));
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void loadingNodesFailed(String directory, Throwable throwable) {
+               /* TODO */
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void savingNodesDone(String directory) {
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.savingNodesDone"));
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void savingNodesFailed(String directory, Throwable throwable) {
+               /* TODO */
+       }
+
+       /**
+        * {@inheritDoc}
+        */
        public void coreLoaded() {
-               this.nodeList = core.getNodes();
-               manageNodesDialog.setNodeList(nodeList);
                mainWindow.setVisible(true);
-               mainWindow.setStatusBarText("Core loaded.");
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.coreLoaded"));
        }
 
        /**
         * {@inheritDoc}
         */
-       public void nodeConnected(Node node) {
+       public void coreStopped() {
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.coreStopped"));
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void nodeAdded(final Node node) {
+               logger.log(Level.INFO, "node added: " + node);
+               nodeList.add(node);
+               node.addPropertyChangeListener(this);
+               logger.log(Level.FINE, "nodeList.size(): " + nodeList.size());
+               nodeConnectActions.put(node, new I18nAction("mainWindow.menu.node.item.connect") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent e) {
+                               nodeConnect(node);
+                       }
+               });
+               nodeDisconnectActions.put(node, new I18nAction("mainWindow.menu.node.item.disconnect") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent e) {
+                               nodeDisconnect(node);
+                       }
+               });
+               nodeDisconnectActions.get(node).setEnabled(false);
+               nodeEditActions.put(node, new I18nAction("mainWindow.menu.node.item.edit") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               editNode(node);
+                       }
+               });
+               nodeDeleteActions.put(node, new I18nAction("mainWindow.menu.node.item.remove") {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               deleteNode(node);
+                       }
+               });
+               mainWindow.addNode(node);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void nodeRemoved(Node node) {
+               logger.log(Level.INFO, "node removed: " + node);
+               nodeList.remove(node);
+               node.removePropertyChangeListener(this);
+               nodeConnectActions.remove(node);
+               nodeDisconnectActions.remove(node);
+               nodeEditActions.remove(node);
+               nodeDeleteActions.remove(node);
+               mainWindow.removeNode(node);
        }
 
        /**
         * {@inheritDoc}
         */
        public void nodeConnecting(Node node) {
+               nodeConnectActions.get(node).setEnabled(false);
+               nodeEditActions.get(node).setEnabled(false);
+               nodeDeleteActions.get(node).setEnabled(false);
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.connectingToNode", node.getName(), node.getHostname(), node.getPort()));
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void nodeConnected(Node node) {
+               nodeDisconnectActions.get(node).setEnabled(true);
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.connectedToNode", node.getName(), node.getHostname(), node.getPort()));
+               mainWindow.setOnline(node);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void nodeConnectionFailed(Node node, Throwable cause) {
+               nodeConnectActions.get(node).setEnabled(true);
+               nodeEditActions.get(node).setEnabled(true);
+               nodeDeleteActions.get(node).setEnabled(true);
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.connectionToNodeFailed", node.getName(), node.getHostname(), node.getPort(), (cause != null) ? cause.getMessage() : "no reason given"));
+               mainWindow.setError(node);
+               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);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public void nodeDisconnected(Node node, Throwable throwable) {
+               nodeDisconnectActions.get(node).setEnabled(false);
+               nodeConnectActions.get(node).setEnabled(true);
+               nodeEditActions.get(node).setEnabled(true);
+               nodeDeleteActions.get(node).setEnabled(true);
+               mainWindow.setStatusBarText(I18n.get("mainWindow.statusBar.disconnectedFromNode", node.getName(), node.getHostname(), node.getPort()));
+               mainWindow.setOffline(node);
+       }
+
+       /**
+        * @see net.pterodactylus.jsite.core.CoreListener#projectInsertStarted(net.pterodactylus.jsite.core.Project)
+        */
+       public void projectInsertStarted(Project project) {
+               mainWindow.projectInsertStarted(project);
+       }
+
+       /**
+        * @see net.pterodactylus.jsite.core.CoreListener#projectInsertProgressed(net.pterodactylus.jsite.core.Project,
+        *      int, int, int, int, int, boolean)
+        */
+       public void projectInsertProgressed(Project project, int totalBlocks, int requiredBlocks, int successfulBlocks, int failedBlocks, int fatallyFailedBlocks, boolean finalizedTotal) {
+               mainWindow.projectInsertProgressed(project, totalBlocks, requiredBlocks, successfulBlocks, failedBlocks, fatallyFailedBlocks, finalizedTotal);
        }
 
        /**
+        * @see net.pterodactylus.jsite.core.CoreListener#projectInsertGeneratedURI(net.pterodactylus.jsite.core.Project,
+        *      java.lang.String)
+        */
+       public void projectInsertGeneratedURI(Project project, String uri) {
+               mainWindow.projectInsertGeneratedURI(project);
+       }
+
+       /**
+        * @see net.pterodactylus.jsite.core.CoreListener#projectInsertFinished(net.pterodactylus.jsite.core.Project,
+        *      boolean)
+        */
+       public void projectInsertFinished(Project project, boolean success) {
+               mainWindow.projectInsertFinished(project, success);
+       }
+
+       //
+       // INTERFACE LoggingListener
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       public void logged(LogRecord logRecord) {
+               logWindow.logged(logRecord);
+       }
+
+       //
+       // INTERFACE PropertyChangeListener
+       //
+
+       /**
         * {@inheritDoc}
         */
-       public void nodeDisconnected(Node node) {
+       public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
+               /* do not react to anything (yet). */
        }
 
 }