add close button to file manager
[jSite2.git] / src / net / pterodactylus / jsite / gui / FileManager.java
1 /*
2  * jSite2 - FileManager.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.BorderLayout;
23 import java.awt.Component;
24 import java.awt.FlowLayout;
25 import java.awt.event.ActionEvent;
26 import java.beans.PropertyChangeEvent;
27 import java.beans.PropertyChangeListener;
28 import java.io.File;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33
34 import javax.swing.BorderFactory;
35 import javax.swing.JButton;
36 import javax.swing.JDialog;
37 import javax.swing.JFrame;
38 import javax.swing.JPanel;
39 import javax.swing.JScrollPane;
40 import javax.swing.JTree;
41 import javax.swing.event.TreeModelListener;
42 import javax.swing.tree.TreeModel;
43 import javax.swing.tree.TreePath;
44
45 import net.pterodactylus.jsite.i18n.I18n;
46 import net.pterodactylus.jsite.i18n.I18nable;
47 import net.pterodactylus.jsite.i18n.gui.I18nAction;
48 import net.pterodactylus.jsite.project.Entry;
49 import net.pterodactylus.jsite.project.Project;
50 import net.pterodactylus.util.data.Node;
51 import net.pterodactylus.util.data.Tree;
52 import net.pterodactylus.util.logging.Logging;
53 import net.pterodactylus.util.swing.SwingUtils;
54
55 /**
56  * Manages physical and virtual files in a project.
57  * 
58  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
59  */
60 public class FileManager extends JDialog implements I18nable {
61
62         /** Logger. */
63         private static final Logger logger = Logging.getLogger(FileManager.class.getName());
64
65         /** The project whose files to manage. */
66         private final Project project;
67
68         /** The tree model for the project files. */
69         private final FileTreeModel fileTreeModel;
70
71         /** The “close” action. */
72         private I18nAction closeAction;
73
74         /** The tree that shows the files. */
75         private JTree fileTree;
76
77         /**
78          * Creates a new file manager.
79          * 
80          * @param parent
81          *            The parent frame
82          * @param project
83          *            The project whose files to manage
84          */
85         public FileManager(JFrame parent, Project project) {
86                 super(parent, I18n.get("fileManager.title", project.getName()), true);
87                 logger.log(Level.FINEST, "project: " + project);
88                 this.project = project;
89                 fileTreeModel = new FileTreeModel();
90                 initActions();
91                 initComponents();
92                 SwingUtils.repackCentered(this);
93         }
94
95         //
96         // PRIVATE METHODS
97         //
98
99         /**
100          * Initializes all actions.
101          */
102         private void initActions() {
103                 closeAction = new I18nAction("fileManager.button.close") {
104
105                         /**
106                          * {@inheritDoc}
107                          */
108                         public void actionPerformed(ActionEvent e) {
109                                 setVisible(false);
110                         }
111                 };
112         }
113
114         /**
115          * Initializes all components.
116          */
117         private void initComponents() {
118                 JPanel contentPanel = new JPanel(new BorderLayout(12, 12));
119                 contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
120
121                 contentPanel.add(createFileManagerPanel(), BorderLayout.CENTER);
122                 contentPanel.add(createButtonPanel(), BorderLayout.PAGE_END);
123
124                 setContentPane(contentPanel);
125         }
126
127         /**
128          * Creates the main panel with the file tree and the file properties.
129          * 
130          * @return The mail panel
131          */
132         private Component createFileManagerPanel() {
133                 JPanel fileManagerPanel = new JPanel(new BorderLayout(12, 12));
134                 fileManagerPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(12, 12, 12, 12)));
135
136                 fileTree = new JTree(fileTreeModel);
137                 fileTree.setShowsRootHandles(false);
138                 fileManagerPanel.add(new JScrollPane(fileTree), BorderLayout.CENTER);
139
140                 return fileManagerPanel;
141         }
142
143         /**
144          * Creates the button panel.
145          * 
146          * @return The button panel
147          */
148         private Component createButtonPanel() {
149                 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 12, 12));
150
151                 buttonPanel.setBorder(BorderFactory.createEmptyBorder(-12, -12, -12, -12));
152                 JButton closeButton = new JButton(closeAction);
153                 buttonPanel.add(closeButton);
154
155                 getRootPane().setDefaultButton(closeButton);
156                 return buttonPanel;
157         }
158
159         //
160         // INTERFACE I18nable
161         //
162
163         /**
164          * {@inheritDoc}
165          */
166         public void updateI18n() {
167                 setTitle(I18n.get("fileManager.title", project.getName()));
168         }
169
170         /**
171          * Model for the tree of files.
172          * 
173          * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
174          */
175         private class FileTreeModel implements TreeModel, PropertyChangeListener {
176
177                 /** Tree model listeners. */
178                 private final List<TreeModelListener> treeModelListeners = new ArrayList<TreeModelListener>();
179
180                 /** The tree of files. */
181                 private final Tree<FileTreePath> fileTreePathTree = new Tree<FileTreePath>();
182
183                 /**
184                  * Creates a new file tree model.
185                  */
186                 FileTreeModel() {
187                         buildTree();
188                 }
189
190                 //
191                 // EVENT MANAGEMENT
192                 //
193
194                 /**
195                  * {@inheritDoc}
196                  */
197                 public void addTreeModelListener(TreeModelListener treeModelListener) {
198                         treeModelListeners.add(treeModelListener);
199                 }
200
201                 /**
202                  * {@inheritDoc}
203                  */
204                 public void removeTreeModelListener(TreeModelListener treeModelListener) {
205                         treeModelListeners.remove(treeModelListener);
206                 }
207
208                 //
209                 // ACCESSORS
210                 //
211
212                 /**
213                  * {@inheritDoc}
214                  */
215                 @SuppressWarnings("synthetic-access")
216                 public Object getChild(Object parent, int index) {
217                         logger.log(Level.FINEST, "getChild(" + parent + ", " + index + ")");
218                         Node<FileTreePath> parentNode = findNode(parent);
219                         if (parentNode != null) {
220                                 return parentNode.getChild(index).getElement();
221                         }
222                         return null;
223                 }
224
225                 /**
226                  * {@inheritDoc}
227                  */
228                 @SuppressWarnings("synthetic-access")
229                 public int getChildCount(Object parent) {
230                         logger.log(Level.FINEST, "getChildCount(" + parent + ")");
231                         Node<FileTreePath> parentNode = findNode(parent);
232                         if (parentNode != null) {
233                                 logger.log(Level.FINEST, "getChildCount(" + parent + "): " + parentNode.size());
234                                 return parentNode.size();
235                         }
236                         return -1;
237                 }
238
239                 /**
240                  * {@inheritDoc}
241                  */
242                 @SuppressWarnings("synthetic-access")
243                 public int getIndexOfChild(Object parent, Object child) {
244                         logger.log(Level.FINEST, "getIndexOfChild(" + parent + ", " + child + ")");
245                         Node<FileTreePath> parentNode = findNode(parent);
246                         if (parentNode != null) {
247                                 return parentNode.getIndexOfChild((FileTreePath) child);
248                         }
249                         return -1;
250                 }
251
252                 /**
253                  * {@inheritDoc}
254                  */
255                 @SuppressWarnings("synthetic-access")
256                 public Object getRoot() {
257                         logger.log(Level.FINEST, "getRoot()");
258                         return fileTreePathTree.getRootNode().getChild(0).getElement();
259                 }
260
261                 /**
262                  * {@inheritDoc}
263                  */
264                 @SuppressWarnings("synthetic-access")
265                 public boolean isLeaf(Object node) {
266                         logger.log(Level.FINEST, "isLeaf(" + node + ")");
267                         Node<FileTreePath> parentNode = findNode(node);
268                         if (parentNode != null) {
269                                 return parentNode.size() == 0;
270                         }
271                         return true;
272                 }
273
274                 //
275                 // ACTIONS
276                 //
277
278                 /**
279                  * {@inheritDoc}
280                  */
281                 public void valueForPathChanged(TreePath path, Object newValue) {
282                         /* TODO - implement */
283                 }
284
285                 //
286                 // PRIVATE METHODS
287                 //
288
289                 /**
290                  * Finds the node for the given object. This method is quite necessary
291                  * because the element for the root node of the JTree is
292                  * <code>null</code>
293                  * 
294                  * @param node
295                  *            The element whose node to return
296                  * @return The node, or <code>null</code> if no node could be found
297                  */
298                 private Node<FileTreePath> findNode(Object node) {
299                         if (node == null) {
300                                 return fileTreePathTree.getRootNode().getChild(0);
301                         }
302                         return fileTreePathTree.getRootNode().getChild(0).findChild((FileTreePath) node);
303                 }
304
305                 /**
306                  * Builds the tree from the project’s file entries.
307                  */
308                 @SuppressWarnings("synthetic-access")
309                 private void buildTree() {
310                         Tree<String> pathTree = new Tree<String>();
311                         Node<String> pathRootNode = pathTree.getRootNode().addChild(File.separator);
312                         logger.log(Level.FINEST, "project: " + project);
313                         buildTree(pathRootNode, project.getBasePathEntries());
314                         buildTree(pathRootNode, project.getVirtualEntries());
315                         /* now convert to a tree suitable for the JTree. */
316                         Node<FileTreePath> fileTreePathRootNode = fileTreePathTree.getRootNode();
317                         fileTreePathRootNode.removeAllChildren();
318                         convertTree(File.separator, pathRootNode, fileTreePathRootNode.addChild(new FileTreePath(File.separator, project.getName())));
319                         /* TODO - now add entries to all file tree path tree nodes. */
320                 }
321
322                 /**
323                  * Traverses the tree of path nodes and converts all paths to
324                  * {@link FileTreePath} objects, suitable for the JTree.
325                  * 
326                  * @param completePath
327                  *            The base path of the current root node
328                  * @param pathRootNode
329                  *            The root node of the path tree
330                  * @param fileTreePathRootNode
331                  *            The root node of the file tree path tree.
332                  */
333                 private void convertTree(String completePath, Node<String> pathRootNode, Node<FileTreePath> fileTreePathRootNode) {
334                         for (Node<String> pathChild: pathRootNode) {
335                                 String currentFilePath = completePath + pathChild.getElement();
336                                 Node<FileTreePath> newNode = fileTreePathRootNode.addChild(new FileTreePath(currentFilePath));
337                                 convertTree(currentFilePath, pathChild, newNode);
338                         }
339                         fileTreePathRootNode.sortChildren();
340                 }
341
342                 /**
343                  * Builds a tree matching the directory structure of the given entries.
344                  * 
345                  * @param pathRootNode
346                  *            The root node of the tree
347                  * @param entries
348                  *            The entries
349                  */
350                 private void buildTree(Node<String> pathRootNode, List<Entry> entries) {
351                         for (Entry basePathEntry: entries) {
352                                 String entryName = basePathEntry.getName();
353                                 String[] directories = entryName.split("\\" + File.separator);
354                                 Node<String> currentPathNode = pathRootNode;
355                                 for (String directory: directories) {
356                                         if (!currentPathNode.hasChild(directory)) {
357                                                 currentPathNode = currentPathNode.addChild(directory);
358                                         } else {
359                                                 currentPathNode = currentPathNode.getChild(directory);
360                                         }
361                                 }
362                         }
363                 }
364
365                 //
366                 // INTERFACE PropertyChangeListener
367                 //
368
369                 /**
370                  * {@inheritDoc}
371                  */
372                 public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
373                         if (propertyChangeEvent.getSource() instanceof Project) {
374                                 if (propertyChangeEvent.getPropertyName().equals(Project.PROPERTY_BASE_PATH_ENTRIES)) {
375                                         buildTree();
376                                 }
377                         }
378                 }
379
380         }
381
382         /**
383          * Container that is used to back the {@link FileTreeModel}. Each
384          * FileTreePath contains a complete path name, a filename, and the
385          * associated {@link Entry}, if any.
386          * 
387          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
388          */
389         private static class FileTreePath implements Comparable<FileTreePath> {
390
391                 /** The complete file path. */
392                 private final String filePath;
393
394                 /** The file name. */
395                 private final String fileName;
396
397                 /** The file entry, if any. */
398                 private Entry fileEntry;
399
400                 /**
401                  * Creates a new file tree path with an auto-detected file name. The
402                  * file name is everything after the last separator in the complete
403                  * path, or the complete path itself if it does not contain any
404                  * separators.
405                  * 
406                  * @param filePath
407                  *            The complete file path
408                  */
409                 public FileTreePath(String filePath) {
410                         this(filePath, null);
411                 }
412
413                 /**
414                  * Creates a new file tree path with the given file path and file name.
415                  * 
416                  * @param filePath
417                  *            The complete file path
418                  * @param fileName
419                  *            The file name
420                  */
421                 public FileTreePath(String filePath, String fileName) {
422                         this.filePath = filePath;
423                         if (fileName == null) {
424                                 if (filePath.indexOf(File.separatorChar) != -1) {
425                                         this.fileName = filePath.substring(filePath.lastIndexOf(File.separatorChar) + 1);
426                                 } else {
427                                         this.fileName = filePath;
428                                 }
429                         } else {
430                                 this.fileName = fileName;
431                         }
432                 }
433
434                 /**
435                  * Returns the complete file path.
436                  * 
437                  * @return The file path
438                  */
439                 public String getFilePath() {
440                         return filePath;
441                 }
442
443                 /**
444                  * Returns the file name, i.e. everything after the last
445                  * {@link File#separatorChar}.
446                  * 
447                  * @return The file name
448                  */
449                 public String getFileName() {
450                         return fileName;
451                 }
452
453                 /**
454                  * Returns the file entry associated with this path, if any.
455                  * 
456                  * @return The file entry associated with this path, or
457                  *         <code>null</code> if this path denotes a directory
458                  */
459                 public Entry getFileEntry() {
460                         return fileEntry;
461                 }
462
463                 /**
464                  * Sets the entry associated with this path.
465                  * 
466                  * @param fileEntry
467                  *            The entry
468                  */
469                 public void setFileEntry(Entry fileEntry) {
470                         this.fileEntry = fileEntry;
471                 }
472
473                 /**
474                  * {@inheritDoc}
475                  */
476                 @Override
477                 public boolean equals(Object object) {
478                         if ((object == null) || !(object instanceof FileTreePath)) {
479                                 return false;
480                         }
481                         FileTreePath fileTreePath = (FileTreePath) object;
482                         return fileTreePath.filePath.equals(filePath);
483                 }
484
485                 /**
486                  * {@inheritDoc}
487                  */
488                 @Override
489                 public int hashCode() {
490                         return filePath.hashCode();
491                 }
492
493                 /**
494                  * {@inheritDoc}
495                  */
496                 @Override
497                 public String toString() {
498                         return fileName;
499                 }
500
501                 //
502                 // INTERFACE Comparable
503                 //
504
505                 /**
506                  * {@inheritDoc}
507                  */
508                 public int compareTo(FileTreePath otherFileTreePath) {
509                         return filePath.compareTo(otherFileTreePath.filePath);
510                 }
511
512         }
513
514 }