/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.main.tree; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.util.FileComparator; import com.mucommander.conf.MuConfigurations; import com.mucommander.conf.MuPreferences; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.RefreshAction; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.main.ConfigurableFolderFilter; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.ui.theme.ThemeListener; /** * A panel which contains a directory tree. This panel is attached to the left * side of the files table. It allows for a quick navigation in a directory * tree. Selecting folder on the tree changes folder in files folder. * * @author Mariusz Jakubowski * */ public class FoldersTreePanel extends JPanel implements TreeSelectionListener, LocationListener, FocusListener, ThemeListener, TreeModelListener, ConfigurationListener { private static final Logger LOGGER = LoggerFactory.getLogger(FoldersTreePanel.class); /** Directory tree */ private JTree tree; /** Folder panel to which this tree is attached */ private FolderPanel folderPanel; /** A model with a directory tree */ private FilesTreeModel model; /** A timer that fires a directory change */ private ChangeTimer changeTimer = new ChangeTimer(); static { TreeIOThreadManager.getInstance().start(); } /** * Creates a panel with directory tree attached to a specified folder panel. * @param folderPanel a folder panel to attach tree */ public FoldersTreePanel(FolderPanel folderPanel) { super(); this.folderPanel = folderPanel; setLayout(new BorderLayout()); // Filters out the files that should not be displayed in the tree view AndFileFilter treeFileFilter = new AndFileFilter( new AttributeFileFilter(FileAttribute.DIRECTORY), new ConfigurableFolderFilter() ); FileComparator sort = new FileComparator(FileComparator.NAME_CRITERION, true, true); model = new FilesTreeModel(treeFileFilter, sort); tree = new JTree(model); tree.setFont(ThemeCache.tableFont); tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]); tree.getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setExpandsSelectedPaths(true); tree.getModel().addTreeModelListener(this); JScrollPane sp = new JScrollPane(tree); // JScrollPane usually comes with a tiny border, remove it sp.setBorder(null); add(sp, BorderLayout.CENTER); // Create tree renderer. We're not using default tree renderer, because // AbstractFile.toString method returns full path, and we want to // display only a file name. FoldersTreeRenderer renderer = new FoldersTreeRenderer(tree); tree.setCellRenderer(renderer); tree.addTreeSelectionListener(this); tree.addFocusListener(this); // add a popup menu final JPopupMenu popup = new JPopupMenu(); // refresh action JMenuItem item = new JMenuItem( ActionProperties.getActionLabel(RefreshAction.Descriptor.ACTION_ID), KeyEvent.VK_R); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { model.refresh(tree.getSelectionPath()); // model.fireTreeStructureChanged(tree, tree.getSelectionPath()); } }); popup.add(item); tree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { popup.show(e.getComponent(), e.getX(), e.getY()); } } }); ThemeCache.addThemeListener(this); MuConfigurations.addPreferencesListener(this); } /** * Listens to certain configuration variables. */ public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); if (var.equals(MuPreferences.SHOW_HIDDEN_FILES) || var.equals(MuPreferences.SHOW_DS_STORE_FILES) || var.equals(MuPreferences.SHOW_SYSTEM_FOLDERS)) { Object root = model.getRoot(); if (root != null) { TreePath path = new TreePath(root); model.refresh(path); } } } /** * Adds or removes location change listeners depending on the tree * visibility. */ @Override public void setVisible(boolean flag) { super.setVisible(flag); if (flag) { updateSelectedFolder(); folderPanel.getLocationManager().addLocationListener(this); // tree.requestFocus(); } else { folderPanel.getLocationManager().removeLocationListener(this); } } /** * Updates selection in a tree to the current folder. When necessary updates * the current root of a tree. Invoked when location on folder pane has changed or * when a tree has been updated (when directories have been loaded). */ private void updateSelectedFolder() { final AbstractFile currentFolder = folderPanel.getCurrentFolder(); // get selected directory (ignore archives - TODO make archives browsable (option)) AbstractFile tempFolder = currentFolder; AbstractFile tempParent; while (!tempFolder.isDirectory()) { tempParent = tempFolder.getParent(); if(tempParent==null) break; tempFolder = tempParent; } // compare selection on tree and panel final AbstractFile selectedFolder = tempFolder; TreePath selectionPath = tree.getSelectionPath(); if (selectionPath != null) { if (selectionPath.getLastPathComponent() == currentFolder) return; } // check if root has changed final AbstractFile currentRoot = selectedFolder.getRoot(); if (!currentRoot.equals(model.getRoot())) { model.setRoot(currentRoot); } // refresh selection on tree SwingUtilities.invokeLater(new Runnable() { public void run() { try { TreePath path = new TreePath(model.getPathToRoot(selectedFolder)); tree.expandPath(path); tree.setSelectionPath(path); tree.scrollPathToVisible(path); } catch (Exception e) { LOGGER.debug("Caught exception", e); } } }); } /** * Refreshes folder after a change (e.g. mkdir). * @param folder a folder to refresh on the tree */ public void refreshFolder(AbstractFile folder) { if (!isVisible()) return; model.fireTreeStructureChanged(tree, new TreePath(model.getPathToRoot(folder))); } /** * Changes focus to tree. */ @Override public void requestFocus() { tree.requestFocus(); } /** * Returns tree component. * @return tree component */ public JTree getTree() { return tree; } // - TreeSelectionListener code -------------------------------------------- // ------------------------------------------------------------------------- /** * This class is used to change folder after a user selects a folder in * tree. This change occurs after small delay (1 sec) to allow a user to * navigate a tree using keyboard. * * @author Mariusz Jakubowski * */ private class ChangeTimer extends Timer { private transient AbstractFile folder; public ChangeTimer() { super(1000, null); setRepeats(false); } @Override public void fireActionPerformed(ActionEvent ae) { if (!folderPanel.getCurrentFolder().equals(folder)) { folderPanel.tryChangeCurrentFolder(folder); } } } /** * Changes the current folder in an associated folder panel, depending on * the current selection in tree. */ public void valueChanged(TreeSelectionEvent e) { TreePath path = e.getNewLeadSelectionPath(); if (path != null) { AbstractFile f = (AbstractFile) path.getLastPathComponent(); if (f != null && f.isBrowsable() && f != folderPanel.getCurrentFolder()) { changeTimer.folder = f; changeTimer.restart(); } } } // - LocationListener code ------------------------------------------------- // ------------------------------------------------------------------------- public void locationCancelled(LocationEvent locationEvent) { } public void locationChanged(LocationEvent locationEvent) { updateSelectedFolder(); } public void locationChanging(LocationEvent locationEvent) { } public void locationFailed(LocationEvent locationEvent) { } // - FocusListener code ---------------------------------------------------- // ------------------------------------------------------------------------- public void focusGained(FocusEvent e) { tree.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]); } public void focusLost(FocusEvent e) { tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]); } // - ThemeListener code ---------------------------------------------------- // ------------------------------------------------------------------------- public void colorChanged(ColorChangedEvent event) { if (tree.hasFocus()) { tree.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]); } else { tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]); } tree.repaint(); } public void fontChanged(FontChangedEvent event) { tree.setFont(ThemeCache.tableFont); tree.repaint(); } // - TreeModelListener code ------------------------------------------------ // ------------------------------------------------------------------------- public void treeNodesChanged(TreeModelEvent e) { } public void treeNodesInserted(TreeModelEvent e) { } public void treeNodesRemoved(TreeModelEvent e) { } public void treeStructureChanged(TreeModelEvent e) { // ensures that a selection is repainted correctly // after nodes have been inserted if (!changeTimer.isRunning()) { updateSelectedFolder(); tree.repaint(); } } }