// Copyright (c) 2006 - 2008, Markus Strauch. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. package net.sf.sdedit.editor; import java.awt.Image; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.Collections; import java.util.LinkedList; import java.util.List; import javax.swing.Action; import net.sf.sdedit.Constants; import net.sf.sdedit.config.Configuration; import net.sf.sdedit.config.ConfigurationManager; import net.sf.sdedit.config.GlobalConfiguration; import net.sf.sdedit.diagram.Diagram; import net.sf.sdedit.drawable.Drawable; import net.sf.sdedit.drawable.Note; //import net.sf.sdedit.editor.apple.AppInstaller; import net.sf.sdedit.server.RealtimeServer; import net.sf.sdedit.text.TextHandler; import net.sf.sdedit.ui.ImagePaintDevice; import net.sf.sdedit.ui.PanelPaintDeviceListener; import net.sf.sdedit.ui.UserInterface; import net.sf.sdedit.ui.UserInterfaceListener; import net.sf.sdedit.ui.components.configuration.Bean; import net.sf.sdedit.ui.components.configuration.ConfigurationAction; import net.sf.sdedit.ui.impl.UserInterfaceImpl; import net.sf.sdedit.util.OS; import net.sf.sdedit.util.Pair; import net.sf.sdedit.util.DocUtil.XMLException; /** * The control class of the Quick Sequence Diagram Editor. * * @author Markus Strauch */ public final class Editor implements Constants, PanelPaintDeviceListener, UserInterfaceListener { private GlobalConfiguration globalConfiguration; private UserInterface ui; private Actions actions; private Engine engine; // Reference to the real-time-server, if one is running, otherwise null private RealtimeServer server; private LinkedList<String> recentFiles; private LinkedList<Action> recentFileActions; // Flag denoting if the application has already been set up. private boolean setup = false; private static Editor instance; public static Editor getEditor() { if (instance == null) { instance = new Editor(); } return instance; } private Editor() { ui = newUI(); // if (OS.TYPE == OS.Type.MAC) { // AppInstaller.installApplication(this); // } recentFiles = new LinkedList<String>(); recentFileActions = new LinkedList<Action>(); globalConfiguration = ConfigurationManager.getGlobalConfiguration(); ui.addListener(this); setupUI(); readRecentFiles(); engine = new Engine(this); if (globalConfiguration.isAutostartServer()) { startServer(globalConfiguration.getRealtimeServerPort()); } setup = true; // if (OS.TYPE == OS.Type.MAC) { // File fileToLoad = AppInstaller.getFileToLoad(); // if (fileToLoad != null) { // try { // loadCode(fileToLoad); // } catch (RuntimeException e) { // throw e; // } catch (Exception e) { // ui.errorMessage("Cannot load " // + fileToLoad.getAbsolutePath() + "\n" // + "due to an exception of type " // + e.getClass().getSimpleName() + "\n" // + "with the message: " + e.getMessage()); // } // } // } } public void startServer(int port) { try { startRealtimeServer(port); System.out.println("Started real-time diagram server @localhost:" + server.getPort()); //ui.message("Started real-time diagram server @localhost:" + server.getPort()); } catch (Exception e) { ui .errorMessage("The real-time diagram server could not be started due to\n" + "an exception of type " + e.getClass().getSimpleName() + "\n" + "with the message: " + e.getMessage()); } } /** * @see net.sf.sdedit.ui.UserInterfaceListener#currentTabClosing() */ public void currentTabClosing() { actions.closeDiagramAction.actionPerformed(null); } /** * @see net.sf.sdedit.ui.UserInterfaceListener#hyperlinkClicked(java.lang.String) */ public void hyperlinkClicked(String hyperlink) { if (hyperlink.startsWith("example:")) { String file = hyperlink.substring(hyperlink.indexOf(':') + 1); actions.getExampleAction(file, file).actionPerformed(null); } else if (hyperlink.startsWith("help:")) { int first = hyperlink.indexOf(':'); int last = hyperlink.lastIndexOf(':'); String title = hyperlink.substring(first + 1, last); String file = hyperlink.substring(last + 1); ui.help(title, "/net/sf/sdedit/help/" + file, false); } } public void error(String msg) { ui.errorMessage(msg); } private void readRecentFiles() { String sep = System.getProperty("path.separator"); String recent = globalConfiguration.getRecentFiles(); if (recent != null && !recent.equals("")) { int i = 0; for (String file : recent.split(sep)) { if (new File(file).exists()) { i++; recentFiles.add(file); Action act = actions.getRecentFileAction(file); recentFileActions.add(act); ui.addAction("&File.Open &recent file", act, null); if (i == globalConfiguration.getMaxNumOfRecentFiles()) { return; } } } } } public List<String> getRecentFiles() { return Collections.checkedList(recentFiles, String.class); } private void addToRecentFiles(String file) { int max = globalConfiguration.getMaxNumOfRecentFiles(); if (max == 0) { return; } int i = recentFiles.indexOf(file); Action act; if (i >= 0) { recentFiles.remove(i); act = recentFileActions.get(i); recentFileActions.remove(i); } else { act = actions.getRecentFileAction(file); ui.addAction("&File.Open &recent file", act, null); if (recentFiles.size() == max) { Action last = recentFileActions.removeLast(); ui.removeAction("&File.Open &recent file", last); recentFiles.removeLast(); } } recentFiles.addFirst(file); recentFileActions.addFirst(act); } private void writeRecentFiles() { String sep = System.getProperty("path.separator"); StringBuffer buffer = new StringBuffer(); for (String file : recentFiles) { if (buffer.length() > 0) { buffer.append(sep); } buffer.append(file); } globalConfiguration.setRecentFiles(buffer.toString()); } public int startRealtimeServer(int port) throws IOException { if (isServerRunning()) { return 0; } server = new RealtimeServer(port, this); server.setDaemon(true); server.start(); return server.getPort(); } public boolean isServerRunning() { return server != null; } public void shutDownServer() { if (isServerRunning()) { server.shutDown(); server = null; } } private void setupUI() { ui.setTitle("Quick Sequence Diagram Editor - ABS Version"); addActions(); ui.showUI(); ui.addToolbarSeparator(); ui.addToToolbar(actions.helpAction, null); } private void addActions() { actions = new Actions(this); ui.addAction("&File", actions.newDiagramAction, null); ui.addAction("&File", actions.loadCodeAction, null); ui.addCategory("&File.Open &recent file", "open"); ui.addAction("&File", actions.saveCodeAction, actions.regularTabActivator); ui.addAction("&File", actions.saveCodeAsAction, actions.regularTabActivator); Action exportAction = actions.getExportAction(); if (exportAction != null) { ui.addAction("&File", exportAction, actions.nonEmptyDiagramActivator); } else { ui.addAction("&File", actions.saveImageAction, actions.nonEmptyDiagramActivator); } ui.addAction("&File", actions.closeDiagramAction, null); ui.addAction("&File", actions.closeAllAction, null); Action printPDFAction = actions.getPrintAction("pdf"); if (printPDFAction != null) { ui.addAction("&File", printPDFAction, actions.noDiagramErrorActivator); } ui.addAction("&File", actions.quitAction, null); ConfigurationAction<Configuration> wrapAction = new ConfigurationAction<Configuration>( "lineWrap", "[control shift W]&Wrap lines", "Wrap lines whose length exceed the width of the text area", "wrap") { @Override public Bean<Configuration> getBean() { return ui.getConfiguration(); } }; ConfigurationAction<Configuration> threadedAction = new ConfigurationAction<Configuration>( "threaded", Shortcuts.getShortcut(Shortcuts.ENABLE_THREADS) + "Enable &multithreading", "Create diagrams with arbitrarily many sequences running concurrently", "threads") { @Override public Bean<Configuration> getBean() { return ui.getConfiguration(); } }; ConfigurationAction<GlobalConfiguration> autoUpdateAction = new ConfigurationAction<GlobalConfiguration>( "autoUpdate", "Auto-redraw", "Update diagram as you type", "reload") { @Override public Bean<GlobalConfiguration> getBean() { return ConfigurationManager.getGlobalConfigurationBean(); } }; ConfigurationAction<GlobalConfiguration> autoScrollAction = new ConfigurationAction<GlobalConfiguration>( "autoScroll", "Auto-scrolling", "Scroll automatically to where the message currently being specified is visible", "autoscroll") { @Override public Bean<GlobalConfiguration> getBean() { return ConfigurationManager.getGlobalConfigurationBean(); } }; ui.addAction("&Edit", actions.undoAction, actions.regularTabActivator); ui.addAction("&Edit", actions.redoAction, actions.regularTabActivator); ui.addAction("&Edit", actions.clearAction, actions.regularTabActivator); ui.addConfigurationAction("&Edit", threadedAction, actions.regularTabActivator); ui.addAction("&Edit", actions.configureGloballyAction, null); ui.addAction("&Edit", actions.configureDiagramAction, actions.regularTabActivator); ui.addCategory("&View", null); ui.addConfigurationAction("&View", autoUpdateAction, null); ui.addConfigurationAction("&View", autoScrollAction, null); ui .addAction("&View", actions.redrawAction, actions.regularTabActivator); ui.addAction("&View", actions.widenAction, actions.canConfigureActivator); ui.addAction("&View", actions.narrowAction, actions.canNarrowActivator); ui.addConfigurationAction("&View", wrapAction, actions.regularTabActivator); ui.addAction("&View", actions.fullScreenAction, actions.nonEmptyDiagramActivator); ui.addAction("&View", actions.splitLeftRightAction, actions.horizontalSplitPossibleActivator); ui.addAction("&View", actions.splitTopBottomAction, actions.verticalSplitPossibleActivator); if (OS.TYPE != OS.Type.MAC) { ui.setQuitAction(actions.quitAction); } ui.addToToolbar(actions.newDiagramAction, null); ui.addToToolbar(actions.loadCodeAction, null); ui.addToToolbar(actions.saveCodeAction, actions.regularTabActivator); ui.addToToolbar(actions.saveCodeAsAction, actions.regularTabActivator); if (exportAction != null) { ui.addToToolbar(exportAction, actions.nonEmptyDiagramActivator); } else { ui.addToToolbar(actions.saveImageAction, actions.nonEmptyDiagramActivator); } if (printPDFAction != null) { ui.addToToolbar(printPDFAction, actions.noDiagramErrorActivator); } ui.addToolbarSeparator(); ui.addToToolbar(actions.configureGloballyAction, null); ui.addToToolbar(actions.configureDiagramAction, actions.regularTabActivator); ui.addToToolbar(actions.redrawAction, actions.regularTabActivator); ui.addToolbarSeparator(); ui.addToToolbar(actions.fullScreenAction, actions.nonEmptyDiagramActivator); ui.addToToolbar(actions.splitLeftRightAction, actions.horizontalSplitPossibleActivator); ui.addToToolbar(actions.splitTopBottomAction, actions.verticalSplitPossibleActivator); ui.addAction("E&xtras", actions.serverAction, null); ui.addAction("E&xtras", actions.filterAction, actions.regularTabActivator); ui.addAction("E&xtras", new ExportMapAction(this), actions.nonEmptyDiagramActivator); ui.addAction("&Help", actions.helpAction, null); ui.addAction("&Help", actions.helpOnMultithreadingAction, null); ui.addAction("&Help", actions.asyncNotesAction, null); if (OS.TYPE != OS.Type.MAC) { ui.addAction("&Help", actions.showAboutDialogAction, null); } ui.addAction("&Help.&Examples", actions.getExampleAction( "Ticket order", "order.sdx"), null); ui.addAction("&Help.&Examples", actions.getExampleAction( "Breadth first search", "bfs.sdx"), null); ui.addAction("&Help.&Examples", actions.getExampleAction( "Levels and mnemonics", "levels.sdx"), null); ui.addAction("&Help.&Examples", actions.getExampleAction( "SSH 2 (by courtesy of Carlos Duarte)", "ssh.sdx"), null); ui.addAction("&Help.&Examples", actions.getExampleAction("Webserver", "webserver.sdx"), null); } void loadCode() throws IOException, XMLException { File[] files = ui.getFiles(true, true, "Open diagram file(s)", null, null, "Plain diagram source (.sd)", "sd", "Diagram source with preferences (.sdx)", "sdx"); if (files != null) { for (File file : files) { loadCode(file); } } } public void loadCode(File file) throws IOException, XMLException { InputStream stream = new FileInputStream(file); try { loadCode(stream, file.getName()); addToRecentFiles(file.getAbsolutePath()); ui.setCurrentFile(file); } finally { stream.close(); } } public boolean isSetup() { return setup; } /** * Asks for a file and then tries to load code from that file. If it has * been loaded successfully, the current file is set to that file. * * @throws IOException * if the file cannot be read */ void loadCode(InputStream stream, String tabName) throws IOException, XMLException { String encoding = globalConfiguration.getFileEncoding(); Pair<String, Bean<Configuration>> result = DiagramLoader.load(stream, encoding); ui.addTab(tabName, result.getSecond()); ui.setCode(result.getFirst()); // happens automatically via DocumentListener // engine.render(result.getSecond().getDataObject(), false, true); ui.home(); } public void quit() { if (closeAll()) { ui.exit(); writeRecentFiles(); try { ConfigurationManager.storeConfigurations(); } catch (IOException e) { ui.errorMessage("Could not save the global settings file:\n" + GLOBAL_CONF_FILE.getAbsolutePath() + "\n" + "due to an exception of type\n" + e.getClass().getSimpleName() + "\n" + "with the message: " + e.getMessage()); e.printStackTrace(); } if (server != null) { server.shutDown(); } System.exit(0); } } /** * Returns true if ALL tabs could be closed. */ boolean closeAll() { boolean confirmed = false; do { if (!confirmed && !ui.isClean()) { String choice = ui.getOption ("<html>There are unsaved changes. " + "Do you want<br>to save them?", "Cancel", "No", "::::Yes#", "No to all"); if (choice == null || choice.equals("Cancel")) { return false; } if (choice.equals("Yes")) { try { if (!saveCode(false)) { return false; } } catch (RuntimeException re) { throw re; } catch (Exception ex) { ui.message("The diagram could not be stored due to an exception\n" + "of type " + ex.getClass().getSimpleName() + " with the message: " + ex.getMessage()); } } if (choice.equals("No to all")) { confirmed = true; } } } while (ui.removeCurrentTab(false)); return true; } /** * Saves the code from the text area to the current file or to a file whose * name is given by the user. If the file has been successfully written, the * current file is set to that file. * * @param as * flag denoting whether the user should give the file, if not, * the current file (if present) is used * @throws IOException * if the file cannot be written * @return flag denoting if the code has actually been saved */ public boolean saveCode(boolean as) throws IOException, XMLException { String code = ui.getCode().trim(); Bean<Configuration> configuration = ui.getConfiguration(); boolean createNew = as || ui.getCurrentFile() == null; File file = null; if (createNew) { String currentFile; File current = ui.getCurrentFile(); if (current != null) { currentFile = current.getName(); current = current.getParentFile(); } else { currentFile = "untitled.sdx"; } File[] files = ui.getFiles(false, false, "Save diagram file", currentFile, current, "Plain diagram source (.sd)", "sd", "Diagram source with preferences (.sdx)", "sdx"); if (files != null) { file = files[0]; } } else { file = ui.getCurrentFile(); } if (file == null) { return false; } if (file.exists()) { if (createNew) { String option = ui.getOption("Overwrite existing file?", "Cancel", "No", "Yes#"); if (!option.equals("Yes")) { return false; } } if (globalConfiguration.isBackupFiles()) { File backup = new File(file.getParent(), file.getName() + ".bak"); if (backup.exists()) { backup.delete(); } file.renameTo(backup); } } // set configuration to null if the diagram is to be saved as // plain text configuration = file.getName().toLowerCase().endsWith("sdx") ? configuration : null; OutputStream stream = new FileOutputStream(file); String encoding = globalConfiguration.getFileEncoding(); try { DiagramLoader.saveDiagram(code, configuration, stream, encoding); addToRecentFiles(file.getAbsolutePath()); ui.setCurrentFile(file); ui.setClean(); if (createNew) { ui.setTabTitle(file.getName()); } } finally { stream.close(); } return true; } private boolean firstImageSaved = false; /** * Saves the current diagram as a PNG image file whose name is chosen by the * user. Asks for confirmation, if a file would be overwritten. * * @throws IOException * if the image file cannot be written due to an i/o error */ void saveImage() throws IOException { String code = getUI().getCode().trim(); if (code.equals("")) { return; } ImagePaintDevice ipd = new ImagePaintDevice(); TextHandler handler = new TextHandler(code); Configuration conf = getUI().getConfiguration().getDataObject(); try { new Diagram(conf, handler, ipd).generate(); } catch (Exception ex) { ui.errorMessage("The diagram source text has errors."); return; } Image image = ipd.getImage(); if (image != null) { File current = null; if (!firstImageSaved) { current = ui.getCurrentFile(); if (current != null) { current = current.getParentFile(); } firstImageSaved = true; } String currentFile = null; if (ui.getCurrentFile() != null) { currentFile = ui.getCurrentFile().getName(); int dot = currentFile.lastIndexOf('.'); if (dot >= 0) { currentFile = currentFile.substring(0, dot + 1) + "png"; } } File[] files = ui.getFiles(false, false, "save as PNG", currentFile, current, "PNG image", "png"); File imageFile = files != null ? files[0] : null; if (imageFile != null && (!imageFile.exists() || 1 == ui .confirmOrCancel("Overwrite existing file " + imageFile.getName() + "?"))) { ipd.saveImage(imageFile); ui.message("Exported image as\n" + imageFile.getAbsolutePath()); } } } /** * Starts a new thread that generates a diagram from the source text found * in the text area of the currently selected tab. * * @param syntaxCheckOnly * flag denoting if only syntax is checked and no diagram is * generated yet */ public void codeChanged(boolean syntaxCheckOnly) { ui.setErrorStatus(false, "", -1, -1); String code = ui.getCode(); if (code != null && !code.trim().equals("")) { engine.render(ui.getConfiguration().getDataObject(), syntaxCheckOnly, false); } else { ui.clearDisplay(); } } /** * Returns the user interface. * * @return the user interface */ public UserInterface getUI() { return ui; } private UserInterface newUI() { return new UserInterfaceImpl(); } // TODO move this to the GUI side (use diagram reference of paint device) /** * Moves the cursor to the position in the text area where the object or * message corresponding to the drawable instance is declared. * * @param drawable * a drawable instance to show the corresponding declaration for */ public void mouseClickedDrawable(Drawable drawable) { Diagram diag = ui.getDiagram(); if (diag != null) { Integer pos = (Integer) diag.getStateForDrawable(drawable); if (pos != null) { ui.moveCursorToPosition(pos - 1); } } if (drawable instanceof Note) { Note note = (Note) drawable; URI link = note.getLink(); if (link != null) { File current = ui.getCurrentFile(); File linked; if (current != null) { linked = new File(current.toURI().resolve(link)); } else { linked = new File(link); } if (!ui.selectTabWith(linked)) { try { loadCode(linked); } catch (RuntimeException e) { throw e; } catch (Exception e) { ui .errorMessage(linked.getAbsolutePath() + "\n" + "could not be loaded due to an exception of\n" + "type " + e.getClass().getSimpleName() + " with the message\n" + e.getMessage()); } } } } } /** * Returns true if and only if the given drawable instance is associated by * the most recently used diagram with a DiagramDataProvider state, id est a * position in the text area. * * @param drawable * a drawable instance such that the mouse has just entered it * * @return true if and only if the given drawable instance is associated by * the most recently used diagram with a position in the text area */ public boolean mouseEnteredDrawable(Drawable drawable) { Diagram diag = ui.getDiagram(); if (diag != null && diag.getStateForDrawable(drawable) != null) { return true; } return false; } /** * @see net.sf.sdedit.ui.PanelPaintDeviceListener#mouseExitedDrawable(net.sf.sdedit.drawable.Drawable) */ public void mouseExitedDrawable(Drawable drawable) { /* empty */ } public PanelPaintDeviceListener getPanelPaintDeviceListener() { return this; } }