| author | wenzelm | 
| Wed, 30 Dec 2015 20:12:26 +0100 | |
| changeset 61992 | 6d02bb8b5fe1 | 
| parent 61176 | 9791f631c20d | 
| child 66205 | e9fa94f43a15 | 
| permissions | -rw-r--r-- | 
| 59202 | 1 | /* Title: Tools/Graphview/graph_panel.scala | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 2 | Author: Markus Kaiser, TU Muenchen | 
| 59240 | 3 | Author: Makarius | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 4 | |
| 59461 | 5 | GUI panel for graph layout. | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 6 | */ | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 7 | |
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 8 | package isabelle.graphview | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 9 | |
| 55618 | 10 | |
| 49558 | 11 | import isabelle._ | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 12 | |
| 59441 | 13 | import java.awt.{Dimension, Graphics2D, Point, Rectangle}
 | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 14 | import java.awt.geom.{AffineTransform, Point2D}
 | 
| 59408 | 15 | import javax.imageio.ImageIO | 
| 59225 | 16 | import javax.swing.{JScrollPane, JComponent, SwingUtilities}
 | 
| 59408 | 17 | import javax.swing.border.EmptyBorder | 
| 49729 | 18 | |
| 59408 | 19 | import scala.swing.{BorderPanel, Button, CheckBox, Action, FileChooser, Panel, ScrollPane}
 | 
