package org.vorthmann.zome.app.impl; 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.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.net.URI; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import org.vorthmann.j3d.Platform; import org.vorthmann.ui.Controller; import org.vorthmann.ui.DefaultController; import org.vorthmann.zome.ui.ApplicationUI; import com.vzome.core.algebra.AlgebraicField; import com.vzome.core.commands.Command.Failure; import com.vzome.core.commands.Command.FailureChannel; import com.vzome.core.editor.DocumentModel; import com.vzome.core.exporters.Exporter3d; import com.vzome.core.math.symmetry.Symmetry; import com.vzome.core.model.Connector; import com.vzome.core.model.Strut; import com.vzome.core.render.Colors; import com.vzome.core.render.RenderedManifestation; import com.vzome.core.render.RenderedModel; import com.vzome.core.viewing.Lights; import com.vzome.desktop.controller.RenderingViewer; public class ApplicationController extends DefaultController { private static final Logger logger = Logger.getLogger( "org.vorthmann.zome.controller" ); private final Map<String, DocumentController> docControllers = new HashMap<>(); private final ActionListener ui; private final Properties userPreferences = new Properties(); private final Properties properties = new Properties(); private final Map<Symmetry, RenderedModel> mSymmetryModels = new HashMap<>(); private RenderingViewer.Factory rvFactory; private final com.vzome.core.editor.Application modelApp; private final File preferencesFile; private int lastUntitled = 0; public ApplicationController( ActionListener ui, Properties commandLineArgs ) { super(); long starttime = System.currentTimeMillis(); if ( logger .isLoggable( Level .INFO ) ) logger .info( "ApplicationController .initialize() starting" ); this.ui = ui; File prefsFolder = Platform .getPreferencesFolder(); File prefsFile = new File( prefsFolder, "vZome.preferences" ); if ( ! prefsFile .exists() ) { prefsFolder = new File( System.getProperty( "user.home" ) ); prefsFile = new File( prefsFolder, "vZome.preferences" ); } if ( ! prefsFile .exists() ) { prefsFile = new File( prefsFolder, ".vZome.prefs" ); } this.preferencesFile = prefsFile; if ( ! prefsFile .exists() ) { logger .config( "Used default preferences." ); } else { try { InputStream in = new FileInputStream( prefsFile ); userPreferences .load( in ); logger .config( "User Preferences loaded from " + prefsFile .getAbsolutePath() ); } catch ( Throwable t ) { logger .severe( "problem reading user preferences: " + t.getMessage() ); } } Properties defaults = new Properties(); String defaultRsrc = "org/vorthmann/zome/app/defaultPrefs.properties"; try { ClassLoader cl = ApplicationUI.class.getClassLoader(); InputStream in = cl.getResourceAsStream( defaultRsrc ); if ( in != null ) defaults .load( in ); // override the core defaults } catch ( IOException ioe ) { logger.severe( "problem reading default preferences: " + defaultRsrc ); } // last-wins, so getProperty() will show command-line args overriding user prefs, which override built-in defaults properties .putAll( defaults ); properties .putAll( userPreferences ); properties .putAll( commandLineArgs ); // This seems to get rid of the "white-out" problem on David's (Windows) computer. // Otherwise it shows up spuratically, but still frequently. // It is usually, but not always, triggered by such events as context menus, // tool tips, or modal dialogs being rendered on top of the main frame. // Since the problem has not been reported elsewhere, this fix will be configurable, rather than hard coded. final String NOERASEBACKGROUND = "sun.awt.noerasebackground"; if( propertyIsTrue(NOERASEBACKGROUND)) { // if it's set to true in the prefs file or command line System.setProperty(NOERASEBACKGROUND, "true"); // then set the System property so the AWT/Swing components will use it. logger .config( NOERASEBACKGROUND + " is set to 'true'." ); } final FailureChannel failures = new FailureChannel() { @Override public void reportFailure( Failure f ) { mErrors.reportError( USER_ERROR_CODE, new Object[] { f } ); } }; modelApp = new com.vzome.core.editor.Application( true, failures, properties ); Colors colors = modelApp .getColors(); { boolean useEmissiveColor = ! propertyIsTrue( "no.glowing.selection" ); // need this set up before we do any loadModel String factoryName = getProperty( "RenderingViewer.Factory.class" ); if ( factoryName == null ) factoryName = "org.vorthmann.zome.render.java3d.Java3dFactory"; try { Class<?> factoryClass = Class.forName( factoryName ); Constructor<?> constructor = factoryClass .getConstructor( new Class<?>[] { Colors.class, Boolean.class } ); rvFactory = (RenderingViewer.Factory) constructor.newInstance( new Object[] { colors, useEmissiveColor } ); } catch ( Exception e ) { mErrors.reportError( "Unable to instantiate RenderingViewer.Factory class: " + factoryName, new Object[] {} ); System.exit( 0 ); } } RenderedModel model; AlgebraicField field = modelApp .getField( "golden" ); Symmetry symmetry = field .getSymmetry( "icosahedral" ); { if ( propertyIsTrue( "rzome.trackball" ) ) model = loadModelPanels( "org/vorthmann/zome/app/rZomeTrackball-vef.vZome" ); else if ( userHasEntitlement( "developer.extras" ) ) model = loadModelPanels( "org/vorthmann/zome/app/icosahedral-developer.vZome" ); else model = loadModelPanels( "org/vorthmann/zome/app/icosahedral-vef.vZome" ); mSymmetryModels.put( symmetry, model ); } symmetry = field .getSymmetry( "octahedral" ); { // this has to happen after the OctahedralSymmetry constructor, which registers with the field model = loadModelPanels( "org/vorthmann/zome/app/octahedral-vef.vZome" ); mSymmetryModels.put( symmetry, model ); } field = modelApp .getField( "snubDodec" ); symmetry = field .getSymmetry( "icosahedral" ); { model = loadModelPanels( "org/vorthmann/zome/app/icosahedral-vef.vZome" ); mSymmetryModels.put( symmetry, model ); } field = modelApp .getField( "heptagon" ); symmetry = field .getSymmetry( "heptagonal antiprism" ); { model = loadModelPanels( "org/vorthmann/zome/app/heptagonal antiprism.vZome" ); mSymmetryModels.put( symmetry, model ); model = loadModelPanels( "org/vorthmann/zome/app/octahedral-vef.vZome" ); symmetry = field .getSymmetry( "triangular antiprism" ); mSymmetryModels.put( symmetry, model ); symmetry = field .getSymmetry( "octahedral" ); mSymmetryModels.put( symmetry, model ); } field = modelApp .getField( "rootTwo" ); symmetry = field .getSymmetry( "octahedral" ); { model = loadModelPanels( "org/vorthmann/zome/app/octahedral-vef.vZome" ); mSymmetryModels.put( symmetry, model ); symmetry = field .getSymmetry( "synestructics" ); mSymmetryModels.put( symmetry, model ); } field = modelApp .getField( "rootThree" ); symmetry = field .getSymmetry( "octahedral" ); { // yes, reusing the model from above mSymmetryModels.put( symmetry, model ); } symmetry = field .getSymmetry( "dodecagonal" ); { model = loadModelPanels( "org/vorthmann/zome/app/dodecagonal.vZome" ); mSymmetryModels.put( symmetry, model ); } // addStyle( new ModeledShapes( "pentagonal", "pentagonal prismatic", DecagonSymmetry.INSTANCE ) ); long endtime = System.currentTimeMillis(); if ( logger .isLoggable( Level .INFO ) ) logger .log(Level.INFO, "ApplicationController initialization in milliseconds: {0}", ( endtime - starttime )); } @Override public void doAction( String action, ActionEvent event ) { try { if ( action .equals( "showAbout" ) || action .equals( "openURL" ) || action .equals( "quit" ) ) { this .ui .actionPerformed( event ); return; } if( "launch".equals(action) ) { String sawWelcome = userPreferences .getProperty( "saw.welcome" ); if ( sawWelcome == null ) { String welcome = properties .getProperty( "welcome" ); doAction( "openResource-" + welcome, null ); userPreferences .setProperty( "saw.welcome", "true" ); FileWriter writer; try { writer = new FileWriter( preferencesFile ); userPreferences .store( writer, "" ); writer .close(); } catch ( IOException e ) { logger.fine(e.toString()); } return; } action = "new"; } if ( "new" .equals( action ) ) { String fieldName = properties .getProperty( "default.field" ); action = "new-" + fieldName; } if ( action .startsWith( "new-" ) ) { String fieldName = action .substring( "new-" .length() ); File prototype = new File( Platform .getPreferencesFolder(), "Prototypes/" + fieldName + ".vZome" ); if ( prototype .exists() ) { logger.log(Level.CONFIG, "Loading default template from {0}", prototype.getCanonicalPath()); doFileAction( "newFromTemplate", prototype ); } else { // creating a new Document Properties docProps = new Properties(); docProps .setProperty( "new.document", "true" ); DocumentModel document = modelApp .createDocument( fieldName ); String title = "Untitled " + ++lastUntitled; docProps .setProperty( "window.title", title ); docProps .setProperty( "edition", this .properties .getProperty( "edition" ) ); docProps .setProperty( "version", this .properties .getProperty( "version" ) ); docProps .setProperty( "buildNumber", this .properties .getProperty( "buildNumber" ) ); docProps .setProperty( "gitCommit", this .properties .getProperty( "gitCommit" ) ); DocumentController newest = new DocumentController( document, this, docProps ); newDocumentController( title, newest ); } } else if ( action .startsWith( "openResource-" ) ) { Properties docProps = new Properties(); docProps .setProperty( "reader.preview", "true" ); String path = action .substring( "openResource-" .length() ); docProps .setProperty( "window.title", path ); ClassLoader cl = Thread .currentThread() .getContextClassLoader(); InputStream bytes = cl .getResourceAsStream( path ); loadDocumentController( path, bytes, docProps ); } else if ( action .startsWith( "openURL-" ) ) { Properties docProps = new Properties(); docProps .setProperty( "as.template", "true" ); String path = action .substring( "openURL-" .length() ); docProps .setProperty( "window.title", path ); if ( path .toLowerCase() .endsWith( ".vzome" ) ) { URI uri = new URI( path ); URL url = uri .toURL(); InputStream bytes = url .openStream(); loadDocumentController( path, bytes, docProps ); } } else { this .mErrors .reportError( UNKNOWN_ACTION, new Object[]{ action } ); } } catch ( Exception e ) { this .mErrors .reportError( UNKNOWN_ERROR_CODE, new Object[]{ e } ); } } @Override public void doFileAction( String command, File file ) { if ( file != null ) { Properties docProps = new Properties(); String path = file .getAbsolutePath(); docProps .setProperty( "window.title", path ); switch ( command ) { case "open": docProps .setProperty( "window.file", path ); break; case "newFromTemplate": String title = "Untitled " + ++lastUntitled; docProps .setProperty( "window.title", title ); // override the default above docProps .setProperty( "as.template", "true" ); // don't set window.file! break; case "openDeferringRedo": docProps .setProperty( "open.undone", "true" ); docProps .setProperty( "window.file", path ); break; default: this .mErrors .reportError( UNKNOWN_ACTION, new Object[]{ command } ); return; } try { InputStream bytes = new FileInputStream( file ); loadDocumentController( path, bytes, docProps ); } catch ( Exception e ) { this .mErrors .reportError( UNKNOWN_ERROR_CODE, new Object[]{ e } ); } } } private void loadDocumentController( final String name, final InputStream bytes, final Properties properties ) throws Exception { DocumentModel document = modelApp .loadDocument( bytes ); DocumentController newest = new DocumentController( document, ApplicationController.this, properties ); newDocumentController( name, newest ); } RenderingViewer.Factory getJ3dFactory() { return rvFactory; } @Override public boolean userHasEntitlement( String propName ) { switch ( propName ) { case "save.files": return getProperty( "licensed.user" ) != null; case "all.tools": return propertyIsTrue( "entitlement.all.tools" ); case "developer.extras": return getProperty( "vZomeDeveloper" ) != null; default: // TODO make this work more like developer.extras return propertyIsTrue( "entitlement." + propName ); // this IS the backstop controller, so no purpose in calling super } } @Override public final String getProperty( String propName ) { switch ( propName ) { case "formatIsSupported": return "true"; case "untitled.title": return "Untitled " + ++lastUntitled; case "coreVersion": return this .modelApp .getCoreVersion(); default: if ( propName .startsWith( "field.label." ) ) { String fieldName = propName .substring( "field.label." .length() ); // TODO implement AlgebraicField.getLabel() switch ( fieldName ) { case "golden": return "Zome (Golden)"; case "rootTwo": return "\u221A2"; case "rootThree": return "\u221A3"; case "heptagon": return "Heptagon"; case "snubDodec": return "Snub Dodec"; default: return fieldName; } } if ( propName .startsWith( "enable." ) && propName .endsWith( ".field" ) ) { String fieldName = propName .substring( "enable." .length() ); fieldName = fieldName .substring( 0, fieldName .lastIndexOf( ".field" ) ); switch ( fieldName ) { case "golden": return "false"; // this one is forcibly enabled by the menu, and we don't want it listed twice case "dodecagon": return "false"; // this is just an alias for rootThree default: // fall through } if ( getProperty( "vZomeDeveloper" ) != null ) return "true"; // developer sees all available fields switch ( fieldName ) { case "rootTwo": case "rootThree": case "heptagon": return "true"; // these are enabled for everyone default: // fall through, see if it is explicitly set } } return properties .getProperty( propName ); } } @Override public Controller getSubController( final String name ) { return docControllers .get( name ); } private void newDocumentController( final String name, final DocumentController newest ) { this .registerDocumentController( name, newest ); // trigger window creation in the UI this .properties() .firePropertyChange( "newDocument", null, newest ); } private void registerDocumentController( final String name, final DocumentController newest ) { this .docControllers .put( name, newest ); newest .addPropertyListener( new PropertyChangeListener() { @Override public void propertyChange( PropertyChangeEvent evt ) { switch ( evt .getPropertyName() ) { case "name": docControllers .remove( name ); // important to re-register under the new name, AND get a new listener, or removes won't work newest .removePropertyListener( this ); registerDocumentController( (String) evt .getNewValue(), newest ); break; case "visible": if ( Boolean.FALSE .equals( evt .getNewValue() ) ) { docControllers .remove( name ); if ( docControllers .isEmpty() ) // closed the last window, so we're exiting System .exit( 0 ); } break; default: break; } } }); } private RenderedModel loadModelPanels( String path ) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); InputStream bytes = cl.getResourceAsStream( path ); try { DocumentModel document = this .modelApp .loadDocument( bytes ); // a RenderedModel that only creates panels document .setRenderedModel( new RenderedModel( document .getField(), true ) { @Override protected void resetAttributes( RenderedManifestation rm, boolean justShape, Strut strut ) {} @Override protected void resetAttributes(RenderedManifestation rm, boolean justShape, Connector m) {} } .withColorPanels( false ) ); document .finishLoading( false, false ); return document .getRenderedModel(); } catch ( Exception e ) { throw new RuntimeException( e ); } } public Colors getColors() { return this .modelApp .getColors(); } public Exporter3d getExporter( String format ) { return this .modelApp .getExporter( format ); } // public RenderingViewer.Factory getRenderingViewerFactory() // { // return mRVFactory; // } public RenderedModel getSymmetryModel( Symmetry symm ) { return mSymmetryModels.get( symm ); } @Override public String[] getCommandList( String listName ) { if ( listName .startsWith( "fields" ) ) { Set<String> names = modelApp .getFieldNames(); SortedSet<String> sorted = new TreeSet<String>( names ); return sorted .toArray( new String[]{} ); } else if ( listName .startsWith( "symmetries." ) ) { String fieldName = listName.substring( 11 ); AlgebraicField field = modelApp .getField( fieldName ); Symmetry[] symms = field.getSymmetries(); String[] result = new String[symms.length]; for ( int i = 0; i < symms.length; i++ ) result[i] = symms[i].getName(); return result; } return new String[0]; } public Lights getLights() { return modelApp .getLights(); } }