From 7af147b55877a594dbee0a7e845d368373bce58a Mon Sep 17 00:00:00 2001 From: Robert Grider Date: Fri, 15 May 2015 17:41:51 -0500 Subject: [PATCH 1/9] Adds LevelSpace zooming - Converts GUIPanel, GUIChildModel to scala - Adds ZoomableInterfaceComponent and friends - Adds a ZoomMenu to the menu bar for LevelSpace frames --- build.sbt | 4 +- src/main/GUIChildModel.java | 100 ------------ src/main/GUIChildModel.scala | 80 ++++++++++ src/main/GUIPanel.java | 44 ------ src/main/GUIPanel.scala | 26 +++ src/main/ZoomableInterfaceComponent.scala | 183 ++++++++++++++++++++++ 6 files changed, 292 insertions(+), 145 deletions(-) delete mode 100644 src/main/GUIChildModel.java create mode 100644 src/main/GUIChildModel.scala delete mode 100644 src/main/GUIPanel.java create mode 100644 src/main/GUIPanel.scala create mode 100644 src/main/ZoomableInterfaceComponent.scala diff --git a/build.sbt b/build.sbt index 16926bf..2d7ce9a 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") 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..c524fe2 --- /dev/null +++ b/src/main/GUIChildModel.scala @@ -0,0 +1,80 @@ +import java.awt._ +import java.awt.event.{ WindowAdapter, WindowEvent } +import java.util.concurrent.Callable +import javax.swing.{ FocusManager, JFrame, JMenuBar, JOptionPane, WindowConstants } +import org.nlogo.api._ +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: InterfaceComponent = + runUISafely(new Callable[InterfaceComponent]() { + @throws(classOf[Exception]) + def call: InterfaceComponent = { + val component: InterfaceComponent = new ZoomableInterfaceComponent(frame()) + panel = new GUIPanel(component) + frame().add(panel) + val currentlyFocused: Window = FocusManager.getCurrentManager.getActiveWindow + frame().setLocationRelativeTo(currentlyFocused) + frame().setLocationByPlatform(true) + frame().setVisible(true) + currentlyFocused.toFront() + component.open(path) + val c: Array[Component] = component.workspace.viewWidget.controlStrip.getComponents + for (co <- c) { + if (co.isInstanceOf[SpeedSliderPanel]) { + co.setVisible(false) + (co.asInstanceOf[SpeedSliderPanel]).setValue(0) + } + } + frame.pack() + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE) + frame.addWindowListener(new WindowAdapter() { + override def windowClosing(windowEvent: WindowEvent) { + 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 => + } + } + }) + 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 + } + }) + init() + + 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 + + def frame(): JFrame = _frame +} 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/GUIPanel.scala b/src/main/GUIPanel.scala new file mode 100644 index 0000000..b1b6f97 --- /dev/null +++ b/src/main/GUIPanel.scala @@ -0,0 +1,26 @@ +import org.nlogo.app.CommandCenter +import org.nlogo.lite.InterfaceComponent +import org.nlogo.window.{Zoomable, Events} +import javax.swing._ +import java.awt._ +import java.awt.event.ActionEvent + +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/ZoomableInterfaceComponent.scala b/src/main/ZoomableInterfaceComponent.scala new file mode 100644 index 0000000..a42dfdd --- /dev/null +++ b/src/main/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) + } +} From 4076d1ecfedfcf1910cb8ffa6b3d516d523fdd33 Mon Sep 17 00:00:00 2001 From: Robert Grider Date: Thu, 21 May 2015 10:45:29 -0500 Subject: [PATCH 2/9] Adds the ability to open up LevelSpace code tabs --- src/main/BackingModelManager.scala | 62 ++++++ src/main/ChildModel.java | 10 + src/main/GUIChildModel.scala | 1 + src/main/LevelsSpace.java | 28 ++- src/main/{ => gui}/GUIPanel.scala | 13 +- src/main/gui/LevelSpaceMenu.scala | 196 ++++++++++++++++++ src/main/gui/ModelProceduresTab.scala | 116 +++++++++++ .../ZoomableInterfaceComponent.scala | 0 8 files changed, 416 insertions(+), 10 deletions(-) create mode 100644 src/main/BackingModelManager.scala rename src/main/{ => gui}/GUIPanel.scala (92%) create mode 100644 src/main/gui/LevelSpaceMenu.scala create mode 100644 src/main/gui/ModelProceduresTab.scala rename src/main/{ => gui}/ZoomableInterfaceComponent.scala (100%) diff --git a/src/main/BackingModelManager.scala b/src/main/BackingModelManager.scala new file mode 100644 index 0000000..5b5d2df --- /dev/null +++ b/src/main/BackingModelManager.scala @@ -0,0 +1,62 @@ +import gui.{ModelProceduresTab, LevelSpaceMenu} + +import org.nlogo.app.App +import org.nlogo.workspace.AbstractWorkspace + +import scala.collection.JavaConversions._ +import scala.collection.parallel.mutable.ParHashMap + +class BackingModelManager extends gui.ModelManager { + val guiComponent = new LevelSpaceMenu(App.app.tabs, this) + val backingModels = ParHashMap.empty[String, (ChildModel, ModelProceduresTab)] + var openModels = Map.empty[String, ChildModel] + + def updateChildModels(indexedModels: java.util.HashMap[java.lang.Integer, ChildModel]): Unit = { + val models = indexedModels.values + val modelPaths: Seq[String] = models.map(_.workspace().getModelPath).toSeq + 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 & openModelPaths).foreach(replaceTabAtPath) + (newlyOpenedPaths & openModelPaths).foreach(replaceTabAtPath) + guiComponent.addMenuItemsForOpenModels( + models.map(_.workspace().getModelPath).toSeq) + } + + private def replaceTabAtPath(filePath: String) = + guiComponent.replaceTab(backingModels(filePath)._2) + + def openModelPaths = backingModels.seq.keySet + + def existingTab(filePath: String): Option[ModelProceduresTab] = + 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) + } + } +} + diff --git a/src/main/ChildModel.java b/src/main/ChildModel.java index 241713d..ee6f653 100644 --- a/src/main/ChildModel.java +++ b/src/main/ChildModel.java @@ -114,6 +114,13 @@ void checkResult(Object reporterResult) throws ExtensionException { } } + volatile private boolean jobThreadFinished = false; + volatile private boolean uiThreadFinished = false; + + final public boolean isDead() { + return jobThreadFinished && uiThreadFinished; + } + final public void kill() throws ExtensionException, HaltException { // We can't run this synchronously at all. I kept getting freezes when closing/quitting/opening new models // through the GUI. It looks like the EDT can't wait for the job thread to die. BCH 1/15/2015 @@ -131,6 +138,8 @@ public Object call() throws Exception { } } catch (InterruptedException e) { // ok + } finally { + jobThreadFinished = false; } return null; } @@ -142,6 +151,7 @@ public Object call() { if (frame() != null) { frame().dispose(); } + uiThreadFinished = true; return null; } }); diff --git a/src/main/GUIChildModel.scala b/src/main/GUIChildModel.scala index c524fe2..5e75f43 100644 --- a/src/main/GUIChildModel.scala +++ b/src/main/GUIChildModel.scala @@ -2,6 +2,7 @@ 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.lite.{LiteWorkspace, InterfaceComponent} import org.nlogo.nvm.HaltException diff --git a/src/main/LevelsSpace.java b/src/main/LevelsSpace.java index 4b7dac6..e46bc27 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,7 +24,7 @@ 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; @@ -56,6 +54,8 @@ public void actionPerformed(ActionEvent arg0) { } }; + private static BackingModelManager modelManager = new BackingModelManager(); + @Override public void load(PrimitiveManager primitiveManager) throws ExtensionException { primitiveManager.addPrimitive("ask", new Ask()); @@ -137,6 +137,7 @@ public static int castToId(Object id) throws ExtensionException { @Override public void unload(ExtensionManager arg0) throws ExtensionException { + App.app().frame().getJMenuBar().remove(modelManager.guiComponent()); if (haltButton != null) { haltButton.removeActionListener(haltListener); } @@ -188,6 +189,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 +201,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 +432,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 +669,12 @@ public ExtensionObject readExtensionObject(ExtensionManager arg0, @Override public void runOnce(ExtensionManager arg0) throws ExtensionException { + modelManager.updateChildModels(models); + 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/GUIPanel.scala b/src/main/gui/GUIPanel.scala similarity index 92% rename from src/main/GUIPanel.scala rename to src/main/gui/GUIPanel.scala index b1b6f97..8ae1a4b 100644 --- a/src/main/GUIPanel.scala +++ b/src/main/gui/GUIPanel.scala @@ -1,9 +1,12 @@ -import org.nlogo.app.CommandCenter -import org.nlogo.lite.InterfaceComponent -import org.nlogo.window.{Zoomable, Events} -import javax.swing._ +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) @@ -19,7 +22,7 @@ class GUIPanel(ws: InterfaceComponent) extends JPanel with Events.OutputEvent.Ha def handle(outputEvent: Events.OutputEvent): Unit = { if (outputEvent.clear) - cc.output.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..96aba0c --- /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.{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[ModelProceduresTab] + 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[ModelProceduresTab] = + filePath.flatMap(path => locateExistingTab(path) orElse createNewTab(path)) + + private def locateExistingTab(path: String): Option[ModelProceduresTab] = + modelManager.existingTab(path) + + private def createNewTab(path: String): Option[ModelProceduresTab] = { + 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[ModelProceduresTab] = + 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/ZoomableInterfaceComponent.scala b/src/main/gui/ZoomableInterfaceComponent.scala similarity index 100% rename from src/main/ZoomableInterfaceComponent.scala rename to src/main/gui/ZoomableInterfaceComponent.scala From e5a216fa7b29f97f2261763993a0a9fd85089ec5 Mon Sep 17 00:00:00 2001 From: Robert Grider Date: Thu, 11 Jun 2015 08:50:01 -0500 Subject: [PATCH 3/9] Fix NPE when opening GUI child model, minimize LevelSpace memory usage by avoiding anonymous classes --- src/main/ChildModel.java | 2 +- src/main/GUIChildModel.scala | 96 +++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/src/main/ChildModel.java b/src/main/ChildModel.java index ee6f653..311344a 100644 --- a/src/main/ChildModel.java +++ b/src/main/ChildModel.java @@ -275,7 +275,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.scala b/src/main/GUIChildModel.scala index 5e75f43..341d7b7 100644 --- a/src/main/GUIChildModel.scala +++ b/src/main/GUIChildModel.scala @@ -4,6 +4,7 @@ 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 @@ -17,54 +18,59 @@ class GUIChildModel @throws(classOf[InterruptedException]) @throws(classOf[Exten final val _frame: JFrame = new JFrame var panel: GUIPanel = null + val component = runUISafely(new RunGUIChildModel) - val component: InterfaceComponent = - runUISafely(new Callable[InterfaceComponent]() { - @throws(classOf[Exception]) - def call: InterfaceComponent = { - val component: InterfaceComponent = new ZoomableInterfaceComponent(frame()) - panel = new GUIPanel(component) - frame().add(panel) - val currentlyFocused: Window = FocusManager.getCurrentManager.getActiveWindow - frame().setLocationRelativeTo(currentlyFocused) - frame().setLocationByPlatform(true) - frame().setVisible(true) - currentlyFocused.toFront() - component.open(path) - val c: Array[Component] = component.workspace.viewWidget.controlStrip.getComponents - for (co <- c) { - if (co.isInstanceOf[SpeedSliderPanel]) { - co.setVisible(false) - (co.asInstanceOf[SpeedSliderPanel]).setValue(0) - } - } - frame.pack() - frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE) - frame.addWindowListener(new WindowAdapter() { - override def windowClosing(windowEvent: WindowEvent) { - 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 => - } + init() + + class 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 => } - }) - 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 + case 1 => hide() + case 2 => } - }) - init() + } + } def setSpeed(d: Double): Unit = { val c: Array[Component] = component.workspace.viewWidget.controlStrip.getComponents From 96ad49378dbd9e2b1209dd5765c30c1fb2dd0e82 Mon Sep 17 00:00:00 2001 From: Robert Grider Date: Wed, 17 Jun 2015 14:50:24 -0500 Subject: [PATCH 4/9] Depend on LS1 release of NetLogo --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 2d7ce9a..507674e 100644 --- a/build.sbt +++ b/build.sbt @@ -15,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" From 1a49e123ae4b780b4792e8c15c786b052e6af02c Mon Sep 17 00:00:00 2001 From: Robert Grider Date: Thu, 18 Jun 2015 08:55:50 -0500 Subject: [PATCH 5/9] Fixes for code review --- src/main/BackingModelManager.scala | 22 ++++++++++++++++++---- src/main/ChildModel.java | 10 ---------- src/main/GUIChildModel.scala | 25 +++++++++++-------------- src/main/LevelsSpace.java | 15 ++++++++++----- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/main/BackingModelManager.scala b/src/main/BackingModelManager.scala index 5b5d2df..8eb0433 100644 --- a/src/main/BackingModelManager.scala +++ b/src/main/BackingModelManager.scala @@ -1,13 +1,20 @@ import gui.{ModelProceduresTab, LevelSpaceMenu} +import java.util.{ Map => JMap } + import org.nlogo.app.App import org.nlogo.workspace.AbstractWorkspace import scala.collection.JavaConversions._ import scala.collection.parallel.mutable.ParHashMap -class BackingModelManager extends gui.ModelManager { - val guiComponent = new LevelSpaceMenu(App.app.tabs, this) +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] @@ -17,8 +24,8 @@ class BackingModelManager extends gui.ModelManager { 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 & openModelPaths).foreach(replaceTabAtPath) - (newlyOpenedPaths & openModelPaths).foreach(replaceTabAtPath) + (closedModelsPaths intersect openModelPaths).foreach(replaceTabAtPath) + (newlyOpenedPaths intersect openModelPaths).foreach(replaceTabAtPath) guiComponent.addMenuItemsForOpenModels( models.map(_.workspace().getModelPath).toSeq) } @@ -60,3 +67,10 @@ class BackingModelManager extends gui.ModelManager { } } +// 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 311344a..f2abb30 100644 --- a/src/main/ChildModel.java +++ b/src/main/ChildModel.java @@ -114,13 +114,6 @@ void checkResult(Object reporterResult) throws ExtensionException { } } - volatile private boolean jobThreadFinished = false; - volatile private boolean uiThreadFinished = false; - - final public boolean isDead() { - return jobThreadFinished && uiThreadFinished; - } - final public void kill() throws ExtensionException, HaltException { // We can't run this synchronously at all. I kept getting freezes when closing/quitting/opening new models // through the GUI. It looks like the EDT can't wait for the job thread to die. BCH 1/15/2015 @@ -138,8 +131,6 @@ public Object call() throws Exception { } } catch (InterruptedException e) { // ok - } finally { - jobThreadFinished = false; } return null; } @@ -151,7 +142,6 @@ public Object call() { if (frame() != null) { frame().dispose(); } - uiThreadFinished = true; return null; } }); diff --git a/src/main/GUIChildModel.scala b/src/main/GUIChildModel.scala index 341d7b7..be94147 100644 --- a/src/main/GUIChildModel.scala +++ b/src/main/GUIChildModel.scala @@ -15,24 +15,24 @@ 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 + final val frame: JFrame = new JFrame var panel: GUIPanel = null - val component = runUISafely(new RunGUIChildModel) + val component = runUISafely(RunGUIChildModel) init() - class RunGUIChildModel extends Callable[InterfaceComponent] { + object RunGUIChildModel extends Callable[InterfaceComponent] { @throws(classOf[Exception]) def call: InterfaceComponent = { - val component: InterfaceComponent = new ZoomableInterfaceComponent(frame()) + val component: InterfaceComponent = new ZoomableInterfaceComponent(frame) panel = new GUIPanel(component) - frame().add(panel) + frame.add(panel) val currentlyFocused: Window = Option(KeyboardFocusManager.getCurrentKeyboardFocusManager.getActiveWindow).getOrElse(App.app.frame) - frame().setLocationRelativeTo(currentlyFocused) - frame().setLocationByPlatform(true) - frame().setVisible(true) + frame.setLocationRelativeTo(currentlyFocused) + frame.setLocationByPlatform(true) + frame.setVisible(true) currentlyFocused.toFront() component.open(path) val c: Array[Component] = component.workspace.viewWidget.controlStrip.getComponents @@ -42,9 +42,9 @@ class GUIChildModel @throws(classOf[InterruptedException]) @throws(classOf[Exten ssp.setValue(0) case _ => } - frame().pack() - frame().setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE) - frame().addWindowListener(new GUIWindowAdapter) + 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]) @@ -53,7 +53,6 @@ class GUIChildModel @throws(classOf[InterruptedException]) @throws(classOf[Exten } } - class GUIWindowAdapter extends WindowAdapter { override def windowClosing(windowEvent: WindowEvent): Unit = { val options: Array[AnyRef] = Array("Close Model", "Run in Background", "Cancel") @@ -82,6 +81,4 @@ class GUIChildModel @throws(classOf[InterruptedException]) @throws(classOf[Exten } def workspace: AbstractWorkspace = component.workspace - - def frame(): JFrame = _frame } diff --git a/src/main/LevelsSpace.java b/src/main/LevelsSpace.java index e46bc27..4276b47 100644 --- a/src/main/LevelsSpace.java +++ b/src/main/LevelsSpace.java @@ -30,6 +30,7 @@ import org.nlogo.window.SpeedSliderPanel; import org.nlogo.window.ViewUpdatePanel; +import gui.ModelManager; public class LevelsSpace implements org.nlogo.api.ClassManager { @@ -54,7 +55,7 @@ public void actionPerformed(ActionEvent arg0) { } }; - private static BackingModelManager modelManager = new BackingModelManager(); + private static LSModelManager modelManager = useGUI() ? new BackingModelManager() : new HeadlessBackingModelManager(); @Override public void load(PrimitiveManager primitiveManager) throws ExtensionException { @@ -137,7 +138,9 @@ public static int castToId(Object id) throws ExtensionException { @Override public void unload(ExtensionManager arg0) throws ExtensionException { - App.app().frame().getJMenuBar().remove(modelManager.guiComponent()); + if (useGUI()) { + App.app().frame().getJMenuBar().remove(modelManager.guiComponent()); + } if (haltButton != null) { haltButton.removeActionListener(haltListener); } @@ -671,9 +674,11 @@ public ExtensionObject readExtensionObject(ExtensionManager arg0, public void runOnce(ExtensionManager arg0) throws ExtensionException { modelManager.updateChildModels(models); - final JMenuBar menuBar = App.app().frame().getJMenuBar(); - if (menuBar.getComponentIndex(modelManager.guiComponent()) == -1) { - menuBar.add(modelManager.guiComponent()); + if (useGUI()) { + final JMenuBar menuBar = App.app().frame().getJMenuBar(); + if (menuBar.getComponentIndex(modelManager.guiComponent()) == -1) { + menuBar.add(modelManager.guiComponent()); + } } } From 3c965c33f9d620b3357805d79f1b930b9761ce28 Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Sun, 21 Jun 2015 14:27:08 -0500 Subject: [PATCH 6/9] Fix model menu population --- src/main/BackingModelManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/BackingModelManager.scala b/src/main/BackingModelManager.scala index 8eb0433..55e8a7b 100644 --- a/src/main/BackingModelManager.scala +++ b/src/main/BackingModelManager.scala @@ -18,7 +18,7 @@ class BackingModelManager extends LSModelManager { val backingModels = ParHashMap.empty[String, (ChildModel, ModelProceduresTab)] var openModels = Map.empty[String, ChildModel] - def updateChildModels(indexedModels: java.util.HashMap[java.lang.Integer, ChildModel]): Unit = { + override def updateChildModels(indexedModels: JMap[java.lang.Integer, ChildModel]): Unit = { val models = indexedModels.values val modelPaths: Seq[String] = models.map(_.workspace().getModelPath).toSeq val closedModelsPaths = (openModels.values.toSet &~ models.toSet).map(_.workspace().getModelPath) From a5765c3db2fadc4567c94c49fcbd97d3ff7289be Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Mon, 22 Jun 2015 12:51:05 -0500 Subject: [PATCH 7/9] Remove duplicate entries in LS menu --- src/main/BackingModelManager.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/BackingModelManager.scala b/src/main/BackingModelManager.scala index 55e8a7b..969662a 100644 --- a/src/main/BackingModelManager.scala +++ b/src/main/BackingModelManager.scala @@ -19,15 +19,17 @@ class BackingModelManager extends LSModelManager { var openModels = Map.empty[String, ChildModel] override def updateChildModels(indexedModels: JMap[java.lang.Integer, ChildModel]): Unit = { - val models = indexedModels.values - val modelPaths: Seq[String] = models.map(_.workspace().getModelPath).toSeq - 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 + 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( - models.map(_.workspace().getModelPath).toSeq) + guiComponent.addMenuItemsForOpenModels(modelPaths) } private def replaceTabAtPath(filePath: String) = From 6894d49eace9917b0c11ca88fb4328ab349bb991 Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Mon, 22 Jun 2015 13:20:50 -0500 Subject: [PATCH 8/9] Prevent child models from opening LS menus --- src/main/LevelsSpace.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/LevelsSpace.java b/src/main/LevelsSpace.java index 4276b47..1e0cf5a 100644 --- a/src/main/LevelsSpace.java +++ b/src/main/LevelsSpace.java @@ -119,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); @@ -138,7 +149,7 @@ public static int castToId(Object id) throws ExtensionException { @Override public void unload(ExtensionManager arg0) throws ExtensionException { - if (useGUI()) { + if (useGUI() && isMainModel()) { App.app().frame().getJMenuBar().remove(modelManager.guiComponent()); } if (haltButton != null) { @@ -674,7 +685,7 @@ public ExtensionObject readExtensionObject(ExtensionManager arg0, public void runOnce(ExtensionManager arg0) throws ExtensionException { modelManager.updateChildModels(models); - if (useGUI()) { + if (useGUI() && isMainModel()) { final JMenuBar menuBar = App.app().frame().getJMenuBar(); if (menuBar.getComponentIndex(modelManager.guiComponent()) == -1) { menuBar.add(modelManager.guiComponent()); From c3f78f45217e34cd31b18a246e4749e74209f29f Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Mon, 22 Jun 2015 14:11:32 -0500 Subject: [PATCH 9/9] Reuse main model's code tab when user tries to open it --- src/main/BackingModelManager.scala | 9 ++++++--- src/main/gui/LevelSpaceMenu.scala | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/BackingModelManager.scala b/src/main/BackingModelManager.scala index 969662a..270f120 100644 --- a/src/main/BackingModelManager.scala +++ b/src/main/BackingModelManager.scala @@ -2,7 +2,7 @@ import gui.{ModelProceduresTab, LevelSpaceMenu} import java.util.{ Map => JMap } -import org.nlogo.app.App +import org.nlogo.app.{ProceduresTab, App} import org.nlogo.workspace.AbstractWorkspace import scala.collection.JavaConversions._ @@ -37,8 +37,11 @@ class BackingModelManager extends LSModelManager { def openModelPaths = backingModels.seq.keySet - def existingTab(filePath: String): Option[ModelProceduresTab] = - backingModels.get(filePath).map(_._2) + 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)) diff --git a/src/main/gui/LevelSpaceMenu.scala b/src/main/gui/LevelSpaceMenu.scala index 96aba0c..156f1f3 100644 --- a/src/main/gui/LevelSpaceMenu.scala +++ b/src/main/gui/LevelSpaceMenu.scala @@ -7,7 +7,7 @@ import javax.swing._ import org.nlogo.api.ModelSections.{BufSaveable, Saveable} import org.nlogo.api.{CompilerException, ExtensionException, ModelReader, ModelSections, Shape, Version} -import org.nlogo.app.{ModelSaver, App, Tabs} +import org.nlogo.app.{ProceduresTab, ModelSaver, App, Tabs} import org.nlogo.awt.UserCancelException import org.nlogo.shape.{VectorShape, LinkShape} import org.nlogo.swing.FileDialog @@ -18,7 +18,7 @@ import scala.collection.JavaConversions._ trait ModelManager { def removeTab(tab: ModelProceduresTab): Unit - def existingTab(filePath: String): Option[ModelProceduresTab] + def existingTab(filePath: String): Option[ProceduresTab] def registerTab(filePath: String) (f: AbstractWorkspace => ModelProceduresTab): Option[ModelProceduresTab] } @@ -71,13 +71,13 @@ object LevelSpaceMenu { def filePath: Option[String] - def actingTab: Option[ModelProceduresTab] = + def actingTab: Option[ProceduresTab] = filePath.flatMap(path => locateExistingTab(path) orElse createNewTab(path)) - private def locateExistingTab(path: String): Option[ModelProceduresTab] = + private def locateExistingTab(path: String): Option[ProceduresTab] = modelManager.existingTab(path) - private def createNewTab(path: String): Option[ModelProceduresTab] = { + private def createNewTab(path: String): Option[ProceduresTab] = { modelManager.registerTab(path) { workspace => val tab = new ModelProceduresTab(workspace, tabs, modelManager) tabs.addTab(tab.tabName, tab) @@ -99,7 +99,7 @@ object LevelSpaceMenu { override def filePath: Option[String] = selectFile - override def actingTab: Option[ModelProceduresTab] = + override def actingTab: Option[ProceduresTab] = try { super.actingTab } catch {