| 59253 | 20 | import scala.swing.event.{Event, Key, MousePressed, MouseDragged, MouseClicked, MouseEvent}
 | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 21 | |
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 22 | |
| 59459 | 23 | class Graph_Panel(val graphview: Graphview) extends BorderPanel | 
| 49729 | 24 | {
 | 
| 59408 | 25 | graph_panel => | 
| 26 | ||
| 49729 | 27 | |
| 59398 | 28 | |
| 59408 | 29 | /** graph **/ | 
| 59398 | 30 | |
| 59408 | 31 | /* painter */ | 
| 59243 
21ef04bd4e17
recovered tooltip from 6e77ddb1e3fb: non-null default is required as prerequisite;
 wenzelm parents: 
59241diff
changeset | 32 | |
| 59408 | 33 | private val painter = new Panel | 
| 34 |   {
 | |
| 35 | override def paint(gfx: Graphics2D) | |
| 36 |     {
 | |
| 37 | super.paintComponent(gfx) | |
| 38 | ||
| 59459 | 39 | gfx.setColor(graphview.background_color) | 
| 59408 | 40 | gfx.fillRect(0, 0, peer.getWidth, peer.getHeight) | 
| 41 | ||
| 42 | gfx.transform(Transform()) | |
| 59460 | 43 | graphview.paint(gfx) | 
| 59408 | 44 | } | 
| 49729 | 45 | } | 
| 46 | ||
| 59408 | 47 | def set_preferred_size() | 
| 48 |   {
 | |
| 59459 | 49 | val box = graphview.bounding_box() | 
| 59408 | 50 | val s = Transform.scale_discrete | 
| 51 | painter.preferredSize = new Dimension((box.width * s).ceil.toInt, (box.height * s).ceil.toInt) | |
| 52 | painter.revalidate() | |
| 53 | } | |
| 59398 | 54 | |
| 59408 | 55 | def refresh() | 
| 56 |   {
 | |
| 57 |     if (painter != null) {
 | |
| 58 | set_preferred_size() | |
| 59 | painter.repaint() | |
| 60 | } | |
| 61 | } | |
| 59237 
ac135eff1ffb
clarified mouse wheel: conventional scrolling, not scaling;
 wenzelm parents: 
59234diff
changeset | 62 | |
| 59398 | 63 | def scroll_to_node(node: Graph_Display.Node) | 
| 49735 
30e2f3f1c623
more precise repaint and revalidate -- the latter is important to keep in sync with content update;
 wenzelm parents: 
49733diff
changeset | 64 |   {
 | 
| 59459 | 65 | val gap = graphview.metrics.gap | 
| 66 | val info = graphview.layout.get_node(node) | |
| 59398 | 67 | |
| 68 | val t = Transform() | |
| 69 | val p = | |
| 70 | t.transform(new Point2D.Double(info.x - info.width2 - gap, info.y - info.height2 - gap), null) | |
| 71 | val q = | |
| 72 | t.transform(new Point2D.Double(info.x + info.width2 + gap, info.y + info.height2 + gap), null) | |
| 73 | ||
| 59408 | 74 | painter.peer.scrollRectToVisible( | 
| 59398 | 75 | new Rectangle(p.getX.toInt, p.getY.toInt, | 
| 76 | (q.getX - p.getX).ceil.toInt, (q.getY - p.getY).ceil.toInt)) | |
| 49735 
30e2f3f1c623
more precise repaint and revalidate -- the latter is important to keep in sync with content update;
 wenzelm parents: 
49733diff
changeset | 77 | } | 
| 
30e2f3f1c623
more precise repaint and revalidate -- the latter is important to keep in sync with content update;
 wenzelm parents: 
49733diff
changeset | 78 | |
| 50470 | 79 | |
| 59408 | 80 | /* scrollable graph pane */ | 
| 81 | ||
| 82 |   private val graph_pane: ScrollPane = new ScrollPane(painter) {
 | |
| 83 | tooltip = "" | |
| 50478 | 84 | |
| 59408 | 85 |     override lazy val peer: JScrollPane = new JScrollPane with SuperMixin {
 | 
| 86 | override def getToolTipText(event: java.awt.event.MouseEvent): String = | |
| 59459 | 87 |         graphview.find_node(Transform.pane_to_graph_coordinates(event.getPoint)) match {
 | 
| 59408 | 88 | case Some(node) => | 
| 59459 | 89 |             graphview.model.full_graph.get_node(node) match {
 | 
| 59408 | 90 | case Nil => null | 
| 91 | case content => | |
| 59459 | 92 | graphview.make_tooltip(graph_pane.peer, event.getX, event.getY, content) | 
| 59408 | 93 | } | 
| 94 | case None => null | |
| 95 | } | |
| 59218 | 96 | } | 
| 50470 | 97 | |
| 59408 | 98 | horizontalScrollBarPolicy = ScrollPane.BarPolicy.Always | 
| 99 | verticalScrollBarPolicy = ScrollPane.BarPolicy.Always | |
| 100 | ||
| 101 | listenTo(mouse.moves) | |
| 102 | listenTo(mouse.clicks) | |
| 103 | reactions += | |
| 59218 | 104 |     {
 | 
| 59408 | 105 | case MousePressed(_, p, _, _, _) => | 
| 106 | Mouse_Interaction.pressed(p) | |
| 107 | painter.repaint() | |
| 108 | case MouseDragged(_, to, _) => | |
| 109 | Mouse_Interaction.dragged(to) | |
| 110 | painter.repaint() | |
| 111 | case e @ MouseClicked(_, p, m, n, _) => | |
| 112 | Mouse_Interaction.clicked(p, m, n, SwingUtilities.isRightMouseButton(e.peer)) | |
| 113 | painter.repaint() | |
| 114 | } | |
| 50470 | 115 | |
| 59408 | 116 | contents = painter | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 117 | } | 
| 59408 | 118 | graph_pane.peer.getVerticalScrollBar.setUnitIncrement(10) | 
| 50470 | 119 | |
| 59408 | 120 | |
| 121 | /* transform */ | |
| 49733 
38a68e6593be
prefer synchronous Mutator_Event.Bus on Swing_Thread;
 wenzelm parents: 
49732diff
changeset | 122 | |
| 59398 | 123 | def rescale(s: Double) | 
| 124 |   {
 | |
| 125 | Transform.scale = s | |
| 126 | if (zoom != null) zoom.set_item((Transform.scale_discrete * 100).floor.toInt) | |
| 127 | refresh() | |
| 128 | } | |
| 50470 | 129 | |
| 59399 | 130 | def fit_to_window() | 
| 131 |   {
 | |
| 132 | Transform.fit_to_window() | |
| 133 | refresh() | |
| 134 | } | |
| 135 | ||
| 50469 | 136 | private object Transform | 
| 137 |   {
 | |
| 50474 
6ee044e2d1a7
initial layout coordinates more like old browser;
 wenzelm parents: 
50470diff
changeset | 138 | private var _scale: Double = 1.0 | 
| 50477 | 139 | def scale: Double = _scale | 
| 140 | def scale_=(s: Double) | |
| 50468 | 141 |     {
 | 
| 59241 
541b95e94dc7
clarified bounding box, similar to old graph browser;
 wenzelm parents: 
59240diff
changeset | 142 | _scale = (s min 10.0) max 0.1 | 
| 50468 | 143 | } | 
| 59255 
db265648139c
clarified fit_to_window: floor scale within window bounds;
 wenzelm parents: 
59253diff
changeset | 144 | |
| 50477 | 145 | def scale_discrete: Double = | 
| 59286 
ac74eedb910a
GUI.imitate_font: more explicit result size, e.g. relevant for caching;
 wenzelm parents: 
59262diff
changeset | 146 |     {
 | 
| 59459 | 147 | val font_height = GUI.line_metrics(graphview.metrics.font).getHeight.toInt | 
| 59286 
ac74eedb910a
GUI.imitate_font: more explicit result size, e.g. relevant for caching;
 wenzelm parents: 
59262diff
changeset | 148 | (scale * font_height).floor / font_height | 
| 
ac74eedb910a
GUI.imitate_font: more explicit result size, e.g. relevant for caching;
 wenzelm parents: 
59262diff
changeset | 149 | } | 
| 50470 | 150 | |
| 59218 | 151 | def apply() = | 
| 152 |     {
 | |
| 59459 | 153 | val box = graphview.bounding_box() | 
| 59397 | 154 | val t = AffineTransform.getScaleInstance(scale_discrete, scale_discrete) | 
| 155 | t.translate(- box.x, - box.y) | |
| 156 | t | |
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 157 | } | 
| 50470 | 158 | |
| 59218 | 159 | def fit_to_window() | 
| 160 |     {
 | |
| 59459 | 161 | if (graphview.visible_graph.is_empty) | 
| 50491 | 162 | rescale(1.0) | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 163 |       else {
 | 
| 59459 | 164 | val box = graphview.bounding_box() | 
| 59241 
541b95e94dc7
clarified bounding box, similar to old graph browser;
 wenzelm parents: 
59240diff
changeset | 165 | rescale((size.width / box.width) min (size.height / box.height)) | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 166 | } | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 167 | } | 
| 50470 | 168 | |
| 59218 | 169 | def pane_to_graph_coordinates(at: Point2D): Point2D = | 
| 170 |     {
 | |
| 50477 | 171 | val s = Transform.scale_discrete | 
| 59408 | 172 | val p = Transform().inverseTransform(graph_pane.peer.getViewport.getViewPosition, null) | 
| 50470 | 173 | |
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 174 | p.setLocation(p.getX + at.getX / s, p.getY + at.getY / s) | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 175 | p | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 176 | } | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 177 | } | 
| 50470 | 178 | |
| 59398 | 179 | |
| 180 | /* interaction */ | |
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 181 | |
| 59459 | 182 |   graphview.model.Colors.events += { case _ => painter.repaint() }
 | 
| 183 |   graphview.model.Mutators.events += { case _ => painter.repaint() }
 | |
| 59398 | 184 | |
| 185 | private object Mouse_Interaction | |
| 186 |   {
 | |
| 187 | private var draginfo: (Point, List[Graph_Display.Node], List[Layout.Dummy]) = | |
| 188 | (new Point(0, 0), Nil, Nil) | |
| 59253 | 189 | |
| 190 | def pressed(at: Point) | |
| 59218 | 191 |     {
 | 
| 59253 | 192 | val c = Transform.pane_to_graph_coordinates(at) | 
| 193 | val l = | |
| 59459 | 194 |         graphview.find_node(c) match {
 | 
| 59253 | 195 | case Some(node) => | 
| 59459 | 196 | if (graphview.Selection.contains(node)) graphview.Selection.get() | 
| 59253 | 197 | else List(node) | 
| 198 | case None => Nil | |
| 199 | } | |
| 200 | val d = | |
| 201 |         l match {
 | |
| 59459 | 202 | case Nil => graphview.find_dummy(c).toList | 
| 59253 | 203 | case _ => Nil | 
| 204 | } | |
| 205 | draginfo = (at, l, d) | |
| 206 | } | |
| 50470 | 207 | |
| 59398 | 208 | def dragged(to: Point) | 
| 59253 | 209 |     {
 | 
| 59398 | 210 | val (from, p, d) = draginfo | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 211 | |
| 59253 | 212 | val s = Transform.scale_discrete | 
| 59398 | 213 | val dx = to.x - from.x | 
| 214 | val dy = to.y - from.y | |
| 215 | ||
| 59253 | 216 |       (p, d) match {
 | 
| 217 | case (Nil, Nil) => | |
| 59408 | 218 | val r = graph_pane.peer.getViewport.getViewRect | 
| 59302 
4d985afc0565
explict layout graph structure, with dummies and coordinates;
 wenzelm parents: 
59294diff
changeset | 219 | r.translate(- dx, - dy) | 
| 59408 | 220 | painter.peer.scrollRectToVisible(r) | 
| 50470 | 221 | |
| 59253 | 222 | case (Nil, ds) => | 
| 59459 | 223 | ds.foreach(d => graphview.translate_vertex(d, dx / s, dy / s)) | 
| 59253 | 224 | |
| 225 | case (ls, _) => | |
| 59459 | 226 | ls.foreach(l => graphview.translate_vertex(Layout.Node(l), dx / s, dy / s)) | 
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 227 | } | 
| 59398 | 228 | |
| 229 | draginfo = (to, p, d) | |
| 230 | } | |
| 231 | ||
| 232 | def clicked(at: Point, m: Key.Modifiers, clicks: Int, right_click: Boolean) | |
| 233 |     {
 | |
| 234 | val c = Transform.pane_to_graph_coordinates(at) | |
| 235 | ||
| 236 |       if (clicks < 2) {
 | |
| 237 |         if (right_click) {
 | |
| 59403 | 238 | // FIXME | 
| 239 |           if (false) {
 | |
| 59459 | 240 | val menu = Popups(graph_panel, graphview.find_node(c), graphview.Selection.get()) | 
| 59408 | 241 | menu.show(graph_pane.peer, at.x, at.y) | 
| 59403 | 242 | } | 
| 59398 | 243 | } | 
| 244 |         else {
 | |
| 59459 | 245 |           (graphview.find_node(c), m) match {
 | 
| 246 | case (Some(node), Key.Modifier.Control) => graphview.Selection.add(node) | |
| 59398 | 247 | case (None, Key.Modifier.Control) => | 
| 248 | ||
| 59459 | 249 | case (Some(node), Key.Modifier.Shift) => graphview.Selection.add(node) | 
| 59398 | 250 | case (None, Key.Modifier.Shift) => | 
| 251 | ||
| 252 | case (Some(node), _) => | |
| 59459 | 253 | graphview.Selection.clear() | 
| 254 | graphview.Selection.add(node) | |
| 59398 | 255 | case (None, _) => | 
| 59459 | 256 | graphview.Selection.clear() | 
| 59398 | 257 | } | 
| 258 | } | |
| 259 | } | |
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 260 | } | 
| 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 261 | } | 
| 59408 | 262 | |
| 263 | ||
| 264 | ||
| 265 | /** controls **/ | |
| 266 | ||
| 267 | private val mutator_dialog = | |
| 59459 | 268 | new Mutator_Dialog(graphview, graphview.model.Mutators, "Filters", "Hide", false) | 
| 59408 | 269 | private val color_dialog = | 
| 59459 | 270 | new Mutator_Dialog(graphview, graphview.model.Colors, "Colorations") | 
| 59408 | 271 | |
| 272 |   private val chooser = new FileChooser {
 | |
| 273 | fileSelectionMode = FileChooser.SelectionMode.FilesOnly | |
| 274 | title = "Save image (.png or .pdf)" | |
| 275 | } | |
| 276 | ||
| 59409 | 277 |   private val show_content = new CheckBox() {
 | 
| 59459 | 278 | selected = graphview.show_content | 
| 59409 | 279 |     action = Action("Show content") {
 | 
| 59459 | 280 | graphview.show_content = selected | 
| 281 | graphview.update_layout() | |
| 59409 | 282 | refresh() | 
| 283 | } | |
| 284 | tooltip = "Show full node content within graph layout" | |
| 285 | } | |
| 286 | ||
| 287 |   private val show_arrow_heads = new CheckBox() {
 | |
| 59459 | 288 | selected = graphview.show_arrow_heads | 
| 59409 | 289 |     action = Action("Show arrow heads") {
 | 
| 59459 | 290 | graphview.show_arrow_heads = selected | 
| 59409 | 291 | painter.repaint() | 
| 292 | } | |
| 293 | tooltip = "Draw edges with explicit arrow heads" | |
| 294 | } | |
| 295 | ||
| 296 |   private val show_dummies = new CheckBox() {
 | |
| 59459 | 297 | selected = graphview.show_dummies | 
| 59409 | 298 |     action = Action("Show dummies") {
 | 
| 59459 | 299 | graphview.show_dummies = selected | 
| 59409 | 300 | painter.repaint() | 
| 301 | } | |
| 302 | tooltip = "Draw dummy nodes within graph layout, for easier mouse dragging" | |
| 303 | } | |
| 304 | ||
| 305 |   private val save_image = new Button {
 | |
| 306 |     action = Action("Save image") {
 | |
| 307 |       chooser.showSaveDialog(this) match {
 | |
| 59441 | 308 | case FileChooser.Result.Approve => | 
| 59459 | 309 |           try { Graph_File.write(chooser.selectedFile, graphview) }
 | 
| 59441 | 310 |           catch {
 | 
| 311 | case ERROR(msg) => GUI.error_dialog(this.peer, "Error", GUI.scrollable_text(msg)) | |
| 312 | } | |
| 59409 | 313 | case _ => | 
| 314 | } | |
| 315 | } | |
| 316 | tooltip = "Save current graph layout as PNG or PDF" | |
| 317 | } | |
| 318 | ||
| 319 |   private val zoom = new GUI.Zoom_Box { def changed = rescale(0.01 * factor) }
 | |
| 320 | ||
| 321 |   private val fit_window = new Button {
 | |
| 322 |     action = Action("Fit to window") { fit_to_window() }
 | |
| 323 | tooltip = "Zoom to fit window width and height" | |
| 324 | } | |
| 325 | ||
| 326 |   private val update_layout = new Button {
 | |
| 327 |     action = Action("Update layout") {
 | |
| 59459 | 328 | graphview.update_layout() | 
| 59409 | 329 | refresh() | 
| 330 | } | |
| 331 | tooltip = "Regenerate graph layout according to built-in algorithm" | |
| 332 | } | |
| 333 | ||
| 61176 | 334 |   private val editor_style = new CheckBox() {
 | 
| 335 | selected = graphview.editor_style | |
| 336 |     action = Action("Editor style") {
 | |
| 337 | graphview.editor_style = selected | |
| 338 | graphview.update_layout() | |
| 339 | refresh() | |
| 340 | } | |
| 341 | tooltip = "Use editor font and colors for painting" | |
| 342 | } | |
| 343 | ||
| 59409 | 344 |   private val colorations = new Button { action = Action("Colorations") { color_dialog.open } }
 | 
| 345 |   private val filters = new Button { action = Action("Filters") { mutator_dialog.open } }
 | |
| 346 | ||
| 59408 | 347 | private val controls = | 
| 59409 | 348 | new Wrap_Panel(Wrap_Panel.Alignment.Right)(show_content, show_arrow_heads, show_dummies, | 
| 61176 | 349 | save_image, zoom, fit_window, update_layout, editor_style) // FIXME colorations, filters | 
| 59408 | 350 | |
| 351 | ||
| 352 | ||
| 353 | /** main layout **/ | |
| 354 | ||
| 355 | layout(graph_pane) = BorderPanel.Position.Center | |
| 356 | layout(controls) = BorderPanel.Position.North | |
| 357 | ||
| 358 | rescale(1.0) | |
| 49557 
61988f9df94d
added Graphview tool, based on Isabelle/Scala and Swing/Graphics2D;
 Markus Kaiser <markus.kaiser@in.tum.de> parents: diff
changeset | 359 | } |