diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md
index 322e6d0..82b9a41 100644
--- a/BEdit/AGENTS.md
+++ b/BEdit/AGENTS.md
@@ -66,6 +66,11 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Do not scatter those constants through painters.
+- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
+ They use integer layers below or above graph layer 0. Annotation lines reuse
+ connection routing and absolute `properties.waypoints` semantics. Graph
+ annotations and icon elements share `ShapeOptionsDialog` and `shape_pen` so
+ their style fields and rendering must remain aligned.
- Icon editing uses a fixed 128×128 coordinate space.
- The visible/selectable component hitbox is calculated from vector elements,
not from the complete 128×128 icon canvas.
diff --git a/BEdit/README.md b/BEdit/README.md
index 1262b35..1f9ac7f 100644
--- a/BEdit/README.md
+++ b/BEdit/README.md
@@ -112,26 +112,36 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
- Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then
click an output and input. Angled connections accept intermediate corner
- clicks; right-click cancels an unfinished connection.
-- Double-click a graph component to open its owned subgraph; use **Up** to return.
+ clicks, use right-angle segments, and snap to the graph snapping grid.
+ Right-click cancels an unfinished connection.
+- Select a routed connection to reveal its draggable nodes. Right-click a line
+ to add a node, or right-click a node to delete it.
+- Use **Box**, **Line**, and **Text** to add persistent graph annotations. Lines
+ share the Direct, Angled, and Spline routing controls. Annotation context menus
+ provide the same shape styling as the icon editor and can move annotations
+ through integer layers below or above graph layer 0.
+- Double-click a graph component or select it and use **Down** to open its owned
+ subgraph; use **Up** to return.
- Graph components show **Pointer**, **Input**, and **Output** tools. Select an
interface tool and click the canvas to add a visible internal terminal and a
corresponding external block port. Interface terminals can be moved afterward.
- Right-click a component on the canvas or in Current Document to edit its name,
icon shape, icon text, fill color, and border color. The same dialog can hide
that component's contained subtree from the Libraries tree.
+- The icon editor uses Pointer and click-drag drawing tools. Its toolbar and
+ mouse wheel provide zoom in, zoom out, and fit-to-canvas controls.
- Double-click a text component to edit its input list, output list, and
`implementation.source` JSON.
- Right-click any graph component under Current Document to add nested graph or
text blocks. Any current-document component can also be deleted there.
-- The active document hierarchy has its own Document panel; the Libraries panel
- contains only configured external libraries.
-- Select one or more blocks and press `Ctrl+R`, or use the Transform toolbar, to
+- The left navigator presents the current Document and external Libraries as tabs.
+- Select one or more blocks and press `Ctrl+R`, or use **Rotate** in the graph header, to
rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo.
**Apply JSON** updates that source and participates in undo/redo.
- File → Save writes the complete recursive document to JSON.
- File → Close Document removes the active document and returns to an empty
- workspace. An open graph uses a light gray, 32-unit dotted canvas.
+ workspace. An open graph uses a light gray grid whose visible and snapping
+ spacing are configured separately.
- Edit → Settings → Libraries accepts document files or folders of JSON files.
Every component owns its ports, declarative icon, properties, and child graph:
diff --git a/BEdit/resources/icons/arrow-down.png b/BEdit/resources/icons/arrow-down.png
new file mode 100644
index 0000000..7c9274f
Binary files /dev/null and b/BEdit/resources/icons/arrow-down.png differ
diff --git a/BEdit/resources/icons/arrow-up.png b/BEdit/resources/icons/arrow-up.png
new file mode 100644
index 0000000..758a0d1
Binary files /dev/null and b/BEdit/resources/icons/arrow-up.png differ
diff --git a/BEdit/resources/icons/draw-bezier-curves.png b/BEdit/resources/icons/draw-bezier-curves.png
new file mode 100644
index 0000000..1ab00cc
Binary files /dev/null and b/BEdit/resources/icons/draw-bezier-curves.png differ
diff --git a/BEdit/resources/icons/draw-rectangle.png b/BEdit/resources/icons/draw-rectangle.png
new file mode 100644
index 0000000..6c3cd09
Binary files /dev/null and b/BEdit/resources/icons/draw-rectangle.png differ
diff --git a/BEdit/resources/icons/draw-text.png b/BEdit/resources/icons/draw-text.png
new file mode 100644
index 0000000..4ce5479
Binary files /dev/null and b/BEdit/resources/icons/draw-text.png differ
diff --git a/BEdit/resources/icons/edit-select.png b/BEdit/resources/icons/edit-select.png
new file mode 100644
index 0000000..f52c450
Binary files /dev/null and b/BEdit/resources/icons/edit-select.png differ
diff --git a/BEdit/resources/icons/zoom-in.png b/BEdit/resources/icons/zoom-in.png
new file mode 100644
index 0000000..d90f809
Binary files /dev/null and b/BEdit/resources/icons/zoom-in.png differ
diff --git a/BEdit/resources/icons/zoom-original.png b/BEdit/resources/icons/zoom-original.png
new file mode 100644
index 0000000..c243a32
Binary files /dev/null and b/BEdit/resources/icons/zoom-original.png differ
diff --git a/BEdit/resources/icons/zoom-out.png b/BEdit/resources/icons/zoom-out.png
new file mode 100644
index 0000000..e3eb020
Binary files /dev/null and b/BEdit/resources/icons/zoom-out.png differ
diff --git a/BEdit/resources/resources.qrc b/BEdit/resources/resources.qrc
index 7ba7f7e..4e624d3 100644
--- a/BEdit/resources/resources.qrc
+++ b/BEdit/resources/resources.qrc
@@ -1,14 +1,25 @@
+ icons/arrow-down.png
+ icons/configure.png
+ icons/arrow-up.png
+ icons/document-new.png
+ icons/document-open.png
+ icons/document-save-as.png
+ icons/document-save.png
+ icons/draw-bezier-curves.png
+ icons/draw-rectangle.png
+ icons/draw-text.png
+ icons/edit-copy.png
+ icons/edit-cut.png
icons/edit-paste.png
icons/edit-redo.png
- icons/edit-cut.png
- icons/edit-copy.png
+ icons/edit-select.png
icons/edit-undo.png
- icons/document-save.png
- icons/document-save-as.png
- icons/document-open.png
- icons/document-new.png
+ icons/media-playback-start.png
icons/transform-rotate.png
+ icons/zoom-in.png
+ icons/zoom-original.png
+ icons/zoom-out.png
diff --git a/BEdit/resources/resources_rc.py b/BEdit/resources/resources_rc.py
index 915731e..3752d1e 100644
--- a/BEdit/resources/resources_rc.py
+++ b/BEdit/resources/resources_rc.py
@@ -97,6 +97,124 @@ d\x01\x97\xdb%;\xbb\xf6\x9f\xfe\xea\xe8\xb1b\x99\xdb\
\xdf\x22n\xd7\x80\xca&\x04\x84\xf8\x1f\xf9\x17g\x93$\
jH\x9d\xf5+\x00\x00\x00\x00IEND\xaeB`\
\x82\
+\x00\x00\x077\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\
+\x12%\x1d\xb8\xa17t\x00\x00\x06\xb7IDATX\
+\xc3\xc5\x96mPT\xd7\x19\xc7\x9fs\xee\xdd{w\xef\
+\xee\x22\xb0Kx\x0b\xb0+\xac\x1d\x86\xb6X,\x08:\
+V$\xd1i5\x99\x14-b\x0cVh\xeb\x84L\xda\
+t&\x9dL\x13\x8d\xfd\x82\x9d6\x1f\xd21\x99\x1a\x0d\
+\x12A4N\x09RM:a&\xe9\xb4U\xa9\x81\x08\
+\x81\x00\x01$\x09\xc8\xb2\xbb \xfb\xc6\xbe\xde\xb7\xbd\xf7\
+\x9e~,\x9d1i\x16C\xfb\xfbt\xe6\x9c;s~\
+\xf3\xdc\xff3\xe7\x01H\x02\xb7\xc7\xfd\x85g\x1f\x8f\x8e\
+\xc0j\xc0\xc9\x5cl6\x99w\x07\x82\xfe\xab\x91ph\
+!\x1c\x0a\x92@\xc0K\x96\x96\x16\xdc\xf3\xce\xb96\xb3\
+y]\x15\x00\xc0\xc0@\x7fR\x02\xe8\xab|\xe4\xf5z\
+7\x12\xa2\xbc\xe3Z\xf4\xe5]\xe9\xed[6\x1a\x0c\x08\
+c\x0c,\xcb@4\x1a\x85h,\x06\xb5\xdf\xafJe\
+\x19v,\xa1j\x0fUVT\xf8\xbf6\x01\xb7\xc7\xf3\
+\xb8\xaa\xca\x97\xce\x5c\xfc\xab\xd7n\xb3\xa9\xf5\xb5;\x0d\
+\x1a\x01\xa2i\x0a\x01@@Q\x14\xd2T\x02o\x5cz\
+GXt}\xce\xfepg\x99E\xd1\xa0\xb2\xa6\xba\xe6\
+\xc3\xfb\x16\x98\xbd3S#\x8a\xe2\xdfZ\xdf\xeaw\xfd\
+\xb2\xf9\x09\x1dB\xa0\x22\x0c*FX\xc3\x88\x10\x02\x00\
+@\x00i\x1a`@@E\x22<~\xf5\xf49\xed\x91\
+m\xebs\xe5\x84V\x18\x8dFf\x9b\x1a\x7f\x92\xbc\xc0\
+\xe0\xe0-0\xa4\x98iJI$:{\xc7]\x87\x0f\
+\xee\xd5\x08\xd1$\x8b\x85\x13cQ\x190\x06\x15cD\
+\x10\x00h\x04@\xd3\x08e21(\x10\x14\xf4\x92,\
+\xb3\x1d\xed\x1d\xfa2G\xaap\xe8P\xa3}U!,\
+/\xaf\x00)\x1eo\xb992\x13\xd9\x5c\xb95\x1e\x13\
+\x94PF\x16\xb7\xb4\xa9bW\xf1:+s\x17Q\xc8\
+\x8b(\xb4\x04\x14ZB\x14\xf2\xe5Y\x19W\xd9\xe6]\
+\xc5F\xb3\xc1\xa7hTh\xd3\x96\x87\xc32\xd1\xe5]\
+\xb8p\xfe\x89\xdf\xbftbu]\xa0*\xd2\xb3\xff\x9c\
+\xe6\xe7\xd2-i<\xad\xe7\xfc\xcdO\x1d/\xdb\xb1{\
+\xaf\x87\x10\xb8\x8buh\x11\xeb\xd0\x02\xc5\xa0\x85\xf4u\
+\xb4\xf3\xc0\x91\x17\xbf\xb7\xe3\x07{]\x04A\x90\xd6a\
+\xdeQd\x13'\x9d\xbcgci\xe9\xd3\xcf\xff\xfa\xc5\
+\xe4\x05\xae\xdf\xb8V\x10\x0d\xc7\x18W\xd4\xe8\x99\x0fH\
+\x9ep\x8c\x0f\xb6\xb7\xb6\x88:.-\x801xh\x06\
+\xbb3\x0d\xd4\xe7\x14\x8d\xddqE\x9b\xef8{B\xd0\
+\x19R\xfd\x06=\x08\xa9\x9c\x8e2\xb2\x94zWJ\xf5\
+\xc4\xe3\x91\xaaU\xfd\x02\x9e\xe77\xcc{#j\xba5\
+C\x1cq\xf23\x93>e\xe2\x13?\xb4H\x09\x99g\
+\x00\xdcV\x1dv=z\xf0\xd9\xe7\x1f\xa0\x91K\x03\x14\
+&\x00\xc7\xa4\x84, \x1a\xe6dB\x16=\xcb\xb2\x93\
+1g\x85>\x99\x98\x82\x9f\x1di\xca\xfd2\x01\xfa^\
+\x9b\x82 \xa4\x08\xa2,\xb3\xac^\xd1\xb3LB\xa6 \
+\xe8\xa7\xc1\x19X\x8eyg\x00\xfafb\x09\xb9\xf3\xd2\
+\x1f\x02\xef\xc6\x95\xebz\x8c\x0c\xe94\xa5z\x83\xb1]\
+\x0b\x00\x7f\xf7\xf1jx1\xa6\xe6\x82\xce\x90u{z\
+\x1a8\xceH%]\x01Q\x14=H\x93\x0c\xaaF\x00\
+QH\xd3\xe9p\xc2d\x02\x01\x00\x11#\x80\x9a\x86\x11\
+m\x05\xd8\x94\x82\x013\x08I&\x00\x05\x08!\x08 \
+\xe8\x91\xb5\xd9\x88H\x22:\x9aB\xa2\x10\x03A\xe2\x97\
+\x93\x16p\xba\x9d\x1f\xa7\x9b\x19\xd0dQo\xa0\x11K\
+\x00\x08E\x83\x12\x8d\xc4\x15\x1d@\xb8\x9a\xa3\x83;\xf7\
+?3\xbd\xcd@\xfbh\x04\x22\x07 E\x22|B\x03\
+\x88\x18t\x18q,\xd6\xeb\x13\x8b\x19<\x1f\x8b\x9d=\
+s.\x9a\xb4\xc0\x0b\xcf\x1d\x15srs\x87\xb6\xda\xe5\
+\xe2-\x85\x86o\xa5\xb3\xc8\xe2\xf1I\xca\x9f;\x7fU\
+\x8ad\xb8\xf1B \x1c\xeez\xeb\xd5\xe57Cq\x89\
+B }$(\xb1\xb7\xdf|nS\x9a\x02\xbd,\x01\
+F\x91\x94\x14\xc57\x91\x1f\x8d\x89=\xab\x0a\xe1\xa7\x13\
+\xb30y{\xfaDMYV\xd1\xccB\x1c\xf9\x83\x8a\
+\x01K\xc4\xfa\xa1\xa0\xd4\x8fkjM\xad\xc9l\xedU\
+\xd5\xd2\x12#GQ\x08\xc4L\x8ab\x87\x05u\xf7\x82\
+\xaa\x1e1\x11(\xf1\xfb\xc2\x96\xd8\xe2p>\xa6\xa8c\
+u\xf5\xfb \xe9\x10n(Y\x0f\x00\xf0\xf6\x85\x8b\x1d\
+\x83s\x0b\xca\x86X\xce\x9eI\xa4\xa7\xb2\x1d\xa9tH\
+O\x01\xd6\x10,\x17k\xc8\x80\x11\x8a#\x02\x8c\x0e\x11\
+\xd6\xa2C\xc5\xaa\xa0\x95\x8f\xbb\xd5\x8c\xe0H\xc7\xc3\xf1\
+\xb8p\xa6\xe7\xf2U\xcf\x7f\xab\xc0\x17&\xf4\xf8o\x8e\
+A\xdb\xebg\xdb\xf3\xb3\x8dGc\xde\xd9\x02\xd1X\x1a\
+\xd0#\x842\xad:\x85\xa3\x80\xe2\x00\xe9\xd21pf\
+\x0c\xb6\x14\x0a\x7fG\x91`\xe3\xcdq!\xef\xce\xb5\xd3\
+\xb53\xb7?\x9a\xef\xe9\xbe\xba\xa7\xae~\x1fLNL\
+\xad\xfe1\xfa\xc53O\x83\x94\x90L\xaa,\x0f(\x98\
++29\xf6\xfc\xc3\xb1e\xf7\xa0#\x0f/\xe5X\x80\
+7\xd0@\x0b\x12\x98\x9d\x1e%{qjx\xf3\xc4\xf5\
+\xce\xadn\xd7\xfc\xc0\xd5+\x7f\xd9ZW\xbf\x0f\xba\xbb\
+z\xee\xff9n\xfa\xe9ah\x7f\xe3<<\xdeP\xff\
+sU\x96^\xa2\xf5F\xced)ts)\x96\x08\xa3\
+g\x88\x1c\xf1e\xc8\x11\xcf\x03.\xb7s\xfe\xc7\x87\x9a\
+\xf2\x03\x81`\xa8\xf9\xc9\xa7\xd2\xea\x1b\xea\xcc\x9a\xa4D\
+\xbb\xbb\xaf\xdc\xff@\xb2\x92\xfd\x07~T\xaai\xeaN\
+\xa2\x11+\x00\xb0\x08\xa3)\x84\xf0\x8d\xee\xae\x9e\xdb\xad\
+g[;\xaa\xb7W?v\xf3f_j\xff@\x7fW\
+\xeb\xebm\x07`-X\x99\xec\x95\xeb\xae?uM\x12\
+B\xc8\xb5\xeb\xd7\xb4\xdf\xfe\xae\x85\xd4\xd6=Vr\xe0\
+\xe0\xfe\xd5\x85\xf0\xcbX\x19\xac\x95k\xbb\xcd^RX\
+TX\x9e\x93\x93\x83\xe299\xb5\xb6\x02CCC066\
+\xb6h\xb7\xdb7\x96|\xb3\xe4\x1b)\xe6\x14\xa4$\x14\
+\xb2\x1c\x5c.\xef<\x7f\xf1\xfc\x9aW\x00\x00\xa0\xa1\xa1\
+\x01FGG{9#w\xb4\xaa\xaa\x0ax\x9eG<\
+\x1f\xb7\xa5[\xd6\xf5m\xab\xd8rghdxm\x05\
+\xc6\xc6\xc6\xc0\xe5r\xc9\x8e\x22\x87\xf6`\xde\x83;r\
+r\xb2I\x22\x91@\x81\x80\x7f\xfb\x1f_;\xfd\xca\x9a\
+t\xc1\xbd8y\xf2d\xcb{\xef\xbf\x17\xd6\xeb\x0dr\
+ff\xa6\x5cP`\xb7\xadY\x1b\xde\x8b\xcb\x97/\xc3\
+\xdc\xdc\xdc\xae\x80?\xc0\xb2,\xcbX\xac\x96\x8b\xf0\xff\
+\x80\x90\x84\xfe\x83\xfe\x0fl\x00\x00/\xbf\xf2\xf2\x7f\x9c\
+\xfd\x0bVu/\x8d\xbcS'\xfc\x00\x00\x00\x00IE\
+ND\xaeB`\x82\
\x00\x00\x08h\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -342,6 +460,199 @@ K\x18+\x8f\xf7\xd7\x8d<\xbdGN\xca\xda\xd3S\xd4\
n\xb8\x10\xff\x17\x1a\x96\x9e\xd2\xd9Z\xe6L-\xdc\xe3\
\x8c\xc7y\xbe\x06\xff\x01\xd8\x02\xdah\xfa;i$\x00\
\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x03\x9f\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\
+\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\
+\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\
+tware\x00www.inksca\
+pe.org\x9b\xee<\x1a\x00\x00\x03\x1cID\
+ATx\xda\xed\xd7\xdfk\x1cU\x14\x07\xf0\xef\xf7\xde\
+;wfvf\x13\xb3i6\x0d\x1a6!Ulc\
+#Q\xfb\xa0\x06eAJ#\xb6VcS\x11\xfa\xdc\
+\xb4O\x0a\x01m)\x11\x11\x04\x9f\x05A\xa8\x14\xc4\xb7\
+B\x15,\xf6IP\xff\x0a\xa1\xc5\x07\xed\x83H\x91Z\
+\xb3\xba\xdd\xd9{\xaf\xf3#\xec\x0fK(\xd54A\xd8\
+s\xf8\xb2\xf3v>\x9c9\xb0\x0c\x9ds\xd8\xc9\x12i\
+\x06\x80\x01\xe0\xff\x0b`\x9d\xa2~\xb2\xaev\x04\xc07\
+8=\xf6X\xf5\xda\xe4\x81\xda\xf5\xeaj\xf5\xe1m\x05\
+p75\x04\xbf\xdc%G\xa7\x7f\xbe\xfe\xd3\xf8\x8d\xf5\
+\xdf\xbe\xe0,\xf5\xb6\x01\xf0\xafD\x95\xb9\x181\
+\xbe\xff\xe6;h\xe8Y<\xc7\x8b\xdb\x02\xe02\xcf\x84\
+Q\xf8\xda\xbe\xb1\xbd\xb0\x9e\x85\x0b\x1d\xcc/\x09\xbc\x92\
+\x7f\x98\xa7\xe5{\xf7\x15\xc0W\xb8(}\xb5\xf6\xe4\xd4\
+\x13\xa4&\x9cv`L\xb4e\x1b\xf2&\xa9\x86\xbcw\
+\xb8\xa2\x8e\xde\x17\x00\x8fq\x0a\x1e.\x1c\x98y*\x88\
+\xe2\x08F\x19\x18\xcf@\xc6\x12(\x01M\xf3\x17\xda\xa6\
+\xa51\x82O\xb9\xc2\x99-\x05\xe4\x07\x96\xe0\xd2\xfe\xe9\
+\xfd\xe3\x13\x13\x13\xb0\xd2\xe6\xeb\xb7\xca\x16\x80\x08E\x9a\
+\x0e0\xae\x82\x8a\xba\xcc\x17\xe9o\x19\x003\xf8\xac\xf6\
+`m~nv\x0eV\xe4\x83\x8bh\x0b\x96\x08\x94\xd0\
+A(O\xa0\x1c\xc5\x8f\xe2qyiK\x00|\x89g\
+*C\x95\xe5\xfa3\xf5|\xb8\x91&K\x0ep\x9e\x83\
+\xf5m\x06\xe8 \xdaa\x1b\x89l\x22,\x97\x16\xf9\xb6\
+\xfc\xe0?\x01x\x98\x8bZ\xeb\xb5#\x07\x8fP\xfa2\
+\x03\x14\x916\xbf\x01\x06\x84\x0dl\xdf\x06\x10\x03\xcd\xa0\
+\x09\x15\x93z8X\xe5\xaaZ\xfaW\x00\xbe\xca)\x10\
+\x17\x96\x0e.\x05\xe5\x912l\xd6\xcc\x01\x9d\x0dP\x13\
+\x89N\x80\x10=\x88\x22\x7f\xf8\xb70\xb4+\xf6X\x11\
+\xe7\xf9&\xf7\xdc\x03\xa0{t\x87\x9e>4^\x9b\xa9\
+\xe5\x83\x0d\x0d\xd2.\x10\xd2v\x0e\x11\x012@?\x22\
+.rC\xff\x0a\x17\xd8\x07P\xf5\xaelv\x94b\xb3\
+\xa3\x9b\x7fd~~\xe1\xd9\x05\x18g`\x9d\x85\xc5\x06\
+B\x98\xcekp\xca\x01>\x80\xa0\x17\xd0\x1f\xaf*P\
+\x19\x1e\xd9\x83\x05\xf5\xd5]\x00\xdd\xa3\x9b\x1c\x9b\x5c>\
+\xfe\xf2q$.\xc9\x86w\x927\xbb\x10'\x1c\xa0\xd1\
+\x8b\xe8n *@I\x90\xe0\xf6P\x03qT~\x81\
+\xef{\x1fn\x02\xe8\x1e]),\xad\x9dz\xfd\x14\xa9\
+\x09k-\x8c5\xc5\x16\x8a\xee\xdb\x06$\x00\x95\xc6K\
+\xa3\xfb\x10E\xc2\x02\xd7P\x0dxe2\x88\xc2\xb7\xf8\
+\xae:\x86\x9eR\xbd\xff\xed\x88\xf1\xc9\xd9\x13g\x83\xd1\
+\xdd\xa3X\xffs\x1dZj\xf8\xc2GK\xb6\x10\xa4\xed\
+\xe8@E\x88\xb4e\xd6~\x9a\x96\x04=\xe6\x08\xfa\x04\
+K,~\x0d\x01\x02\xc2\x08\xa0\x0d\x98$\xc1\xd4\xe8C\
+\xde\x0f\xb8\xf6\x11\x80\x8bw\x00\xf0-\xdc\xca\xc7+\xbc\
+\xfa\xe3U\x9c;\x7fnC\x95\xc6\xffG\xb2ajc\
+\x98\x12\xa0\x14\x80 \xa0\xd2H\x16\xcfB\x00,\x00\xa0\
+\x04\x1c`@4\xfc\xdf\xb3\xe7[\xe8\xa9\xbe\xef\x02.\
+S\xa0\x81ahl}\xb5P\xd4\xd7\xb8\xe9z\x86\x0e\
+>L\x06\x80\x01`\xc7\x01\x7f\x03\xdc\xff\x15WD\x01\
+nd\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x02V\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
+\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x0c\x0c\x09\
+6\x16qS[\x00\x00\x00\xb7PLTE\x00\x00\x00\
+\x14\x14\x14\x17\x17\x17YYYjjj\x86\x86\x86\xa4\
+\xc0\xe4hhh\x80\x80\x80\x1e\x1e\x1e(((xx\
+x\x87\x87\x87iiiddd\x5c\x5c\x5ckkk\
+ppp\x16\x16\x16rrr\xa8\xc5\xea\x1f\x1f\x1f\x1f\
+\x1f\x1fbbbccc\x16\x16\x16\x1f\x1f\x1f((\
+(111222999:::CCC\
+DDDLLLNNNUUUWWW\x5c\
+\x5c\x5c^^^```bbbcccdd\
+dgggiiijjjppprrr\
+wwwyyy{{{~~~\x81\x81\x81\x84\
+\x84\x84\x85\x85\x85\x88\x88\x88\xa8\xc5\xea\xa9\xc6\xec\xaa\xc8\
+\xed\xff\xff\xff\x828\xd2\xed\x00\x00\x00\x19tRNS\
+\x00\x00\x00\x00\x00\x00\x00#)?F\x81\xc0\xc6\xec\xf3\
+\xf3\xf8\xfa\xfa\xfb\xfc\xfe\xfe\xfe!\xf9 \xd7\x00\x00\x00\
+\x01bKGD<\xa7ja\xcf\x00\x00\x00\xf3ID\
+ATx\xda\x8d\x93\xd7\x12\x820\x10\x00\xcf\x8a]\xec\
+\xbdWP,\xd1\xa8\xc0\xff\xff\x97g\x1c\x1dr\xc6\xd3\
+\xcdC\x96c'3y\x08\xfcC\x1a\x17?\x0e\x80@\
+\xc7\xa19\x08#\x96\x80\x0f\x12Z\x00\xe5%\xa1\x0cz\
+PrZ\xae\xbbq\xdb\x8e\xd3\xc6\xcdm9%\x12\xd8\
+\xbb\xa2\x85\xe6!\xb8Y\xc5\x9dM\x82Z\x05\x92h\x87\
+\xfd\xfe\x00\x80Z\xa9\x91@\xe4\x95\x89\xe3Q(\xc9\x0b\
+\x12\xc8\x8c2y>K%\x19I\x82\xc2\xd3n\xd7\xeb\
+\xedi\x05=H\xe1zp\x91\xf2\xa2\x04\x07Z\xf0\xe2\
+$\xc4I\x1b\x13S\xb7`\x03o\xbb\xf5\xd8`\xbdZ\
+\xad\xd9`1\x9f/\xd8`6\x9d\xce\xd8`2\x1eO\
+\xd8`4\x1c\x8e\xd8`\xd0\xef\x0f\xb8 \xd7Cr_\
+\x828@\xb6\xd1\xecv\x9b\x8d,~\x98N\xa8v\xde\
+TM'\xc4 B,\x12\xa4\x01|0\xe2?~\x22\
+\x81_\xf7C\x038\x0e~\x9f\xc0?\xbd\x7f\x1f/\xcf\
+\x1d5T&3\xf6\xb7\xdc>\x00\x00\x00\x00IEN\
+D\xaeB`\x82\
+\x00\x00\x05\xaa\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01^\x1a\
+\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x01\x04\x03)\
+%F\xe3\xacU\x00\x00\x00\x06bKGD\x00\xff\x00\
+\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x05*IDATx\
+\xda\xbdV[PSW\x145\x80\xa5\x82h\xab\x9d\x22\
+\xa8\x88\x80\xa0b\x15e4\x80\x222E\xa7B\x00C\
+x\x04\x08\xc1\x82\x10\x08\x22\xa8\x10\xc5\xa8!\xe4!\x86\
+\x87\x04\x83omm\x95\xa8\x14\x15\xc4\xdaV\xb1\xa5U\
+\xa7:-\xa8\xa8XEZ\x94\x8e\x04\xa9|\xd8\x96\x87\
+\xc2\xea\xbe)\x9f\xfd\xcc\xed\x9eY?\xe7\xae3k\x9f\
+}\xce]{\x8fa#\x5c]gX+\x95\x85\xb1\x97\
+\xbe\xbaT\xd7x\xb5\xf1\xf1\xe5+\xdf\xb4\x9d;_[\
+{\xf0\xf0~\x81\x9d\xbd\x9d\xf5\x186c\x09w\xb1\xbd\
+\xd1X]\xdf\xdf\xdf\x8f\xc1\xa1A\x98L\xddx\xf4\xf8\
+\x17\xfcx\xf3\x06N\x9d\xae\x1e\x96d\xa4\xc9\x89\xc6Z\
+\x12V\x1br\xb2\xcb{zLx\xf5\xea\x15\x86^\x0f\
+\x99\x13PkT\xc3\x01\xfe\xfe\x9d\xd6\xd6\xd6\x95\xc4\xf1\
+exl%0\xb1j\xdf^S\xd7\xef]x\xf9\xf2\
+%\x06\x06\xfa\xf1\xb0\xed\x01h\xbd\x9c\xe0A\xb0%p\
+X+\xff\xfc\x05\xf3\xe6\xcb\xb7o\xc3\xd3g\x9dx\xd1\
+\xfb\xc2\x5c\x85\xd6\xd6\xbb\xb0\x1bg\xb7|\xcc\xff\x11\xfc\
+\xa8\xc8d\xf9\x8em\xb8~\xfd\x1a\x95\xdeDU\xf8\x03\
+\xbd\x94H^~\x9e\xe4?N\xceY\xba,\x80c\xd1\
+\xfb\xcf\xc9\xddPy\xe8\xf0\x01\x94\x94\xecF7\xdd}\
+\xcf\x8b\x1e\x98\xe8=\x5ci\xbcl*R+\xd6\x8c&\
+aUm<\x11R{\xb6\xe6k\x0f\x0fw?K&\
+\xf0\xf6^C\xe5\xad\xba\xfas\xc8\xc9\xc9\xc6\x9d\xbb\xb7\
+\xd1\xf1\xa4\x1d\xcf\xbb\x9f\xe3IG;\x0cU\x95\xd8\x9c\
+\xb7\xb1\xb6\xa9\xe9\xdb\x9b-\xcd?\xa3\xb4\xac\xa4\x9f\xf6\
+\x84XL}\xea4g\x97\x0b\x0d\xf5\x83\xdf5]\x85\
+\xa2P\x81\xd5\xbcp\x08b\xe3\xf0\xac\xeb\x999\x81\x86\
+\x8b\x17 \xdf^\x80f\x12\xbf\xff\xe0\x1e\xfc\xfc\xfcZ\
+i\x9b\x8b\xa5\xf49\xf4\xfb\xa51\xa7NHJ\x828\
+%\x15z\x83\x01\xde\x8b|QW_gN\xe0n\xeb\
+\x1d(\x14;\xd0\xd2\xd2\x0c\xadV3D{6\x13\xc6\
+ZD\xdde\xc6\xf4\xf1u\xf5\xe7o?\xed\xec\xc4\xe9\
+3g0w\x81\x0f\x8c\xa7\x8c\x08\x8d\x5c\x83\xb8\x84D\
+\xb4\xb7?6\x9f\xba\xa4T\x87|Y\xfe\x1b\x1b\x1b\x9b\
+j\xda6\xc5B\xb6\xeb:\xae\xf6\xec\x17u-\xcd\xcd\
+\xe8\xf8\xb5\x03\xddt\xe7\xfc\xe8\x18\xa4\xa6K LH\
+0W\xe0\x1e\x89\x1f=v\xe4MF\xa6\xa4\x97\xb6\xec\
+g\xb6Y\xc4\x0f\x16.\xf2\xb1\xaf\xa99\xd3`\xa82\
+ hE\x10\x96\x05\x06\x9a\x0d\xa8\xeda\x1b\xc8\xff\x87\
+\xa3\xa2\xf8}\xde\xde\xde\xad\xee\xee\xee5D\xdfD\x08\
+\x22\xbck\x91\x93\x07\x07\xaf\x98x\xb2\xfa\xc4\xe5\xad\x05\
+[\x11\x10\x10\x80P^(\xc4\xc9b\x90\x0d\x0fgJ\
+3\x7f\xe2p8\x85D[E\x98Ip\xb0\xa8\xff\x87\
+\xf1V\xbfw\xfc\xb3On\x88\xc4\x22x\xcd\xf6\x02\xd7\
+\x8f\x8b\xf0\x88\x084\xb74\x8f\x88D\x89\xccig\x13\
+\xc6\xb2b\xbbQ\x02\xfe\xd4CG\x0e\xdc\x8e\xe0G\xc0\
+q\x8a#k~/\x8c\x8fq\xd7\x1b*\x1e\x85\xac\x0c\x81\
+\xa3\x93#\x9c\x9c\x9d0m\xfa4T\xe8\xf7@&\xcb\
+\xbfF\x94\x0fX\x13\x17%%\xcc\xd3\x95\x14?]\xb9\
+j%\xdc\xdc\xdd\x18as\x05\xb2\xc9\xf5\xd4j\xd5}\
+\xa2\xf8\xb3\xd6f\xc5\xc9\xa2%*\xb5\xd2\x14\xc6\x0b\x03\
+\x97\xcb\x85\xcfB\x1fxzy\x22.>\x0ee\xe5\xba\
+.zp<\xa2\xd9\xb0\xa1\xcd\xd9\x94\xb7q\x85R\xa5\
+\xe8\x13\x08\x04\x08\x0d\x0b\x05S\xfe\x80\xa5\x01\xe0\x0b\xf8\
+(\xab(\xedsppHa\xfa<+\xe2\xfa\xca\x0a\
+\xde\xa7\xc7\x8f\xfd\x99\xbc6\x19b\xb1\x18B\xa1\x10\x11\
+\x91\x11\x10D\x0b\xa8\x99\xe8\xfevvv* \x9e=\
++\xe2\x87\x0e\x1f\x14~y\xa9a 77\x07\xb9\xb9\
+\xb9\xc8\xca\xcaBJj\x0aD\xe2$\xec\xd6i^{\
+\xce\x9e\xa5'\xde$V\xe6:\x8d\xa6h\x1d\xf5\xeb\xd7\
+2\x99\xcc\xdc\xd9\x14\x0a\x053T\x80\x0c\x06\xdab\xd5\
+\xc8\x22\xdf\x85\xa7\x98\x06\xc8\x8a\xb8D\x9a\xb6\xa9\xb4\x5c\
+7\x9c\xb5>\x0br\xb9\x1c\xda]Z\x14\xa9T`\x92\
+Qk\x8b\xc8r\x977\x8e\x1a\x0d\xc7\xe2\xe2\x19\xd2t\
+\xb9|\xfbV$$&@*\x95\xd2\x00\xb1\x19\x05\xdb\
+\x0a \xdb\x22C\xa5A\x8f\xc85\xe1-\xc4\xe32\x5c\
+\x8b\x8b\xaf\xcf\xceT\xd2kG\x9c0\x0e\xf1\xf1\xf1`\
+\x1e^\xba$\x1d\x92\x0c\x09\xaa\x8d'\xa1P\xee\xf8\x8d\
+x\x1f\xb11\xd3[\xedT\xca\xb7h\xb4E\xc3\xd1\xb1\
+\x02SJ\xea\xda\xbe}\x07\xf7!Q\x94\x88\x98\xd8\x18\
+\x9c\xac\xfe\x1c\xd4\xeb{mmm\x93\x88\xfb\x16\xc1\xb2\
+Aw\xea\xbaG_:\xe8\xe5\xe5\xc94\x91\xe5\x04\xfe\
+\xc7\xa9\xc9\x0f\xd7\xa5\xa5BG\xc3\xe5\xf7?4\xfd\xe5\
+\xe2\xe2\x92G\xebv\xac\xb8\x1c/<4\xbc\x5c_2\
+2\xda:\xad\x08c\x05\xd1\xfcbm\xb1\x9a\xa6\x1b\xe3\
+\x80\xf7<\xefbZ{\x87\xc0NL\x9e<\xc9_W\
+\xbakpW\xb1\x86\x11\xb2\x09\x0c\x0a\x1c\xaf\xd2\x146\
+Q\x02Cs\xe6xU\xd1\xda\xfb\x04Vc2\xf5\xf7\
+\xba\xbdU\x15#\x15\xfa\xb2[e{t\x1dJ\xf5\xce\
+A7\xb7\x99GG\xffu\xd6\x83CX0w\xee\x1c\
+cx$\xaf\xfd\xc3\x90\xe0\x96\x09\x13&\xec\xa45g\
+\xf37\x96\xe3\x1f\x0b\xbb\x8dhG\xcdo0\x00\x00\x00\
+\x00IEND\xaeB`\x82\
\x00\x00\x07\xe4\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -471,6 +782,124 @@ Z\x03BI\x10B\x02\x87lFH\x9a\x05,a\x81\
[y\xdc\xfb\x08\xdf\xfc\xdf\xf0\x93D\xe0\x13\xe6\xbf\x92\
r\x1f*A9\x0fr\x00\x00\x00\x00IEND\xae\
B`\x82\
+\x00\x00\x072\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\
+\x12\x00\x003T\x8bJ\x00\x00\x06\xb2IDATX\
+\xc3\xc5\x96\x7flS\xd7\x15\xc7\xcf\xbd\xef\xf9\xbdg\x1b\
+;\x89\x9d4qP\x12\x1b'\xa9P\xc6\xa0-i\x93\
+\xd0\x96\x04\xca\xa4\xb6hl\xb4)\xc9\x96\x96\xa4\xedF\
+Z\xb6I\x9b4m\x83\xb5\xfft\x15\xeb\x1f\x95\xa84\
+~\x8c\x06\x0c\x1dT\xcb\x92\xa8\xb4\xa3\x13B\xa3\x14B\
+\x9b\x8c\xa4\x81\x18\xf2\x03\x08\xc4\x8ec;?\xfc+v\
+\xec\xe7\xf7\xe3\xbe\xbb?\xa6iL\xcaJ\x1d\x9a\xee\xfb\
+\xd7\xd5=G\xe7~\xee\xb9\xf7\xe8\x1c\x80\x0c4\xe9\x9f\
+\xfc\x9f\xb6+\x83\x97a1\xc2\x99\x1cl6\x99\x9f\x8a\
+D\xc3'\xe7\xe3s\x81\xc4\x5c\x8cF\x22\xb3tf&\
+8\xe9\x9b\xf0\xb4\x99\xcd\xd9\xd5\x00\x00\xbd\xbd=\x19\x01\
+\xa0\xaf\xe2\x14\x9a\x9d]\xa3i\xeaG\xbe\xe0LQ\xd7\
+\xc7\x17\xa2F\xbd\x1ea\x8cA\xe0y\x88'\x12\x90\x98\
+O\xc0\xd6'k\xb2\x05\x9ew+\x84n|\xa4\xb22\
+\xf4\xb5\x01\xf8\x03\x81F\xa2J\xef\x1f\xf8\xd3\x99\x19\xbb\
+\xc3A\x1a\xb7n\xd2k\x1aP\xa2\x11\x0a\x80\x80e0\
+\xd24\x80\xb6\x13'\xc5\x80o\x8c\xdf\xba\xe9!\xabJ\
+QU\xdd\xfa\xba\x7f\xdc3\xc0\xb8g|C:-\x9e\
+=\xf8\xe7\xcf|?\x7f\xf5y\x1dB@\x10\xa6\x04\x01\
+\xd60P\xaa\xfd+\x00\xa2\x14a\x0a\x94I$D\xbc\
+w\xffa\xed\xbb\x8f9\x97\xcb\xaa\xe6\x8c\xc7\xe7n\xb7\
+4\xbf\x989@_\xdf%0d\x99Y\xac*\xca{\
+\xa7\x06}\xcd?|V\xd3(\x91X\x06)\x0c\x83U\
+\x86a\xb4\x5c\x0b\xab \x00\x08G\x14V\xd5(C\x88\
+\xc6\xaaD\xd3\xc9\x8a\xca\xbb\x8e\x1c\x11\x1e,\xcf\x11\x9f\
+ojv,:\x03\x03\x03_\xec\xf9bx\xe2\xd5<\
+\xfb\x03\x81\xe5\x05\x16\x89\xe7\x984\xa7cd\x1d\x87\xe4\
+\xfb,\x9cTY\xb5\xa5\x96R\x8azz>:7\x17\
+S8E\xa5\x9c$\x13N\x96\x89~\xe4\xa6W/\xcd\
+\x0c:9D\xb6\xfb\x03\x13'~\xfd\xab\xdff^\x05\
+\x84H\xbf\xb88\x9a\xf2X\xac9)Y\x83\x98\x0a8\
+\x0c\x0c\x9aF\x18M\x01@\xb0z\xc3S\x93\xd5\x1b\x9e\
+\xf41\x18\x02\x88CA\xc4\xc2\x14fp\x84\xd5\xe1T\
+Y\xa9#=\xe4M\xf9\x1fX\xb3z\xe7\x97\x1d\x0e\x00\
+\xc0.\xb4y\xa1\xfb|I|.\xc1\xf9\x12F\xffD\
+XVK\xf2\xf5\x04X\x14\xcd\xcf\xe3\x22\x06\x00\x91\x03\
+H\x9a-\xf9\xa5\x1a\x05l\x06\x183-c\x05\x11@\
+\x98\x8d\xabeH\xc5\xb9\xf3\x98\x90\xe9t\x96?\x99\x8c\
+W\xdf\xed\x09\x16\x04H\xa5R\xe5S\xa1yb\xc9+\
+L_\xf6$\xbdQ\xc2\x84\xeb\x1f7\xe6\xacY\xf5x\
+\xa3\xadhE\x5cQ$m\xf3\x96z\x07 \x80\xb5O\
+4Zu\x1c\x8f\x02\xde[\xa6\x9bC\xddg&B2\
+\x9e\x8c\xca\x883\xdb\xf8\xab\xd7F\xe0\xe5\x1f\xb5,o\
+{\xd7\xe5\xcf\x08@\x14Es*-\xcb\xa8\x02\
+\xcf)\x12F\x11\x09 \x02\xbcy*\xbbt\xddUI\
+Rd\xc1\x98e\x01\x000\xafx\xf4\x22\xc7\xe9t\xc1\
+\xe9\xd0\xaa4@\xefx\x92X\x82\x09R\x0c:}\xc1\
+\xe8\x8dQ0\x18\x8cL\xc6\x19H\xa7E?\xd6$\xbd\
+\xa6Q@\x0c\xd2t:\xa4(\x8af\xbf\xd9\x7f\xea\x14\
+\x01 \x02\x80\xb8\xf3\xf7\x1d\xd7\x11\x00|rh\xe7\x0d\
+\x09\x80g\xe0\xc7\xb7\x03\x94\xe6\xfb$\xedzR\xa2\xd9\
+:\x96\xb1\xa5S\xf3 \xab$\x9a1\x80\xc7\xe7\xbd\xe2\
+,.\x062\x95\x16\x04\x16\xf1,\x83\xb0;\xa1\xfe}\
+\x84\xc7Q\x8cP\xeaa\x03\x13\x1b\x19\x0b8(\x00h\
+\x00\x87\xcf\x8a\xc4\x04\x14\xf4\x08QS\x16\x8f\xcb)\xc7\
+\x08z%\x98\x97H\xcd\xcf\x1f9\xf2~\x22c\x80\xdf\
+\xfcrW\xfab\xf7\xa7\xfd\x8f*\xf2\xcaUN\xbda\
+\x8a\xa0Y\x16\x8032\x88e\x011\x16\x00<7\x97\
+\x94\xa9F\x01\x03\xd0\x1c\x8c\x90J\x81!\x08t\x98A\
+\xd9!I5+\xb3C\xc5\x89d\xbakQ\xcd\xe8\xc6\
+\xd0m\x18\x1a\xbd\xfe\xbb\x8d\x0f\x16\x94\xde\x0a$Q(\
+\xa2\xea\xc5\x141\xfc\xdb\xff\xafIE:\xdb\xb1\xab\xf6\
+\x93\xae\xdd\xb5\xaf\xc7\xe2I\x8c\x90\x8a\x80\x223\x80\x95\
+W\xc1\x11\x9a\x9d\xb3\xce\x07\x07\x8a\x19\xcc\xec\xae\xdf\xf6\
+L\xe6UP^\xb1\x02\x00\xe0\xc3\xe3'\x8e\xf6y\xfc\
+j\xf9|\xe1\xe6a$0\xb6\xec\x1c]\x8ce@\xca\
+g\x19\xf3_\x80V\xee@X\xb9J\xe82J4^\
+\xcf\x22;\x11\xb5\xb5\x03\x93j^\xe4\xb2\xeb\x89dR\
+<\xd8\xd5y\xd2\xbf\xe8v\xfc\xda\xeb\xbba\xcf\x9b{\
+j\xc4\xe9+\x86\xe4\x15\xd7f\x8fW\xbe\xdf\x7fSZ\
+\xc9Rd3`\xc8\xaf\xd3\x90s\x9aR{>\x86\x95\
+6\x1d^\xc7(\xa8\xbaw(]\xe2=\x7f\xe0{\x13\
+\xb7\xaf\x8dwu\x9e|\xe5n\xb7\xbfk3\xfa\xe9\xcf\
+v\x82\xacH\xcbTY\xeeU\xb1\xa1tY\xd9\xd3\xe7\
+\xcaj\x9e\xee+/F\xd3\x85VH\x09,\xb0\xa2\x04\
+&\xef$\xb1\x05G\x06\x1e\xb9v\xfe\xd8\xbaI\x9f\xb7\
+\xf7\xc3\x0fN\xad\xab\xdf\xf6\x0ct\xb4w\xdd{;n\
+yi;\xb8\x0e\x1f\x83\xc6\xa6\x86\x9f\x109\xfd\x16\xcb\
+\x1b\x0d\xc6\x5c\xe7\xa4\xc1l\x8ds\x02G\xe5\xf8l\x9e\
+\x12\xf7\xdf\xe7\xf3y&^x\xe1\xa5\xe2p8\x1ck\
+\xdd\xf1JNC\xd3s&\x22)\x89\x8e\x8e\x0f\xee}\
+ \xb9S\xdb\x1a\x9e]M4\xb2\x89j4\x17\x00x\
+\x84\xd1\x08B\xf8BG{\xd7\xe8\xa1w\x0f\x1d\xad\xad\
+\xad\xdd\xf2\xd9\xc5\xee\xec\x9e\xde\x9e\xf6C\x7flk\x80\
+\xa5\xd0\x9do{\xe7\xba\xbd\xbd}\x98RJ\xcf}z\
+N{s\xcf\x1b\xf4\xfb\xf5[*\x1a~\xf0\xdc\x97\xc6\
+b\x16\x030<4\xb2\xe0\xdaawT8K\x9d\x95\
+\x85\xcb\x0bQ2\x99\xa2\xe1Pd\xa3\xeb\xf0\xd1}_\
+;\xc0\x82\x7f\xa5\xa5\x05\xf6\xed\xdf\xf7\xb1\xc3\xe9x\xad\
+n}-\x12E\x11I\xe9tnn\xae\xe5\x96{\xf0\
+\xaa\xfb\x9e\xa6\xe2\xaf\x22\x97\xcb\x05\x00\x00c\xd7\xc7Z\
+/]\xeaC\xe5\xe5\xe5`\xb7\xdbi\x81\xcd\xd6\x06\x00\
+\xd0\xdc\xdc\xbc\xb4\x19\xf8\xcf8\xd77PT\x5c\xb4\xbd\
+\xaa\xaa*\x9ba0Rd\x85\xb5Zs\xcc.\xd7\xd1\
+3\xdf\x08@kk+LOOws\x1c\xb7c\xed\
+C\x95\x90H\xc4!\x16\x8bU\x1bM\x86\x83\xabW\x7f\
+;9<<\xb2\xb4\x00\xfd\xfd\xfd\xe0v\xbb\x83\x0e\x87\
+cM\xc5\xb7*\xee7\x99\xccHUU\x1a\x8dF+\
+\xdf;v\xfc\xd8\x92g\x00\x00\xa0\xa9\xa9\x09\x06\x07\x07\
+\xfff0\x1avU\xd7TC*\x99Bb*e\xcf\
+\xb1fu?\xf6p\xcdx\xff\xe5\x81\xa5\x05p\xbb\xdd\
+\xe0\xf3\xf9\xe4\xb2\xd22\xad\xa8\xa8\xa8\xae\xb0\xd0Fe\
+YF\xe1px\xfd\x1f\xf6\x1fxgI\xaa`!\xed\
+\xdd\xbb\xf7\x8d\xd3\xa7O\xcf\x09\x82^.(\xc8\x97K\
+J\xec\xf6%+\xc3\x85\xd4\xd9\xd9\x09\x1e\x8f\xe7;\xe1\
+p\x98\xe7y\x81\xb3\xe6Z\x8f\xc3\xffC\x94*\xc2\xe7\
+=\x9f\xdb\x01\x00\xde~\xe7\xed\xff\xb2\xfd\x13\x9dV-\
+8\xcd\xc8:\x1c\x00\x00\x00\x00IEND\xaeB`\
+\x82\
\x00\x00\x03|\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -712,6 +1141,189 @@ $&\x93\xc8\x7f\xb3\xe7\xcc*\xc2w\xf1nHG\x09\
\xf9\x5c\x8a\xda\xed\x0c\xf6t\xf6\xe1\xf9-'\x97\x16\xdc\
5\x03\xef$\x18\xdea\xfc\x07;\x00\xdf\x09\xb1\xdb|\
\x84\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x04+\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x06\x0f\
+\x0b\x14\x19hV\x92\xc5\x00\x00\x03\xabIDATx\
+\xda\xdd\xd6\x7fh\x1be\x1c\xc7\xf1\xf7swI\x97\xad\
+R\xa7m\xd1:+\xa8\x88\xdd\xd4\xfa\x8f\xf8\x8f\x7f\xcc\
+\xb9\xb1iQ\xd9(\x03i\xad\x93n\xb8\x04\xb5\xba*\
+\x95m\xdav\xf4\x0f\x8b\xb2\xea\x84u\xd0uk\xa4A\
+\x18\x81\xfac\x82\x136\x1d:'\xfe\xa3\xe0\x0c\x22\xae\
+*\xb2V\xd6\x96\xb4k\x97\xa4w\x97\xe71G\x1a9\
+z\xacK\x13V\xd1\x17<\xf0\xf0\xe4\x8e\xef\xe7\x9eo\
+\xee\x07\xffI\xa1H\x02\xb7W\xa2VY\xeb1k\xa5\
+\xe7\x98<\x08\x80?\x80\xdb\x80ys\x8f\xe6\x81\x04\xfd\
+\xdb\x96\x13\x1a4\xab\x15\xb4\xeb\xba~\xb7\x10L*\xa4\
+s\x9a)\x84v\x83B\x5c/\xd3i\xa9$C\xbd\x8d\
+\xbe0W!\x86\x80\xd5P*a\x00@\x83m1\x98\
+\xd9\x0c\xb85\xf6]f\xcbv\xc9'\x03\xbe!]'\
+%\xd0v\xf55\xf9G\xb9\x82\x1d\x83\xd6:\xd2\xf2%\
+[h\xdf\x1dm\xf2u9\xe7\x0f\xeeX\xc1|\x9aS\
+\xe8\x07\x18\xa8\x81\xfa\xda\xcc\xf8\x1e\xc2\xf3\x8b\xd7\xf7\xce\
+ \x85aD\xfb\xc4ov\x9a\xbd\x87\x9b\x96=e\xa7\
+\xadQ\x16`[\xe6\xa9\xbegJ\x9eH\xdb\xea\xc3\x86\
+\xc3\xa9\x93R\xf8n\x06/\x0d`\x14\x18\x00Z\xa8\x16\
+\x93\x90b\x9eh\xb0\x94Y+}\xcaT\xc6z[\xa6\
+\x7f\x028\xfa\xec\x0a\x16\x92\xfb\xdd\xb6\xads\x91\xed\xcb\
+\x1e\x11\xbaoopH=\xc8\x1c\xff\xc6\x08\x0e\xd1\x93\
+MQ\xaa \xec\x03k7m\xf1)\xba\x83\xb8<\xfe\
+\xdel\x8dB\xee>\xfeB\xe0i\x0a\xa4\xad\x0f_@\
+Q\xa5\x1b\x06\xc0\x88u\xa2\xe1\x16rz\x007?\x9d\
+Ff\xdc\xe1\xe7\x0d\x1c\x1b\xf6':6\xec\xbf\xbc\xb6\
+\xee\xdd\x19\x0ae\xac=\xa0<\xf3\x5c\x0b^\x06\xdcL\
+\xdamP\xed\xa0\xd5\x00\xcc\xa6\xa9L\x9a\xf2\xe2\xa7-\
+\xa5\x14,1F\x8er\xcd5\xf0r\xae\xdc\xa4\xa3\x09\
+D+\xb4\x96\xa4,y\xc9\xb4\xb5\xf2\xda\x8eK\x14J\
+\xa6\xc6G\xf4\xfbB\xca\x19*91\x86\x977D\xce\
+\xadu_=_\xbb/\xf1\x0e\x05\xd2\xefi\xf6\xae\xad\
+i&o:m]\xd5\x8d1\x05^gX2o\xee\
+9Vv\xbb\xfa\x1a\xfe\x19g\xe0{\
+w\xdd#.[@p\xdb\x16\x00A\x92\x19\x09\x1bh\
+>rJ\x0b\x8d\xdfT\x9f\xfcq\xb5\xd7\x16\xd8\xb4\xed\
+\x07\xb5\x97\xee\x19\xc0?\xea\xff\xa1\xa6\xa5\xfe\xfe\xd6\x9f\
+\xba\xc7_\xda\xb7\xc7A\x04\x9b\x98\xb0\x09\x8c3\x12B\
+\x00\x80\x00\x09A\x0c\x10R,\xae\xb1\xdf\x1fl\xe1\xdb\
+\x1f,[mZ|\xed\xdc\xdc\xdc\xed\xc6\x86g\x17\x0f\
+\xd0\xd3\xf3)<\xd9Y23\x0d\xb3\xfd\xf4\x95\xf1\xc6\
+\xfa\xa78\xe7\xb6.KdJ\x12\xb3\x18#\xce\x18\x09\
+\x02\xc0\x05\xc0\xb9\x90l\x9b\xcb\x96\xcd\x1d\x86i\xa9\xad\
+\xef\xb6:\x1f\xb8/G\xdbS\xdfP\xbad\x07\xfa\xfb\
+\xfb^\xef\x1d\x1a\xfdI^\xc9\xfd\x13\xab\x0bV\xea\xaa\
+\x22\xa5\x15\x87d8\x142dF\x16c\xc4\x01@\x08\
+0\xdb\x16\x92i\x09E7l\xc50l\xd7\xc8\x8dQ\
+\x97~\xf7\xcaZ\x95\xd9{\x03\xc1\xb1\xa3/\xff\xf2W\
+\x8bO\x81m\xe9/\x9d\xbd\x9e\xf2\xaf\xcc\xcdI\x19\x1c\
+\xb3\x16X\x04\x12M1\x89&\xc9A!r\xd0\x04S\
+hBRh\x82\x14\x0aA\xc6$I,*;X\xaa\
+\xa2\xbc4=\xe4O\x06\xef\xaf\xaa\xda\xf7e\x8f\x03\x80\
+\xbc\xd0\xe6\xb9\xeeOJb\xb1\xb82\x1e_\x11\x1c\x8b\
+\x18V\xc9*\x97\x0d\x99f\xf2\xf3\x94\xa8\x1b\xd0\x1c\x80\
+\x0e\xc0\x9e\xe7\xa2C\x03\x9cw\xe3V9\x99,7\xa1\
+\xd9\xf6d:;\x98H\xc66\x7f\xd5\x17,\x08\x90L\
+\xa6*B\xe1\xb8\xed\xcd-L\xf7\x8f&Gg\xb8\x1c\
+\xad\xdb\xea\xf1\xac\xab\xdc\xba\xa7\xa0xM\x0c\x82C@\
+\x08\x08\x02\x11\x08\xc4Dp\xecf\xe6\xad\xa1\xee\x0f\xc7\
+\x22\x86\x14\x981\xa1d\x15\xa8W\xaf\x0d\xe3\xf9\x17\x9e\
+]\xfdNskpQ\x00\x9a\x96\xcaLi\x86\xa1\xaa\
+N\xcb\xa9(\xa6N\x88j\xc0$9\xb3&s\xca\xb7\
+\x5c\xe3\x5cp\x02 \xc4\xbf\x93\xc0\x98\x84\x89\xc9\xf0w\
+\x92\xc0\x85;\x09{U(n\x15\x93\xc3\x9d?|}\
+\x04.\x97GZ\xb4\x03\x9a\x96\x0e2\xae\xbbl!@\
+\x12qE!\xc3\xd4y\xfe\x8d\xbe\xd3\x7f\xb6\x00K\x02\
+,\xfa\xfc\x0b\xc0\x01I\xc6\xf3\x83cBx\x03:\xbf\
+\x9d\xd0E\xb6C\x96\x0a\x0c-\x09\xdd\xb2g\x16\x0d0\
+\x1a\xf0\x0f\xac)*\x86\x1d\xd2\x9cN\x99T\xc6\x88\x0d\
+'\xad\xee\x9b\x16\x8bJD\x1a\x114\x0629\x04g\
+ \xc6!\x1c\x5c\xc0E$22\x9d\xac\x9c\xab\x92\xd3\
+i\x86\xf2\x12\xa9D\xa2\xb5\xf5\xbd\xf8\xa2\x01^\xfe\xc5\
+\xfe\xf4\xb9s]\xbd[M\xf3\xdb\xeb\xca\xdc\xee\x90\x85\
+i\x09\xf0\xbb%\x92e\x10\x93\x09$\x138\xc08 \
+`\x09\x82%\xc0l\x82L\x8ce\x87u+\xd3\x9c\xbe\
+V\x1cO\xa6O.\xa9\x19}6t\x1bC#\xff\xfc\
+\xcd\xc3\x0f\xe4\x97\xdd\x0c$(\x1c\xb5\x5c\xe9\x94\xed\xfe\
+\xcf}\x02l\x222%\x12iFH3\x22\x8b (\
+\x13\xf0\xaa\x96(\x0d\xdf\x9d\xf3&C}\xc5\x92$\xbd\
+\xb2cg\xdd\xe2SPQ\xb9\x06\x00>8z\xb4\xa3\
+\xc7\x1f4+\x12\x85\x8f\x0f\x93K*\xc8\xceq\xccJ\
+\x0c:\x88\x18Ap\x99X\x9a\x0b\xa1\xc8$T\xafL\
+>[\xe3\xd5\x97\x03v^\xb4\xbf\xed\xe1d2}\xf8\
+d\xe7\xa9\xe0W9\xf0\x85\x15\xfa\xea\xaf_A\xf3\xe1\
+\xb7\xdb|\x85\x9e\xfd\x89\xa9[%i\xcf\xfa\x88\x93\x88\
+V\xe59,\x8f\x04\xc9\x0drx\x19\xdc\x19\x0c\xbe,\
+\x99}\xd7\xd4\xa9\xaa\xfb\xaaVt\xe7\xe3CO\xdc\xbe\
+\xde7\xd6y\xe2\xd4c;v\xd6axhd\xe9\xcd\
+\xe8g?\xdf\x07\xc3\xd2WX\xbaq\xd1b\xae\xb2\x15\
+\xe5\x8f\x7f\x5c^\xf3XOE1M\x15z\x91r\xca\
+\x90\xd3:2\xfcA^01\xdc\xf7\xbd\xa1\xae\xf6-\
+\x81\xc0\xd8\xc5\x0f\xde?\xbde\xc7\xce:\x9c8v\xf2\
+\xde\xdbq\xe3s{\xd1\xf6n\x07v\xd7\xef\xfa\xa9e\
+h\xbf\x93U\x8f\xdb\xe3]\x1bpg\xe5\xc6\x14U\x11\
+F|:\xcf\x9c\x0b|k,0:\xd6\xb0\xf7\xb9\xe2\
+H$:\xfb\xe2\x0bM9\xbb\xea\x9f\xce\xb0u3~\
+\xe2\xc4\xfb\xf7>\x90\xcc\xd7\xceg\x9eZo\xdb\xfcG\
+B\xf0\x5c\x08\xa8\xc4h\x84\x11\xfb\xe4\xf8\xb1\x93\xd7\x9b\
+\xdfin\xaf\xad\xad\xdd\xde}\xeel\xf6\xf9\x0b\xe7\x8f\
+5\xbf\xdd\xb2\x0b\xcb\xa1\xf9\x95=\x7f}\xfc\xf8\xf1a\
+!\x84\xe8\xfaG\x17\xff\xed\xeb\xaf\x89'wl\xaf\xdc\
+\xb5\xfb\xe9\xa5\x15\xe1\x97i~a\xcd_\xfbJ|\x95\
+e\xe5e\x1b\x0aW\x17R\x22\x99\x14\x91H\xe4\xa1\xb6\
+\x96\xf6\x83\xdf8\xc0\x82\xb5\xd2\xd8\x88\x83\x87\x0e~X\
+\xba\xa6\xf4\xd5\xdam\xdb(\x95\xd2HO\xa7sss\
+W\xde\x1a\xbcru\xf0\x9e\xa6\xe2\xaf\xa3\xb6\xb66\x00\
+\xc0\x8d\xcfn4]\xba\xf4)UTT\xc0\xe7\xf3\x89\
+\x82\x82\xfc\x16\x00hhhX^\x07>\x1f\xe7z.\
+\x17\x15\x17\xed\xdd\xb4iS\xb6$12\x0cS\xf6z\
+s2\xdb\xda\xda\xff\xf6?\x01hjj\xc2\xd4\xd4\xd4\
+YEU^\xdcP\xbd\x11s\xb1\x18\xe6\xe6f7{\
+2\xdc\x87\xd7\xaf_\x97\x1c\x1e\x1eY^\x80\xde\xde^\
+\x0c\x0e\x0e\x86J}\xa5U\x95\x95\x95\xf7edf\x92\
+eY\x22\x1a\x9d\xd9\xf0\xc7\x8e#\x1d\xcb\xee\x00\x00\xd4\
+\xd7\xd7c``\xe0#\xb7\xc7\xbd\xbf\xa6\xa6\x06\xc9d\
+\x8a4-\xe5\xcb\xf1f\x9f\xdd\xba\xb1\xe6No\xff\xe5\
+\xe5\x05\x18\x1c\x1cD \x100\xca\xcb\xcayQQQ\
+maa\x810\x0c\x83\x22\x91\xc8\x83\x7f8\xf4\xd6\x9b\
+\xcb\x92\x82\x85t\xe0\xc0\x81\xd7\xce\x9c93\xe7r\xb9\
+\x8d\xfc\xfc|\xa3\xa4\xc4\xe7[\xb6\x18.\xa4\xce\xceN\
+\xf8\xfd\xfeG\xc2\xe1\xb0\xaa\xaa\xaa\x92\x9b\x97{\x04\xff\
+\x0f\x09a:\xcf_8\xef\x03\x807\xde|\xe3\xbf\xce\
+\xfe\x05\xad\x92\x16\xfc$\x148\xf4\x00\x00\x00\x00IE\
+ND\xaeB`\x82\
\x00\x00\x07\x06\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -917,6 +1529,135 @@ x\x9e\xc7\xe0\xe0\xd0o\x0b\xf9\xfc\x0f\xc0V\x1dC\xdd\
\xdd\xdd\xdc\xbf\x7f\xffM\xff)\xfb\x80\xf7\xb6\xf1w]\
\x00\x16\x81'@b\x8a\xfd\x0fg\xea\x16`\x16W\xbd\
\x9b\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x03\xdf\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x1a\
+\x0d)\x10%H\x90p\x00\x00\x03_IDATx\
+\xda\xbd\x97;H\x9bQ\x14\xc7\xcf\x97\xe7\x97V\x17k\
+\x07GG\x87n\x9d\xc5\xc1R*\x08\xf5\x91\xf8\xea\xa0\
+\x85B'\xa1\x94\x0eB!CP\xb4\x93\x9d\x0aBp\
+((\x99*\xe9`\xd3\xe2\xd4V\x1cB\x16c!C\
+\x03E\xa8\x06\xd4\xa8\xa4I\xcc\xeb\xf6\xfc\xc3\xbdp\x09\
+\xd2\x9a|I\x0e\x1c\xee\xcdw!\xe7w\xff\xe7\xf1%\
+\xf4?\x9b\x9a\x9a\xaa\xd97\xd7\x0c\xba\x81\xcd\xcc\xcc|\
+-\x14\x0a\xddN\xa7\xf3\x9e\x10\xa2\xb4\xb9\xb9Im\xb5\
+\x91\x91\x11\xb1\xb3\xb3S\xe15\xa9\x94h\xab\x0d\x0f\x0f\
+\x0b\xd8\xc6\xc6Fell\xecG\xdb!\x00\xa0lm\
+mM\x8c\x8f\x8f\xc7$D{\x01\x0e\x0e\x0e\x04lu\
+uUx\xbd\xdeo\x12\xa2}\x00\xd1hT\xc4\xe3q\
+\x01[ZZ\x12>\x9f\xef\xb3U\x08\x1b\xd5a\xe5r\
+\x99\xb2\xd9,1\x04-,,P__\xdf\x03\x86\xf8\
+\x80\xae\x00D\xcb\x00\x0c\xc3 \x18\xb7\x22\x95J%\xca\
+d2\xb4\xbf\xbfO~\xbf\x9fz{{\x1fOLL\
+\xbc\x97\x10\xadS\xa0R\xa9\x10+\x0f\xaf\xee///\
+\xab\x10\x81@\x80zzz\x9eLNN\xbe\x93\x10\xad\
+\x01@`\x97\xcbE\x0e\x87\x83\xecv;\xd9l\xb6*\
+D\x22\x91 \xae\x07\xea\xea\xeaz\xce\xc1\xdf\x00\xa2e\
+\x0a 8OC8\xf6U???\xa7d2I\xcb\
+\xcb\xcb\xd4\xd1\xd1\xf1jzz\xfau3\x01\x0c\xd2L\
+\x01\xe8+<\x9dN\xd3\xe1\xe1!\xad\xac\xac\x90\xdb\xed\
+\x0e0\xc4|KR\xa0\x07\xd7\x1d\xcf\xce\xce\xce(\x95\
+JA\x09\xc1\x9f\xdf2\xc4\xd3f\x03 \xf7\xfa\xcdu\
+%p\x06\x08\xa4\xc4X\x5c\x5c\x14\xdc9A\x86\xf05\
+s\x0e\x5c\xab\x80\x0e\x01H@\x5c]]\x19\xdc\x1d\x82\
+\xeb&\xc4o\xd2\x87Vk@W\xe0\xda\xdb+\xc7\xb9\
+\x820M\xd3\x98\x9d\x9d\x15<7\xfc\xf4\x0fs\xd4Q\
+\x84\x0a@u\x85\x02\xc3^\x9d\xe1\xf6\x08\x8e\x89)\xd6\
+\xd7\xd7\x0d>\x9b\xc7s\x06\xa9\x1f\xa0\x16B\xdd\x12\x93\
+Q\x01(\x18\x8ciU#\x9d\x9d\x9d\x98\x0dF.\x97\
+{\x19\x0e\x87\xe3R\xe9J\xa3\x0a\xd84\x05\x94\xeb\x0a\
+ 8\xe0\xb0b Q0\x18\xa4\x93\x93\x93\x8f\x1c\x1c\
+S\xc9\xc3^j\x14\x00fS\x81ps%5\x00t\
+\xf9\xe1\x90>\x16\x8b\x09\x1e\xd1?\xb7\xb6\xb6^\x10\x91\
+\xc9\x9e\xb7Z\x03\x06\x02{<\x1e\x8c\xe2j e\xc8\
+7\x00\x90_\x8cf\xd8\xd1\xd1\x91\xc1/\xad0\x04c\
+\xcf\xb2\x17p\xfbF\x00\x0c\xe9&[ttt\xf4>\
+n\xac\xacX,\xd2\xdc\xdc\x9c\x18\x1a\x1aB@\x00T\
+\xfd\xe2\xe2\x02g\xbfe\xf0?\xe0\x04\x80\x15\x05n\x85\
+B!/RQ\xd3\x15\xb7\xb9\xe0\xbe\xf0zwpp\
+\x10\x10\x90\x0a\x83\x08\x00\xbfd\xf0\x1c{\xb9\x19m\xe8\
+fw\xd5>\xdf\xde\xde~\xc4\xcb'NCw\x7f\x7f\
+\xbf\x01\x05\xf0[!\x9f\xcf\x03 /\x8bOX\x01\x10\
+R\xbe\x92J\x89\x9e\x1e\xae\x01\x11\x89D\x00\x119>\
+>\xbe\xc3oET\x7fjoo/\xa1\x82[\xfdc\
+\x02@\x0f\xd2\x80\xbd\xde\x9aR\x11'\x9es\xdf\x9b\x03\
+\x03\x03\xcfNOO\xbf\xef\xee\xeeF\xa4\xf4\x19\xc8o\
+\x15\x00\xe7v\xcd\x95ao\xa2\x0e\x00\xa8\xc1\x95e\xe0\
+4\x8aP\x01XMA\xe9\x9a/\xb2\xd7HlJU\
+\x8a\xd2\xcb\xea\xcc*\x80\x0e\xa2\x9b\x9alBzY\x03\
+\xc8\xc9\xb5b\xf9\xcf\xe9\x0d\xa7\xa4\x13\xae\xa9RP\xea\
+\xb4\x1e@/J\xbdk\xea\xb0\xbf\xf4\xf6\xd5\xe4\xcfb\
+6\xed\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x03\xee\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\
+\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\
+\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\
+tware\x00www.inksca\
+pe.org\x9b\xee<\x1a\x00\x00\x03kID\
+ATx\xda\xed\x97\xddk\x1cU\x18\xc6\x9fs\xce\xee\
+&iS\x13\x0b)E\xaf\xea\x85x\xe1\x95\x1f\xf4O\
+\x10\x04\xb1H\xdb\x9b\x22\x94\xa2\xe8\x7f\xe0\x85\xa8\xb5\x94\
+6\xd4B\xafl\xf0\xda\x1bI\xfaA\x1b\xa1\x1f\xb4\x08\
+\xc5\x8f@\xb1Y\x08\xb1\x1f\x9a\x94\x18\xd2&M\xb2;\
+\x99\x9d33g\xce9\xf3z2g\xb1[\xa3\xa5f\
+I\xbdp\xf7\xe5\x07\xef\x0c\xbb\xf3\
+4\xf4\xcd\x10\xe5Y\x0e\xce9\x04\x13\x10\x5c\x80\xaf\x16\
+w4K\xb8\x02\x01\xc8\x1d\xd6a\x1c\xda\x91:d\x13\
+\xe5\xcf\xf5\xe6\xbd\xd0\x01Q*\x93\x13\xf4\xa99\xf5\xd8\
+\xb7\x80\xbe\xa5\xa3\xb3\x8b\xb3#\xc3\xe7\x87QF\xd9\x8b\
+2O\xab\xb8\xc8\x05\x98e^8+\xf0\xe2q\x8b\x81\
+\x04(get\xd7z\x11\xca\x95+\xf4\x89\xfe\xe8\xc9\
+^\xc3\xdf\xf1\xee\xf8\x9d\xf1\xea\xb5\x1f\xae\xf9\xbb\xf7\x06\
+\xbc0\xb9c\xe2\x05,g\x8f\x8a'M\x03\x91C\xfa\
+~\xeb\xca\x00\x96j\xcbS\x183o=\xf1<@U\
+\xca\xd0\x85]\x97\x7f\xba\xbcp\xf7\xb7\xbb^\x9c\x0ay\
+p\xf2=\xb7\x1c\x5cs@\xfdE\x5c>4\xb0Mn\
+\xc7\x83\xf9E\x17\x18\xfd\x06\x9d'\xf5\xaf&\x22\x1a\xa1\
+\x19\x10\xf6\x9f\xb9t&\x0d\x97C?\xf8\xd4\xc4zH\
+S1\xc4HZ\x0dx\xfa\x92~\x04\xf7BMA~\
+\x80\x8e\xd3\xd4:fB\x1fJ\xad\xf5\xe7\xa3\x97F\xc9\
+*\x0b\x9es\x87\x17\x17F\x80\x14\x81+\xbeF\xbc'\
+\xed\x81Y\x22\xca\xc2\xf4\x0b:f\xce\xb65\x15;\x13\
+\x83\xb5\xb06r\xf5\xfb\xab\xab\xe2E\xf8\x84\x15\xe0\x86\
+\x83e\xcc\x1bh\x11/\xc5%Td\x0f\xe4Jt\x81\
+\x06\xed\xc7\xed\xaf\x05\xcdP\xce\xce\xcdV\xab\x13Up\
+\xcb=\xdaC1y\x03M\x13[\xa8\x0f+\x8d\xf06\
+&\xed;\xed/F\xad\xa1,c\xd7\xe4\xf4\xe4\xc2\xdc\
+\xfd9p\xc3=\x9a\xc3J\xfb\xa7x_\xb9\x1f\xf5(\
+\xa8!0o\xfa\xd0\xb5g`m(5\xf6\xff|\xfb\
+F*\x1b\xb2x\x0cB\x0b\xd8\xc8\x16\xe2\x9b\xf8fH\
+\x9dd\x08p\x80\xbe\xa4\xe9\x0dY\x8e\xe9,]\xb4\xca\
+\x1c\xba15N\xa4\xa8\xc8\x00E\x84J^\x81\xed&\
+2\xa1\x1e\xa4!snC\xf7\x034LGc\x19\x9f\
+\xbey\xffV1\x02%U\x82x\xb6\x0c%\x93Q:\
+i?{:\x1b\x92[\xb4\xaf\x16\xd5&\x22\x92x\xe5\
+\xb5Wa\xc9\xde\xc4\x8f\xb4\xe7)\xed\x88|(\xbbx\
+\xe5\xed@\x073\xcfm\x7f~a\xe7\x8e\xd7w\x17A\
+]\xd7\xc5\x88\xd6\xcd\xc1\xe1\x83\xa5\xb1;c=\xed\x5c\
+\xa3\xf3\xcf\xa8c\xa0c\xe0\x0f\xf9CYQ\xbdDt\
+g\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04\xef\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -998,6 +1739,126 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x02\x92\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
+\x00\x00\x00\x03sBIT\x08\x08\x08\xdb\xe1O\xe0\x00\
+\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01\
+}\xd5\x82\xcc\x00\x00\x00\x19tEXtSoft\
+ware\x00www.inkscap\
+e.org\x9b\xee<\x1a\x00\x00\x00\xd8PLT\
+E\xff\xff\xff\xb9\xb9\xb9\xb9\xb9\xd1\xaa\xaa\xaa\x9f\x9f\x9f\
+\x9f\x9f\xa7\xa7\xa7\xa7\xa7\xa7\xaf\xaf\xaf\xaf\xaf\xaf\xb7\xb7\
+\xb7\xb7\xb7\xb7\xbf\xbf\xbf\xbf\xc2\xc2\xc6\xc2\xc2\xc6\xae\xae\
+\xb1\xae\xae\xb1\x9c\x9c\xa1\x9d\x9d\xa2\x9e\x9e\xa2\x9f\x9f\xa4\
+\xa0\xa0\xa5\xa1\xa1\xa5\xa2\xa2\xa6\xa3\xa3\xa8\xa4\xa4\xa8\xa5\
+\xa5\xa9\xa6\xa6\xab\xa7\xa7\xaa\xa8\xa8\xac\xa9\xa9\xae\xaa\xaa\
+\xad\xab\xab\xae\xac\xac\xb1\xad\xad\xaf\xae\xae\xb1\xb0\xb0\xb4\
+\xb3\xb3\xb7\xb6\xb6\xba\xb9\xb9\xbd\xbc\xbc\xc0\xbf\xbf\xc3\xc2\
+\xc2\xc6\xe4\xe4\xe4\xe5\xe5\xe5\xe6\xe6\xe6\xe7\xe7\xe7\xe8\xe8\
+\xe8\xe9\xe9\xe9\xea\xea\xea\xeb\xeb\xeb\xec\xec\xec\xec\xec\xed\
+\xed\xed\xed\xee\xee\xee\xef\xef\xef\xf0\xf0\xf0\xf1\xf1\xf1\xf2\
+\xf2\xf2\xf3\xf3\xf3\xf4\xf4\xf4\xf5\xf5\xf5\xf6\xf6\xf6\xf7\xf7\
+\xf7\xf8\xf8\xf8\xf9\xf9\xf9\xfa\xfa\xfa\xfb\xfb\xfb\xfc\xfc\xfc\
+\xfd\xfd\xfd\xfe\xfe\xfe\xff\xff\xff\xf1_M\x9b\x00\x00\x00\
+\x11tRNS\x00\x0b\x0b\x0f \
+ \xc0\xd4\xd5\xe5\xd1\x9dOM\x00\x00\x01\x0fID\
+ATx\xda\xad\xd3Kn\x021\x10\x04\xd0*\x87|\
+\x84\xb8\x01\xdc\xffJ\x1c \xebd\xc3\x06&\xd8\x15\xe8\
+.\xcb\xf2((\x8b\xc4\xb3\x98\x8f\x9f\xba\xbbf4\x1b\
+\xfc\xb2\xfe\x05\x94\xdd\xe1\xc1\xe6\xfb)\xc0v\xbf\xdd@\
+q\xb4<\xeb\x95\x04\xb1\x1c\x8e\x01\xa0\x97\x86\x5c\xa5Q\
+\x14\x9f\x19w\x8b\x10@\x22\x95\x87\xdb\x16\x10Pm\x1a\
+\x00\x16\xf7\x12\x85\xc1x\xc1\x00\xa5\x0d\xf1\x14\xfd\xc1\xab\
+h\x00\xc5\x93.\x18\xd7\xd0B\x18\xa8\x06\xb0\x00\xc3\xb3\
+\x01\xa83\xc8\x11mt\xe5\x04J\x03\xb2\x85\x92\xaa\xcd\
+\x00\xc4\x0d\xb9\x09\x90\xedi\x00\x03\x10}_\xacw\xbb\
+\xaa\xe0(\xd9+\xd5\x88\x99C\x8c(\x0a%\x83xu\
+FJ\xaf\xf8*%\x01i\x00\xba\xbc\x13\x83\xbd\x02h\
+\x90Q\x80\xfeU\x0c\x08\xd0C8!S\xb8B\xb4\xe2\
+\xa82\xc4\xa8\x00\x80s\x94\x10c\x06\xc0\xbb=\x8aE\
+O\x11`\x9at\x95b\x0d\x5c\x80\xd3\x0c\x98\xa3\xa4\xe8\
+)\xceo]\x8cIE-\xae\xf0\xf5I\xfc\xbc>\x12\
+\xd4\xd3\xf9\x01\xb8$@\xad\x7f\xf9y\xbf\x01\x17\xfa\xb0\
+d\xb4\x11\xf2\x0d\x00\x00\x00\x00IEND\xaeB`\
+\x82\
+\x00\x00\x04\x99\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x04\x00\x00\x00\xd9s\xb2\x7f\
+\x00\x00\x00\x02sBIT\x08\x08U\xecF\x04\x00\x00\
+\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01}\
+\xd5\x82\xcc\x00\x00\x00\x19tEXtSoftw\
+are\x00www.inkscape\
+.org\x9b\xee<\x1a\x00\x00\x04\x18IDAT\
+x\xda}\x95\xcdk\x9cU\x14\xc6\x7f\xf7}\xa73\x93\
+I\xa6Mk\x91\x0cL[\x15\xa5\x0d\xe2B\xa1F+\
+\xd5\xd2fU4-\x08~P\x5c\x89\x7f\x80\xba\x90J\
+Hi\x08\x16\xc16(\xb8\xb1K\x13M[\x14\xdbE\
+\x11jk\xedB\x8d\x11D\xa4D\xa4\xc5E\x8b\x13\x89\
+\xd46\x1f3\xf3N\xe6\xbd\xd7\x87\xc30C\x88\xe4\xfe\
+\xb8$/\x9c\xe7\xb9\xe7$\xe7\x9e\xeb\x02k\xd7\x89\x9c\
+?\xe0\x86\xe8\xa7$\xa0\x22f\xc3\x85\xe8\xf2\xd1\x845\
+k\x8d\xc1X)\x8cp\xc4\x17\x03\x9e@+\x88\x08\xed\
+E&\xdd\xe8pe\x1d\x83\xd1|\x18\xe6M_h\xb2\
+BB\x83\x14\x0f\x12\xc7d\xc9\xb1\x81\x0cQ\x95q7\
+6R\xff_\x83c}\xe1\xab0\x90RgY\xa2\x12\
+e\x8at\x83\xbe\x16\xb9M\x05\xaf\xaf<1n\xda\x1d\
+>>\xb7\xc6`\xf8\xb1p\xd1\x97\x1bTe\xb0\x9b\x9d\
+l\x22ogBJC\xd4\xf8\x95\x19\xf2\x14\xc8\x12\xdd\
+v\x07\xc7~[e\xf0n_\x98\xf1\xe5:\x0bla\
+\x1f\xdb\xe9%\x873 \x90\x8a&\xcb\xccq\x91;l\
+\x94\x8d,v\xbf7\xd76x'\x1f\xae\xfa\x01\xa5\xce\
+\x13:\xbd\x8f.2\xc4\xc2\x89\x00x\xb1B\x83\x84\x05\
+\xbeg\xc6J\x89\xa6\xdd\xbe\xf7\xeb\x10\x01\xa4\xc3~ \
+\xe1\x9e\xe4\x03l\xa3\x87<9\x91\xb5\xad\xdfmw\xd1\
+-6\xf3\x1cO*2\xc1\x0f\xa4\xc3\xad\x0c\xde.\x85\
+\x1b\xcd\xc2\x12E^e{\xab\xf2\x98K<+\x01\xb6\
+\x82\xd1\xb4,j,pZ\xbb\x87L\xd5=|\xb2\x12\
+A:\xd2,\xd4\xa93HY\xf2l\xcb\xe0\x06\x13\xfc\
+Ad\xc4\xc6\x06\xcb\xabK\xd2\x17\xa9\x8bf!\x1d\x81\
+\xf8N\xce\x7f\xba\x92[\xe0\x19\x15\xb0Y\xf2\x8c\x02c\
+\x89~d\x89_\xf8\x8b\x87\xc8\x02\xae\x0d@\x1e\xf8]\
+qn\xe7\xd7\xa7\x22\xbf?-6\xf4\xd1\xcf\x16b\xc3\
+\x19ujT\xf9\x81c\xfc\xdc\xe9G\xcb#+\x9e\xd2\
+O5Z\xd1\xef\x8f\xd2C)\x09\xdb\xec\x1f\x97\xb1\x90\
+\x08\x07$\xc8@\xfc\xcd)>b\xa1m\x91\x11Y\x0a\
+\xe0c\x161\x03\xcbs\xabT2\
+(e|\xc9\x93\xd2c\xe9w\xce\x87?I\xa8[\xf3\
+xbk\xe1k\xdc\xe4\x08\x8f\xe3,r\x0b\x92#\x83\
+\x80h%\x07\x1d\x83[\xdc\xe3\xaev`\x93\x911\x01\
+\x80\xc5\x89`dB%<\xe2X\xc2ll\xd3\xae6\
+Ra\x81\xa8e\xbe\x877(\xe2\xb18\x99\x9bE%\
+\xe3\xcd\xe0.M\xbc\x08\xc2\x96\xf5C\x1e\x08\xd6\x5c[\
+9\xca\xdevWz1o\x06\xbe\x12\x85Y\x88U\xf1\
+\x8a\xa5\x98\xb6-\xb2\x12vS\x14\xdd<\xcfg\x1dy\
++nV*\x08\xb3Q8\x1f\xacq\x13R\x9a\x1d\x03\
+\xbb<=\x92\xef\xe0CN\xd0\x0b\x1d\x03\x8b\xbb.\x95\
+\xbe\xceG\xe1\x0a\x8b15f\xa8\xd2\xa0)\xacJ\x0a\
+f\xf0\x02\xe78\x00m\xb9\xb7\x88\x06\xd7\x14\x1d\xc3b\
+\xb8\x12M$a2\x22\xc77\xcc\xd1\x10V\x0aA\x06\
+%\x8es\x8c\xdeU\xf2\xd4n\xe4?|!ED\x98\
+\x9cH\x22\x08\xa3\xa1\x1a\x03S,PG&\x96\xe2\xd3\
+\x9cd\x0f\xab\xc5:[\xd4\xf9\x84\x80\x0a\xa8\x86Q\x88\
+\xe0\xf3\x0a\xe3\xb1\x1coq\x95%j$b\x85\xd7)\
+\xa0\x5c\x8c\xd4X!\x115\xe5zS\xd11\x8cKI\
+\x06\x801\x06\xe3\x81..\xe1\x18\xc4\x04\xab\x86Zk\
+*\xa2\xd3%\xff\x92.$\x9f\x96\xaa3T_\xd1P\
+\x0d6\x93w\xf0\x1ae\xb2\xc2\x9a\x1b\x00/$g\x9e\
+\xd3\xdc\xb0\xb9\xec4T\xa7:C\xd5,4\xd6C\xb9\
+i\xbd?\xc4^\x85\xc5\xad\xabm\xf5\x8b\xef8\x87\xb3\
+K/\xf9\xc1\xa9\xd5c\x1d\xe0\xe5>\xf4\xb0xK\xb4\
+\x9b]<\xca\xfd\xdc\x87\xe3_\xe6\xb9.\x96\xac'e\
+9\xcd\xe13k\x1f\x16\xb3\xb0\xa7-\x14\xecO\xd6\xee\
+\x08'\x91\x0d:\xe1\xeci;S_\xe7q}\xc9\x1e\
+\xd7P\xec\x5c.g[\xd8\xe3zv\x9d\xc7\xb5c\x92\
+\x0b\x07\x18r\xfd\xa1\xf5\xbc\xbbJ\x98\xe5\x82\xbb|6\
+a\xcd\xfa\x0f\xcb\x16\xf49\xc3\x9f\xd1\x8a\x00\x00\x00\x00\
+IEND\xaeB`\x82\
"
qt_resource_name = b"\
@@ -1009,6 +1870,11 @@ qt_resource_name = b"\
\x0c\xbc.g\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00n\x00e\x00w\x00.\x00p\x00n\x00g\
+\x00\x11\
+\x0e\xab3G\
+\x00z\
+\x00o\x00o\x00m\x00-\x00o\x00r\x00i\x00g\x00i\x00n\x00a\x00l\x00.\x00p\x00n\x00g\
+\
\x00\x14\
\x0b\xa9\xab'\
\x00d\
@@ -1019,10 +1885,26 @@ qt_resource_name = b"\
\x00t\
\x00r\x00a\x00n\x00s\x00f\x00o\x00r\x00m\x00-\x00r\x00o\x00t\x00a\x00t\x00e\x00.\
\x00p\x00n\x00g\
+\x00\x0c\
+\x09\xd0z\x07\
+\x00a\
+\x00r\x00r\x00o\x00w\x00-\x00u\x00p\x00.\x00p\x00n\x00g\
+\x00\x0d\
+\x0b\xe6\x1f\xa7\
+\x00d\
+\x00r\x00a\x00w\x00-\x00t\x00e\x00x\x00t\x00.\x00p\x00n\x00g\
+\x00\x0d\
+\x08Q\xc9'\
+\x00c\
+\x00o\x00n\x00f\x00i\x00g\x00u\x00r\x00e\x00.\x00p\x00n\x00g\
\x00\x0d\
\x03\xd2\xbeg\
\x00e\
\x00d\x00i\x00t\x00-\x00u\x00n\x00d\x00o\x00.\x00p\x00n\x00g\
+\x00\x0b\
+\x03\x03\x9bG\
+\x00z\
+\x00o\x00o\x00m\x00-\x00i\x00n\x00.\x00p\x00n\x00g\
\x00\x0c\
\x07\xb1Y'\
\x00e\
@@ -1035,6 +1917,15 @@ qt_resource_name = b"\
\x0c\xd2\xbf\xe7\
\x00e\
\x00d\x00i\x00t\x00-\x00r\x00e\x00d\x00o\x00.\x00p\x00n\x00g\
+\x00\x16\
+\x0c\xb7\x89\xc7\
+\x00d\
+\x00r\x00a\x00w\x00-\x00b\x00e\x00z\x00i\x00e\x00r\x00-\x00c\x00u\x00r\x00v\x00e\
+\x00s\x00.\x00p\x00n\x00g\
+\x00\x0c\
+\x06\xeb\x97\xe7\
+\x00z\
+\x00o\x00o\x00m\x00-\x00o\x00u\x00t\x00.\x00p\x00n\x00g\
\x00\x11\
\x01\xa6\xc4\x87\
\x00d\
@@ -1044,11 +1935,29 @@ qt_resource_name = b"\
\x0c\xaa\xc0\xa7\
\x00e\
\x00d\x00i\x00t\x00-\x00p\x00a\x00s\x00t\x00e\x00.\x00p\x00n\x00g\
+\x00\x0f\
+\x0bO\xc9\xc7\
+\x00e\
+\x00d\x00i\x00t\x00-\x00s\x00e\x00l\x00e\x00c\x00t\x00.\x00p\x00n\x00g\
+\x00\x0e\
+\x06\x0c\x0a\x07\
+\x00a\
+\x00r\x00r\x00o\x00w\x00-\x00d\x00o\x00w\x00n\x00.\x00p\x00n\x00g\
\x00\x11\
\x0f\xe3\xd5g\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
+\x00\x12\
+\x09\xb3>\xc7\
+\x00d\
+\x00r\x00a\x00w\x00-\x00r\x00e\x00c\x00t\x00a\x00n\x00g\x00l\x00e\x00.\x00p\x00n\
+\x00g\
+\x00\x18\
+\x0f\xa4\x86G\
+\x00m\
+\x00e\x00d\x00i\x00a\x00-\x00p\x00l\x00a\x00y\x00b\x00a\x00c\x00k\x00-\x00s\x00t\
+\x00a\x00r\x00t\x00.\x00p\x00n\x00g\
"
qt_resource_struct = b"\
@@ -1056,27 +1965,49 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x15\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xfa\
+\x00\x00\x01r\x00\x00\x00\x00\x00\x01\x00\x00:\x16\
\x00\x00\x01\x9f{C\xf1'\
-\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00+,\
+\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00Pr\
\x00\x00\x01\x9f{0\xc99\
-\x00\x00\x00\x92\x00\x00\x00\x00\x00\x01\x00\x00\x14\x92\
+\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00/`\
+\x00\x00\x01\x9f\x7f&\x83\xcd\
+\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00'x\
\x00\x00\x01\x9f{C\xf1\x18\
-\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x1cz\
+\x00\x00\x02p\x00\x00\x00\x00\x00\x01\x00\x00`\xe1\
+\x00\x00\x01\x9f\x7f&\x83r\
+\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x00Iw\
+\x00\x00\x01\x9f\x7f&\x83\xb1\
+\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x006\x96\
\x00\x00\x01\x9f{C\xf1.\
-\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
+\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x01\x00\x00!\xca\
+\x00\x00\x01\x9f{{\xa5\xd5\
+\x00\x00\x02\xba\x00\x00\x00\x00\x00\x01\x00\x00i\xc6\
+\x00\x00\x01\x9f\x7f&\x83\x10\
+\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xcd\
+\x00\x00\x01\x9f\x7f&\x83~\
+\x00\x00\x02L\x00\x00\x00\x00\x00\x01\x00\x00\x5c\xfe\
+\x00\x00\x01\x9f\x7f&\x83T\
+\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f{0\xc9B\
-\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x0026\
+\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x1fp\
+\x00\x00\x01\x9f\x7f&\x83)\
+\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x00W|\
\x00\x00\x01\x9f{C\xf1N\
-\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf2\
+\x00\x00\x01\xb2\x00\x00\x00\x00\x00\x01\x00\x00EH\
+\x00\x00\x01\x9f\x7f&\x82\xf2\
+\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x15-\
\x00\x00\x01\x9f{\x8d\xf34\
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9f{0\xc9+\
-\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00#Z\
+\x00\x00\x01\x92\x00\x00\x00\x00\x00\x01\x00\x00=v\
\x00\x00\x01\x9f{C\xf1=\
-\x00\x00\x01Z\x00\x00\x00\x00\x00\x01\x00\x007\xb8\
+\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
+\x00\x00\x01\x9f\x7f&\x83\x99\
+\x00\x00\x02\xe4\x00\x00\x00\x00\x00\x01\x00\x00l\x5c\
+\x00\x00\x01\x9f{{\xa5\xe3\
+\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00d\xd3\
\x00\x00\x01\x9f{0\xc9R\
"
diff --git a/BEdit/src/bedit/core/model.py b/BEdit/src/bedit/core/model.py
index 758c835..b918133 100644
--- a/BEdit/src/bedit/core/model.py
+++ b/BEdit/src/bedit/core/model.py
@@ -51,18 +51,33 @@ class Icon:
def __post_init__(self) -> None:
if not self.elements:
- self.elements = [{
- "type": "ellipse" if self.shape == "ellipse" else "rectangle",
- "x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0,
- "fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
- "lineStyle": "solid", "cornerRadius": 5.0,
- }]
+ self.elements = [
+ {
+ "type": "ellipse" if self.shape == "ellipse" else "rectangle",
+ "x": 32.0,
+ "y": 32.0,
+ "width": 64.0,
+ "height": 64.0,
+ "fill": self.fill,
+ "stroke": self.border,
+ "lineWidth": 1.5,
+ "lineStyle": "solid",
+ "cornerRadius": 5.0,
+ }
+ ]
if self.text:
- self.elements.append({
- "type": "text", "x": 40.0, "y": 40.0,
- "width": 48.0, "height": 48.0,
- "text": self.text, "color": "#202020", "fontSize": 12.0,
- })
+ self.elements.append(
+ {
+ "type": "text",
+ "x": 40.0,
+ "y": 40.0,
+ "width": 48.0,
+ "height": 48.0,
+ "text": self.text,
+ "color": "#202020",
+ "fontSize": 12.0,
+ }
+ )
def to_dict(self) -> dict[str, Any]:
return {
@@ -137,15 +152,57 @@ class Connection:
)
+@dataclass
+class Annotation:
+ id: str
+ kind: str
+ x: float = 0.0
+ y: float = 0.0
+ width: float = 0.0
+ height: float = 0.0
+ text: str = ""
+ layer: int = -1
+ properties: dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "id": self.id,
+ "kind": self.kind,
+ "position": {"x": self.x, "y": self.y},
+ "size": {"width": self.width, "height": self.height},
+ "text": self.text,
+ "layer": self.layer,
+ "properties": deepcopy(self.properties),
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "Annotation":
+ position = data.get("position", {})
+ size = data.get("size", {})
+ return cls(
+ id=str(data["id"]),
+ kind=str(data["kind"]),
+ x=float(position.get("x", 0)),
+ y=float(position.get("y", 0)),
+ width=float(size.get("width", 0)),
+ height=float(size.get("height", 0)),
+ text=str(data.get("text", "")),
+ layer=int(data.get("layer", -1)),
+ properties=deepcopy(data.get("properties", {})),
+ )
+
+
@dataclass
class Graph:
blocks: dict[str, Component] = field(default_factory=dict)
connections: dict[str, Connection] = field(default_factory=dict)
+ annotations: dict[str, Annotation] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"blocks": [block.to_dict() for block in self.blocks.values()],
"connections": [connection.to_dict() for connection in self.connections.values()],
+ "annotations": [item.to_dict() for item in self.annotations.values()],
}
@classmethod
@@ -153,13 +210,19 @@ class Graph:
data = data or {}
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
+ annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
if len({block.id for block in blocks}) != len(blocks):
raise ValueError("A graph contains duplicate component IDs")
if len({connection.id for connection in connections}) != len(connections):
raise ValueError("A graph contains duplicate connection IDs")
+ if len({item.id for item in annotations}) != len(annotations):
+ raise ValueError("A graph contains duplicate annotation IDs")
+ if any(item.kind not in {"box", "line", "text"} for item in annotations):
+ raise ValueError("A graph contains an unknown annotation kind")
return cls(
blocks={block.id: block for block in blocks},
connections={connection.id: connection for connection in connections},
+ annotations={item.id: item for item in annotations},
)
@@ -349,14 +412,35 @@ def clone_component(source: Component) -> Component:
for connection in current.graph.connections.values()
for new_id in [str(uuid4())]
},
+ annotations={
+ new_id: Annotation(
+ new_id,
+ annotation.kind,
+ annotation.x,
+ annotation.y,
+ annotation.width,
+ annotation.height,
+ annotation.text,
+ annotation.layer,
+ deepcopy(annotation.properties),
+ )
+ for annotation in current.graph.annotations.values()
+ for new_id in [str(uuid4())]
+ },
)
return Component(
id=str(uuid4()),
name=current.name,
x=current.x,
y=current.y,
- inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.inputs],
- outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.outputs],
+ inputs=[
+ Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
+ for port in current.inputs
+ ],
+ outputs=[
+ Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
+ for port in current.outputs
+ ],
icon=Icon.from_dict(current.icon.to_dict()),
properties=deepcopy(current.properties),
implementation_kind=current.implementation_kind,
diff --git a/BEdit/src/bedit/gui/controllers/commands.py b/BEdit/src/bedit/gui/controllers/commands.py
index e428a30..bbae113 100644
--- a/BEdit/src/bedit/gui/controllers/commands.py
+++ b/BEdit/src/bedit/gui/controllers/commands.py
@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand
-from bedit.core.model import Component, Connection, Port
+from bedit.core.model import Annotation, Component, Connection, Port
class AddComponentCommand(QUndoCommand):
@@ -76,6 +76,55 @@ class AddConnectionCommand(QUndoCommand):
self.controller._remove_connection(self.owner_id, self.connection.id)
+class AddAnnotationCommand(QUndoCommand):
+ def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
+ super().__init__(f"Draw {annotation.kind}")
+ self.controller, self.owner_id, self.annotation = controller, owner_id, annotation
+
+ def redo(self) -> None:
+ self.controller._insert_annotation(self.owner_id, self.annotation)
+
+ def undo(self) -> None:
+ self.controller._remove_annotation(self.owner_id, self.annotation.id)
+
+
+class EditGraphItemCommand(QUndoCommand):
+ def __init__(
+ self,
+ controller,
+ owner_id: str,
+ item_kind: str,
+ item_id: str,
+ old: dict,
+ new: dict,
+ text: str,
+ ) -> None:
+ super().__init__(text)
+ self.controller, self.owner_id = controller, owner_id
+ self.item_kind, self.item_id = item_kind, item_id
+ self.old, self.new = old, new
+
+ def redo(self) -> None:
+ self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.new)
+
+ def undo(self) -> None:
+ self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old)
+
+
+class DeleteAnnotationsCommand(QUndoCommand):
+ def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None:
+ super().__init__("Delete annotations")
+ self.controller, self.owner_id, self.annotations = controller, owner_id, annotations
+
+ def redo(self) -> None:
+ for annotation_id in self.annotations:
+ self.controller._remove_annotation(self.owner_id, annotation_id)
+
+ def undo(self) -> None:
+ for annotation in self.annotations.values():
+ self.controller._insert_annotation(self.owner_id, annotation)
+
+
class ReplaceComponentCommand(QUndoCommand):
def __init__(self, controller, old: Component, new: Component) -> None:
super().__init__("Apply JSON changes")
diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py
index 1abc7c2..12f3120 100644
--- a/BEdit/src/bedit/gui/controllers/document.py
+++ b/BEdit/src/bedit/gui/controllers/document.py
@@ -8,8 +8,11 @@ from PySide6.QtGui import QUndoStack
from bedit.gui.controllers.commands import (
AddComponentCommand,
AddConnectionCommand,
+ AddAnnotationCommand,
AddInterfacePortCommand,
DeleteSelectionCommand,
+ DeleteAnnotationsCommand,
+ EditGraphItemCommand,
EditTextDefinitionCommand,
EditComponentAppearanceCommand,
MoveComponentCommand,
@@ -21,6 +24,7 @@ from bedit.gui.controllers.commands import (
RotateComponentsCommand,
)
from bedit.core.model import (
+ Annotation,
Component,
Connection,
Endpoint,
@@ -43,6 +47,9 @@ class DocumentController(QObject):
componentRotated = Signal(str, float)
connectionAdded = Signal(str)
connectionRemoved = Signal(str)
+ graphItemChanged = Signal(str, str)
+ annotationAdded = Signal(str)
+ annotationRemoved = Signal(str)
interfaceChanged = Signal()
filePathChanged = Signal(object)
modifiedChanged = Signal(bool)
@@ -221,16 +228,14 @@ class DocumentController(QObject):
if (component := self.active_component.graph.blocks.get(component_id)) is not None
}
if rotations:
- self.undo_stack.push(
- RotateComponentsCommand(self, self.active_component_id, rotations)
- )
+ self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations))
def connect(
self,
source: Endpoint,
target: Endpoint,
*,
- routing: str = "spline",
+ routing: str = "angled",
waypoints: list[QPointF] | None = None,
) -> str:
if self.active_component_id is None:
@@ -240,9 +245,7 @@ class DocumentController(QObject):
if source_port is None or target_port is None:
raise ValueError("A connection endpoint no longer exists")
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
- raise ValueError(
- f"Cannot connect {source_port.type!r} to {target_port.type!r}"
- )
+ raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
if routing not in {"direct", "angled", "spline"}:
raise ValueError(f"Unknown connection routing: {routing}")
connection = Connection(
@@ -251,16 +254,154 @@ class DocumentController(QObject):
target,
properties={
"routing": routing,
- "waypoints": [
- {"x": point.x(), "y": point.y()} for point in (waypoints or [])
- ],
+ "waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
},
)
- self.undo_stack.push(
- AddConnectionCommand(self, self.active_component_id, connection)
- )
+ self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
return connection.id
+ def add_annotation(
+ self,
+ kind: str,
+ start: QPointF,
+ end: QPointF,
+ *,
+ text: str = "",
+ routing: str = "angled",
+ waypoints: list[QPointF] | None = None,
+ ) -> str:
+ if self.active_component_id is None:
+ raise ValueError("There is no active graph")
+ if kind != "line":
+ left, right = sorted((start.x(), end.x()))
+ top, bottom = sorted((start.y(), end.y()))
+ start, end = QPointF(left, top), QPointF(right, bottom)
+ style = {
+ "stroke": "#303030",
+ "lineWidth": 1.5,
+ "lineStyle": "solid",
+ "fill": "none" if kind in {"line", "text"} else "#dbeafe",
+ }
+ if kind == "box":
+ style["cornerRadius"] = 0.0
+ if kind == "text":
+ style.update({"fontSize": 12.0, "color": "#202020"})
+ annotation = Annotation(
+ id=str(uuid4()),
+ kind=kind,
+ x=start.x(),
+ y=start.y(),
+ width=end.x() - start.x(),
+ height=end.y() - start.y(),
+ text=text,
+ layer=-1,
+ properties={
+ **style,
+ "routing": routing,
+ "waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
+ },
+ )
+ self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation))
+ return annotation.id
+
+ def set_route_waypoints(
+ self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None
+ ) -> None:
+ item = (
+ self.active_graph.connections
+ if item_kind == "connection"
+ else self.active_graph.annotations
+ ).get(item_id)
+ if item is None or self.active_component_id is None:
+ return
+ old = deepcopy(item.properties)
+ new = deepcopy(old)
+ if routing is not None:
+ new["routing"] = routing
+ new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints]
+ if old != new:
+ self.undo_stack.push(
+ EditGraphItemCommand(
+ self, self.active_component_id, item_kind, item_id, old, new, "Edit line nodes"
+ )
+ )
+
+ def set_annotation_geometry(self, annotation_id: str, old: dict, new: dict) -> None:
+ if old != new and self.active_component_id is not None:
+ self.undo_stack.push(
+ EditGraphItemCommand(
+ self,
+ self.active_component_id,
+ "annotation_geometry",
+ annotation_id,
+ old,
+ new,
+ "Move annotation",
+ )
+ )
+
+ def edit_annotation(self, annotation_id: str, values: dict) -> None:
+ annotation = self.active_graph.annotations.get(annotation_id)
+ if annotation is None or self.active_component_id is None:
+ return
+ old = annotation.to_dict()
+ if old != values:
+ self.undo_stack.push(
+ EditGraphItemCommand(
+ self,
+ self.active_component_id,
+ "annotation_data",
+ annotation_id,
+ old,
+ deepcopy(values),
+ "Edit shape",
+ )
+ )
+
+ def reorder_annotations(self, annotation_ids: set[str], operation: str) -> None:
+ if not annotation_ids or self.active_component_id is None:
+ return
+ graph = self.active_graph
+ layers = [item.layer for item in graph.annotations.values()]
+ minimum, maximum = min(layers, default=-1), max(layers, default=1)
+ for annotation_id in annotation_ids:
+ item = graph.annotations.get(annotation_id)
+ if item is None:
+ continue
+ old = {"layer": item.layer}
+ forward = item.layer + 1
+ backward = item.layer - 1
+ if forward == 0:
+ forward = 1
+ if backward == 0:
+ backward = -1
+ layer = {
+ "forward": forward,
+ "backward": backward,
+ "front": max(1, maximum + 1),
+ "back": min(-1, minimum - 1),
+ }[operation]
+ self.undo_stack.push(
+ EditGraphItemCommand(
+ self,
+ self.active_component_id,
+ "annotation_layer",
+ annotation_id,
+ old,
+ {"layer": layer},
+ "Reorder annotation",
+ )
+ )
+
+ def delete_annotations(self, annotation_ids: set[str]) -> None:
+ items = {
+ key: self.active_graph.annotations[key]
+ for key in annotation_ids
+ if key in self.active_graph.annotations
+ }
+ if items and self.active_component_id is not None:
+ self.undo_stack.push(DeleteAnnotationsCommand(self, self.active_component_id, items))
+
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
owner = self.active_component
if owner is None:
@@ -272,7 +413,9 @@ class DocumentController(QObject):
if component is None:
return None
ports = component.outputs if role == "source" else component.inputs
- return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None)
+ return next(
+ (port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
+ )
def connection_port_type(self, connection: Connection) -> str:
port = self._port_for_endpoint(connection.source, "source")
@@ -289,9 +432,7 @@ class DocumentController(QObject):
x=position.x(),
y=position.y(),
)
- self.undo_stack.push(
- AddInterfacePortCommand(self, component.id, direction, port)
- )
+ self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
return port.id
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
@@ -350,11 +491,17 @@ class DocumentController(QObject):
parent = self.document.find_parent(component.id)
if parent is not None:
for connection in parent.graph.connections.values():
- if connection.target.block == component.id and connection.target.port not in input_ids:
+ if (
+ connection.target.block == component.id
+ and connection.target.port not in input_ids
+ ):
raise ValueError(
f"Input {connection.target.port!r} is still connected in the containing graph"
)
- if connection.source.block == component.id and connection.source.port not in output_ids:
+ if (
+ connection.source.block == component.id
+ and connection.source.port not in output_ids
+ ):
raise ValueError(
f"Output {connection.source.port!r} is still connected in the containing graph"
)
@@ -422,9 +569,15 @@ class DocumentController(QObject):
parent = self.document.find_parent(component_id)
if parent is not None:
for connection in parent.graph.connections.values():
- if connection.target.block == component_id and connection.target.port not in input_ids:
+ if (
+ connection.target.block == component_id
+ and connection.target.port not in input_ids
+ ):
raise ValueError("An input cannot be removed or reoriented while connected")
- if connection.source.block == component_id and connection.source.port not in output_ids:
+ if (
+ connection.source.block == component_id
+ and connection.source.port not in output_ids
+ ):
raise ValueError("An output cannot be removed or reoriented while connected")
for connection in component.graph.connections.values():
if connection.source.interface and connection.source.interface not in input_ids:
@@ -471,7 +624,9 @@ class DocumentController(QObject):
or connection.target.interface in output_ids
):
all_connection_ids.add(connection.id)
- blocks = {block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks}
+ blocks = {
+ block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks
+ }
connections = {
connection_id: graph.connections[connection_id]
for connection_id in all_connection_ids
@@ -526,9 +681,7 @@ class DocumentController(QObject):
)
connections[connection.id] = connection
if blocks:
- self.undo_stack.push(
- PasteSelectionCommand(self, owner.id, blocks, connections)
- )
+ self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks)
def _graph_for(self, owner_id: str):
@@ -571,9 +724,7 @@ class DocumentController(QObject):
self.componentMoved.emit(component_id, position)
self.documentReset.emit()
- def _rotate_component(
- self, owner_id: str, component_id: str, rotation: float
- ) -> None:
+ def _rotate_component(self, owner_id: str, component_id: str, rotation: float) -> None:
component = self._graph_for(owner_id).blocks.get(component_id)
if component is None:
return
@@ -593,6 +744,46 @@ class DocumentController(QObject):
self.connectionRemoved.emit(connection_id)
self.documentReset.emit()
+ def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
+ self._graph_for(owner_id).annotations[annotation.id] = annotation
+ if owner_id == self.active_component_id:
+ self.annotationAdded.emit(annotation.id)
+ self.documentReset.emit()
+
+ def _remove_annotation(self, owner_id: str, annotation_id: str) -> None:
+ self._graph_for(owner_id).annotations.pop(annotation_id, None)
+ if owner_id == self.active_component_id:
+ self.annotationRemoved.emit(annotation_id)
+ self.documentReset.emit()
+
+ def _set_graph_item_data(
+ self, owner_id: str, item_kind: str, item_id: str, values: dict
+ ) -> None:
+ graph = self._graph_for(owner_id)
+ if item_kind == "connection":
+ item = graph.connections.get(item_id)
+ if item is not None:
+ item.properties = deepcopy(values)
+ else:
+ item = graph.annotations.get(item_id)
+ if item is not None:
+ if item_kind == "annotation_geometry":
+ item.x, item.y = float(values["x"]), float(values["y"])
+ item.width, item.height = float(values["width"]), float(values["height"])
+ elif item_kind == "annotation_layer":
+ item.layer = int(values["layer"])
+ elif item_kind == "annotation_data":
+ replacement = Annotation.from_dict(values)
+ item.kind = replacement.kind
+ item.x, item.y = replacement.x, replacement.y
+ item.width, item.height = replacement.width, replacement.height
+ item.text, item.layer = replacement.text, replacement.layer
+ item.properties = replacement.properties
+ else:
+ item.properties = deepcopy(values)
+ if owner_id == self.active_component_id:
+ self.graphItemChanged.emit(item_kind, item_id)
+
def _replace_component(self, old_id: str, replacement: Component) -> None:
if self.document is None:
return
diff --git a/BEdit/src/bedit/gui/generated/resources_rc.py b/BEdit/src/bedit/gui/generated/resources_rc.py
index 915731e..3752d1e 100644
--- a/BEdit/src/bedit/gui/generated/resources_rc.py
+++ b/BEdit/src/bedit/gui/generated/resources_rc.py
@@ -97,6 +97,124 @@ d\x01\x97\xdb%;\xbb\xf6\x9f\xfe\xea\xe8\xb1b\x99\xdb\
\xdf\x22n\xd7\x80\xca&\x04\x84\xf8\x1f\xf9\x17g\x93$\
jH\x9d\xf5+\x00\x00\x00\x00IEND\xaeB`\
\x82\
+\x00\x00\x077\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\
+\x12%\x1d\xb8\xa17t\x00\x00\x06\xb7IDATX\
+\xc3\xc5\x96mPT\xd7\x19\xc7\x9fs\xee\xdd{w\xef\
+\xee\x22\xb0Kx\x0b\xb0+\xac\x1d\x86\xb6X,\x08:\
+V$\xd1i5\x99\x14-b\x0cVh\xeb\x84L\xda\
+t&\x9dL\x13\x8d\xfd\x82\x9d6\x1f\xd21\x99\x1a\x0d\
+\x12A4N\x09RM:a&\xe9\xb4U\xa9\x81\x08\
+\x81\x00\x01$\x09\xc8\xb2\xbb \xfb\xc6\xbe\xde\xb7\xbd\xf7\
+\x9e~,\x9d1i\x16C\xfb\xfbt\xe6\x9c;s~\
+\xf3\xdc\xff3\xe7\x01H\x02\xb7\xc7\xfd\x85g\x1f\x8f\x8e\
+\xc0j\xc0\xc9\x5cl6\x99w\x07\x82\xfe\xab\x91ph\
+!\x1c\x0a\x92@\xc0K\x96\x96\x16\xdc\xf3\xce\xb96\xb3\
+y]\x15\x00\xc0\xc0@\x7fR\x02\xe8\xab|\xe4\xf5z\
+7\x12\xa2\xbc\xe3Z\xf4\xe5]\xe9\xed[6\x1a\x0c\x08\
+c\x0c,\xcb@4\x1a\x85h,\x06\xb5\xdf\xafJe\
+\x19v,\xa1j\x0fUVT\xf8\xbf6\x01\xb7\xc7\xf3\
+\xb8\xaa\xca\x97\xce\x5c\xfc\xab\xd7n\xb3\xa9\xf5\xb5;\x0d\
+\x1a\x01\xa2i\x0a\x01@@Q\x14\xd2T\x02o\x5cz\
+GXt}\xce\xfepg\x99E\xd1\xa0\xb2\xa6\xba\xe6\
+\xc3\xfb\x16\x98\xbd3S#\x8a\xe2\xdfZ\xdf\xeaw\xfd\
+\xb2\xf9\x09\x1dB\xa0\x22\x0c*FX\xc3\x88\x10\x02\x00\
+@\x00i\x1a`@@E\x22<~\xf5\xf49\xed\x91\
+m\xebs\xe5\x84V\x18\x8dFf\x9b\x1a\x7f\x92\xbc\xc0\
+\xe0\xe0-0\xa4\x98iJI$:{\xc7]\x87\x0f\
+\xee\xd5\x08\xd1$\x8b\x85\x13cQ\x190\x06\x15cD\
+\x10\x00h\x04@\xd3\x08e21(\x10\x14\xf4\x92,\
+\xb3\x1d\xed\x1d\xfa2G\xaap\xe8P\xa3}U!,\
+/\xaf\x00)\x1eo\xb992\x13\xd9\x5c\xb95\x1e\x13\
+\x94PF\x16\xb7\xb4\xa9bW\xf1:+s\x17Q\xc8\
+\x8b(\xb4\x04\x14ZB\x14\xf2\xe5Y\x19W\xd9\xe6]\
+\xc5F\xb3\xc1\xa7hTh\xd3\x96\x87\xc32\xd1\xe5]\
+\xb8p\xfe\x89\xdf\xbftbu]\xa0*\xd2\xb3\xff\x9c\
+\xe6\xe7\xd2-i<\xad\xe7\xfc\xcdO\x1d/\xdb\xb1{\
+\xaf\x87\x10\xb8\x8buh\x11\xeb\xd0\x02\xc5\xa0\x85\xf4u\
+\xb4\xf3\xc0\x91\x17\xbf\xb7\xe3\x07{]\x04A\x90\xd6a\
+\xdeQd\x13'\x9d\xbcgci\xe9\xd3\xcf\xff\xfa\xc5\
+\xe4\x05\xae\xdf\xb8V\x10\x0d\xc7\x18W\xd4\xe8\x99\x0fH\
+\x9ep\x8c\x0f\xb6\xb7\xb6\x88:.-\x801xh\x06\
+\xbb3\x0d\xd4\xe7\x14\x8d\xddqE\x9b\xef8{B\xd0\
+\x19R\xfd\x06=\x08\xa9\x9c\x8e2\xb2\x94zWJ\xf5\
+\xc4\xe3\x91\xaaU\xfd\x02\x9e\xe77\xcc{#j\xba5\
+C\x1cq\xf23\x93>e\xe2\x13?\xb4H\x09\x99g\
+\x00\xdcV\x1dv=z\xf0\xd9\xe7\x1f\xa0\x91K\x03\x14\
+&\x00\xc7\xa4\x84, \x1a\xe6dB\x16=\xcb\xb2\x93\
+1g\x85>\x99\x98\x82\x9f\x1di\xca\xfd2\x01\xfa^\
+\x9b\x82 \xa4\x08\xa2,\xb3\xac^\xd1\xb3LB\xa6 \
+\xe8\xa7\xc1\x19X\x8eyg\x00\xfafb\x09\xb9\xf3\xd2\
+\x1f\x02\xef\xc6\x95\xebz\x8c\x0c\xe94\xa5z\x83\xb1]\
+\x0b\x00\x7f\xf7\xf1jx1\xa6\xe6\x82\xce\x90u{z\
+\x1a8\xceH%]\x01Q\x14=H\x93\x0c\xaaF\x00\
+QH\xd3\xe9p\xc2d\x02\x01\x00\x11#\x80\x9a\x86\x11\
+m\x05\xd8\x94\x82\x013\x08I&\x00\x05\x08!\x08 \
+\xe8\x91\xb5\xd9\x88H\x22:\x9aB\xa2\x10\x03A\xe2\x97\
+\x93\x16p\xba\x9d\x1f\xa7\x9b\x19\xd0dQo\xa0\x11K\
+\x00\x08E\x83\x12\x8d\xc4\x15\x1d@\xb8\x9a\xa3\x83;\xf7\
+?3\xbd\xcd@\xfbh\x04\x22\x07 E\x22|B\x03\
+\x88\x18t\x18q,\xd6\xeb\x13\x8b\x19<\x1f\x8b\x9d=\
+s.\x9a\xb4\xc0\x0b\xcf\x1d\x15srs\x87\xb6\xda\xe5\
+\xe2-\x85\x86o\xa5\xb3\xc8\xe2\xf1I\xca\x9f;\x7fU\
+\x8ad\xb8\xf1B \x1c\xeez\xeb\xd5\xe57Cq\x89\
+B }$(\xb1\xb7\xdf|nS\x9a\x02\xbd,\x01\
+F\x91\x94\x14\xc57\x91\x1f\x8d\x89=\xab\x0a\xe1\xa7\x13\
+\xb30y{\xfaDMYV\xd1\xccB\x1c\xf9\x83\x8a\
+\x01K\xc4\xfa\xa1\xa0\xd4\x8fkjM\xad\xc9l\xedU\
+\xd5\xd2\x12#GQ\x08\xc4L\x8ab\x87\x05u\xf7\x82\
+\xaa\x1e1\x11(\xf1\xfb\xc2\x96\xd8\xe2p>\xa6\xa8c\
+u\xf5\xfb \xe9\x10n(Y\x0f\x00\xf0\xf6\x85\x8b\x1d\
+\x83s\x0b\xca\x86X\xce\x9eI\xa4\xa7\xb2\x1d\xa9tH\
+O\x01\xd6\x10,\x17k\xc8\x80\x11\x8a#\x02\x8c\x0e\x11\
+\xd6\xa2C\xc5\xaa\xa0\x95\x8f\xbb\xd5\x8c\xe0H\xc7\xc3\xf1\
+\xb8p\xa6\xe7\xf2U\xcf\x7f\xab\xc0\x17&\xf4\xf8o\x8e\
+A\xdb\xebg\xdb\xf3\xb3\x8dGc\xde\xd9\x02\xd1X\x1a\
+\xd0#\x842\xad:\x85\xa3\x80\xe2\x00\xe9\xd21pf\
+\x0c\xb6\x14\x0a\x7fG\x91`\xe3\xcdq!\xef\xce\xb5\xd3\
+\xb53\xb7?\x9a\xef\xe9\xbe\xba\xa7\xae~\x1fLNL\
+\xad\xfe1\xfa\xc53O\x83\x94\x90L\xaa,\x0f(\x98\
++29\xf6\xfc\xc3\xb1e\xf7\xa0#\x0f/\xe5X\x80\
+7\xd0@\x0b\x12\x98\x9d\x1e%{qjx\xf3\xc4\xf5\
+\xce\xadn\xd7\xfc\xc0\xd5+\x7f\xd9ZW\xbf\x0f\xba\xbb\
+z\xee\xff9n\xfa\xe9ah\x7f\xe3<<\xdeP\xff\
+sU\x96^\xa2\xf5F\xced)ts)\x96\x08\xa3\
+g\x88\x1c\xf1e\xc8\x11\xcf\x03.\xb7s\xfe\xc7\x87\x9a\
+\xf2\x03\x81`\xa8\xf9\xc9\xa7\xd2\xea\x1b\xea\xcc\x9a\xa4D\
+\xbb\xbb\xaf\xdc\xff@\xb2\x92\xfd\x07~T\xaai\xeaN\
+\xa2\x11+\x00\xb0\x08\xa3)\x84\xf0\x8d\xee\xae\x9e\xdb\xad\
+g[;\xaa\xb7W?v\xf3f_j\xff@\x7fW\
+\xeb\xebm\x07`-X\x99\xec\x95\xeb\xae?uM\x12\
+B\xc8\xb5\xeb\xd7\xb4\xdf\xfe\xae\x85\xd4\xd6=Vr\xe0\
+\xe0\xfe\xd5\x85\xf0\xcbX\x19\xac\x95k\xbb\xcd^RX\
+TX\x9e\x93\x93\x83\xe299\xb5\xb6\x02CCC066\
+\xb6h\xb7\xdb7\x96|\xb3\xe4\x1b)\xe6\x14\xa4$\x14\
+\xb2\x1c\x5c.\xef<\x7f\xf1\xfc\x9aW\x00\x00\xa0\xa1\xa1\
+\x01FGG{9#w\xb4\xaa\xaa\x0ax\x9eG<\
+\x1f\xb7\xa5[\xd6\xf5m\xab\xd8rghdxm\x05\
+\xc6\xc6\xc6\xc0\xe5r\xc9\x8e\x22\x87\xf6`\xde\x83;r\
+r\xb2I\x22\x91@\x81\x80\x7f\xfb\x1f_;\xfd\xca\x9a\
+t\xc1\xbd8y\xf2d\xcb{\xef\xbf\x17\xd6\xeb\x0dr\
+ff\xa6\x5cP`\xb7\xadY\x1b\xde\x8b\xcb\x97/\xc3\
+\xdc\xdc\xdc\xae\x80?\xc0\xb2,\xcbX\xac\x96\x8b\xf0\xff\
+\x80\x90\x84\xfe\x83\xfe\x0fl\x00\x00/\xbf\xf2\xf2\x7f\x9c\
+\xfd\x0bVu/\x8d\xbcS'\xfc\x00\x00\x00\x00IE\
+ND\xaeB`\x82\
\x00\x00\x08h\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -342,6 +460,199 @@ K\x18+\x8f\xf7\xd7\x8d<\xbdGN\xca\xda\xd3S\xd4\
n\xb8\x10\xff\x17\x1a\x96\x9e\xd2\xd9Z\xe6L-\xdc\xe3\
\x8c\xc7y\xbe\x06\xff\x01\xd8\x02\xdah\xfa;i$\x00\
\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x03\x9f\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\
+\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\
+\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\
+tware\x00www.inksca\
+pe.org\x9b\xee<\x1a\x00\x00\x03\x1cID\
+ATx\xda\xed\xd7\xdfk\x1cU\x14\x07\xf0\xef\xf7\xde\
+;wfvf\x13\xb3i6\x0d\x1a6!Ulc\
+#Q\xfb\xa0\x06eAJ#\xb6VcS\x11\xfa\xdc\
+\xb4O\x0a\x01m)\x11\x11\x04\x9f\x05A\xa8\x14\xc4\xb7\
+B\x15,\xf6IP\xff\x0a\xa1\xc5\x07\xed\x83H\x91Z\
+\xb3\xba\xdd\xd9{\xaf\xf3#\xec\x0fK(\xd54A\xd8\
+s\xf8\xb2\xf3v>\x9c9\xb0\x0c\x9ds\xd8\xc9\x12i\
+\x06\x80\x01\xe0\xff\x0b`\x9d\xa2~\xb2\xaev\x04\xc07\
+8=\xf6X\xf5\xda\xe4\x81\xda\xf5\xeaj\xf5\xe1m\x05\
+p75\x04\xbf\xdc%G\xa7\x7f\xbe\xfe\xd3\xf8\x8d\xf5\
+\xdf\xbe\xe0,\xf5\xb6\x01\xf0\xafD\x95\xb9\x181\
+\xbe\xff\xe6;h\xe8Y<\xc7\x8b\xdb\x02\xe02\xcf\x84\
+Q\xf8\xda\xbe\xb1\xbd\xb0\x9e\x85\x0b\x1d\xcc/\x09\xbc\x92\
+\x7f\x98\xa7\xe5{\xf7\x15\xc0W\xb8(}\xb5\xf6\xe4\xd4\
+\x13\xa4&\x9cv`L\xb4e\x1b\xf2&\xa9\x86\xbcw\
+\xb8\xa2\x8e\xde\x17\x00\x8fq\x0a\x1e.\x1c\x98y*\x88\
+\xe2\x08F\x19\x18\xcf@\xc6\x12(\x01M\xf3\x17\xda\xa6\
+\xa51\x82O\xb9\xc2\x99-\x05\xe4\x07\x96\xe0\xd2\xfe\xe9\
+\xfd\xe3\x13\x13\x13\xb0\xd2\xe6\xeb\xb7\xca\x16\x80\x08E\x9a\
+\x0e0\xae\x82\x8a\xba\xcc\x17\xe9o\x19\x003\xf8\xac\xf6\
+`m~nv\x0eV\xe4\x83\x8bh\x0b\x96\x08\x94\xd0\
+A(O\xa0\x1c\xc5\x8f\xe2qyiK\x00|\x89g\
+*C\x95\xe5\xfa3\xf5|\xb8\x91&K\x0ep\x9e\x83\
+\xf5m\x06\xe8 \xdaa\x1b\x89l\x22,\x97\x16\xf9\xb6\
+\xfc\xe0?\x01x\x98\x8bZ\xeb\xb5#\x07\x8fP\xfa2\
+\x03\x14\x916\xbf\x01\x06\x84\x0dl\xdf\x06\x10\x03\xcd\xa0\
+\x09\x15\x93z8X\xe5\xaaZ\xfaW\x00\xbe\xca)\x10\
+\x17\x96\x0e.\x05\xe5\x912l\xd6\xcc\x01\x9d\x0dP\x13\
+\x89N\x80\x10=\x88\x22\x7f\xf8\xb70\xb4+\xf6X\x11\
+\xe7\xf9&\xf7\xdc\x03\xa0{t\x87\x9e>4^\x9b\xa9\
+\xe5\x83\x0d\x0d\xd2.\x10\xd2v\x0e\x11\x012@?\x22\
+.rC\xff\x0a\x17\xd8\x07P\xf5\xaelv\x94b\xb3\
+\xa3\x9b\x7fd~~\xe1\xd9\x05\x18g`\x9d\x85\xc5\x06\
+B\x98\xcekp\xca\x01>\x80\xa0\x17\xd0\x1f\xaf*P\
+\x19\x1e\xd9\x83\x05\xf5\xd5]\x00\xdd\xa3\x9b\x1c\x9b\x5c>\
+\xfe\xf2q$.\xc9\x86w\x927\xbb\x10'\x1c\xa0\xd1\
+\x8b\xe8n *@I\x90\xe0\xf6P\x03qT~\x81\
+\xef{\x1fn\x02\xe8\x1e]),\xad\x9dz\xfd\x14\xa9\
+\x09k-\x8c5\xc5\x16\x8a\xee\xdb\x06$\x00\x95\xc6K\
+\xa3\xfb\x10E\xc2\x02\xd7P\x0dxe2\x88\xc2\xb7\xf8\
+\xae:\x86\x9eR\xbd\xff\xed\x88\xf1\xc9\xd9\x13g\x83\xd1\
+\xdd\xa3X\xffs\x1dZj\xf8\xc2GK\xb6\x10\xa4\xed\
+\xe8@E\x88\xb4e\xd6~\x9a\x96\x04=\xe6\x08\xfa\x04\
+K,~\x0d\x01\x02\xc2\x08\xa0\x0d\x98$\xc1\xd4\xe8C\
+\xde\x0f\xb8\xf6\x11\x80\x8bw\x00\xf0-\xdc\xca\xc7+\xbc\
+\xfa\xe3U\x9c;\x7fnC\x95\xc6\xffG\xb2ajc\
+\x98\x12\xa0\x14\x80 \xa0\xd2H\x16\xcfB\x00,\x00\xa0\
+\x04\x1c`@4\xfc\xdf\xb3\xe7[\xe8\xa9\xbe\xef\x02.\
+S\xa0\x81ahl}\xb5P\xd4\xd7\xb8\xe9z\x86\x0e\
+>L\x06\x80\x01`\xc7\x01\x7f\x03\xdc\xff\x15WD\x01\
+nd\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x02V\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\
+\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x0c\x0c\x09\
+6\x16qS[\x00\x00\x00\xb7PLTE\x00\x00\x00\
+\x14\x14\x14\x17\x17\x17YYYjjj\x86\x86\x86\xa4\
+\xc0\xe4hhh\x80\x80\x80\x1e\x1e\x1e(((xx\
+x\x87\x87\x87iiiddd\x5c\x5c\x5ckkk\
+ppp\x16\x16\x16rrr\xa8\xc5\xea\x1f\x1f\x1f\x1f\
+\x1f\x1fbbbccc\x16\x16\x16\x1f\x1f\x1f((\
+(111222999:::CCC\
+DDDLLLNNNUUUWWW\x5c\
+\x5c\x5c^^^```bbbcccdd\
+dgggiiijjjppprrr\
+wwwyyy{{{~~~\x81\x81\x81\x84\
+\x84\x84\x85\x85\x85\x88\x88\x88\xa8\xc5\xea\xa9\xc6\xec\xaa\xc8\
+\xed\xff\xff\xff\x828\xd2\xed\x00\x00\x00\x19tRNS\
+\x00\x00\x00\x00\x00\x00\x00#)?F\x81\xc0\xc6\xec\xf3\
+\xf3\xf8\xfa\xfa\xfb\xfc\xfe\xfe\xfe!\xf9 \xd7\x00\x00\x00\
+\x01bKGD<\xa7ja\xcf\x00\x00\x00\xf3ID\
+ATx\xda\x8d\x93\xd7\x12\x820\x10\x00\xcf\x8a]\xec\
+\xbdWP,\xd1\xa8\xc0\xff\xff\x97g\x1c\x1dr\xc6\xd3\
+\xcdC\x96c'3y\x08\xfcC\x1a\x17?\x0e\x80@\
+\xc7\xa19\x08#\x96\x80\x0f\x12Z\x00\xe5%\xa1\x0cz\
+PrZ\xae\xbbq\xdb\x8e\xd3\xc6\xcdm9%\x12\xd8\
+\xbb\xa2\x85\xe6!\xb8Y\xc5\x9dM\x82Z\x05\x92h\x87\
+\xfd\xfe\x00\x80Z\xa9\x91@\xe4\x95\x89\xe3Q(\xc9\x0b\
+\x12\xc8\x8c2y>K%\x19I\x82\xc2\xd3n\xd7\xeb\
+\xedi\x05=H\xe1zp\x91\xf2\xa2\x04\x07Z\xf0\xe2\
+$\xc4I\x1b\x13S\xb7`\x03o\xbb\xf5\xd8`\xbdZ\
+\xad\xd9`1\x9f/\xd8`6\x9d\xce\xd8`2\x1eO\
+\xd8`4\x1c\x8e\xd8`\xd0\xef\x0f\xb8 \xd7Cr_\
+\x828@\xb6\xd1\xecv\x9b\x8d,~\x98N\xa8v\xde\
+TM'\xc4 B,\x12\xa4\x01|0\xe2?~\x22\
+\x81_\xf7C\x038\x0e~\x9f\xc0?\xbd\x7f\x1f/\xcf\
+\x1d5T&3\xf6\xb7\xdc>\x00\x00\x00\x00IEN\
+D\xaeB`\x82\
+\x00\x00\x05\xaa\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01^\x1a\
+\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x01\x04\x03)\
+%F\xe3\xacU\x00\x00\x00\x06bKGD\x00\xff\x00\
+\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x05*IDATx\
+\xda\xbdV[PSW\x145\x80\xa5\x82h\xab\x9d\x22\
+\xa8\x88\x80\xa0b\x15e4\x80\x222E\xa7B\x00C\
+x\x04\x08\xc1\x82\x10\x08\x22\xa8\x10\xc5\xa8!\xe4!\x86\
+\x87\x04\x83omm\x95\xa8\x14\x15\xc4\xdaV\xb1\xa5U\
+\xa7:-\xa8\xa8XEZ\x94\x8e\x04\xa9|\xd8\x96\x87\
+\xc2\xea\xbe)\x9f\xfd\xcc\xed\x9eY?\xe7\xae3k\x9f\
+}\xce]{\x8fa#\x5c]gX+\x95\x85\xb1\x97\
+\xbe\xbaT\xd7x\xb5\xf1\xf1\xe5+\xdf\xb4\x9d;_[\
+{\xf0\xf0~\x81\x9d\xbd\x9d\xf5\x186c\x09w\xb1\xbd\
+\xd1X]\xdf\xdf\xdf\x8f\xc1\xa1A\x98L\xddx\xf4\xf8\
+\x17\xfcx\xf3\x06N\x9d\xae\x1e\x96d\xa4\xc9\x89\xc6Z\
+\x12V\x1br\xb2\xcb{zLx\xf5\xea\x15\x86^\x0f\
+\x99\x13PkT\xc3\x01\xfe\xfe\x9d\xd6\xd6\xd6\x95\xc4\xf1\
+exl%0\xb1j\xdf^S\xd7\xef]x\xf9\xf2\
+%\x06\x06\xfa\xf1\xb0\xed\x01h\xbd\x9c\xe0A\xb0%p\
+X+\xff\xfc\x05\xf3\xe6\xcb\xb7o\xc3\xd3g\x9dx\xd1\
+\xfb\xc2\x5c\x85\xd6\xd6\xbb\xb0\x1bg\xb7|\xcc\xff\x11\xfc\
+\xa8\xc8d\xf9\x8em\xb8~\xfd\x1a\x95\xdeDU\xf8\x03\
+\xbd\x94H^~\x9e\xe4?N\xceY\xba,\x80c\xd1\
+\xfb\xcf\xc9\xddPy\xe8\xf0\x01\x94\x94\xecF7\xdd}\
+\xcf\x8b\x1e\x98\xe8=\x5ci\xbcl*R+\xd6\x8c&\
+aUm<\x11R{\xb6\xe6k\x0f\x0fw?K&\
+\xf0\xf6^C\xe5\xad\xba\xfas\xc8\xc9\xc9\xc6\x9d\xbb\xb7\
+\xd1\xf1\xa4\x1d\xcf\xbb\x9f\xe3IG;\x0cU\x95\xd8\x9c\
+\xb7\xb1\xb6\xa9\xe9\xdb\x9b-\xcd?\xa3\xb4\xac\xa4\x9f\xf6\
+\x84XL}\xea4g\x97\x0b\x0d\xf5\x83\xdf5]\x85\
+\xa2P\x81\xd5\xbcp\x08b\xe3\xf0\xac\xeb\x999\x81\x86\
+\x8b\x17 \xdf^\x80f\x12\xbf\xff\xe0\x1e\xfc\xfc\xfcZ\
+i\x9b\x8b\xa5\xf49\xf4\xfb\xa51\xa7NHJ\x828\
+%\x15z\x83\x01\xde\x8b|QW_gN\xe0n\xeb\
+\x1d(\x14;\xd0\xd2\xd2\x0c\xadV3D{6\x13\xc6\
+ZD\xdde\xc6\xf4\xf1u\xf5\xe7o?\xed\xec\xc4\xe9\
+3g0w\x81\x0f\x8c\xa7\x8c\x08\x8d\x5c\x83\xb8\x84D\
+\xb4\xb7?6\x9f\xba\xa4T\x87|Y\xfe\x1b\x1b\x1b\x9b\
+j\xda6\xc5B\xb6\xeb:\xae\xf6\xec\x17u-\xcd\xcd\
+\xe8\xf8\xb5\x03\xddt\xe7\xfc\xe8\x18\xa4\xa6K LH\
+0W\xe0\x1e\x89\x1f=v\xe4MF\xa6\xa4\x97\xb6\xec\
+g\xb6Y\xc4\x0f\x16.\xf2\xb1\xaf\xa99\xd3`\xa82\
+ hE\x10\x96\x05\x06\x9a\x0d\xa8\xeda\x1b\xc8\xff\x87\
+\xa3\xa2\xf8}\xde\xde\xde\xad\xee\xee\xee5D\xdfD\x08\
+\x22\xbck\x91\x93\x07\x07\xaf\x98x\xb2\xfa\xc4\xe5\xad\x05\
+[\x11\x10\x10\x80P^(\xc4\xc9b\x90\x0d\x0fgJ\
+3\x7f\xe2p8\x85D[E\x98Ip\xb0\xa8\xff\x87\
+\xf1V\xbfw\xfc\xb3On\x88\xc4\x22x\xcd\xf6\x02\xd7\
+\x8f\x8b\xf0\x88\x084\xb74\x8f\x88D\x89\xccig\x13\
+\xc6\xb2b\xbbQ\x02\xfe\xd4CG\x0e\xdc\x8e\xe0G\xc0\
+q\x8a#k~/\x8c\x8fq\xd7\x1b*\x1e\x85\xac\x0c\x81\
+\xa3\x93#\x9c\x9c\x9d0m\xfa4T\xe8\xf7@&\xcb\
+\xbfF\x94\x0fX\x13\x17%%\xcc\xd3\x95\x14?]\xb9\
+j%\xdc\xdc\xdd\x18as\x05\xb2\xc9\xf5\xd4j\xd5}\
+\xa2\xf8\xb3\xd6f\xc5\xc9\xa2%*\xb5\xd2\x14\xc6\x0b\x03\
+\x97\xcb\x85\xcfB\x1fxzy\x22.>\x0ee\xe5\xba\
+.zp<\xa2\xd9\xb0\xa1\xcd\xd9\x94\xb7q\x85R\xa5\
+\xe8\x13\x08\x04\x08\x0d\x0b\x05S\xfe\x80\xa5\x01\xe0\x0b\xf8\
+(\xab(\xedsppHa\xfa<+\xe2\xfa\xca\x0a\
+\xde\xa7\xc7\x8f\xfd\x99\xbc6\x19b\xb1\x18B\xa1\x10\x11\
+\x91\x11\x10D\x0b\xa8\x99\xe8\xfevvv* \x9e=\
++\xe2\x87\x0e\x1f\x14~y\xa9a 77\x07\xb9\xb9\
+\xb9\xc8\xca\xcaBJj\x0aD\xe2$\xec\xd6i^{\
+\xce\x9e\xa5'\xde$V\xe6:\x8d\xa6h\x1d\xf5\xeb\xd7\
+2\x99\xcc\xdc\xd9\x14\x0a\x053T\x80\x0c\x06\xdab\xd5\
+\xc8\x22\xdf\x85\xa7\x98\x06\xc8\x8a\xb8D\x9a\xb6\xa9\xb4\x5c\
+7\x9c\xb5>\x0br\xb9\x1c\xda]Z\x14\xa9T`\x92\
+Qk\x8b\xc8r\x977\x8e\x1a\x0d\xc7\xe2\xe2\x19\xd2t\
+\xb9|\xfbV$$&@*\x95\xd2\x00\xb1\x19\x05\xdb\
+\x0a \xdb\x22C\xa5A\x8f\xc85\xe1-\xc4\xe32\x5c\
+\x8b\x8b\xaf\xcf\xceT\xd2kG\x9c0\x0e\xf1\xf1\xf1`\
+\x1e^\xba$\x1d\x92\x0c\x09\xaa\x8d'\xa1P\xee\xf8\x8d\
+x\x1f\xb11\xd3[\xedT\xca\xb7h\xb4E\xc3\xd1\xb1\
+\x02SJ\xea\xda\xbe}\x07\xf7!Q\x94\x88\x98\xd8\x18\
+\x9c\xac\xfe\x1c\xd4\xeb{mmm\x93\x88\xfb\x16\xc1\xb2\
+Aw\xea\xbaG_:\xe8\xe5\xe5\xc94\x91\xe5\x04\xfe\
+\xc7\xa9\xc9\x0f\xd7\xa5\xa5BG\xc3\xe5\xf7?4\xfd\xe5\
+\xe2\xe2\x92G\xebv\xac\xb8\x1c/<4\xbc\x5c_2\
+2\xda:\xad\x08c\x05\xd1\xfcbm\xb1\x9a\xa6\x1b\xe3\
+\x80\xf7<\xefbZ{\x87\xc0NL\x9e<\xc9_W\
+\xbakpW\xb1\x86\x11\xb2\x09\x0c\x0a\x1c\xaf\xd2\x146\
+Q\x02Cs\xe6xU\xd1\xda\xfb\x04Vc2\xf5\xf7\
+\xba\xbdU\x15#\x15\xfa\xb2[e{t\x1dJ\xf5\xce\
+A7\xb7\x99GG\xffu\xd6\x83CX0w\xee\x1c\
+cx$\xaf\xfd\xc3\x90\xe0\x96\x09\x13&\xec\xa45g\
+\xf37\x96\xe3\x1f\x0b\xbb\x8dhG\xcdo0\x00\x00\x00\
+\x00IEND\xaeB`\x82\
\x00\x00\x07\xe4\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -471,6 +782,124 @@ Z\x03BI\x10B\x02\x87lFH\x9a\x05,a\x81\
[y\xdc\xfb\x08\xdf\xfc\xdf\xf0\x93D\xe0\x13\xe6\xbf\x92\
r\x1f*A9\x0fr\x00\x00\x00\x00IEND\xae\
B`\x82\
+\x00\x00\x072\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\
+\x12\x00\x003T\x8bJ\x00\x00\x06\xb2IDATX\
+\xc3\xc5\x96\x7flS\xd7\x15\xc7\xcf\xbd\xef\xf9\xbdg\x1b\
+;\x89\x9d4qP\x12\x1b'\xa9P\xc6\xa0-i\x93\
+\xd0\x96\x04\xca\xa4\xb6hl\xb4)\xc9\x96\x96\xa4\xedF\
+Z\xb6I\x9b4m\x83\xb5\xfft\x15\xeb\x1f\x95\xa84\
+~\x8c\x06\x0c\x1dT\xcb\x92\xa8\xb4\xa3\x13B\xa3\x14B\
+\x9b\x8c\xa4\x81\x18\xf2\x03\x08\xc4\x8ec;?\xfc+v\
+\xec\xe7\xf7\xe3\xbe\xbb?\xa6iL\xcaJ\x1d\x9a\xee\xfb\
+\xd7\xd5=G\xe7~\xee\xb9\xf7\xe8\x1c\x80\x0c4\xe9\x9f\
+\xfc\x9f\xb6+\x83\x97a1\xc2\x99\x1cl6\x99\x9f\x8a\
+D\xc3'\xe7\xe3s\x81\xc4\x5c\x8cF\x22\xb3tf&\
+8\xe9\x9b\xf0\xb4\x99\xcd\xd9\xd5\x00\x00\xbd\xbd=\x19\x01\
+\xa0\xaf\xe2\x14\x9a\x9d]\xa3i\xeaG\xbe\xe0LQ\xd7\
+\xc7\x17\xa2F\xbd\x1ea\x8cA\xe0y\x88'\x12\x90\x98\
+O\xc0\xd6'k\xb2\x05\x9ew+\x84n|\xa4\xb22\
+\xf4\xb5\x01\xf8\x03\x81F\xa2J\xef\x1f\xf8\xd3\x99\x19\xbb\
+\xc3A\x1a\xb7n\xd2k\x1aP\xa2\x11\x0a\x80\x80e0\
+\xd24\x80\xb6\x13'\xc5\x80o\x8c\xdf\xba\xe9!\xabJ\
+QU\xdd\xfa\xba\x7f\xdc3\xc0\xb8g|C:-\x9e\
+=\xf8\xe7\xcf|?\x7f\xf5y\x1dB@\x10\xa6\x04\x01\
+\xd60P\xaa\xfd+\x00\xa2\x14a\x0a\x94I$D\xbc\
+w\xffa\xed\xbb\x8f9\x97\xcb\xaa\xe6\x8c\xc7\xe7n\xb7\
+4\xbf\x989@_\xdf%0d\x99Y\xac*\xca{\
+\xa7\x06}\xcd?|V\xd3(\x91X\x06)\x0c\x83U\
+\x86a\xb4\x5c\x0b\xab \x00\x08G\x14V\xd5(C\x88\
+\xc6\xaaD\xd3\xc9\x8a\xca\xbb\x8e\x1c\x11\x1e,\xcf\x11\x9f\
+ojv,:\x03\x03\x03_\xec\xf9bx\xe2\xd5<\
+\xfb\x03\x81\xe5\x05\x16\x89\xe7\x984\xa7cd\x1d\x87\xe4\
+\xfb,\x9cTY\xb5\xa5\x96R\x8azz>:7\x17\
+S8E\xa5\x9c$\x13N\x96\x89~\xe4\xa6W/\xcd\
+\x0c:9D\xb6\xfb\x03\x13'~\xfd\xab\xdff^\x05\
+\x84H\xbf\xb88\x9a\xf2X\xac9)Y\x83\x98\x0a8\
+\x0c\x0c\x9aF\x18M\x01@\xb0z\xc3S\x93\xd5\x1b\x9e\
+\xf41\x18\x02\x88CA\xc4\xc2\x14fp\x84\xd5\xe1T\
+Y\xa9#=\xe4M\xf9\x1fX\xb3z\xe7\x97\x1d\x0e\x00\
+\xc0.\xb4y\xa1\xfb|I|.\xc1\xf9\x12F\xffD\
+XVK\xf2\xf5\x04X\x14\xcd\xcf\xe3\x22\x06\x00\x91\x03\
+H\x9a-\xf9\xa5\x1a\x05l\x06\x183-c\x05\x11@\
+\x98\x8d\xabeH\xc5\xb9\xf3\x98\x90\xe9t\x96?\x99\x8c\
+W\xdf\xed\x09\x16\x04H\xa5R\xe5S\xa1yb\xc9+\
+L_\xf6$\xbdQ\xc2\x84\xeb\x1f7\xe6\xacY\xf5x\
+\xa3\xadhE\x5cQ$m\xf3\x96z\x07 \x80\xb5O\
+4Zu\x1c\x8f\x02\xde[\xa6\x9bC\xddg&B2\
+\x9e\x8c\xca\x883\xdb\xf8\xab\xd7F\xe0\xe5\x1f\xb5,o\
+{\xd7\xe5\xcf\x08@\x14Es*-\xcb\xa8\x02\
+\xcf)\x12F\x11\x09 \x02\xbcy*\xbbt\xddUI\
+Rd\xc1\x98e\x01\x000\xafx\xf4\x22\xc7\xe9t\xc1\
+\xe9\xd0\xaa4@\xefx\x92X\x82\x09R\x0c:}\xc1\
+\xe8\x8dQ0\x18\x8cL\xc6\x19H\xa7E?\xd6$\xbd\
+\xa6Q@\x0c\xd2t:\xa4(\x8af\xbf\xd9\x7f\xea\x14\
+\x01 \x02\x80\xb8\xf3\xf7\x1d\xd7\x11\x00|rh\xe7\x0d\
+\x09\x80g\xe0\xc7\xb7\x03\x94\xe6\xfb$\xedzR\xa2\xd9\
+:\x96\xb1\xa5S\xf3 \xab$\x9a1\x80\xc7\xe7\xbd\xe2\
+,.\x062\x95\x16\x04\x16\xf1,\x83\xb0;\xa1\xfe}\
+\x84\xc7Q\x8cP\xeaa\x03\x13\x1b\x19\x0b8(\x00h\
+\x00\x87\xcf\x8a\xc4\x04\x14\xf4\x08QS\x16\x8f\xcb)\xc7\
+\x08z%\x98\x97H\xcd\xcf\x1f9\xf2~\x22c\x80\xdf\
+\xfcrW\xfab\xf7\xa7\xfd\x8f*\xf2\xcaUN\xbda\
+\x8a\xa0Y\x16\x8032\x88e\x011\x16\x00<7\x97\
+\x94\xa9F\x01\x03\xd0\x1c\x8c\x90J\x81!\x08t\x98A\
+\xd9!I5+\xb3C\xc5\x89d\xbakQ\xcd\xe8\xc6\
+\xd0m\x18\x1a\xbd\xfe\xbb\x8d\x0f\x16\x94\xde\x0a$Q(\
+\xa2\xea\xc5\x141\xfc\xdb\xff\xafIE:\xdb\xb1\xab\xf6\
+\x93\xae\xdd\xb5\xaf\xc7\xe2I\x8c\x90\x8a\x80\x223\x80\x95\
+W\xc1\x11\x9a\x9d\xb3\xce\x07\x07\x8a\x19\xcc\xec\xae\xdf\xf6\
+L\xe6UP^\xb1\x02\x00\xe0\xc3\xe3'\x8e\xf6y\xfc\
+j\xf9|\xe1\xe6a$0\xb6\xec\x1c]\x8ce@\xca\
+g\x19\xf3_\x80V\xee@X\xb9J\xe82J4^\
+\xcf\x22;\x11\xb5\xb5\x03\x93j^\xe4\xb2\xeb\x89dR\
+<\xd8\xd5y\xd2\xbf\xe8v\xfc\xda\xeb\xbba\xcf\x9b{\
+j\xc4\xe9+\x86\xe4\x15\xd7f\x8fW\xbe\xdf\x7fSZ\
+\xc9Rd3`\xc8\xaf\xd3\x90s\x9aR{>\x86\x95\
+6\x1d^\xc7(\xa8\xbaw(]\xe2=\x7f\xe0{\x13\
+\xb7\xaf\x8dwu\x9e|\xe5n\xb7\xbfk3\xfa\xe9\xcf\
+v\x82\xacH\xcbTY\xeeU\xb1\xa1tY\xd9\xd3\xe7\
+\xcaj\x9e\xee+/F\xd3\x85VH\x09,\xb0\xa2\x04\
+&\xef$\xb1\x05G\x06\x1e\xb9v\xfe\xd8\xbaI\x9f\xb7\
+\xf7\xc3\x0fN\xad\xab\xdf\xf6\x0ct\xb4w\xdd{;n\
+yi;\xb8\x0e\x1f\x83\xc6\xa6\x86\x9f\x109\xfd\x16\xcb\
+\x1b\x0d\xc6\x5c\xe7\xa4\xc1l\x8ds\x02G\xe5\xf8l\x9e\
+\x12\xf7\xdf\xe7\xf3y&^x\xe1\xa5\xe2p8\x1ck\
+\xdd\xf1JNC\xd3s&\x22)\x89\x8e\x8e\x0f\xee}\
+ \xb9S\xdb\x1a\x9e]M4\xb2\x89j4\x17\x00x\
+\x84\xd1\x08B\xf8BG{\xd7\xe8\xa1w\x0f\x1d\xad\xad\
+\xad\xdd\xf2\xd9\xc5\xee\xec\x9e\xde\x9e\xf6C\x7flk\x80\
+\xa5\xd0\x9do{\xe7\xba\xbd\xbd}\x98RJ\xcf}z\
+N{s\xcf\x1b\xf4\xfb\xf5[*\x1a~\xf0\xdc\x97\xc6\
+b\x16\x030<4\xb2\xe0\xdaawT8K\x9d\x95\
+\x85\xcb\x0bQ2\x99\xa2\xe1Pd\xa3\xeb\xf0\xd1}_\
+;\xc0\x82\x7f\xa5\xa5\x05\xf6\xed\xdf\xf7\xb1\xc3\xe9x\xad\
+n}-\x12E\x11I\xe9tnn\xae\xe5\x96{\xf0\
+\xaa\xfb\x9e\xa6\xe2\xaf\x22\x97\xcb\x05\x00\x00c\xd7\xc7Z\
+/]\xeaC\xe5\xe5\xe5`\xb7\xdbi\x81\xcd\xd6\x06\x00\
+\xd0\xdc\xdc\xbc\xb4\x19\xf8\xcf8\xd77PT\x5c\xb4\xbd\
+\xaa\xaa*\x9ba0Rd\x85\xb5Zs\xcc.\xd7\xd1\
+3\xdf\x08@kk+LOOws\x1c\xb7c\xed\
+C\x95\x90H\xc4!\x16\x8bU\x1bM\x86\x83\xabW\x7f\
+;9<<\xb2\xb4\x00\xfd\xfd\xfd\xe0v\xbb\x83\x0e\x87\
+cM\xc5\xb7*\xee7\x99\xccHUU\x1a\x8dF+\
+\xdf;v\xfc\xd8\x92g\x00\x00\xa0\xa9\xa9\x09\x06\x07\x07\
+\xfff0\x1avU\xd7TC*\x99Bb*e\xcf\
+\xb1fu?\xf6p\xcdx\xff\xe5\x81\xa5\x05p\xbb\xdd\
+\xe0\xf3\xf9\xe4\xb2\xd22\xad\xa8\xa8\xa8\xae\xb0\xd0Fe\
+YF\xe1px\xfd\x1f\xf6\x1fxgI\xaa`!\xed\
+\xdd\xbb\xf7\x8d\xd3\xa7O\xcf\x09\x82^.(\xc8\x97K\
+J\xec\xf6%+\xc3\x85\xd4\xd9\xd9\x09\x1e\x8f\xe7;\xe1\
+p\x98\xe7y\x81\xb3\xe6Z\x8f\xc3\xffC\x94*\xc2\xe7\
+=\x9f\xdb\x01\x00\xde~\xe7\xed\xff\xb2\xfd\x13\x9dV-\
+8\xcd\xc8:\x1c\x00\x00\x00\x00IEND\xaeB`\
+\x82\
\x00\x00\x03|\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -712,6 +1141,189 @@ $&\x93\xc8\x7f\xb3\xe7\xcc*\xc2w\xf1nHG\x09\
\xf9\x5c\x8a\xda\xed\x0c\xf6t\xf6\xe1\xf9-'\x97\x16\xdc\
5\x03\xef$\x18\xdea\xfc\x07;\x00\xdf\x09\xb1\xdb|\
\x84\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x04+\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x06\x0f\
+\x0b\x14\x19hV\x92\xc5\x00\x00\x03\xabIDATx\
+\xda\xdd\xd6\x7fh\x1be\x1c\xc7\xf1\xf7swI\x97\xad\
+R\xa7m\xd1:+\xa8\x88\xdd\xd4\xfa\x8f\xf8\x8f\x7f\xcc\
+\xb9\xb1iQ\xd9(\x03i\xad\x93n\xb8\x04\xb5\xba*\
+\x95m\xdav\xf4\x0f\x8b\xb2\xea\x84u\xd0uk\xa4A\
+\x18\x81\xfac\x82\x136\x1d:'\xfe\xa3\xe0\x0c\x22\xae\
+*\xb2V\xd6\x96\xb4k\x97\xa4w\x97\xe71G\x1a9\
+z\xacK\x13V\xd1\x17<\xf0\xf0\xe4\x8e\xef\xe7\x9eo\
+\xee\x07\xffI\xa1H\x02\xb7W\xa2VY\xeb1k\xa5\
+\xe7\x98<\x08\x80?\x80\xdb\x80ys\x8f\xe6\x81\x04\xfd\
+\xdb\x96\x13\x1a4\xab\x15\xb4\xeb\xba~\xb7\x10L*\xa4\
+s\x9a)\x84v\x83B\x5c/\xd3i\xa9$C\xbd\x8d\
+\xbe0W!\x86\x80\xd5P*a\x00@\x83m1\x98\
+\xd9\x0c\xb85\xf6]f\xcbv\xc9'\x03\xbe!]'\
+%\xd0v\xf55\xf9G\xb9\x82\x1d\x83\xd6:\xd2\xf2%\
+[h\xdf\x1dm\xf2u9\xe7\x0f\xeeX\xc1|\x9aS\
+\xe8\x07\x18\xa8\x81\xfa\xda\xcc\xf8\x1e\xc2\xf3\x8b\xd7\xf7\xce\
+ \x85aD\xfb\xc4ov\x9a\xbd\x87\x9b\x96=e\xa7\
+\xadQ\x16`[\xe6\xa9\xbegJ\x9eH\xdb\xea\xc3\x86\
+\xc3\xa9\x93R\xf8n\x06/\x0d`\x14\x18\x00Z\xa8\x16\
+\x93\x90b\x9eh\xb0\x94Y+}\xcaT\xc6z[\xa6\
+\x7f\x028\xfa\xec\x0a\x16\x92\xfb\xdd\xb6\xads\x91\xed\xcb\
+\x1e\x11\xbaoopH=\xc8\x1c\xff\xc6\x08\x0e\xd1\x93\
+MQ\xaa \xec\x03k7m\xf1)\xba\x83\xb8<\xfe\
+\xdel\x8dB\xee>\xfeB\xe0i\x0a\xa4\xad\x0f_@\
+Q\xa5\x1b\x06\xc0\x88u\xa2\xe1\x16rz\x007?\x9d\
+Ff\xdc\xe1\xe7\x0d\x1c\x1b\xf6':6\xec\xbf\xbc\xb6\
+\xee\xdd\x19\x0ae\xac=\xa0<\xf3\x5c\x0b^\x06\xdcL\
+\xdamP\xed\xa0\xd5\x00\xcc\xa6\xa9L\x9a\xf2\xe2\xa7-\
+\xa5\x14,1F\x8er\xcd5\xf0r\xae\xdc\xa4\xa3\x09\
+D+\xb4\x96\xa4,y\xc9\xb4\xb5\xf2\xda\x8eK\x14J\
+\xa6\xc6G\xf4\xfbB\xca\x19*91\x86\x977D\xce\
+\xadu_=_\xbb/\xf1\x0e\x05\xd2\xefi\xf6\xae\xad\
+i&o:m]\xd5\x8d1\x05^gX2o\xee\
+9Vv\xbb\xfa\x1a\xfe\x19g\xe0{\
+w\xdd#.[@p\xdb\x16\x00A\x92\x19\x09\x1bh\
+>rJ\x0b\x8d\xdfT\x9f\xfcq\xb5\xd7\x16\xd8\xb4\xed\
+\x07\xb5\x97\xee\x19\xc0?\xea\xff\xa1\xa6\xa5\xfe\xfe\xd6\x9f\
+\xba\xc7_\xda\xb7\xc7A\x04\x9b\x98\xb0\x09\x8c3\x12B\
+\x00\x80\x00\x09A\x0c\x10R,\xae\xb1\xdf\x1fl\xe1\xdb\
+\x1f,[mZ|\xed\xdc\xdc\xdc\xed\xc6\x86g\x17\x0f\
+\xd0\xd3\xf3)<\xd9Y23\x0d\xb3\xfd\xf4\x95\xf1\xc6\
+\xfa\xa78\xe7\xb6.KdJ\x12\xb3\x18#\xce\x18\x09\
+\x02\xc0\x05\xc0\xb9\x90l\x9b\xcb\x96\xcd\x1d\x86i\xa9\xad\
+\xef\xb6:\x1f\xb8/G\xdbS\xdfP\xbad\x07\xfa\xfb\
+\xfb^\xef\x1d\x1a\xfdI^\xc9\xfd\x13\xab\x0bV\xea\xaa\
+\x22\xa5\x15\x87d8\x142dF\x16c\xc4\x01@\x08\
+0\xdb\x16\x92i\x09E7l\xc50l\xd7\xc8\x8dQ\
+\x97~\xf7\xcaZ\x95\xd9{\x03\xc1\xb1\xa3/\xff\xf2W\
+\x8bO\x81m\xe9/\x9d\xbd\x9e\xf2\xaf\xcc\xcdI\x19\x1c\
+\xb3\x16X\x04\x12M1\x89&\xc9A!r\xd0\x04S\
+hBRh\x82\x14\x0aA\xc6$I,*;X\xaa\
+\xa2\xbc4=\xe4O\x06\xef\xaf\xaa\xda\xf7e\x8f\x03\x80\
+\xbc\xd0\xe6\xb9\xeeOJb\xb1\xb82\x1e_\x11\x1c\x8b\
+\x18V\xc9*\x97\x0d\x99f\xf2\xf3\x94\xa8\x1b\xd0\x1c\x80\
+\x0e\xc0\x9e\xe7\xa2C\x03\x9cw\xe3V9\x99,7\xa1\
+\xd9\xf6d:;\x98H\xc66\x7f\xd5\x17,\x08\x90L\
+\xa6*B\xe1\xb8\xed\xcd-L\xf7\x8f&Gg\xb8\x1c\
+\xad\xdb\xea\xf1\xac\xab\xdc\xba\xa7\xa0xM\x0c\x82C@\
+\x08\x08\x02\x11\x08\xc4Dp\xecf\xe6\xad\xa1\xee\x0f\xc7\
+\x22\x86\x14\x981\xa1d\x15\xa8W\xaf\x0d\xe3\xf9\x17\x9e\
+]\xfdNskpQ\x00\x9a\x96\xcaLi\x86\xa1\xaa\
+N\xcb\xa9(\xa6N\x88j\xc0$9\xb3&s\xca\xb7\
+\x5c\xe3\x5cp\x02 \xc4\xbf\x93\xc0\x98\x84\x89\xc9\xf0w\
+\x92\xc0\x85;\x09{U(n\x15\x93\xc3\x9d?|}\
+\x04.\x97GZ\xb4\x03\x9a\x96\x0e2\xae\xbbl!@\
+\x12qE!\xc3\xd4y\xfe\x8d\xbe\xd3\x7f\xb6\x00K\x02\
+,\xfa\xfc\x0b\xc0\x01I\xc6\xf3\x83cBx\x03:\xbf\
+\x9d\xd0E\xb6C\x96\x0a\x0c-\x09\xdd\xb2g\x16\x0d0\
+\x1a\xf0\x0f\xac)*\x86\x1d\xd2\x9cN\x99T\xc6\x88\x0d\
+'\xad\xee\x9b\x16\x8bJD\x1a\x114\x0629\x04g\
+ \xc6!\x1c\x5c\xc0E$22\x9d\xac\x9c\xab\x92\xd3\
+i\x86\xf2\x12\xa9D\xa2\xb5\xf5\xbd\xf8\xa2\x01^\xfe\xc5\
+\xfe\xf4\xb9s]\xbd[M\xf3\xdb\xeb\xca\xdc\xee\x90\x85\
+i\x09\xf0\xbb%\x92e\x10\x93\x09$\x138\xc08 \
+`\x09\x82%\xc0l\x82L\x8ce\x87u+\xd3\x9c\xbe\
+V\x1cO\xa6O.\xa9\x19}6t\x1bC#\xff\xfc\
+\xcd\xc3\x0f\xe4\x97\xdd\x0c$(\x1c\xb5\x5c\xe9\x94\xed\xfe\
+\xcf}\x02l\x222%\x12iFH3\x22\x8b (\
+\x13\xf0\xaa\x96(\x0d\xdf\x9d\xf3&C}\xc5\x92$\xbd\
+\xb2cg\xdd\xe2SPQ\xb9\x06\x00>8z\xb4\xa3\
+\xc7\x1f4+\x12\x85\x8f\x0f\x93K*\xc8\xceq\xccJ\
+\x0c:\x88\x18Ap\x99X\x9a\x0b\xa1\xc8$T\xafL\
+>[\xe3\xd5\x97\x03v^\xb4\xbf\xed\xe1d2}\xf8\
+d\xe7\xa9\xe0W9\xf0\x85\x15\xfa\xea\xaf_A\xf3\xe1\
+\xb7\xdb|\x85\x9e\xfd\x89\xa9[%i\xcf\xfa\x88\x93\x88\
+V\xe59,\x8f\x04\xc9\x0drx\x19\xdc\x19\x0c\xbe,\
+\x99}\xd7\xd4\xa9\xaa\xfb\xaaVt\xe7\xe3CO\xdc\xbe\
+\xde7\xd6y\xe2\xd4c;v\xd6axhd\xe9\xcd\
+\xe8g?\xdf\x07\xc3\xd2WX\xbaq\xd1b\xae\xb2\x15\
+\xe5\x8f\x7f\x5c^\xf3XOE1M\x15z\x91r\xca\
+\x90\xd3:2\xfcA^01\xdc\xf7\xbd\xa1\xae\xf6-\
+\x81\xc0\xd8\xc5\x0f\xde?\xbde\xc7\xce:\x9c8v\xf2\
+\xde\xdbq\xe3s{\xd1\xf6n\x07v\xd7\xef\xfa\xa9e\
+h\xbf\x93U\x8f\xdb\xe3]\x1bpg\xe5\xc6\x14U\x11\
+F|:\xcf\x9c\x0b|k,0:\xd6\xb0\xf7\xb9\xe2\
+H$:\xfb\xe2\x0bM9\xbb\xea\x9f\xce\xb0u3~\
+\xe2\xc4\xfb\xf7>\x90\xcc\xd7\xceg\x9eZo\xdb\xfcG\
+B\xf0\x5c\x08\xa8\xc4h\x84\x11\xfb\xe4\xf8\xb1\x93\xd7\x9b\
+\xdfin\xaf\xad\xad\xdd\xde}\xeel\xf6\xf9\x0b\xe7\x8f\
+5\xbf\xdd\xb2\x0b\xcb\xa1\xf9\x95=\x7f}\xfc\xf8\xf1a\
+!\x84\xe8\xfaG\x17\xff\xed\xeb\xaf\x89'wl\xaf\xdc\
+\xb5\xfb\xe9\xa5\x15\xe1\x97i~a\xcd_\xfbJ|\x95\
+e\xe5e\x1b\x0aW\x17R\x22\x99\x14\x91H\xe4\xa1\xb6\
+\x96\xf6\x83\xdf8\xc0\x82\xb5\xd2\xd8\x88\x83\x87\x0e~X\
+\xba\xa6\xf4\xd5\xdam\xdb(\x95\xd2HO\xa7sss\
+W\xde\x1a\xbcru\xf0\x9e\xa6\xe2\xaf\xa3\xb6\xb66\x00\
+\xc0\x8d\xcfn4]\xba\xf4)UTT\xc0\xe7\xf3\x89\
+\x82\x82\xfc\x16\x00hhhX^\x07>\x1f\xe7z.\
+\x17\x15\x17\xed\xdd\xb4iS\xb6$12\x0cS\xf6z\
+s2\xdb\xda\xda\xff\xf6?\x01hjj\xc2\xd4\xd4\xd4\
+YEU^\xdcP\xbd\x11s\xb1\x18\xe6\xe6f7{\
+2\xdc\x87\xd7\xaf_\x97\x1c\x1e\x1eY^\x80\xde\xde^\
+\x0c\x0e\x0e\x86J}\xa5U\x95\x95\x95\xf7edf\x92\
+eY\x22\x1a\x9d\xd9\xf0\xc7\x8e#\x1d\xcb\xee\x00\x00\xd4\
+\xd7\xd7c``\xe0#\xb7\xc7\xbd\xbf\xa6\xa6\x06\xc9d\
+\x8a4-\xe5\xcb\xf1f\x9f\xdd\xba\xb1\xe6No\xff\xe5\
+\xe5\x05\x18\x1c\x1cD \x100\xca\xcb\xcayQQQ\
+maa\x810\x0c\x83\x22\x91\xc8\x83\x7f8\xf4\xd6\x9b\
+\xcb\x92\x82\x85t\xe0\xc0\x81\xd7\xce\x9c93\xe7r\xb9\
+\x8d\xfc\xfc|\xa3\xa4\xc4\xe7[\xb6\x18.\xa4\xce\xceN\
+\xf8\xfd\xfeG\xc2\xe1\xb0\xaa\xaa\xaa\x92\x9b\x97{\x04\xff\
+\x0f\x09a:\xcf_8\xef\x03\x807\xde|\xe3\xbf\xce\
+\xfe\x05\xad\x92\x16\xfc$\x148\xf4\x00\x00\x00\x00IE\
+ND\xaeB`\x82\
\x00\x00\x07\x06\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -917,6 +1529,135 @@ x\x9e\xc7\xe0\xe0\xd0o\x0b\xf9\xfc\x0f\xc0V\x1dC\xdd\
\xdd\xdd\xdc\xbf\x7f\xffM\xff)\xfb\x80\xf7\xb6\xf1w]\
\x00\x16\x81'@b\x8a\xfd\x0fg\xea\x16`\x16W\xbd\
\x9b\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x03\xdf\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
+\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
+\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\
+B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x1a\
+\x0d)\x10%H\x90p\x00\x00\x03_IDATx\
+\xda\xbd\x97;H\x9bQ\x14\xc7\xcf\x97\xe7\x97V\x17k\
+\x07GG\x87n\x9d\xc5\xc1R*\x08\xf5\x91\xf8\xea\xa0\
+\x85B'\xa1\x94\x0eB!CP\xb4\x93\x9d\x0aBp\
+((\x99*\xe9`\xd3\xe2\xd4V\x1cB\x16c!C\
+\x03E\xa8\x06\xd4\xa8\xa4I\xcc\xeb\xf6\xfc\xc3\xbdp\x09\
+\xd2\x9a|I\x0e\x1c\xee\xcdw!\xe7w\xff\xe7\xf1%\
+\xf4?\x9b\x9a\x9a\xaa\xd97\xd7\x0c\xba\x81\xcd\xcc\xcc|\
+-\x14\x0a\xddN\xa7\xf3\x9e\x10\xa2\xb4\xb9\xb9Im\xb5\
+\x91\x91\x11\xb1\xb3\xb3S\xe15\xa9\x94h\xab\x0d\x0f\x0f\
+\x0b\xd8\xc6\xc6Fell\xecG\xdb!\x00\xa0lm\
+mM\x8c\x8f\x8f\xc7$D{\x01\x0e\x0e\x0e\x04lu\
+uUx\xbd\xdeo\x12\xa2}\x00\xd1hT\xc4\xe3q\
+\x01[ZZ\x12>\x9f\xef\xb3U\x08\x1b\xd5a\xe5r\
+\x99\xb2\xd9,1\x04-,,P__\xdf\x03\x86\xf8\
+\x80\xae\x00D\xcb\x00\x0c\xc3 \x18\xb7\x22\x95J%\xca\
+d2\xb4\xbf\xbfO~\xbf\x9fz{{\x1fOLL\
+\xbc\x97\x10\xadS\xa0R\xa9\x10+\x0f\xaf\xee///\
+\xab\x10\x81@\x80zzz\x9eLNN\xbe\x93\x10\xad\
+\x01@`\x97\xcbE\x0e\x87\x83\xecv;\xd9l\xb6*\
+D\x22\x91 \xae\x07\xea\xea\xeaz\xce\xc1\xdf\x00\xa2e\
+\x0a 8OC8\xf6U???\xa7d2I\xcb\
+\xcb\xcb\xd4\xd1\xd1\xf1jzz\xfau3\x01\x0c\xd2L\
+\x01\xe8+<\x9dN\xd3\xe1\xe1!\xad\xac\xac\x90\xdb\xed\
+\x0e0\xc4|KR\xa0\x07\xd7\x1d\xcf\xce\xce\xce(\x95\
+JA\x09\xc1\x9f\xdf2\xc4\xd3f\x03 \xf7\xfa\xcdu\
+%p\x06\x08\xa4\xc4X\x5c\x5c\x14\xdc9A\x86\xf05\
+s\x0e\x5c\xab\x80\x0e\x01H@\x5c]]\x19\xdc\x1d\x82\
+\xeb&\xc4o\xd2\x87Vk@W\xe0\xda\xdb+\xc7\xb9\
+\x820M\xd3\x98\x9d\x9d\x15<7\xfc\xf4\x0fs\xd4Q\
+\x84\x0a@u\x85\x02\xc3^\x9d\xe1\xf6\x08\x8e\x89)\xd6\
+\xd7\xd7\x0d>\x9b\xc7s\x06\xa9\x1f\xa0\x16B\xdd\x12\x93\
+Q\x01(\x18\x8ciU#\x9d\x9d\x9d\x98\x0dF.\x97\
+{\x19\x0e\x87\xe3R\xe9J\xa3\x0a\xd84\x05\x94\xeb\x0a\
+ 8\xe0\xb0b Q0\x18\xa4\x93\x93\x93\x8f\x1c\x1c\
+S\xc9\xc3^j\x14\x00fS\x81ps%5\x00t\
+\xf9\xe1\x90>\x16\x8b\x09\x1e\xd1?\xb7\xb6\xb6^\x10\x91\
+\xc9\x9e\xb7Z\x03\x06\x02{<\x1e\x8c\xe2j e\xc8\
+7\x00\x90_\x8cf\xd8\xd1\xd1\x91\xc1/\xad0\x04c\
+\xcf\xb2\x17p\xfbF\x00\x0c\xe9&[ttt\xf4>\
+n\xac\xacX,\xd2\xdc\xdc\x9c\x18\x1a\x1aB@\x00T\
+\xfd\xe2\xe2\x02g\xbfe\xf0?\xe0\x04\x80\x15\x05n\x85\
+B!/RQ\xd3\x15\xb7\xb9\xe0\xbe\xf0zwpp\
+\x10\x10\x90\x0a\x83\x08\x00\xbfd\xf0\x1c{\xb9\x19m\xe8\
+fw\xd5>\xdf\xde\xde~\xc4\xcb'NCw\x7f\x7f\
+\xbf\x01\x05\xf0[!\x9f\xcf\x03 /\x8bOX\x01\x10\
+R\xbe\x92J\x89\x9e\x1e\xae\x01\x11\x89D\x00\x119>\
+>\xbe\xc3oET\x7fjoo/\xa1\x82[\xfdc\
+\x02@\x0f\xd2\x80\xbd\xde\x9aR\x11'\x9es\xdf\x9b\x03\
+\x03\x03\xcfNOO\xbf\xef\xee\xeeF\xa4\xf4\x19\xc8o\
+\x15\x00\xe7v\xcd\x95ao\xa2\x0e\x00\xa8\xc1\x95e\xe0\
+4\x8aP\x01XMA\xe9\x9a/\xb2\xd7HlJU\
+\x8a\xd2\xcb\xea\xcc*\x80\x0e\xa2\x9b\x9alBzY\x03\
+\xc8\xc9\xb5b\xf9\xcf\xe9\x0d\xa7\xa4\x13\xae\xa9RP\xea\
+\xb4\x1e@/J\xbdk\xea\xb0\xbf\xf4\xf6\xd5\xe4\xcfb\
+6\xed\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x03\xee\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\
+\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\
+\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\
+\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\
+tware\x00www.inksca\
+pe.org\x9b\xee<\x1a\x00\x00\x03kID\
+ATx\xda\xed\x97\xddk\x1cU\x18\xc6\x9fs\xce\xee\
+&iS\x13\x0b)E\xaf\xea\x85x\xe1\x95\x1f\xf4O\
+\x10\x04\xb1H\xdb\x9b\x22\x94\xa2\xe8\x7f\xe0\x85\xa8\xb5\x94\
+6\xd4B\xafl\xf0\xda\x1bI\xfaA\x1b\xa1\x1f\xb4\x08\
+\xc5\x8f@\xb1Y\x08\xb1\x1f\x9a\x94\x18\xd2&M\xb2;\
+\x99\x9d33g\xce9\xf3z2g\xb1[\xa3\xa5f\
+I\xbdp\xf7\xe5\x07\xef\x0c\xbb\xf3\
+4\xf4\xcd\x10\xe5Y\x0e\xce9\x04\x13\x10\x5c\x80\xaf\x16\
+w4K\xb8\x02\x01\xc8\x1d\xd6a\x1c\xda\x91:d\x13\
+\xe5\xcf\xf5\xe6\xbd\xd0\x01Q*\x93\x13\xf4\xa99\xf5\xd8\
+\xb7\x80\xbe\xa5\xa3\xb3\x8b\xb3#\xc3\xe7\x87QF\xd9\x8b\
+2O\xab\xb8\xc8\x05\x98e^8+\xf0\xe2q\x8b\x81\
+\x04(get\xd7z\x11\xca\x95+\xf4\x89\xfe\xe8\xc9\
+^\xc3\xdf\xf1\xee\xf8\x9d\xf1\xea\xb5\x1f\xae\xf9\xbb\xf7\x06\
+\xbc0\xb9c\xe2\x05,g\x8f\x8a'M\x03\x91C\xfa\
+~\xeb\xca\x00\x96j\xcbS\x183o=\xf1<@U\
+\xca\xd0\x85]\x97\x7f\xba\xbcp\xf7\xb7\xbb^\x9c\x0ay\
+p\xf2=\xb7\x1c\x5cs@\xfdE\x5c>4\xb0Mn\
+\xc7\x83\xf9E\x17\x18\xfd\x06\x9d'\xf5\xaf&\x22\x1a\xa1\
+\x19\x10\xf6\x9f\xb9t&\x0d\x97C?\xf8\xd4\xc4zH\
+S1\xc4HZ\x0dx\xfa\x92~\x04\xf7BMA~\
+\x80\x8e\xd3\xd4:fB\x1fJ\xad\xf5\xe7\xa3\x97F\xc9\
+*\x0b\x9es\x87\x17\x17F\x80\x14\x81+\xbeF\xbc'\
+\xed\x81Y\x22\xca\xc2\xf4\x0b:f\xce\xb65\x15;\x13\
+\x83\xb5\xb06r\xf5\xfb\xab\xab\xe2E\xf8\x84\x15\xe0\x86\
+\x83e\xcc\x1bh\x11/\xc5%Td\x0f\xe4Jt\x81\
+\x06\xed\xc7\xed\xaf\x05\xcdP\xce\xce\xcdV\xab\x13Up\
+\xcb=\xdaC1y\x03M\x13[\xa8\x0f+\x8d\xf06\
+&\xed;\xed/F\xad\xa1,c\xd7\xe4\xf4\xe4\xc2\xdc\
+\xfd9p\xc3=\x9a\xc3J\xfb\xa7x_\xb9\x1f\xf5(\
+\xa8!0o\xfa\xd0\xb5g`m(5\xf6\xff|\xfb\
+F*\x1b\xb2x\x0cB\x0b\xd8\xc8\x16\xe2\x9b\xf8fH\
+\x9dd\x08p\x80\xbe\xa4\xe9\x0dY\x8e\xe9,]\xb4\xca\
+\x1c\xba15N\xa4\xa8\xc8\x00E\x84J^\x81\xed&\
+2\xa1\x1e\xa4!snC\xf7\x034LGc\x19\x9f\
+\xbey\xffV1\x02%U\x82x\xb6\x0c%\x93Q:\
+i?{:\x1b\x92[\xb4\xaf\x16\xd5&\x22\x92x\xe5\
+\xb5Wa\xc9\xde\xc4\x8f\xb4\xe7)\xed\x88|(\xbbx\
+\xe5\xed@\x073\xcfm\x7f~a\xe7\x8e\xd7w\x17A\
+]\xd7\xc5\x88\xd6\xcd\xc1\xe1\x83\xa5\xb1;c=\xed\x5c\
+\xa3\xf3\xcf\xa8c\xa0c\xe0\x0f\xf9CYQ\xbdDt\
+g\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x04\xef\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -998,6 +1739,126 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\
\x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\
\xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\
\xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x02\x92\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\
+\x00\x00\x00\x03sBIT\x08\x08\x08\xdb\xe1O\xe0\x00\
+\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01\
+}\xd5\x82\xcc\x00\x00\x00\x19tEXtSoft\
+ware\x00www.inkscap\
+e.org\x9b\xee<\x1a\x00\x00\x00\xd8PLT\
+E\xff\xff\xff\xb9\xb9\xb9\xb9\xb9\xd1\xaa\xaa\xaa\x9f\x9f\x9f\
+\x9f\x9f\xa7\xa7\xa7\xa7\xa7\xa7\xaf\xaf\xaf\xaf\xaf\xaf\xb7\xb7\
+\xb7\xb7\xb7\xb7\xbf\xbf\xbf\xbf\xc2\xc2\xc6\xc2\xc2\xc6\xae\xae\
+\xb1\xae\xae\xb1\x9c\x9c\xa1\x9d\x9d\xa2\x9e\x9e\xa2\x9f\x9f\xa4\
+\xa0\xa0\xa5\xa1\xa1\xa5\xa2\xa2\xa6\xa3\xa3\xa8\xa4\xa4\xa8\xa5\
+\xa5\xa9\xa6\xa6\xab\xa7\xa7\xaa\xa8\xa8\xac\xa9\xa9\xae\xaa\xaa\
+\xad\xab\xab\xae\xac\xac\xb1\xad\xad\xaf\xae\xae\xb1\xb0\xb0\xb4\
+\xb3\xb3\xb7\xb6\xb6\xba\xb9\xb9\xbd\xbc\xbc\xc0\xbf\xbf\xc3\xc2\
+\xc2\xc6\xe4\xe4\xe4\xe5\xe5\xe5\xe6\xe6\xe6\xe7\xe7\xe7\xe8\xe8\
+\xe8\xe9\xe9\xe9\xea\xea\xea\xeb\xeb\xeb\xec\xec\xec\xec\xec\xed\
+\xed\xed\xed\xee\xee\xee\xef\xef\xef\xf0\xf0\xf0\xf1\xf1\xf1\xf2\
+\xf2\xf2\xf3\xf3\xf3\xf4\xf4\xf4\xf5\xf5\xf5\xf6\xf6\xf6\xf7\xf7\
+\xf7\xf8\xf8\xf8\xf9\xf9\xf9\xfa\xfa\xfa\xfb\xfb\xfb\xfc\xfc\xfc\
+\xfd\xfd\xfd\xfe\xfe\xfe\xff\xff\xff\xf1_M\x9b\x00\x00\x00\
+\x11tRNS\x00\x0b\x0b\x0f \
+ \xc0\xd4\xd5\xe5\xd1\x9dOM\x00\x00\x01\x0fID\
+ATx\xda\xad\xd3Kn\x021\x10\x04\xd0*\x87|\
+\x84\xb8\x01\xdc\xffJ\x1c \xebd\xc3\x06&\xd8\x15\xe8\
+.\xcb\xf2((\x8b\xc4\xb3\x98\x8f\x9f\xba\xbbf4\x1b\
+\xfc\xb2\xfe\x05\x94\xdd\xe1\xc1\xe6\xfb)\xc0v\xbf\xdd@\
+q\xb4<\xeb\x95\x04\xb1\x1c\x8e\x01\xa0\x97\x86\x5c\xa5Q\
+\x14\x9f\x19w\x8b\x10@\x22\x95\x87\xdb\x16\x10Pm\x1a\
+\x00\x16\xf7\x12\x85\xc1x\xc1\x00\xa5\x0d\xf1\x14\xfd\xc1\xab\
+h\x00\xc5\x93.\x18\xd7\xd0B\x18\xa8\x06\xb0\x00\xc3\xb3\
+\x01\xa83\xc8\x11mt\xe5\x04J\x03\xb2\x85\x92\xaa\xcd\
+\x00\xc4\x0d\xb9\x09\x90\xedi\x00\x03\x10}_\xacw\xbb\
+\xaa\xe0(\xd9+\xd5\x88\x99C\x8c(\x0a%\x83xu\
+FJ\xaf\xf8*%\x01i\x00\xba\xbc\x13\x83\xbd\x02h\
+\x90Q\x80\xfeU\x0c\x08\xd0C8!S\xb8B\xb4\xe2\
+\xa82\xc4\xa8\x00\x80s\x94\x10c\x06\xc0\xbb=\x8aE\
+O\x11`\x9at\x95b\x0d\x5c\x80\xd3\x0c\x98\xa3\xa4\xe8\
+)\xceo]\x8cIE-\xae\xf0\xf5I\xfc\xbc>\x12\
+\xd4\xd3\xf9\x01\xb8$@\xad\x7f\xf9y\xbf\x01\x17\xfa\xb0\
+d\xb4\x11\xf2\x0d\x00\x00\x00\x00IEND\xaeB`\
+\x82\
+\x00\x00\x04\x99\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00 \x00\x00\x00 \x08\x04\x00\x00\x00\xd9s\xb2\x7f\
+\x00\x00\x00\x02sBIT\x08\x08U\xecF\x04\x00\x00\
+\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01}\
+\xd5\x82\xcc\x00\x00\x00\x19tEXtSoftw\
+are\x00www.inkscape\
+.org\x9b\xee<\x1a\x00\x00\x04\x18IDAT\
+x\xda}\x95\xcdk\x9cU\x14\xc6\x7f\xf7}\xa73\x93\
+I\xa6Mk\x91\x0cL[\x15\xa5\x0d\xe2B\xa1F+\
+\xd5\xd2fU4-\x08~P\x5c\x89\x7f\x80\xba\x90J\
+Hi\x08\x16\xc16(\xb8\xb1K\x13M[\x14\xdbE\
+\x11jk\xedB\x8d\x11D\xa4D\xa4\xc5E\x8b\x13\x89\
+\xd46\x1f3\xf3N\xe6\xbd\xd7\x87\xc30C\x88\xe4\xfe\
+\xb8$/\x9c\xe7\xb9\xe7$\xe7\x9e\xeb\x02k\xd7\x89\x9c\
+?\xe0\x86\xe8\xa7$\xa0\x22f\xc3\x85\xe8\xf2\xd1\x845\
+k\x8d\xc1X)\x8cp\xc4\x17\x03\x9e@+\x88\x08\xed\
+E&\xdd\xe8pe\x1d\x83\xd1|\x18\xe6M_h\xb2\
+BB\x83\x14\x0f\x12\xc7d\xc9\xb1\x81\x0cQ\x95q7\
+6R\xff_\x83c}\xe1\xab0\x90RgY\xa2\x12\
+e\x8at\x83\xbe\x16\xb9M\x05\xaf\xaf<1n\xda\x1d\
+>>\xb7\xc6`\xf8\xb1p\xd1\x97\x1bTe\xb0\x9b\x9d\
+l\x22ogBJC\xd4\xf8\x95\x19\xf2\x14\xc8\x12\xdd\
+v\x07\xc7~[e\xf0n_\x98\xf1\xe5:\x0bla\
+\x1f\xdb\xe9%\x873 \x90\x8a&\xcb\xccq\x91;l\
+\x94\x8d,v\xbf7\xd76x'\x1f\xae\xfa\x01\xa5\xce\
+\x13:\xbd\x8f.2\xc4\xc2\x89\x00x\xb1B\x83\x84\x05\
+\xbeg\xc6J\x89\xa6\xdd\xbe\xf7\xeb\x10\x01\xa4\xc3~ \
+\xe1\x9e\xe4\x03l\xa3\x87<9\x91\xb5\xad\xdfmw\xd1\
+-6\xf3\x1cO*2\xc1\x0f\xa4\xc3\xad\x0c\xde.\x85\
+\x1b\xcd\xc2\x12E^e{\xab\xf2\x98K<+\x01\xb6\
+\x82\xd1\xb4,j,pZ\xbb\x87L\xd5=|\xb2\x12\
+A:\xd2,\xd4\xa93HY\xf2l\xcb\xe0\x06\x13\xfc\
+Ad\xc4\xc6\x06\xcb\xabK\xd2\x17\xa9\x8bf!\x1d\x81\
+\xf8N\xce\x7f\xba\x92[\xe0\x19\x15\xb0Y\xf2\x8c\x02c\
+\x89~d\x89_\xf8\x8b\x87\xc8\x02\xae\x0d@\x1e\xf8]\
+qn\xe7\xd7\xa7\x22\xbf?-6\xf4\xd1\xcf\x16b\xc3\
+\x19ujT\xf9\x81c\xfc\xdc\xe9G\xcb#+\x9e\xd2\
+O5Z\xd1\xef\x8f\xd2C)\x09\xdb\xec\x1f\x97\xb1\x90\
+\x08\x07$\xc8@\xfc\xcd)>b\xa1m\x91\x11Y\x0a\
+\xe0c\x161\x03\xcbs\xabT2\
+(e|\xc9\x93\xd2c\xe9w\xce\x87?I\xa8[\xf3\
+xbk\xe1k\xdc\xe4\x08\x8f\xe3,r\x0b\x92#\x83\
+\x80h%\x07\x1d\x83[\xdc\xe3\xaev`\x93\x911\x01\
+\x80\xc5\x89`dB%<\xe2X\xc2ll\xd3\xae6\
+Ra\x81\xa8e\xbe\x877(\xe2\xb18\x99\x9bE%\
+\xe3\xcd\xe0.M\xbc\x08\xc2\x96\xf5C\x1e\x08\xd6\x5c[\
+9\xca\xdevWz1o\x06\xbe\x12\x85Y\x88U\xf1\
+\x8a\xa5\x98\xb6-\xb2\x12vS\x14\xdd<\xcfg\x1dy\
++nV*\x08\xb3Q8\x1f\xacq\x13R\x9a\x1d\x03\
+\xbb<=\x92\xef\xe0CN\xd0\x0b\x1d\x03\x8b\xbb.\x95\
+\xbe\xceG\xe1\x0a\x8b15f\xa8\xd2\xa0)\xacJ\x0a\
+f\xf0\x02\xe78\x00m\xb9\xb7\x88\x06\xd7\x14\x1d\xc3b\
+\xb8\x12M$a2\x22\xc77\xcc\xd1\x10V\x0aA\x06\
+%\x8es\x8c\xdeU\xf2\xd4n\xe4?|!ED\x98\
+\x9cH\x22\x08\xa3\xa1\x1a\x03S,PG&\x96\xe2\xd3\
+\x9cd\x0f\xab\xc5:[\xd4\xf9\x84\x80\x0a\xa8\x86Q\x88\
+\xe0\xf3\x0a\xe3\xb1\x1coq\x95%j$b\x85\xd7)\
+\xa0\x5c\x8c\xd4X!\x115\xe5zS\xd11\x8cKI\
+\x06\x801\x06\xe3\x81..\xe1\x18\xc4\x04\xab\x86Zk\
+*\xa2\xd3%\xff\x92.$\x9f\x96\xaa3T_\xd1P\
+\x0d6\x93w\xf0\x1ae\xb2\xc2\x9a\x1b\x00/$g\x9e\
+\xd3\xdc\xb0\xb9\xec4T\xa7:C\xd5,4\xd6C\xb9\
+i\xbd?\xc4^\x85\xc5\xad\xabm\xf5\x8b\xef8\x87\xb3\
+K/\xf9\xc1\xa9\xd5c\x1d\xe0\xe5>\xf4\xb0xK\xb4\
+\x9b]<\xca\xfd\xdc\x87\xe3_\xe6\xb9.\x96\xac'e\
+9\xcd\xe13k\x1f\x16\xb3\xb0\xa7-\x14\xecO\xd6\xee\
+\x08'\x91\x0d:\xe1\xeci;S_\xe7q}\xc9\x1e\
+\xd7P\xec\x5c.g[\xd8\xe3zv\x9d\xc7\xb5c\x92\
+\x0b\x07\x18r\xfd\xa1\xf5\xbc\xbbJ\x98\xe5\x82\xbb|6\
+a\xcd\xfa\x0f\xcb\x16\xf49\xc3\x9f\xd1\x8a\x00\x00\x00\x00\
+IEND\xaeB`\x82\
"
qt_resource_name = b"\
@@ -1009,6 +1870,11 @@ qt_resource_name = b"\
\x0c\xbc.g\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00n\x00e\x00w\x00.\x00p\x00n\x00g\
+\x00\x11\
+\x0e\xab3G\
+\x00z\
+\x00o\x00o\x00m\x00-\x00o\x00r\x00i\x00g\x00i\x00n\x00a\x00l\x00.\x00p\x00n\x00g\
+\
\x00\x14\
\x0b\xa9\xab'\
\x00d\
@@ -1019,10 +1885,26 @@ qt_resource_name = b"\
\x00t\
\x00r\x00a\x00n\x00s\x00f\x00o\x00r\x00m\x00-\x00r\x00o\x00t\x00a\x00t\x00e\x00.\
\x00p\x00n\x00g\
+\x00\x0c\
+\x09\xd0z\x07\
+\x00a\
+\x00r\x00r\x00o\x00w\x00-\x00u\x00p\x00.\x00p\x00n\x00g\
+\x00\x0d\
+\x0b\xe6\x1f\xa7\
+\x00d\
+\x00r\x00a\x00w\x00-\x00t\x00e\x00x\x00t\x00.\x00p\x00n\x00g\
+\x00\x0d\
+\x08Q\xc9'\
+\x00c\
+\x00o\x00n\x00f\x00i\x00g\x00u\x00r\x00e\x00.\x00p\x00n\x00g\
\x00\x0d\
\x03\xd2\xbeg\
\x00e\
\x00d\x00i\x00t\x00-\x00u\x00n\x00d\x00o\x00.\x00p\x00n\x00g\
+\x00\x0b\
+\x03\x03\x9bG\
+\x00z\
+\x00o\x00o\x00m\x00-\x00i\x00n\x00.\x00p\x00n\x00g\
\x00\x0c\
\x07\xb1Y'\
\x00e\
@@ -1035,6 +1917,15 @@ qt_resource_name = b"\
\x0c\xd2\xbf\xe7\
\x00e\
\x00d\x00i\x00t\x00-\x00r\x00e\x00d\x00o\x00.\x00p\x00n\x00g\
+\x00\x16\
+\x0c\xb7\x89\xc7\
+\x00d\
+\x00r\x00a\x00w\x00-\x00b\x00e\x00z\x00i\x00e\x00r\x00-\x00c\x00u\x00r\x00v\x00e\
+\x00s\x00.\x00p\x00n\x00g\
+\x00\x0c\
+\x06\xeb\x97\xe7\
+\x00z\
+\x00o\x00o\x00m\x00-\x00o\x00u\x00t\x00.\x00p\x00n\x00g\
\x00\x11\
\x01\xa6\xc4\x87\
\x00d\
@@ -1044,11 +1935,29 @@ qt_resource_name = b"\
\x0c\xaa\xc0\xa7\
\x00e\
\x00d\x00i\x00t\x00-\x00p\x00a\x00s\x00t\x00e\x00.\x00p\x00n\x00g\
+\x00\x0f\
+\x0bO\xc9\xc7\
+\x00e\
+\x00d\x00i\x00t\x00-\x00s\x00e\x00l\x00e\x00c\x00t\x00.\x00p\x00n\x00g\
+\x00\x0e\
+\x06\x0c\x0a\x07\
+\x00a\
+\x00r\x00r\x00o\x00w\x00-\x00d\x00o\x00w\x00n\x00.\x00p\x00n\x00g\
\x00\x11\
\x0f\xe3\xd5g\
\x00d\
\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\
\
+\x00\x12\
+\x09\xb3>\xc7\
+\x00d\
+\x00r\x00a\x00w\x00-\x00r\x00e\x00c\x00t\x00a\x00n\x00g\x00l\x00e\x00.\x00p\x00n\
+\x00g\
+\x00\x18\
+\x0f\xa4\x86G\
+\x00m\
+\x00e\x00d\x00i\x00a\x00-\x00p\x00l\x00a\x00y\x00b\x00a\x00c\x00k\x00-\x00s\x00t\
+\x00a\x00r\x00t\x00.\x00p\x00n\x00g\
"
qt_resource_struct = b"\
@@ -1056,27 +1965,49 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x15\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xfa\
+\x00\x00\x01r\x00\x00\x00\x00\x00\x01\x00\x00:\x16\
\x00\x00\x01\x9f{C\xf1'\
-\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00+,\
+\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00Pr\
\x00\x00\x01\x9f{0\xc99\
-\x00\x00\x00\x92\x00\x00\x00\x00\x00\x01\x00\x00\x14\x92\
+\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00/`\
+\x00\x00\x01\x9f\x7f&\x83\xcd\
+\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00'x\
\x00\x00\x01\x9f{C\xf1\x18\
-\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x1cz\
+\x00\x00\x02p\x00\x00\x00\x00\x00\x01\x00\x00`\xe1\
+\x00\x00\x01\x9f\x7f&\x83r\
+\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x00Iw\
+\x00\x00\x01\x9f\x7f&\x83\xb1\
+\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x006\x96\
\x00\x00\x01\x9f{C\xf1.\
-\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
+\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x01\x00\x00!\xca\
+\x00\x00\x01\x9f{{\xa5\xd5\
+\x00\x00\x02\xba\x00\x00\x00\x00\x00\x01\x00\x00i\xc6\
+\x00\x00\x01\x9f\x7f&\x83\x10\
+\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xcd\
+\x00\x00\x01\x9f\x7f&\x83~\
+\x00\x00\x02L\x00\x00\x00\x00\x00\x01\x00\x00\x5c\xfe\
+\x00\x00\x01\x9f\x7f&\x83T\
+\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\
\x00\x00\x01\x9f{0\xc9B\
-\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x0026\
+\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x1fp\
+\x00\x00\x01\x9f\x7f&\x83)\
+\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x00W|\
\x00\x00\x01\x9f{C\xf1N\
-\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf2\
+\x00\x00\x01\xb2\x00\x00\x00\x00\x00\x01\x00\x00EH\
+\x00\x00\x01\x9f\x7f&\x82\xf2\
+\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x15-\
\x00\x00\x01\x9f{\x8d\xf34\
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9f{0\xc9+\
-\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00#Z\
+\x00\x00\x01\x92\x00\x00\x00\x00\x00\x01\x00\x00=v\
\x00\x00\x01\x9f{C\xf1=\
-\x00\x00\x01Z\x00\x00\x00\x00\x00\x01\x00\x007\xb8\
+\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\
+\x00\x00\x01\x9f\x7f&\x83\x99\
+\x00\x00\x02\xe4\x00\x00\x00\x00\x00\x01\x00\x00l\x5c\
+\x00\x00\x01\x9f{{\xa5\xe3\
+\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00d\xd3\
\x00\x00\x01\x9f{0\xc9R\
"
diff --git a/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py b/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py
index 9d03a00..83f44d7 100644
--- a/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py
+++ b/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py
@@ -31,6 +31,13 @@ class Ui_IconEditorDialog(object):
self.dialogLayout.setObjectName(u"dialogLayout")
self.shapeToolbarLayout = QHBoxLayout()
self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout")
+ self.pointerButton = QToolButton(IconEditorDialog)
+ self.pointerButton.setObjectName(u"pointerButton")
+ self.pointerButton.setCheckable(True)
+ self.pointerButton.setChecked(True)
+
+ self.shapeToolbarLayout.addWidget(self.pointerButton)
+
self.addShapeLabel = QLabel(IconEditorDialog)
self.addShapeLabel.setObjectName(u"addShapeLabel")
@@ -38,31 +45,37 @@ class Ui_IconEditorDialog(object):
self.addRectangleButton = QToolButton(IconEditorDialog)
self.addRectangleButton.setObjectName(u"addRectangleButton")
+ self.addRectangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addRectangleButton)
self.addCircleButton = QToolButton(IconEditorDialog)
self.addCircleButton.setObjectName(u"addCircleButton")
+ self.addCircleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addCircleButton)
self.addEllipseButton = QToolButton(IconEditorDialog)
self.addEllipseButton.setObjectName(u"addEllipseButton")
+ self.addEllipseButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addEllipseButton)
self.addLineButton = QToolButton(IconEditorDialog)
self.addLineButton.setObjectName(u"addLineButton")
+ self.addLineButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addLineButton)
self.addTriangleButton = QToolButton(IconEditorDialog)
self.addTriangleButton.setObjectName(u"addTriangleButton")
+ self.addTriangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTriangleButton)
self.addTextButton = QToolButton(IconEditorDialog)
self.addTextButton.setObjectName(u"addTextButton")
+ self.addTextButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTextButton)
@@ -70,6 +83,21 @@ class Ui_IconEditorDialog(object):
self.shapeToolbarLayout.addItem(self.toolbarSpacer)
+ self.zoomInButton = QToolButton(IconEditorDialog)
+ self.zoomInButton.setObjectName(u"zoomInButton")
+
+ self.shapeToolbarLayout.addWidget(self.zoomInButton)
+
+ self.zoomOutButton = QToolButton(IconEditorDialog)
+ self.zoomOutButton.setObjectName(u"zoomOutButton")
+
+ self.shapeToolbarLayout.addWidget(self.zoomOutButton)
+
+ self.centerButton = QToolButton(IconEditorDialog)
+ self.centerButton.setObjectName(u"centerButton")
+
+ self.shapeToolbarLayout.addWidget(self.centerButton)
+
self.deleteSelectedButton = QPushButton(IconEditorDialog)
self.deleteSelectedButton.setObjectName(u"deleteSelectedButton")
@@ -106,6 +134,7 @@ class Ui_IconEditorDialog(object):
def retranslateUi(self, IconEditorDialog):
IconEditorDialog.setWindowTitle(QCoreApplication.translate("IconEditorDialog", u"Icon Editor", None))
+ self.pointerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Pointer", None))
self.addShapeLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Add:", None))
self.addRectangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Rectangle", None))
self.addCircleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Circle", None))
@@ -113,6 +142,18 @@ class Ui_IconEditorDialog(object):
self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None))
self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None))
self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None))
+ self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
+#if QT_CONFIG(tooltip)
+ self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None))
+#endif // QT_CONFIG(tooltip)
+ self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
+#if QT_CONFIG(tooltip)
+ self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None))
+#endif // QT_CONFIG(tooltip)
+ self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
+#if QT_CONFIG(tooltip)
+ self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None))
+#endif // QT_CONFIG(tooltip)
self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None))
self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None))
# retranslateUi
diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py
index 73f09c7..59553a7 100644
--- a/BEdit/src/bedit/gui/generated/ui_main_window.py
+++ b/BEdit/src/bedit/gui/generated/ui_main_window.py
@@ -29,7 +29,7 @@ class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
- MainWindow.resize(1000, 700)
+ MainWindow.resize(1209, 777)
self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew")
icon = QIcon()
@@ -42,54 +42,63 @@ class Ui_MainWindow(object):
self.actionRotateClockwise.setIcon(icon1)
self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn")
+ icon2 = QIcon()
+ icon2.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionZoomIn.setIcon(icon2)
self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut")
+ icon3 = QIcon()
+ icon3.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionZoomOut.setIcon(icon3)
self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView")
+ icon4 = QIcon()
+ icon4.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionCenterView.setIcon(icon4)
self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen")
- icon2 = QIcon()
- icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionOpen.setIcon(icon2)
+ icon5 = QIcon()
+ icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionOpen.setIcon(icon5)
self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave")
- icon3 = QIcon()
- icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionSave.setIcon(icon3)
+ icon6 = QIcon()
+ icon6.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionSave.setIcon(icon6)
self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
- icon4 = QIcon()
- icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionSaveAs.setIcon(icon4)
+ icon7 = QIcon()
+ icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionSaveAs.setIcon(icon7)
self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo")
- icon5 = QIcon()
- icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionUndo.setIcon(icon5)
+ icon8 = QIcon()
+ icon8.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionUndo.setIcon(icon8)
self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo")
- icon6 = QIcon()
- icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionRedo.setIcon(icon6)
+ icon9 = QIcon()
+ icon9.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionRedo.setIcon(icon9)
self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut")
- icon7 = QIcon()
- icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionCut.setIcon(icon7)
+ icon10 = QIcon()
+ icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionCut.setIcon(icon10)
self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy")
- icon8 = QIcon()
- icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionCopy.setIcon(icon8)
+ icon11 = QIcon()
+ icon11.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionCopy.setIcon(icon11)
self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste")
- icon9 = QIcon()
- icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.actionPaste.setIcon(icon9)
+ icon12 = QIcon()
+ icon12.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.actionPaste.setIcon(icon12)
self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow)
@@ -129,9 +138,9 @@ class Ui_MainWindow(object):
self.librariesLayout.setContentsMargins(0, 0, 0, 0)
self.treeView = QTreeView(self.dockWidgetContents)
self.treeView.setObjectName(u"treeView")
- self.treeView.setIconSize(QSize(28, 28))
self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }")
self.treeView.setAlternatingRowColors(True)
+ self.treeView.setIconSize(QSize(28, 28))
self.treeView.setUniformRowHeights(True)
self.librariesLayout.addWidget(self.treeView)
@@ -149,8 +158,8 @@ class Ui_MainWindow(object):
self.documentPanelLayout.setContentsMargins(0, 0, 0, 0)
self.documentTreeView = QTreeView(self.documentDockContents)
self.documentTreeView.setObjectName(u"documentTreeView")
- self.documentTreeView.setIconSize(QSize(16, 16))
self.documentTreeView.setAlternatingRowColors(True)
+ self.documentTreeView.setIconSize(QSize(16, 16))
self.documentTreeView.setUniformRowHeights(True)
self.documentPanelLayout.addWidget(self.documentTreeView)
@@ -179,12 +188,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton")
+ icon13 = QIcon()
+ icon13.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.navigateUpButton.setIcon(icon13)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False)
+ icon14 = QIcon()
+ icon14.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.navigateDownButton.setIcon(icon14)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
@@ -210,6 +225,9 @@ class Ui_MainWindow(object):
self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton")
+ icon15 = QIcon()
+ icon15.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.pointerToolButton.setIcon(icon15)
self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True)
@@ -221,6 +239,39 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
+ self.boxToolButton = QToolButton(self.workspaceHeader)
+ self.boxToolButton.setObjectName(u"boxToolButton")
+ icon16 = QIcon()
+ icon16.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.boxToolButton.setIcon(icon16)
+ self.boxToolButton.setCheckable(True)
+
+ self.workspaceHeaderLayout.addWidget(self.boxToolButton)
+
+ self.lineToolButton = QToolButton(self.workspaceHeader)
+ self.lineToolButton.setObjectName(u"lineToolButton")
+ icon17 = QIcon()
+ icon17.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.lineToolButton.setIcon(icon17)
+ self.lineToolButton.setCheckable(True)
+
+ self.workspaceHeaderLayout.addWidget(self.lineToolButton)
+
+ self.textToolButton = QToolButton(self.workspaceHeader)
+ self.textToolButton.setObjectName(u"textToolButton")
+ icon18 = QIcon()
+ icon18.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.textToolButton.setIcon(icon18)
+ self.textToolButton.setCheckable(True)
+
+ self.workspaceHeaderLayout.addWidget(self.textToolButton)
+
+ self.rotateToolButton = QToolButton(self.workspaceHeader)
+ self.rotateToolButton.setObjectName(u"rotateToolButton")
+ self.rotateToolButton.setIcon(icon1)
+
+ self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
+
self.routingLabel = QLabel(self.workspaceHeader)
self.routingLabel.setObjectName(u"routingLabel")
@@ -235,13 +286,13 @@ class Ui_MainWindow(object):
self.angledRoutingButton = QToolButton(self.workspaceHeader)
self.angledRoutingButton.setObjectName(u"angledRoutingButton")
self.angledRoutingButton.setCheckable(True)
+ self.angledRoutingButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.angledRoutingButton)
self.splineRoutingButton = QToolButton(self.workspaceHeader)
self.splineRoutingButton.setObjectName(u"splineRoutingButton")
self.splineRoutingButton.setCheckable(True)
- self.splineRoutingButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.splineRoutingButton)
@@ -296,7 +347,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar")
- self.menubar.setGeometry(QRect(0, 0, 1000, 24))
+ self.menubar.setGeometry(QRect(0, 0, 1209, 24))
self.menuFile = QMenu(self.menubar)
self.menuFile.setObjectName(u"menuFile")
self.menuEdit = QMenu(self.menubar)
@@ -324,9 +375,6 @@ class Ui_MainWindow(object):
self.editToolbar = QToolBar(MainWindow)
self.editToolbar.setObjectName(u"editToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
- self.transformToolbar = QToolBar(MainWindow)
- self.transformToolbar.setObjectName(u"transformToolbar")
- MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar)
self.cameraToolbar = QToolBar(MainWindow)
self.cameraToolbar.setObjectName(u"cameraToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
@@ -366,7 +414,6 @@ class Ui_MainWindow(object):
self.editToolbar.addAction(self.actionCopy)
self.editToolbar.addAction(self.actionCut)
self.editToolbar.addAction(self.actionPaste)
- self.transformToolbar.addAction(self.actionRotateClockwise)
self.cameraToolbar.addAction(self.actionZoomIn)
self.cameraToolbar.addAction(self.actionZoomOut)
self.cameraToolbar.addAction(self.actionCenterView)
@@ -469,19 +516,35 @@ class Ui_MainWindow(object):
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None))
- self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
#if QT_CONFIG(tooltip)
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
#endif // QT_CONFIG(tooltip)
- self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
+ self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
#if QT_CONFIG(tooltip)
self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None))
#endif // QT_CONFIG(tooltip)
+ self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
+#if QT_CONFIG(tooltip)
+ self.boxToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a box annotation", None))
+#endif // QT_CONFIG(tooltip)
+ self.boxToolButton.setText(QCoreApplication.translate("MainWindow", u"Box", None))
+#if QT_CONFIG(tooltip)
+ self.lineToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a line annotation", None))
+#endif // QT_CONFIG(tooltip)
+ self.lineToolButton.setText(QCoreApplication.translate("MainWindow", u"Line", None))
+#if QT_CONFIG(tooltip)
+ self.textToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add a text annotation", None))
+#endif // QT_CONFIG(tooltip)
+ self.textToolButton.setText(QCoreApplication.translate("MainWindow", u"Text", None))
+#if QT_CONFIG(tooltip)
+ self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
+#endif // QT_CONFIG(tooltip)
+ self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None))
self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None))
self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None))
@@ -496,7 +559,6 @@ class Ui_MainWindow(object):
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
- self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))
# retranslateUi
diff --git a/BEdit/src/bedit/gui/graphics/icon_canvas.py b/BEdit/src/bedit/gui/graphics/icon_canvas.py
index c1145d8..f6279f7 100644
--- a/BEdit/src/bedit/gui/graphics/icon_canvas.py
+++ b/BEdit/src/bedit/gui/graphics/icon_canvas.py
@@ -1,5 +1,6 @@
from PySide6.QtCore import QPointF, QRectF
-from PySide6.QtGui import QColor, QPainter, QPen
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor, QPainter, QPen, QWheelEvent
from PySide6.QtWidgets import QGraphicsView
from bedit.gui.preferences import application_settings
@@ -22,3 +23,25 @@ class IconCanvasView(QGraphicsView):
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
for y in range(top, int(rect.bottom()) + grid, grid):
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
+
+ def _zoom(self, factor: float) -> None:
+ target = self.transform().m11() * factor
+ if 0.25 <= target <= 16.0:
+ self.scale(factor, factor)
+
+ def zoom_in(self) -> None:
+ self._zoom(1.2)
+
+ def zoom_out(self) -> None:
+ self._zoom(1 / 1.2)
+
+ def center_icon(self) -> None:
+ if self.scene() is not None:
+ self.fitInView(
+ self.scene().sceneRect().adjusted(-10, -10, 10, 10),
+ Qt.AspectRatioMode.KeepAspectRatio,
+ )
+
+ def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
+ self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
+ event.accept()
diff --git a/BEdit/src/bedit/gui/graphics/icon_editor.py b/BEdit/src/bedit/gui/graphics/icon_editor.py
index 0ac5fcb..992e949 100644
--- a/BEdit/src/bedit/gui/graphics/icon_editor.py
+++ b/BEdit/src/bedit/gui/graphics/icon_editor.py
@@ -1,13 +1,15 @@
from copy import deepcopy
-from PySide6.QtCore import QPointF, QRectF, Qt
-from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
+from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt
+from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import (
QColorDialog,
+ QButtonGroup,
QDialog,
QGraphicsEllipseItem,
QGraphicsItem,
QGraphicsObject,
+ QGraphicsPathItem,
QGraphicsScene,
QGraphicsSceneContextMenuEvent,
QGraphicsSceneMouseEvent,
@@ -19,7 +21,7 @@ from bedit.core.model import Component, Icon, Port
from bedit.gui.generated.ui_icon_editor_dialog import Ui_IconEditorDialog
from bedit.gui.generated.ui_shape_options_dialog import Ui_ShapeOptionsDialog
from bedit.gui.graphics.icon_canvas import icon_grid_size
-from bedit.gui.graphics.icon_renderer import _pen
+from bedit.gui.graphics.icon_renderer import shape_pen
def _icon_grid_size() -> int:
@@ -49,6 +51,21 @@ class ResizeHandle(QGraphicsEllipseItem):
if self.owner.scene()
else QPointF(128, 128)
)
+ if self.owner.element.get("type") == "line":
+ minimum = (
+ self.owner.scene().sceneRect().topLeft() - self.owner.pos()
+ if self.owner.scene()
+ else QPointF(-128, -128)
+ )
+ value = QPointF(
+ min(maximum.x(), max(minimum.x(), _snap(value.x()))),
+ min(maximum.y(), max(minimum.y(), _snap(value.y()))),
+ )
+ self.owner.prepareGeometryChange()
+ self.owner.element["width"] = value.x()
+ self.owner.element["height"] = value.y()
+ self.owner.update()
+ return super().itemChange(change, value)
value = QPointF(
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
@@ -82,9 +99,17 @@ class ShapeOptionsDialog(QDialog):
self.ui.cornerRadiusSpin.setValue(float(self.element.get("cornerRadius", 0)))
self.ui.textEdit.setText(str(self.element.get("text", "Text")))
self.ui.fontSizeSpin.setValue(float(self.element.get("fontSize", 12)))
- self._show_row(self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line")
- self._show_row(self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line")
- self._show_row(self.ui.cornerRadiusLabel, self.ui.cornerRadiusSpin, self.element.get("type") == "rectangle")
+ self._show_row(
+ self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line"
+ )
+ self._show_row(
+ self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line"
+ )
+ self._show_row(
+ self.ui.cornerRadiusLabel,
+ self.ui.cornerRadiusSpin,
+ self.element.get("type") == "rectangle",
+ )
is_text = self.element.get("type") == "text"
self._show_row(self.ui.textLabel, self.ui.textEdit, is_text)
self._show_row(self.ui.fontSizeLabel, self.ui.fontSizeSpin, is_text)
@@ -99,7 +124,9 @@ class ShapeOptionsDialog(QDialog):
def _choose_color(self, current: str) -> str:
color = QColorDialog.getColor(
- QColor(current), self, "Choose colour",
+ QColor(current),
+ self,
+ "Choose colour",
QColorDialog.ColorDialogOption.ShowAlphaChannel,
)
if not color.isValid():
@@ -129,7 +156,9 @@ class ShapeOptionsDialog(QDialog):
self.element["width"] = self.ui.widthSpin.value()
self.element["height"] = self.ui.heightSpin.value()
if self.element.get("type") != "line":
- self.element["fill"] = self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none"
+ self.element["fill"] = (
+ self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none"
+ )
if self.element.get("type") == "rectangle":
self.element["cornerRadius"] = self.ui.cornerRadiusSpin.value()
if self.element.get("type") == "text":
@@ -144,21 +173,35 @@ class ShapeItem(QGraphicsObject):
super().__init__()
self.element = element
self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
- self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
+ self.setFlags(
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable
+ | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
+ | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
+ )
self.resize_handle = ResizeHandle(self)
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
self.resize_handle.hide()
def boundingRect(self) -> QRectF: # noqa: N802
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
- return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin)
+ return (
+ QRectF(
+ 0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
+ )
+ .normalized()
+ .adjusted(-margin, -margin, margin, margin)
+ )
def paint(self, painter: QPainter, option, widget=None) -> None:
del option, widget
- rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)))
- painter.setPen(_pen(self.element))
+ rect = QRectF(
+ 0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
+ )
+ painter.setPen(shape_pen(self.element))
fill = self.element.get("fill", "none")
- painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
+ painter.setBrush(
+ Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
+ )
kind = self.element.get("type")
if kind == "rectangle":
radius = float(self.element.get("cornerRadius", 0))
@@ -168,13 +211,17 @@ class ShapeItem(QGraphicsObject):
elif kind == "line":
painter.drawLine(rect.topLeft(), rect.bottomRight())
elif kind == "triangle":
- painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()]))
+ painter.drawPolygon(
+ QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()])
+ )
elif kind == "text":
painter.setPen(QColor(self.element.get("color", "#202020")))
font = painter.font()
font.setPointSizeF(float(self.element.get("fontSize", 12)))
painter.setFont(font)
- painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text")))
+ painter.drawText(
+ rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text"))
+ )
if self.isSelected():
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
@@ -182,23 +229,7 @@ class ShapeItem(QGraphicsObject):
def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
- bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
- value = QPointF(
- max(
- bounds.left(),
- min(
- bounds.right() - float(self.element.get("width", 20)),
- _snap(value.x()),
- ),
- ),
- max(
- bounds.top(),
- min(
- bounds.bottom() - float(self.element.get("height", 20)),
- _snap(value.y()),
- ),
- ),
- )
+ value = self._bounded_position(value)
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
self.element["x"], self.element["y"] = value.x(), value.y()
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
@@ -207,10 +238,17 @@ class ShapeItem(QGraphicsObject):
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
super().mouseReleaseEvent(event)
+ self.setPos(self._bounded_position(self.pos()))
+
+ def _bounded_position(self, position: QPointF) -> QPointF:
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
- self.setPos(
- max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))),
- max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))),
+ width = float(self.element.get("width", 20))
+ height = float(self.element.get("height", 20))
+ minimum_x, maximum_x = min(0.0, width), max(0.0, width)
+ minimum_y, maximum_y = min(0.0, height), max(0.0, height)
+ return QPointF(
+ max(bounds.left() - minimum_x, min(bounds.right() - maximum_x, _snap(position.x()))),
+ max(bounds.top() - minimum_y, min(bounds.bottom() - maximum_y, _snap(position.y()))),
)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
@@ -219,11 +257,19 @@ class ShapeItem(QGraphicsObject):
delete = menu.addAction("Delete Shape")
chosen = menu.exec(event.screenPos())
if chosen is options:
- dialog = ShapeOptionsDialog(self.element)
+ options_element = deepcopy(self.element)
+ width_sign = -1 if float(options_element.get("width", 20)) < 0 else 1
+ height_sign = -1 if float(options_element.get("height", 20)) < 0 else 1
+ options_element["width"] = abs(float(options_element.get("width", 20)))
+ options_element["height"] = abs(float(options_element.get("height", 20)))
+ dialog = ShapeOptionsDialog(options_element)
if dialog.exec() == dialog.DialogCode.Accepted:
self.prepareGeometryChange()
self.element.clear()
self.element.update(dialog.element)
+ if self.element.get("type") == "line":
+ self.element["width"] *= width_sign
+ self.element["height"] *= height_sign
self.resize_handle.setPos(
float(self.element.get("width", 20)),
float(self.element.get("height", 20)),
@@ -244,7 +290,11 @@ class PortHandle(QGraphicsEllipseItem):
self.setPen(QPen(QColor("#ffffff"), 1.5))
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
self.setZValue(10)
- self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
+ self.setFlags(
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable
+ | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
+ | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
+ )
def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
@@ -266,6 +316,81 @@ class PortHandle(QGraphicsEllipseItem):
)
+class IconDrawingScene(QGraphicsScene):
+ def __init__(self, editor: "IconEditorDialog", rect: QRectF) -> None:
+ super().__init__(rect, editor)
+ self.editor = editor
+ self.mode = "pointer"
+ self.start: QPointF | None = None
+ self.preview: QGraphicsPathItem | None = None
+
+ def set_mode(self, mode: str) -> None:
+ self.mode = mode
+ self._clear_preview()
+
+ def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ if self.mode == "pointer":
+ super().mousePressEvent(event)
+ return
+ point = self._bounded(event.scenePos())
+ if self.mode == "text":
+ text, accepted = QInputDialog.getText(self.editor, "Add Text", "Text:", text="Text")
+ if accepted:
+ self.editor.create_shape("text", point, point + QPointF(55, 35), text)
+ event.accept()
+ return
+ self.start = point
+ self.preview = QGraphicsPathItem()
+ self.preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
+ self.preview.setZValue(100)
+ self.addItem(self.preview)
+ event.accept()
+
+ def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ if self.start is None or self.preview is None:
+ super().mouseMoveEvent(event)
+ return
+ end = self._bounded(event.scenePos())
+ path = QPainterPath()
+ rect = QRectF(self.start, end).normalized()
+ if self.mode == "line":
+ path.moveTo(self.start)
+ path.lineTo(end)
+ elif self.mode in {"circle", "ellipse"}:
+ if self.mode == "circle":
+ side = min(rect.width(), rect.height())
+ rect.setSize(QSizeF(side, side))
+ path.addEllipse(rect)
+ else:
+ path.addRect(rect)
+ self.preview.setPath(path)
+ event.accept()
+
+ def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ if self.start is None:
+ super().mouseReleaseEvent(event)
+ return
+ end = self._bounded(event.scenePos())
+ start = self.start
+ self._clear_preview()
+ if start != end:
+ self.editor.create_shape(self.mode, start, end)
+ event.accept()
+
+ def _bounded(self, point: QPointF) -> QPointF:
+ rect = self.sceneRect()
+ return QPointF(
+ min(rect.right(), max(rect.left(), _snap(point.x()))),
+ min(rect.bottom(), max(rect.top(), _snap(point.y()))),
+ )
+
+ def _clear_preview(self) -> None:
+ if self.preview is not None:
+ self.removeItem(self.preview)
+ self.preview = None
+ self.start = None
+
+
class IconEditorDialog(QDialog):
def __init__(self, component: Component, parent=None) -> None:
super().__init__(parent)
@@ -275,6 +400,10 @@ class IconEditorDialog(QDialog):
self.icon = Icon.from_dict(component.icon.to_dict())
self.inputs = deepcopy(component.inputs)
self.outputs = deepcopy(component.outputs)
+ self.tool_group = QButtonGroup(self)
+ self.tool_group.setExclusive(True)
+ self.tool_group.addButton(self.ui.pointerButton)
+ self.ui.pointerButton.clicked.connect(lambda: self.set_draw_mode("pointer"))
for button, kind in (
(self.ui.addRectangleButton, "rectangle"),
(self.ui.addCircleButton, "circle"),
@@ -283,43 +412,70 @@ class IconEditorDialog(QDialog):
(self.ui.addTriangleButton, "triangle"),
(self.ui.addTextButton, "text"),
):
- button.clicked.connect(
- lambda _checked=False, value=kind: self.add_shape(value)
- )
+ self.tool_group.addButton(button)
+ button.clicked.connect(lambda _checked=False, value=kind: self.set_draw_mode(value))
self.ui.deleteSelectedButton.clicked.connect(self.delete_selected)
- self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
+ self.scene = IconDrawingScene(self, QRectF(0, 0, self.icon.width, self.icon.height))
self.ui.iconView.setScene(self.scene)
self.view = self.ui.iconView
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
+ self.ui.zoomInButton.clicked.connect(self.view.zoom_in)
+ self.ui.zoomOutButton.clicked.connect(self.view.zoom_out)
+ self.ui.centerButton.clicked.connect(self.view.center_icon)
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
for element in self.icon.elements:
self.scene.addItem(ShapeItem(element))
self._add_ports(self.inputs, "input", 0.0)
self._add_ports(self.outputs, "output", self.icon.width)
- self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio)
+ self.view.center_icon()
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
spacing = self.icon.height / (len(ports) + 1)
for index, port in enumerate(ports, 1):
saved = port.properties.get("iconPosition", {})
- position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index)))
+ position = QPointF(
+ float(saved.get("x", default_x)), float(saved.get("y", spacing * index))
+ )
self.scene.addItem(PortHandle(port, direction, position))
- def add_shape(self, kind: str) -> None:
- count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)])
- x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25
- element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"}
+ def set_draw_mode(self, mode: str) -> None:
+ self.scene.set_mode(mode)
+ self.view.setDragMode(
+ self.view.DragMode.RubberBandDrag if mode == "pointer" else self.view.DragMode.NoDrag
+ )
+
+ def create_shape(self, kind: str, start: QPointF, end: QPointF, text: str = "Text") -> None:
+ left, right = sorted((start.x(), end.x()))
+ top, bottom = sorted((start.y(), end.y()))
+ element = {
+ "type": kind,
+ "x": left,
+ "y": top,
+ "width": max(_icon_grid_size(), right - left),
+ "height": max(_icon_grid_size(), bottom - top),
+ "fill": "#dbeafe",
+ "stroke": "#303030",
+ "lineWidth": 1.5,
+ "lineStyle": "solid",
+ }
if kind == "circle":
- element["width"] = element["height"] = 35
+ element["width"] = element["height"] = min(element["width"], element["height"])
if kind == "line":
+ element["x"], element["y"] = start.x(), start.y()
+ element["width"], element["height"] = end.x() - start.x(), end.y() - start.y()
element["fill"] = "none"
if kind == "rectangle":
element["cornerRadius"] = 0
if kind == "text":
- text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text")
- if not accepted:
- return
- element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"})
+ element.update(
+ {
+ "text": text,
+ "fontSize": 12,
+ "color": "#202020",
+ "fill": "none",
+ "lineStyle": "none",
+ }
+ )
self.icon.elements.append(element)
item = ShapeItem(element)
self.scene.addItem(item)
@@ -332,5 +488,7 @@ class IconEditorDialog(QDialog):
item.element["_deleted"] = True
def accept(self) -> None:
- self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)]
+ self.icon.elements = [
+ element for element in self.icon.elements if not element.pop("_deleted", False)
+ ]
super().accept()
diff --git a/BEdit/src/bedit/gui/graphics/icon_renderer.py b/BEdit/src/bedit/gui/graphics/icon_renderer.py
index 84f6f75..3097f6c 100644
--- a/BEdit/src/bedit/gui/graphics/icon_renderer.py
+++ b/BEdit/src/bedit/gui/graphics/icon_renderer.py
@@ -13,14 +13,14 @@ def icon_bounds(icon: Icon) -> QRectF:
rect = QRectF(
float(element.get("x", 0)),
float(element.get("y", 0)),
- max(0.0, float(element.get("width", 0))),
- max(0.0, float(element.get("height", 0))),
- )
+ float(element.get("width", 0)),
+ float(element.get("height", 0)),
+ ).normalized()
bounds = rect if bounds.isNull() else bounds.united(rect)
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
-def _pen(element: dict) -> QPen:
+def shape_pen(element: dict) -> QPen:
styles = {
"solid": Qt.PenStyle.SolidLine,
"dash": Qt.PenStyle.DashLine,
@@ -51,12 +51,16 @@ def paint_icon(
for element in icon.elements:
kind = element.get("type", "rectangle")
rect = QRectF(
- float(element.get("x", 0)), float(element.get("y", 0)),
- float(element.get("width", 20)), float(element.get("height", 20)),
+ float(element.get("x", 0)),
+ float(element.get("y", 0)),
+ float(element.get("width", 20)),
+ float(element.get("height", 20)),
)
- painter.setPen(_pen(element))
+ painter.setPen(shape_pen(element))
fill = element.get("fill", "#ffffff")
- painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
+ painter.setBrush(
+ Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
+ )
if kind == "rectangle":
radius = float(element.get("cornerRadius", 0))
painter.drawRoundedRect(rect, radius, radius)
@@ -65,7 +69,11 @@ def paint_icon(
elif kind == "line":
painter.drawLine(rect.topLeft(), rect.bottomRight())
elif kind == "triangle":
- painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]))
+ painter.drawPolygon(
+ QPolygonF(
+ [QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]
+ )
+ )
elif kind == "text":
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
font = QFont()
diff --git a/BEdit/src/bedit/gui/graphics/workspace.py b/BEdit/src/bedit/gui/graphics/workspace.py
index eb0cf8e..734b175 100644
--- a/BEdit/src/bedit/gui/graphics/workspace.py
+++ b/BEdit/src/bedit/gui/graphics/workspace.py
@@ -24,15 +24,18 @@ from PySide6.QtWidgets import (
QGraphicsView,
QApplication,
QMenu,
+ QInputDialog,
QStyleOptionGraphicsItem,
QToolTip,
QWidget,
)
-from bedit.core.model import Component, Connection, Endpoint, Port
+from bedit.core.model import Annotation, Component, Connection, Endpoint, Port
from bedit.gui.controllers.document import DocumentController
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
from bedit.gui.graphics.icon_renderer import icon_bounds, paint_icon
+from bedit.gui.graphics.icon_editor import ShapeOptionsDialog
+from bedit.gui.graphics.icon_renderer import shape_pen
from bedit.gui.graphics.connection_styles import ConnectionStyle, connection_style
from bedit.gui.preferences import application_settings
@@ -94,7 +97,9 @@ class ComponentGraphicsItem(QGraphicsObject):
position = port.properties.get("iconPosition", {})
item.setPos(
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
- float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
+ float(position.get("y", spacing * index))
+ * self.HEIGHT
+ / self.component.icon.height,
)
result[port.id] = item
return result
@@ -212,6 +217,9 @@ class InterfaceTerminalItem(QGraphicsObject):
super().mousePressEvent(event)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
+ if self.index < 0:
+ event.accept()
+ return
menu = QMenu()
options_action = menu.addAction(f"{self.direction.title()} Options…")
if menu.exec(event.screenPos()) is options_action:
@@ -253,13 +261,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.end_direction = QPointF(1, 0)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self._update_pen()
- self.setZValue(-1)
+ self.setZValue(0)
self.setToolTip(name or "Connection")
+ self.handles: list[WaypointHandle] = []
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
+ if not self.isSelected():
+ self.scene().clearSelection()
+ self.setSelected(True)
menu = QMenu()
+ add_node_action = menu.addAction("Add Node")
+ menu.addSeparator()
options_action = menu.addAction("Connection Options…")
- if menu.exec(event.screenPos()) is options_action:
+ selected = menu.exec(event.screenPos())
+ if selected is add_node_action:
+ scene = self.scene()
+ if isinstance(scene, GraphScene):
+ scene.add_route_node("connection", self.connection_id, event.scenePos())
+ elif selected is options_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.connectionOptionsRequested.emit(self.connection_id)
@@ -269,6 +288,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
result = super().itemChange(change, value)
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
self._update_pen()
+ self._update_handles()
return result
def _update_pen(self) -> None:
@@ -291,6 +311,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2]
+ def set_waypoints(self, points: list[QPointF]) -> None:
+ for handle in self.handles:
+ if handle.scene() is not None:
+ handle.scene().removeItem(handle)
+ self.handles = [
+ WaypointHandle("connection", self.connection_id, index, point)
+ for index, point in enumerate(points)
+ ]
+ scene = self.scene()
+ if scene is not None:
+ for handle in self.handles:
+ scene.addItem(handle)
+ self._update_handles()
+
+ def _update_handles(self) -> None:
+ for handle in self.handles:
+ handle.setVisible(self.isSelected())
+
@staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
@@ -306,7 +344,211 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
if self.style.arrow_at_target:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
if self.style.arrow_at_source:
- painter.drawPolygon(self._arrow(self.start, -self.start_direction, self.style.arrow_size))
+ painter.drawPolygon(
+ self._arrow(self.start, -self.start_direction, self.style.arrow_size)
+ )
+
+
+class WaypointHandle(QGraphicsEllipseItem):
+ def __init__(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
+ super().__init__(-5, -5, 10, 10)
+ self.item_kind, self.item_id, self.index = item_kind, item_id, index
+ self.setPos(position)
+ self.setBrush(QColor("#ffffff"))
+ self.setPen(QPen(QColor("#2563eb"), 1.5))
+ self.setZValue(1000)
+ self.setFlags(
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable
+ | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
+ )
+ self.setCursor(Qt.CursorShape.SizeAllCursor)
+
+ def itemChange(self, change, value): # noqa: N802
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
+ snapped = _snapped(value)
+ scene = self.scene()
+ if isinstance(scene, GraphScene):
+ scene.preview_route_node(self.item_kind, self.item_id, self.index, snapped)
+ return snapped
+ return super().itemChange(change, value)
+
+ def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ super().mouseReleaseEvent(event)
+ scene = self.scene()
+ if isinstance(scene, GraphScene):
+ scene.move_route_node(self.item_kind, self.item_id, self.index, self.pos())
+
+ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
+ menu = QMenu()
+ delete_action = menu.addAction("Delete Node")
+ if menu.exec(event.screenPos()) is delete_action:
+ scene = self.scene()
+ if isinstance(scene, GraphScene):
+ scene.delete_route_node(self.item_kind, self.item_id, self.index)
+ event.accept()
+
+
+class AnnotationGraphicsItem(QGraphicsObject):
+ def __init__(self, annotation: Annotation, controller: DocumentController) -> None:
+ super().__init__()
+ self.annotation = annotation
+ self.annotation_id = annotation.id
+ self.controller = controller
+ self.drag_start = QPointF(annotation.x, annotation.y)
+ self.setPos(annotation.x, annotation.y)
+ self.setZValue(annotation.layer)
+ self.setFlags(
+ QGraphicsItem.GraphicsItemFlag.ItemIsMovable
+ | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
+ | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
+ )
+
+ def boundingRect(self) -> QRectF: # noqa: N802
+ width, height = self.annotation.width, self.annotation.height
+ return QRectF(min(0, width), min(0, height), abs(width), abs(height)).adjusted(-4, -4, 4, 4)
+
+ def paint(self, painter: QPainter, option, widget=None) -> None:
+ del option, widget
+ rect = QRectF(0, 0, self.annotation.width, self.annotation.height).normalized()
+ painter.setPen(shape_pen(self.annotation.properties))
+ fill = self.annotation.properties.get("fill", "none")
+ painter.setBrush(
+ Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
+ )
+ if self.annotation.kind == "box":
+ radius = float(self.annotation.properties.get("cornerRadius", 0))
+ painter.drawRoundedRect(rect, radius, radius)
+ else:
+ painter.setPen(
+ QColor(
+ self.annotation.properties.get(
+ "color", self.annotation.properties.get("stroke", "#202020")
+ )
+ )
+ )
+ font = painter.font()
+ font.setPointSizeF(float(self.annotation.properties.get("fontSize", 12)))
+ painter.setFont(font)
+ painter.drawText(
+ rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, self.annotation.text
+ )
+ if self.isSelected():
+ painter.setBrush(Qt.BrushStyle.NoBrush)
+ painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
+ painter.drawRect(self.boundingRect())
+
+ def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ self.drag_start = self.pos()
+ super().mousePressEvent(event)
+
+ def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ super().mouseReleaseEvent(event)
+ new_pos = _snapped(self.pos())
+ self.setPos(new_pos)
+ old = {
+ "x": self.drag_start.x(),
+ "y": self.drag_start.y(),
+ "width": self.annotation.width,
+ "height": self.annotation.height,
+ }
+ new = {**old, "x": new_pos.x(), "y": new_pos.y()}
+ self.controller.set_annotation_geometry(self.annotation_id, old, new)
+
+ def itemChange(self, change, value): # noqa: N802
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
+ return _snapped(value)
+ return super().itemChange(change, value)
+
+ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
+ scene = self.scene()
+ if not isinstance(scene, GraphScene):
+ return
+ if not self.isSelected():
+ scene.clearSelection()
+ self.setSelected(True)
+ menu = QMenu()
+ options_action = menu.addAction("Shape Options…")
+ menu.addSeparator()
+ actions = {
+ menu.addAction("Bring Forward"): "forward",
+ menu.addAction("Send Backward"): "backward",
+ menu.addAction("Bring to Front"): "front",
+ menu.addAction("Send to Back"): "back",
+ }
+ selected = menu.exec(event.screenPos())
+ if selected is options_action:
+ scene.edit_annotation_options(self.annotation_id)
+ elif selected in actions:
+ scene.reorder_selected_annotations(actions[selected])
+ event.accept()
+
+
+class LineAnnotationGraphicsItem(QGraphicsPathItem):
+ def __init__(self, annotation: Annotation) -> None:
+ super().__init__()
+ self.annotation = annotation
+ self.annotation_id = annotation.id
+ self.handles: list[WaypointHandle] = []
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
+ self.setZValue(annotation.layer)
+ self._update_pen()
+
+ def _update_pen(self) -> None:
+ pen = shape_pen(self.annotation.properties)
+ if self.isSelected():
+ pen.setColor(QColor("#2563eb"))
+ pen.setWidthF(max(3.0, pen.widthF()))
+ self.setPen(pen)
+
+ def set_waypoints(self, points: list[QPointF], start: QPointF, end: QPointF) -> None:
+ for handle in self.handles:
+ if handle.scene() is not None:
+ handle.scene().removeItem(handle)
+ self.handles = [WaypointHandle("annotation", self.annotation_id, -1, start)]
+ self.handles.extend(
+ WaypointHandle("annotation", self.annotation_id, index, point)
+ for index, point in enumerate(points)
+ )
+ self.handles.append(WaypointHandle("annotation", self.annotation_id, -2, end))
+ scene = self.scene()
+ if scene is not None:
+ for handle in self.handles:
+ scene.addItem(handle)
+ for handle in self.handles:
+ handle.setVisible(self.isSelected())
+
+ def itemChange(self, change, value): # noqa: N802
+ result = super().itemChange(change, value)
+ if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
+ for handle in self.handles:
+ handle.setVisible(self.isSelected())
+ self._update_pen()
+ return result
+
+ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
+ if not self.isSelected():
+ self.scene().clearSelection()
+ self.setSelected(True)
+ menu = QMenu()
+ add_action = menu.addAction("Add Node")
+ options_action = menu.addAction("Shape Options…")
+ menu.addSeparator()
+ actions = {
+ menu.addAction("Bring Forward"): "forward",
+ menu.addAction("Send Backward"): "backward",
+ menu.addAction("Bring to Front"): "front",
+ menu.addAction("Send to Back"): "back",
+ }
+ selected = menu.exec(event.screenPos())
+ scene = self.scene()
+ if isinstance(scene, GraphScene):
+ if selected is add_action:
+ scene.add_route_node("annotation", self.annotation_id, event.scenePos())
+ elif selected is options_action:
+ scene.edit_annotation_options(self.annotation_id)
+ elif selected in actions:
+ scene.reorder_selected_annotations(actions[selected])
+ event.accept()
class GraphScene(QGraphicsScene):
@@ -322,17 +564,21 @@ class GraphScene(QGraphicsScene):
self.input_items: dict[str, InterfaceTerminalItem] = {}
self.output_items: dict[str, InterfaceTerminalItem] = {}
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
+ self.annotation_items: dict[str, QGraphicsItem] = {}
self.pending_source: ConnectionPortItem | None = None
self.pending_waypoints: list[QPointF] = []
self.pending_preview: QGraphicsPathItem | None = None
self.interaction_mode = "pointer"
- self.connection_routing = "spline"
+ self.connection_routing = "angled"
+ self.drawing_start: QPointF | None = None
+ self.drawing_waypoints: list[QPointF] = []
self.setSceneRect(-2000, -2000, 4000, 4000)
controller.documentReset.connect(self.rebuild)
controller.activeGraphChanged.connect(self.rebuild)
controller.componentMoved.connect(self.set_component_position)
controller.componentRotated.connect(self.set_component_rotation)
+ controller.graphItemChanged.connect(self.refresh_graph_item)
self.rebuild()
def rebuild(self) -> None:
@@ -341,6 +587,7 @@ class GraphScene(QGraphicsScene):
self.input_items.clear()
self.output_items.clear()
self.connection_items.clear()
+ self.annotation_items.clear()
self.pending_source = None
self.pending_waypoints.clear()
self.pending_preview = None
@@ -371,6 +618,16 @@ class GraphScene(QGraphicsScene):
self.addItem(item)
self.connection_items[connection.id] = item
self.update_connection(connection.id)
+ for annotation in owner.graph.annotations.values():
+ if annotation.kind == "line":
+ item = LineAnnotationGraphicsItem(annotation)
+ self.addItem(item)
+ self.annotation_items[annotation.id] = item
+ self.update_annotation(annotation.id)
+ else:
+ item = AnnotationGraphicsItem(annotation, self.controller)
+ self.addItem(item)
+ self.annotation_items[annotation.id] = item
def set_component_position(self, component_id: str, position: QPointF) -> None:
item = self.component_items.get(component_id)
@@ -383,6 +640,52 @@ class GraphScene(QGraphicsScene):
item.setRotation(rotation)
self.update_connections_for_block(component_id)
+ def refresh_graph_item(self, item_kind: str, item_id: str) -> None:
+ if item_kind == "connection":
+ self.update_connection(item_id)
+ return
+ annotation = self.controller.active_graph.annotations.get(item_id)
+ graphics = self.annotation_items.get(item_id)
+ if annotation is None or graphics is None:
+ return
+ graphics.setZValue(annotation.layer)
+ if isinstance(graphics, LineAnnotationGraphicsItem):
+ graphics._update_pen()
+ self.update_annotation(item_id)
+ elif isinstance(graphics, AnnotationGraphicsItem):
+ graphics.prepareGeometryChange()
+ graphics.setPos(annotation.x, annotation.y)
+ graphics.update()
+
+ def edit_annotation_options(self, annotation_id: str) -> None:
+ annotation = self.controller.active_graph.annotations.get(annotation_id)
+ if annotation is None:
+ return
+ element = {
+ "type": {"box": "rectangle", "line": "line", "text": "text"}[annotation.kind],
+ "width": abs(annotation.width),
+ "height": abs(annotation.height),
+ "text": annotation.text,
+ **annotation.properties,
+ }
+ dialog = ShapeOptionsDialog(element)
+ if dialog.exec() != dialog.DialogCode.Accepted:
+ return
+ values = annotation.to_dict()
+ values["size"] = {
+ "width": dialog.element["width"] * (-1 if annotation.width < 0 else 1),
+ "height": dialog.element["height"] * (-1 if annotation.height < 0 else 1),
+ }
+ values["text"] = str(dialog.element.get("text", annotation.text))
+ values["properties"].update(
+ {
+ key: value
+ for key, value in dialog.element.items()
+ if key not in {"type", "x", "y", "width", "height", "text"}
+ }
+ )
+ self.controller.edit_annotation(annotation_id, values)
+
def update_connections_for_block(self, component_id: str) -> None:
for connection in self.controller.active_graph.connections.values():
if component_id in (connection.source.block, connection.target.block):
@@ -418,7 +721,7 @@ class GraphScene(QGraphicsScene):
if source is None or target is None:
return
start, end = source.scenePos(), target.scenePos()
- routing = str(connection.properties.get("routing", "spline"))
+ routing = str(connection.properties.get("routing", "angled"))
waypoints = [
QPointF(float(point["x"]), float(point["y"]))
for point in connection.properties.get("waypoints", [])
@@ -426,6 +729,35 @@ class GraphScene(QGraphicsScene):
]
path, direction_points = self._route_path(start, end, routing, waypoints)
graphics.set_connection_path(path, direction_points)
+ graphics.set_waypoints(waypoints)
+
+ def update_annotation(self, annotation_id: str) -> None:
+ annotation = self.controller.active_graph.annotations.get(annotation_id)
+ graphics = self.annotation_items.get(annotation_id)
+ if annotation is None or not isinstance(graphics, LineAnnotationGraphicsItem):
+ return
+ start = QPointF(annotation.x, annotation.y)
+ end = start + QPointF(annotation.width, annotation.height)
+ waypoints = [
+ QPointF(float(p["x"]), float(p["y"]))
+ for p in annotation.properties.get("waypoints", [])
+ ]
+ routing = str(annotation.properties.get("routing", "angled"))
+ path, points = self._route_path(start, end, routing, waypoints)
+ graphics.setPath(path)
+ graphics.set_waypoints(waypoints, start, end)
+
+ @staticmethod
+ def _orthogonal_points(start: QPointF, end: QPointF, waypoints: list[QPointF]) -> list[QPointF]:
+ points = [start]
+ for waypoint in [*waypoints, end]:
+ previous = points[-1]
+ if previous.x() != waypoint.x() and previous.y() != waypoint.y():
+ elbow = QPointF(waypoint.x(), previous.y())
+ points.append(elbow)
+ if waypoint != points[-1]:
+ points.append(waypoint)
+ return points
@staticmethod
def _route_path(
@@ -437,16 +769,23 @@ class GraphScene(QGraphicsScene):
waypoints = waypoints or []
path = QPainterPath(start)
if routing == "angled":
- points = [start, *waypoints, end]
+ points = GraphScene._orthogonal_points(start, end, waypoints)
for point in points[1:]:
path.lineTo(point)
return path, points
if routing == "direct":
path.lineTo(end)
return path, [start, end]
+ if waypoints:
+ points = [start, *waypoints, end]
+ for index, point in enumerate(points[1:-1], start=1):
+ following = points[index + 1]
+ midpoint = QPointF((point.x() + following.x()) / 2, (point.y() + following.y()) / 2)
+ path.quadTo(point, midpoint)
+ path.quadTo(points[-2], end)
+ return path, points
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
- first_control = start + QPointF(distance, 0)
- second_control = end - QPointF(distance, 0)
+ first_control, second_control = start + QPointF(distance, 0), end - QPointF(distance, 0)
path.cubicTo(first_control, second_control, end)
return path, [start, first_control, second_control, end]
@@ -454,12 +793,16 @@ class GraphScene(QGraphicsScene):
self.interaction_mode = mode
if mode != "connect":
self._clear_pending_source()
+ self._clear_drawing()
def set_connection_routing(self, routing: str) -> None:
self.connection_routing = routing
self._clear_pending_source()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ if self.interaction_mode in {"box", "text", "line"}:
+ self._drawing_press(event)
+ return
if self.interaction_mode != "connect":
self._clear_pending_source()
super().mousePressEvent(event)
@@ -483,7 +826,15 @@ class GraphScene(QGraphicsScene):
self.pending_source.endpoint,
item.endpoint,
routing=self.connection_routing,
- waypoints=self.pending_waypoints,
+ waypoints=(
+ self._completed_angled_waypoints(
+ self.pending_source.scenePos(),
+ item.scenePos(),
+ self.pending_waypoints,
+ )
+ if self.connection_routing == "angled"
+ else self.pending_waypoints
+ ),
)
except ValueError as error:
QToolTip.showText(event.screenPos(), str(error))
@@ -500,8 +851,222 @@ class GraphScene(QGraphicsScene):
self._update_preview(event.scenePos())
event.accept()
return
+ if self.interaction_mode in {"box", "line"} and self.drawing_start is not None:
+ self._update_drawing_preview(event.scenePos())
+ event.accept()
+ return
super().mouseMoveEvent(event)
+ def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ if self.interaction_mode == "box" and self.drawing_start is not None:
+ end = _snapped(event.scenePos())
+ if end != self.drawing_start:
+ self.controller.add_annotation("box", self.drawing_start, end)
+ self._clear_drawing()
+ event.accept()
+ return
+ super().mouseReleaseEvent(event)
+
+ def _drawing_press(self, event: QGraphicsSceneMouseEvent) -> None:
+ if event.button() == Qt.MouseButton.RightButton:
+ self._clear_drawing()
+ event.accept()
+ return
+ point = _snapped(event.scenePos())
+ if self.interaction_mode == "text":
+ text_value, accepted = QInputDialog.getText(None, "Add Text", "Text:")
+ if accepted and text_value:
+ self.controller.add_annotation(
+ "text", point, point + QPointF(160, 48), text=text_value
+ )
+ event.accept()
+ return
+ if self.drawing_start is None:
+ self.drawing_start = point
+ self.drawing_waypoints = []
+ self._create_preview()
+ elif self.interaction_mode == "line" and self.connection_routing == "angled":
+ self.drawing_waypoints.append(point)
+ self._update_drawing_preview(point)
+ elif self.interaction_mode == "line":
+ self.controller.add_annotation(
+ "line", self.drawing_start, point, routing=self.connection_routing
+ )
+ self._clear_drawing()
+ event.accept()
+
+ def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
+ if self.interaction_mode == "line" and self.drawing_start is not None:
+ end = _snapped(event.scenePos())
+ waypoints = (
+ self.drawing_waypoints[:-1]
+ if self.drawing_waypoints and self.drawing_waypoints[-1] == end
+ else self.drawing_waypoints
+ )
+ completed = self._completed_angled_waypoints(self.drawing_start, end, waypoints)
+ self.controller.add_annotation(
+ "line", self.drawing_start, end, routing="angled", waypoints=completed
+ )
+ self._clear_drawing()
+ event.accept()
+ return
+ super().mouseDoubleClickEvent(event)
+
+ @staticmethod
+ def _completed_angled_waypoints(
+ start: QPointF, end: QPointF, waypoints: list[QPointF]
+ ) -> list[QPointF]:
+ return GraphScene._orthogonal_points(start, end, waypoints)[1:-1]
+
+ def _update_drawing_preview(self, cursor: QPointF) -> None:
+ if self.drawing_start is None or self.pending_preview is None:
+ return
+ end = _snapped(cursor)
+ if self.interaction_mode == "box":
+ path = QPainterPath()
+ path.addRect(QRectF(self.drawing_start, end).normalized())
+ else:
+ path, _ = self._route_path(
+ self.drawing_start, end, self.connection_routing, self.drawing_waypoints
+ )
+ self.pending_preview.setPath(path)
+
+ def _clear_drawing(self) -> None:
+ if self.pending_preview is not None and self.pending_source is None:
+ self.removeItem(self.pending_preview)
+ self.pending_preview = None
+ self.drawing_start = None
+ self.drawing_waypoints.clear()
+
+ def _route_values(self, item_kind: str, item_id: str):
+ item = (
+ self.controller.active_graph.connections
+ if item_kind == "connection"
+ else self.controller.active_graph.annotations
+ ).get(item_id)
+ if item is None:
+ return None, []
+ points = [
+ QPointF(float(p["x"]), float(p["y"])) for p in item.properties.get("waypoints", [])
+ ]
+ return item, points
+
+ def add_route_node(self, item_kind: str, item_id: str, position: QPointF) -> None:
+ item, points = self._route_values(item_kind, item_id)
+ if item is None:
+ return
+ snapped = _snapped(position)
+ if item_kind == "annotation":
+ start = QPointF(item.x, item.y)
+ end = QPointF(item.x + item.width, item.y + item.height)
+ else:
+ graphics = self.connection_items[item_id]
+ start, end = graphics.start, graphics.end
+ anchors = [start, *points, end]
+
+ def segment_distance(point: QPointF, first: QPointF, second: QPointF) -> float:
+ delta = second - first
+ length_squared = delta.x() ** 2 + delta.y() ** 2
+ if length_squared == 0:
+ return (point - first).manhattanLength()
+ ratio = max(
+ 0.0,
+ min(
+ 1.0,
+ ((point.x() - first.x()) * delta.x() + (point.y() - first.y()) * delta.y())
+ / length_squared,
+ ),
+ )
+ nearest = first + delta * ratio
+ return (point.x() - nearest.x()) ** 2 + (point.y() - nearest.y()) ** 2
+
+ insertion = min(
+ range(len(anchors) - 1),
+ key=lambda index: segment_distance(snapped, anchors[index], anchors[index + 1]),
+ )
+ points.insert(insertion, snapped)
+ routing = "angled" if item.properties.get("routing") == "direct" else None
+ self.controller.set_route_waypoints(item_kind, item_id, points, routing)
+
+ def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
+ item, points = self._route_values(item_kind, item_id)
+ if item is None:
+ return
+ position = _snapped(position)
+ if item_kind == "annotation" and index in {-1, -2}:
+ old = {
+ "x": item.x,
+ "y": item.y,
+ "width": item.width,
+ "height": item.height,
+ }
+ start = position if index == -1 else QPointF(item.x, item.y)
+ end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height)
+ new = {
+ "x": start.x(),
+ "y": start.y(),
+ "width": end.x() - start.x(),
+ "height": end.y() - start.y(),
+ }
+ self.controller.set_annotation_geometry(item_id, old, new)
+ return
+ if index < len(points):
+ points[index] = position
+ self.controller.set_route_waypoints(item_kind, item_id, points)
+
+ def preview_route_node(
+ self, item_kind: str, item_id: str, index: int, position: QPointF
+ ) -> None:
+ item, points = self._route_values(item_kind, item_id)
+ if item is None:
+ return
+ if item_kind == "annotation" and index in {-1, -2}:
+ graphics = self.annotation_items.get(item_id)
+ if not isinstance(graphics, LineAnnotationGraphicsItem):
+ return
+ start = position if index == -1 else QPointF(item.x, item.y)
+ end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height)
+ path, _direction_points = self._route_path(
+ start,
+ end,
+ str(item.properties.get("routing", "angled")),
+ points,
+ )
+ graphics.setPath(path)
+ return
+ if index >= len(points):
+ return
+ points[index] = position
+ routing = str(item.properties.get("routing", "angled"))
+ if item_kind == "connection":
+ graphics = self.connection_items.get(item_id)
+ if graphics is None:
+ return
+ path, direction_points = self._route_path(graphics.start, graphics.end, routing, points)
+ graphics.set_connection_path(path, direction_points)
+ return
+ graphics = self.annotation_items.get(item_id)
+ if not isinstance(graphics, LineAnnotationGraphicsItem):
+ return
+ start = QPointF(item.x, item.y)
+ end = QPointF(item.x + item.width, item.y + item.height)
+ path, _direction_points = self._route_path(start, end, routing, points)
+ graphics.setPath(path)
+
+ def delete_route_node(self, item_kind: str, item_id: str, index: int) -> None:
+ _item, points = self._route_values(item_kind, item_id)
+ if 0 <= index < len(points):
+ points.pop(index)
+ self.controller.set_route_waypoints(item_kind, item_id, points)
+
+ def reorder_selected_annotations(self, operation: str) -> None:
+ ids = {
+ item.annotation_id
+ for item in self.selectedItems()
+ if isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem))
+ }
+ self.controller.reorder_annotations(ids, operation)
+
def _create_preview(self) -> None:
self.pending_preview = QGraphicsPathItem()
self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
@@ -511,9 +1076,10 @@ class GraphScene(QGraphicsScene):
def _update_preview(self, cursor: QPointF) -> None:
if self.pending_source is None or self.pending_preview is None:
return
+ preview_cursor = _snapped(cursor) if self.connection_routing == "angled" else cursor
path, _points = self._route_path(
self.pending_source.scenePos(),
- cursor,
+ preview_cursor,
self.connection_routing,
self.pending_waypoints,
)
@@ -622,6 +1188,7 @@ class GraphWorkspaceView(QGraphicsView):
connections: set[str] = set()
inputs: set[str] = set()
outputs: set[str] = set()
+ annotations: set[str] = set()
for item in self.scene().selectedItems():
if isinstance(item, ComponentGraphicsItem):
blocks.add(item.component_id)
@@ -629,32 +1196,29 @@ class GraphWorkspaceView(QGraphicsView):
connections.add(item.connection_id)
elif isinstance(item, InterfaceTerminalItem):
(inputs if item.direction == "input" else outputs).add(item.port.id)
+ elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
+ annotations.add(item.annotation_id)
self.controller.delete_selection(blocks, connections, inputs, outputs)
+ self.controller.delete_annotations(annotations)
def has_selected_components(self) -> bool:
scene = self.scene()
return bool(
- scene
- and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
+ scene and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
)
def has_single_selected_component(self) -> bool:
scene = self.scene()
return bool(
scene
- and sum(
- isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()
- )
- == 1
+ and sum(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) == 1
)
def open_selected_component(self) -> bool:
if self.controller is None or self.scene() is None:
return False
selected = [
- item
- for item in self.scene().selectedItems()
- if isinstance(item, ComponentGraphicsItem)
+ item for item in self.scene().selectedItems() if isinstance(item, ComponentGraphicsItem)
]
if len(selected) != 1:
return False
diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py
index a843e3c..d1396b7 100644
--- a/BEdit/src/bedit/gui/main_window.py
+++ b/BEdit/src/bedit/gui/main_window.py
@@ -4,7 +4,14 @@ from pathlib import Path
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QCloseEvent
-from PySide6.QtWidgets import QButtonGroup, QFileDialog, QMainWindow, QMenu, QMessageBox
+from PySide6.QtWidgets import (
+ QButtonGroup,
+ QFileDialog,
+ QMainWindow,
+ QMenu,
+ QMessageBox,
+ QTabWidget,
+)
from bedit.core.model import Component, Port
from bedit.core.serializer import JsonDocumentSerializer
@@ -51,11 +58,11 @@ class MainWindow(QMainWindow):
self.ui.leftDockHost.show()
self.ui.panel_libraries.show()
self.ui.panel_document.show()
- self.ui.leftDockHost.splitDockWidget(
- self.ui.panel_document,
- self.ui.panel_libraries,
- Qt.Orientation.Vertical,
+ self.ui.leftDockHost.setTabPosition(
+ Qt.DockWidgetArea.LeftDockWidgetArea, QTabWidget.TabPosition.North
)
+ self.ui.leftDockHost.tabifyDockWidget(self.ui.panel_document, self.ui.panel_libraries)
+ self.ui.panel_document.raise_()
self.ui.workspaceSplitter.setSizes([280, 720])
self.reload_libraries()
self._active_graph_changed()
@@ -67,22 +74,14 @@ class MainWindow(QMainWindow):
self.ui.treeView.setDragEnabled(True)
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
- self.ui.treeView.customContextMenuRequested.connect(
- self.show_external_library_context_menu
- )
+ self.ui.treeView.customContextMenuRequested.connect(self.show_external_library_context_menu)
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
self.ui.documentTreeView.setModel(self.document_tree_model)
self.ui.documentTreeView.setHeaderHidden(True)
self.ui.documentTreeView.setDragEnabled(True)
- self.ui.documentTreeView.setDragDropMode(
- self.ui.documentTreeView.DragDropMode.DragOnly
- )
- self.ui.documentTreeView.setContextMenuPolicy(
- Qt.ContextMenuPolicy.CustomContextMenu
- )
- self.ui.documentTreeView.customContextMenuRequested.connect(
- self.show_library_context_menu
- )
+ self.ui.documentTreeView.setDragDropMode(self.ui.documentTreeView.DragDropMode.DragOnly)
+ self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+ self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
self.ui.graphView.set_model(self.document_controller)
@@ -97,6 +96,9 @@ class MainWindow(QMainWindow):
self.mode_button_group.setExclusive(True)
self.mode_button_group.addButton(self.ui.pointerToolButton)
self.mode_button_group.addButton(self.ui.connectToolButton)
+ self.mode_button_group.addButton(self.ui.boxToolButton)
+ self.mode_button_group.addButton(self.ui.lineToolButton)
+ self.mode_button_group.addButton(self.ui.textToolButton)
self.routing_button_group = QButtonGroup(self)
self.routing_button_group.setExclusive(True)
for button in (
@@ -109,15 +111,13 @@ class MainWindow(QMainWindow):
self.ui.navigateDownButton.clicked.connect(self.navigate_down)
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
self.ui.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect"))
- self.ui.directRoutingButton.clicked.connect(
- lambda: self.set_connection_routing("direct")
- )
- self.ui.angledRoutingButton.clicked.connect(
- lambda: self.set_connection_routing("angled")
- )
- self.ui.splineRoutingButton.clicked.connect(
- lambda: self.set_connection_routing("spline")
- )
+ self.ui.boxToolButton.clicked.connect(lambda: self.set_graph_tool("box"))
+ self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
+ self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
+ self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
+ self.ui.directRoutingButton.clicked.connect(lambda: self.set_connection_routing("direct"))
+ self.ui.angledRoutingButton.clicked.connect(lambda: self.set_connection_routing("angled"))
+ self.ui.splineRoutingButton.clicked.connect(lambda: self.set_connection_routing("spline"))
self.ui.applyJsonButton.clicked.connect(self.apply_json)
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
@@ -160,7 +160,6 @@ class MainWindow(QMainWindow):
for toolbar in (
self.ui.fileToolbar,
self.ui.editToolbar,
- self.ui.transformToolbar,
self.ui.cameraToolbar,
):
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
@@ -184,7 +183,11 @@ class MainWindow(QMainWindow):
if self.document_controller.document is None:
self.setWindowTitle("BEdit")
return
- name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled"
+ name = (
+ self.document_controller.file_path.name
+ if self.document_controller.file_path
+ else "Untitled"
+ )
modified = "*" if not self.document_controller.undo_stack.isClean() else ""
self.setWindowTitle(f"{modified}{name} — BEdit")
@@ -217,12 +220,17 @@ class MainWindow(QMainWindow):
def _update_edit_actions(self) -> None:
component = self.document_controller.active_component
is_graph = component is not None and component.implementation_kind == "graph"
- has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems())
+ has_selection = bool(
+ self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
+ )
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
action.setEnabled(is_graph and has_selection)
self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components()
)
+ self.ui.rotateToolButton.setEnabled(
+ is_graph and self.ui.graphView.has_selected_components()
+ )
self.ui.actionSelectAll.setEnabled(is_graph)
self.ui.actionPaste.setEnabled(is_graph)
self.ui.navigateDownButton.setEnabled(
@@ -243,6 +251,10 @@ class MainWindow(QMainWindow):
for widget in (
self.ui.pointerToolButton,
self.ui.connectToolButton,
+ self.ui.boxToolButton,
+ self.ui.lineToolButton,
+ self.ui.textToolButton,
+ self.ui.rotateToolButton,
self.ui.routingLabel,
self.ui.directRoutingButton,
self.ui.angledRoutingButton,
@@ -252,7 +264,13 @@ class MainWindow(QMainWindow):
def set_graph_tool(self, mode: str) -> None:
self.ui.graphView.set_tool_mode(mode)
- (self.ui.pointerToolButton if mode == "pointer" else self.ui.connectToolButton).setChecked(True)
+ {
+ "pointer": self.ui.pointerToolButton,
+ "connect": self.ui.connectToolButton,
+ "box": self.ui.boxToolButton,
+ "line": self.ui.lineToolButton,
+ "text": self.ui.textToolButton,
+ }[mode].setChecked(True)
def set_connection_routing(self, routing: str) -> None:
self.ui.graphView.set_connection_routing(routing)
@@ -312,9 +330,7 @@ class MainWindow(QMainWindow):
raise ValueError("'source' must be an object")
inputs = [Port.from_dict(item) for item in data["inputs"]]
outputs = [Port.from_dict(item) for item in data["outputs"]]
- self.document_controller.replace_active_text_definition(
- inputs, outputs, data["source"]
- )
+ self.document_controller.replace_active_text_definition(inputs, outputs, data["source"])
except (TypeError, ValueError, json.JSONDecodeError) as error:
QMessageBox.critical(self, "Invalid text component JSON", str(error))
return False
@@ -497,9 +513,7 @@ class MainWindow(QMainWindow):
try:
if library is not None:
library.document.validate()
- JsonDocumentSerializer.save(
- library.document, Path(library.source_path)
- )
+ JsonDocumentSerializer.save(library.document, Path(library.source_path))
except (OSError, ValueError) as error:
component.inputs, component.outputs = old_inputs, old_outputs
QMessageBox.warning(self, "Cannot change library ports", str(error))
@@ -559,9 +573,7 @@ class MainWindow(QMainWindow):
connection = owner.graph.connections.get(connection_id)
if connection is None:
return
- dialog = ItemOptionsDialog(
- "Connection Options", connection.name, self, name_required=False
- )
+ dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.rename_connection(connection_id, dialog.name)
diff --git a/BEdit/ui/icon_editor_dialog.ui b/BEdit/ui/icon_editor_dialog.ui
index 15d49a1..c59f54a 100644
--- a/BEdit/ui/icon_editor_dialog.ui
+++ b/BEdit/ui/icon_editor_dialog.ui
@@ -7,14 +7,18 @@
-
+
- Pointertruetrue
- Add:
- - Rectangle
- - Circle
- - Ellipse
- - Line
- - Triangle
- - Text
+ - Rectangletrue
+ - Circletrue
+ - Ellipsetrue
+ - Linetrue
+ - Triangletrue
+ - Texttrue
- Qt::Orientation::Horizontal4020
+ - +Zoom in
+ - −Zoom out
+ - FitFit and center the icon canvas
- Delete selected
diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui
index af6d04a..ab5950d 100644
--- a/BEdit/ui/main_window.ui
+++ b/BEdit/ui/main_window.ui
@@ -6,8 +6,8 @@
0
0
- 1000
- 700
+ 1209
+ 777
@@ -83,11 +83,18 @@
-
- 2828
- QTreeView::item { height: 32px; }
+
+ QTreeView::item { height: 32px; }
+
true
+
+
+ 28
+ 28
+
+
true
@@ -98,22 +105,48 @@
- 22091
+
+ 220
+ 91
+
- Document
- 1
+
+ Document
+
+
+ 1
+
- 0
- 0
- 0
- 0
- 0
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
- 1616
- true
- true
+
+ true
+
+
+
+ 16
+ 16
+
+
+
+ true
+
@@ -128,59 +161,320 @@
- 0
- 0
- 0
- 0
- 0
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
-
- 034
- 1677721534
- QFrame::Shape::StyledPanel
+
+
+ 0
+ 34
+
+
+
+
+ 16777215
+ 34
+
+
+
+ QFrame::Shape::StyledPanel
+
- 6
- 2
- 6
- 2
-
- UpOpen the containing graph
- - DownOpen the selected blockfalse
- - Untitled
- - Graph
- - Qt::Orientation::Horizontal4020
- - Apply JSONfalse
- - Pointertruetrue
- - Connecttrue
- - Line:
- - Directtrue
- - Angledtrue
- - Splinetruetrue
+
+ 6
+
+
+ 2
+
+
+ 6
+
+
+ 2
+
+ -
+
+
+ Open the containing graph
+
+
+ Up
+
+
+
+ :/icons/icons/arrow-up.png:/icons/icons/arrow-up.png
+
+
+
+ -
+
+
+ false
+
+
+ Open the selected block
+
+
+ Down
+
+
+
+ :/icons/icons/arrow-down.png:/icons/icons/arrow-down.png
+
+
+
+ -
+
+
+ Untitled
+
+
+
+ -
+
+
+ Graph
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ false
+
+
+ Apply JSON
+
+
+
+ -
+
+
+ Pointer
+
+
+
+ :/icons/icons/edit-select.png:/icons/icons/edit-select.png
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Connect
+
+
+ true
+
+
+
+ -
+
+
+ Draw a box annotation
+
+
+ Box
+
+
+
+ :/icons/icons/draw-rectangle.png:/icons/icons/draw-rectangle.png
+
+
+ true
+
+
+
+ -
+
+
+ Draw a line annotation
+
+
+ Line
+
+
+
+ :/icons/icons/draw-bezier-curves.png:/icons/icons/draw-bezier-curves.png
+
+
+ true
+
+
+
+ -
+
+
+ Add a text annotation
+
+
+ Text
+
+
+
+ :/icons/icons/draw-text.png:/icons/icons/draw-text.png
+
+
+ true
+
+
+
+ -
+
+
+ Rotate selected blocks clockwise
+
+
+ Rotate
+
+
+
+ :/icons/icons/transform-rotate.png:/icons/icons/transform-rotate.png
+
+
+
+ -
+
+
+ Line:
+
+
+
+ -
+
+
+ Direct
+
+
+ true
+
+
+
+ -
+
+
+ Angled
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Spline
+
+
+ true
+
+
+
-
- 0
+
+ 0
+
- 0000
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
- 0000
- - QPlainTextEdit::LineWrapMode::NoWrapComponent JSON
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QPlainTextEdit::LineWrapMode::NoWrap
+
+
+ Component JSON
+
+
+
- background-color: #9a9a9a;
+
+ background-color: #9a9a9a;
+
-
- background: transparent; color: #202020;
- No document open
- Qt::AlignmentFlag::AlignCenter
+
+ background: transparent; color: #202020;
+
+
+ No document open
+
+
+ Qt::AlignmentFlag::AlignCenter
+
@@ -198,7 +492,7 @@
0
0
- 1000
+ 1209
24
@@ -314,16 +608,16 @@
-
- Transform
- TopToolBarArea
- false
-
-
- Camera
- TopToolBarArea
- false
+
+ Camera
+
+
+ TopToolBarArea
+
+
+ false
+
@@ -348,13 +642,52 @@
:/icons/icons/transform-rotate.png:/icons/icons/transform-rotate.png
- Rotate Clockwise
- Rotate selected blocks clockwise by 90 degrees
- Ctrl+R
+
+ Rotate Clockwise
+
+
+ Rotate selected blocks clockwise by 90 degrees
+
+
+ Ctrl+R
+
+
+
+
+
+ :/icons/icons/zoom-in.png:/icons/icons/zoom-in.png
+
+
+ Zoom In
+
+
+ Ctrl++
+
+
+
+
+
+ :/icons/icons/zoom-out.png:/icons/icons/zoom-out.png
+
+
+ Zoom Out
+
+
+ Ctrl+-
+
+
+
+
+
+ :/icons/icons/zoom-original.png:/icons/icons/zoom-original.png
+
+
+ Center
+
+
+ Ctrl+0
+
- Zoom InCtrl++
- Zoom OutCtrl+-
- CenterCtrl+0
@@ -406,8 +739,12 @@
- &Close Document
- Ctrl+W
+
+ &Close Document
+
+
+ Ctrl+W
+
@@ -478,8 +815,12 @@
- &Delete
- Del
+
+ &Delete
+
+
+ Del
+
diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json
new file mode 100644
index 0000000..9b8454e
--- /dev/null
+++ b/BEdit/untitled.bedit.json
@@ -0,0 +1,355 @@
+{
+ "format": "bedit-document",
+ "version": 1,
+ "metadata": {
+ "name": "Untitled"
+ },
+ "roots": [
+ {
+ "id": "f4a9c769-d49a-46e1-90d8-a8175bf42e9c",
+ "name": "New Graph Block 1",
+ "position": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "rotation": 0.0,
+ "interface": {
+ "inputs": [],
+ "outputs": []
+ },
+ "icon": {
+ "shape": "rectangle",
+ "fill": "#f4f4f4",
+ "border": "#303030",
+ "text": "Graph",
+ "size": {
+ "width": 128.0,
+ "height": 128.0
+ },
+ "elements": [
+ {
+ "type": "rectangle",
+ "x": 32.0,
+ "y": 32.0,
+ "width": 64.0,
+ "height": 64.0,
+ "fill": "#f4f4f4",
+ "stroke": "#303030",
+ "lineWidth": 1.5,
+ "lineStyle": "solid",
+ "cornerRadius": 5.0
+ },
+ {
+ "type": "text",
+ "x": 40.0,
+ "y": 40.0,
+ "width": 48.0,
+ "height": 48.0,
+ "text": "Graph",
+ "color": "#202020",
+ "fontSize": 12.0
+ }
+ ]
+ },
+ "properties": {},
+ "library": {
+ "showSubtree": true
+ },
+ "implementation": {
+ "kind": "graph",
+ "graph": {
+ "blocks": [
+ {
+ "id": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
+ "name": "A",
+ "position": {
+ "x": -192.0,
+ "y": -192.0
+ },
+ "rotation": 0.0,
+ "interface": {
+ "inputs": [
+ {
+ "id": "port-5c5d4695",
+ "name": "Port 1",
+ "position": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "properties": {
+ "iconPosition": {
+ "x": 32.0,
+ "y": 64.0
+ }
+ },
+ "type": "signal"
+ }
+ ],
+ "outputs": [
+ {
+ "id": "port-de109124",
+ "name": "Port 2",
+ "position": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "properties": {
+ "iconPosition": {
+ "x": 96.0,
+ "y": 64.0
+ }
+ },
+ "type": "signal"
+ }
+ ]
+ },
+ "icon": {
+ "shape": "rectangle",
+ "fill": "#f4f4f4",
+ "border": "#303030",
+ "text": "Text",
+ "size": {
+ "width": 128.0,
+ "height": 128.0
+ },
+ "elements": [
+ {
+ "cornerRadius": 5.0,
+ "fill": "#f4f4f4",
+ "height": 64.0,
+ "lineStyle": "solid",
+ "lineWidth": 1.5,
+ "stroke": "#303030",
+ "type": "rectangle",
+ "width": 64.0,
+ "x": 32.0,
+ "y": 32.0
+ },
+ {
+ "color": "#303030",
+ "fill": "#ffffff",
+ "fontSize": 12.0,
+ "height": 48.0,
+ "lineStyle": "solid",
+ "lineWidth": 1.5,
+ "stroke": "#303030",
+ "text": "A",
+ "type": "text",
+ "width": 48.0,
+ "x": 40.0,
+ "y": 40.0
+ }
+ ]
+ },
+ "properties": {},
+ "library": {
+ "showSubtree": true
+ },
+ "implementation": {
+ "kind": "text",
+ "source": {
+ "equations": [],
+ "parameters": {}
+ }
+ }
+ },
+ {
+ "id": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
+ "name": "B",
+ "position": {
+ "x": 128.0,
+ "y": -128.0
+ },
+ "rotation": 0.0,
+ "interface": {
+ "inputs": [
+ {
+ "id": "port-4f732b3e",
+ "name": "Port 1",
+ "position": {
+ "x": -176.0,
+ "y": -144.0
+ },
+ "properties": {
+ "iconPosition": {
+ "x": 32.0,
+ "y": 48.0
+ }
+ },
+ "type": "signal"
+ },
+ {
+ "id": "port-ef7c4218",
+ "name": "Port 2",
+ "position": {
+ "x": -176.0,
+ "y": -16.0
+ },
+ "properties": {
+ "iconPosition": {
+ "x": 32.0,
+ "y": 80.0
+ }
+ },
+ "type": "signal"
+ }
+ ],
+ "outputs": [
+ {
+ "id": "port-b68679b2",
+ "name": "Port 3",
+ "position": {
+ "x": 128.0,
+ "y": -80.0
+ },
+ "properties": {
+ "iconPosition": {
+ "x": 96.0,
+ "y": 64.0
+ }
+ },
+ "type": "signal"
+ }
+ ]
+ },
+ "icon": {
+ "shape": "rectangle",
+ "fill": "#f4f4f4",
+ "border": "#303030",
+ "text": "Graph",
+ "size": {
+ "width": 128.0,
+ "height": 128.0
+ },
+ "elements": [
+ {
+ "cornerRadius": 5.0,
+ "fill": "#f4f4f4",
+ "height": 64.0,
+ "lineStyle": "solid",
+ "lineWidth": 1.5,
+ "stroke": "#303030",
+ "type": "rectangle",
+ "width": 64.0,
+ "x": 32.0,
+ "y": 32.0
+ },
+ {
+ "color": "#303030",
+ "fill": "#ffffff",
+ "fontSize": 12.0,
+ "height": 48.0,
+ "lineStyle": "solid",
+ "lineWidth": 1.5,
+ "stroke": "#303030",
+ "text": "B",
+ "type": "text",
+ "width": 48.0,
+ "x": 40.0,
+ "y": 40.0
+ }
+ ]
+ },
+ "properties": {},
+ "library": {
+ "showSubtree": true
+ },
+ "implementation": {
+ "kind": "graph",
+ "graph": {
+ "blocks": [],
+ "connections": [],
+ "annotations": []
+ }
+ }
+ }
+ ],
+ "connections": [
+ {
+ "id": "c59eac1f-5f24-45de-90ee-bfd039894ed9",
+ "source": {
+ "block": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
+ "port": "port-de109124"
+ },
+ "target": {
+ "block": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
+ "port": "port-4f732b3e"
+ },
+ "name": "",
+ "properties": {
+ "routing": "angled",
+ "waypoints": [
+ {
+ "x": -64.0,
+ "y": -128.0
+ },
+ {
+ "x": -64.0,
+ "y": 64.0
+ },
+ {
+ "x": 64.0,
+ "y": 64.0
+ },
+ {
+ "x": 64.0,
+ "y": -80.0
+ }
+ ]
+ }
+ }
+ ],
+ "annotations": [
+ {
+ "id": "bfe7d48b-fe54-4378-ab1b-f9b46c2efa87",
+ "kind": "box",
+ "position": {
+ "x": 48.0,
+ "y": -328.0
+ },
+ "size": {
+ "width": 160.0,
+ "height": 112.0
+ },
+ "text": "",
+ "layer": -1,
+ "properties": {
+ "stroke": "#303030",
+ "lineWidth": 1.5,
+ "lineStyle": "solid",
+ "fill": "#dbeafe",
+ "cornerRadius": 0.0,
+ "routing": "angled",
+ "waypoints": []
+ }
+ },
+ {
+ "id": "aa69f7b3-f5af-426b-9eb3-c63a268346b5",
+ "kind": "text",
+ "position": {
+ "x": -272.0,
+ "y": -216.0
+ },
+ "size": {
+ "width": 400.0,
+ "height": 48.0
+ },
+ "text": "This is an annotation",
+ "layer": -1,
+ "properties": {
+ "stroke": "#023010",
+ "lineWidth": 3.0,
+ "lineStyle": "dash",
+ "fill": "none",
+ "fontSize": 18.0,
+ "color": "#023010",
+ "routing": "angled",
+ "waypoints": []
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}