2 * jSite2 - FileManager.java -
3 * Copyright © 2008 David Roden
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.
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.
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.
20 package net.pterodactylus.jsite.gui;
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;
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.util.ArrayList;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.List;
41 import java.util.logging.Level;
42 import java.util.logging.Logger;
44 import javax.swing.BorderFactory;
45 import javax.swing.JButton;
46 import javax.swing.JCheckBoxMenuItem;
47 import javax.swing.JDialog;
48 import javax.swing.JLabel;
49 import javax.swing.JPanel;
50 import javax.swing.JPopupMenu;
51 import javax.swing.JScrollPane;
52 import javax.swing.JTextField;
53 import javax.swing.JTree;
54 import javax.swing.event.TreeModelEvent;
55 import javax.swing.event.TreeModelListener;
56 import javax.swing.event.TreeSelectionEvent;
57 import javax.swing.event.TreeSelectionListener;
58 import javax.swing.tree.DefaultTreeCellRenderer;
59 import javax.swing.tree.TreeModel;
60 import javax.swing.tree.TreePath;
62 import net.pterodactylus.jsite.i18n.I18n;
63 import net.pterodactylus.jsite.i18n.I18nable;
64 import net.pterodactylus.jsite.i18n.gui.I18nAction;
65 import net.pterodactylus.jsite.i18n.gui.I18nLabel;
66 import net.pterodactylus.jsite.project.Project;
67 import net.pterodactylus.jsite.project.ProjectFile;
68 import net.pterodactylus.util.logging.Logging;
69 import net.pterodactylus.util.swing.SwingUtils;
72 * Manages physical and virtual files in a project.
74 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
76 public class FileManager extends JDialog implements I18nable, ActionListener, TreeSelectionListener, MouseListener {
79 private static final Logger logger = Logging.getLogger(FileManager.class.getName());
81 /** The Swing interface. */
82 private final SwingInterface swingInterface;
84 /** The project whose files to manage. */
85 private final Project project;
87 /** The tree model for the project files. */
88 private final ProjectFileTreeModel fileTreeModel;
90 /** The tree cell renderer. */
91 private final FileCellRenderer fileCellRenderer;
93 /** The “rescan” action. */
94 private I18nAction rescanAction;
96 /** The “close” action. */
97 private I18nAction closeAction;
99 /** The “set default file” action. */
100 private I18nAction setDefaultFileAction;
102 /** The “insert” action. */
103 private I18nAction insertAction;
105 /** The “project files” label. */
106 private I18nLabel projectFilesLabel;
108 /** The tree that shows the files. */
109 private JTree fileTree;
111 /** The scroll pane that holds the file tree. */
112 private JScrollPane fileScrollPane;
114 /** The “file properties” label. */
115 private I18nLabel filePropertiesLabel;
117 /** The “file path” label. */
118 private I18nLabel filePathLabel;
120 /** The “file path” textfield. */
121 private JTextField filePathTextField;
123 /** The “file name” label. */
124 private I18nLabel fileNameLabel;
126 /** The “file name” textfield. */
127 private JTextField fileNameTextField;
129 /** The “file size” label. */
130 private I18nLabel fileSizeLabel;
132 /** The “file size” text field. */
133 private JTextField fileSizeTextField;
135 /** The context menu for the tree. */
136 private JPopupMenu treeContextMenu;
138 /** The “insert” checkbox. */
139 private JCheckBoxMenuItem insertCheckBoxMenuItem;
142 * Creates a new file manager.
144 * @param swingInterface
145 * The Swing interface
147 * The project whose files to manage
149 public FileManager(SwingInterface swingInterface, Project project) {
150 super(swingInterface.getMainWindow(), I18n.get("fileManager.title", project.getName()), true);
151 logger.log(Level.FINEST, "project: " + project);
152 this.swingInterface = swingInterface;
153 this.project = project;
154 fileTreeModel = new ProjectFileTreeModel();
155 fileCellRenderer = new FileCellRenderer();
159 SwingUtils.center(this);
167 * @see java.awt.Component#setVisible(boolean)
170 public void setVisible(boolean visible) {
174 super.setVisible(visible);
182 * Initializes all actions.
184 private void initActions() {
185 closeAction = new I18nAction("fileManager.button.close") {
190 public void actionPerformed(ActionEvent e) {
194 rescanAction = new I18nAction("fileManager.button.rescan") {
199 @SuppressWarnings("synthetic-access")
200 public void actionPerformed(ActionEvent actionEvent) {
204 setDefaultFileAction = new I18nAction("fileManager.menu.item.setDefaultFile") {
209 public void actionPerformed(ActionEvent e) {
213 insertAction = new I18nAction("fileManager.menu.item.insert") {
218 public void actionPerformed(ActionEvent e) {
225 * Initializes all components.
227 private void initComponents() {
228 treeContextMenu = new JPopupMenu();
229 treeContextMenu.add(setDefaultFileAction);
230 insertCheckBoxMenuItem = new JCheckBoxMenuItem(insertAction);
231 treeContextMenu.add(insertCheckBoxMenuItem);
233 JPanel contentPanel = new JPanel(new BorderLayout(12, 12));
234 contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
236 contentPanel.add(createFileManagerPanel(), BorderLayout.CENTER);
237 contentPanel.add(createButtonPanel(), BorderLayout.PAGE_END);
239 setContentPane(contentPanel);
243 * Creates the main panel with the file tree and the file properties.
245 * @return The mail panel
247 private Component createFileManagerPanel() {
248 JPanel fileManagerPanel = new JPanel(new BorderLayout(12, 12));
250 /* file tree panel */
251 JPanel fileTreePanel = new JPanel(new BorderLayout(12, 12));
252 fileManagerPanel.add(fileTreePanel, BorderLayout.LINE_START);
254 fileTree = new JTree(fileTreeModel);
255 fileTree.setShowsRootHandles(false);
256 fileTree.addTreeSelectionListener(this);
257 fileTree.addMouseListener(this);
258 fileTree.setCellRenderer(fileCellRenderer);
259 fileTreePanel.add(fileScrollPane = new JScrollPane(fileTree), BorderLayout.CENTER);
260 fileScrollPane.setPreferredSize(new Dimension(250, 400));
262 projectFilesLabel = new I18nLabel("fileManager.label.projectFiles", fileTree);
263 JPanel projectFilesLabelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
264 fileTreePanel.add(projectFilesLabelPanel, BorderLayout.NORTH);
265 projectFilesLabelPanel.add(projectFilesLabel);
267 /* the right panel */
268 JPanel rightPanel = new JPanel(new BorderLayout(12, 12));
269 fileManagerPanel.add(rightPanel, BorderLayout.CENTER);
271 /* properties panel */
272 JPanel propertiesPanel = new JPanel(new GridBagLayout());
273 rightPanel.add(propertiesPanel, BorderLayout.CENTER);
274 propertiesPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(12, 12, 12, 12)));
276 filePropertiesLabel = new I18nLabel("fileManager.label.fileProperties");
277 filePropertiesLabel.setFont(filePropertiesLabel.getFont().deriveFont(Font.BOLD));
278 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));
280 filePathLabel = new I18nLabel("fileManager.label.filePath");
281 filePathTextField = new JTextField();
282 filePathTextField.setEditable(false);
283 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));
284 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));
286 fileNameLabel = new I18nLabel("fileManager.label.fileName");
287 fileNameTextField = new JTextField();
288 fileNameTextField.setEditable(false);
289 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));
290 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));
292 fileSizeLabel = new I18nLabel("fileManager.label.fileSize");
293 fileSizeTextField = new JTextField();
294 fileSizeTextField.setEditable(false);
295 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));
296 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));
299 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));
301 /* action button panel */
302 JPanel actionButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 12, 12));
303 rightPanel.add(actionButtonPanel, BorderLayout.PAGE_END);
304 actionButtonPanel.setBorder(BorderFactory.createEtchedBorder());
306 JButton rescanButton = new JButton(rescanAction);
307 actionButtonPanel.add(rescanButton);
309 return fileManagerPanel;
313 * Creates the button panel.
315 * @return The button panel
317 private Component createButtonPanel() {
318 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 12, 12));
320 buttonPanel.setBorder(BorderFactory.createEmptyBorder(-12, -12, -12, -12));
321 JButton closeButton = new JButton(closeAction);
322 buttonPanel.add(closeButton);
324 getRootPane().setDefaultButton(closeButton);
329 * Initiates a file scan.
331 private void initiateFileScan() {
332 swingInterface.getThreadPool().execute(new Runnable() {
335 * @see java.lang.Runnable#run()
337 @SuppressWarnings("synthetic-access")
339 fileTree.setEnabled(false);
340 rescanAction.setEnabled(false);
341 ProjectFile baseProjectFile = project.getBaseFile();
342 if (baseProjectFile != null) {
343 fileTreeModel.setBaseProjectFile(baseProjectFile);
345 // fileScrollPane.revalidate();
346 rescanAction.setEnabled(true);
347 fileTree.setEnabled(true);
354 * Checks whether the given mouse event is a popup trigger and occured over
355 * a file. If so, the context menu is shown.
358 * The mouse event to check
360 private void maybeShowContextMenu(MouseEvent mouseEvent) {
361 if (!mouseEvent.isPopupTrigger()) {
364 Point eventLocation = mouseEvent.getPoint();
365 TreePath clickedPath = fileTree.getPathForLocation(eventLocation.x, eventLocation.y);
366 if (clickedPath == null) {
369 fileTree.setSelectionPath(clickedPath);
370 treeContextMenu.show(fileTree, eventLocation.x, eventLocation.y);
374 // INTERFACE I18nable
380 public void updateI18n() {
381 setTitle(I18n.get("fileManager.title", project.getName()));
382 projectFilesLabel.updateI18n();
383 filePropertiesLabel.updateI18n();
384 filePathLabel.updateI18n();
388 // INTERFACE TreeSelectionListener
394 public void valueChanged(TreeSelectionEvent treeSelectionEvent) {
395 TreePath[] selectedPaths = fileTree.getSelectionPaths();
396 if (selectedPaths.length == 1) {
397 Object lastPathComponent = selectedPaths[0].getLastPathComponent();
398 if (!(lastPathComponent instanceof ProjectFileWrapper)) {
399 logger.log(Level.SEVERE, "lastPathComponent is not a ProjectFileWrapper!");
402 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) lastPathComponent;
403 ProjectFile projectFile = projectFileWrapper.getProjectFile();
404 if (projectFile.isFile()) {
405 fileNameTextField.setText(projectFile.getName());
406 fileSizeTextField.setText(String.valueOf(projectFile.getSize()));
412 // INTERFACE ActionListener
418 public void actionPerformed(ActionEvent actionEvent) {
423 // INTERFACE MouseListener
429 public void mouseClicked(MouseEvent mouseEvent) {
430 maybeShowContextMenu(mouseEvent);
436 public void mouseEntered(MouseEvent mouseEvent) {
443 public void mouseExited(MouseEvent mouseEvent) {
450 public void mousePressed(MouseEvent mouseEvent) {
451 maybeShowContextMenu(mouseEvent);
457 public void mouseReleased(MouseEvent mouseEvent) {
458 maybeShowContextMenu(mouseEvent);
462 * Tree cell renderer that takes care of certain display properties for
463 * project-specific stuff.
465 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
467 private class FileCellRenderer extends DefaultTreeCellRenderer {
477 * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(javax.swing.JTree,
478 * java.lang.Object, boolean, boolean, boolean, int, boolean)
480 @SuppressWarnings("synthetic-access")
482 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
483 Component superCellRenderer = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
484 if (!(superCellRenderer instanceof JLabel)) {
485 logger.log(Level.SEVERE, "superCellRenderer is not a JLabel!");
486 return superCellRenderer;
488 if (!(value instanceof ProjectFileWrapper)) {
489 logger.log(Level.SEVERE, "value is not a ProjectFileWrapper!");
490 return superCellRenderer;
492 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) value;
493 ProjectFile projectFile = projectFileWrapper.getProjectFile();
494 String completePath = projectFile.getCompletePath();
495 if (projectFile.isHidden()) {
496 /* TODO - check override */
497 Color foreground = superCellRenderer.getForeground();
498 Color background = selected ? getBackgroundSelectionColor() : getBackgroundNonSelectionColor();
499 Color averageColor = new Color((foreground.getRed() + background.getRed()) / 2, (foreground.getGreen() + background.getGreen()) / 2, (foreground.getBlue() + background.getBlue()) / 2);
500 superCellRenderer.setForeground(averageColor);
501 } else if (completePath.equals(project.getDefaultFile())) {
502 superCellRenderer.setFont(superCellRenderer.getFont().deriveFont(Font.BOLD));
503 } else if (projectFile.getParents().size() == 1) {
504 superCellRenderer.setFont(superCellRenderer.getFont().deriveFont(Font.BOLD));
506 superCellRenderer.setFont(superCellRenderer.getFont().deriveFont(Font.PLAIN));
508 return superCellRenderer;
514 * TreeModel that is based on {@link Project#getBaseFile()}.
516 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
518 private class ProjectFileTreeModel implements TreeModel {
520 /** Tree model listeners. */
521 private final List<TreeModelListener> treeModelListeners = Collections.synchronizedList(new ArrayList<TreeModelListener>());
523 /** The base project file. */
524 private ProjectFile baseProjectFile;
526 /** Maps project files to wrappers. */
527 private final Map<ProjectFile, ProjectFileWrapper> projectFileWrappers = Collections.synchronizedMap(new HashMap<ProjectFile, ProjectFileWrapper>());
532 ProjectFileTreeModel() {
543 public void addTreeModelListener(TreeModelListener treeModelListener) {
544 treeModelListeners.add(treeModelListener);
550 public void removeTreeModelListener(TreeModelListener treeModelListener) {
551 treeModelListeners.remove(treeModelListener);
555 * Notifies all listeners that the tree structure has changed
558 * @see TreeModelListener#treeStructureChanged(TreeModelEvent)
561 protected void fireTreeStructureChanged(ProjectFileWrapper newRootNode) {
562 for (TreeModelListener treeModelListener: treeModelListeners) {
563 treeModelListener.treeStructureChanged(new TreeModelEvent(this, new Object[] { newRootNode }));
572 * Sets the new base project file. This causes the model to reload.
574 * @param baseProjectFile
575 * The new base project file
577 @SuppressWarnings("synthetic-access")
578 public synchronized void setBaseProjectFile(ProjectFile baseProjectFile) {
579 this.baseProjectFile = baseProjectFile;
580 projectFileWrappers.clear();
581 createWrappers(baseProjectFile);
582 projectFileWrappers.get(baseProjectFile).setNameOverride(project.getName());
583 fireTreeStructureChanged(projectFileWrappers.get(baseProjectFile));
591 * Creates {@link ProjectFileWrapper}s for all files below the given
595 * The base project file for all project files to create
598 private void createWrappers(ProjectFile projectFile) {
599 projectFileWrappers.put(projectFile, new ProjectFileWrapper(projectFile));
600 for (ProjectFile projectFileChild: projectFile.getFiles()) {
601 if (projectFileChild.isDirectory()) {
602 createWrappers(projectFileChild);
604 projectFileWrappers.put(projectFileChild, new ProjectFileWrapper(projectFileChild));
609 // INTERFACE TreeModel
615 public Object getRoot() {
616 return projectFileWrappers.get(baseProjectFile);
622 @SuppressWarnings("synthetic-access")
623 public Object getChild(Object parent, int index) {
624 if (!(parent instanceof ProjectFileWrapper)) {
625 logger.log(Level.SEVERE, "parent is not a ProjectFileWrapper!");
628 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) parent;
629 ProjectFile projectFile = projectFileWrapper.getProjectFile();
630 return projectFileWrappers.get(projectFile.getFiles().get(index));
636 @SuppressWarnings("synthetic-access")
637 public int getChildCount(Object parent) {
638 if (!(parent instanceof ProjectFileWrapper)) {
639 logger.log(Level.SEVERE, "parent is not a ProjectFileWrapper!");
642 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) parent;
643 ProjectFile projectFile = projectFileWrapper.getProjectFile();
644 return projectFile.getFiles().size();
650 @SuppressWarnings("synthetic-access")
651 public int getIndexOfChild(Object parent, Object child) {
652 if (!(parent instanceof ProjectFileWrapper)) {
653 logger.log(Level.SEVERE, "parent is not a ProjectFileWrapper!");
656 if (!(child instanceof ProjectFileWrapper)) {
657 logger.log(Level.SEVERE, "child is not a ProjectFileWrapper!");
660 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) parent;
661 ProjectFile projectFile = projectFileWrapper.getProjectFile();
662 return projectFile.getFiles().indexOf(((ProjectFileWrapper) child).getProjectFile());
668 @SuppressWarnings("synthetic-access")
669 public boolean isLeaf(Object node) {
670 if (!(node instanceof ProjectFileWrapper)) {
671 logger.log(Level.SEVERE, "node is not a ProjectFileWrapper!");
674 ProjectFileWrapper projectFileWrapper = (ProjectFileWrapper) node;
675 ProjectFile projectFile = projectFileWrapper.getProjectFile();
676 return projectFile.getFiles().isEmpty();
682 public void valueForPathChanged(TreePath path, Object newValue) {
683 /* ignore, items will not be modified in tree. */
689 * Wrapper around a {@link ProjectFile} that overwrites
690 * {@link Object#toString()} to return the project file’s name.
692 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
694 private static class ProjectFileWrapper {
696 /** The wrapped project file. */
697 private final ProjectFile projectFile;
699 /** The override name. */
700 private String nameOverride;
703 * Creates a new wrapper around a project file.
706 * The project file to wrap
708 public ProjectFileWrapper(ProjectFile projectFile) {
709 this.projectFile = projectFile;
713 * Returns the wrapped project file.
715 * @return The wrapped project file
717 public ProjectFile getProjectFile() {
722 * Sets the name override. If the name override is not <code>null</code>
723 * it will be shown insted of the project file’s name.
725 * @param nameOverride
728 void setNameOverride(String nameOverride) {
729 this.nameOverride = nameOverride;
736 public String toString() {
737 return (nameOverride != null) ? nameOverride : projectFile.getName();