/*
* 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.Frame;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import javax.swing.LookAndFeel;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.ShutdownHook;
import com.mucommander.commons.conf.ConfigurationEvent;
import com.mucommander.commons.conf.ConfigurationListener;
import com.mucommander.conf.MuConfigurations;
import com.mucommander.conf.MuPreference;
import com.mucommander.conf.MuPreferences;
import com.mucommander.extension.ExtensionManager;
import com.mucommander.ui.main.commandbar.CommandBar;
import com.mucommander.ui.main.frame.MainFrameBuilder;
/**
* Window Manager is responsible for creating, disposing, switching,
* in other words managing :) muCommander windows.
*
* @author Maxence Bernard, Arik Hadas
*/
//public class WindowManager implements ActionListener, WindowListener, ActivePanelListener, LocationListener, ConfigurationListener {
public class WindowManager implements WindowListener, ConfigurationListener {
private static final Logger LOGGER = LoggerFactory.getLogger(WindowManager.class);
// - MainFrame positioning --------------------------------------------------
// --------------------------------------------------------------------------
// The following constants are used to compute the proper position of a new MainFrame.
/** MainFrame (main muCommander window) instances */
private List<MainFrame> mainFrames;
/** MainFrame currently being used (that has focus),
* or last frame to have been used if muCommander doesn't have focus */
private MainFrame currentMainFrame;
private static final WindowManager instance = new WindowManager();
// - Initialization ---------------------------------------------------------
// --------------------------------------------------------------------------
/**
* Installs all custom look and feels.
*/
private static void installCustomLookAndFeels() {
List<String> plafs; // All available custom look and feels.
// Tries to retrieve the custom look and feels list.
if((plafs = MuConfigurations.getPreferences().getListVariable(MuPreference.CUSTOM_LOOK_AND_FEELS, MuPreferences.CUSTOM_LOOK_AND_FEELS_SEPARATOR)) == null)
return;
// Goes through the list and install every custom look and feel we could find.
// Look and feels that aren't supported under the current platform are ignored.
for(String plaf : plafs) {
try {installLookAndFeel(plaf);}
catch(Throwable e) {
LOGGER.info("Failed to install Look&Feel "+plaf, e);
}
}
}
/**
* Creates a new instance of WindowManager.
*/
private WindowManager() {
mainFrames = new Vector<MainFrame>();
// Notifies Swing that look&feels must be loaded as extensions.
// This is necessary to ensure that look and feels placed in the extensions folder
// are accessible.
UIManager.getDefaults().put("ClassLoader", ExtensionManager.getClassLoader());
// Installs all custom look and feels.
installCustomLookAndFeels();
// Sets custom lookAndFeel if different from current lookAndFeel
String lnfName = MuConfigurations.getPreferences().getVariable(MuPreference.LOOK_AND_FEEL);
if(lnfName!=null && !lnfName.equals(UIManager.getLookAndFeel().getName()))
setLookAndFeel(lnfName);
if(lnfName == null)
LOGGER.debug("Could load look'n feel from preferences");
MuConfigurations.addPreferencesListener(this);
}
/**
* Returns the sole instance of WindowManager.
*
* @return the sole instance of WindowManager
*/
public static WindowManager getInstance() {
return instance;
}
/**
* Returns the <code>MainFrame</code> instance that was last active. Note that the returned <code>MainFrame</code>
* may or may not be currently active.
*
* @return the <code>MainFrame</code> instance that was last active
*/
public static MainFrame getCurrentMainFrame() {
return instance.currentMainFrame;
}
/**
* Returns a <code>Vector</code> of all <code>MainFrame</code> instances currently displaying.
*
* @return a <code>Vector</code> of all <code>MainFrame</code> instances currently displaying
*/
public static List<MainFrame> getMainFrames() {
return instance.mainFrames;
}
/**
* Refreshes all panels in all frames in an asynchronous manner.
*/
public static void tryRefreshCurrentFolders() {
// Starts with the main frame to make sure that results are immediately
// visible to the user.
instance.currentMainFrame.tryRefreshCurrentFolders();
for(MainFrame mainFrame : instance.mainFrames)
if(mainFrame != instance.currentMainFrame)
mainFrame.tryRefreshCurrentFolders();
}
/**
* Creates a new MainFrame and makes it visible on the screen, on top of any other frames.
*
* @param leftFolders initial paths for the left frame.
* @param rightFolders initial paths for the right frame.
* @return the newly created MainFrame.
*/
public static synchronized void createNewMainFrame(MainFrameBuilder mainFrameBuilder) {
MainFrame[] newMainFrames = mainFrameBuilder.build();
// To catch user window closing actions
for (MainFrame frame : newMainFrames)
frame.addWindowListener(instance);
// Adds the new MainFrame to the vector
instance.mainFrames.addAll(Arrays.asList(newMainFrames));
// Set new window's title. Window titles show window number only if there is more than one window.
// So if a second window was just created, we update first window's title so that it shows window number (#1).
for (MainFrame frame : instance.mainFrames)
frame.updateWindowTitle();
// Make frames visible
for (MainFrame frame : newMainFrames)
frame.setVisible(true);
if (instance.mainFrames.size() > 0) {
int previouslySelectedMainFrame = mainFrameBuilder.getSelectedFrame();
instance.mainFrames.get(previouslySelectedMainFrame).toFront();
}
}
/**
* Disposes all opened windows, ending with the one that is currently active if there is one,
* or the last one which was activated.
*/
public static synchronized void quit() {
// Dispose all MainFrames, ending with the currently active one.
int nbFrames = instance.mainFrames.size();
if(nbFrames>0) { // If an uncaught exception occurred in the startup sequence, there is no MainFrame to dispose
// Retrieve current MainFrame's index
int currentMainFrameIndex = getCurrentWindowIndex();
// Dispose all MainFrames but the current one
for(int i=0; i<nbFrames; i++) {
if(i!=currentMainFrameIndex)
instance.mainFrames.get(i).dispose();
}
// Dispose current MainFrame last so that its attributes (last folders, window position...) are saved last
// in the preferences
instance.mainFrames.get(currentMainFrameIndex).dispose();
}
// Dispose all other frames (viewers, editors...)
Frame frames[] = Frame.getFrames();
nbFrames = frames.length;
Frame frame;
for(int i=0; i<nbFrames; i++) {
frame = frames[i];
if(frame.isShowing()) {
LOGGER.debug("disposing frame#"+i);
frame.dispose();
}
}
// Initiate shutdown sequence.
// Important note: we cannot rely on windowClosed() triggering the shutdown sequence as
// Quit under OS X shuts down the app as soon as this method returns and as a result,
// windowClosed() events are never dispatched to the MainFrames
ShutdownHook.initiateShutdown();
}
/**
* Returns the index of the currently selected window
* @return index of currently selected window
*/
public static int getCurrentWindowIndex() {
return instance.mainFrames.indexOf(instance.currentMainFrame);
}
/**
* Switches to the next MainFrame, in the order of which they were created.
*/
public static void switchToNextWindow() {
int frameIndex = getCurrentWindowIndex();
MainFrame mainFrame = instance.mainFrames.get((frameIndex+1) % instance.mainFrames.size());
mainFrame.toFront();
}
/**
* Switches to previous MainFrame, in the order of which they were created.
*/
public static void switchToPreviousWindow() {
int frameIndex = getCurrentWindowIndex();
int nbFrames = instance.mainFrames.size();
MainFrame mainFrame = instance.mainFrames.get((frameIndex-1+nbFrames) % nbFrames);
mainFrame.toFront();
}
public static void installLookAndFeel(String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
LookAndFeel plaf;
plaf = (LookAndFeel)Class.forName(className, true, ExtensionManager.getClassLoader()).newInstance();
if(plaf.isSupportedLookAndFeel())
UIManager.installLookAndFeel(plaf.getName(), plaf.getClass().getName());
}
/**
* Changes LooknFeel to the given one, updating the UI of each MainFrame.
*
* @param lnfName name of the new LooknFeel to use
*/
private static void setLookAndFeel(String lnfName) {
try {
ClassLoader oldLoader;
Thread currentThread;
// Initializes class loading.
// This is necessary due to Swing's UIDefaults.LazyProxyValue behaviour that just
// won't use the right ClassLoader instance to load resources.
currentThread = Thread.currentThread();
oldLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(ExtensionManager.getClassLoader());
UIManager.setLookAndFeel((LookAndFeel)Class.forName(lnfName, true, ExtensionManager.getClassLoader()).newInstance());
// Restores the contextual ClassLoader.
currentThread.setContextClassLoader(oldLoader);
for(int i=0; i<instance.mainFrames.size(); i++)
SwingUtilities.updateComponentTreeUI(instance.mainFrames.get(i));
}
catch(Throwable e) {
LOGGER.debug("Exception caught", e);
}
}
////////////////////////////
// WindowListener methods //
////////////////////////////
public void windowActivated(WindowEvent e) {
Object source = e.getSource();
// Return if event doesn't originate from a MainFrame (e.g. ViewerFrame or EditorFrame)
if(!(source instanceof MainFrame))
return;
currentMainFrame = (MainFrame)e.getSource();
// Let MainFrame know that it is active in the foreground
currentMainFrame.setForegroundActive(true);
// Resets shift mode to false, since keyReleased events may have been lost during window switching
CommandBar commandBar = currentMainFrame.getCommandBar();
if(commandBar!=null)
commandBar.setAlternateActionsMode(false);
}
public void windowDeactivated(WindowEvent e) {
Object source = e.getSource();
// Workaround for JRE bug #4841881 (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4841881) /
// which causes Alt+Tab to focus the menu bar under certain L&F.
// This bug has also been reported as muCommander bug #89.
MenuSelectionManager.defaultManager().clearSelectedPath();
// Return if event doesn't originate from a MainFrame (e.g. ViewerFrame or EditorFrame)
if(!(source instanceof MainFrame))
return;
// Let MainFrame know that it is not active anymore
((MainFrame)e.getSource()).setForegroundActive(false);
}
public void windowClosing(WindowEvent e) {
}
/**
* windowClosed is synchronized so that it doesn't get called while quit() is executing.
*/
public synchronized void windowClosed(WindowEvent e) {
LOGGER.trace("called");
Object source = e.getSource();
if(source instanceof MainFrame) {
// Remove disposed MainFrame from the MainFrame list
int frameIndex = mainFrames.indexOf(source);
mainFrames.remove(source);
// Update following windows titles to reflect the MainFrame's disposal.
// Window titles show window number only if there is more than one window.
// So if there is only one window left, we update first window's title so that it removes window number (#1).
int nbFrames = mainFrames.size();
if(nbFrames==1) {
mainFrames.get(0).updateWindowTitle();
}
else {
if(frameIndex!=-1) {
for(int i=frameIndex; i<nbFrames; i++)
mainFrames.get(i).updateWindowTitle();
}
}
}
// Test if there is at least one MainFrame still showing
if(mainFrames.size()>0)
return;
// Test if there is at least one window (viewer, editor...) still showing
Frame frames[] = Frame.getFrames();
int nbFrames = frames.length;
Frame frame;
for(int i=0; i<nbFrames; i++) {
frame = frames[i];
if(frame.isShowing()) {
LOGGER.debug("found active frame#"+i);
return;
}
}
// No more window showing, initiate shutdown sequence
ShutdownHook.initiateShutdown();
}
public void windowIconified(WindowEvent e) {
}
public void windowDeiconified(WindowEvent e) {
}
public void windowOpened(WindowEvent e) {
}
///////////////////////////////////
// ConfigurationListener methods //
///////////////////////////////////
/**
* Listens to certain configuration variables.
*/
public void configurationChanged(ConfigurationEvent event) {
String var = event.getVariable();
// /!\ font.size is set after font.family in AppearancePrefPanel
// that's why we only listen to this one in order not to change Font twice
if (var.equals(MuPreferences.LOOK_AND_FEEL)) {
String lnfName = event.getValue();
if(!UIManager.getLookAndFeel().getClass().getName().equals(lnfName))
setLookAndFeel(lnfName);
}
}
}