fix file information display
[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.Color;
24 import java.awt.Component;
25 import java.awt.Dimension;
26 import java.awt.FlowLayout;
27 import java.awt.Font;
28 import java.awt.GridBagConstraints;
29 import java.awt.GridBagLayout;
30 import java.awt.Insets;
31 import java.awt.Point;
32 import java.awt.event.ActionEvent;
33 import java.awt.event.ActionListener;
34 import java.awt.event.MouseEvent;
35 import java.awt.event.MouseListener;
36 import java.beans.PropertyChangeEvent;
37 import java.beans.PropertyChangeListener;
38 import java.io.File;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.logging.Level;
45 import java.util.logging.Logger;
46
47 import javax.swing.BorderFactory;
48 import javax.swing.JButton;
49 import javax.swing.JCheckBoxMenuItem;
50 import javax.swing.JDialog;
51 import javax.swing.JLabel;
52 import javax.swing.JOptionPane;
53 import javax.swing.JPanel;
54 import javax.swing.JPopupMenu;
55 import javax.swing.JScrollPane;
56 import javax.swing.JTextField;
57 import javax.swing.JTree;
58 import javax.swing.event.TreeModelEvent;
59 import javax.swing.event.TreeModelListener;
60 import javax.swing.event.TreeSelectionEvent;
61 import javax.swing.event.TreeSelectionListener;
62 import javax.swing.tree.DefaultTreeCellRenderer;
63 import javax.swing.tree.TreeModel;
64 import javax.swing.tree.TreePath;
65
66 import net.pterodactylus.jsite.i18n.I18n;
67 import net.pterodactylus.jsite.i18n.I18nable;
68 import net.pterodactylus.jsite.i18n.gui.I18nAction;
69 import net.pterodactylus.jsite.i18n.gui.I18nLabel;
70 import net.pterodactylus.jsite.project.FileOverride;
71 import net.pterodactylus.jsite.project.Project;
72 import net.pterodactylus.jsite.project.ProjectFile;
73 import net.pterodactylus.util.logging.Logging;
74 import net.pterodactylus.util.swing.SwingUtils;
75
76 /**
77  * Manages physical and virtual files in a project.
78  *
79  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
80  */
81 public class FileManager extends JDialog implements I18nable, ActionListener, TreeSelectionListener, MouseListener {
82
83         /** Logger. */
84         private static final Logger logger = Logging.getLogger(FileManager.class.getName());
85
86         /** The Swing interface. */
87         private final SwingInterface swingInterface;
88
89         /** The project whose files to manage. */
90         private final Project project;
91
92         /** The tree model for the project files. */
93         private final ProjectFileTreeModel fileTreeModel;
94
95         /** The tree cell renderer. */
96         private final FileCellRenderer fileCellRenderer;
97
98         /** The “rescan” action. */
99         private I18nAction rescanAction;
100
101         /** The “close” action. */
102         private I18nAction closeAction;
103
104         /** The “set default file” action. */
105         private I18nAction setDefaultFileAction;
106
107         /** The “insert” action. */
108         private I18nAction insertAction;
109
110         /** The “project files” label. */
111         private I18nLabel projectFilesLabel;
112
113         /** The tree that shows the files. */
114         private JTree fileTree;
115
116         /** The scroll pane that holds the file tree. */
117         private JScrollPane fileScrollPane;
118
119         /** The “file properties” label. */
120         private I18nLabel filePropertiesLabel;
121
122         /** The “file path” label. */
123         private I18nLabel filePathLabel;
124
125         /** The “file path” textfield. */
126         private JTextField filePathTextField;
127
128         /** The “file name” label. */
129         private I18nLabel fileNameLabel;
130
131         /** The “file name” textfield. */
132         private JTextField fileNameTextField;
133
134         /** The “file size” label. */
135         private I18nLabel fileSizeLabel;
136
137         /** The “file size” text field. */
138         private JTextField fileSizeTextField;
139
140         /** The context menu for the tree. */
141         private JPopupMenu treeContextMenu;
142
143         /** The “insert” checkbox. */
144         private JCheckBoxMenuItem insertCheckBoxMenuItem;
145
146         /**
147          * Creates a new file manager.
148          *
149          * @param swingInterface
150          *            The Swing interface
151          * @param project
152          *            The project whose files to manage
153          */
154         public FileManager(SwingInterface swingInterface, Project project) {
155                 super(swingInterface.getMainWindow(), I18n.get("fileManager.title", project.getName()), true);
156                 logger.log(Level.FINEST, "project: " + project);
157                 this.swingInterface = swingInterface;
158                 this.project = project;
159                 fileTreeModel = new ProjectFileTreeModel();
160                 project.addPropertyChangeListener(fileTreeModel);
161                 fileCellRenderer = new FileCellRenderer();
162                 initActions();
163                 initComponents();
164                 pack();
165                 SwingUtils.center(this);
166         }
167
168         //
169         // ACTIONS
170         //
171
172         /**
173          * @see java.awt.Component#setVisible(boolean)
174          */
175         @Override
176         public void setVisible(boolean visible) {
177                 if (visible) {
178                         initiateFileScan();
179                 }
180                 super.setVisible(visible);
181         }
182
183         //
184         // PRIVATE METHODS
185         //
186
187         /**
188          * Initializes all actions.
189          */
190         private void initActions() {
191                 closeAction = new I18nAction("fileManager.button.close") {
192
193                         /**
194                          * {@inheritDoc}
195                          */
196                         public void actionPerformed(ActionEvent e) {
197                                 setVisible(false);
198                         }
199                 };
200                 rescanAction = new I18nAction("fileManager.button.rescan") {
201
202                         /**
203                          * {@inheritDoc}
204                          */
205                         @SuppressWarnings("synthetic-access")
206                         public void actionPerformed(ActionEvent actionEvent) {
207                                 initiateFileScan();
208                         }
209                 };
210                 setDefaultFileAction = new I18nAction("fileManager.menu.item.setDefaultFile") {
211
212                         /**
213                          * {@inheritDoc}
214                          */
215                         public void actionPerformed(ActionEvent actionEvent) {
216                                 TreePath selectedPath = fileTree.getSelectionPath();
217                                 if (selectedPath == null) {
218                                         logger.log(Level.WARNING, "nothing selected!");
219                                         return;
220                                 }
221                                 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) selectedPath.getLastPathComponent();
222                                 if (isHidden(projectFileWrapper)) {
223                                         /* TODO - i18n */
224                                         JOptionPane.showMessageDialog(FileManager.this, I18n.get(""), I18n.get(""), JOptionPane.ERROR_MESSAGE);
225                                         return;
226                                 }
227                                 if (projectFileWrapper.getProjectFile().isDirectory()) {
228                                         /* TODO - i18n */
229                                         JOptionPane.showMessageDialog(FileManager.this, I18n.get(""), I18n.get(""), JOptionPane.ERROR_MESSAGE);
230                                         return;
231                                 }
232                                 String completePath = projectFileWrapper.getProjectFile().getCompletePath();
233                                 project.setDefaultFile(completePath);
234                         }
235                 };
236                 insertAction = new I18nAction("fileManager.menu.item.insert") {
237
238                         /**
239                          * {@inheritDoc}
240                          */
241                         public void actionPerformed(ActionEvent e) {
242                                 /* TODO */
243                         }
244                 };
245         }
246
247         /**
248          * Initializes all components.
249          */
250         private void initComponents() {
251                 treeContextMenu = new JPopupMenu();
252                 treeContextMenu.add(setDefaultFileAction);
253                 insertCheckBoxMenuItem = new JCheckBoxMenuItem(insertAction);
254                 treeContextMenu.add(insertCheckBoxMenuItem);
255
256                 JPanel contentPanel = new JPanel(new BorderLayout(12, 12));
257                 contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
258
259                 contentPanel.add(createFileManagerPanel(), BorderLayout.CENTER);
260                 contentPanel.add(createButtonPanel(), BorderLayout.PAGE_END);
261
262                 setContentPane(contentPanel);
263         }
264
265         /**
266          * Creates the main panel with the file tree and the file properties.
267          *
268          * @return The mail panel
269          */
270         private Component createFileManagerPanel() {
271                 JPanel fileManagerPanel = new JPanel(new BorderLayout(12, 12));
272
273                 /* file tree panel */
274                 JPanel fileTreePanel = new JPanel(new BorderLayout(12, 12));
275                 fileManagerPanel.add(fileTreePanel, BorderLayout.LINE_START);
276
277                 fileTree = new JTree(fileTreeModel);
278                 fileTree.setShowsRootHandles(false);
279                 fileTree.addTreeSelectionListener(this);
280                 fileTree.addMouseListener(this);
281                 fileTree.setCellRenderer(fileCellRenderer);
282                 fileTreePanel.add(fileScrollPane = new JScrollPane(fileTree), BorderLayout.CENTER);
283                 fileScrollPane.setPreferredSize(new Dimension(200, 350));
284
285                 projectFilesLabel = new I18nLabel("fileManager.label.projectFiles", fileTree);
286                 JPanel projectFilesLabelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
287                 fileTreePanel.add(projectFilesLabelPanel, BorderLayout.NORTH);
288                 projectFilesLabelPanel.add(projectFilesLabel);
289
290                 /* the right panel */
291                 JPanel rightPanel = new JPanel(new BorderLayout(12, 12));
292                 fileManagerPanel.add(rightPanel, BorderLayout.CENTER);
293
294                 /* properties panel */
295                 JPanel propertiesPanel = new JPanel(new GridBagLayout());
296                 rightPanel.add(propertiesPanel, BorderLayout.CENTER);
297                 propertiesPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(12, 12, 12, 12)));
298                 propertiesPanel.setPreferredSize(new Dimension(400, 350));
299
300                 filePropertiesLabel = new I18nLabel("fileManager.label.fileProperties");
301                 filePropertiesLabel.setFont(filePropertiesLabel.getFont().deriveFont(Font.BOLD));
302                 propertiesPanel.add(filePropertiesLabel, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
303
304                 filePathLabel = new I18nLabel("fileManager.label.filePath");
305                 filePathTextField = new JTextField();
306                 filePathTextField.setEditable(false);
307                 propertiesPanel.add(filePathLabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 24, 0, 0), 0, 0));
308                 propertiesPanel.add(filePathTextField, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 12, 0, 0), 0, 0));
309
310                 fileNameLabel = new I18nLabel("fileManager.label.fileName");
311                 fileNameTextField = new JTextField();
312                 fileNameTextField.setEditable(false);
313                 propertiesPanel.add(fileNameLabel, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 24, 0, 0), 0, 0));
314                 propertiesPanel.add(fileNameTextField, new GridBagConstraints(1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 12, 0, 0), 0, 0));
315
316                 fileSizeLabel = new I18nLabel("fileManager.label.fileSize");
317                 fileSizeTextField = new JTextField();
318                 fileSizeTextField.setEditable(false);
319                 propertiesPanel.add(fileSizeLabel, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 24, 0, 0), 0, 0));
320                 propertiesPanel.add(fileSizeTextField, new GridBagConstraints(1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 12, 0, 0), 0, 0));
321
322                 /* glue panel. */
323                 propertiesPanel.add(new JPanel(), new GridBagConstraints(0, 4, 2, 1, 1.0, 1.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
324
325                 /* action button panel */
326                 JPanel actionButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 12, 12));
327                 rightPanel.add(actionButtonPanel, BorderLayout.PAGE_END);
328                 actionButtonPanel.setBorder(BorderFactory.createEtchedBorder());
329
330                 JButton rescanButton = new JButton(rescanAction);
331                 actionButtonPanel.add(rescanButton);
332
333                 return fileManagerPanel;
334         }
335
336         /**
337          * Creates the button panel.
338          *
339          * @return The button panel
340          */
341         private Component createButtonPanel() {
342                 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 12, 12));
343
344                 buttonPanel.setBorder(BorderFactory.createEmptyBorder(-12, -12, -12, -12));
345                 JButton closeButton = new JButton(closeAction);
346                 buttonPanel.add(closeButton);
347
348                 getRootPane().setDefaultButton(closeButton);
349                 return buttonPanel;
350         }
351
352         /**
353          * Initiates a file scan.
354          */
355         private void initiateFileScan() {
356                 swingInterface.getThreadPool().execute(new Runnable() {
357
358                         /**
359                          * @see java.lang.Runnable#run()
360                          */
361                         @SuppressWarnings("synthetic-access")
362                         public void run() {
363                                 fileTree.setEnabled(false);
364                                 rescanAction.setEnabled(false);
365                                 ProjectFile baseProjectFile = project.getBaseFile();
366                                 if (baseProjectFile != null) {
367                                         fileTreeModel.setBaseProjectFile(baseProjectFile);
368                                 }
369                                 // fileScrollPane.revalidate();
370                                 rescanAction.setEnabled(true);
371                                 fileTree.setEnabled(true);
372                         }
373
374                 });
375         }
376
377         /**
378          * Checks whether the given mouse event is a popup trigger and occured over
379          * a file. If so, the context menu is shown.
380          *
381          * @param mouseEvent
382          *            The mouse event to check
383          */
384         private void maybeShowContextMenu(MouseEvent mouseEvent) {
385                 if (!mouseEvent.isPopupTrigger()) {
386                         return;
387                 }
388                 Point eventLocation = mouseEvent.getPoint();
389                 TreePath clickedPath = fileTree.getPathForLocation(eventLocation.x, eventLocation.y);
390                 if (clickedPath == null) {
391                         return;
392                 }
393                 fileTree.setSelectionPath(clickedPath);
394                 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) clickedPath.getLastPathComponent();
395                 insertCheckBoxMenuItem.setSelected(!isHidden(projectFileWrapper));
396                 treeContextMenu.show(fileTree, eventLocation.x, eventLocation.y);
397         }
398
399         /**
400          * Finds whether the {@link ProjectFile} given by
401          * <code>projectFileWrapper</code> is hidden.
402          *
403          * @param projectFileWrapper
404          *            The wrapped project file
405          * @return <code>true</code> if the file is hidden and should not be
406          *         inserted, <code>false</code> otherwise
407          */
408         private boolean isHidden(ProjectFileWrapper projectFileWrapper) {
409                 ProjectFile projectFile = projectFileWrapper.getProjectFile();
410                 FileOverride fileOverride = project.getFileOverrides().get(projectFile.getCompletePath());
411                 return ((fileOverride == null) && projectFile.isHidden()) || ((fileOverride != null) && (fileOverride.isInsert()));
412         }
413
414         //
415         // INTERFACE I18nable
416         //
417
418         /**
419          * {@inheritDoc}
420          */
421         public void updateI18n() {
422                 setTitle(I18n.get("fileManager.title", project.getName()));
423                 projectFilesLabel.updateI18n();
424                 filePropertiesLabel.updateI18n();
425                 filePathLabel.updateI18n();
426         }
427
428         //
429         // INTERFACE TreeSelectionListener
430         //
431
432         /**
433          * {@inheritDoc}
434          */
435         public void valueChanged(TreeSelectionEvent treeSelectionEvent) {
436                 TreePath[] selectedPaths = fileTree.getSelectionPaths();
437                 filePathTextField.setText("");
438                 fileNameTextField.setText("");
439                 fileSizeTextField.setText("");
440                 if ((selectedPaths != null) && (selectedPaths.length == 1)) {
441                         Object lastPathComponent = selectedPaths[0].getLastPathComponent();
442                         if (!(lastPathComponent instanceof ProjectFileWrapper)) {
443                                 logger.log(Level.SEVERE, "lastPathComponent is not a ProjectFileWrapper!");
444                                 return;
445                         }
446                         ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) lastPathComponent;
447                         ProjectFile projectFile = projectFileWrapper.getProjectFile();
448                         if (projectFile.isFile()) {
449                                 String completePath = projectFile.getCompletePath();
450                                 int lastSeparator = completePath.lastIndexOf(File.separatorChar);
451                                 if (lastSeparator == -1) {
452                                         filePathTextField.setText("");
453                                 } else {
454                                         filePathTextField.setText(completePath.substring(0, lastSeparator));
455                                 }
456                                 fileNameTextField.setText(projectFile.getName());
457                                 fileSizeTextField.setText(String.valueOf(projectFile.getSize()));
458                         }
459                 }
460         }
461
462         //
463         // INTERFACE ActionListener
464         //
465
466         /**
467          * {@inheritDoc}
468          */
469         public void actionPerformed(ActionEvent actionEvent) {
470                 /* TODO */
471         }
472
473         //
474         // INTERFACE MouseListener
475         //
476
477         /**
478          * {@inheritDoc}
479          */
480         public void mouseClicked(MouseEvent mouseEvent) {
481                 maybeShowContextMenu(mouseEvent);
482         }
483
484         /**
485          * {@inheritDoc}
486          */
487         public void mouseEntered(MouseEvent mouseEvent) {
488                 /* ignore. */
489         }
490
491         /**
492          * {@inheritDoc}
493          */
494         public void mouseExited(MouseEvent mouseEvent) {
495                 /* ignore. */
496         }
497
498         /**
499          * {@inheritDoc}
500          */
501         public void mousePressed(MouseEvent mouseEvent) {
502                 maybeShowContextMenu(mouseEvent);
503         }
504
505         /**
506          * {@inheritDoc}
507          */
508         public void mouseReleased(MouseEvent mouseEvent) {
509                 maybeShowContextMenu(mouseEvent);
510         }
511
512         /**
513          * Tree cell renderer that takes care of certain display properties for
514          * project-specific stuff.
515          *
516          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
517          */
518         private class FileCellRenderer extends DefaultTreeCellRenderer {
519
520                 /**
521                  * Empty constructor.
522                  */
523                 FileCellRenderer() {
524                         /* do nothing. */
525                 }
526
527                 /**
528                  * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(javax.swing.JTree,
529                  *      java.lang.Object, boolean, boolean, boolean, int, boolean)
530                  */
531                 @SuppressWarnings("synthetic-access")
532                 @Override
533                 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
534                         Component superCellRenderer = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
535                         if (!(superCellRenderer instanceof JLabel)) {
536                                 logger.log(Level.SEVERE, "superCellRenderer is not a JLabel!");
537                                 return superCellRenderer;
538                         }
539                         if (!(value instanceof ProjectFileWrapper)) {
540                                 logger.log(Level.SEVERE, "value is not a ProjectFileWrapper!");
541                                 return superCellRenderer;
542                         }
543                         ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) value;
544                         ProjectFile projectFile = projectFileWrapper.getProjectFile();
545                         String completePath = projectFile.getCompletePath();
546                         boolean paintBold = false;
547                         boolean paintHalfColor = false;
548                         if (projectFile.isFile() && projectFile.isHidden()) {
549                                 /* TODO - check override */
550                                 paintHalfColor = true;
551                         } else if (completePath.equals(project.getDefaultFile())) {
552                                 paintBold = true;
553                         } else if (projectFile.getParents().size() == 1) {
554                                 paintBold = true;
555                         }
556                         if (paintHalfColor) {
557                                 /* TODO - cache colors */
558                                 Color foreground = superCellRenderer.getForeground();
559                                 Color background = selected ? getBackgroundSelectionColor() : getBackgroundNonSelectionColor();
560                                 Color averageColor = new Color((foreground.getRed() + background.getRed()) / 2, (foreground.getGreen() + background.getGreen()) / 2, (foreground.getBlue() + background.getBlue()) / 2);
561                                 superCellRenderer.setForeground(averageColor);
562                         } else {
563                                 superCellRenderer.setForeground(selected ? getTextSelectionColor() : getTextNonSelectionColor());
564                         }
565                         if (paintBold) {
566                                 superCellRenderer.setFont(superCellRenderer.getFont().deriveFont(Font.BOLD));
567                         } else {
568                                 superCellRenderer.setFont(superCellRenderer.getFont().deriveFont(Font.PLAIN));
569                         }
570                         return superCellRenderer;
571                 }
572
573         }
574
575         /**
576          * TreeModel that is based on {@link Project#getBaseFile()}.
577          *
578          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
579          */
580         private class ProjectFileTreeModel implements TreeModel, PropertyChangeListener {
581
582                 /** Tree model listeners. */
583                 private final List<TreeModelListener> treeModelListeners = Collections.synchronizedList(new ArrayList<TreeModelListener>());
584
585                 /** The base project file. */
586                 private ProjectFile baseProjectFile;
587
588                 /** Maps path names to project files. */
589                 private final Map<String, ProjectFile> pathProjectFiles = Collections.synchronizedMap(new HashMap<String, ProjectFile>());
590
591                 /** Maps project files to wrappers. */
592                 private final Map<ProjectFile, ProjectFileWrapper> projectFileWrappers = Collections.synchronizedMap(new HashMap<ProjectFile, ProjectFileWrapper>());
593
594                 /**
595                  * Empty constructor.
596                  */
597                 ProjectFileTreeModel() {
598                         /* do nothing. */
599                 }
600
601                 //
602                 // EVENT MANAGEMENT
603                 //
604
605                 /**
606                  * {@inheritDoc}
607                  */
608                 public void addTreeModelListener(TreeModelListener treeModelListener) {
609                         treeModelListeners.add(treeModelListener);
610                 }
611
612                 /**
613                  * {@inheritDoc}
614                  */
615                 public void removeTreeModelListener(TreeModelListener treeModelListener) {
616                         treeModelListeners.remove(treeModelListener);
617                 }
618
619                 /**
620                  * Notifies all listeners that a node has changed.
621                  *
622                  * @param changedProjectFileWrapper
623                  *            The wrapper around the changed project file
624                  */
625                 protected void fireTreeNodesChanged(ProjectFileWrapper changedProjectFileWrapper) {
626                         ProjectFile changedProjectFile = changedProjectFileWrapper.getProjectFile();
627                         ProjectFile changedProjectFileParent = changedProjectFile.getParent();
628                         ProjectFile currentProjectFileParent = changedProjectFile;
629                         List<ProjectFileWrapper> parentProjectFileWrappers = new ArrayList<ProjectFileWrapper>();
630                         do {
631                                 parentProjectFileWrappers.add(0, projectFileWrappers.get(currentProjectFileParent));
632                                 currentProjectFileParent = currentProjectFileParent.getParent();
633                         } while (currentProjectFileParent != null);
634                         TreeModelEvent treeModelEvent = new TreeModelEvent(this, parentProjectFileWrappers.toArray(), new int[] { getIndexOfChild(projectFileWrappers.get(changedProjectFileParent), changedProjectFileWrapper) }, new Object[] { changedProjectFileWrapper });
635                         for (TreeModelListener treeModelListener : treeModelListeners) {
636                                 treeModelListener.treeNodesChanged(treeModelEvent);
637                         }
638                 }
639
640                 /**
641                  * Notifies all listeners that the tree structure has changed
642                  * significantly.
643                  *
644                  * @see TreeModelListener#treeStructureChanged(TreeModelEvent)
645                  * @param newRootNode
646                  */
647                 protected void fireTreeStructureChanged(ProjectFileWrapper newRootNode) {
648                         for (TreeModelListener treeModelListener : treeModelListeners) {
649                                 treeModelListener.treeStructureChanged(new TreeModelEvent(this, new Object[] { newRootNode }));
650                         }
651                 }
652
653                 //
654                 // ACCESSORS
655                 //
656
657                 /**
658                  * Sets the new base project file. This causes the model to reload.
659                  *
660                  * @param baseProjectFile
661                  *            The new base project file
662                  */
663                 @SuppressWarnings("synthetic-access")
664                 public synchronized void setBaseProjectFile(ProjectFile baseProjectFile) {
665                         this.baseProjectFile = baseProjectFile;
666                         projectFileWrappers.clear();
667                         pathProjectFiles.clear();
668                         createWrappers(baseProjectFile);
669                         projectFileWrappers.get(baseProjectFile).setNameOverride(project.getName());
670                         fireTreeStructureChanged(projectFileWrappers.get(baseProjectFile));
671                 }
672
673                 //
674                 // PRIVATE METHODS
675                 //
676
677                 /**
678                  * Creates {@link ProjectFileWrapper}s for all files below the given
679                  * project file.
680                  *
681                  * @param projectFile
682                  *            The base project file for all project files to create
683                  *            wrappers for
684                  */
685                 private void createWrappers(ProjectFile projectFile) {
686                         projectFileWrappers.put(projectFile, new ProjectFileWrapper(projectFile));
687                         pathProjectFiles.put(projectFile.getCompletePath(), projectFile);
688                         for (ProjectFile projectFileChild : projectFile.getFiles()) {
689                                 if (projectFileChild.isDirectory()) {
690                                         createWrappers(projectFileChild);
691                                 }
692                                 projectFileWrappers.put(projectFileChild, new ProjectFileWrapper(projectFileChild));
693                                 pathProjectFiles.put(projectFileChild.getCompletePath(), projectFileChild);
694                         }
695                 }
696
697                 //
698                 // INTERFACE TreeModel
699                 //
700
701                 /**
702                  * {@inheritDoc}
703                  */
704                 public Object getRoot() {
705                         return projectFileWrappers.get(baseProjectFile);
706                 }
707
708                 /**
709                  * {@inheritDoc}
710                  */
711                 @SuppressWarnings("synthetic-access")
712                 public Object getChild(Object parent, int index) {
713                         if (!(parent instanceof ProjectFileWrapper)) {
714                                 logger.log(Level.SEVERE, "parent is not a ProjectFileWrapper!");
715                                 return null;
716                         }
717                         ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) parent;
718                         ProjectFile projectFile = projectFileWrapper.getProjectFile();
719                         return projectFileWrappers.get(projectFile.getFiles().get(index));
720                 }
721
722                 /**
723                  * {@inheritDoc}
724                  */
725                 @SuppressWarnings("synthetic-access")
726                 public int getChildCount(Object parent) {
727                         if (!(parent instanceof ProjectFileWrapper)) {
728                                 logger.log(Level.SEVERE, "parent is not a ProjectFileWrapper!");
729                                 return -1;
730                         }
731                         ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) parent;
732                         ProjectFile projectFile = projectFileWrapper.getProjectFile();
733                         return projectFile.getFiles().size();
734                 }
735
736                 /**
737                  * {@inheritDoc}
738                  */
739                 @SuppressWarnings("synthetic-access")
740                 public int getIndexOfChild(Object parent, Object child) {
741                         if (!(parent instanceof ProjectFileWrapper)) {
742                                 logger.log(Level.SEVERE, "parent is not a ProjectFileWrapper!");
743                                 return -1;
744                         }
745                         if (!(child instanceof ProjectFileWrapper)) {
746                                 logger.log(Level.SEVERE, "child is not a ProjectFileWrapper!");
747                                 return -1;
748                         }
749                         ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) parent;
750                         ProjectFile projectFile = projectFileWrapper.getProjectFile();
751                         return projectFile.getFiles().indexOf(((ProjectFileWrapper) child).getProjectFile());
752                 }
753
754                 /**
755                  * {@inheritDoc}
756                  */
757                 @SuppressWarnings("synthetic-access")
758                 public boolean isLeaf(Object node) {
759                         if (!(node instanceof ProjectFileWrapper)) {
760                                 logger.log(Level.SEVERE, "node is not a ProjectFileWrapper!");
761                                 return true;
762                         }
763                         ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) node;
764                         return projectFileWrapper.getProjectFile().isFile();
765                 }
766
767                 /**
768                  * {@inheritDoc}
769                  */
770                 public void valueForPathChanged(TreePath path, Object newValue) {
771                         /* ignore, items will not be modified in tree. */
772                 }
773
774                 //
775                 // INTERFACE PropertyChangeListener
776                 //
777
778                 /**
779                  * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
780                  */
781                 public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
782                         if (Project.PROPERTY_DEFAULT_FILE.equals(propertyChangeEvent.getPropertyName())) {
783                                 String oldCompletePath = (String) propertyChangeEvent.getOldValue();
784                                 String newCompletePath = (String) propertyChangeEvent.getNewValue();
785                                 ProjectFile oldProjectFile = pathProjectFiles.get(oldCompletePath);
786                                 ProjectFile newProjectFile = pathProjectFiles.get(newCompletePath);
787                                 ProjectFileWrapper oldProjectFileWrapper = projectFileWrappers.get(oldProjectFile);
788                                 ProjectFileWrapper newProjectFileWrapper = projectFileWrappers.get(newProjectFile);
789                                 System.out.println("oldProjectFileWrapper: " + oldProjectFileWrapper);
790                                 System.out.println("newProjectFileWrapper: " + newProjectFileWrapper);
791                                 fireTreeNodesChanged(oldProjectFileWrapper);
792                                 fireTreeNodesChanged(newProjectFileWrapper);
793                                 /* HACK - swing sucks a bit. */
794                                 fileTree.setShowsRootHandles(false);
795                         }
796                 }
797
798         }
799
800         /**
801          * Wrapper around a {@link ProjectFile} that overwrites
802          * {@link Object#toString()} to return the project file’s name.
803          *
804          * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
805          */
806         private static class ProjectFileWrapper {
807
808                 /** The wrapped project file. */
809                 private final ProjectFile projectFile;
810
811                 /** The override name. */
812                 private String nameOverride;
813
814                 /**
815                  * Creates a new wrapper around a project file.
816                  *
817                  * @param projectFile
818                  *            The project file to wrap
819                  */
820                 public ProjectFileWrapper(ProjectFile projectFile) {
821                         this.projectFile = projectFile;
822                 }
823
824                 /**
825                  * Returns the wrapped project file.
826                  *
827                  * @return The wrapped project file
828                  */
829                 public ProjectFile getProjectFile() {
830                         return projectFile;
831                 }
832
833                 /**
834                  * Sets the name override. If the name override is not <code>null</code>
835                  * it will be shown insted of the project file’s name.
836                  *
837                  * @param nameOverride
838                  *            The name override
839                  */
840                 void setNameOverride(String nameOverride) {
841                         this.nameOverride = nameOverride;
842                 }
843
844                 /**
845                  * {@inheritDoc}
846                  */
847                 @Override
848                 public String toString() {
849                         return (nameOverride != null) ? nameOverride : projectFile.getName();
850                 }
851
852         }
853
854 }