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