/* Title: Tools/Graphview/graph_panel.scala Author: Markus Kaiser, TU Muenchen Author: Makarius GUI panel for graph layout. */ package isabelle.graphview import isabelle._ import java.awt.{Dimension, Graphics2D, Point, Rectangle} import java.awt.geom.{AffineTransform, Point2D} import javax.imageio.ImageIO import javax.swing.{JScrollPane, JComponent, SwingUtilities} import javax.swing.border.EmptyBorder import scala.swing.{BorderPanel, Button, CheckBox, Action, FileChooser, Panel, ScrollPane} import scala.swing.event.{Event, Key, MousePressed, MouseDragged, MouseClicked, MouseEvent} class Graph_Panel(val graphview: Graphview) extends BorderPanel { graph_panel => /** graph **/ /* painter */ private val painter = new Panel { override def paint(gfx: Graphics2D) { super.paintComponent(gfx) gfx.setColor(graphview.background_color) gfx.fillRect(0, 0, peer.getWidth, peer.getHeight) gfx.transform(Transform()) graphview.paint(gfx) } } def set_preferred_size() { val box = graphview.bounding_box() val s = Transform.scale_discrete painter.preferredSize = new Dimension((box.width * s).ceil.toInt, (box.height * s).ceil.toInt) painter.revalidate() } def refresh() { if (painter != null) { set_preferred_size() painter.repaint() } } def scroll_to_node(node: Graph_Display.Node) { val gap = graphview.metrics.gap val info = graphview.layout.get_node(node) val t = Transform() val p = t.transform(new Point2D.Double(info.x - info.width2 - gap, info.y - info.height2 - gap), null) val q = t.transform(new Point2D.Double(info.x + info.width2 + gap, info.y + info.height2 + gap), null) painter.peer.scrollRectToVisible( new Rectangle(p.getX.toInt, p.getY.toInt, (q.getX - p.getX).ceil.toInt, (q.getY - p.getY).ceil.toInt)) } /* scrollable graph pane */ private val graph_pane: ScrollPane = new ScrollPane(painter) { tooltip = "" override lazy val peer: JScrollPane = new JScrollPane with SuperMixin { override def getToolTipText(event: java.awt.event.MouseEvent): String = graphview.find_node(Transform.pane_to_graph_coordinates(event.getPoint)) match { case Some(node) => graphview.model.full_graph.get_node(node) match { case Nil => null case content => graphview.make_tooltip(graph_pane.peer, event.getX, event.getY, content) } case None => null } } horizontalScrollBarPolicy = ScrollPane.BarPolicy.Always verticalScrollBarPolicy = ScrollPane.BarPolicy.Always listenTo(mouse.moves) listenTo(mouse.clicks) reactions += { case MousePressed(_, p, _, _, _) => Mouse_Interaction.pressed(p) painter.repaint() case MouseDragged(_, to, _) => Mouse_Interaction.dragged(to) painter.repaint() case e @ MouseClicked(_, p, m, n, _) => Mouse_Interaction.clicked(p, m, n, SwingUtilities.isRightMouseButton(e.peer)) painter.repaint() } contents = painter } graph_pane.peer.getVerticalScrollBar.setUnitIncrement(10) /* transform */ def rescale(s: Double) { Transform.scale = s if (zoom != null) zoom.set_item((Transform.scale_discrete * 100).floor.toInt) refresh() } def fit_to_window() { Transform.fit_to_window() refresh() } private object Transform { private var _scale: Double = 1.0 def scale: Double = _scale def scale_=(s: Double) { _scale = (s min 10.0) max 0.1 } def scale_discrete: Double = { val font_height = GUI.line_metrics(graphview.metrics.font).getHeight.toInt (scale * font_height).floor / font_height } def apply() = { val box = graphview.bounding_box() val t = AffineTransform.getScaleInstance(scale_discrete, scale_discrete) t.translate(- box.x, - box.y) t } def fit_to_window() { if (graphview.visible_graph.is_empty) rescale(1.0) else { val box = graphview.bounding_box() rescale((size.width / box.width) min (size.height / box.height)) } } def pane_to_graph_coordinates(at: Point2D): Point2D = { val s = Transform.scale_discrete val p = Transform().inverseTransform(graph_pane.peer.getViewport.getViewPosition, null) p.setLocation(p.getX + at.getX / s, p.getY + at.getY / s) p } } /* interaction */ graphview.model.Colors.events += { case _ => painter.repaint() } graphview.model.Mutators.events += { case _ => painter.repaint() } private object Mouse_Interaction { private var draginfo: (Point, List[Graph_Display.Node], List[Layout.Dummy]) = (new Point(0, 0), Nil, Nil) def pressed(at: Point) { val c = Transform.pane_to_graph_coordinates(at) val l = graphview.find_node(c) match { case Some(node) => if (graphview.Selection.contains(node)) graphview.Selection.get() else List(node) case None => Nil } val d = l match { case Nil => graphview.find_dummy(c).toList case _ => Nil } draginfo = (at, l, d) } def dragged(to: Point) { val (from, p, d) = draginfo val s = Transform.scale_discrete val dx = to.x - from.x val dy = to.y - from.y (p, d) match { case (Nil, Nil) => val r = graph_pane.peer.getViewport.getViewRect r.translate(- dx, - dy) painter.peer.scrollRectToVisible(r) case (Nil, ds) => ds.foreach(d => graphview.translate_vertex(d, dx / s, dy / s)) case (ls, _) => ls.foreach(l => graphview.translate_vertex(Layout.Node(l), dx / s, dy / s)) } draginfo = (to, p, d) } def clicked(at: Point, m: Key.Modifiers, clicks: Int, right_click: Boolean) { val c = Transform.pane_to_graph_coordinates(at) if (clicks < 2) { if (right_click) { // FIXME if (false) { val menu = Popups(graph_panel, graphview.find_node(c), graphview.Selection.get()) menu.show(graph_pane.peer, at.x, at.y) } } else { (graphview.find_node(c), m) match { case (Some(node), Key.Modifier.Control) => graphview.Selection.add(node) case (None, Key.Modifier.Control) => case (Some(node), Key.Modifier.Shift) => graphview.Selection.add(node) case (None, Key.Modifier.Shift) => case (Some(node), _) => graphview.Selection.clear() graphview.Selection.add(node) case (None, _) => graphview.Selection.clear() } } } } } /** controls **/ private val mutator_dialog = new Mutator_Dialog(graphview, graphview.model.Mutators, "Filters", "Hide", false) private val color_dialog = new Mutator_Dialog(graphview, graphview.model.Colors, "Colorations") private val chooser = new FileChooser { fileSelectionMode = FileChooser.SelectionMode.FilesOnly title = "Save image (.png or .pdf)" } private val show_content = new CheckBox() { selected = graphview.show_content action = Action("Show content") { graphview.show_content = selected graphview.update_layout() refresh() } tooltip = "Show full node content within graph layout" } private val show_arrow_heads = new CheckBox() { selected = graphview.show_arrow_heads action = Action("Show arrow heads") { graphview.show_arrow_heads = selected painter.repaint() } tooltip = "Draw edges with explicit arrow heads" } private val show_dummies = new CheckBox() { selected = graphview.show_dummies action = Action("Show dummies") { graphview.show_dummies = selected painter.repaint() } tooltip = "Draw dummy nodes within graph layout, for easier mouse dragging" } private val save_image = new Button { action = Action("Save image") { chooser.showSaveDialog(this) match { case FileChooser.Result.Approve => try { Graph_File.write(chooser.selectedFile, graphview) } catch { case ERROR(msg) => GUI.error_dialog(this.peer, "Error", GUI.scrollable_text(msg)) } case _ => } } tooltip = "Save current graph layout as PNG or PDF" } private val zoom = new GUI.Zoom_Box { def changed = rescale(0.01 * factor) } private val fit_window = new Button { action = Action("Fit to window") { fit_to_window() } tooltip = "Zoom to fit window width and height" } private val update_layout = new Button { action = Action("Update layout") { graphview.update_layout() refresh() } tooltip = "Regenerate graph layout according to built-in algorithm" } private val editor_style = new CheckBox() { selected = graphview.editor_style action = Action("Editor style") { graphview.editor_style = selected graphview.update_layout() refresh() } tooltip = "Use editor font and colors for painting" } private val colorations = new Button { action = Action("Colorations") { color_dialog.open } } private val filters = new Button { action = Action("Filters") { mutator_dialog.open } } private val controls = Wrap_Panel( List(show_content, show_arrow_heads, show_dummies, save_image, zoom, fit_window, update_layout, editor_style)) // FIXME colorations, filters /** main layout **/ layout(graph_pane) = BorderPanel.Position.Center layout(controls) = BorderPanel.Position.North rescale(1.0) }