/*
* This file is part of NodeBox.
*
* Copyright (C) 2008 Frederik De Bleser (frederik@pandora.be)
*
* NodeBox 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.
*
* NodeBox 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 NodeBox. If not, see <http://www.gnu.org/licenses/>.
*/
package nodebox.client;
import nodebox.node.NodeLibrary;
import nodebox.node.NodeRepository;
import nodebox.ui.ExceptionDialog;
import nodebox.ui.LastResortHandler;
import nodebox.ui.Platform;
import nodebox.ui.ProgressDialog;
import nodebox.versioncheck.Host;
import nodebox.versioncheck.Updater;
import nodebox.versioncheck.Version;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
public class Application implements Host {
public static final String PREFERENCE_ENABLE_DEVICE_SUPPORT = "NBEnableDeviceSupport";
public static boolean ENABLE_DEVICE_SUPPORT = false;
private static Application instance;
private JFrame hiddenFrame;
private ExamplesBrowser examplesBrowser;
public static Application getInstance() {
return instance;
}
private AtomicBoolean startingUp = new AtomicBoolean(true);
private SwingWorker<Throwable, String> startupWorker;
private Updater updater;
private List<NodeBoxDocument> documents = new ArrayList<NodeBoxDocument>();
private NodeBoxDocument currentDocument;
private NodeRepository systemRepository;
private ProgressDialog startupDialog;
private Version version;
private List<File> filesToLoad = Collections.synchronizedList(new ArrayList<File>());
private Console console = null;
public static final String NAME = "NodeBox";
private static Logger logger = Logger.getLogger("nodebox.client.Application");
private Application() {
instance = this;
initLastResortHandler();
initLookAndFeel();
}
//// Application Load ////
/**
* Starts a SwingWorker that loads the application in the background.
* <p/>
* Called in the event dispatch thread using invokeLater.
*/
private void run() {
showProgressDialog();
startupWorker = new SwingWorker<Throwable, String>() {
@Override
protected Throwable doInBackground() throws Exception {
try {
publish("Starting NodeBox");
initApplication();
checkForUpdates();
} catch (RuntimeException ex) {
return ex;
}
return null;
}
@Override
protected void process(List<String> strings) {
final String firstString = strings.get(0);
startupDialog.setMessage(firstString);
}
@Override
protected void done() {
startingUp.set(false);
startupDialog.setVisible(false);
// See if application startup has generated an exception.
Throwable t;
try {
t = get();
} catch (Exception e) {
t = e;
}
if (t != null) {
ExceptionDialog ed = new ExceptionDialog(null, t);
ed.setVisible(true);
System.exit(-1);
}
if (documents.isEmpty() && filesToLoad.isEmpty()) {
instance.createNewDocument();
} else {
for (File f : filesToLoad) {
openDocument(f);
}
}
}
};
startupWorker.execute();
}
private void initApplication() {
installDefaultExceptionHandler();
setNodeBoxVersion();
createNodeBoxDataDirectories();
applyPreferences();
registerForMacOSXEvents();
initPython();
}
private void installDefaultExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, final Throwable e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ExceptionDialog d = new ExceptionDialog(null, e);
d.setVisible(true);
}
});
}
});
}
private void checkForUpdates() {
updater = new Updater(Application.this);
updater.checkForUpdatesInBackground();
}
/**
* Sets a handler for uncaught exceptions that pops up a message dialog with the exception.
* <p/>
* Called from the constructor, in the main thread.
*/
private void initLastResortHandler() {
Thread.currentThread().setUncaughtExceptionHandler(new LastResortHandler());
}
/**
* Initializes Swing's look and feel to the system native look and feel.
* On Mac, uses the system menu bar.
*/
private void initLookAndFeel() {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {
}
System.setProperty("apple.laf.useScreenMenuBar", "true");
UIManager.put("Table.alternateRowColor", new Color(243, 246, 250));
}
/**
* Shows the progress dialog.
* <p/>
* Called from the run() method (which is called in invokeLater).
*/
private void showProgressDialog() {
startupDialog = new ProgressDialog(null, "Starting " + NAME);
startupDialog.setVisible(true);
}
/**
* Retrieves the NodeBox version number from the version.properties file and sets it in the app.
*
* @throws RuntimeException if we're not able to retrieve the version number. This is fatal.
*/
private void setNodeBoxVersion() throws RuntimeException {
Properties properties = new Properties();
try {
properties.load(new FileInputStream("version.properties"));
version = new Version(properties.getProperty("nodebox.version"));
} catch (IOException e) {
throw new RuntimeException("Could not read NodeBox version file. Please re-install NodeBox.", e);
}
}
/**
* Creates the necessary directories used for storing user scripts and Python libraries.
*
* @throws RuntimeException if we can't create the user directories. This is fatal.
*/
private void createNodeBoxDataDirectories() throws RuntimeException {
Platform.getUserDataDirectory().mkdir();
Platform.getUserScriptsDirectory().mkdir();
Platform.getUserPythonDirectory().mkdir();
}
/**
* Load the preferences and make them available to the Application object.
*/
private void applyPreferences() {
Preferences preferences = Preferences.userNodeForPackage(Application.class);
ENABLE_DEVICE_SUPPORT = Boolean.valueOf(preferences.get(Application.PREFERENCE_ENABLE_DEVICE_SUPPORT, "false"));
}
/**
* Register for special events available on the Mac, such as showing the about screen,
* showing the preferences or double-clicking a file.
*
* @throws RuntimeException if the adapter methods could not be loaded.
*/
private void registerForMacOSXEvents() throws RuntimeException {
if (!Platform.onMac()) return;
try {
// Generate and register the OSXAdapter, passing it a hash of all the methods we wish to
// use as delegates for various com.apple.eawt.ApplicationListener methods
OSXAdapter.setQuitHandler(this, getClass().getDeclaredMethod("quit", (Class[]) null));
OSXAdapter.setAboutHandler(this, getClass().getDeclaredMethod("showAbout", (Class[]) null));
OSXAdapter.setPreferencesHandler(this, getClass().getDeclaredMethod("showPreferences", (Class[]) null));
OSXAdapter.setFileHandler(this, getClass().getDeclaredMethod("readFromFile", String.class));
} catch (Exception e) {
throw new RuntimeException("Error while loading the OS X Adapter.", e);
}
// On the Mac, if all windows are closed the menu bar will be empty.
// To solve this, we create an off-screen window with the same menu bar as visible windows.
hiddenFrame = new JFrame();
hiddenFrame.setJMenuBar(new NodeBoxMenuBar());
hiddenFrame.setUndecorated(true);
hiddenFrame.setSize(0, 0);
hiddenFrame.setLocation(-100, -100);
hiddenFrame.pack();
hiddenFrame.setVisible(true);
}
private void initPython() {
// Actually initializing Python happens in the library.
lookForLibraries();
}
private void lookForLibraries() {
List<NodeLibrary> libraries = new ArrayList<NodeLibrary>();
libraries.add(systemLibrary("math"));
libraries.add(systemLibrary("string"));
libraries.add(systemLibrary("color"));
libraries.add(systemLibrary("list"));
libraries.add(systemLibrary("data"));
libraries.add(systemLibrary("corevector"));
libraries.add(systemLibrary("network"));
if (Application.ENABLE_DEVICE_SUPPORT) {
libraries.add(systemLibrary("device"));
}
systemRepository = NodeRepository.of(libraries.toArray(new NodeLibrary[]{}));
}
private NodeLibrary systemLibrary(String name) {
String fileName = String.format("libraries/%s/%s.ndbx", name, name);
return NodeLibrary.load(new File(fileName), NodeRepository.of());
}
//// Application events ////
public boolean quit() {
// Because documents will disappear from the list once they are closed,
// make a copy of the list.
java.util.List<NodeBoxDocument> documents = new ArrayList<NodeBoxDocument>(getDocuments());
for (NodeBoxDocument d : documents) {
if (!d.close())
return false;
}
System.exit(0);
return true;
}
public void showAbout() {
String javaVersion = System.getProperty("java.runtime.version");
JOptionPane.showMessageDialog(null, NAME + " version " + getVersion() + "\nJava " + javaVersion, NAME, JOptionPane.INFORMATION_MESSAGE);
}
public void showPreferences() {
PreferencePanel preferencePanel = new PreferencePanel(this, getCurrentDocument());
preferencePanel.setLocationRelativeTo(getCurrentDocument());
preferencePanel.setVisible(true);
}
public void showConsole() {
java.awt.Dimension d = new java.awt.Dimension(400, 400);
if (console == null) {
console = new Console();
console.setPreferredSize(d);
console.setSize(d);
console.setLocationRelativeTo(getCurrentDocument());
console.setVisible(true);
}
console.setLocationRelativeTo(getCurrentDocument());
console.setVisible(true);
for (NodeBoxDocument document : documents) {
//document.onConsoleVisibleEvent(true);
}
}
public void hideConsole() {
console.setVisible(false);
onHideConsole();
}
public void onHideConsole() {
for (NodeBoxDocument document : documents) {
//document.onConsoleVisibleEvent(false);
}
}
public boolean isConsoleOpened() {
if (console == null) return false;
return console.isVisible();
}
public void readFromFile(String path) {
// This method looks unused, but is actually called using reflection by the OS X adapter.
// If the application is still starting up, don't open the document immediately but place it in a file loading queue.
if (startingUp.get()) {
filesToLoad.add(new File(path));
} else {
openDocument(new File(path));
}
}
//// Document management ////
public List<NodeBoxDocument> getDocuments() {
return documents;
}
public int getDocumentCount() {
return documents.size();
}
public void removeDocument(NodeBoxDocument document) {
documents.remove(document);
}
public NodeBoxDocument createNewDocument() {
NodeBoxDocument doc = new NodeBoxDocument();
addDocument(doc);
return doc;
}
public NodeBoxDocument openExample(File file) {
NodeBoxDocument doc = openDocument(file);
if (doc != null) {
doc.setNeedsResave(true);
}
return doc;
}
public NodeBoxDocument openDocument(File file) {
// Check if the document is already open.
String path;
try {
path = file.getCanonicalPath();
for (NodeBoxDocument doc : Application.getInstance().getDocuments()) {
try {
if (doc.getDocumentFile() == null) continue;
if (doc.getDocumentFile().getCanonicalPath().equals(path)) {
// The document is already open. Bring it to the front.
doc.toFront();
doc.requestFocus();
NodeBoxMenuBar.addRecentFile(file);
return doc;
}
} catch (IOException e) {
logger.log(Level.WARNING, "The document " + doc.getDocumentFile() + " refers to path with errors", e);
}
}
} catch (IOException e) {
logger.log(Level.WARNING, "The document " + file + " refers to path with errors", e);
}
try {
NodeBoxDocument doc = NodeBoxDocument.load(file);
addDocument(doc);
NodeBoxMenuBar.addRecentFile(file);
return doc;
} catch (RuntimeException e) {
logger.log(Level.SEVERE, "Error while loading " + file, e);
ExceptionDialog d = new ExceptionDialog(null, e);
d.setVisible(true);
return null;
}
}
private void addDocument(NodeBoxDocument doc) {
doc.setVisible(true);
doc.requestFocus();
doc.focusNetworkView();
doc.setActiveNetwork("/");
documents.add(doc);
currentDocument = doc;
}
public NodeBoxDocument getCurrentDocument() {
return currentDocument;
}
void setCurrentDocument(NodeBoxDocument document) {
currentDocument = document;
}
public NodeRepository getSystemRepository() {
return systemRepository;
}
public void takeScreenshotOfDocument(String documentFileName) {
File documentFile = new File(documentFileName);
File documentDirectory = documentFile.getParentFile();
String imageName = FileUtils.getBaseName(documentFile.getName()) + ".png";
File screenshotFile = new File(documentDirectory, imageName);
NodeBoxDocument doc = NodeBoxDocument.load(documentFile);
addDocument(doc);
doc.setVisible(true);
doc.takeScreenshot(screenshotFile);
doc.close();
}
public void openExamplesBrowser() {
if (examplesBrowser == null) {
examplesBrowser = new ExamplesBrowser();
}
examplesBrowser.setVisible(true);
examplesBrowser.toFront();
}
//// Host implementation ////
public String getName() {
return "NodeBox";
}
public URL getIconFile() {
return Application.class.getResource("/application-logo.png");
}
public Version getVersion() {
return version;
}
public String getAppcastURL() {
StringBuilder b = new StringBuilder("https://secure.nodebox.net/app/nodebox/appcast.xml");
b.append("?v=");
b.append(getVersion().toString());
b.append("&p=");
b.append(Platform.current_platform);
return b.toString();
}
public Updater getUpdater() {
return updater;
}
public static void main(String[] args) {
final Application app = new Application();
if (args.length == 2 && args[0].equals("--screenshot")) {
String fileName = args[1];
app.initApplication();
app.takeScreenshotOfDocument(fileName);
app.quit();
} else {
if (args.length == 1)
app.filesToLoad.add(new File(args[0]));
SwingUtilities.invokeLater(new Runnable() {
public void run() {
app.run();
}
});
}
}
}