From 7af147b55877a594dbee0a7e845d368373bce58a Mon Sep 17 00:00:00 2001 From: Robert Grider Date: Fri, 15 May 2015 17:41:51 -0500 Subject: [PATCH] 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) + } +}