/**
* 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 Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file.icon.impl;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.icon.CacheableFileIconProvider;
import com.mucommander.commons.file.icon.CachedFileIconProvider;
import com.mucommander.commons.file.icon.IconCache;
import com.mucommander.commons.file.icon.LocalFileIconProvider;
import com.mucommander.commons.file.protocol.FileProtocols;
import com.mucommander.commons.file.protocol.local.LocalFile;
import com.mucommander.commons.file.util.ResourceLoader;
import com.mucommander.commons.io.SilenceableOutputStream;
import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.commons.runtime.OsVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.filechooser.FileSystemView;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.PrintStream;
import java.net.URL;
/**
* Package-protected class which provides the {@link com.mucommander.commons.file.icon.LocalFileIconProvider} and
* {@link com.mucommander.commons.file.icon.CacheableFileIconProvider} implementations to {@link SwingFileIconProvider}.
*
* @see SwingFileIconProvider
* @author Maxence Bernard
*/
class SwingFileIconProviderImpl extends LocalFileIconProvider implements CacheableFileIconProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(SwingFileIconProviderImpl.class);
/** Swing object used to retrieve file icons, used on all platforms but Mac OS X */
private static FileSystemView fileSystemView;
/** Swing object used to retrieve file icons, used under Mac OS X only */
private static JFileChooser fileChooser;
/** Caches icons for directories, used only for non-local files */
protected static IconCache directoryIconCache = CachedFileIconProvider.createCache();
/** Caches icons for regular files, used only for non-local files */
protected static IconCache fileIconCache = CachedFileIconProvider.createCache();
/** True if init has been called */
protected static boolean initialized;
/** Name of the 'symlink' icon resource located in the same package as this class */
private final static String SYMLINK_ICON_NAME = "link.png";
/** Icon that is painted over a symlink's target file icon to symbolize a symlink to the target file. */
protected static ImageIcon SYMLINK_OVERLAY_ICON;
/** Allows stderr to be 'silenced' when needed */
protected static SilenceableOutputStream errOut;
/**
* Initializes the Swing object used to retrieve icons the first time this method is called, does nothing
* subsequent calls.
* Note: instanciating this object is expensive (I/O bound) so we want to do that only if needed, and only once.
*/
synchronized static void checkInit() {
// This method is synchronized to ensure that the initialization happens only once
if(initialized)
return;
if(OsFamily.MAC_OS_X.isCurrent())
fileChooser = new JFileChooser();
else
fileSystemView = FileSystemView.getFileSystemView();
// Loads the symlink overlay icon
URL iconURL = ResourceLoader.getPackageResourceAsURL(SwingFileIconProviderImpl.class.getPackage(), SYMLINK_ICON_NAME);
if(iconURL==null)
throw new RuntimeException("Could not locate required symlink icon: "+SYMLINK_ICON_NAME);
SYMLINK_OVERLAY_ICON = new ImageIcon(iconURL);
// Replace stderr with a SilenceablePrintStream that can be 'silenced' when needed
System.setErr(new PrintStream(errOut = new SilenceableOutputStream(System.err, false), true));
initialized = true;
}
/**
* Returns an icon for the given <code>java.io.File</code> using the underlying Swing provider component,
* <code>null</code> in case of an error.
*
* @param javaIoFile the file for which to return an icon
* @return an icon for the specified file, null in case of an unexpected error
*/
private static Icon getSwingIcon(java.io.File javaIoFile) {
try {
if(fileSystemView!=null) {
// FileSystemView.getSystemIcon() will behave in the following way if the specified file doesn't exist
// when the icon is requested:
// - throw a NullPointerException (caused by a java.io.FileNotFoundException) => OK why not
// - dump the stack trace to System.err => bad! bad! bad!
//
// A way to workaround this odd behavior would be to test if the file exists when it is requested,
// but a/ this is an expensive operation (especially under Windows) and b/ it wouldn't guarantee that
// the file effectively exists when the icon is requested.
// So the workaround here is to catch exceptions and 'silence' System.err output during the call.
errOut.setSilenced(true);
return fileSystemView.getSystemIcon(javaIoFile);
}
else {
return fileChooser.getIcon(javaIoFile);
}
}
catch(Exception e) {
LOGGER.info("Caught exception while retrieving system icon for file {}", javaIoFile.getAbsolutePath(), e);
return null;
}
finally {
if(fileSystemView!=null)
errOut.setSilenced(false);
}
}
/**
* Returns an icon symbolizing a symlink to the given target icon. The returned icon uses the specified icon as
* its background and overlays a 'link' icon on top of it.
*
* @param targetFileIcon the icon representing the symlink's target
* @return an icon symbolizing a symlink to the given target
*/
private static ImageIcon getSymlinkIcon(Icon targetFileIcon) {
BufferedImage bi = new BufferedImage(targetFileIcon.getIconWidth(), targetFileIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = bi.getGraphics();
targetFileIcon.paintIcon(null, g, 0, 0);
SYMLINK_OVERLAY_ICON.paintIcon(null, g, 0, 0);
return new ImageIcon(bi);
}
/**
* Returns the extension of the given file using {@link AbstractFile#getExtension()}. If the extension is
* <code>null</code>, the empty string <code>""</code> is returned, making the returned extension safe for use
* in a hash map where null keys are forbidden.
*
* @param file file on which to call {@link AbstractFile#getExtension}
* @return the file's extension, may be the empty string but never <code>null</code>
*/
private static String getCheckedExtension(AbstractFile file) {
String extension = file.getExtension();
return extension==null?"":extension;
}
//////////////////////////////////////////
// LocalFileIconProvider implementation //
//////////////////////////////////////////
/**
* <b>Implementation notes:</b> returns <code>false</code> (no caching) for:
* <ul>
* <li>local files: their icons are cached by the Swing component that provides icons.</li>
* <li>symlinks: their icon cannot be cached using the file's extension as a key.</li>
* </ul>
* <code>true</code> is returned for non-local files that are not symlinks to avoid excessive temporary file
* creation.
*/
public boolean isCacheable(AbstractFile file, Dimension preferredResolution) {
return !((file.getTopAncestor() instanceof LocalFile) || file.isSymlink());
}
public Icon lookupCache(AbstractFile file, Dimension preferredResolution) {
// Under Mac OS X, return the icon of /Network for the root of remote (non-local) locations.
if(OsFamily.MAC_OS_X.isCurrent() && !FileProtocols.FILE.equals(file.getURL().getScheme()) && file.isRoot())
return getSwingIcon(new java.io.File("/Network"));
// Look for an existing icon instance for the file's extension
return (file.isDirectory()? directoryIconCache : fileIconCache).get(getCheckedExtension(file));
}
public void addToCache(AbstractFile file, Icon icon, Dimension preferredResolution) {
// Map the extension onto the given icon
(file.isDirectory()? directoryIconCache : fileIconCache).put(getCheckedExtension(file), icon);
}
/**
* <i>Implementation note:</i> only one resolution is available (usually 16x16) and blindly returned, the
* <code>preferredResolution</code> argument is simply ignored.
*/
@Override
public Icon getLocalFileIcon(LocalFile localFile, AbstractFile originalFile, Dimension preferredResolution) {
// Initialize the Swing object the first time this method is called
checkInit();
// Retrieve the icon using the Swing provider component
Icon icon = getSwingIcon((java.io.File)localFile.getUnderlyingFileObject());
// Add a symlink indication to the icon if:
// - the original file is a symlink AND
// - the original file is not a local file OR
// - the original file is a local file but the Swing component generates icons which do not have a symlink
// indication. That is the case on Mac OS X 10.5 (regression, 10.4 did this just fine).
//
// Note that the symlink test is performed last because it is the most expensive.
//
if((!(originalFile.getTopAncestor() instanceof LocalFile) || (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrent()))
&& originalFile.isSymlink()) {
icon = getSymlinkIcon(icon);
}
return icon;
}
}