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