/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2012 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.theme;
import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Vector;
import java.util.WeakHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.PlatformManager;
import com.mucommander.RuntimeConstants;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.filter.ExtensionFilenameFilter;
import com.mucommander.commons.file.util.ResourceLoader;
import com.mucommander.commons.io.StreamUtils;
import com.mucommander.commons.util.StringUtils;
import com.mucommander.conf.MuConfigurations;
import com.mucommander.conf.MuPreference;
import com.mucommander.conf.MuPreferences;
import com.mucommander.io.backup.BackupInputStream;
import com.mucommander.io.backup.BackupOutputStream;
import com.mucommander.text.Translator;
/**
* Offers methods for accessing and modifying themes.
* @author Nicolas Rinaudo
*/
public class ThemeManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ThemeManager.class);
// - Class variables -----------------------------------------------------------------
// -----------------------------------------------------------------------------------
/** Path to the user defined theme file. */
private static AbstractFile userThemeFile;
/** Default user defined theme file name. */
private static final String USER_THEME_FILE_NAME = "user_theme.xml";
/** Path to the custom themes repository. */
private static final String CUSTOM_THEME_FOLDER = "themes";
/** List of all registered theme change listeners. */
private static final WeakHashMap<ThemeListener, Object> listeners = new WeakHashMap<ThemeListener, Object>();
/** List of all predefined theme names. */
private static final String[] PREDEFINED_THEME_NAMES = {
"ClassicCommander",
"Native",
"RetroCommander",
"Striped"
};
// - Instance variables --------------------------------------------------------------
// -----------------------------------------------------------------------------------
/** Whether or not the user theme was modified. */
private static boolean wasUserThemeModified;
/** Theme that is currently applied to muCommander. */
private static Theme currentTheme;
/** Used to listen on the current theme's modifications. */
private static ThemeListener listener = new CurrentThemeListener();
// - Initialisation ------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Prevents instanciation of the class.
*/
private ThemeManager() {}
/**
* Loads the current theme.
* <p>
* This method goes through the following steps:
* <ul>
* <li>Try to load the theme defined in the configuration.</li>
* <li>If that failed, try to load the default theme.</li>
* <li>If that failed, try to load the user theme if that hasn't been tried yet.</li>
* <li>If that failed, use an empty theme.</li>
* </ul>
* </p>
*/
public static void loadCurrentTheme() {
int type; // Current theme's type.
String name; // Current theme's name.
boolean wasUserThemeLoaded; // Whether we have tried loading the user theme or not.
// Loads the current theme type as defined in configuration.
try {type = getThemeTypeFromLabel(MuConfigurations.getPreferences().getVariable(MuPreference.THEME_TYPE, MuPreferences.DEFAULT_THEME_TYPE));}
catch(Exception e) {type = getThemeTypeFromLabel(MuPreferences.DEFAULT_THEME_TYPE);}
// Loads the current theme name as defined in configuration.
if(type != Theme.USER_THEME) {
wasUserThemeLoaded = false;
name = MuConfigurations.getPreferences().getVariable(MuPreference.THEME_NAME, MuPreferences.DEFAULT_THEME_NAME);
}
else {
name = null;
wasUserThemeLoaded = true;
}
// If the current theme couldn't be loaded, uses the default theme as defined in the configuration.
currentTheme = null;
try {currentTheme = readTheme(type, name);}
catch(Exception e1) {
type = getThemeTypeFromLabel(MuPreferences.DEFAULT_THEME_TYPE);
name = MuPreferences.DEFAULT_THEME_NAME;
if(type == Theme.USER_THEME)
wasUserThemeLoaded = true;
// If the default theme can be loaded, tries to load the user theme if we haven't done so yet.
// If we have, or if it fails, defaults to an empty user theme.
try {currentTheme = readTheme(type, name);}
catch(Exception e2) {
if(!wasUserThemeLoaded) {
try {currentTheme = readTheme(Theme.USER_THEME, null);}
catch(Exception e3) {}
}
if(currentTheme == null) {
currentTheme = new Theme(listener);
wasUserThemeModified = true;
}
}
setConfigurationTheme(currentTheme);
}
}
// - Themes access -------------------------------------------------------------------
// -----------------------------------------------------------------------------------
private static Iterator<String> predefinedThemeNames() {
// The list of predefined themes is no longer dynamically created as this causes Webstart to retrieve and
// explore the application's JAR via HTTP, which is inefficient and prevents the application from being
// launched offline.
// try {
// return getThemeNames(ResourceLoader.getRootPackageAsFile(ThemeManager.class).getChild(PathUtils.removeLeadingSeparator(RuntimeConstants.THEMES_PATH, "/")));
// }
// catch(IOException e) {return Collections.emptyList().iterator();}
return Arrays.asList(PREDEFINED_THEME_NAMES).iterator();
}
private static Iterator<String> customThemeNames() throws IOException {
return getThemeNames(FileFactory.getFile(getCustomThemesFolder().getAbsolutePath()));
}
private static Iterator<String> getThemeNames(AbstractFile themeFolder) {
AbstractFile[] files;
Vector<String> names;
try {
files = themeFolder.ls(new ExtensionFilenameFilter(".xml"));
names = new Vector<String>();
for (AbstractFile file : files)
names.add(getThemeName(file));
return names.iterator();
}
catch(Exception e) {return new Vector<String>().iterator();}
}
public static Vector<Theme> getAvailableThemes() {
Vector<Theme> themes;
Iterator<String> iterator;
String name;
themes = new Vector<Theme>();
// Tries to load the user theme. If it's corrupt, uses an empty user theme.
try {themes.add(readTheme(Theme.USER_THEME, null));}
catch(Exception e) {themes.add(new Theme(listener));}
// Loads predefined themes.
iterator = predefinedThemeNames();
while(iterator.hasNext()) {
name = iterator.next();
try {themes.add(readTheme(Theme.PREDEFINED_THEME, name));}
catch(Exception e) {
LOGGER.warn("Failed to load predefined theme " + name, e);
}
}
// Loads custom themes.
try {
iterator = customThemeNames();
while(iterator.hasNext()) {
name = iterator.next();
try {themes.add(readTheme(Theme.CUSTOM_THEME, name));}
catch(Exception e) {
LOGGER.warn("Failed to load custom theme " + name, e);
}
}
}
catch(Exception e) {
LOGGER.warn("Failed to load custom themes", e);
}
// Sorts the themes by name.
Collections.sort(themes, new Comparator<Theme>() {
public int compare(Theme t1, Theme t2) {return (t1.getName()).compareTo(t2.getName());}
});
return themes;
}
public static Vector<String> getAvailableThemeNames() {
Vector<String> themes;
Iterator<String> iterator;
themes = new Vector<String>();
// Adds the user theme name.
themes.add(Translator.get("theme.custom_theme"));
// Adds predefined theme names.
iterator = predefinedThemeNames();
while(iterator.hasNext())
themes.add(iterator.next());
// Adds custom theme names.
try {
iterator = customThemeNames();
while(iterator.hasNext())
themes.add(iterator.next());
}
catch(Exception e) {
LOGGER.debug("Failed to load custom theme names", e);
}
// Sorts the theme names.
Collections.sort(themes);
return themes;
}
public static Iterator<String> availableThemeNames() {return getAvailableThemeNames().iterator();}
public static synchronized Iterator<Theme> availableThemes() {return getAvailableThemes().iterator();}
// - Theme paths access --------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns the path to the user's theme file.
* <p>
* This method cannot guarantee the file's existence, and it's up to the caller
* to deal with the fact that the user might not actually have created a user theme.
* </p>
* <p>
* This method's return value can be modified through {@link #setUserThemeFile(String)}.
* If this wasn't called, the default path will be used. This is generated by calling
* <code>new java.io.File({@link com.mucommander.PlatformManager#getPreferencesFolder()}, {@link #USER_THEME_FILE_NAME})</code>.
* </p>
* @return the path to the user's theme file.
* @see #setUserThemeFile(String)
* @throws IOException if an error occured while locating the default user theme file.
*/
public static AbstractFile getUserThemeFile() throws IOException {
if(userThemeFile == null)
return PlatformManager.getPreferencesFolder().getChild(USER_THEME_FILE_NAME);
return userThemeFile;
}
/**
* Sets the path to the user theme file.
* <p>
* The specified file does not have to exist. If it does, however, it must be accessible.
* </p>
* @param file path to the user theme file.
* @throws FileNotFoundException if <code>file</code> is not accessible.
* @see #getUserThemeFile()
*/
public static void setUserThemeFile(File file) throws FileNotFoundException {setUserThemeFile(FileFactory.getFile(file.getAbsolutePath()));}
/**
* Sets the path to the user theme file.
* <p>
* The specified file does not have to exist. If it does, however, it must be accessible.
* </p>
* @param file path to the user theme file.
* @throws IllegalArgumentException if <code>file</code> exists but is not accessible.
* @see #getUserThemeFile()
*/
public static void setUserThemeFile(AbstractFile file) throws FileNotFoundException {
if(file.isBrowsable())
throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath());
userThemeFile = file;
}
/**
* Sets the path to the user theme file.
* <p>
* The specified file does not have to exist. If it does, however, it must be accessible.
* </p>
* @param path path to the user theme file.
* @throws FileNotFoundException if <code>path</code> is not accessible.
* @see #getUserThemeFile()
*/
public static void setUserThemeFile(String path) throws FileNotFoundException {
AbstractFile file;
if((file = FileFactory.getFile(path)) == null)
setUserThemeFile(new File(path));
else
setUserThemeFile(file);
}
/**
* Returns the path to the custom themes' folder.
* <p>
* This method guarantees that the returned file actually exists.
* </p>
* @return the path to the custom themes' folder.
* @throws IOException if an error occured while locating the default user themes folder.
*/
public static AbstractFile getCustomThemesFolder() throws IOException {
AbstractFile customFolder;
// Retrieves the path to the custom themes folder and creates it if necessary.
customFolder = PlatformManager.getPreferencesFolder().getChild(CUSTOM_THEME_FOLDER);
if(!customFolder.exists())
customFolder.mkdir();
return customFolder;
}
// - Theme renaming / deleting -------------------------------------------------------
// -----------------------------------------------------------------------------------
public static void deleteCustomTheme(String name) throws IOException {
AbstractFile file;
// Makes sure the specified theme is not the current one.
if(isCurrentTheme(Theme.CUSTOM_THEME, name))
throw new IllegalArgumentException("Cannot delete current theme.");
// Deletes the theme.
file = getCustomThemesFolder().getChild(name + ".xml");
if(file.exists())
file.delete();
}
public static void renameCustomTheme(Theme theme, String name) throws IOException {
if(theme.getType() != Theme.CUSTOM_THEME)
throw new IllegalArgumentException("Cannot rename non-custom themes.");
// Makes sure the operation is necessary.
if(theme.getName().equals(name))
return;
// Computes a legal new name and renames theme.
name = getAvailableCustomThemeName(name);
getCustomThemesFolder().getChild(theme.getName() + ".xml").renameTo(getCustomThemesFolder().getChild(name + ".xml"));
theme.setName(name);
if(isCurrentTheme(theme))
setConfigurationTheme(theme);
}
// - Theme writing -------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns an output stream on the specified custom theme.
* @param name name of the custom theme on which to open an output stream.
* @return an output stream on the specified custom theme.
* @throws IOException if an I/O related error occurs.
*/
private static BackupOutputStream getCustomThemeOutputStream(String name) throws IOException {
return new BackupOutputStream(getCustomThemesFolder().getChild(name + ".xml"));
}
/**
* Returns an output stream on the user theme.
* @return an output stream on the user theme.
* @throws IOException if an I/O related error occurs.
*/
private static BackupOutputStream getUserThemeOutputStream() throws IOException {
return new BackupOutputStream(getUserThemeFile());
}
/**
* Returns an output stream on the requested theme.
* <p>
* This method is just a convenience, and wraps calls to {@link #getUserThemeInputStream()},
* and {@link #getCustomThemeInputStream(String)}.
* </p>
* <p>
* If <code>type</code> is equal to {@link Theme#USER_THEME}, the <code>name</code> argument
* will be ignored: there is only one user theme.
* </p>
* <p>
* If <code>type</code> is equal to {@link Theme#PREDEFINED_THEME}, an <code>IllegalArgumentException</code>
* will be thrown: predefined themes are not editable.
* </p>
* @param type type of the theme on which to open an output stream.
* @param name name of the theme on which to open an output stream.
* @return an output stream on the requested theme.
* @throws IOException if an I/O related error occurs.
*/
private static BackupOutputStream getOutputStream(int type, String name) throws IOException {
switch(type) {
// Predefined themes.
case Theme.PREDEFINED_THEME:
throw new IllegalArgumentException("Can not open output streams on predefined themes.");
// Custom themes.
case Theme.CUSTOM_THEME:
return getCustomThemeOutputStream(name);
// User theme.
case Theme.USER_THEME:
return getUserThemeOutputStream();
}
// Unknown theme.
throw new IllegalArgumentException("Illegal theme type: " + type);
}
/**
* Writes the content of the specified theme data to the specified output stream.
* <p>
* This method differs from {@link #exportTheme(Theme,OutputStream)} in that it will
* write the theme data only, skipping comments and other metadata.
* </p>
* @param data theme data to write.
* @param out where to write the theme data.
* @throws IOException if an I/O related error occurs.
* @see #exportTheme(Theme,OutputStream)
* @see #exportTheme(Theme,File)
* @see #writeThemeData(ThemeData,File).
*/
public static void writeThemeData(ThemeData data, OutputStream out) throws IOException {ThemeWriter.write(data, out);}
/**
* Writes the content of the specified theme data to the specified file.
* <p>
* This method differs from {@link #exportTheme(Theme,File)} in that it will
* write the theme data only, skipping comments and other metadata.
* </p>
* @param data theme data to write.
* @param file file in which to write the theme data.
* @throws IOException if an I/O related error occurs.
* @see #exportTheme(Theme,OutputStream)
* @see #exportTheme(Theme,File)
* @see #writeThemeData(ThemeData,OutputStream).
*/
public static void writeThemeData(ThemeData data, File file) throws IOException {
OutputStream out; // OutputStream on file.
out = null;
// Writes the theme data.
try {writeThemeData(data, out = new FileOutputStream(file));}
// Cleanup.
finally {
if(out != null) {
try {out.close();}
catch(Exception e) {}
}
}
}
/**
* Writes the content of the specified theme to its description file.
* @param theme theme to write.
* @throws IOException if any I/O related error occurs.
* @throws IllegalArgumentException if <code>theme</code> is a predefined theme.
* @see #writeTheme(ThemeData,int,String)
*/
public static void writeTheme(Theme theme) throws IOException {writeTheme(theme, theme.getType(), theme.getName());}
/**
* Writes the specified theme data over the theme described by <code>type</code> and <code>name</code>.
* <p>
* Note that this method doesn't check whether this will overwrite an existing theme.
* </p>
* <p>
* If <code>type</code> equals {@link Theme#USER_THEME}, <code>name</code> will be ignored.
* </p>
* @param data data to write.
* @param type type of the theme that is being written.
* @param name name of the theme that is being written.
* @throws IOException if any I/O related error occurs.
* @throws IllegalArgumentException if <code>theme</code> is a predefined theme.
* @see #writeTheme(Theme)
*/
public static void writeTheme(ThemeData data, int type, String name) throws IOException {
OutputStream out;
out = null;
try {writeThemeData(data, out = getOutputStream(type, name));}
finally {
if(out != null) {
try {out.close();}
catch(Exception e) {}
}
}
}
/**
* Exports the specified theme to the specified output stream.
* <p>
* If <code>type</code> is equal to {@link Theme#USER_THEME}, the <code>name</code> argument will be ignored
* as there is only one user theme.
* </p>
* <p>
* This method differs from {@link #writeThemeData(ThemeData,OutputStream)} in that it doesn't only copy
* the theme's data, but the whole content of the theme file, including comments. It also requires the theme
* file to exist.
* </p>
* @param type type of the theme to export.
* @param name name of the theme to export.
* @param out where to write the theme.
* @throws IOException if any I/O related error occurs.
* @see #exportTheme(int,String,File)
* @see #writeThemeData(ThemeData,OutputStream)
*/
public static void exportTheme(int type, String name, OutputStream out) throws IOException {
InputStream in; // Where to read the theme from.
in = null;
try {StreamUtils.copyStream(in = getInputStream(type, name), out);}
finally {
if(in != null) {
try {in.close();}
catch(Exception e) {}
}
}
}
/**
* Exports the specified theme to the specified output stream.
* <p>
* If <code>type</code> is equal to {@link Theme#USER_THEME}, the <code>name</code> argument will be ignored
* as there is only one user theme.
* </p>
* <p>
* This method differs from {@link #writeThemeData(ThemeData,File)} in that it doesn't only copy
* the theme's data, but the whole content of the theme file, including comments.
* </p>
* @param type type of the theme to export.
* @param name name of the theme to export.
* @param file where to write the theme.
* @throws IOException if any I/O related error occurs
* @see #exportTheme(int,String,OutputStream)
* @see #writeThemeData(ThemeData,File).
*/
public static void exportTheme(int type, String name, File file) throws IOException {
OutputStream out; // Where to write the data to.
out = null;
try {exportTheme(type, name, out = new FileOutputStream(file));}
finally {
if(out != null) {
try {out.close();}
catch(Exception e) {}
}
}
}
/**
* Exports the specified theme to the specified output stream.
* <p>
* This is a convenience method only and is strictly equivalent to calling
* <code>{@link #exportTheme(int,String,OutputStream) exportTheme(}theme.getType(), theme.getName(), out);</code>
* </p>
* @param theme theme to export.
* @param out where to write the theme.
* @throws IOException if any I/O related error occurs.
*/
public static void exportTheme(Theme theme, OutputStream out) throws IOException {exportTheme(theme.getType(), theme.getName(), out);}
/**
* Exports the specified theme to the specified output stream.
* <p>
* This is a convenience method only and is strictly equivalent to calling
* <code>{@link #exportTheme(int,String,File) exportTheme(}theme.getType(), theme.getName(), file);</code>
* </p>
* @param theme theme to export.
* @param file where to write the theme.
* @throws IOException if any I/O related error occurs.
*/
public static void exportTheme(Theme theme, File file) throws IOException {exportTheme(theme.getType(), theme.getName(), file);}
private static String getAvailableCustomThemeName(File file) {
String name;
// Retrieves the file's name, cutting the .xml extension off if
// necessary.
if(StringUtils.endsWithIgnoreCase(name = file.getName(), ".xml"))
name = name.substring(0, name.length() - 4);
return getAvailableCustomThemeName(name);
}
private static boolean isNameAvailable(String name, Iterator<String> names) {
while(names.hasNext())
if(names.next().equals(name))
return false;
return true;
}
private static String getAvailableCustomThemeName(String name) {
Vector<String> names;
int i;
String buffer;
names = getAvailableThemeNames();
// If the name is available, no need to suffix it with (xx).
if(isNameAvailable(name, names.iterator()))
return name;
// Removes any trailing (x) construct, and adds a trailing space if necessary.
name = name.replaceFirst("\\([0-9]+\\)$", "");
if(name.charAt(name.length() - 1) != ' ')
name = name + ' ';
i = 1;
do {buffer = name + '(' + (++i) + ')';}
while(!isNameAvailable(buffer, names.iterator()));
return buffer;
}
public static Theme duplicateTheme(Theme theme) throws IOException, Exception {return importTheme(theme.cloneData(), theme.getName());}
public static Theme importTheme(ThemeData data, String name) throws IOException, Exception {
writeTheme(data, Theme.CUSTOM_THEME, name = getAvailableCustomThemeName(name));
return new Theme(listener, data, Theme.CUSTOM_THEME, name);
}
public static Theme importTheme(File file) throws IOException, Exception {
String name; // Name of the new theme.
OutputStream out; // Where to write the theme data to.
InputStream in; // Where to read the theme data from.
ThemeData data;
// Makes sure the file contains a valid theme.
data = readThemeData(file);
// Initialisation.
name = getAvailableCustomThemeName(file);
out = null;
in = null;
// Imports the theme.
try {StreamUtils.copyStream(in = new FileInputStream(file), out = getCustomThemeOutputStream(name));}
// Cleanup.
finally {
if(in != null) {
try {in.close();}
catch(Exception e) {}
}
if(out != null) {
try {out.close();}
catch(Exception e) {}
}
}
return new Theme(listener, data, Theme.CUSTOM_THEME, name);
}
// - Theme reading -------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns an input stream on the user theme.
* @return an input stream on the user theme.
* @throws IOException if an I/O related error occurs.
*/
private static InputStream getUserThemeInputStream() throws IOException {
return new BackupInputStream(getUserThemeFile());
}
/**
* Returns an input stream on the requested predefined theme.
* @param name name of the predefined theme on which to open an input stream.
* @return an input stream on the requested predefined theme.
* @throws IOException if an I/O related error occurs.
*/
private static InputStream getPredefinedThemeInputStream(String name) throws IOException {
return ResourceLoader.getResourceAsStream(RuntimeConstants.THEMES_PATH + "/" + name + ".xml");
}
/**
* Returns an input stream on the requested custom theme.
* @param name name of the custom theme on which to open an input stream.
* @return an input stream on the requested custom theme.
* @throws IOException if an I/O related error occurs.
*/
private static InputStream getCustomThemeInputStream(String name) throws IOException {
return new BackupInputStream(getCustomThemesFolder().getChild(name + ".xml"));
}
/**
* Opens an input stream on the requested theme.
* <p>
* This method is just a convenience, and wraps calls to {@link #getUserThemeInputStream()},
* {@link #getPredefinedThemeInputStream(String)} and {@link #getCustomThemeInputStream(String)}.
* </p>
* @param type type of the theme to open an input stream on.
* @param name name of the theme to open an input stream on.
* @return an input stream opened on the requested theme.
* @throws IOException thrown if an IO related error occurs.
* @throws IllegalArgumentException thrown if <code>type</code> is not a legal theme type.
*/
private static InputStream getInputStream(int type, String name) throws IOException {
switch(type) {
// User theme.
case Theme.USER_THEME:
return getUserThemeInputStream();
// Predefined theme.
case Theme.PREDEFINED_THEME:
return getPredefinedThemeInputStream(name);
// Custom theme.
case Theme.CUSTOM_THEME:
return getCustomThemeInputStream(name);
}
// Error handling.
throw new IllegalArgumentException("Illegal theme type: " + type);
}
/**
* Returns the requested theme.
* @param type type of theme to retrieve.
* @param name name of the theme to retrieve.
* @return the requested theme.
*/
public static Theme readTheme(int type, String name) throws Exception {
ThemeData data; // Buffer for the theme data.
InputStream in; // Where to read the theme from.
// Do not reload the current theme, both for optimisation purposes and because
// it might cause user theme modifications to be lost.
if(currentTheme != null && isCurrentTheme(type, name))
return currentTheme;
// Reads the theme data.
in = null;
try {data = readThemeData(in = getInputStream(type, name));}
finally {
if(in != null) {
try {in.close();}
catch(Exception e) {}
}
}
// Creates the corresponding theme.
return new Theme(listener, data, type, name);
}
/**
* Reads theme data from the specified input stream.
* @param in where to read the theme data from.
* @return the resulting theme data.
* @throws Exception if an I/O or syntax error occurs.
*/
public static ThemeData readThemeData(InputStream in) throws Exception {
ThemeData data; // Buffer for the data.
// Reads the theme data.
ThemeReader.read(in, data = new ThemeData());
return data;
}
/**
* Reads theme data from the specified file.
* @param file where to read the theme data from.
* @return the resulting theme data.
* @throws Exception if an I/O or syntax error occurs.
*/
public static ThemeData readThemeData(File file) throws Exception {
InputStream in; // InputStream on file.
in = null;
// Loads the theme data.
try {return readThemeData(in = new FileInputStream(file));}
// Cleanup.
finally {
if(in != null) {
try {in.close();}
catch(Exception e) {}
}
}
}
// - Current theme access ------------------------------------------------------------
// -----------------------------------------------------------------------------------
private static void setConfigurationTheme(int type, String name) {
// Sets configuration depending on the new theme's type.
switch(type) {
// User defined theme.
case Theme.USER_THEME:
MuConfigurations.getPreferences().setVariable(MuPreference.THEME_TYPE, MuPreferences.THEME_USER);
MuConfigurations.getPreferences().setVariable(MuPreference.THEME_NAME, null);
break;
// Predefined themes.
case Theme.PREDEFINED_THEME:
MuConfigurations.getPreferences().setVariable(MuPreference.THEME_TYPE, MuPreferences.THEME_PREDEFINED);
MuConfigurations.getPreferences().setVariable(MuPreference.THEME_NAME, name);
break;
// Custom themes.
case Theme.CUSTOM_THEME:
MuConfigurations.getPreferences().setVariable(MuPreference.THEME_TYPE, MuPreferences.THEME_CUSTOM);
MuConfigurations.getPreferences().setVariable(MuPreference.THEME_NAME, name);
break;
// Error.
default:
throw new IllegalStateException("Illegal theme type: " + type);
}
}
/**
* Sets the specified theme as the current theme in configuration.
* @param theme theme to set as current.
*/
private static void setConfigurationTheme(Theme theme) {setConfigurationTheme(theme.getType(), theme.getName());}
/**
* Saves the current theme if necessary.
*/
public static void saveCurrentTheme() throws IOException {
// Makes sure no NullPointerException is raised if this method is called
// before themes have been initialised.
if(currentTheme == null)
return;
// Saves the user theme if it's the current one.
if(currentTheme.getType() == Theme.USER_THEME && wasUserThemeModified) {
writeTheme(currentTheme);
wasUserThemeModified = false;
}
}
public static Theme getCurrentTheme() {return currentTheme;}
/**
* Changes the current theme.
* <p>
* This method will change the current theme and trigger all the proper events.
* </p>
* @param theme theme to use as the current theme.
* @throws IllegalArgumentException thrown if the specified theme could not be loaded.
*/
public synchronized static void setCurrentTheme(Theme theme) {
Theme oldTheme;
// Makes sure we're not doing something useless.
if(isCurrentTheme(theme))
return;
// Saves the current theme if necessary.
try {saveCurrentTheme();}
catch(IOException e) {
LOGGER.warn("Couldn't save current theme", e);
}
// Updates muCommander's configuration.
oldTheme = currentTheme;
setConfigurationTheme(currentTheme = theme);
// Triggers the events generated by the theme change.
triggerThemeChange(oldTheme, currentTheme);
}
public synchronized static Font getCurrentFont(int id) {return currentTheme.getFont(id);}
public synchronized static Color getCurrentColor(int id) {return currentTheme.getColor(id);}
public synchronized static Theme overwriteUserTheme(ThemeData themeData) throws IOException {
// If the current theme is the user one, we just need to import the new data.
if(currentTheme.getType() == Theme.USER_THEME) {
currentTheme.importData(themeData);
writeTheme(currentTheme);
return currentTheme;
}
else {
writeTheme(themeData, Theme.USER_THEME, null);
return new Theme(listener, themeData);
}
}
/**
* Checks whether setting the specified font would require overwriting of the user theme.
* @param fontId identifier of the font to set.
* @param font value for the specified font.
* @return <code>true</code> if applying the specified font will overwrite the user theme,
* <code>false</code> otherwise.
*/
public synchronized static boolean willOverwriteUserTheme(int fontId, Font font) {
if(currentTheme.isFontDifferent(fontId, font))
return currentTheme.getType() != Theme.USER_THEME;
return false;
}
/**
* Checks whether setting the specified color would require overwriting of the user theme.
* @param colorId identifier of the color to set.
* @param color value for the specified color.
* @return <code>true</code> if applying the specified color will overwrite the user theme,
* <code>false</code> otherwise.
*/
public synchronized static boolean willOverwriteUserTheme(int colorId, Color color) {
if(currentTheme.isColorDifferent(colorId, color))
return currentTheme.getType() != Theme.USER_THEME;
return false;
}
/**
* Updates the current theme with the specified font.
* <p>
* This method might require to overwrite the user theme: custom and predefined themes are
* read only. In order to modify them, the ThemeManager must overwrite the user theme with
* the current theme and then set the font.<br/>
* If necessary, this can be checked beforehand by a call to {@link #willOverwriteUserTheme(int,Font)}.
* </p>
* @param id identifier of the font to set.
* @param font font to set.
*/
public synchronized static boolean setCurrentFont(int id, Font font) {
// Only updates if necessary.
if(currentTheme.isFontDifferent(id, font)) {
// Checks whether we need to overwrite the user theme to perform this action.
if(currentTheme.getType() != Theme.USER_THEME) {
currentTheme.setType(Theme.USER_THEME);
setConfigurationTheme(currentTheme);
}
currentTheme.setFont(id, font);
return true;
}
return false;
}
/**
* Updates the current theme with the specified color.
* <p>
* This method might require to overwrite the user theme: custom and predefined themes are
* read only. In order to modify them, the ThemeManager must overwrite the user theme with
* the current theme and then set the color.<br/>
* If necessary, this can be checked beforehand by a call to {@link #willOverwriteUserTheme(int,Color)}.
* </p>
* @param id identifier of the color to set.
* @param color color to set.
*/
public synchronized static boolean setCurrentColor(int id, Color color) {
// Only updates if necessary.
if(currentTheme.isColorDifferent(id, color)) {
// Checks whether we need to overwrite the user theme to perform this action.
if(currentTheme.getType() != Theme.USER_THEME) {
currentTheme.setType(Theme.USER_THEME);
setConfigurationTheme(currentTheme);
}
// Updates the color and notifies listeners.
currentTheme.setColor(id, color);
return true;
}
return false;
}
/**
* Returns <code>true</code> if the specified theme is the current one.
* @param theme theme to check.
* @return <code>true</code> if the specified theme is the current one, <code>false</code> otherwise.
*/
public static boolean isCurrentTheme(Theme theme) {return theme == currentTheme;}
private static boolean isCurrentTheme(int type, String name) {
if(type != currentTheme.getType())
return false;
if(type == Theme.USER_THEME)
return true;
return name.equals(currentTheme.getName());
}
// - Events management ---------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Notifies all listeners that the current theme has changed.
* <p>
* This method is meant to be called when the current theme has been changed.
* It will compare all fonts and colors in <code>oldTheme</code> and <code>newTheme</code> and,
* if any is found to be different, trigger the corresponding event.
* </p>
* <p>
* At the end of this method, all registered listeners will have been made aware of the new values
* they should be using.
* </p>
* @param oldTheme previous current theme.
* @param newTheme new current theme.
* @see #triggerFontEvent(FontChangedEvent)
* @see #triggerColorEvent(ColorChangedEvent)
*/
private static void triggerThemeChange(Theme oldTheme, Theme newTheme) {
// Triggers font events.
for(int i = 0; i < Theme.FONT_COUNT; i++)
if(oldTheme.isFontDifferent(i, newTheme.getFont(i)))
triggerFontEvent(new FontChangedEvent(currentTheme, i, newTheme.getFont(i)));
// Triggers color events.
for(int i = 0; i < Theme.COLOR_COUNT; i++)
if(oldTheme.isColorDifferent(i, newTheme.getColor(i)))
triggerColorEvent(new ColorChangedEvent(currentTheme, i, newTheme.getColor(i)));
}
/**
* Adds the specified object to the list of registered current theme listeners.
* <p>
* Any object registered through this method will received {@link ThemeListener#colorChanged(ColorChangedEvent) color}
* and {@link ThemeListener#fontChanged(FontChangedEvent) font} events whenever the current theme changes.
* </p>
* <p>
* Note that these events will not necessarily be fired as a result of a direct theme change: if, for example,
* the current theme is using look&feel dependant values and the current look&feel changes, the corresponding
* events will be passed to registered listeners.
* </p>
* <p>
* Listeners are stored as weak references, to make sure that the API doesn't keep ghost copies of objects
* whose usefulness is long since past. This forces callers to make sure they keep a copy of the listener's instance: if
* they do not, the instance will be weakly linked and garbage collected out of existence.
* </p>
* @param listener new current theme listener.
*/
public static void addCurrentThemeListener(ThemeListener listener) {synchronized (listeners) {listeners.put(listener, null);}}
/**
* Removes the specified object from the list of registered theme listeners.
* <p>
* Note that since listeners are stored as weak references, calling this method is not strictly necessary. As soon
* as a listener instance is not referenced anymore, it will automatically be caught and destroyed by the garbage
* collector.
* </p>
* @param listener current theme listener to remove.
*/
public static void removeCurrentThemeListener(ThemeListener listener) {synchronized (listeners) {listeners.remove(listener);}}
/**
* Notifies all theme listeners of the specified font event.
* @param event event to pass down to registered listeners.
* @see #triggerThemeChange(Theme,Theme)
*/
private static void triggerFontEvent(FontChangedEvent event) {
synchronized (listeners) {
for(ThemeListener listener : listeners.keySet())
listener.fontChanged(event);
}
}
/**
* Notifies all theme listeners of the specified color event.
* @param event event to pass down to registered listeners.
* @see #triggerThemeChange(Theme,Theme)
*/
private static void triggerColorEvent(ColorChangedEvent event) {
synchronized (listeners) {
for(ThemeListener listener : listeners.keySet())
listener.colorChanged(event);
}
}
// - Helper methods ------------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* Returns a valid type identifier from the specified configuration type definition.
* @param label label of the theme type as defined in {@link MuPreferences}.
* @return a valid theme type identifier.
*/
private static int getThemeTypeFromLabel(String label) {
if(label.equals(MuPreferences.THEME_USER))
return Theme.USER_THEME;
else if(label.equals(MuPreferences.THEME_PREDEFINED))
return Theme.PREDEFINED_THEME;
else if(label.equals(MuPreferences.THEME_CUSTOM))
return Theme.CUSTOM_THEME;
throw new IllegalStateException("Unknown theme type: " + label);
}
private static String getThemeName(AbstractFile themeFile) {
return themeFile.getNameWithoutExtension();
}
// - Listener methods ----------------------------------------------------------------
// -----------------------------------------------------------------------------------
/**
* @author Nicolas Rinaudo
*/
private static class CurrentThemeListener implements ThemeListener {
public void fontChanged(FontChangedEvent event) {
if(event.getSource().getType() == Theme.USER_THEME)
wasUserThemeModified = true;
if(event.getSource() == currentTheme)
triggerFontEvent(event);
}
public void colorChanged(ColorChangedEvent event) {
if(event.getSource().getType() == Theme.USER_THEME)
wasUserThemeModified = true;
if(event.getSource() == currentTheme)
triggerColorEvent(event);
}
}
}