package org.vorthmann.zome.ui;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javax.swing.JOptionPane;
import org.vorthmann.j3d.Platform;
import org.vorthmann.ui.Controller;
import org.vorthmann.ui.SplashScreen;
import org.vorthmann.zome.app.impl.ApplicationController;
/**
* Top-level UI class for vZome.
*
* This class has few responsibilities:
*
* - initialization of the root level logging system
* - create and destroy a splash screen during initial launch
* - create DocumentFrames for DocumentControllers
* - convey command line arguments to the ApplicationController
* - provide a static main() entry point
* - provide a common static entry point (initialize() method) for different launch adapters
* - provide quit, about, and open handlers for the Mac platform Adapter
* - provide the final Controller.ErrorChannel UI
* - display the About dialog
* - log build properties as early as possible
*
* I've tried to remove any state (fields) that is not necessary for those functions,
* and delegated as much as possible to the ApplicationController, including management
* of user preferences.
*
* @author vorth
*
*/
public final class ApplicationUI implements ActionListener, PropertyChangeListener
{
private Controller mController;
private Controller.ErrorChannel errors;
private final Collection<DocumentFrame> windowsToClose = new ArrayList<>();
// loggerClassName = "org.vorthmann.zome.ui.ApplicationUI"
// Initializing it this way just ensures that any copied code uses the correct class name for a static Logger in any class.
private static final String loggerClassName = new Throwable().getStackTrace()[0].getClassName();
private static final Logger logger = Logger .getLogger( loggerClassName );
// static class initializer configures global logging before any instance of the class is created.
static {
Logger rootLogger = Logger.getLogger("");
Level minLevel = Level.SEVERE;
if (rootLogger.getLevel().intValue() > minLevel.intValue()) {
// Set minimum logging level
rootLogger.setLevel(minLevel);
}
// If a FileHandler is already pre-configured by the logging.properties file then just use it as-is.
FileHandler fh = null;
for (Handler handler : rootLogger.getHandlers()) {
if (handler.getClass().isAssignableFrom(FileHandler.class)) {
fh = (FileHandler) handler;
break;
}
}
// If no FileHandler was pre-configured, then initialze our own default
if (fh == null) {
File logsFolder = Platform.logsFolder();
logsFolder.mkdir();
try {
// If there is a log file naming conflict and no "%u" field has been specified,
// an incremental unique number will be added at the end of the filename after a dot.
// This behavior interferes with file associations based on the .log file extension.
// It also results in the log files accumulating forever rather than overwriting the older ones
// and leaving only the number of log files specified in the constructor,
// so be sure to specify %u in the format string.
// If we limit the number of logs to 10, then sorting them alphabetically (0-9) conveniently sorts them by date & time as well.
//
// SV: I've reversed the %u and %g, so that sorting by name puts related logs together, in order. The Finder / Explorer already
// knows how to sort by date, so we don't need to support that.
fh = new FileHandler("%h/" + Platform.logsPath() + "/vZome60_%u_%g.log", 500000, 10);
} catch (Exception e1) {
rootLogger.log(Level.WARNING, "unable to set up vZome file log handler", e1);
try {
fh = new FileHandler();
} catch (Exception e2) {
rootLogger.log(Level.WARNING, "unable to set up default file log handler", e2);
}
}
if (fh != null) {
fh.setFormatter(new SimpleFormatter());
rootLogger.addHandler(fh);
}
}
}
private static ApplicationUI theUI;
private ApplicationUI() {}
// This is not used on the Mac, where the MacAdapter is the main class.
public static void main( String[] args )
{
try {
String prop = System .getProperty( "user.dir" );
File workingDir = new File( prop );
URL codebase = workingDir .toURI() .toURL();
initialize( args, codebase );
} catch ( Throwable e ) {
e .printStackTrace();
System.out.println( "problem in main(): " + e.getMessage() );
}
}
/**
* A common entry point for main() and com.vzome.platform.mac.Adapter.main().
*
* @param args
* @param codebase
* @return
* @throws MalformedURLException
*/
public static ApplicationUI initialize( String[] args, URL codebase ) throws MalformedURLException
{
/*
* First, fail-fast if we can see any environmental issue that will prevent vZome from launching correctly.
*/
if( System.getProperty("os.name").toLowerCase().contains("windows")) {
if( "console".compareToIgnoreCase(System.getenv("SESSIONNAME")) != 0) {
logger.info("Java OpenGL (JOGL) is not supported by Windows Terminal Services.");
final String msg = "vZome cannot be run under Windows Terminal Services.";
logger.severe(msg);
JOptionPane.showMessageDialog( null, msg, "vZome Fatal Error", JOptionPane.ERROR_MESSAGE );
System .exit( 0 );
}
}
/*
* Get the splash screen up before doing any more work.
*/
SplashScreen splash = null;
String splashImage = "org/vorthmann/zome/ui/vZome-6-splash.png";
if ( splashImage != null ) {
splash = new SplashScreen( splashImage );
splash .splash();
logger .info( "splash screen displayed" );
}
else {
logger .severe( "splash screen not found at " + splashImage );
}
theUI = new ApplicationUI();
/*
* Implementation Note:
*
* Note that the launch thread of any GUI application is in effect an initial
* worker thread - it is not the event dispatch thread, where the bulk of processing
* takes place. Thus, once the launch thread realizes a window, then the launch
* thread should almost always manipulate such a window through
* <code>EventQueue.invokeLater</code>. (This is done for closing the splash
* screen, for example.)
*/
// NOW we're ready to spend the cost of further initialization, but on the event thread
EventQueue .invokeLater( new InitializationWorker( theUI, args, codebase, splash ) );
return theUI;
}
private static class InitializationWorker implements Runnable
{
private final ApplicationUI ui;
private final String[] args;
private final URL codebase;
private final SplashScreen splash;
public InitializationWorker( ApplicationUI ui, String[] args, URL codebase, SplashScreen splash )
{
this.ui = ui;
this.args = args;
this.codebase = codebase;
this.splash = splash;
}
@Override
public void run()
{
String defaultAction = "launch";
Properties configuration = new Properties();
for ( int i = 0; i < args.length; i++ ) {
if ( args[i] .startsWith( "-" ) ) {
String propName = args[i++] .substring( 1 );
String propValue = args[i];
configuration .setProperty( propName, propValue );
}
else
try {
URL url = new URL( codebase, args[i] );
defaultAction = "openURL-" + url .toExternalForm();
} catch ( MalformedURLException e ) {
logger .severe( "Unable to construct URL from codebase: " + codebase + ", url argument" + args[i] );
}
}
configuration .putAll( loadBuildProperties() );
ui .mController = new ApplicationController( ui, configuration );
configuration .setProperty( "coreVersion", ui .mController .getProperty( "coreVersion" ) );
logConfig( configuration );
ui.errors = new Controller.ErrorChannel()
{
@Override
public void reportError( String errorCode, Object[] arguments )
{
// code copied from DocumentFrame!
if ( Controller.USER_ERROR_CODE.equals( errorCode ) ) {
errorCode = ( (Exception) arguments[0] ).getMessage();
// don't want a stack trace for a user error
logger.log( Level.WARNING, errorCode );
} else if ( Controller.UNKNOWN_ERROR_CODE.equals( errorCode ) ) {
errorCode = ( (Exception) arguments[0] ).getMessage();
logger.log( Level.WARNING, "internal error: " + errorCode, ( (Exception) arguments[0] ) );
errorCode = "internal error, see the log file at " + getLogFileName();
} else {
logger.log( Level.WARNING, "reporting error: " + errorCode, arguments );
// TODO use resources
}
// TODO use resources
JOptionPane .showMessageDialog( null, errorCode, "Error", JOptionPane.ERROR_MESSAGE );
}
@Override
public void clearError()
{}
};
ui.mController .setErrorChannel( ui.errors );
ui.mController .addPropertyListener( ui );
ui.mController .actionPerformed( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, defaultAction ) );
if ( splash != null )
splash .dispose();
}
}
@Override
public void propertyChange( PropertyChangeEvent evt )
{
switch ( evt .getPropertyName() ) {
case "newDocument":
Controller controller = (Controller) evt. getNewValue();
DocumentFrame window = new DocumentFrame( controller );
window .setVisible( true );
window .setAppUI( new PropertyChangeListener() {
@Override
public void propertyChange( PropertyChangeEvent evt )
{
windowsToClose .remove( window );
}
} );
windowsToClose .add( window );
break;
default:
break;
}
}
@Override
public void actionPerformed( ActionEvent event )
{
String action = event. getActionCommand();
if ( "new" .equals( action ) )
action = "new-golden";
switch ( action ) {
case "showAbout":
about();
break;
case "openURL":
String str = JOptionPane .showInputDialog( null, "Enter the URL for an online .vZome file.", "Open URL",
JOptionPane.PLAIN_MESSAGE );
mController .actionPerformed( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, "openURL-" + str ) );
break;
case "quit":
quit();
break;
default:
JOptionPane .showMessageDialog( null,
"No handler for action: \"" + action + "\"",
"Error Performing Action", JOptionPane.ERROR_MESSAGE );
}
}
public static Properties loadBuildProperties()
{
String defaultRsrc = "build.properties";
Properties defaults = new Properties();
try {
ClassLoader cl = ApplicationUI.class.getClassLoader();
InputStream in = cl.getResourceAsStream( defaultRsrc );
if ( in != null )
defaults .load( in );
} catch ( IOException ioe ) {
logger.warning( "problem reading build properties: " + defaultRsrc );
}
return defaults;
}
// Be sure logConfig is not called until after loadBuildProperties()
private static void logConfig( Properties src )
{
StringBuilder sb = new StringBuilder("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Initializing Application:");
appendPropertiesList(sb, null, new String[]
{ // use with System.getProperty(propName)
"java.version",
"java.vendor",
"java.home",
"java.util.logging.config.file",
"user.dir", // current working directory
"os.name",
"os.arch",
"sun.java.command",
});
appendPropertiesList(sb, loggingProperties(), new String[]
{
"logfile.name",
"logging.properties.filename",
"logging.properties.file.exists",
});
appendPropertiesList(sb, src, new String[]
{ // use with src.getProperty(propName)
"edition",
"version",
"buildNumber",
"gitCommit",
"coreVersion"
});
// Use an anonymousLogger to ensure that this is always written to the log file
// regardless of the settings in the logging.properties file
// and without changing the settings of any static loggers including the root logger
Logger anonymousLogger = Logger.getAnonymousLogger();
anonymousLogger.setLevel(Level.ALL);
anonymousLogger.config(sb.toString());
logJVMArgs();
logExtendedCharacters();
}
private static Properties loggingProperties() {
Properties props = new Properties();
File f = new File(".", "logging.properties");
// Use same logic to locate the file as LogManager.getLogManager().readConfiguration() uses...
String fname = System.getProperty("java.util.logging.config.file");
if (fname == null) {
fname = System.getProperty("java.home");
if (fname == null) {
throw new Error("Can't find java.home ??");
}
f = new File(fname, "lib");
f = new File(f, "logging.properties");
}
try {
fname = f.getCanonicalPath();
} catch (IOException ex) {
ex.printStackTrace();
}
props.put("logfile.name", getLogFileName());
props.put("logging.properties.filename", fname);
props.put("logging.properties.file.exists", Boolean.toString(f.exists()));
return props;
}
private static void logJVMArgs() {
Level level = Level.CONFIG;
if(logger.isLoggable(level)) {
Properties props = new Properties();
RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
List<String> arguments = runtimeMxBean.getInputArguments();
for(String argument : arguments) {
String[] parts = argument.split("=", 2);
String key = parts[0];
String value = (parts.length > 1) ? parts[1] : "";
props.put(key, value);
}
StringBuilder sb = new StringBuilder("JVM args:");
appendPropertiesList(sb, props);
logger.log(level, sb.toString());
}
}
private static void logExtendedCharacters() {
Level level = Level.FINE;
if(logger.isLoggable(level)) {
Properties props = new Properties();
props.put("phi", "\u03C6");
props.put("rho", "\u03C1");
props.put("sigma", "\u03C3");
props.put("sqrt", "\u221A");
props.put("xi", "\u03BE");
StringBuilder sb = new StringBuilder("Extended characters:");
appendPropertiesList(sb, props);
logger.log(level, sb.toString());
}
}
// appends the whole list sorted by its keys
private static void appendPropertiesList(StringBuilder sb, Properties props) {
String[] keys = props.keySet().toArray(new String[props.keySet().size()]);
Arrays.sort(keys);
appendPropertiesList(sb, props, keys);
}
private static void appendPropertiesList(StringBuilder sb, Properties src, String[] propNames) {
for (String propName : propNames) {
String propValue = (src == null)
? System.getProperty(propName)
: src.getProperty(propName);
propValue = (propValue == null ? "<null>" : propValue);
sb.append(System.getProperty("line.separator"))
.append(" ")
.append(propName)
.append(" = ")
.append(propValue);
}
}
private static String logFileName = null;
public static String getLogFileName() {
// determined on demand and cached so we only need to do all of this the first time.
if (logFileName == null) {
for (Handler handler : Logger.getLogger("").getHandlers()) {
if (handler.getClass().isAssignableFrom(FileHandler.class)) {
FileHandler fileHandler = (FileHandler) handler;
try {
// FileHandler.files has private access,
// so I'm going to resort to reflection to get the file name.
Field privateFilesField = fileHandler.getClass().getDeclaredField("files");
privateFilesField.setAccessible(true); // allow access to this private field
File[] files = (File[]) privateFilesField.get(fileHandler);
logFileName = files[0].getCanonicalPath();
break;
} catch (NullPointerException | NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | IOException ex) {
logger.log(Level.SEVERE, "Unable to determine log file name.", ex);
}
}
}
if (logFileName == null) {
logFileName = "your home directory"; // just be sure it's not null
logger.warning("Unable to identify log file name.");
}
}
return logFileName;
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// These next three methods may be invoked by the mac Adapter.
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
public void openFile( File file )
{
mController .doFileAction( "open", file );
}
public boolean quit()
{
for ( DocumentFrame documentFrame : windowsToClose ) {
if ( ! documentFrame .closeWindow() )
return false;
}
return true;
}
public void about()
{
String version = mController.getProperty( "edition" ) + " " + mController.getProperty( "version" ) + ", build "
+ mController .getProperty( "buildNumber" );
if ( mController .userHasEntitlement( "developer.extras" ) )
version += "\n\nGit commit: " + mController .getProperty( "gitCommit" )
+ "\n\nvzome-core: " + mController .getProperty( "coreVersion" );
JOptionPane.showMessageDialog( null, version + "\n\n"
+ "Contributors:\n\n" + "Scott Vorthmann\n" + "David Hall\n" + "\n"
+ "Acknowledgements:\n\n" + "Paul Hildebrandt\n" + "Marc Pelletier\n"
+ "David Richter\n" + "Brian Hall\n" + "Dan Duddy\n" + "Fabien Vienne\n" + "George Hart\n"
+ "Edmund Harriss\n" + "Corrado Falcolini\n" + "Ezra Bradford\n" + "Chris Kling\n" + "Samuel Verbiese\n" + "Walt Venable\n"
+ "Will Ackel\n" + "Tom Darrow\n" + "Sam Vandervelde\n" + "Henri Picciotto\n" + "Florelia Braschi\n"
+ "\n" + "Dedicated to Everett Vorthmann,\n" + "who made me an engineer\n"
+ "\n",
"About vZome", JOptionPane.PLAIN_MESSAGE );
}
}