/*
* 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.desktop;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.runtime.JavaVersion;
import com.mucommander.desktop.gnome.ConfiguredGnomeDesktopAdapter;
import com.mucommander.desktop.gnome.GuessedGnomeDesktopAdapter;
import com.mucommander.desktop.kde.ConfiguredKde3DesktopAdapter;
import com.mucommander.desktop.kde.ConfiguredKde4DesktopAdapter;
import com.mucommander.desktop.kde.GuessedKde3DesktopAdapter;
import com.mucommander.desktop.kde.GuessedKde4DesktopAdapter;
import com.mucommander.desktop.openvms.OpenVMSDesktopAdapter;
import com.mucommander.desktop.osx.OSXDesktopAdapter;
import com.mucommander.desktop.windows.Win9xDesktopAdapter;
import com.mucommander.desktop.windows.WinNtDesktopAdapter;
import com.mucommander.desktop.xfce.GuessedXfceDesktopAdapter;
/**
* @author Nicolas Rinaudo
*/
public class DesktopManager {
private static final Logger LOGGER = LoggerFactory.getLogger(DesktopManager.class);
// - Predefined operation types --------------------------------------
// -------------------------------------------------------------------
/**
* Represents "browse" operations.
* <p>
* These operations are used to open URL in a browser.
* </p>
* @see #canBrowse()
* @see #browse(URL)
*/
public static final String BROWSE = "browse";
/**
* Represents "file manager" operations.
* <p>
* These operations are used to reveal local files in a file manager.
* </p>
* @see #canOpenInFileManager()
* @see #openInFileManager(File)
*/
public static final String OPEN_IN_FILE_MANAGER = "openFM";
/**
* Represents "open" operations.
* <p>
* These operations are used to open local files.
* </p>
* @see #canOpen()
* @see #open(File)
*/
public static final String OPEN = "open";
// - Operation priority ----------------------------------------------
// -------------------------------------------------------------------
/**
* Represents system operations.
* <p>
* These operations are for internal use only, and cannot be registered through the
* {@link #registerOperation(String,int,DesktopOperation)} method.
* </p>
*/
public static final int SYSTEM_OPERATION = 0;
/**
* Non-system operations.
* <p>
* These operations are treated with a lower priority than {@link #SYSTEM_OPERATION} ones, but higher
* than {@link #FALLBACK_OPERATION} ones. They are meant for plugin specific operations and are expected
* to be fairly reliable.
* </p>
*/
public static final int CUSTOM_OPERATION = 1;
/**
* Last resort operations.
* <p>
* These operations will only ever be used if nothing else is available. They need only be a best-effort solution.
* </p>
*/
public static final int FALLBACK_OPERATION = 2;
// - Class fields ----------------------------------------------------
// -------------------------------------------------------------------
/** All available desktop operations. */
private static Map<String, List<DesktopOperation>>[] operations;
/** All known desktops. */
private static Vector<DesktopAdapter> desktops;
/** Current desktop. */
private static DesktopAdapter desktop;
/** Object used to create instances of {@link AbstractTrash}. */
private static TrashProvider trashProvider;
// - Initialisation --------------------------------------------------
// -------------------------------------------------------------------
/**
* Prevents instanciation of the class.
*/
private DesktopManager() {}
/*
* Static initialisation.
* Bear in mind that adapters and operations are registered the 'wrong' way around:
* the earlier they are registered, the lower their priority.
*/
static {
// - Adapters initialisation -------------------------------------
// ---------------------------------------------------------------
desktops = new Vector<DesktopAdapter>();
// The default desktop adapter must be registered first, as we only want to use
// it if nothing else worked.
registerAdapter(new DefaultDesktopAdapter());
// Unix desktops:
// - check for Gnome before KDE, as it seems to be more popular.
// - check for 'configured' before 'guessed', as guesses are less reliable and more expensive.
registerAdapter(new GuessedXfceDesktopAdapter());
registerAdapter(new GuessedKde3DesktopAdapter());
registerAdapter(new GuessedKde4DesktopAdapter());
registerAdapter(new GuessedGnomeDesktopAdapter());
registerAdapter(new ConfiguredKde3DesktopAdapter());
registerAdapter(new ConfiguredKde4DesktopAdapter());
registerAdapter(new ConfiguredGnomeDesktopAdapter());
// Known OS adapters.
registerAdapter(new OpenVMSDesktopAdapter());
registerAdapter(new OSXDesktopAdapter());
registerAdapter(new Win9xDesktopAdapter());
registerAdapter(new WinNtDesktopAdapter());
// - Operations initialization -----------------------------------
// ---------------------------------------------------------------
operations = new Hashtable[3];
// Having 1.6 specific operations registered as the lowest priority system
// ones ensures that:
// - they are executed after CommandXXX operations (important, since CommandXXX
// operations are user configurable).
// - they are executed before any other operations (if available, they will
// provide safer and better integration than any other operation).
if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) {
innerRegisterOperation(OPEN, SYSTEM_OPERATION, new InternalOpen());
innerRegisterOperation(BROWSE, SYSTEM_OPERATION, new InternalBrowse());
}
// Registers CommandXXX operations.
innerRegisterOperation(BROWSE, SYSTEM_OPERATION, new CommandBrowse());
innerRegisterOperation(OPEN_IN_FILE_MANAGER, SYSTEM_OPERATION, new CommandOpenInFileManager());
innerRegisterOperation(OPEN, SYSTEM_OPERATION, new CommandOpen(false));
// The only FALLBACK operation we have at the time of writing is for OPEN,
// where we can try to run the file as if it was an executable.
innerRegisterOperation(OPEN, FALLBACK_OPERATION, new CommandOpen(true));
}
/**
* Initialises desktop management.
* <p>
* If <code>install</code> is set to <code>true</code>, this method
* might result in installing desktop specific data such as bookmarks, keyboard
* shortcuts...
* </p>
* @param install whether or not to install desktop specific information.
* @throws DesktopInitialisationException if an error occured while initialising desktops.
*/
public static void init(boolean install) throws DesktopInitialisationException {
// Browses desktop from the last registered to the first, to make sure that
// custom desktop adapters are used before the default ones.
for(int i = desktops.size() - 1; i >= 0; i--) {
DesktopAdapter current = desktops.elementAt(i);
if(current.isAvailable()) {
desktop = current;
LOGGER.debug("Using desktop: " + desktop);
desktop.init(install);
return;
}
}
}
/**
* Makes sure that we have a {@link DesktopAdapter} to work with.
* <p>
* If the {@link #init(boolean)} method wasn't called, the <code>DesktopManager</code>
* will find itself in a situation where it doesn't know which desktop it's running on.
* Calling this method ensures that, if a {@link DesktopAdapter} instance is required,
* we have at least the {@link DefaultDesktopAdapter} to work with.
* </p>
*/
private static void checkInit() {
if(desktop == null)
desktop = new DefaultDesktopAdapter();
}
// - Desktop adapter registration ------------------------------------
// -------------------------------------------------------------------
/**
* Registers the specified {@link DesktopAdapter}.
* <p>
* Note that the later an adapter is registered, the higher its priority. Since all
* default adapters are registered at initialisation time, any call to this method
* will result in the new adapter to be checked before them.
* </p>
* @param adapter desktop adapter to register.
*/
public static void registerAdapter(DesktopAdapter adapter) {desktops.add(adapter);}
// - Operation registration ------------------------------------------
// -------------------------------------------------------------------
/**
* Registers the specified operation for the specified type and priority.
*/
private static void innerRegisterOperation(String type, int priority, DesktopOperation operation) {
// Makes sure we have a container for operations of the specified priority.
if(operations[priority] == null)
operations[priority] = new Hashtable<String, List<DesktopOperation>>();
List<DesktopOperation> container = operations[priority].get(type);
// Makes sure we have a container for operations of the specified type.
if (container == null)
operations[priority].put(type, container = new Vector<DesktopOperation>());
// Creates the requested entry.
container.add(operation);
}
public static void registerOperation(String type, int priority, DesktopOperation operation) {
if(priority != FALLBACK_OPERATION && priority != CUSTOM_OPERATION)
throw new IllegalArgumentException();
innerRegisterOperation(type, priority, operation);
}
// - Operation support -----------------------------------------------
// -------------------------------------------------------------------
private static List<DesktopOperation> getOperations(String type, int priority) {
if(operations[priority] == null)
return null;
return operations[priority].get(type);
}
private static DesktopOperation getAvailableOperation(String type, int priority) {
List<DesktopOperation> container = getOperations(type, priority);
// If the operation vector is null, no need to look further.
if((container ) != null)
for(int i = container.size() - 1; i >= 0; i--) {
DesktopOperation operation = container.get(i);
if (operation.isAvailable())
return operation;
}
return null;
}
private static DesktopOperation getSupportedOperation(String type, int priority, Object[] target) {
List<DesktopOperation> container = getOperations(type, priority);
// If the operation vector is null, no need to look further.
if (container != null)
for(int i = container.size() - 1; i >= 0; i--) {
DesktopOperation operation = container.get(i);
if (operation.canExecute(target))
return operation;
}
return null;
}
private static DesktopOperation getSupportedOperation(String type, Object[] target) {
DesktopOperation operation;
if((operation = getSupportedOperation(type, SYSTEM_OPERATION, target)) != null)
return operation;
if((operation = getSupportedOperation(type, CUSTOM_OPERATION, target)) != null)
return operation;
if((operation = getSupportedOperation(type, FALLBACK_OPERATION, target)) != null)
return operation;
return null;
}
private static DesktopOperation getAvailableOperation(String type) {
DesktopOperation operation;
if((operation = getAvailableOperation(type, SYSTEM_OPERATION)) != null)
return operation;
if((operation = getAvailableOperation(type, CUSTOM_OPERATION)) != null)
return operation;
if((operation = getAvailableOperation(type, FALLBACK_OPERATION)) != null)
return operation;
return null;
}
public static boolean isOperationAvailable(String type) {return getAvailableOperation(type) != null;}
public static boolean isOperationSupported(String type, Object[] target) {return getSupportedOperation(type, target) != null;}
public static void executeOperation(String type, Object[] target) throws IOException, UnsupportedOperationException {
DesktopOperation operation = getSupportedOperation(type, target);
if (operation!= null)
operation.execute(target);
else
throw new UnsupportedOperationException();
}
public static String getOperationName(String type) throws UnsupportedOperationException {
DesktopOperation operation = getAvailableOperation(type);
if (operation != null)
return operation.getName();
throw new UnsupportedOperationException();
}
public static String getOperationName(String type, Object[] target) throws UnsupportedOperationException {
DesktopOperation operation = getSupportedOperation(type, target);
if (operation != null)
return operation.getName();
throw new UnsupportedOperationException();
}
// - Browser helpers -------------------------------------------------
// -------------------------------------------------------------------
public static boolean canBrowse() {return isOperationAvailable(BROWSE);}
public static boolean canBrowse(URL url) {return isOperationSupported(BROWSE, new Object[] {url});}
public static boolean canBrowse(String url) {return isOperationSupported(BROWSE, new Object[] {url});}
public static boolean canBrowse(AbstractFile url) {return isOperationSupported(BROWSE, new Object[] {url});}
public static void browse(URL url) throws IOException, UnsupportedOperationException {executeOperation(BROWSE, new Object[] {url});}
public static void browse(String url) throws IOException, UnsupportedOperationException {executeOperation(BROWSE, new Object[] {url});}
public static void browse(AbstractFile url) throws IOException, UnsupportedOperationException {executeOperation(BROWSE, new Object[] {url});}
// - File opening helpers --------------------------------------------
// -------------------------------------------------------------------
public static boolean canOpen() {return isOperationAvailable(OPEN);}
public static boolean canOpen(File file) {return isOperationSupported(OPEN, new Object[] {file});}
public static boolean canOpen(String file) {return isOperationSupported(OPEN, new Object[] {file});}
public static boolean canOpen(AbstractFile file) {return isOperationSupported(OPEN, new Object[] {file});}
public static void open(File file) throws IOException, UnsupportedOperationException {executeOperation(OPEN, new Object[] {file});}
public static void open(String file) throws IOException, UnsupportedOperationException {executeOperation(OPEN, new Object[] {file});}
public static void open(AbstractFile file) throws IOException, UnsupportedOperationException {executeOperation(OPEN, new Object[] {file});}
// - File manager helpers --------------------------------------------
// -------------------------------------------------------------------
public static boolean canOpenInFileManager() {return isOperationAvailable(OPEN_IN_FILE_MANAGER);}
public static boolean canOpenInFileManager(File file) {return isOperationSupported(OPEN_IN_FILE_MANAGER, new Object[] {file});}
public static boolean canOpenInFileManager(String file) {return isOperationSupported(OPEN_IN_FILE_MANAGER, new Object[] {file});}
public static boolean canOpenInFileManager(AbstractFile file) {return isOperationSupported(OPEN_IN_FILE_MANAGER, new Object[] {file});}
public static void openInFileManager(File file) throws IOException, UnsupportedOperationException {executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file});}
public static void openInFileManager(String file) throws IOException, UnsupportedOperationException {executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file});}
public static void openInFileManager(AbstractFile file) throws IOException, UnsupportedOperationException {executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file});}
private static String getFileManagerName(DesktopOperation operation) throws UnsupportedOperationException {
if(operation == null)
throw new UnsupportedOperationException();
return operation.getName();
}
public static String getFileManagerName() throws UnsupportedOperationException {return getFileManagerName(getAvailableOperation(OPEN_IN_FILE_MANAGER));}
public static String getFileManagerName(File file) throws UnsupportedOperationException {
return getFileManagerName(getSupportedOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}));
}
public static String getFileManagerName(String file) throws UnsupportedOperationException {
return getFileManagerName(getSupportedOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}));
}
public static String getFileManagerName(AbstractFile file) throws UnsupportedOperationException {
return getFileManagerName(getSupportedOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}));
}
// - Trash management ------------------------------------------------
// -------------------------------------------------------------------
/**
* Returns an instance of the {@link com.mucommander.desktop.AbstractTrash} implementation that can be used on the current platform.
* @return an instance of the AbstractTrash implementation that can be used on the current platform, or <code>null</code> if none is available.
*/
public static AbstractTrash getTrash() {
TrashProvider provider = getTrashProvider();
return provider == null ? null : provider.getTrash();
}
/**
* Returns the object used to create instances of {@link com.mucommander.desktop.AbstractTrash}.
* @return the object used to create instances of {@link AbstractTrash} if any, <code>null</code> otherwise.
*/
public static TrashProvider getTrashProvider() {
return trashProvider;
}
/**
* Sets the object that is used to create instances of {@link com.mucommander.desktop.AbstractTrash}.
* @param provider object that will be used to create instances of {@link com.mucommander.desktop.AbstractTrash}.
*/
public static void setTrashProvider(TrashProvider provider) {trashProvider = provider;}
// - Mouse management ------------------------------------------------
// -------------------------------------------------------------------
/**
* Checks whether the specified <code>MouseEvent</code> is a left-click for this destop.
* <p>
* There are some cases where Java doesn't detect mouse events properly - for example,
* <i>CONTROL + LEFT CLICK</i> is a <i>RIGHT CLICK</i> under Mac OS X.<br>
* The goal of this method is to allow desktop to check for such non-standard behaviours.
* </p>
* @param e event to check.
* @return <code>true</code> if the specified event is a left-click for this desktop, <code>false</code> otherwise.
* @see #isRightMouseButton(MouseEvent)
* @see #isMiddleMouseButton(MouseEvent)
*/
public static boolean isLeftMouseButton(MouseEvent e) {
checkInit();
return desktop.isLeftMouseButton(e);
}
/**
* Checks whether the specified <code>MouseEvent</code> is a left-click for this destop.
* <p>
* There are some cases where Java doesn't detect mouse events properly - for example,
* <i>CONTROL + LEFT CLICK</i> is a <i>RIGHT CLICK</i> under Mac OS X.<br>
* The goal of this method is to allow desktop to check for such non-standard behaviours.
* </p>
* @param e event to check.
* @return <code>true</code> if the specified event is a left-click for this desktop, <code>false</code> otherwise.
* @see #isMiddleMouseButton(MouseEvent)
* @see #isLeftMouseButton(MouseEvent)
*/
public static boolean isRightMouseButton(MouseEvent e) {
checkInit();
return desktop.isRightMouseButton(e);
}
/**
* Checks whether the specified <code>MouseEvent</code> is a left-click for this destop.
* <p>
* There are some cases where Java doesn't detect mouse events properly - for example,
* <i>CONTROL + LEFT CLICK</i> is a <i>RIGHT CLICK</i> under Mac OS X.<br>
* The goal of this method is to allow desktop to check for such non-standard behaviours.
* </p>
* @param e event to check.
* @return <code>true</code> if the specified event is a left-click for this desktop, <code>false</code> otherwise.
* @see #isRightMouseButton(MouseEvent)
* @see #isLeftMouseButton(MouseEvent)
*/
public static boolean isMiddleMouseButton(MouseEvent e) {
checkInit();
return desktop.isMiddleMouseButton(e);
}
/**
* Returns the maximum interval in milliseconds between mouse clicks for them to be considered as 'multi-clicks'
* (e.g. double-clicks). The returned value should reflects the desktop's multi-click (or double-click) interval,
* which may or may not correspond to the one Java uses for double-clicks.
* @return the maximum interval in milliseconds between mouse clicks for them to be considered as 'multi-clicks'.
*/
public static int getMultiClickInterval() {
checkInit();
return desktop.getMultiClickInterval();
}
// - Misc. -----------------------------------------------------------
// -------------------------------------------------------------------
/**
* Returns the command used to start shell processes.
* <p>
* The returned command must set the shell in its 'run script' mode.
* For example, for bash, the returned command should be <code>/bin/bash -l -c"</code>.
* </p>
* @return the command used to start shell processes.
*/
public static String getDefaultShell() {
checkInit();
return desktop.getDefaultShell();
}
/**
* Returns <code>true</code> if the given file is an application file. What an application file actually is
* is system-dependent and can take various forms.
* It can be a simple executable file, as in the case of Windows <code>.exe</code> files, or a directory
* containing an executable and various meta-information files, like Mac OS X's <code>.app</code> files.
*
* @param file the file to test
* @return <code>true</code> if the given file is an application file
*/
public static boolean isApplication(AbstractFile file) {
return desktop.isApplication(file);
}
}