/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved.
* Copyright (C) 2010 Illya Yalovyy (yalovoy@gmail.com). All rights reserved.
* Copyright (C) 2010 Peransin Nicolas. All rights reserved.
* Use is subject to license terms.
*/
package org.mypsycho.swing.app;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.beans.Beans;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.EventObject;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import org.mypsycho.beans.Inject;
import org.mypsycho.beans.Injectable;
import org.mypsycho.beans.InjectionContext;
import org.mypsycho.swing.app.task.DoWaitForEmptyEventQ;
import org.mypsycho.swing.app.utils.SwingHelper;
import org.mypsycho.text.Localized;
/**
* The base class for Swing applications.
* <p>
* This class defines a simple lifecyle for Swing applications: {@code initialize}, {@code startup},
* {@code ready}, and {@code shutdown}. The {@code Application's} {@code startup} method is
* responsible for creating the initial GUI and making it visible, and the {@code shutdown} method
* for hiding the GUI and performing any other cleanup actions before the application exits. The
* {@code initialize} method can be used configure system properties that must be set before the GUI
* is constructed and the {@code ready} method is for applications that want to do a little bit of
* extra work once the GUI is "ready" to use. Concrete subclasses must override the {@code startup}
* method.
* <p>
* Applications are started with the static {@code launch} method. Applications use the
* {@code ApplicationContext} {@link Application#getContext} to find resources, actions, local
* storage, and so on.
* <p>
* All {@code Application} subclasses must override {@code startup} and they should call
* {@link #exit} (which calls {@code shutdown}) to exit. Here's an example of a complete
* "Hello World" Application:
*
* <pre>
*
* public class MyApplication extends Application {
*
* JFrame mainFrame = null;
*
* @Override
* protected void startup() {
* mainFrame = new JFrame("Hello World");
* mainFrame.add(new JLabel("Hello World"));
* mainFrame.addWindowListener(new MainFrameListener());
* mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
* mainFrame.pack();
* mainFrame.setVisible(true);
* }
*
* @Override
* protected void shutdown() {
* mainFrame.setVisible(false);
* }
*
* private class MainFrameListener extends WindowAdapter {
*
* public void windowClosing(WindowEvent e) {
* exit();
* }
* }
*
* public static void main(String[] args) {
* new MyApplication().launch(args);
* }
* }
* </pre>
* <p>
* The {@code mainFrame's} {@code defaultCloseOperation} is set to {@code DO_NOTHING_ON_CLOSE}
* because we're handling attempts to close the window by calling {@code ApplicationContext}
* {@link #exit}.
* <p>
* Simple single frame applications like the example can be defined more easily with the
* {@link SingleFrameApplication} {@code Application} subclass.
* <p>
* All of the Application's methods are called (must be called) on the EDT.
* <p>
* All but the most trivial applications should define a Property-based ResourceBundle in same
* package (like {@code MyApplication.properties}).
* This ResourceBundle contains resources shared by the entire application and should begin with the
* following the standard Application resources:
* <pre>
* Application.name = A short name, typically just a few words
* Application.id = Suitable for Application specific identifiers, like file names
* Application.title = A title suitable for dialogs and frames
* Application.version = A version string that can be incorporated into messages
* Application.vendor = A proper name, like Sun Microsystems, Inc.
* Application.vendorId = suitable for Application-vendor specific identifiers, like file names.
* Application.homepage = A URL like http://www.javadesktop.org
* Application.description = One brief sentence
* Application.lookAndFeel = either system, default, or a LookAndFeel class name
* </pre>
* Default value are provided using Class and Package name.
* <p>
* The {@code Application.lookAndFeel} resource is used to initialize the
* {@code UIManager lookAndFeel} as follows:
* <ul>
* <li>{@code system} - the system (native) look and feel</li>
* <li>{@code default} - use the JVM default, typically the cross platform look and feel</li>
* <li>{@code nimbus} - use the modern cross platform look and feel Nimbus
* <li>a LookAndFeel class name - use the specified class
* </ul>
*
* @see SingleFrameApplication
* @see ApplicationContext
* @see UIManager#setLookAndFeel
* @author Hans Muller (Hans.Muller@Sun.COM)
*/
@Inject(order="actionMap", deferred={View.PROP_NAME, OptionView.PROP_NAME})
public abstract class Application extends SwingBean implements Injectable, Localized {
// Lifecycle list is not a enum to allow aggregation
public static final String LAUNCH_LIFECYCLE = "launch";
public static final String END_LIFECYCLE = "end";
public static final String TITLE_PROP = "Application.title";
public static final String VENDOR_PROP = "Application.vendor";
public static final String DESCRIPTION_PROP = "Application.description";
public static final String LNF_PROP = "Application.lookAndFeel";
public static final String SYSTEM_LNF_ID = "system";
public static final String DEFAULT_LNF_ID = "default";
public static final String STATE_PROP = "state";
public interface LifecycleStep {
// Get id can be use to hide internal state and only expose
Object getId();
void run(Application app, EventObject e) throws Exception ;
}
public enum LaunchState implements LifecycleStep {
CREATING {
@Override
public void run(Application app, EventObject e) throws Exception {
app.create();
}
},
INITIALING {
@Override
public void run(Application app, EventObject e) throws Exception {
app.initialize(app.arguments);
}
},
STARTING {
@Override
public void run(Application app, EventObject e) throws Exception {
app.startup();
}
},
READY {
@Override
public void run(final Application app, EventObject e) throws Exception {
app.getContext().getTaskService().execute(new DoWaitForEmptyEventQ() {
@Override
protected void finished() {
app.ready = true;
app.ready();
}
});
}
};
public Object getId() {
return this;
}
}
public enum EndState implements LifecycleStep {
NOTIFY_EVENT {
@Override
public void run(Application app, EventObject e) throws Exception {
for (ApplicationListener listener : app.listeners) {
try {
listener.willExit(e);
} catch (Exception ex) {
app.exceptionThrown(Level.WARNING,
"willExit", "Listener notification failed",
ex);
}
}
}
},
DISPOSE {
@Override
public void run(Application app, EventObject e) throws Exception {
app.dispose();
}
},
SHUTDOWN {
@Override
public void run(Application app, EventObject e) throws Exception {
app.shutdown();
}
};
public Object getId() {
return this;
}
}
private final List<ApplicationListener> listeners =
new CopyOnWriteArrayList<ApplicationListener>();
private final ApplicationContext context;
private String[] arguments;
private Object state = null;
private boolean ready = false; // The last lifecycle step
private ViewBehaviour commonBehaviour = null;
protected Properties properties = new Properties();
protected Locale locale = JComponent.getDefaultLocale();
/**
* Not to be called directly, see {@link #launch launch}.
* <p>
* Subclasses can provide a no-args construtor
* to initialize private final state however GUI
* initialization, and anything else that might refer to
* public API, should be done in the {@link #startup startup}
* method.
*/
protected Application() {
// createPlateformsSpecs(plateforms);
context = createContext();
}
/**
* This method is called when a recoverable exception has
* been caught.
*
* @param e The exception that was caught.
*/
public void exceptionThrown(Level level, Object id, String context, Throwable cause) {
try {
for (ApplicationListener listener : listeners) {
listener.exceptionThrown(level, id, context, cause);
}
} catch (Exception e) {
// a listener is corrupted,
// we cannot let it jeopardizes the application,
// we cannot invoke the listeners as we will have a inifite loop
// we cannot let it go complety silenty
e.printStackTrace();
}
}
/**
* Do something TODO.
* <p>
* Details of the function.
* </p>
*
* @return
*/
protected ApplicationContext createContext() {
return new ApplicationContext(this);
}
public String getProperty(String propertyName) {
return properties.getProperty(propertyName);
}
/**
* Return the lifecycle step.
* <p>
* Lifecyles are identified by string, not enum, to be extensible.
* </p>
* <p>
* LifecycleStep are interface (not enum) to be extensible.
* </p>
*
* @param lifecyle
* @return
*/
protected LifecycleStep[] getLifecycleStep(String lifecyle) {
if (LAUNCH_LIFECYCLE.equals(lifecyle)) {
return LaunchState.values();
} else if (END_LIFECYCLE.equals(lifecyle)) {
return EndState.values();
} else {
return null;
}
}
protected final void doLifecycle(String life, EventObject event) throws Exception {
for (ApplicationListener listener : listeners) {
listener.beforeCycle(life, event);
}
Exception failure = null;
try {
for (LifecycleStep step : getLifecycleStep(life)) {
setState(step.getId());
step.run(Application.this, event);
}
} catch (Exception t) {
failure = t;
throw t;
} finally {
for (ApplicationListener listener : listeners) {
listener.afterCycle(life, event, failure);
}
}
}
public void launch(final String... args) {
launch(true, args);
}
/**
* Creates an instance of the specified {@code Application}
* subclass, sets the {@code ApplicationContext} {@code
* application} property, and then calls the new {@code
* Application's} {@code initialize} and {@code startup} methods.
*
* When UI is ready, method {@code ready} is called.
*
* The {@code launch} method is
* typically called from the Application's {@code main}:
* <pre>
* public static void main(String[] args) {
* Application.launch(MyApplication.class, args);
* }
* </pre>
* The {@code applicationClass} constructor and {@code startup} methods
* run on the event dispatching thread.
*
* @param applicationClass the {@code Application} class to launch
* @param args {@code main} method arguments
* @see #shutdown
* @see ApplicationContext#getApplication
*/
public final void launch(final boolean wait, final String... args) {
// No synchro
final Throwable[] issue = { null };
Runnable doLaunch = new Runnable() {
@Override
public void run() {
try {
if (state != null) {
throw new IllegalStateException("Application already launched");
}
arguments = args;
doLifecycle(LAUNCH_LIFECYCLE, null);
ready = true;
} catch (Throwable e) {
issue[0] = e;
if (!wait) {
exceptionThrown(Level.SEVERE, "launch", "Error at state " + state, e);
end();
}
}
}
};
if (wait) {
if (!EventQueue.isDispatchThread()) {
try {
SwingUtilities.invokeAndWait(doLaunch);
} catch (Exception e) {
issue[0] = e;
}
} else {
doLaunch.run();
}
// InvocationTargetException is not a meaning full in a framework
while (issue[0] instanceof InvocationTargetException) {
issue[0] = ((InvocationTargetException) issue[0]).getTargetException();
}
if (issue[0] instanceof Error) {
throw (Error) issue[0];
}
if (issue[0] instanceof RuntimeException) {
throw (RuntimeException) issue[0];
}
if (issue[0] != null) {
throw new IllegalStateException(issue[0]);
}
} else {
SwingUtilities.invokeLater(doLaunch);
}
}
/* Initializes the ApplicationContext applicationClass and application
* properties.
*
* Note that, as of Java SE 5, referring to a class literal
* doesn't force the class to be loaded. More info:
* http://java.sun.com/javase/technologies/compatibility.jsp#literal
* It's important to perform these initializations early, so that
* Application static blocks/initializers happen afterwards.
*
* @param applicationClass the {@code Application} class to create
* @return created application instance
*/
void create() throws Exception {
if (!Beans.isDesignTime()) {
/* A common mistake for privileged applications that make
* network requests (and aren't applets or web started) is to
* not configure the http.proxyHost/Port system properties.
* We paper over that issue here.
*/
try {
System.setProperty("java.net.useSystemProxies", "true");
} catch (SecurityException ignoreException) {
// Unsigned apps can't set this property.
}
}
// Add plateform in context, inject Application, create injection propagator
getContext().init();
if (!Beans.isDesignTime()) {
installLookAndFeel();
}
}
public void initResources(InjectionContext context) {
properties.putAll(context.getRootContext());
}
protected void installLookAndFeel() {
String lnf = properties.getProperty(LNF_PROP, DEFAULT_LNF_ID); // or default ?
if (!DEFAULT_LNF_ID.equalsIgnoreCase(lnf)) {
try {
if (SYSTEM_LNF_ID.equalsIgnoreCase(lnf)) {
String name = UIManager.getSystemLookAndFeelClassName();
UIManager.setLookAndFeel(name);
return;
}
LookAndFeelInfo lnfInfo = null;
for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if (info.getName().equals(lnf)) {
lnfInfo = info;
break;
}
}
if (lnfInfo != null) { // By reflection
UIManager.setLookAndFeel(lnfInfo.getClassName());
} else {
UIManager.setLookAndFeel(lnf);
}
} catch (Exception e) {
String s = "Failed to set LookandFeel " + LNF_PROP + " = \"" + lnf + "\"";
exceptionThrown(Level.WARNING, LNF_PROP, s, e);
}
}
// /!\ look and feel is defined AFTER the injection of application resources
// So application resources
lnf = UIManager.getLookAndFeel().getID();
getContext().getResourceManager().addGlobal(ApplicationContext.LOOKNFEEL_PROP, lnf);
}
/**
* Responsible for initializations that must occur before the
* GUI is constructed by {@code startup}.
* <p>
* This method is called by the static {@code launch} method,
* before {@code startup} is called. Subclasses that want
* to do any initialization work before {@code startup} must
* override it. The {@code initialize} method
* runs on the event dispatching thread.
* <p>
* By default initialize() does nothing.
*
* @param args the main method's arguments.
* @see #launch
* @see #startup
* @see #shutdown
*/
protected void initialize(String[] args) {
}
/**
* Responsible for starting the application; for creating and showing
* the initial GUI.
* <p>
* This method is called by the static {@code launch} method,
* subclasses must override it. It runs on the event dispatching
* thread.
*
* @see #launch
* @see #initialize
* @see #shutdown
*/
protected abstract void startup();
/**
* Called after the startup() method has returned and there
* are no more events on the
* {@link Toolkit#getSystemEventQueue system event queue}.
* When this method is called, the application's GUI is ready
* to use.
* <p>
* It's usually important for an application to start up as
* quickly as possible. Applications can override this method
* to do some additional start up work, after the GUI is up
* and ready to use.
*
* @see #launch
* @see #startup
* @see #shutdown
*/
protected void ready() {
}
protected void dispose() {
for (View view : View.getViews(this)) {
view.release();
}
}
/**
* Called when the application {@link #exit exits}.
* Subclasses may override this method to do any cleanup
* tasks that are necessary before exiting. Obviously, you'll want to try
* and do as little as possible at this point. This method runs
* on the event dispatching thread.
*
* @see #startup
* @see #ready
* @see #exit
* @see #addExitListener
*/
protected void shutdown() {
}
/**
* Gracefully shutdowns the application, calls {@code exit(null)}
* This version of exit() is convenient if the decision to exit the
* application wasn't triggered by an event.
*
* @see #exit(EventObject)
*/
public final void exit() {
exit(null);
}
/**
* Gracefully shutdowns the application.
* <p>
* If none of the {@code ExitListener.canExit()} methods return false,
* calls the {@code ExitListener.willExit()} methods, then
* {@code shutdown()}, and then exits the Application with
* {@link #end end}. Exceptions thrown while running willExit() or shutdown()
* are logged but otherwise ignored.
* <p>
* If the caller is responding to an GUI event, it's helpful to pass the
* event along so that ExitListeners' canExit methods that want to popup
* a dialog know on which screen to show the dialog. For example:
* <pre>
* class ConfirmExit implements Application.ExitListener {
* public boolean canExit(EventObject e) {
* Object source = (e != null) ? e.getSource() : null;
* Component owner = (source instanceof Component) ? (Component)source : null;
* int option = JOptionPane.showConfirmDialog(owner, "Really Exit?");
* return option == JOptionPane.YES_OPTION;
* }
* public void willExit(EventObejct e) {}
* }
* myApplication.addExitListener(new ConfirmExit());
* </pre>
* The {@code eventObject} argument may be null, e.g. if the exit
* call was triggered by non-GUI code, and {@code canExit}, {@code
* willExit} methods must guard against the possibility that the
* {@code eventObject} argument's {@code source} is not a {@code
* Component}.
*
* @param event the EventObject that triggered this call or null
* @see #addExitListener
* @see #removeExitListener
* @see #shutdown
* @see #end
*/
@Action
// remove ambiguity with #exit()
public void exit(final EventObject event) {
if (SwingUtilities.isEventDispatchThread()) {
doExit(event);
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
doExit(event);
}
});
} catch (Exception ignore) {}
}
}
protected void doExit(final EventObject event) {
List<LifecycleStep> steps = Arrays.asList(getLifecycleStep(END_LIFECYCLE));
if (steps.contains(getState())) {
return; // Already exiting, further request are ignored
}
//
for (ApplicationListener listener : listeners) {
if (!listener.canExit(event)) {
return;
}
}
try {
doLifecycle(END_LIFECYCLE, event);
} catch (Exception e) {
exceptionThrown(Level.WARNING, "exit",
"Exception interrupt Application exit", e);
} finally {
setState(null);
end();
}
}
/**
* Called by {@link #exit exit} to terminate the application. Calls
* {@code Runtime.getRuntime().exit(0)}, which halts the JVM.
*
* @see #exit
*/
protected void end() {
Runtime.getRuntime().exit(0);
}
/**
* Adds an {@code ExitListener} to the list.
*
* @param listener the {@code ExitListener}
* @see #removeExitListener
* @see #getExitListeners
*/
public void addApplicationListener(ApplicationListener listener) {
listeners.add(listener);
}
/**
* Removes an {@code ExitListener} from the list.
*
* @param listener the {@code ExitListener}
* @see #addExitListener
* @see #getExitListeners
*/
public void removeApplicationListener(ApplicationListener listener) {
listeners.remove(listener);
}
/**
* All of the {@code ExitListeners} added so far.
*
* @return all of the {@code ExitListeners} added so far.
*/
public ApplicationListener[] getApplicationListeners() {
return listeners.toArray(new ApplicationListener[listeners.size()]);
}
/**
* The ApplicationContext for this Application.
*
* @return the Application's ApplicationContext
*/
public final ApplicationContext getContext() {
return context;
}
protected View createView(RootPaneContainer c) {
View view = new View(this, c.getRootPane());
registerCommonBehavior(view);
return view;
}
public View show(RootPaneContainer c) {
SwingHelper.assertNotNull("window", c);
View view = View.getView(c);
if (view == null) {
view = createView(c);
}
show(view);
return view;
}
/**
* Shows the application {@code View}
* @param view - View to show
* @see View
*/
public void show(View view) {
if (view.getApplication() != this) {
throw new IllegalStateException("Application does not own the view");
}
view.show();
}
/**
* Returns the state.
*
* @return the state
*/
public Object getState() {
return state;
}
/**
* Hides the application {@code View}
* @param view
* @see View
*/
public void hide(View view) {
if (view.getApplication() != this) {
throw new IllegalStateException("Application does not own the view");
}
view.hide();
}
/**
* Returns the locale.
*
* @return the locale
*/
public Locale getLocale() {
return locale;
}
/**
* Sets the locale.
*
* @param locale the locale to set
*/
public void setLocale(Locale locale) {
if (locale == null) {
locale = JComponent.getDefaultLocale();
}
Locale old = this.locale;
this.locale = locale;
firePropertyChange(Locales.LOCALE_PROP, old, locale);
}
/**
* State is managed by lifecycle
*
* @param id the new state
*/
private void setState(Object id) {
Object old = state;
state = id;
firePropertyChange(STATE_PROP, old, state);
}
private static Component getSourceComponent(EventObject evt) {
if ((evt == null) || !(evt.getSource() instanceof Component)) {
return null;
}
return (Component) evt.getSource();
}
public Object showOption(EventObject evt, String name) {
return showOption(getSourceComponent(evt), name);
}
public Object showOption(EventObject evt, String name, Object message) {
return showOption(getSourceComponent(evt), name, message);
}
public Object showOption(EventObject evt, JOptionPane option) {
return showOption(getSourceComponent(evt), option);
}
public Object showOption(EventObject evt, String name, JOptionPane option) {
return showOption(getSourceComponent(evt), name, option);
}
public Object showOption(Component parent, String name) {
return showOption(parent, name, new JOptionPane());
}
public Object showOption(Component parent, String name, Object message) {
return showOption(parent, name, new JOptionPane(message));
}
public Object showOption(Component parent, JOptionPane option) {
return showOption(parent, option.getName(), option);
}
public Object showOption(Component parent, String name, JOptionPane option) {
option.setName(name);
option.setComponentOrientation(((parent == null) ?
JOptionPane.getRootFrame() : parent).getComponentOrientation());
JDialog dialog = option.createDialog(parent, name + "Option");
dialog.setName(name);
if (option.getMessage() instanceof Component) {
// Message with component can be big
// If the dialog is not resizable, it can extend outside the screen !!
dialog.setResizable(true);
}
if (Locales.isForced(option)) {
Locales.setLocale(dialog, option.getLocale());
} else if (parent != null) {
// We fix the locale from parent otherwise the frame locale will be propagated
Locales.setLocale(dialog, parent.getLocale());
}
option.selectInitialValue(); // ?? Values may not be defined
show(new OptionView(this, dialog, parent));
dialog.dispose();
return option.getValue();
}
@Deprecated /** Use showOption */
public Object show(EventObject evt, JOptionPane option) {
return showOption(getSourceComponent(evt), option);
}
@Deprecated /** Use showOption */
public Object show(EventObject evt, String name, JOptionPane option) {
return showOption(getSourceComponent(evt), name, option);
}
@Deprecated /** Use showOption */
public Object show(Component parent, JOptionPane option) {
return showOption(parent, option.getName(), option);
}
@Deprecated /** Use showOption */
public Object show(Component parent, String name, JOptionPane option) {
return showOption(parent, name, option);
}
/**
* The state of the initial UI.
* @return true if the initial UI is ready
*/
public boolean isReady() {
return ready;
}
protected void registerCommonBehavior(View view) {
if (commonBehaviour != null) {
view.register(commonBehaviour);
}
}
/**
* Returns the commonBehaviour.
*
* @return the commonBehaviour
*/
public ViewBehaviour getCommonBehaviour() {
return commonBehaviour;
}
/**
* Sets the commonBehaviour.
*
* @param commonBehaviour the commonBehaviour to set
*/
public void setCommonBehaviour(ViewBehaviour commonBehaviour) {
this.commonBehaviour = commonBehaviour;
}
}