/* * Copyright 2011 Christian Thiemann <christian@spato.net> * Developed at Northwestern University <http://rocs.northwestern.edu> * * This file is part of the SPaTo Visual Explorer (SPaTo). * * SPaTo 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. * * SPaTo 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 SPaTo. If not, see <http://www.gnu.org/licenses/>. */ package net.spato.sve.app; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Frame; import java.awt.event.KeyEvent; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.prefs.Preferences; import javax.swing.JFrame; import net.spato.sve.app.data.*; import net.spato.sve.app.layout.*; import net.spato.sve.app.platform.*; import net.spato.sve.app.util.*; import processing.core.PApplet; import processing.pdf.PGraphicsPDF; import processing.xml.XMLElement; import de.cthiemann.tGUI.*; @SuppressWarnings("serial") public class SPaTo_Visual_Explorer extends PApplet { public static final String VERSION = "1.2.3_01"; public static final String VERSION_DEBUG = "beta"; public static final String VERSION_TIMESTAMP = "20110831T160000"; public static final String VERSION_DATE = new SimpleDateFormat("MMMM d, yyyy", Locale.US).format(parseISO8601(VERSION_TIMESTAMP)); public ExecutorService worker = Executors.newSingleThreadExecutor(); // FIXME: public? public Preferences prefs = Preferences.userRoot().node("/net/spato/SPaTo_Visual_Explorer"); // FIXME: should not be public public boolean canHandleOpenFileEvents = false; // indicates that GUI and workspace are ready to open files // FIXME: get rid of this variable protected String cmdLineArgs[] = new String[0]; float t, tt, dt; // this frame's time, last frame's time, and delta between the two boolean resizeRequest = false; int resizeWidth, resizeHeight; PlatformMagic platformMagic = null; public DataTransferHandler dataTransferHandler = null; // FIXME: public? public Workspace workspace = null; // FIXME: public? public SPaToDocument doc = null; // FIXME: get rid of this variable public SPaToGUI gui = null; // FIXME: public? public TConsole console; // FIXME: get rid of this variable public void setup() { randomSeed(second() + 60*minute() + 3600*hour()); platformMagic = PlatformMagic.createInstance(this, cmdLineArgs); workspace = new Workspace(this); checkForUpdates(false); // setup window setupWindow(); // setup GUI gui = new SPaToGUI(this); console = gui.console; gui.update(); // go smooth(); tt = millis()/1000.f; if (prefs.getBoolean("workspace.auto-recover", false)) workspace.replaceWorkspace(XMLElement.parse(prefs.get("workspace", "<workspace />"))); canHandleOpenFileEvents = true; // FIXME platformMagic.ready(); } public void setupWindow() { frame.setTitle("SPaTo Visual Explorer " + VERSION + ((VERSION_DEBUG.length() > 0) ? " (" + VERSION_DEBUG + ")" : "")); // set window size int w = prefs.getInt("window.width", 1280); // technically, that's not the window size int h = prefs.getInt("window.height", 720); // but the size of the PApplet (but what the heck...) if (w > screenWidth) { w = screenWidth; h = 9*w/16; } if (h > screenHeight) { h = round(0.9f*screenHeight); w = 16*h/9; } size(w, h); // set window location int x = prefs.getInt("window.posx", screenWidth/2 - w/2); int y = prefs.getInt("window.posy", screenHeight/2 - h/2); x = max(x, 0); y = max(y, 0); x = min(x, screenWidth - frame.getWidth()); y = min(y, screenHeight - frame.getHeight()); frame.setLocation(x, y); // set up event handlers frame.setResizable(true); for (java.awt.event.ComponentListener cl : getComponentListeners()) if (cl.getClass().getName().startsWith("processing.core.PApplet")) removeComponentListener(cl); // ouch, all of this is so hacked... addComponentListener(new java.awt.event.ComponentAdapter() { public void componentResized(java.awt.event.ComponentEvent e) { java.awt.Component comp = e.getComponent(); // ensure minimum size int cw = comp.getWidth(), ch = comp.getHeight(), fw = frame.getWidth(), fh = frame.getHeight(); int minWidth = 500, minHeight = 500; if ((cw < minWidth) || (ch < minHeight)) { if (cw < minWidth) { fw += (minWidth - cw); cw = minWidth; } if (ch < minHeight) { fh += (minHeight - ch); ch = minHeight; } comp.setPreferredSize(new Dimension(cw, ch)); frame.pack(); } // request applet resizing (duplicating code from PApplet) resizeRequest = true; resizeWidth = cw; resizeHeight = ch; // save new size in preferences prefs.putInt("window.width", cw); prefs.putInt("window.height", ch); } }); frame.addComponentListener(new java.awt.event.ComponentAdapter() { public void componentMoved(java.awt.event.ComponentEvent e) { prefs.putInt("window.posx", e.getComponent().getLocation().x); prefs.putInt("window.posy", e.getComponent().getLocation().y); } }); addMouseWheelListener(new java.awt.event.MouseWheelListener() { public void mouseWheelMoved(java.awt.event.MouseWheelEvent evt) { mouseWheel(evt.getWheelRotation()); } }); } public void focusGained() { loop(); } public void focusLost() { if (!fireworks) noLoop(); } public void draw() { if (resizeRequest) { resizeRenderer(resizeWidth, resizeHeight); resizeRequest = false; } // handle resize (duplicated from PApplet.run()) t = millis()/1000.f; dt = t - tt; tt = t; if (fireworks && fw.alpha == 1) { fw.draw(); return; } // draw background(255); if (doc != null) doc.view.draw(g); gui.update(true); // fireworks transition drawing and finishing if (fireworks) { fw.draw(); if (fw.finished && (fw.alpha == 0)) disposeFireworks(); } } boolean dragged = false; public void mouseReleased() { if (fireworks) return; if (dragged) { dragged = false; return; } if (mouseEvent.isConsumed()) return; if (doc == null) return; switch (mouseButton) { case LEFT: doc.view.setRootNode(doc.view.ih); break; case RIGHT: if ((doc.view.viewMode == SPaToView.VIEW_MAP) && gui.btnTom.isEnabled()) gui.btnTom.handleMouseClicked(); else if ((doc.view.viewMode == SPaToView.VIEW_TOM) && gui.btnMap.isEnabled()) gui.btnMap.handleMouseClicked(); } } public void mouseDragged() { if (fireworks) return; dragged = true; if (mouseEvent.isConsumed()) return; if (doc == null) return; if (mouseButton == LEFT) { doc.view.xoff[doc.view.viewMode] += mouseX - pmouseX; doc.view.yoff[doc.view.viewMode] += mouseY - pmouseY; } else if (mouseButton == RIGHT) { float refX = width/2 + doc.view.xoff[doc.view.viewMode]; float refY = height/2 + doc.view.yoff[doc.view.viewMode]; changeZoom(dist(refX, refY, mouseX, mouseY)/dist(refX, refY, pmouseX, pmouseY)); } } boolean isAltDown = false; public void keyPressed() { if (fireworks) { if (keyCode == ESC) disposeFireworks(); return; } isAltDown = keyEvent.isAltDown(); if (keyEvent.isConsumed()) return; if (keyEvent.isControlDown() || keyEvent.isMetaDown()) { if (keyEvent.isAltDown()) { // FIXME: All this could be handled by TPopupMenu switch (keyEvent.getKeyCode()) { case KeyEvent.VK_O: gui.actionPerformed("workspace##open"); return; case KeyEvent.VK_S: if (workspace.docs.size() > 0) gui.actionPerformed("workspace##save" + (keyEvent.isShiftDown() ? "As" : "")); return; case KeyEvent.VK_F: startFireworks(); return; } return; } switch (keyEvent.getKeyCode()) { case KeyEvent.VK_N: gui.actionPerformed("document##new"); return; case KeyEvent.VK_O: gui.actionPerformed("document##open"); return; case KeyEvent.VK_S: if (doc != null) gui.actionPerformed("document##save" + (keyEvent.isShiftDown() ? "As" : "")); return; case KeyEvent.VK_W: if (doc != null) gui.actionPerformed("document##close"); return; } return; } switch (key) { case '{': SPaToView.linkLineWidth = max(0.25f, SPaToView.linkLineWidth - 0.25f); console.logNote("Link line width is now " + SPaToView.linkLineWidth); return; case '}': SPaToView.linkLineWidth = min(1, SPaToView.linkLineWidth + 0.25f); console.logNote("Link line width is now " + SPaToView.linkLineWidth); return; } if (doc == null) return; if (keyEvent.isAltDown()) switch (keyCode) { case 91/*{*/: doc.view.nodeSizeFactor = max(0.05f, doc.view.nodeSizeFactor/1.5f); console.logNote("Node size factor is now " + doc.view.nodeSizeFactor); return; case 93/*}*/: doc.view.nodeSizeFactor = min(2.00f, doc.view.nodeSizeFactor*1.5f); console.logNote("Node size factor is now " + doc.view.nodeSizeFactor); return; } switch (key) { case '=': doc.view.zoom[doc.view.viewMode] = 1; doc.view.xoff[doc.view.viewMode] = doc.view.yoff[doc.view.viewMode] = 0; return; case '[': changeZoom(.5f); return; case ']': changeZoom(2); return; case 's': new Screenshot(this).showSettingsWindow(); return; case 'S': new Screenshot(this).save(); return; case 'r': // random walk //if (doc.view.hasLinks && (doc.view.links.index[doc.view.r].length > 0)) // doc.view.setRootNode(doc.view.links.index[doc.view.r][floor(random(doc.view.links.index[doc.view.r].length))]); //else doc.view.setRootNode(floor(random(doc.view.NN))); return; case '@': if (doc.view.hasNodes && (doc.view.ih != -1)) doc.view.nodes[doc.view.ih].showLabel = !doc.view.nodes[doc.view.ih].showLabel; return; } if (doc.getAlbum() != null) switch (key) { case 'A': doc.setSelectedSnapshot(doc.getAlbum(), -1, true); gui.updateAlbumControls(); return; case 'a': doc.setSelectedSnapshot(doc.getAlbum(), +1, true); gui.updateAlbumControls(); return; } if ((doc.getSelectedQuantity() != null) && (doc.getSelectedSnapshot(doc.getSelectedQuantity()) != doc.getSelectedQuantity())) switch (key) { case '<': doc.setSelectedSnapshot(doc.getSelectedQuantity(), -10, true); return; case '>': doc.setSelectedSnapshot(doc.getSelectedQuantity(), +10, true); return; } } public void keyReleased() { isAltDown = keyEvent.isAltDown(); } // Overriding exit() to prevent program being closed by ESC or Ctrl/Cmd-W. This is somewhat stupidly hacked... public void exit() { if ((keyEvent != null) && ((key == KeyEvent.VK_ESCAPE) || ((keyEvent.getModifiers() == MENU_SHORTCUT) && (keyEvent.getKeyCode() == 'W')))) return; // looks like exit() was called upon hitting these keys; but we don't want that super.exit(); } public void mouseWheel(int delta) { if (fireworks) return; if ((gui.componentAtMouse == null) && (gui.componentMouseClicked == null)) changeZoom((delta < 0) ? (1 - .05f*delta) : 1/(1 + .05f*delta)); } public void changeZoom(float f) { if (doc == null) return; f = max(f, 1/doc.view.zoom[doc.view.viewMode]); // do not allow zoom < 1 f = min(f, 10/doc.view.zoom[doc.view.viewMode]); // do not allow zoom > 10 doc.view.zoom[doc.view.viewMode] *= f; doc.view.xoff[doc.view.viewMode] *= f; doc.view.yoff[doc.view.viewMode] *= f; } public void actionPerformed(String cmd) { gui.actionPerformed(cmd); } public void checkForUpdates(boolean force) { // on first start, ask user for permission to check for updates if (prefs.get("update.check", null) == null) { int res = javax.swing.JOptionPane.showConfirmDialog(frame, "Do you want me to automatically check for updates?", "SPaTo Updater", javax.swing.JOptionPane.YES_NO_OPTION); prefs.putBoolean("update.check", res == javax.swing.JOptionPane.YES_OPTION); } // check for updates if requested if (force) prefs.remove("update.skip"); if (prefs.getBoolean("update.check", true) || force) new Updater(this, force).start(); } public boolean fireworks = false; // FIXME: public? Fireworks fw = null; public void startFireworks() { gui.setVisible(false); gui.setEnabled(false); fw = new Fireworks(this); fireworks = true; fw.setup(); } public void disposeFireworks() { fireworks = false; fw = null; gui.setEnabled(true); gui.setVisible(true); } public static Date parseISO8601(String timestamp) { try { return new SimpleDateFormat("yyyyMMdd'T'HHmmss").parse(timestamp); } catch (Exception e) { e.printStackTrace(); return null; } } // FIXME: get rid of this function public Frame getParentFrame() { checkParentFrame(); return parentFrame; } public JFrame jframe = null; // FIXME: should not be public // overriding main() to create the applet within a JFrame (needed to do much of the Mac magic). public static void main(String args[]) { if (args.length > 0) { println("cmd line args:"); for (int i = 0; i < args.length; i++) println(" [" + i + "] " + args[i]); } SPaTo_Visual_Explorer applet = new SPaTo_Visual_Explorer(); applet.cmdLineArgs = args; if (platform == MACOSX) { // Wrap the applet into a JFrame for maximum MacMagic! // setup various stuff (see PApplet.runSketch) applet.sketchPath = System.getProperty("user.dir"); applet.args = args; // setup shiny JFrame applet.frame = applet.jframe = new JFrame(); applet.jframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); applet.jframe.setLayout(new BorderLayout()); applet.jframe.add(applet, BorderLayout.CENTER); // ... and go! applet.init(); while (applet.defaultSize && !applet.finished) // see PApplet.runSketch try { Thread.sleep(5); } catch (InterruptedException e) {} applet.jframe.pack(); if (applet.displayable()) applet.jframe.setVisible(true); applet.requestFocus(); } else { // pass the constructed applet to PApplet.runSketch PApplet.runSketch(new String[] { "SPaTo_Visual_Explorer" }, applet); } } }