diff --git a/build.sbt b/build.sbt index 16926bf..507674e 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,9 @@ scalaVersion := "2.9.2" retrieveManaged := true -javaSource in Compile <<= baseDirectory(_ / "src" / "main") +javaSource in Compile <<= baseDirectory(_ / "src" / "main") + +scalaSource in Compile <<= baseDirectory(_ / "src" / "main") scalaSource in Test <<= baseDirectory(_ / "src" / "test") @@ -13,8 +15,8 @@ scalacOptions ++= Seq("-deprecation", "-unchecked", "-Xfatal-warnings", libraryDependencies ++= Seq( "com.google.guava" % "guava" % "18.0", - "org.nlogo" % "NetLogo" % "5.1.0" from "http://ccl.northwestern.edu/netlogo/5.1.0/NetLogo.jar", - "org.nlogo" % "NetLogo-tests" % "5.1.0" % "test" from "http://ccl.northwestern.edu/netlogo/5.1.0/NetLogo-tests.jar", + "org.nlogo" % "NetLogo" % "5.2.0-LS1" from "http://ccl.northwestern.edu/devel/5.2.0-LS1/NetLogo.jar", + "org.nlogo" % "NetLogo-tests" % "5.2.0-LS1" % "test" from "http://ccl.northwestern.edu/devel/5.2.0-LS1/NetLogo-tests.jar", "org.scalatest" %% "scalatest" % "1.8" % "test", "org.picocontainer" % "picocontainer" % "2.13.6" % "test", "asm" % "asm-all" % "3.3.1" % "test" diff --git a/src/main/BackingModelManager.scala b/src/main/BackingModelManager.scala new file mode 100644 index 0000000..270f120 --- /dev/null +++ b/src/main/BackingModelManager.scala @@ -0,0 +1,81 @@ +import gui.{ModelProceduresTab, LevelSpaceMenu} + +import java.util.{ Map => JMap } + +import org.nlogo.app.{ProceduresTab, App} +import org.nlogo.workspace.AbstractWorkspace + +import scala.collection.JavaConversions._ +import scala.collection.parallel.mutable.ParHashMap + +trait LSModelManager extends gui.ModelManager { + def updateChildModels(map: JMap[java.lang.Integer, ChildModel]): Unit = {} + def guiComponent: LevelSpaceMenu = null +} + +class BackingModelManager extends LSModelManager { + override val guiComponent = new LevelSpaceMenu(App.app.tabs, this) + val backingModels = ParHashMap.empty[String, (ChildModel, ModelProceduresTab)] + var openModels = Map.empty[String, ChildModel] + + override def updateChildModels(indexedModels: JMap[java.lang.Integer, ChildModel]): Unit = { + val models = indexedModels.values + + // toSeq.distinct preserves ordering, whereas toSet does not + val modelPaths = models.map(_.workspace().getModelPath).toSeq.distinct + + val closedModelsPaths = (openModels.values.toSet &~ models.toSet).map(_.workspace().getModelPath) + val newlyOpenedPaths = (models.toSet &~ openModels.values.toSet).map(_.workspace().getModelPath) + openModels = (modelPaths zip models).toMap + (closedModelsPaths intersect openModelPaths).foreach(replaceTabAtPath) + (newlyOpenedPaths intersect openModelPaths).foreach(replaceTabAtPath) + guiComponent.addMenuItemsForOpenModels(modelPaths) + } + + private def replaceTabAtPath(filePath: String) = + guiComponent.replaceTab(backingModels(filePath)._2) + + def openModelPaths = backingModels.seq.keySet + + def existingTab(filePath: String): Option[ProceduresTab] = + if (filePath == App.app.workspace.getModelPath) + Some(App.app.tabs.proceduresTab) + else + backingModels.get(filePath).map(_._2) + + def removeTab(tab: ModelProceduresTab): Unit = { + if (! openModelPaths(tab.filePath)) + backingModels.get(tab.filePath).foreach(_._1.kill()) + backingModels -= tab.filePath + } + + def registerTab(filePath: String, model: ChildModel) + (f: AbstractWorkspace => ModelProceduresTab): Option[ModelProceduresTab] = { + if (backingModels.get(filePath).isDefined) { + None + } else { + val tab = f(model.workspace()) + backingModels += filePath ->(model, tab) + Some(tab) + } + } + + def registerTab(filePath: String) + (f: AbstractWorkspace => ModelProceduresTab): Option[ModelProceduresTab] = { + if (backingModels.get(filePath).isDefined) { + None + } else { + val newModel = + openModels.getOrElse(filePath, new HeadlessChildModel(App.app.workspace.world(), filePath, -1)) + registerTab(filePath, newModel)(f) + } + } +} + +// this is an example of the +class HeadlessBackingModelManager extends LSModelManager { + def removeTab(tab: ModelProceduresTab): Unit = {} + def existingTab(filePath: String): Option[ModelProceduresTab] = None + def registerTab(filePath: String) + (f: AbstractWorkspace => ModelProceduresTab): Option[ModelProceduresTab] = None +} diff --git a/src/main/ChildModel.java b/src/main/ChildModel.java index 241713d..f2abb30 100644 --- a/src/main/ChildModel.java +++ b/src/main/ChildModel.java @@ -265,7 +265,7 @@ public T runNlogoSafely(final Callable callable) throws HaltException, Ex public T runUISafely(final Callable callable) throws ExtensionException, HaltException { // waitFor is unsafe on the event queue, so if we're on the event queue, just run directly. - if (EventQueue.isDispatchThread()) { + if (SwingUtilities.isEventDispatchThread()) { try { return callable.call(); } catch (ExtensionException e) { diff --git a/src/main/GUIChildModel.java b/src/main/GUIChildModel.java deleted file mode 100644 index 35a7cbb..0000000 --- a/src/main/GUIChildModel.java +++ /dev/null @@ -1,100 +0,0 @@ -import java.awt.*; -import java.util.concurrent.Callable; - -import javax.swing.*; - -import org.nlogo.api.*; -import org.nlogo.lite.InterfaceComponent; -import org.nlogo.nvm.HaltException; -import org.nlogo.window.SpeedSliderPanel; -import org.nlogo.workspace.AbstractWorkspace; - - -public class GUIChildModel extends ChildModel { - - final JFrame frame = new JFrame(); - InterfaceComponent component; - GUIPanel panel; - - - public GUIChildModel(World parentWorld, final String path, final int levelsSpaceNumber) - throws InterruptedException, ExtensionException, HaltException { - super(parentWorld, levelsSpaceNumber); - - component = runUISafely(new Callable() { - public InterfaceComponent call() throws Exception { - // For some reason, the IC must be added to the frame before the model is opened. - InterfaceComponent component = new InterfaceComponent(frame); - panel = new GUIPanel(component); - frame.add(panel); - Window currentlyFocused = FocusManager.getCurrentManager().getActiveWindow(); - frame.setLocationRelativeTo(currentlyFocused); - frame.setLocationByPlatform(true); - frame.setVisible(true); - currentlyFocused.toFront(); - component.open(path); - // get all components, find the speed slider, and hide it. - Component[] c = component.workspace().viewWidget.controlStrip.getComponents(); - for (Component co : c) { - if (co instanceof SpeedSliderPanel) { - co.setVisible(false); - ((SpeedSliderPanel) co).setValue(0); - } - } - frame.pack(); - // Make sure that the model doesn't close if people accidentally click the close button - frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - // Adding window listener so that the model calls the method that removes it from - // the extension if closed. - frame.addWindowListener(new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { - Object[] options = {"Close Model", "Run in Background", "Cancel"}; - int n = JOptionPane.showOptionDialog(frame, - "Close the model, run it in the background, or do nothing?", - null, JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[2]); - switch (n) { - case 0: - try { - LevelsSpace.closeModel(levelsSpaceNumber); - } catch (ExtensionException e) { - throw new RuntimeException(e); - } catch (HaltException e) { - //ignore - } - break; - case 1: - hide(); - } - } - }); - return component; - } - }); - init(); - } - - public void setSpeed(double d){ - Component[] c = component.workspace().viewWidget.controlStrip.getComponents(); - for (Component co : c){ - if (co instanceof SpeedSliderPanel){ - ((SpeedSliderPanel) co).setValue((int)d); - } - } - - } - - @Override - public AbstractWorkspace workspace() { - return component.workspace(); - } - - @Override - JFrame frame() { - return frame; - } -} diff --git a/src/main/GUIChildModel.scala b/src/main/GUIChildModel.scala new file mode 100644 index 0000000..be94147 --- /dev/null +++ b/src/main/GUIChildModel.scala @@ -0,0 +1,84 @@ +import java.awt._ +import java.awt.event.{ WindowAdapter, WindowEvent } +import java.util.concurrent.Callable +import javax.swing.{ FocusManager, JFrame, JMenuBar, JOptionPane, WindowConstants } +import gui.{ZoomableInterfaceComponent, GUIPanel} +import org.nlogo.api._ +import org.nlogo.app.App +import org.nlogo.lite.{LiteWorkspace, InterfaceComponent} +import org.nlogo.nvm.HaltException +import org.nlogo.window.Events.ZoomedEvent +import org.nlogo.window.Widget.LoadHelper +import org.nlogo.window._ +import org.nlogo.workspace.AbstractWorkspace + +class GUIChildModel @throws(classOf[InterruptedException]) @throws(classOf[ExtensionException]) @throws(classOf[HaltException]) (parentWorld: World, path: String, levelsSpaceNumber: Int) + extends ChildModel(parentWorld, levelsSpaceNumber) { + + final val frame: JFrame = new JFrame + + var panel: GUIPanel = null + val component = runUISafely(RunGUIChildModel) + + init() + + object RunGUIChildModel extends Callable[InterfaceComponent] { + @throws(classOf[Exception]) + def call: InterfaceComponent = { + val component: InterfaceComponent = new ZoomableInterfaceComponent(frame) + panel = new GUIPanel(component) + frame.add(panel) + val currentlyFocused: Window = + Option(KeyboardFocusManager.getCurrentKeyboardFocusManager.getActiveWindow).getOrElse(App.app.frame) + frame.setLocationRelativeTo(currentlyFocused) + frame.setLocationByPlatform(true) + frame.setVisible(true) + currentlyFocused.toFront() + component.open(path) + val c: Array[Component] = component.workspace.viewWidget.controlStrip.getComponents + c.foreach { + case ssp: SpeedSliderPanel => + ssp.setVisible(false) + ssp.setValue(0) + case _ => + } + frame.pack() + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE) + frame.addWindowListener(new GUIWindowAdapter) + val newMenuBar = new JMenuBar() + val zoomMenuClass = Class.forName("org.nlogo.app.ZoomMenu") + newMenuBar.add(zoomMenuClass.newInstance().asInstanceOf[org.nlogo.swing.Menu]) + frame.setJMenuBar(newMenuBar) + component + } + } + + class GUIWindowAdapter extends WindowAdapter { + override def windowClosing(windowEvent: WindowEvent): Unit = { + val options: Array[AnyRef] = Array("Close Model", "Run in Background", "Cancel") + val n: Int = JOptionPane.showOptionDialog(frame, "Close the model, run it in the background, or do nothing?", null, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options(2)) + n match { + case 0 => + try { + LevelsSpace.closeModel(levelsSpaceNumber) + } catch { + case e: ExtensionException => throw new RuntimeException(e) + case e: HaltException => + } + case 1 => hide() + case 2 => + } + } + } + + def setSpeed(d: Double): Unit = { + val c: Array[Component] = component.workspace.viewWidget.controlStrip.getComponents + for (co <- c) { + if (co.isInstanceOf[SpeedSliderPanel]) { + (co.asInstanceOf[SpeedSliderPanel]).setValue(d.toInt) + } + } + } + + def workspace: AbstractWorkspace = component.workspace +} diff --git a/src/main/GUIPanel.java b/src/main/GUIPanel.java deleted file mode 100644 index b2ff043..0000000 --- a/src/main/GUIPanel.java +++ /dev/null @@ -1,44 +0,0 @@ -import org.nlogo.app.CommandCenter; -import org.nlogo.lite.InterfaceComponent; -import org.nlogo.window.Events; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ActionEvent; - -public class GUIPanel extends JPanel implements Events.OutputEvent.Handler { - CommandCenter cc; - - public GUIPanel(InterfaceComponent ws){ - // Border layout makes contents expand with the frame. - setLayout(new BorderLayout()); - cc = new CommandCenter(ws.workspace(), new AbstractAction(){ - - @Override - public void actionPerformed(ActionEvent actionEvent) { - - } - }); - JScrollPane scrollPane = new JScrollPane( - ws, - ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - JSplitPane splitPane = new JSplitPane( - JSplitPane.VERTICAL_SPLIT, - true, - scrollPane, cc); - splitPane.setOneTouchExpandable(true); - splitPane.setResizeWeight(1); - this.add(splitPane); - } - - @Override - public void handle(Events.OutputEvent outputEvent) { - if (outputEvent.clear){ - cc.output().clear(); - }else{ - cc.output().append(outputEvent.outputObject, outputEvent.wrapLines); - } - - } -} diff --git a/src/main/LevelsSpace.java b/src/main/LevelsSpace.java index 4b7dac6..1e0cf5a 100644 --- a/src/main/LevelsSpace.java +++ b/src/main/LevelsSpace.java @@ -9,16 +9,14 @@ import java.util.HashMap; import java.util.List; -import javax.swing.JMenuItem; -import javax.swing.JSlider; -import javax.swing.MenuElement; +import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.nlogo.api.*; import org.nlogo.api.Argument; import org.nlogo.api.Context; -import org.nlogo.app.App; +import org.nlogo.app.*; import org.nlogo.api.ExtensionObject; import org.nlogo.api.ImportErrorHandler; import org.nlogo.api.LogoException; @@ -26,12 +24,13 @@ import org.nlogo.api.LogoListBuilder; import org.nlogo.api.PrimitiveManager; import org.nlogo.api.Syntax; -import org.nlogo.app.ToolsMenu; +import org.nlogo.awt.EventQueue$; import org.nlogo.nvm.*; import org.nlogo.nvm.ReporterTask; import org.nlogo.window.SpeedSliderPanel; import org.nlogo.window.ViewUpdatePanel; +import gui.ModelManager; public class LevelsSpace implements org.nlogo.api.ClassManager { @@ -56,6 +55,8 @@ public void actionPerformed(ActionEvent arg0) { } }; + private static LSModelManager modelManager = useGUI() ? new BackingModelManager() : new HeadlessBackingModelManager(); + @Override public void load(PrimitiveManager primitiveManager) throws ExtensionException { primitiveManager.addPrimitive("ask", new Ask()); @@ -118,6 +119,17 @@ public static boolean useGUI() { return !"true".equals(System.getProperty("java.awt.headless")); } + public static boolean isMainModel() { + for (ClassManager cm : App.app().workspace().getExtensionManager().loadedExtensions()) { + if (cm.getClass() == LevelsSpace.class) { + return true; + } + } + // The contains check determines whether or not LS has finished loading. If it hasn't, we're + // almost certainly the main model. + return !App.app().workspace().getExtensionManager().getExtensionNames().contains("ls"); + } + public static ChildModel getModel(int id) throws ExtensionException { if (models.containsKey(id)) { return models.get(id); @@ -137,6 +149,9 @@ public static int castToId(Object id) throws ExtensionException { @Override public void unload(ExtensionManager arg0) throws ExtensionException { + if (useGUI() && isMainModel()) { + App.app().frame().getJMenuBar().remove(modelManager.guiComponent()); + } if (haltButton != null) { haltButton.removeActionListener(haltListener); } @@ -188,6 +203,7 @@ public void perform(Argument args[], Context ctx) throws ExtensionException, org args[1].getCommandTask().perform(ctx, new Object[]{(double) modelCounter}); } modelCounter++; + updateModelMenu(); } catch (CompilerException e) { throw new ExtensionException(modelPath + " did not compile properly. There is probably something wrong " + "with its code. Exception said" + e.getMessage()); @@ -199,6 +215,16 @@ public void perform(Argument args[], Context ctx) throws ExtensionException, org } } + public static void updateModelMenu() { + Runnable reportModelOpened = new Runnable() { + @Override + public void run() { + modelManager.updateChildModels(models); + } + }; + EventQueue$.MODULE$.invokeLater(reportModelOpened); + } + public static void reset() throws ExtensionException, HaltException { modelCounter = 0; @@ -420,6 +446,7 @@ public void perform(Argument args[], Context context) public static void closeModel(int modelNumber) throws ExtensionException, HaltException { getModel(modelNumber).kill(); models.remove(modelNumber); + updateModelMenu(); } public static class UpdateView extends DefaultCommand { @@ -656,7 +683,14 @@ public ExtensionObject readExtensionObject(ExtensionManager arg0, @Override public void runOnce(ExtensionManager arg0) throws ExtensionException { + modelManager.updateChildModels(models); + if (useGUI() && isMainModel()) { + final JMenuBar menuBar = App.app().frame().getJMenuBar(); + if (menuBar.getComponentIndex(modelManager.guiComponent()) == -1) { + menuBar.add(modelManager.guiComponent()); + } + } } private static void updateChildModelsSpeed(){ diff --git a/src/main/gui/GUIPanel.scala b/src/main/gui/GUIPanel.scala new file mode 100644 index 0000000..8ae1a4b --- /dev/null +++ b/src/main/gui/GUIPanel.scala @@ -0,0 +1,29 @@ +package gui + +import java.awt._ +import java.awt.event.ActionEvent +import javax.swing._ + +import org.nlogo.app.CommandCenter +import org.nlogo.lite.InterfaceComponent +import org.nlogo.window.Events + +class GUIPanel(ws: InterfaceComponent) extends JPanel with Events.OutputEvent.Handler { + setLayout(new BorderLayout) + var cc = new CommandCenter(ws.workspace, new AbstractAction() { + def actionPerformed(actionEvent: ActionEvent) { + } + }) + val scrollPane: JScrollPane = new JScrollPane(ws, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED) + val splitPane: JSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, scrollPane, cc) + splitPane.setOneTouchExpandable(true) + splitPane.setResizeWeight(1) + add(splitPane) + + def handle(outputEvent: Events.OutputEvent): Unit = { + if (outputEvent.clear) + cc.output.clear() + else + cc.output.append(outputEvent.outputObject, outputEvent.wrapLines) + } +} diff --git a/src/main/gui/LevelSpaceMenu.scala b/src/main/gui/LevelSpaceMenu.scala new file mode 100644 index 0000000..156f1f3 --- /dev/null +++ b/src/main/gui/LevelSpaceMenu.scala @@ -0,0 +1,196 @@ +package gui + +import java.awt.FileDialog.{LOAD => LOADFILE, SAVE => SAVEFILE} +import java.awt.event.ActionEvent +import java.io.{File, FileWriter, IOException} +import javax.swing._ + +import org.nlogo.api.ModelSections.{BufSaveable, Saveable} +import org.nlogo.api.{CompilerException, ExtensionException, ModelReader, ModelSections, Shape, Version} +import org.nlogo.app.{ProceduresTab, ModelSaver, App, Tabs} +import org.nlogo.awt.UserCancelException +import org.nlogo.shape.{VectorShape, LinkShape} +import org.nlogo.swing.FileDialog +import org.nlogo.util.Exceptions +import org.nlogo.workspace.{AbstractWorkspace, ModelsLibrary} + +import scala.collection.JavaConversions._ + +trait ModelManager { + def removeTab(tab: ModelProceduresTab): Unit + def existingTab(filePath: String): Option[ProceduresTab] + def registerTab(filePath: String) + (f: AbstractWorkspace => ModelProceduresTab): Option[ModelProceduresTab] +} + +class LevelSpaceMenu(tabs: Tabs, val backingModelManager: ModelManager) + extends JMenu("LevelSpace") { + + import LevelSpaceMenu._ + + val selectModel = new SelectModelAction("Open Model in Code Tab", backingModelManager) + val openModels = new JMenu("Edit Open Models...") + val newModel = new NewModelAction("Create new LevelSpace Model", backingModelManager) + + add(selectModel) + add(openModels) + add(newModel) + + def addMenuItemsForOpenModels(modelPaths: Seq[String]) = { + openModels.removeAll() + modelPaths.foreach(addModelAction(openModels, _)) + if (openModels.getMenuComponentCount == 0) + openModels.setEnabled(false) + else + openModels.setEnabled(true) + } + + def replaceTab(oldTab: ModelProceduresTab): Unit = + newHeadlessBackedTab(oldTab.filePath).foreach { + newTab => replaceSwingTab(oldTab, newTab) + } + + private def addModelAction(menu: JMenu, filePath: String): Unit = { + menu.add(new OpenModelAction(filePath, backingModelManager)) + } + + private def newHeadlessBackedTab(filePath: String): Option[ModelProceduresTab] = + backingModelManager.registerTab(filePath) { workspace => + new ModelProceduresTab(workspace, tabs, backingModelManager) + } + + private def replaceSwingTab(oldTab: ModelProceduresTab, newTab: ModelProceduresTab): Unit = { + val i = tabs.getIndexOfComponent(oldTab) + tabs.setComponentAt(i, newTab) + } +} + +object LevelSpaceMenu { + abstract class NewTabAction(name: String, modelManager: ModelManager) extends AbstractAction(name) { + val tabs = App.app.tabs + + def filePath: Option[String] + + def actingTab: Option[ProceduresTab] = + filePath.flatMap(path => locateExistingTab(path) orElse createNewTab(path)) + + private def locateExistingTab(path: String): Option[ProceduresTab] = + modelManager.existingTab(path) + + private def createNewTab(path: String): Option[ProceduresTab] = { + modelManager.registerTab(path) { workspace => + val tab = new ModelProceduresTab(workspace, tabs, modelManager) + tabs.addTab(tab.tabName, tab) + tab + } + } + + override def actionPerformed(actionEvent: ActionEvent): Unit = + actingTab.foreach(tabs.setSelectedComponent) + } + + class OpenModelAction(fileName: String, modelManager: ModelManager) + extends NewTabAction(fileName, modelManager) { + override def filePath: Option[String] = Some(fileName) + } + + class SelectModelAction(name: String, modelManager: ModelManager) + extends NewTabAction(name, modelManager) { + + override def filePath: Option[String] = selectFile + + override def actingTab: Option[ProceduresTab] = + try { + super.actingTab + } catch { + case e: CompilerException => + // we shouldn't have to raise an exception here, we should just be able to open it, but + // in order to do that, we'll need to change child models not to compile in their constructors + throw new ExtensionException(filePath + " did not compile properly. There is probably something wrong " + + "with its code. Exception said" + e.getMessage); + case e: IOException => + throw new ExtensionException("There was no .nlogo file at the path: \"" + filePath + "\"") + } + + + private def selectFile: Option[String] = + showLoadSelection.flatMap(path => + if (ModelsLibrary.getModelPaths.contains(path)) { + showLibraryModelErrorMessage() + None + } else + Some(path)) + + private def showLoadSelection: Option[String] = + try { + Some(FileDialog.show(App.app.frame, "Load a LevelSpace Model...", LOADFILE)) + } catch { + case e: UserCancelException => + Exceptions.ignore(e) + None + } + + private def showLibraryModelErrorMessage(): Unit = + JOptionPane.showMessageDialog( + App.app.frame, + """|The model you selected is a library model, which cannot be opened in a LevelSpace code tab. + |Please save the model elsewhere and try re-opening""".stripMargin) + } + + class NewModelAction(name: String, modelManager: ModelManager) + extends NewTabAction(name, modelManager) { + + override def filePath: Option[String] = + try { + FileDialog.setDirectory(App.app.workspace.getModelDir) + val userEntry = FileDialog.show(App.app.frame, "Select a path for new Model...", SAVEFILE) + // we basically need to write an empty NetLogo model in before we read... + val fileName = + if (userEntry.endsWith(".nlogo")) userEntry else userEntry + ".nlogo" + if (new File(fileName).exists) { + val fileAlreadyExists = "The file " + fileName + " already exists. Please choose a different name" + throw new ExtensionException(fileAlreadyExists) + } + writeEmptyNetLogoFile(fileName) + } catch { + case e: UserCancelException => + Exceptions.ignore(e) + None + } + + private def writeEmptyNetLogoFile(fileName: String): Option[String] = { + val modelString = new ModelSaver(EmptyNetLogoFile).save + val fw = new FileWriter(fileName) + try { + fw.write(modelString) + Some(fileName) + } catch { + case i: IOException => None + } finally { + fw.flush() + fw.close() + } + } + + object EmptyNetLogoFile extends ModelSections { + object EmptySaveable extends Saveable with BufSaveable { + override def save: String = "" + override def save(buf: StringBuilder): Unit = () + } + + override def procedureSource: String = "" + override def aggregateManager: Saveable = EmptySaveable + override def snapOn: Boolean = false + override def previewCommands: String = "" + override def linkShapes: Seq[Shape] = + LinkShape.parseShapes(ModelReader.defaultLinkShapes, Version.version) + override def hubnetManager: BufSaveable = EmptySaveable + override def turtleShapes: Seq[Shape] = + VectorShape.parseShapes(ModelReader.defaultShapes, Version.version) + override def labManager: Saveable = EmptySaveable + override def info: String = "" + override def widgets: Seq[Saveable] = Seq() + override def version: String = Version.version + } + } +} diff --git a/src/main/gui/ModelProceduresTab.scala b/src/main/gui/ModelProceduresTab.scala new file mode 100644 index 0000000..b21a412 --- /dev/null +++ b/src/main/gui/ModelProceduresTab.scala @@ -0,0 +1,116 @@ +package gui + +import java.awt.event.ActionEvent +import java.io.{FileReader, FileWriter} +import javax.swing.JButton + +import org.nlogo.api.{I18N, ModelReader, ModelSection} +import org.nlogo.app +import org.nlogo.app.{ProceduresMenu, ProceduresTab, Tabs} +import org.nlogo.awt.UserCancelException +import org.nlogo.swing.ToolBar +import org.nlogo.swing.ToolBar.Separator +import org.nlogo.util.Utils +import org.nlogo.window.Events.ModelSavedEvent +import org.nlogo.workspace.AbstractWorkspace + +class ModelProceduresTab(workspace: AbstractWorkspace, + tabs: Tabs, + modelManager: ModelManager) + extends ProceduresTab(workspace) + with ModelSavedEvent.Handler { + + val tabName = workspace.getModelFileName + val filePath = workspace.getModelPath + var modelSource = "" + private val fileReader = new FileReader(filePath) + + setIndenter(true) + + try { + modelSource = Utils.reader2String(fileReader) + val modelMap = ModelReader.parseModel(modelSource) + innerSource(modelMap.get(ModelSection.Code).mkString("\n")) + } finally { + fileReader.close() + } + + protected var isDirty = false + + override def getToolBar: ToolBar = { + new ToolBar { + override def addControls(): Unit = { + add(new JButton(org.nlogo.app.FindDialog.FIND_ACTION)) + add(new JButton(compileAction)) + add(new Separator) + add(new JButton(new FileCloseAction)) + add(new Separator) + add(new ProceduresMenu(ModelProceduresTab.this)) + } + } + } + + def close(): Unit = { + try { + if (isDirty && userWantsToSaveFile) + save() + tabs.remove(this) + modelManager.removeTab(this) + } catch { + case e: UserCancelException => + } + } + + override def dirty(): Unit = { + isDirty = true + super.dirty() + } + + private def userWantsToSaveFile: Boolean = { + val options = Array[AnyRef]( + I18N.gui.get("common.buttons.save"), + "Discard", + I18N.gui.get("common.buttons.cancel")) + val message = "Do you want to save the changes you made to " + filePath + "?" + org.nlogo.swing.OptionDialog.show(this, I18N.gui.get("common.messages.warning"), message, options) match { + case 0 => true + case 1 => false + case _ => throw new UserCancelException + } + } + + class FileCloseAction extends javax.swing.AbstractAction("Close") { + override def actionPerformed(actionEvent: ActionEvent): Unit = { + close() + } + } + + val originalModelSource = modelSource + + override def handle(modelSavedEvent: ModelSavedEvent): Unit = + save() + + def save(): Unit = { + // so we're just replacing the old source with the new, which is sort of cheating + // in some future version of NetLogo, where ModelSaver doesn't use an App for saving + // this code should make use of whatever mechanism is used there + val nonCodeSource = modelSource.lines.dropWhile(_ != ModelReader.SEPARATOR) + modelSource = innerSource + nonCodeSource.mkString("\n", "\n", "\n") + val fileWriter = new FileWriter(filePath) + try { + fileWriter.write(modelSource) + changedSourceWarning() + isDirty = false + } finally { + fileWriter.close() + } + } + + def changedSourceWarning(): Unit = + if (modelSource != originalModelSource) { + errorLabel.setVisible(true) + errorLabel.setText("Warning: Changes made to this code will not affect models until they are reloaded") + } else { + errorLabel.setVisible(false) + } +} diff --git a/src/main/gui/ZoomableInterfaceComponent.scala b/src/main/gui/ZoomableInterfaceComponent.scala new file mode 100644 index 0000000..a42dfdd --- /dev/null +++ b/src/main/gui/ZoomableInterfaceComponent.scala @@ -0,0 +1,183 @@ +package gui + +import java.awt._ +import java.awt.event.{ComponentEvent, ComponentListener, ContainerEvent, ContainerListener} +import javax.swing.JFrame + +import org.nlogo.api.{CompilerServices, RandomServices} +import org.nlogo.lite.{InterfaceComponent, LiteWorkspace} +import org.nlogo.window.Events.ZoomedEvent +import org.nlogo.window._ + +import scala.collection.mutable.{HashMap => MMap, MutableList => MList} + +trait ZoomableContainer + extends ComponentListener + with ContainerListener { + + import AwtScalable._ + + private var _zoomFactor = 1.0 + + val zoomMin = 0.1 + + val zoomableComponents = MList.empty[Component] + val unitAttributes = MMap.empty[Component, ScalableAttributes] + + def zoomFactor = _zoomFactor + + def zoomByStep(z: Int): Unit = { + modifyZoomSteps(z) + zoomChildWidgets() + } + + def zoomReset(): Unit = { + _zoomFactor = 1.0 + zoomChildWidgets() + } + + def unitScaleFactor: Double = 1.0 / zoomFactor + + def registerZoomableComponent(c: Component) = { + zoomableComponents += c + recursively(c, registerScalableAttributes) + } + + private def modifyZoomSteps(step: Int): Unit = + _zoomFactor = (_zoomFactor + (step * 0.1)) max zoomMin + + private def registerScalableAttributes(c: Component): Unit = { + unitAttributes += c -> (c.scalableAttributes scale unitScaleFactor) + c.addComponentListener(this) + c match { + case container: Container => container.addContainerListener(this) + case _ => + } + } + + private def deregisterScalableAttributes(c: Component): Unit = { + unitAttributes -= c + c.removeComponentListener(this) + c match { + case container: Container => container.removeContainerListener(this) + case _ => + } + } + + private def zoomChildWidgets(): Unit = { + zoomableComponents.foreach(_.removeComponentListener(this)) + zoomableComponents.foreach(zoomComponent) + zoomableComponents.foreach(_.addComponentListener(this)) + } + + def zoomComponent(c: Component): Unit = + recursively(c, { (com: Component) => + com.removeComponentListener(this) + com.scaleTo(unitAttributes(c) scale zoomFactor) + com.invalidate() + com.validate() + com.addComponentListener(this) + }) + + private def recursively(c: Component, f: Component => Unit): Unit = { + f(c) + c match { + case container: Container => + container.getComponents.foreach(recursively(_, f)) + case _ => + } + } + + override def componentShown(componentEvent: ComponentEvent): Unit = () + + override def componentHidden(componentEvent: ComponentEvent): Unit = () + + override def componentMoved(componentEvent: ComponentEvent): Unit = { + val component = componentEvent.getComponent + unitAttributes(component) = component.scalableAttributes scale unitScaleFactor + } + + override def componentResized(componentEvent: ComponentEvent): Unit = { + val component = componentEvent.getComponent + unitAttributes(component) = component.scalableAttributes scale unitScaleFactor + } + + override def componentAdded(containerEvent: ContainerEvent): Unit = + recursively(containerEvent.getChild, registerScalableAttributes) + + override def componentRemoved(containerEvent: ContainerEvent): Unit = + recursively(containerEvent.getChild, deregisterScalableAttributes) +} + +object AwtScalable { + implicit def toScalableComponent(c: Component): ScalableComponent = + new ScalableComponent(c) + + class ScalableComponent(c: Component) { + def scaleTo(scalableAttributes: ScalableAttributes): Unit = { + import scalableAttributes._ + c.setLocation(location) + c.setSize(size) + c.setFont(font) + } + + def scalableAttributes: ScalableAttributes = + ScalableAttributes(c.getLocation, c.getSize, c.getFont) + } + + // the reason to track all of these as doubles is that otherwise rounding errors + // accumulate and gradually skew positions + class ScalableAttributes(x: Double, + y: Double, + width: Double, + height: Double, + fontSize: Double, + baseFont: Font) { + def scale(d: Double): ScalableAttributes = + new ScalableAttributes(x * d, y * d, width * d, height * d, fontSize * d, baseFont) + + val font: Font = baseFont.deriveFont(fontSize.toFloat) + + val location: Point = new Point(x.ceil.toInt, y.ceil.toInt) + + val size: Dimension = new Dimension(width.ceil.toInt, height.ceil.toInt) + } + + object ScalableAttributes { + def apply(location: Point, size: Dimension, font: Font): ScalableAttributes = + new ScalableAttributes(location.getX, location.getY, size.getWidth, size.getHeight, font.getSize, font) + } +} + +class ZoomableInterfacePanel(viewWidget: ViewWidgetInterface, + compiler: CompilerServices, + random: RandomServices, + plotManager: org.nlogo.plot.PlotManager, + editorFactory: EditorFactory) + extends InterfacePanelLite(viewWidget, compiler, random, plotManager, editorFactory) + with Events.ZoomedEvent.Handler + with ZoomableContainer { + + registerZoomableComponent(viewWidget.asInstanceOf[Widget]) + + override def loadWidget(strings: Array[String], modelVersion: String): Widget = { + val widget = super.loadWidget(strings, modelVersion) + registerZoomableComponent(widget) + widget + } + + override def isZoomed: Boolean = zoomFactor != 1.0 + + override def handle(zoomedEvent: ZoomedEvent): Unit = + if (zoomedEvent.action == 0) + zoomReset() + else + zoomByStep(zoomedEvent.action) +} + +class ZoomableInterfaceComponent(frame: JFrame) extends InterfaceComponent(frame) { + override protected def createInterfacePanel(workspace: LiteWorkspace) = { + new ZoomableInterfacePanel(workspace.viewWidget, workspace, workspace, + workspace.plotManager, liteEditorFactory) + } +}