/* * 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; import java.awt.Dimension; import java.awt.Insets; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Hashtable; import java.util.Map; import java.util.regex.PatternSyntaxException; import javax.swing.AbstractAction; import javax.swing.Icon; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.SwingUtilities; import javax.swing.filechooser.FileSystemView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.bonjour.BonjourMenu; import com.mucommander.bonjour.BonjourService; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkListener; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.bookmark.file.BookmarkProtocolProvider; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.filter.PathFilter; import com.mucommander.commons.file.filter.RegexpPathFilter; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.commons.file.protocol.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.MuConfigurations; import com.mucommander.conf.MuPreference; import com.mucommander.conf.MuPreferences; import com.mucommander.text.Translator; import com.mucommander.ui.action.MuAction; import com.mucommander.ui.action.impl.OpenLocationAction; import com.mucommander.ui.button.PopupButton; import com.mucommander.ui.dialog.server.FTPPanel; import com.mucommander.ui.dialog.server.HTTPPanel; import com.mucommander.ui.dialog.server.NFSPanel; import com.mucommander.ui.dialog.server.SFTPPanel; import com.mucommander.ui.dialog.server.SMBPanel; import com.mucommander.ui.dialog.server.ServerConnectDialog; import com.mucommander.ui.dialog.server.ServerPanel; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; /** * <code>DrivePopupButton</code> is a button which, when clicked, pops up a menu with a list of volumes items that be used * to change the current folder. * * @author Maxence Bernard */ public class DrivePopupButton extends PopupButton implements BookmarkListener, ConfigurationListener, LocationListener { private static final Logger LOGGER = LoggerFactory.getLogger(DrivePopupButton.class); /** FolderPanel instance that contains this button */ private FolderPanel folderPanel; /** Current volumes */ private static AbstractFile volumes[]; /** static FileSystemView instance, has a (non-null) value only under Windows */ private static FileSystemView fileSystemView; /** Caches extended drive names, has a (non-null) value only under Windows */ private static Map<AbstractFile, String> extendedNameCache; /** Caches drive icons */ private static Map<AbstractFile, Icon> iconCache = new Hashtable<AbstractFile, Icon>(); /** Filters out volumes from the list based on the exclude regexp defined in the configuration, null if the regexp * is not defined. */ private static PathFilter volumeFilter; static { if(OsFamily.WINDOWS.isCurrent()) { fileSystemView = FileSystemView.getFileSystemView(); extendedNameCache = new Hashtable<AbstractFile, String>(); } try { String excludeRegexp = MuConfigurations.getPreferences().getVariable(MuPreference.VOLUME_EXCLUDE_REGEXP); if(excludeRegexp!=null) { volumeFilter = new RegexpPathFilter(excludeRegexp, true); volumeFilter.setInverted(true); } } catch(PatternSyntaxException e) { LOGGER.info("Invalid regexp for conf variable "+MuPreferences.VOLUME_EXCLUDE_REGEXP, e); } // Initialize the volumes list volumes = getDisplayableVolumes(); } /** * Creates a new <code>DrivePopupButton</code> which is to be added to the given FolderPanel. * * @param folderPanel the FolderPanel instance this button will be added to */ public DrivePopupButton(FolderPanel folderPanel) { this.folderPanel = folderPanel; // Listen to location events to update the button when the current folder changes folderPanel.getLocationManager().addLocationListener(this); // Listen to bookmark changes to update the button if a bookmark corresponding to the current folder // has been added/edited/removed BookmarkManager.addBookmarkListener(this); // Listen to configuration changes to update the button if the system file icons policy has changed MuConfigurations.addPreferencesListener(this); // Use new JButton decorations introduced in Mac OS X 10.5 (Leopard) if(OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { setMargin(new Insets(1,1,1,1)); putClientProperty("JComponent.sizeVariant", "small"); putClientProperty("JButton.buttonType", "textured"); } } /** * Updates the button's label and icon to reflect the current folder and match one of the current volumes: * <<ul> * <li>If the specified folder corresponds to a bookmark, the bookmark's name will be displayed * <li>If the specified folder corresponds to a local file, the enclosing volume's name will be displayed * <li>If the specified folder corresponds to a remote file, the protocol's name will be displayed * </ul> * The button's icon will be the current folder's one. */ private void updateButton() { AbstractFile currentFolder = folderPanel.getCurrentFolder(); String currentPath = currentFolder.getAbsolutePath(); FileURL currentURL = currentFolder.getURL(); // First try to find a bookmark matching the specified folder for(Bookmark bookmark : BookmarkManager.getBookmarks()) { if(currentPath.equals(bookmark.getLocation())) { // Note: if several bookmarks match current folder, the first one will be used setText(bookmark.getName()); setIcon(IconManager.getIcon(IconManager.FILE_ICON_SET, CustomFileIconProvider.BOOKMARK_ICON_NAME)); return; } } // If no bookmark matched current folder String protocol = currentURL.getScheme(); switch (protocol) { // Local file, use volume's name case FileProtocols.FILE: String newLabel = null; // Patch for Windows UNC network paths (weakly characterized by having a host different from 'localhost'): // display 'SMB' which is the underlying protocol if(OsFamily.WINDOWS.isCurrent() && !FileURL.LOCALHOST.equals(currentURL.getHost())) { newLabel = "SMB"; } else { // getCanonicalPath() must be avoided under Windows for the following reasons: // a) it is not necessary, Windows doesn't have symlinks // b) it triggers the dreaded 'No disk in drive' error popup dialog. // c) when network drives are present but not mounted (e.g. X:\ mapped onto an SMB share), // getCanonicalPath which is I/O bound will take a looooong time to execute if(OsFamily.WINDOWS.isCurrent()) currentPath = currentFolder.getAbsolutePath(false).toLowerCase(); else currentPath = currentFolder.getCanonicalPath(false).toLowerCase(); int bestLength = -1; int bestIndex = 0; String temp; int len; for(int i=0; i< volumes.length; i++) { if(OsFamily.WINDOWS.isCurrent()) temp = volumes[i].getAbsolutePath(false).toLowerCase(); else temp = volumes[i].getCanonicalPath(false).toLowerCase(); len = temp.length(); if (currentPath.startsWith(temp) && len>bestLength) { bestIndex = i; bestLength = len; } } newLabel = volumes[bestIndex].getName(); // Not used because the call to FileSystemView is slow // if(fileSystemView!=null) // newToolTip = getWindowsExtendedDriveName(volumes[bestIndex]); } setText(newLabel); // Set the folder icon based on the current system icons policy setIcon(FileIcons.getFileIcon(currentFolder)); break; case BookmarkProtocolProvider.BOOKMARK: String currentFolderName = currentFolder.getName(); setText(currentFolderName.isEmpty() ? Translator.get("bookmarks_menu") : currentFolderName); setIcon(IconManager.getIcon(IconManager.FILE_ICON_SET, CustomFileIconProvider.BOOKMARK_ICON_NAME)); break; default: // Remote file, use the protocol's name setText(protocol.toUpperCase()); // Set the folder icon based on the current system icons policy setIcon(FileIcons.getFileIcon(currentFolder)); } } /** * Returns the extended name of the given local file, e.g. "Local Disk (C:)" for C:\. The returned value is * interesting only under Windows. This method is I/O bound and very slow so it should not be called from the main * event thread. * * @param localFile the file for which to return the extended name * @return the extended name of the given local file */ private static String getExtendedDriveName(AbstractFile localFile) { // Note: fileSystemView.getSystemDisplayName(java.io.File) is unfortunately very very slow String name = fileSystemView.getSystemDisplayName((java.io.File)localFile.getUnderlyingFileObject()); if(name==null || name.equals("")) // This happens for CD/DVD drives when they don't contain any disc return localFile.getName(); return name; } /** * Returns the list of volumes to be displayed in the popup menu. * * <p>The raw list of volumes is fetched using {@link LocalFile#getVolumes()} and then * filtered using the regexp defined in the {@link MuPreferences#VOLUME_EXCLUDE_REGEXP} configuration variable * (if defined).</p> * * @return the list of volumes to be displayed in the popup menu */ public static AbstractFile[] getDisplayableVolumes() { AbstractFile[] volumes = LocalFile.getVolumes(); if(volumeFilter!=null) return volumeFilter.filter(volumes); return volumes; } //////////////////////////////// // PopupButton implementation // //////////////////////////////// @Override public JPopupMenu getPopupMenu() { JPopupMenu popupMenu = new JPopupMenu(); // Update the list of volumes in case new ones were mounted volumes = getDisplayableVolumes(); // Add volumes int nbVolumes = volumes.length; final MainFrame mainFrame = folderPanel.getMainFrame(); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // Provides mnemonics and ensures uniqueness JMenuItem item; MuAction action; String volumeName; boolean useExtendedDriveNames = fileSystemView!=null; ArrayList<JMenuItem> itemsV = new ArrayList<JMenuItem>(); for(int i=0; i<nbVolumes; i++) { action = new CustomOpenLocationAction(mainFrame, new Hashtable<String, Object>(), volumes[i]); volumeName = volumes[i].getName(); // If several volumes have the same filename, use the volume's path for the action's label instead of the // volume's path, to disambiguate for(int j=0; j<nbVolumes; j++) { if(j!=i && volumes[j].getName().equalsIgnoreCase(volumeName)) { action.setLabel(volumes[i].getAbsolutePath()); break; } } item = popupMenu.add(action); setMnemonic(item, mnemonicHelper); // Set icon from cache Icon icon = iconCache.get(volumes[i]); if (icon!=null) { item.setIcon(icon); } if(useExtendedDriveNames) { // Use the last known value (if any) while we update it in a separate thread String previousExtendedName = extendedNameCache.get(volumes[i]); if(previousExtendedName!=null) item.setText(previousExtendedName); } itemsV.add(item); // JMenu offers no way to retrieve a particular JMenuItem, so we have to keep them } new RefreshDriveNamesAndIcons(popupMenu, itemsV).start(); popupMenu.add(new JSeparator()); // Add boookmarks java.util.List<Bookmark> bookmarks = BookmarkManager.getBookmarks(); if (!bookmarks.isEmpty()) { for(Bookmark bookmark : bookmarks) { item = popupMenu.add(new CustomOpenLocationAction(mainFrame, new Hashtable<String, Object>(), bookmark)); setMnemonic(item, mnemonicHelper); } } else { // No bookmark : add a disabled menu item saying there is no bookmark popupMenu.add(Translator.get("bookmarks_menu.no_bookmark")).setEnabled(false); } popupMenu.add(new JSeparator()); // Add 'Network shares' shortcut if(FileFactory.isRegisteredProtocol(FileProtocols.SMB)) { action = new CustomOpenLocationAction(mainFrame, new Hashtable<String, Object>(), new Bookmark(Translator.get("drive_popup.network_shares"), "smb:///")); action.setIcon(IconManager.getIcon(IconManager.FILE_ICON_SET, CustomFileIconProvider.NETWORK_ICON_NAME)); setMnemonic(popupMenu.add(action), mnemonicHelper); } // Add Bonjour services menu setMnemonic(popupMenu.add(new BonjourMenu() { @Override public MuAction getMenuItemAction(BonjourService bs) { return new CustomOpenLocationAction(mainFrame, new Hashtable<String, Object>(), bs); } }) , mnemonicHelper); popupMenu.add(new JSeparator()); // Add 'connect to server' shortcuts setMnemonic(popupMenu.add(new ServerConnectAction("SMB...", SMBPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("FTP...", FTPPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("SFTP...", SFTPPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("HTTP...", HTTPPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("NFS...", NFSPanel.class)), mnemonicHelper); return popupMenu; } /** * Calls to getExtendedDriveName(String) are very slow, so they are performed in a separate thread so as * to not lock the main even thread. The popup menu gets first displayed with the short drive names, and * then refreshed with the extended names as they are retrieved. */ private class RefreshDriveNamesAndIcons extends Thread { private JPopupMenu popupMenu; private ArrayList<JMenuItem> items; public RefreshDriveNamesAndIcons(JPopupMenu popupMenu, ArrayList<JMenuItem> items) { super("RefreshDriveNamesAndIcons"); this.popupMenu = popupMenu; this.items = items; } @Override public void run() { final boolean useExtendedDriveNames = fileSystemView!=null; for(int i=0; i<items.size(); i++) { final JMenuItem item = items.get(i); String extendedName = null; if (useExtendedDriveNames) { // Under Windows, show the extended drive name (e.g. "Local Disk (C:)" instead of just "C:") but use // the simple drive name for the mnemonic (i.e. 'C' instead of 'L'). extendedName = getExtendedDriveName(volumes[i]); // Keep the extended name for later (see above) extendedNameCache.put(volumes[i], extendedName); } final String extendedNameFinal = extendedName; // Set system icon for volumes, only if system icons are available on the current platform final Icon icon = FileIcons.hasProperSystemIcons()?FileIcons.getSystemFileIcon(volumes[i]):null; if (icon!=null) { iconCache.put(volumes[i], icon); } SwingUtilities.invokeLater(new Runnable() { public void run() { if (useExtendedDriveNames) { item.setText(extendedNameFinal); } if (icon!=null) { item.setIcon(icon); } } }); } // Re-calculate the popup menu's dimensions SwingUtilities.invokeLater(new Runnable() { public void run() { popupMenu.invalidate(); popupMenu.pack(); } }); } } /** * Convenience method that sets a mnemonic to the given JMenuItem, using the specified MnemonicHelper. * * @param menuItem the menu item for which to set a mnemonic * @param mnemonicHelper the MnemonicHelper instance to be used to determine the mnemonic's character. */ private void setMnemonic(JMenuItem menuItem, MnemonicHelper mnemonicHelper) { menuItem.setMnemonic(mnemonicHelper.getMnemonic(menuItem.getText())); } ////////////////////////////// // BookmarkListener methods // ////////////////////////////// public void bookmarksChanged() { // Refresh label in case a bookmark with the current location was changed updateButton(); } /////////////////////////////////// // ConfigurationListener methods // /////////////////////////////////// /** * Listens to certain configuration variables. */ public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); // Update the button's icon if the system file icons policy has changed if (var.equals(MuPreferences.USE_SYSTEM_FILE_ICONS)) updateButton(); } //////////////////////// // Overridden methods // //////////////////////// @Override public Dimension getPreferredSize() { // Limit button's maximum width to something reasonable and leave enough space for location field, // as bookmarks name can be as long as users want them to be. // Note: would be better to use JButton.setMaximumSize() but it doesn't seem to work Dimension d = super.getPreferredSize(); if(d.width > 160) d.width = 160; return d; } /////////////////// // Inner classes // /////////////////// /** * This action pops up {@link com.mucommander.ui.dialog.server.ServerConnectDialog} for a specified * protocol. */ private class ServerConnectAction extends AbstractAction { private Class<? extends ServerPanel> serverPanelClass; private ServerConnectAction(String label, Class<? extends ServerPanel> serverPanelClass) { super(label); this.serverPanelClass = serverPanelClass; } public void actionPerformed(ActionEvent actionEvent) { new ServerConnectDialog(folderPanel, serverPanelClass).showDialog(); } } /** * This modified {@link OpenLocationAction} changes the current folder on the {@link FolderPanel} that contains * this button, instead of the currently active {@link FolderPanel}. */ private class CustomOpenLocationAction extends OpenLocationAction { public CustomOpenLocationAction(MainFrame mainFrame, Map<String,Object> properties, Bookmark bookmark) { super(mainFrame, properties, bookmark); } public CustomOpenLocationAction(MainFrame mainFrame, Map<String,Object> properties, AbstractFile file) { super(mainFrame, properties, file); } public CustomOpenLocationAction(MainFrame mainFrame, Map<String,Object> properties, BonjourService bs) { super(mainFrame, properties, bs); } //////////////////////// // Overridden methods // //////////////////////// @Override protected FolderPanel getFolderPanel() { return folderPanel; } } /********************************** * LocationListener Implementation **********************************/ public void locationChanged(LocationEvent e) { // Update the button's label to reflect the new current folder updateButton(); } public void locationChanging(LocationEvent locationEvent) { } public void locationCancelled(LocationEvent locationEvent) { } public void locationFailed(LocationEvent locationEvent) {} }