/** * */ package photoSpread; import inputOutput.InputOutput; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ComponentEvent; import java.beans.PropertyVetoException; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import javax.naming.InvalidNameException; import javax.swing.JFrame; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import photoSpread.PhotoSpreadException.IllegalArgumentException; import photoSpread.PhotoSpreadException.IllegalPreferenceException; import photoSpread.PhotoSpreadException.IllegalPreferenceValueException; import photoSpread.PhotoSpreadException.NotImplementedException; import photoSpreadObjects.PhotoSpreadTableObject; import photoSpreadObjects.photoSpreadComponents.Workspace; import photoSpreadTable.PhotoSpreadTable; import photoSpreadTable.PhotoSpreadTableMenu; import photoSpreadUtilities.Misc; import photoSpreadUtilities.PhotoSpreadProperties; /** * @author paepcke * * Main class for starting the application * Find user preferences and kick things off. * Prefs may come from a preferences file or * from command line options. The preferences file * is by default expected in: * * 1. The command line as prefsFile=<filePath> * 2. $HOME/_photoSpread/photoSpreadPrefs.cnf * * It's OK if no file is found. Defaults take over * * To add a new preference to the system: * * - Add entry to legalPreferenceAttrNames * - Add a key strings for the preferences properties data structure. * (see all the others. They all end with 'Key'. * - Add a default value for the new preference in initDefaultProperties() * - Add a case statement in validatePref() * * In the program the preferences can be accessed directly * via userPrefs.getProperty(<propKey>). The values are all * strings. Convenience are useful for values that need * to be converted to non-String types: * * - Misc.getPrefInt(<propKey>) */ /** * @author paepcke * */ public class PhotoSpread { // NOTE: Change this for new versions: public static String version = "V7Alpha4"; public static enum DebugLevel { DEBUG, NO_DEBUG } public static DebugLevel currDebugLevel = DebugLevel.NO_DEBUG; private static boolean _DnDInProgress = false; private static JFrame _currentSheetWindow = null; private static Workspace _currentWorkspace = null; private static Component _defaultGlassPane = null; // List of all legal, but currently unimplemented // attribute names: protected static enum unimplementedPrefs { workspaceObjHeight, workspaceFoo; public static boolean hasMember (String pref) { try { valueOf(pref); } catch (java.lang.IllegalArgumentException e) { return false; } return true; } } // List of all legal --pref attribute names. Used in // validatePrefs() to check legality of a commandline // or preferences file attr/value pair: protected static enum legalPreferenceAttrNames { sheetSize, sheetNumCols, sheetNumRows, sheetRowHeightMin, sheetColWidthMin, sheetObjsInCell, sheetCellObjsWidth, workspaceSize, workspaceNumCols, workspaceHGap, workspaceVGap, workspaceObjWidth, workspaceObjHeight, workspaceMaxObjWidth, workspaceMaxObjHeight, editorSize, formulaEditorStripSize, dragGhostSize, resourcePaths, csvFieldDelimiter, prefsFile, metaDataEditorSize, lastDirWritten, lastDirRead } // Key strings used for the preferences properties data structure: public static final String prefsFileKey = "prefsFile"; public static final String csvFieldDelimiterKey = "csvFieldDelimiter"; public static final String lastDirWrittenKey = "lastDirWritten"; public static final String lastDirReadKey = "lastDirRead"; public static final String workspaceSizeKey = "workspaceSize"; public static final String editorSizeKey = "editorSize"; public static final String sheetSizeKey = "sheetSize"; public static final String formulaEditorStripSizeKey = "formulaEditorStripSize"; public static final String dragGhostSizeKey = "dragGhostSize"; public static final String sheetRowHeightMinKey = "sheetRowHeightMin"; public static final String sheetColWidthMinKey = "sheetColWidthMin"; public static final String sheetObjsInCellKey = "sheetObjsInCell"; public static final String sheetCellObjsWidthKey = "sheetCellObjsWidth"; public static final String sheetNumColsKey = "sheetNumCols"; public static final String sheetNumRowsKey = "sheetNumRows"; public static final String workspaceNumColsKey = "workspaceNumCols"; public static final String workspaceHGapKey = "workspaceHGap"; public static final String workspaceVGapKey = "workspaceVGap"; public static final String workspaceObjWidthKey = "workspaceObjWidth"; public static final String workspaceObjHeightKey = "workspaceObjHeight"; public static final String workspaceMaxObjWidthKey = "workspaceMaxObjWidth"; public static final String workspaceMaxObjHeightKey = "workspaceMaxObjHeight"; public static String propertySeparator = "="; public static String prefsFileExtension = "cnf"; // Default and user preferences. Specify enough entries that // no rehashing is required. I experienced a bug with Properties // when their hashtables rehashed: public static PhotoSpreadProperties<String, String> photoSpreadDefaults = new PhotoSpreadProperties<String, String>(100); public static PhotoSpreadProperties<String, String> photoSpreadPrefs = new PhotoSpreadProperties<String, String>(photoSpreadDefaults, 100); private static String photoSpreadPrefsDir = "_photoSpread" + System.getProperty("file.separator"); private static String photoSpreadPrefsFileName = "photoSpreadPrefs." + prefsFileExtension; private static String prefsFilePath; /**************************************************** * Class StartupErrorPanel *****************************************************/ static class StartupErrorPanel extends JFrame { private static final long serialVersionUID = 1L; public StartupErrorPanel (String errMsg) { setVisible(true); Misc.showErrorMsg(errMsg); dispose(); } } /**************************************************** * Static Getters/Setters *****************************************************/ public static Component getDefaultGlassPane() { return _defaultGlassPane; } public static JFrame getCurrentSheetWindow () { return _currentSheetWindow; } public static void setCurrentSheetWindow(JFrame _currentSheetWindow) { PhotoSpread._currentSheetWindow = _currentSheetWindow; } public static void setCurrentWorkspaceWindow (Workspace currWorkspace) { _currentWorkspace = currWorkspace; } public static Workspace getCurrentWorkspaceWindow () { return _currentWorkspace; } /** * Initialize the user preferences defaults. */ //****** Change back to private after JUnit test is done public static void initDefaultProperties() { // Set all the defaults in the separate defaults properties. // They will be used if the program asks for some property // that's not set: photoSpreadDefaults.setProperty(csvFieldDelimiterKey, ","); photoSpreadDefaults.setProperty(lastDirWrittenKey, System.getProperty("user.dir")); photoSpreadDefaults.setProperty(lastDirReadKey, System.getProperty("user.dir")); photoSpreadDefaults.setProperty(editorSizeKey, "830 940"); photoSpreadDefaults.setProperty(sheetSizeKey, "650 500"); photoSpreadDefaults.setProperty(formulaEditorStripSizeKey, "400 30"); photoSpreadDefaults.setProperty(dragGhostSizeKey, "50 50"); photoSpreadDefaults.setProperty(sheetRowHeightMinKey, "60"); photoSpreadDefaults.setProperty(sheetColWidthMinKey, "80"); photoSpreadDefaults.setProperty(sheetObjsInCellKey, "10"); photoSpreadDefaults.setProperty(sheetCellObjsWidthKey, "50"); photoSpreadDefaults.setProperty(sheetNumColsKey, "8"); photoSpreadDefaults.setProperty(sheetNumRowsKey, "8"); photoSpreadDefaults.setProperty(workspaceNumColsKey, "1"); photoSpreadDefaults.setProperty(workspaceHGapKey, "2"); photoSpreadDefaults.setProperty(workspaceVGapKey, "2"); photoSpreadDefaults.setProperty(workspaceObjWidthKey, "500"); // For now we make all items square: photoSpreadDefaults.setProperty(workspaceObjHeightKey, photoSpreadDefaults.getProperty(workspaceObjWidthKey)); //photoSpreadDefaults.setProperty(workspaceObjHeightKey, "500"); photoSpreadDefaults.setProperty(workspaceMaxObjWidthKey, "500"); photoSpreadDefaults.setProperty(workspaceMaxObjHeightKey, "500"); photoSpreadDefaults.setProperty(workspaceSizeKey, "500 500"); } /**************************************************** * Getter/Setter(s) *****************************************************/ public static void setDnDInProgress(boolean dnDInProgress) { _DnDInProgress = dnDInProgress; } public static boolean isDnDInProgress() { return _DnDInProgress; } /**************************************************** * Methods (all static) *****************************************************/ /* * Return false if user just asked for help. Return * true if caller may proceed. */ @SuppressWarnings("static-access") protected static boolean processCommandLineArgs(String[] args) throws PhotoSpreadException.IllegalArgumentException, PropertyVetoException { if ((args == null) || (args.length == 0)) return true; final boolean noArg = false; CommandLine parsedOptions; // Generate an 'Options' instance and // add specifications for each acceptable option. // ...addOption(<shortForm>, <longForm>, <has Arg or not>, <usageDescription> Options cmdLineOptions = new Options(); cmdLineOptions.addOption( "h", "help", noArg, "Print usage description."); // The following code works well for long options: --Foobar, // and you have to use it for options that may occur more than // once. But not for single-letter options... cmdLineOptions.addOption(OptionBuilder .withLongOpt("workspaceSize") .withDescription("Initial size of workspace in pixels \"W H\".") .hasArgs(2) .create()); // create option w/ these above parameters cmdLineOptions.addOption(OptionBuilder .withLongOpt("editorSize") .withDescription("Initial size of editor in pixels: \"W H\".") .hasArgs(2) .create()); // create option w/ these above parameters cmdLineOptions.addOption(OptionBuilder .withLongOpt("metaDataEditorSize") .withDescription("Initial size in pixels of editor window for individual objects (e.g. photos).") .hasArgs(2) // .create()); // create option w/ these above parameters cmdLineOptions.addOption(OptionBuilder .withLongOpt("resourcePaths") .withDescription("Colon-separated paths to PhotoSpread resources (such as photos).") .hasArgs(1) // .create()); // create option w/ these above parameters // Automatically generate a help string from the Options instance: HelpFormatter formatter = new HelpFormatter(); CommandLineParser parser = new GnuParser(); try { // Parse the commandline options: parsedOptions = parser.parse(cmdLineOptions, args); } catch (ParseException e) { // Bad command line option syntax: throw new PhotoSpreadException.IllegalArgumentException("Command line options parsing failed. Reason: " + e.getMessage()); } // Now parsedOptions can be queried for the presence // of options on the command line: if (parsedOptions.hasOption("help")) { formatter.printHelp("PhotoSpread.jar", cmdLineOptions); return false; } if (parsedOptions.hasOption("resourcePaths")) { // TODO: resolve paths here // Get colon-separated list of resource path // String[] rps = parsedOptions.getOptionValues("r"); } // Go through each option object and pull out // the attr/value pairs. Check each for validity: Iterator<Option> it = (Iterator<Option>) parsedOptions.iterator(); ArrayList<String> cleanPrefResult; while (it.hasNext()) { // Get pair [<attrName>, <value>]: cleanPrefResult = cleanCommandLineOption(it.next()); try { validatePref(cleanPrefResult.get(0), cleanPrefResult.get(1)); } catch (PhotoSpreadException.IllegalPreferenceValueException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (NotImplementedException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (IllegalPreferenceException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } // We returned; so all OK. Command-line-specified options // go into the System properties: System.setProperty(cleanPrefResult.get(0), cleanPrefResult.get(1)); } return true; } protected static ArrayList<String> cleanCommandLineOption(Option op) { ArrayList<String> res = new ArrayList<String>(); String argstr = ""; String[] opValues = op.getValues(); res.add(op.getLongOpt()); if (opValues != null) { for (int i=0; i<opValues.length;i++) { argstr = argstr + (i==0 ? "" : " ") + op.getValue(i); } res.add(argstr); } else { res.add(""); } // new StartupErrorPanel("Validate res[0]: '" + res.get(0)+ "'. res[1] '" + res.get(1) + "'."); return res; } /* * Given an attribute name and an attribute value, * check whether the pair is acceptable. */ protected static void validatePref(String attrName, String attrValue) throws NotImplementedException, IllegalPreferenceException, IllegalPreferenceValueException { try { switch (legalPreferenceAttrNames.valueOf(attrName)) { // Legal, but unimplemented PhotoSpread preferences: case workspaceObjHeight: throw new NotImplementedException( "Preference '" + attrName + "' is legal, but not implemented. All Workspace items are square right now, with workspaceObjWidth determining the size."); // Preferences that require a single character: case csvFieldDelimiter: if (attrValue.length() != 1) throw new IllegalPreferenceValueException("Preference '" + attrName + "' requires a single character."); break; // Preferences that take a file or directory name: case lastDirWritten: case lastDirRead: case prefsFile: /* Could check for attrValue being a legal file. But this will happen later when we try to open it. */ break; // Preferences that require one integer: case sheetRowHeightMin: case sheetColWidthMin: case sheetNumRows: case sheetNumCols: case sheetObjsInCell: case sheetCellObjsWidth: case workspaceNumCols: case workspaceHGap: case workspaceVGap: case workspaceObjWidth: case workspaceMaxObjWidth: case workspaceMaxObjHeight: // case workspaceObjHeight: // Not Implemented Integer.parseInt(attrValue); // throws NumberFormatException if not number. break; // Preferences that require two integers: case workspaceSize: case metaDataEditorSize: case editorSize: case sheetSize: case dragGhostSize: // attrValue must be a string of two numbers. // Note: the regular expression "[\s]" should work // and describe white space. But it doesn't: String[] twoNumStrings = attrValue.split("[ \t\n\f\r]"); if (twoNumStrings.length != 2) throw new PhotoSpreadException.IllegalPreferenceValueException( "Preference '" + attrName + " requires two ints. Not: '" + attrValue + "'"); Integer.parseInt(twoNumStrings[0].trim()); // throws NumberFormatException if not numbers. Integer.parseInt(twoNumStrings[1].trim()); break; // Preferences that require a colon-separated set of paths: case resourcePaths: // TODO: check syntax: foo:bar/fum:E:/blue/green only. break; default: return; // We should have either bombed in the valueOf() above, // or we should have covered them all. } } catch (NumberFormatException e) { throw new PhotoSpreadException.IllegalPreferenceValueException("Preference '" + attrName + "' requires integer. Not: '" + attrValue + "'"); } catch (java.lang.IllegalArgumentException e) { /* Thrown by valueOf(attrName) if name is not in the legalPreferenceAttrNames enum. */ throw new PhotoSpreadException.IllegalPreferenceException("Preference '" + attrName + "' is unknown."); } } /* * Once the command line args have been loaded into the * System.properties, we now try to find a preferences * file. */ protected static void initPreferences() throws InvalidNameException, IOException, IllegalArgumentException { // FileInputStream prefsStream; BufferedReader prefsStream; initDefaultProperties(); // Was command line arg included to set the preferences file? prefsFilePath = System.getProperty(prefsFileKey); if (prefsFilePath == null) { // If not: If $HOME available: prefs file is like $HOME/.photoSpread/photoSpreadPrefs.cnf if (System.getenv("HOME") != null) { prefsFilePath = System.getenv("HOME") + System.getProperty("file.separator") + photoSpreadPrefsDir + photoSpreadPrefsFileName; File prefFile = new File(prefsFilePath); if (!prefFile.exists()) // Final fallback: prefs file in dir that program was started with: prefsFilePath = System.getProperty("user.dir") + System.getProperty("file.separator") + photoSpreadPrefsFileName; } } if (prefsFilePath != null) { // Clean up the prefs file path: prefsFilePath = InputOutput.normalizePath(prefsFilePath); photoSpreadPrefs.setProperty(prefsFileKey, prefsFilePath); prefsStream = openPrefsFile(); if (prefsStream != null) { try { // Stock our in-memory preferences from the prefs file: photoSpreadPrefs.load(prefsStream); } catch (java.lang.IllegalArgumentException e) { throw new PhotoSpreadException.IllegalArgumentException("Malformed Unicode in preference file " + prefsFilePath + "."); } catch (IOException e) { throw new IOException("Can open, but not read preference file " + prefsFilePath + "."); } finally { prefsStream.close(); } } // end if } // Check validity of the options that we can check and exit: validatePreferences(); } /** * @return * @throws FileNotFoundException */ protected static BufferedReader openPrefsFile() throws FileNotFoundException { BufferedReader prefsStream; // Try to open the preference file: try { // prefsStream = new FileInputStream(prefsFilePath); prefsStream = new BufferedReader(new FileReader(prefsFilePath)); } catch (FileNotFoundException e) { // No prefs file found (or permissions): prefsStream = null; // If user specified a prefs file explicitly in the command line // then we print an error msg, else we just quietly stick to the defaults: if (System.getProperty(prefsFileKey) != null) throw new FileNotFoundException("Cannot open command-line-specified preference file " + System.getProperty(prefsFileKey) + "."); } // end catch return prefsStream; } public static void savePreferences () throws IOException { FileWriter prefsWriter = null; String prefsFilePath = photoSpreadPrefs.getProperty(prefsFileKey); try { prefsWriter = new FileWriter(photoSpreadPrefs.getProperty(prefsFileKey)); } catch (IOException e) { new IOException("Could not write the preference file " + prefsFilePath + "."); } photoSpreadPrefs.store(prefsWriter, "# Photospread Preferences File\n\n"); } protected static void validatePreferences() { String userPrefKey; String userPrefValue; Enumeration<String> userPrefKeys = photoSpreadPrefs.keys(); while (userPrefKeys.hasMoreElements()) { userPrefKey = (String) userPrefKeys.nextElement(); userPrefValue = photoSpreadPrefs.getProperty(userPrefKey); if (unimplementedPrefs.hasMember(userPrefKey)) continue; try { validatePref(userPrefKey, userPrefValue); } catch (NotImplementedException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (IllegalPreferenceException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (IllegalPreferenceValueException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } } // end while // Check for value errors due to overconstraints: // Sheet width inconsistent with column with * number of columns: Dimension sheetDim = null; try { sheetDim = photoSpreadPrefs.getDimension(sheetSizeKey); int minColWidth = photoSpreadPrefs.getInt(sheetColWidthMinKey); int minRowHeight = photoSpreadPrefs.getInt(sheetRowHeightMinKey); int numCols = photoSpreadPrefs.getInt(sheetNumColsKey); int numRows = photoSpreadPrefs.getInt(sheetNumRowsKey); if ((minColWidth * numCols) > sheetDim.width) { new StartupErrorPanel( "Preference value error: " + sheetColWidthMinKey + " * " + sheetNumColsKey + " must be \nless than or equal to " + sheetDim.width + " (the requested sheet dimension width). \nThat product is " + minColWidth * numCols + ". The sheet width specified in the preferences is " + sheetDim.width + "." ); System.exit(-1); } if ((minRowHeight * numRows) > sheetDim.height) { new StartupErrorPanel( "Preference value error: " + sheetRowHeightMinKey + " * " + sheetNumRowsKey + "\n must be less than or equal to " + sheetDim.height + " (the height specified in " + sheetSizeKey + "). \n But that product is " + minRowHeight * numRows+ ". The sheet height specified in the preferences is " + sheetDim.height + "." ); System.exit(-1); } } catch (NumberFormatException e) { // already checked validity } } public static void trace (String msg) { if (PhotoSpread.currDebugLevel == DebugLevel.DEBUG) System.out.println(msg); } /** * Restores the original glass pane into the sheet window */ public static void restoreGlassPane () { _currentSheetWindow.setGlassPane(_defaultGlassPane); } public static void main(final String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { // TODO **************** Disabled Command Line Arg Checking for JProbe ************ /* try { if (!processCommandLineArgs(args)) System.exit(-1); } catch (IllegalArgumentException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (Exception e) { new StartupErrorPanel(e.getMessage()); // e.printStackTrace(); System.exit(0); } */ // End **************** Disabled Command Line Arg Checking for JProbe ************ // Set up properties file: try { initPreferences(); } catch (InvalidNameException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (IOException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } catch (IllegalArgumentException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } JFrame app = new JFrame(); // Make current sheet window JFrame easy to find: _currentSheetWindow = app; _defaultGlassPane = app.getGlassPane(); app.setTitle("PhotoSpread Sheet " + PhotoSpread.version + " (F1 for Help in all Windows)"); app.setLayout(new BorderLayout()); // Size of sheet: try { app.setMinimumSize(photoSpreadPrefs.getDimension(sheetSizeKey)); } catch (NumberFormatException e) { new StartupErrorPanel(e.getMessage()); System.exit(-1); } PhotoSpreadTableObject tableObject = new PhotoSpreadTableObject((JFrame) app); JPopupMenu.setDefaultLightWeightPopupEnabled(false); PhotoSpreadTableMenu mainMenuBar = new PhotoSpreadTableMenu(tableObject); app.setJMenuBar(mainMenuBar); // Don't know what this size controls: try { // Don't know what this size controls: app.add(tableObject.getObjectComponent(200, 200)); } catch (NumberFormatException e) { new StartupErrorPanel(e.getMessage()); // e.printStackTrace(); System.exit(-1); } // The constructor of the PhotoSpreadTableObject that is invoked // above will set up a confirm dialog for exiting when a window // closing event is generated by the user clicking the Workspace, // or sheet window's X button. The following statement enables these // events to be delivered: app.setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE); // Make cnt-shift-w work. It will put up a confirmation // dialog within the spreadsheet window (therefore the 'app' // parameter in the confirm action creation: Misc.bindKey( app, "control W", new Misc.AppExitWithConfirmAction("Really exit PhotoSpread?", app)); // Center the window on the screen. app.setLocationRelativeTo(null); app.setVisible(true); } }); } }