... | ... |
@@ -87,7 +87,7 @@ class DotWidget(Gtk.DrawingArea): |
87 | 87 |
|
88 | 88 |
self.x, self.y = 0.0, 0.0 |
89 | 89 |
self.zoom_ratio = 1.0 |
90 |
- self.zoom_to_fit_on_resize = False |
|
90 |
+ self.zoom_to_fit_on_resize_reload = False |
|
91 | 91 |
self.animation = animation.NoAnimation(self) |
92 | 92 |
self.drag_action = actions.NullAction(self) |
93 | 93 |
self.presstime = None |
... | ... |
@@ -165,7 +165,7 @@ class DotWidget(Gtk.DrawingArea): |
165 | 165 |
|
166 | 166 |
parser = XDotParser(xdotcode, graphviz_version=self.graphviz_version) |
167 | 167 |
self.graph = parser.parse() |
168 |
- self.zoom_image(self.zoom_ratio, center=center) |
|
168 |
+ self.queue_draw() |
|
169 | 169 |
|
170 | 170 |
def reload(self): |
171 | 171 |
if self.openfilename is not None: |
... | ... |
@@ -177,6 +177,8 @@ class DotWidget(Gtk.DrawingArea): |
177 | 177 |
pass |
178 | 178 |
else: |
179 | 179 |
del self.history_back[:], self.history_forward[:] |
180 |
+ if self.zoom_to_fit_on_resize_reload: |
|
181 |
+ self.zoom_to_fit() |
|
180 | 182 |
|
181 | 183 |
def update(self): |
182 | 184 |
if self.openfilename is not None: |
... | ... |
@@ -250,7 +252,7 @@ class DotWidget(Gtk.DrawingArea): |
250 | 252 |
self.x += x / self.zoom_ratio - x / zoom_ratio |
251 | 253 |
self.y += y / self.zoom_ratio - y / zoom_ratio |
252 | 254 |
self.zoom_ratio = zoom_ratio |
253 |
- self.zoom_to_fit_on_resize = False |
|
255 |
+ self.zoom_to_fit_on_resize_reload = False |
|
254 | 256 |
self.queue_draw() |
255 | 257 |
|
256 | 258 |
def zoom_to_area(self, x1, y1, x2, y2): |
... | ... |
@@ -264,7 +266,7 @@ class DotWidget(Gtk.DrawingArea): |
264 | 266 |
float(rect.width)/float(width), |
265 | 267 |
float(rect.height)/float(height) |
266 | 268 |
) |
267 |
- self.zoom_to_fit_on_resize = False |
|
269 |
+ self.zoom_to_fit_on_resize_reload = False |
|
268 | 270 |
self.x = (x1 + x2) / 2 |
269 | 271 |
self.y = (y1 + y2) / 2 |
270 | 272 |
self.queue_draw() |
... | ... |
@@ -280,7 +282,7 @@ class DotWidget(Gtk.DrawingArea): |
280 | 282 |
float(rect.height)/float(self.graph.height) |
281 | 283 |
) |
282 | 284 |
self.zoom_image(zoom_ratio, center=True) |
283 |
- self.zoom_to_fit_on_resize = True |
|
285 |
+ self.zoom_to_fit_on_resize_reload = True |
|
284 | 286 |
|
285 | 287 |
ZOOM_INCREMENT = 1.25 |
286 | 288 |
ZOOM_TO_FIT_MARGIN = 12 |
... | ... |
@@ -468,7 +470,7 @@ class DotWidget(Gtk.DrawingArea): |
468 | 470 |
return True |
469 | 471 |
|
470 | 472 |
def on_area_size_allocate(self, area, allocation): |
471 |
- if self.zoom_to_fit_on_resize: |
|
473 |
+ if self.zoom_to_fit_on_resize_reload: |
|
472 | 474 |
self.zoom_to_fit() |
473 | 475 |
|
474 | 476 |
def animate_to(self, x, y): |
... | ... |
@@ -599,10 +599,10 @@ class DotWindow(Gtk.Window): |
599 | 599 |
('Reload', Gtk.STOCK_REFRESH, None, None, "Reload graph", self.on_reload), |
600 | 600 |
('Print', Gtk.STOCK_PRINT, None, None, |
601 | 601 |
"Prints the currently visible part of the graph", self.dotwidget.on_print), |
602 |
- ('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, None, self.dotwidget.on_zoom_in), |
|
603 |
- ('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, None, self.dotwidget.on_zoom_out), |
|
604 |
- ('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, None, self.dotwidget.on_zoom_fit), |
|
605 |
- ('Zoom100', Gtk.STOCK_ZOOM_100, None, None, None, self.dotwidget.on_zoom_100), |
|
602 |
+ ('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, "Zoom in", self.dotwidget.on_zoom_in), |
|
603 |
+ ('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, "Zoom out", self.dotwidget.on_zoom_out), |
|
604 |
+ ('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, "Fit zoom", self.dotwidget.on_zoom_fit), |
|
605 |
+ ('Zoom100', Gtk.STOCK_ZOOM_100, None, None, "Reset zoom level", self.dotwidget.on_zoom_100), |
|
606 | 606 |
('FindNext', Gtk.STOCK_GO_FORWARD, 'Next Result', None, 'Move to the next search result', self.on_find_next), |
607 | 607 |
)) |
608 | 608 |
|
Be more tolerate. Return dot output if any, even there was an error.
... | ... |
@@ -355,6 +355,9 @@ class DotWidget(Gtk.DrawingArea): |
355 | 355 |
toolbar = win.uimanager.get_widget("/ToolBar") |
356 | 356 |
toolbar.set_visible(not toolbar.get_visible()) |
357 | 357 |
return True |
358 |
+ if event.keyval == Gdk.KEY_w: |
|
359 |
+ self.zoom_to_fit() |
|
360 |
+ return True |
|
358 | 361 |
return False |
359 | 362 |
|
360 | 363 |
print_settings = None |
With this commit it's now possible to search text of any length.
... | ... |
@@ -640,7 +640,7 @@ class DotWindow(Gtk.Window): |
640 | 640 |
|
641 | 641 |
# Add Find text search |
642 | 642 |
find_toolitem = uimanager.get_widget('/ToolBar/Find') |
643 |
- self.textentry = Gtk.Entry(max_length=20) |
|
643 |
+ self.textentry = Gtk.Entry() |
|
644 | 644 |
self.textentry.set_icon_from_stock(0, Gtk.STOCK_FIND) |
645 | 645 |
find_toolitem.add(self.textentry) |
646 | 646 |
|
... | ... |
@@ -592,9 +592,9 @@ class DotWindow(Gtk.Window): |
592 | 592 |
|
593 | 593 |
# Create actions |
594 | 594 |
actiongroup.add_actions(( |
595 |
- ('Open', Gtk.STOCK_OPEN, None, None, None, self.on_open), |
|
596 |
- ('Export', Gtk.STOCK_SAVE_AS, None, None, "Save graph as picture.", self.on_export), |
|
597 |
- ('Reload', Gtk.STOCK_REFRESH, None, None, None, self.on_reload), |
|
595 |
+ ('Open', Gtk.STOCK_OPEN, None, None, "Open dot-file", self.on_open), |
|
596 |
+ ('Export', Gtk.STOCK_SAVE_AS, None, None, "Export graph to other format", self.on_export), |
|
597 |
+ ('Reload', Gtk.STOCK_REFRESH, None, None, "Reload graph", self.on_reload), |
|
598 | 598 |
('Print', Gtk.STOCK_PRINT, None, None, |
599 | 599 |
"Prints the currently visible part of the graph", self.dotwidget.on_print), |
600 | 600 |
('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, None, self.dotwidget.on_zoom_in), |
... | ... |
@@ -766,12 +766,23 @@ class DotWindow(Gtk.Window): |
766 | 766 |
subprocess.check_call(cmd) |
767 | 767 |
|
768 | 768 |
def on_export(self, action): |
769 |
+ |
|
770 |
+ if self.dotwidget.openfilename is None: |
|
771 |
+ return |
|
772 |
+ |
|
773 |
+ default_filter = "PNG image" |
|
774 |
+ |
|
769 | 775 |
output_formats = { |
776 |
+ "dot file": "dot", |
|
777 |
+ "GIF image": "gif", |
|
778 |
+ "JPG image": "jpg", |
|
779 |
+ "JSON": "json", |
|
780 |
+ "PDF": "pdf", |
|
770 | 781 |
"PNG image": "png", |
782 |
+ "PostScript": "ps", |
|
771 | 783 |
"SVG image": "svg", |
772 |
- "PDF image": "pdf", |
|
773 |
- "GIF image": "gif", |
|
774 |
- "PDF image": "pdf", |
|
784 |
+ "XFIG image": "fig", |
|
785 |
+ "xdot file": "xdot", |
|
775 | 786 |
} |
776 | 787 |
buttons = ( |
777 | 788 |
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, |
... | ... |
@@ -793,7 +804,9 @@ class DotWindow(Gtk.Window): |
793 | 804 |
filter_.set_name(name) |
794 | 805 |
filter_.add_pattern('*.' + ext) |
795 | 806 |
chooser.add_filter(filter_) |
796 |
- |
|
807 |
+ if name == default_filter: |
|
808 |
+ chooser.set_filter(filter_) |
|
809 |
+ |
|
797 | 810 |
if chooser.run() == Gtk.ResponseType.OK: |
798 | 811 |
filename = chooser.get_filename() |
799 | 812 |
format_ = output_formats[chooser.get_filter().get_name()] |
... | ... |
@@ -133,6 +133,7 @@ class DotWidget(Gtk.DrawingArea): |
133 | 133 |
# By default DOT language is UTF-8, but it accepts other encodings |
134 | 134 |
assert isinstance(dotcode, bytes) |
135 | 135 |
xdotcode = self.run_filter(dotcode) |
136 |
+ |
|
136 | 137 |
if xdotcode is None: |
137 | 138 |
return False |
138 | 139 |
try: |
... | ... |
@@ -155,6 +156,7 @@ class DotWidget(Gtk.DrawingArea): |
155 | 156 |
|
156 | 157 |
def set_xdotcode(self, xdotcode, center=True): |
157 | 158 |
assert isinstance(xdotcode, bytes) |
159 |
+ |
|
158 | 160 |
if self.graphviz_version is None: |
159 | 161 |
stdout = subprocess.check_output([self.filter, '-V'], stderr=subprocess.STDOUT) |
160 | 162 |
stdout = stdout.rstrip() |
... | ... |
@@ -537,6 +539,7 @@ class DotWindow(Gtk.Window): |
537 | 539 |
<ui> |
538 | 540 |
<toolbar name="ToolBar"> |
539 | 541 |
<toolitem action="Open"/> |
542 |
+ <toolitem action="Export"/> |
|
540 | 543 |
<toolitem action="Reload"/> |
541 | 544 |
<toolitem action="Print"/> |
542 | 545 |
<separator/> |
... | ... |
@@ -590,6 +593,7 @@ class DotWindow(Gtk.Window): |
590 | 593 |
# Create actions |
591 | 594 |
actiongroup.add_actions(( |
592 | 595 |
('Open', Gtk.STOCK_OPEN, None, None, None, self.on_open), |
596 |
+ ('Export', Gtk.STOCK_SAVE_AS, None, None, "Save graph as picture.", self.on_export), |
|
593 | 597 |
('Reload', Gtk.STOCK_REFRESH, None, None, None, self.on_reload), |
594 | 598 |
('Print', Gtk.STOCK_PRINT, None, None, |
595 | 599 |
"Prints the currently visible part of the graph", self.dotwidget.on_print), |
... | ... |
@@ -749,6 +753,55 @@ class DotWindow(Gtk.Window): |
749 | 753 |
self.open_file(filename) |
750 | 754 |
else: |
751 | 755 |
chooser.destroy() |
756 |
+ |
|
757 |
+ def export_file(self, filename, format_): |
|
758 |
+ if not filename.endswith("." + format_): |
|
759 |
+ filename += '.' + format_ |
|
760 |
+ cmd = [ |
|
761 |
+ self.dotwidget.filter, # program name, usually "dot" |
|
762 |
+ '-T' + format_, |
|
763 |
+ '-o', filename, |
|
764 |
+ self.dotwidget.openfilename, |
|
765 |
+ ] |
|
766 |
+ subprocess.check_call(cmd) |
|
767 |
+ |
|
768 |
+ def on_export(self, action): |
|
769 |
+ output_formats = { |
|
770 |
+ "PNG image": "png", |
|
771 |
+ "SVG image": "svg", |
|
772 |
+ "PDF image": "pdf", |
|
773 |
+ "GIF image": "gif", |
|
774 |
+ "PDF image": "pdf", |
|
775 |
+ } |
|
776 |
+ buttons = ( |
|
777 |
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, |
|
778 |
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK) |
|
779 |
+ chooser = Gtk.FileChooserDialog( |
|
780 |
+ parent=self, |
|
781 |
+ title="Export to other file format.", |
|
782 |
+ action=Gtk.FileChooserAction.SAVE, |
|
783 |
+ buttons=buttons) |
|
784 |
+ chooser.set_default_response(Gtk.ResponseType.OK) |
|
785 |
+ chooser.set_current_folder(self.last_open_dir) |
|
786 |
+ |
|
787 |
+ openfilename = os.path.basename(self.dotwidget.openfilename) |
|
788 |
+ openfileroot = os.path.splitext(openfilename)[0] |
|
789 |
+ chooser.set_current_name(openfileroot) |
|
790 |
+ |
|
791 |
+ for name, ext in output_formats.items(): |
|
792 |
+ filter_ = Gtk.FileFilter() |
|
793 |
+ filter_.set_name(name) |
|
794 |
+ filter_.add_pattern('*.' + ext) |
|
795 |
+ chooser.add_filter(filter_) |
|
796 |
+ |
|
797 |
+ if chooser.run() == Gtk.ResponseType.OK: |
|
798 |
+ filename = chooser.get_filename() |
|
799 |
+ format_ = output_formats[chooser.get_filter().get_name()] |
|
800 |
+ chooser.destroy() |
|
801 |
+ self.export_file(filename, format_) |
|
802 |
+ else: |
|
803 |
+ chooser.destroy() |
|
804 |
+ |
|
752 | 805 |
|
753 | 806 |
def on_reload(self, action): |
754 | 807 |
self.dotwidget.reload() |
The project switched to that since *.dot is taken by Microsoft Word:
https://marc.info/?l=graphviz-devel&m=129418103126092
... | ... |
@@ -725,7 +725,7 @@ class DotWindow(Gtk.Window): |
725 | 725 |
|
726 | 726 |
def on_open(self, action): |
727 | 727 |
chooser = Gtk.FileChooserDialog(parent=self, |
728 |
- title="Open dot File", |
|
728 |
+ title="Open Graphviz File", |
|
729 | 729 |
action=Gtk.FileChooserAction.OPEN, |
730 | 730 |
buttons=(Gtk.STOCK_CANCEL, |
731 | 731 |
Gtk.ResponseType.CANCEL, |
... | ... |
@@ -734,7 +734,8 @@ class DotWindow(Gtk.Window): |
734 | 734 |
chooser.set_default_response(Gtk.ResponseType.OK) |
735 | 735 |
chooser.set_current_folder(self.last_open_dir) |
736 | 736 |
filter = Gtk.FileFilter() |
737 |
- filter.set_name("Graphviz dot files") |
|
737 |
+ filter.set_name("Graphviz files") |
|
738 |
+ filter.add_pattern("*.gv") |
|
738 | 739 |
filter.add_pattern("*.dot") |
739 | 740 |
chooser.add_filter(filter) |
740 | 741 |
filter = Gtk.FileFilter() |
Under Manjaro and Pop_OS, the class was set to "-m" when xdot was run
this way:
python3 -m xdot foo
This solution was suggested by Moritz Meier.
Irrespectively of graphviz version.
Fixes https://github.com/jrfonseca/xdot.py/issues/92
... | ... |
@@ -56,6 +56,7 @@ class DotWidget(Gtk.DrawingArea): |
56 | 56 |
} |
57 | 57 |
|
58 | 58 |
filter = 'dot' |
59 |
+ graphviz_version = None |
|
59 | 60 |
|
60 | 61 |
def __init__(self): |
61 | 62 |
Gtk.DrawingArea.__init__(self) |
... | ... |
@@ -100,6 +101,7 @@ class DotWidget(Gtk.DrawingArea): |
100 | 101 |
|
101 | 102 |
def set_filter(self, filter): |
102 | 103 |
self.filter = filter |
104 |
+ self.graphviz_version = None |
|
103 | 105 |
|
104 | 106 |
def run_filter(self, dotcode): |
105 | 107 |
if not self.filter: |
... | ... |
@@ -153,7 +155,14 @@ class DotWidget(Gtk.DrawingArea): |
153 | 155 |
|
154 | 156 |
def set_xdotcode(self, xdotcode, center=True): |
155 | 157 |
assert isinstance(xdotcode, bytes) |
156 |
- parser = XDotParser(xdotcode) |
|
158 |
+ if self.graphviz_version is None: |
|
159 |
+ stdout = subprocess.check_output([self.filter, '-V'], stderr=subprocess.STDOUT) |
|
160 |
+ stdout = stdout.rstrip() |
|
161 |
+ mo = re.match(br'^.* - .* version (?P<version>.*) \(.*\)$', stdout) |
|
162 |
+ assert mo |
|
163 |
+ self.graphviz_version = mo.group('version').decode('ascii') |
|
164 |
+ |
|
165 |
+ parser = XDotParser(xdotcode, graphviz_version=self.graphviz_version) |
|
157 | 166 |
self.graph = parser.parse() |
158 | 167 |
self.zoom_image(self.zoom_ratio, center=center) |
159 | 168 |
|
... | ... |
@@ -73,7 +73,8 @@ class DotWidget(Gtk.DrawingArea): |
73 | 73 |
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | |
74 | 74 |
Gdk.EventMask.POINTER_MOTION_HINT_MASK | |
75 | 75 |
Gdk.EventMask.BUTTON_RELEASE_MASK | |
76 |
- Gdk.EventMask.SCROLL_MASK) |
|
76 |
+ Gdk.EventMask.SCROLL_MASK | |
|
77 |
+ Gdk.EventMask.SMOOTH_SCROLL_MASK) |
|
77 | 78 |
self.connect("motion-notify-event", self.on_area_motion_notify) |
78 | 79 |
self.connect("scroll-event", self.on_area_scroll_event) |
79 | 80 |
self.connect("size-allocate", self.on_area_size_allocate) |
... | ... |
@@ -439,9 +440,13 @@ class DotWidget(Gtk.DrawingArea): |
439 | 440 |
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT, |
440 | 441 |
pos=(event.x, event.y)) |
441 | 442 |
return True |
442 |
- if event.direction == Gdk.ScrollDirection.DOWN: |
|
443 |
+ elif event.direction == Gdk.ScrollDirection.DOWN: |
|
443 | 444 |
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT, |
444 | 445 |
pos=(event.x, event.y)) |
446 |
+ else: |
|
447 |
+ deltas = event.get_scroll_deltas() |
|
448 |
+ self.zoom_image(self.zoom_ratio * (1 - deltas.delta_y / 10), |
|
449 |
+ pos=(event.x, event.y)) |
|
445 | 450 |
return True |
446 | 451 |
return False |
447 | 452 |
|
That way, the user can use keyboard shortcuts again.
* Always pan to the first result
* Show the number of results
* Provide a button to pan to the next result
... | ... |
@@ -20,6 +20,7 @@ import re |
20 | 20 |
import subprocess |
21 | 21 |
import sys |
22 | 22 |
import time |
23 |
+import operator |
|
23 | 24 |
|
24 | 25 |
import gi |
25 | 26 |
gi.require_version('Gtk', '3.0') |
... | ... |
@@ -534,6 +535,10 @@ class DotWindow(Gtk.Window): |
534 | 535 |
<toolitem action="Zoom100"/> |
535 | 536 |
<separator/> |
536 | 537 |
<toolitem name="Find" action="Find"/> |
538 |
+ <separator name="FindNextSeparator"/> |
|
539 |
+ <toolitem action="FindNext"/> |
|
540 |
+ <separator name="FindStatusSeparator"/> |
|
541 |
+ <toolitem name="FindStatus" action="FindStatus"/> |
|
537 | 542 |
</toolbar> |
538 | 543 |
</ui> |
539 | 544 |
''' |
... | ... |
@@ -577,6 +582,7 @@ class DotWindow(Gtk.Window): |
577 | 582 |
('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, None, self.dotwidget.on_zoom_out), |
578 | 583 |
('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, None, self.dotwidget.on_zoom_fit), |
579 | 584 |
('Zoom100', Gtk.STOCK_ZOOM_100, None, None, None, self.dotwidget.on_zoom_100), |
585 |
+ ('FindNext', Gtk.STOCK_GO_FORWARD, 'Next Result', None, 'Move to the next search result', self.on_find_next), |
|
580 | 586 |
)) |
581 | 587 |
|
582 | 588 |
self.back_action = Gtk.Action('Back', None, None, Gtk.STOCK_GO_BACK) |
... | ... |
@@ -593,6 +599,10 @@ class DotWindow(Gtk.Window): |
593 | 599 |
"Find a node by name", None) |
594 | 600 |
actiongroup.add_action(find_action) |
595 | 601 |
|
602 |
+ findstatus_action = FindMenuToolAction("FindStatus", None, |
|
603 |
+ "Number of results found", None) |
|
604 |
+ actiongroup.add_action(findstatus_action) |
|
605 |
+ |
|
596 | 606 |
# Add the actiongroup to the uimanager |
597 | 607 |
uimanager.insert_action_group(actiongroup, 0) |
598 | 608 |
|
... | ... |
@@ -619,6 +629,15 @@ class DotWindow(Gtk.Window): |
619 | 629 |
self.textentry.connect("activate", self.textentry_activate, self.textentry); |
620 | 630 |
self.textentry.connect("changed", self.textentry_changed, self.textentry); |
621 | 631 |
|
632 |
+ uimanager.get_widget('/ToolBar/FindNextSeparator').set_draw(False) |
|
633 |
+ uimanager.get_widget('/ToolBar/FindStatusSeparator').set_draw(False) |
|
634 |
+ self.find_next_toolitem = uimanager.get_widget('/ToolBar/FindNext') |
|
635 |
+ self.find_next_toolitem.set_sensitive(False) |
|
636 |
+ |
|
637 |
+ self.find_count = Gtk.Label() |
|
638 |
+ findstatus_toolitem = uimanager.get_widget('/ToolBar/FindStatus') |
|
639 |
+ findstatus_toolitem.add(self.find_count) |
|
640 |
+ |
|
622 | 641 |
self.show_all() |
623 | 642 |
|
624 | 643 |
def find_text(self, entry_text): |
... | ... |
@@ -628,9 +647,11 @@ class DotWindow(Gtk.Window): |
628 | 647 |
for element in dot_widget.graph.nodes + dot_widget.graph.edges: |
629 | 648 |
if element.search_text(regexp): |
630 | 649 |
found_items.append(element) |
631 |
- return found_items |
|
650 |
+ return sorted(found_items, key=operator.methodcaller('get_text')) |
|
632 | 651 |
|
633 | 652 |
def textentry_changed(self, widget, entry): |
653 |
+ self.find_index = 0 |
|
654 |
+ self.find_next_toolitem.set_sensitive(False) |
|
634 | 655 |
entry_text = entry.get_text() |
635 | 656 |
dot_widget = self.dotwidget |
636 | 657 |
if not entry_text: |
... | ... |
@@ -639,8 +660,14 @@ class DotWindow(Gtk.Window): |
639 | 660 |
|
640 | 661 |
found_items = self.find_text(entry_text) |
641 | 662 |
dot_widget.set_highlight(found_items, search=True) |
663 |
+ if found_items: |
|
664 |
+ self.find_count.set_label('%d nodes found' % len(found_items)) |
|
665 |
+ else: |
|
666 |
+ self.find_count.set_label('') |
|
642 | 667 |
|
643 | 668 |
def textentry_activate(self, widget, entry): |
669 |
+ self.find_index = 0 |
|
670 |
+ self.find_next_toolitem.set_sensitive(False) |
|
644 | 671 |
entry_text = entry.get_text() |
645 | 672 |
dot_widget = self.dotwidget |
646 | 673 |
if not entry_text: |
... | ... |
@@ -649,8 +676,9 @@ class DotWindow(Gtk.Window): |
649 | 676 |
|
650 | 677 |
found_items = self.find_text(entry_text) |
651 | 678 |
dot_widget.set_highlight(found_items, search=True) |
652 |
- if(len(found_items) == 1): |
|
679 |
+ if found_items: |
|
653 | 680 |
dot_widget.animate_to(found_items[0].x, found_items[0].y) |
681 |
+ self.find_next_toolitem.set_sensitive(len(found_items) > 1) |
|
654 | 682 |
|
655 | 683 |
def set_filter(self, filter): |
656 | 684 |
self.dotwidget.set_filter(filter) |
... | ... |
@@ -717,6 +745,15 @@ class DotWindow(Gtk.Window): |
717 | 745 |
dlg.run() |
718 | 746 |
dlg.destroy() |
719 | 747 |
|
748 |
+ def on_find_next(self, action): |
|
749 |
+ self.find_index += 1 |
|
750 |
+ entry_text = self.textentry.get_text() |
|
751 |
+ # Maybe storing the search result would be better |
|
752 |
+ found_items = self.find_text(entry_text) |
|
753 |
+ found_item = found_items[self.find_index] |
|
754 |
+ self.dotwidget.animate_to(found_item.x, found_item.y) |
|
755 |
+ self.find_next_toolitem.set_sensitive(len(found_items) > self.find_index + 1) |
|
756 |
+ |
|
720 | 757 |
def on_history(self, action, has_back, has_forward): |
721 | 758 |
self.back_action.set_sensitive(has_back) |
722 | 759 |
self.forward_action.set_sensitive(has_forward) |
... | ... |
@@ -336,6 +336,12 @@ class DotWidget(Gtk.DrawingArea): |
336 | 336 |
if event.keyval == Gdk.KEY_p: |
337 | 337 |
self.on_print() |
338 | 338 |
return True |
339 |
+ if event.keyval == Gdk.KEY_t: |
|
340 |
+ # toggle toolbar visibility |
|
341 |
+ win = widget.get_toplevel() |
|
342 |
+ toolbar = win.uimanager.get_widget("/ToolBar") |
|
343 |
+ toolbar.set_visible(not toolbar.get_visible()) |
|
344 |
+ return True |
|
339 | 345 |
return False |
340 | 346 |
|
341 | 347 |
print_settings = None |
Fixes #76.
... | ... |
@@ -168,7 +168,10 @@ class DotWidget(Gtk.DrawingArea): |
168 | 168 |
|
169 | 169 |
def update(self): |
170 | 170 |
if self.openfilename is not None: |
171 |
- current_mtime = os.stat(self.openfilename).st_mtime |
|
171 |
+ try: |
|
172 |
+ current_mtime = os.stat(self.openfilename).st_mtime |
|
173 |
+ except OSError: |
|
174 |
+ return True |
|
172 | 175 |
if current_mtime != self.last_mtime: |
173 | 176 |
self.last_mtime = current_mtime |
174 | 177 |
self.reload() |
... | ... |
@@ -50,7 +50,8 @@ class DotWidget(Gtk.DrawingArea): |
50 | 50 |
# TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object. |
51 | 51 |
__gsignals__ = { |
52 | 52 |
'clicked': (GObject.SIGNAL_RUN_LAST, None, (str, object)), |
53 |
- 'error': (GObject.SIGNAL_RUN_LAST, None, (str,)) |
|
53 |
+ 'error': (GObject.SIGNAL_RUN_LAST, None, (str,)), |
|
54 |
+ 'history': (GObject.SIGNAL_RUN_LAST, None, (bool, bool)) |
|
54 | 55 |
} |
55 | 56 |
|
56 | 57 |
filter = 'dot' |
... | ... |
@@ -89,6 +90,8 @@ class DotWidget(Gtk.DrawingArea): |
89 | 90 |
self.presstime = None |
90 | 91 |
self.highlight = None |
91 | 92 |
self.highlight_search = False |
93 |
+ self.history_back = [] |
|
94 |
+ self.history_forward = [] |
|
92 | 95 |
|
93 | 96 |
def error_dialog(self, message): |
94 | 97 |
self.emit('error', message) |
... | ... |
@@ -160,6 +163,8 @@ class DotWidget(Gtk.DrawingArea): |
160 | 163 |
fp.close() |
161 | 164 |
except IOError: |
162 | 165 |
pass |
166 |
+ else: |
|
167 |
+ del self.history_back[:], self.history_forward[:] |
|
163 | 168 |
|
164 | 169 |
def update(self): |
165 | 170 |
if self.openfilename is not None: |
... | ... |
@@ -439,9 +444,39 @@ class DotWidget(Gtk.DrawingArea): |
439 | 444 |
self.zoom_to_fit() |
440 | 445 |
|
441 | 446 |
def animate_to(self, x, y): |
447 |
+ del self.history_forward[:] |
|
448 |
+ self.history_back.append(self.get_current_pos()) |
|
449 |
+ self.history_changed() |
|
450 |
+ self._animate_to(x, y) |
|
451 |
+ |
|
452 |
+ def _animate_to(self, x, y): |
|
442 | 453 |
self.animation = animation.ZoomToAnimation(self, x, y) |
443 | 454 |
self.animation.start() |
444 | 455 |
|
456 |
+ def history_changed(self): |
|
457 |
+ self.emit( |
|
458 |
+ 'history', |
|
459 |
+ bool(self.history_back), |
|
460 |
+ bool(self.history_forward)) |
|
461 |
+ |
|
462 |
+ def on_go_back(self, action=None): |
|
463 |
+ try: |
|
464 |
+ item = self.history_back.pop() |
|
465 |
+ except LookupError: |
|
466 |
+ return |
|
467 |
+ self.history_forward.append(self.get_current_pos()) |
|
468 |
+ self.history_changed() |
|
469 |
+ self._animate_to(*item) |
|
470 |
+ |
|
471 |
+ def on_go_forward(self, action=None): |
|
472 |
+ try: |
|
473 |
+ item = self.history_forward.pop() |
|
474 |
+ except LookupError: |
|
475 |
+ return |
|
476 |
+ self.history_back.append(self.get_current_pos()) |
|
477 |
+ self.history_changed() |
|
478 |
+ self._animate_to(*item) |
|
479 |
+ |
|
445 | 480 |
def window2graph(self, x, y): |
446 | 481 |
rect = self.get_allocation() |
447 | 482 |
x -= 0.5*rect.width |
... | ... |
@@ -481,6 +516,9 @@ class DotWindow(Gtk.Window): |
481 | 516 |
<toolitem action="Reload"/> |
482 | 517 |
<toolitem action="Print"/> |
483 | 518 |
<separator/> |
519 |
+ <toolitem action="Back"/> |
|
520 |
+ <toolitem action="Forward"/> |
|
521 |
+ <separator/> |
|
484 | 522 |
<toolitem action="ZoomIn"/> |
485 | 523 |
<toolitem action="ZoomOut"/> |
486 | 524 |
<toolitem action="ZoomFit"/> |
... | ... |
@@ -507,6 +545,7 @@ class DotWindow(Gtk.Window): |
507 | 545 |
|
508 | 546 |
self.dotwidget = widget or DotWidget() |
509 | 547 |
self.dotwidget.connect("error", lambda e, m: self.error_dialog(m)) |
548 |
+ self.dotwidget.connect("history", self.on_history) |
|
510 | 549 |
|
511 | 550 |
# Create a UIManager instance |
512 | 551 |
uimanager = self.uimanager = Gtk.UIManager() |
... | ... |
@@ -531,6 +570,16 @@ class DotWindow(Gtk.Window): |
531 | 570 |
('Zoom100', Gtk.STOCK_ZOOM_100, None, None, None, self.dotwidget.on_zoom_100), |
532 | 571 |
)) |
533 | 572 |
|
573 |
+ self.back_action = Gtk.Action('Back', None, None, Gtk.STOCK_GO_BACK) |
|
574 |
+ self.back_action.set_sensitive(False) |
|
575 |
+ self.back_action.connect("activate", self.dotwidget.on_go_back) |
|
576 |
+ actiongroup.add_action(self.back_action) |
|
577 |
+ |
|
578 |
+ self.forward_action = Gtk.Action('Forward', None, None, Gtk.STOCK_GO_FORWARD) |
|
579 |
+ self.forward_action.set_sensitive(False) |
|
580 |
+ self.forward_action.connect("activate", self.dotwidget.on_go_forward) |
|
581 |
+ actiongroup.add_action(self.forward_action) |
|
582 |
+ |
|
534 | 583 |
find_action = FindMenuToolAction("Find", None, |
535 | 584 |
"Find a node by name", None) |
536 | 585 |
actiongroup.add_action(find_action) |
... | ... |
@@ -658,3 +707,7 @@ class DotWindow(Gtk.Window): |
658 | 707 |
dlg.set_title(self.base_title) |
659 | 708 |
dlg.run() |
660 | 709 |
dlg.destroy() |
710 |
+ |
|
711 |
+ def on_history(self, action, has_back, has_forward): |
|
712 |
+ self.back_action.set_sensitive(has_back) |
|
713 |
+ self.forward_action.set_sensitive(has_forward) |
... | ... |
@@ -169,17 +169,26 @@ class DotWidget(Gtk.DrawingArea): |
169 | 169 |
self.reload() |
170 | 170 |
return True |
171 | 171 |
|
172 |
+ def _draw_graph(self, cr, rect): |
|
173 |
+ w, h = float(rect.width), float(rect.height) |
|
174 |
+ cx, cy = 0.5 * w, 0.5 * h |
|
175 |
+ x, y, ratio = self.x, self.y, self.zoom_ratio |
|
176 |
+ x0, y0 = x - cx / ratio, y - cy / ratio |
|
177 |
+ x1, y1 = x0 + w / ratio, y0 + h / ratio |
|
178 |
+ bounding = (x0, y0, x1, y1) |
|
179 |
+ |
|
180 |
+ cr.translate(cx, cy) |
|
181 |
+ cr.scale(ratio, ratio) |
|
182 |
+ cr.translate(-x, -y) |
|
183 |
+ self.graph.draw(cr, highlight_items=self.highlight, bounding=bounding) |
|
184 |
+ |
|
172 | 185 |
def on_draw(self, widget, cr): |
173 | 186 |
rect = self.get_allocation() |
174 | 187 |
Gtk.render_background(self.get_style_context(), cr, 0, 0, |
175 | 188 |
rect.width, rect.height) |
176 | 189 |
|
177 | 190 |
cr.save() |
178 |
- cr.translate(0.5*rect.width, 0.5*rect.height) |
|
179 |
- cr.scale(self.zoom_ratio, self.zoom_ratio) |
|
180 |
- cr.translate(-self.x, -self.y) |
|
181 |
- |
|
182 |
- self.graph.draw(cr, highlight_items=self.highlight) |
|
191 |
+ self._draw_graph(cr, rect) |
|
183 | 192 |
cr.restore() |
184 | 193 |
|
185 | 194 |
self.drag_action.draw(cr) |
... | ... |
@@ -342,13 +351,8 @@ class DotWidget(Gtk.DrawingArea): |
342 | 351 |
|
343 | 352 |
def draw_page(self, operation, context, page_nr): |
344 | 353 |
cr = context.get_cairo_context() |
345 |
- |
|
346 | 354 |
rect = self.get_allocation() |
347 |
- cr.translate(0.5*rect.width, 0.5*rect.height) |
|
348 |
- cr.scale(self.zoom_ratio, self.zoom_ratio) |
|
349 |
- cr.translate(-self.x, -self.y) |
|
350 |
- |
|
351 |
- self.graph.draw(cr, highlight_items=self.highlight) |
|
355 |
+ self._draw_graph(cr, rect) |
|
352 | 356 |
|
353 | 357 |
def get_drag_action(self, event): |
354 | 358 |
state = event.state |
I use a dark GTK+ theme and the stark white background on xdot's graph
canvas area sometimes hurts my eyes. This change makes the widget use
the current theme's background colour instead.
... | ... |
@@ -170,11 +170,11 @@ class DotWidget(Gtk.DrawingArea): |
170 | 170 |
return True |
171 | 171 |
|
172 | 172 |
def on_draw(self, widget, cr): |
173 |
- cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) |
|
174 |
- cr.paint() |
|
173 |
+ rect = self.get_allocation() |
|
174 |
+ Gtk.render_background(self.get_style_context(), cr, 0, 0, |
|
175 |
+ rect.width, rect.height) |
|
175 | 176 |
|
176 | 177 |
cr.save() |
177 |
- rect = self.get_allocation() |
|
178 | 178 |
cr.translate(0.5*rect.width, 0.5*rect.height) |
179 | 179 |
cr.scale(self.zoom_ratio, self.zoom_ratio) |
180 | 180 |
cr.translate(-self.x, -self.y) |
Fixes https://github.com/jrfonseca/xdot.py/issues/56
... | ... |
@@ -122,23 +122,23 @@ class DotWidget(Gtk.DrawingArea): |
122 | 122 |
return None |
123 | 123 |
return xdotcode |
124 | 124 |
|
125 |
- def _set_dotcode(self, dotcode, filename=None): |
|
125 |
+ def _set_dotcode(self, dotcode, filename=None, center=True): |
|
126 | 126 |
# By default DOT language is UTF-8, but it accepts other encodings |
127 | 127 |
assert isinstance(dotcode, bytes) |
128 | 128 |
xdotcode = self.run_filter(dotcode) |
129 | 129 |
if xdotcode is None: |
130 | 130 |
return False |
131 | 131 |
try: |
132 |
- self.set_xdotcode(xdotcode) |
|
132 |
+ self.set_xdotcode(xdotcode, center=center) |
|
133 | 133 |
except ParseError as ex: |
134 | 134 |
self.error_dialog(str(ex)) |
135 | 135 |
return False |
136 | 136 |
else: |
137 | 137 |
return True |
138 | 138 |
|
139 |
- def set_dotcode(self, dotcode, filename=None): |
|
139 |
+ def set_dotcode(self, dotcode, filename=None, center=True): |
|
140 | 140 |
self.openfilename = None |
141 |
- if self._set_dotcode(dotcode, filename): |
|
141 |
+ if self._set_dotcode(dotcode, filename, center=center): |
|
142 | 142 |
if filename is None: |
143 | 143 |
self.last_mtime = None |
144 | 144 |
else: |
... | ... |
@@ -146,17 +146,17 @@ class DotWidget(Gtk.DrawingArea): |
146 | 146 |
self.openfilename = filename |
147 | 147 |
return True |
148 | 148 |
|
149 |
- def set_xdotcode(self, xdotcode): |
|
149 |
+ def set_xdotcode(self, xdotcode, center=True): |
|
150 | 150 |
assert isinstance(xdotcode, bytes) |
151 | 151 |
parser = XDotParser(xdotcode) |
152 | 152 |
self.graph = parser.parse() |
153 |
- self.zoom_image(self.zoom_ratio, center=True) |
|
153 |
+ self.zoom_image(self.zoom_ratio, center=center) |
|
154 | 154 |
|
155 | 155 |
def reload(self): |
156 | 156 |
if self.openfilename is not None: |
157 | 157 |
try: |
158 | 158 |
fp = open(self.openfilename, 'rb') |
159 |
- self._set_dotcode(fp.read(), self.openfilename) |
|
159 |
+ self._set_dotcode(fp.read(), self.openfilename, center=False) |
|
160 | 160 |
fp.close() |
161 | 161 |
except IOError: |
162 | 162 |
pass |
Fixes https://github.com/jrfonseca/xdot.py/issues/50
... | ... |
@@ -122,8 +122,7 @@ class DotWidget(Gtk.DrawingArea): |
122 | 122 |
return None |
123 | 123 |
return xdotcode |
124 | 124 |
|
125 |
- def set_dotcode(self, dotcode, filename=None): |
|
126 |
- self.openfilename = None |
|
125 |
+ def _set_dotcode(self, dotcode, filename=None): |
|
127 | 126 |
# By default DOT language is UTF-8, but it accepts other encodings |
128 | 127 |
assert isinstance(dotcode, bytes) |
129 | 128 |
xdotcode = self.run_filter(dotcode) |
... | ... |
@@ -135,6 +134,11 @@ class DotWidget(Gtk.DrawingArea): |
135 | 134 |
self.error_dialog(str(ex)) |
136 | 135 |
return False |
137 | 136 |
else: |
137 |
+ return True |
|
138 |
+ |
|
139 |
+ def set_dotcode(self, dotcode, filename=None): |
|
140 |
+ self.openfilename = None |
|
141 |
+ if self._set_dotcode(dotcode, filename): |
|
138 | 142 |
if filename is None: |
139 | 143 |
self.last_mtime = None |
140 | 144 |
else: |
... | ... |
@@ -152,7 +156,7 @@ class DotWidget(Gtk.DrawingArea): |
152 | 156 |
if self.openfilename is not None: |
153 | 157 |
try: |
154 | 158 |
fp = open(self.openfilename, 'rb') |
155 |
- self.set_dotcode(fp.read(), self.openfilename) |
|
159 |
+ self._set_dotcode(fp.read(), self.openfilename) |
|
156 | 160 |
fp.close() |
157 | 161 |
except IOError: |
158 | 162 |
pass |
As found and suggested by Overmind JIANG.
Fixes https://github.com/jrfonseca/xdot.py/issues/47
... | ... |
@@ -113,9 +113,9 @@ class DotWidget(Gtk.DrawingArea): |
113 | 113 |
p = subprocess.CalledProcessError(exc.errno, self.filter, exc.strerror) |
114 | 114 |
else: |
115 | 115 |
xdotcode, error = p.communicate(dotcode) |
116 |
+ error = error.decode() |
|
116 | 117 |
error = error.rstrip() |
117 | 118 |
if error: |
118 |
- error = error.decode() |
|
119 | 119 |
sys.stderr.write(error + '\n') |
120 | 120 |
if p.returncode != 0: |
121 | 121 |
self.error_dialog(error) |
Things just happened to work when locale was set to UTF-8, but would
fail otherwise. In particular it would fail on Windows where text files
don't default to UTF-8.
Fixes https://github.com/jrfonseca/xdot.py/issues/46
... | ... |
@@ -124,8 +124,8 @@ class DotWidget(Gtk.DrawingArea): |
124 | 124 |
|
125 | 125 |
def set_dotcode(self, dotcode, filename=None): |
126 | 126 |
self.openfilename = None |
127 |
- if isinstance(dotcode, str): |
|
128 |
- dotcode = dotcode.encode('utf-8') |
|
127 |
+ # By default DOT language is UTF-8, but it accepts other encodings |
|
128 |
+ assert isinstance(dotcode, bytes) |
|
129 | 129 |
xdotcode = self.run_filter(dotcode) |
130 | 130 |
if xdotcode is None: |
131 | 131 |
return False |
... | ... |
@@ -151,7 +151,7 @@ class DotWidget(Gtk.DrawingArea): |
151 | 151 |
def reload(self): |
152 | 152 |
if self.openfilename is not None: |
153 | 153 |
try: |
154 |
- fp = open(self.openfilename, 'rt') |
|
154 |
+ fp = open(self.openfilename, 'rb') |
|
155 | 155 |
self.set_dotcode(fp.read(), self.openfilename) |
156 | 156 |
fp.close() |
157 | 157 |
except IOError: |
... | ... |
@@ -607,7 +607,7 @@ class DotWindow(Gtk.Window): |
607 | 607 |
|
608 | 608 |
def open_file(self, filename): |
609 | 609 |
try: |
610 |
- fp = open(filename, 'rt') |
|
610 |
+ fp = open(filename, 'rb') |
|
611 | 611 |
self.set_dotcode(fp.read(), filename) |
612 | 612 |
fp.close() |
613 | 613 |
except IOError as ex: |
... | ... |
@@ -39,8 +39,8 @@ from gi.repository import Gdk |
39 | 39 |
from . import actions |
40 | 40 |
from ..dot.lexer import ParseError |
41 | 41 |
from ..dot.parser import XDotParser |
42 |
-from .animation import NoAnimation, ZoomToAnimation |
|
43 |
-from .actions import NullAction, PanAction, ZoomAction, ZoomAreaAction |
|
42 |
+from . import animation |
|
43 |
+from . import actions |
|
44 | 44 |
from .elements import Graph |
45 | 45 |
|
46 | 46 |
|
... | ... |
@@ -84,8 +84,8 @@ class DotWidget(Gtk.DrawingArea): |
84 | 84 |
self.x, self.y = 0.0, 0.0 |
85 | 85 |
self.zoom_ratio = 1.0 |
86 | 86 |
self.zoom_to_fit_on_resize = False |
87 |
- self.animation = NoAnimation(self) |
|
88 |
- self.drag_action = NullAction(self) |
|
87 |
+ self.animation = animation.NoAnimation(self) |
|
88 |
+ self.drag_action = actions.NullAction(self) |
|
89 | 89 |
self.presstime = None |
90 | 90 |
self.highlight = None |
91 | 91 |
self.highlight_search = False |
... | ... |
@@ -298,7 +298,7 @@ class DotWidget(Gtk.DrawingArea): |
298 | 298 |
return True |
299 | 299 |
if event.keyval == Gdk.KEY_Escape: |
300 | 300 |
self.drag_action.abort() |
301 |
- self.drag_action = NullAction(self) |
|
301 |
+ self.drag_action = actions.NullAction(self) |
|
302 | 302 |
return True |
303 | 303 |
if event.keyval == Gdk.KEY_r: |
304 | 304 |
self.reload() |
... | ... |
@@ -351,12 +351,12 @@ class DotWidget(Gtk.DrawingArea): |
351 | 351 |
if event.button in (1, 2): # left or middle button |
352 | 352 |
modifiers = Gtk.accelerator_get_default_mod_mask() |
353 | 353 |
if state & modifiers == Gdk.ModifierType.CONTROL_MASK: |
354 |
- return ZoomAction |
|
354 |
+ return actions.ZoomAction |
|
355 | 355 |
elif state & modifiers == Gdk.ModifierType.SHIFT_MASK: |
356 |
- return ZoomAreaAction |
|
356 |
+ return actions.ZoomAreaAction |
|
357 | 357 |
else: |
358 |
- return PanAction |
|
359 |
- return NullAction |
|
358 |
+ return actions.PanAction |
|
359 |
+ return actions.NullAction |
|
360 | 360 |
|
361 | 361 |
def on_area_button_press(self, area, event): |
362 | 362 |
self.animation.stop() |
... | ... |
@@ -389,7 +389,7 @@ class DotWidget(Gtk.DrawingArea): |
389 | 389 |
|
390 | 390 |
def on_area_button_release(self, area, event): |
391 | 391 |
self.drag_action.on_button_release(event) |
392 |
- self.drag_action = NullAction(self) |
|
392 |
+ self.drag_action = actions.NullAction(self) |
|
393 | 393 |
x, y = int(event.x), int(event.y) |
394 | 394 |
if self.is_click(event): |
395 | 395 |
el = self.get_element(x, y) |
... | ... |
@@ -431,7 +431,7 @@ class DotWidget(Gtk.DrawingArea): |
431 | 431 |
self.zoom_to_fit() |
432 | 432 |
|
433 | 433 |
def animate_to(self, x, y): |
434 |
- self.animation = ZoomToAnimation(self, x, y) |
|
434 |
+ self.animation = animation.ZoomToAnimation(self, x, y) |
|
435 | 435 |
self.animation.start() |
436 | 436 |
|
437 | 437 |
def window2graph(self, x, y): |
... | ... |
@@ -47,10 +47,10 @@ from .elements import Graph |
47 | 47 |
class DotWidget(Gtk.DrawingArea): |
48 | 48 |
"""GTK widget that draws dot graphs.""" |
49 | 49 |
|
50 |
- #TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object. |
|
50 |
+ # TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object. |
|
51 | 51 |
__gsignals__ = { |
52 |
- 'clicked' : (GObject.SIGNAL_RUN_LAST, None, (str, object)), |
|
53 |
- 'error' : (GObject.SIGNAL_RUN_LAST, None, (str,)) |
|
52 |
+ 'clicked': (GObject.SIGNAL_RUN_LAST, None, (str, object)), |
|
53 |
+ 'error': (GObject.SIGNAL_RUN_LAST, None, (str,)) |
|
54 | 54 |
} |
55 | 55 |
|
56 | 56 |
filter = 'dot' |
... | ... |
@@ -64,7 +64,8 @@ class DotWidget(Gtk.DrawingArea): |
64 | 64 |
self.set_can_focus(True) |
65 | 65 |
|
66 | 66 |
self.connect("draw", self.on_draw) |
67 |
- self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK) |
|
67 |
+ self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | |
|
68 |
+ Gdk.EventMask.BUTTON_RELEASE_MASK) |
|
68 | 69 |
self.connect("button-press-event", self.on_area_button_press) |
69 | 70 |
self.connect("button-release-event", self.on_area_button_release) |
70 | 71 |
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | |
... | ... |
@@ -317,10 +318,11 @@ class DotWidget(Gtk.DrawingArea): |
317 | 318 |
return False |
318 | 319 |
|
319 | 320 |
print_settings = None |
321 |
+ |
|
320 | 322 |
def on_print(self, action=None): |
321 | 323 |
print_op = Gtk.PrintOperation() |
322 | 324 |
|
323 |
- if self.print_settings != None: |
|
325 |
+ if self.print_settings is not None: |
|
324 | 326 |
print_op.set_print_settings(self.print_settings) |
325 | 327 |
|
326 | 328 |
print_op.connect("begin_print", self.begin_print) |
... | ... |
@@ -346,7 +348,7 @@ class DotWidget(Gtk.DrawingArea): |
346 | 348 |
|
347 | 349 |
def get_drag_action(self, event): |
348 | 350 |
state = event.state |
349 |
- if event.button in (1, 2): # left or middle button |
|
351 |
+ if event.button in (1, 2): # left or middle button |
|
350 | 352 |
modifiers = Gtk.accelerator_get_default_mod_mask() |
351 | 353 |
if state & modifiers == Gdk.ModifierType.CONTROL_MASK: |
352 | 354 |
return ZoomAction |
... | ... |
@@ -376,8 +378,8 @@ class DotWidget(Gtk.DrawingArea): |
376 | 378 |
# for gtk's clicked event instead? |
377 | 379 |
deltax = self.pressx - event.x |
378 | 380 |
deltay = self.pressy - event.y |
379 |
- return (time.time() < self.presstime + click_timeout |
|
380 |
- and math.hypot(deltax, deltay) < click_fuzz) |
|
381 |
+ return (time.time() < self.presstime + click_timeout and |
|
382 |
+ math.hypot(deltax, deltay) < click_fuzz) |
|
381 | 383 |
|
382 | 384 |
def on_click(self, element, event): |
383 | 385 |
"""Override this method in subclass to process |
... | ... |
@@ -513,7 +515,8 @@ class DotWindow(Gtk.Window): |
513 | 515 |
actiongroup.add_actions(( |
514 | 516 |
('Open', Gtk.STOCK_OPEN, None, None, None, self.on_open), |
515 | 517 |
('Reload', Gtk.STOCK_REFRESH, None, None, None, self.on_reload), |
516 |
- ('Print', Gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.dotwidget.on_print), |
|
518 |
+ ('Print', Gtk.STOCK_PRINT, None, None, |
|
519 |
+ "Prints the currently visible part of the graph", self.dotwidget.on_print), |
|
517 | 520 |
('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, None, self.dotwidget.on_zoom_in), |
518 | 521 |
('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, None, self.dotwidget.on_zoom_out), |
519 | 522 |
('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, None, self.dotwidget.on_zoom_fit), |
... | ... |
@@ -521,7 +524,7 @@ class DotWindow(Gtk.Window): |
521 | 524 |
)) |
522 | 525 |
|
523 | 526 |
find_action = FindMenuToolAction("Find", None, |
524 |
- "Find a node by name", None) |
|
527 |
+ "Find a node by name", None) |
|
525 | 528 |
actiongroup.add_action(find_action) |
526 | 529 |
|
527 | 530 |
# Add the actiongroup to the uimanager |
... | ... |
@@ -547,8 +550,8 @@ class DotWindow(Gtk.Window): |
547 | 550 |
find_toolitem.add(self.textentry) |
548 | 551 |
|
549 | 552 |
self.textentry.set_activates_default(True) |
550 |
- self.textentry.connect ("activate", self.textentry_activate, self.textentry); |
|
551 |
- self.textentry.connect ("changed", self.textentry_changed, self.textentry); |
|
553 |
+ self.textentry.connect("activate", self.textentry_activate, self.textentry); |
|
554 |
+ self.textentry.connect("changed", self.textentry_changed, self.textentry); |
|
552 | 555 |
|
553 | 556 |
self.show_all() |
554 | 557 |
|
... | ... |
@@ -576,7 +579,7 @@ class DotWindow(Gtk.Window): |
576 | 579 |
dot_widget = self.dotwidget |
577 | 580 |
if not entry_text: |
578 | 581 |
dot_widget.set_highlight(None, search=True) |
579 |
- return; |
|
582 |
+ return |
|
580 | 583 |
|
581 | 584 |
found_items = self.find_text(entry_text) |
582 | 585 |
dot_widget.set_highlight(found_items, search=True) |
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,649 @@ |
1 |
+# Copyright 2008-2015 Jose Fonseca |
|
2 |
+# |
|
3 |
+# This program is free software: you can redistribute it and/or modify it |
|
4 |
+# under the terms of the GNU Lesser General Public License as published |
|
5 |
+# by the Free Software Foundation, either version 3 of the License, or |
|
6 |
+# (at your option) any later version. |
|
7 |
+# |
|
8 |
+# This program is distributed in the hope that it will be useful, |
|
9 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
+# GNU Lesser General Public License for more details. |
|
12 |
+# |
|
13 |
+# You should have received a copy of the GNU Lesser General Public License |
|
14 |
+# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
15 |
+# |
|
16 |
+ |
|
17 |
+import math |
|
18 |
+import os |
|
19 |
+import re |
|
20 |
+import subprocess |
|
21 |
+import sys |
|
22 |
+import time |
|
23 |
+ |
|
24 |
+import gi |
|
25 |
+gi.require_version('Gtk', '3.0') |
|
26 |
+gi.require_version('PangoCairo', '1.0') |
|
27 |
+ |
|
28 |
+from gi.repository import GLib |
|
29 |
+from gi.repository import GObject |
|
30 |
+from gi.repository import Gtk |
|
31 |
+from gi.repository import Gdk |
|
32 |
+ |
|
33 |
+# See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c |
|
34 |
+ |
|
35 |
+# For pygtk inspiration and guidance see: |
|
36 |
+# - http://mirageiv.berlios.de/ |
|
37 |
+# - http://comix.sourceforge.net/ |
|
38 |
+ |
|
39 |
+from . import actions |
|
40 |
+from ..dot.lexer import ParseError |
|
41 |
+from ..dot.parser import XDotParser |
|
42 |
+from .animation import NoAnimation, ZoomToAnimation |
|
43 |
+from .actions import NullAction, PanAction, ZoomAction, ZoomAreaAction |
|
44 |
+from .elements import Graph |
|
45 |
+ |
|
46 |
+ |
|
47 |
+class DotWidget(Gtk.DrawingArea): |
|
48 |
+ """GTK widget that draws dot graphs.""" |
|
49 |
+ |
|
50 |
+ #TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object. |
|
51 |
+ __gsignals__ = { |
|
52 |
+ 'clicked' : (GObject.SIGNAL_RUN_LAST, None, (str, object)), |
|
53 |
+ 'error' : (GObject.SIGNAL_RUN_LAST, None, (str,)) |
|
54 |
+ } |
|
55 |
+ |
|
56 |
+ filter = 'dot' |
|
57 |
+ |
|
58 |
+ def __init__(self): |
|
59 |
+ Gtk.DrawingArea.__init__(self) |
|
60 |
+ |
|
61 |
+ self.graph = Graph() |
|
62 |
+ self.openfilename = None |
|
63 |
+ |
|
64 |
+ self.set_can_focus(True) |
|
65 |
+ |
|
66 |
+ self.connect("draw", self.on_draw) |
|
67 |
+ self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK) |
|
68 |
+ self.connect("button-press-event", self.on_area_button_press) |
|
69 |
+ self.connect("button-release-event", self.on_area_button_release) |
|
70 |
+ self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | |
|
71 |
+ Gdk.EventMask.POINTER_MOTION_HINT_MASK | |
|
72 |
+ Gdk.EventMask.BUTTON_RELEASE_MASK | |
|
73 |
+ Gdk.EventMask.SCROLL_MASK) |
|
74 |
+ self.connect("motion-notify-event", self.on_area_motion_notify) |
|
75 |
+ self.connect("scroll-event", self.on_area_scroll_event) |
|
76 |
+ self.connect("size-allocate", self.on_area_size_allocate) |
|
77 |
+ |
|
78 |
+ self.connect('key-press-event', self.on_key_press_event) |
|
79 |
+ self.last_mtime = None |
|
80 |
+ |
|
81 |
+ GLib.timeout_add(1000, self.update) |
|
82 |
+ |
|
83 |
+ self.x, self.y = 0.0, 0.0 |
|
84 |
+ self.zoom_ratio = 1.0 |
|
85 |
+ self.zoom_to_fit_on_resize = False |
|
86 |
+ self.animation = NoAnimation(self) |
|
87 |
+ self.drag_action = NullAction(self) |
|
88 |
+ self.presstime = None |
|
89 |
+ self.highlight = None |
|
90 |
+ self.highlight_search = False |
|
91 |
+ |
|
92 |
+ def error_dialog(self, message): |
|
93 |
+ self.emit('error', message) |
|
94 |
+ |
|
95 |
+ def set_filter(self, filter): |
|
96 |
+ self.filter = filter |
|
97 |
+ |
|
98 |
+ def run_filter(self, dotcode): |
|
99 |
+ if not self.filter: |
|
100 |
+ return dotcode |
|
101 |
+ try: |
|
102 |
+ p = subprocess.Popen( |
|
103 |
+ [self.filter, '-Txdot'], |
|
104 |
+ stdin=subprocess.PIPE, |
|
105 |
+ stdout=subprocess.PIPE, |
|
106 |
+ stderr=subprocess.PIPE, |
|
107 |
+ shell=False, |
|
108 |
+ universal_newlines=False |
|
109 |
+ ) |
|
110 |
+ except OSError as exc: |
|
111 |
+ error = '%s: %s' % (self.filter, exc.strerror) |
|
112 |
+ p = subprocess.CalledProcessError(exc.errno, self.filter, exc.strerror) |
|
113 |
+ else: |
|
114 |
+ xdotcode, error = p.communicate(dotcode) |
|
115 |
+ error = error.rstrip() |
|
116 |
+ if error: |
|
117 |
+ error = error.decode() |
|
118 |
+ sys.stderr.write(error + '\n') |
|
119 |
+ if p.returncode != 0: |
|
120 |
+ self.error_dialog(error) |
|
121 |
+ return None |
|
122 |
+ return xdotcode |
|
123 |
+ |
|
124 |
+ def set_dotcode(self, dotcode, filename=None): |
|
125 |
+ self.openfilename = None |
|
126 |
+ if isinstance(dotcode, str): |
|
127 |
+ dotcode = dotcode.encode('utf-8') |
|
128 |
+ xdotcode = self.run_filter(dotcode) |
|
129 |
+ if xdotcode is None: |
|
130 |
+ return False |
|
131 |
+ try: |
|
132 |
+ self.set_xdotcode(xdotcode) |
|
133 |
+ except ParseError as ex: |
|
134 |
+ self.error_dialog(str(ex)) |
|
135 |
+ return False |
|
136 |
+ else: |
|
137 |
+ if filename is None: |
|
138 |
+ self.last_mtime = None |
|
139 |
+ else: |
|
140 |
+ self.last_mtime = os.stat(filename).st_mtime |
|
141 |
+ self.openfilename = filename |
|
142 |
+ return True |
|
143 |
+ |
|
144 |
+ def set_xdotcode(self, xdotcode): |
|
145 |
+ assert isinstance(xdotcode, bytes) |
|
146 |
+ parser = XDotParser(xdotcode) |
|
147 |
+ self.graph = parser.parse() |
|
148 |
+ self.zoom_image(self.zoom_ratio, center=True) |
|
149 |
+ |
|
150 |
+ def reload(self): |
|
151 |
+ if self.openfilename is not None: |
|
152 |
+ try: |
|
153 |
+ fp = open(self.openfilename, 'rt') |
|
154 |
+ self.set_dotcode(fp.read(), self.openfilename) |
|
155 |
+ fp.close() |
|
156 |
+ except IOError: |
|
157 |
+ pass |
|
158 |
+ |
|
159 |
+ def update(self): |
|
160 |
+ if self.openfilename is not None: |
|
161 |
+ current_mtime = os.stat(self.openfilename).st_mtime |
|
162 |
+ if current_mtime != self.last_mtime: |
|
163 |
+ self.last_mtime = current_mtime |
|
164 |
+ self.reload() |
|
165 |
+ return True |
|
166 |
+ |
|
167 |
+ def on_draw(self, widget, cr): |
|
168 |
+ cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) |
|
169 |
+ cr.paint() |
|
170 |
+ |
|
171 |
+ cr.save() |
|
172 |
+ rect = self.get_allocation() |
|
173 |
+ cr.translate(0.5*rect.width, 0.5*rect.height) |
|
174 |
+ cr.scale(self.zoom_ratio, self.zoom_ratio) |
|
175 |
+ cr.translate(-self.x, -self.y) |
|
176 |
+ |
|
177 |
+ self.graph.draw(cr, highlight_items=self.highlight) |
|
178 |
+ cr.restore() |
|
179 |
+ |
|
180 |
+ self.drag_action.draw(cr) |
|
181 |
+ |
|
182 |
+ return False |
|
183 |
+ |
|
184 |
+ def get_current_pos(self): |
|
185 |
+ return self.x, self.y |
|
186 |
+ |
|
187 |
+ def set_current_pos(self, x, y): |
|
188 |
+ self.x = x |
|
189 |
+ self.y = y |
|
190 |
+ self.queue_draw() |
|
191 |
+ |
|
192 |
+ def set_highlight(self, items, search=False): |
|
193 |
+ # Enable or disable search highlight |
|
194 |
+ if search: |
|
195 |
+ self.highlight_search = items is not None |
|
196 |
+ # Ignore cursor highlight while searching |
|
197 |
+ if self.highlight_search and not search: |
|
198 |
+ return |
|
199 |
+ if self.highlight != items: |
|
200 |
+ self.highlight = items |
|
201 |
+ self.queue_draw() |
|
202 |
+ |
|
203 |
+ def zoom_image(self, zoom_ratio, center=False, pos=None): |
|
204 |
+ # Constrain zoom ratio to a sane range to prevent numeric instability. |
|
205 |
+ zoom_ratio = min(zoom_ratio, 1E4) |
|
206 |
+ zoom_ratio = max(zoom_ratio, 1E-6) |
|
207 |
+ |
|
208 |
+ if center: |
|
209 |
+ self.x = self.graph.width/2 |
|
210 |
+ self.y = self.graph.height/2 |
|
211 |
+ elif pos is not None: |
|
212 |
+ rect = self.get_allocation() |
|
213 |
+ x, y = pos |
|
214 |
+ x -= 0.5*rect.width |
|
215 |
+ y -= 0.5*rect.height |
|
216 |
+ self.x += x / self.zoom_ratio - x / zoom_ratio |
|
217 |
+ self.y += y / self.zoom_ratio - y / zoom_ratio |
|
218 |
+ self.zoom_ratio = zoom_ratio |
|
219 |
+ self.zoom_to_fit_on_resize = False |
|
220 |
+ self.queue_draw() |
|
221 |
+ |
|
222 |
+ def zoom_to_area(self, x1, y1, x2, y2): |
|
223 |
+ rect = self.get_allocation() |
|
224 |
+ width = abs(x1 - x2) |
|
225 |
+ height = abs(y1 - y2) |
|
226 |
+ if width == 0 and height == 0: |
|
227 |
+ self.zoom_ratio *= self.ZOOM_INCREMENT |
|
228 |
+ else: |
|
229 |
+ self.zoom_ratio = min( |
|
230 |
+ float(rect.width)/float(width), |
|
231 |
+ float(rect.height)/float(height) |
|
232 |
+ ) |
|
233 |
+ self.zoom_to_fit_on_resize = False |
|
234 |
+ self.x = (x1 + x2) / 2 |
|
235 |
+ self.y = (y1 + y2) / 2 |
|
236 |
+ self.queue_draw() |
|
237 |
+ |
|
238 |
+ def zoom_to_fit(self): |
|
239 |
+ rect = self.get_allocation() |
|
240 |
+ rect.x += self.ZOOM_TO_FIT_MARGIN |
|
241 |
+ rect.y += self.ZOOM_TO_FIT_MARGIN |
|
242 |
+ rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN |
|
243 |
+ rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN |
|
244 |
+ zoom_ratio = min( |
|
245 |
+ float(rect.width)/float(self.graph.width), |
|
246 |
+ float(rect.height)/float(self.graph.height) |
|
247 |
+ ) |
|
248 |
+ self.zoom_image(zoom_ratio, center=True) |
|
249 |
+ self.zoom_to_fit_on_resize = True |
|
250 |
+ |
|
251 |
+ ZOOM_INCREMENT = 1.25 |
|
252 |
+ ZOOM_TO_FIT_MARGIN = 12 |
|
253 |
+ |
|
254 |
+ def on_zoom_in(self, action): |
|
255 |
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) |
|
256 |
+ |
|
257 |
+ def on_zoom_out(self, action): |
|
258 |
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) |
|
259 |
+ |
|
260 |
+ def on_zoom_fit(self, action): |
|
261 |
+ self.zoom_to_fit() |
|
262 |
+ |
|
263 |
+ def on_zoom_100(self, action): |
|
264 |
+ self.zoom_image(1.0) |
|
265 |
+ |
|
266 |
+ POS_INCREMENT = 100 |
|
267 |
+ |
|
268 |
+ def on_key_press_event(self, widget, event): |
|
269 |
+ if event.keyval == Gdk.KEY_Left: |
|
270 |
+ self.x -= self.POS_INCREMENT/self.zoom_ratio |
|
271 |
+ self.queue_draw() |
|
272 |
+ return True |
|
273 |
+ if event.keyval == Gdk.KEY_Right: |
|
274 |
+ self.x += self.POS_INCREMENT/self.zoom_ratio |
|
275 |
+ self.queue_draw() |
|
276 |
+ return True |
|
277 |
+ if event.keyval == Gdk.KEY_Up: |
|
278 |
+ self.y -= self.POS_INCREMENT/self.zoom_ratio |
|
279 |
+ self.queue_draw() |
|
280 |
+ return True |
|
281 |
+ if event.keyval == Gdk.KEY_Down: |
|
282 |
+ self.y += self.POS_INCREMENT/self.zoom_ratio |
|
283 |
+ self.queue_draw() |
|
284 |
+ return True |
|
285 |
+ if event.keyval in (Gdk.KEY_Page_Up, |
|
286 |
+ Gdk.KEY_plus, |
|
287 |
+ Gdk.KEY_equal, |
|
288 |
+ Gdk.KEY_KP_Add): |
|
289 |
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) |
|
290 |
+ self.queue_draw() |
|
291 |
+ return True |
|
292 |
+ if event.keyval in (Gdk.KEY_Page_Down, |
|
293 |
+ Gdk.KEY_minus, |
|
294 |
+ Gdk.KEY_KP_Subtract): |
|
295 |
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) |
|
296 |
+ self.queue_draw() |
|
297 |
+ return True |
|
298 |
+ if event.keyval == Gdk.KEY_Escape: |
|
299 |
+ self.drag_action.abort() |
|
300 |
+ self.drag_action = NullAction(self) |
|
301 |
+ return True |
|
302 |
+ if event.keyval == Gdk.KEY_r: |
|
303 |
+ self.reload() |
|
304 |
+ return True |
|
305 |
+ if event.keyval == Gdk.KEY_f: |
|
306 |
+ win = widget.get_toplevel() |
|
307 |
+ find_toolitem = win.uimanager.get_widget('/ToolBar/Find') |
|
308 |
+ textentry = find_toolitem.get_children() |
|
309 |
+ win.set_focus(textentry[0]) |
|
310 |
+ return True |
|
311 |
+ if event.keyval == Gdk.KEY_q: |
|
312 |
+ Gtk.main_quit() |
|
313 |
+ return True |
|
314 |
+ if event.keyval == Gdk.KEY_p: |
|
315 |
+ self.on_print() |
|
316 |
+ return True |
|
317 |
+ return False |
|
318 |
+ |
|
319 |
+ print_settings = None |
|
320 |
+ def on_print(self, action=None): |
|
321 |
+ print_op = Gtk.PrintOperation() |
|
322 |
+ |
|
323 |
+ if self.print_settings != None: |
|
324 |
+ print_op.set_print_settings(self.print_settings) |
|
325 |
+ |
|
326 |
+ print_op.connect("begin_print", self.begin_print) |
|
327 |
+ print_op.connect("draw_page", self.draw_page) |
|
328 |
+ |
|
329 |
+ res = print_op.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.get_toplevel()) |
|
330 |
+ if res == Gtk.PrintOperationResult.APPLY: |
|
331 |
+ self.print_settings = print_op.get_print_settings() |
|
332 |
+ |
|
333 |
+ def begin_print(self, operation, context): |
|
334 |
+ operation.set_n_pages(1) |
|
335 |
+ return True |
|
336 |
+ |
|
337 |
+ def draw_page(self, operation, context, page_nr): |
|
338 |
+ cr = context.get_cairo_context() |
|
339 |
+ |
|
340 |
+ rect = self.get_allocation() |
|
341 |
+ cr.translate(0.5*rect.width, 0.5*rect.height) |
|
342 |
+ cr.scale(self.zoom_ratio, self.zoom_ratio) |
|
343 |
+ cr.translate(-self.x, -self.y) |
|
344 |
+ |
|
345 |
+ self.graph.draw(cr, highlight_items=self.highlight) |
|
346 |
+ |
|
347 |
+ def get_drag_action(self, event): |
|
348 |
+ state = event.state |
|
349 |
+ if event.button in (1, 2): # left or middle button |
|
350 |
+ modifiers = Gtk.accelerator_get_default_mod_mask() |
|
351 |
+ if state & modifiers == Gdk.ModifierType.CONTROL_MASK: |
|
352 |
+ return ZoomAction |
|
353 |
+ elif state & modifiers == Gdk.ModifierType.SHIFT_MASK: |
|
354 |
+ return ZoomAreaAction |
|
355 |
+ else: |
|
356 |
+ return PanAction |
|
357 |
+ return NullAction |
|
358 |
+ |
|
359 |
+ def on_area_button_press(self, area, event): |
|
360 |
+ self.animation.stop() |
|
361 |
+ self.drag_action.abort() |
|
362 |
+ action_type = self.get_drag_action(event) |
|
363 |
+ self.drag_action = action_type(self) |
|
364 |
+ self.drag_action.on_button_press(event) |
|
365 |
+ self.presstime = time.time() |
|
366 |
+ self.pressx = event.x |
|
367 |
+ self.pressy = event.y |
|
368 |
+ return False |
|
369 |
+ |
|
370 |
+ def is_click(self, event, click_fuzz=4, click_timeout=1.0): |
|
371 |
+ assert event.type == Gdk.EventType.BUTTON_RELEASE |
|
372 |
+ if self.presstime is None: |
|
373 |
+ # got a button release without seeing the press? |
|
374 |
+ return False |
|
375 |
+ # XXX instead of doing this complicated logic, shouldn't we listen |
|
376 |
+ # for gtk's clicked event instead? |
|
377 |
+ deltax = self.pressx - event.x |
|
378 |
+ deltay = self.pressy - event.y |
|
379 |
+ return (time.time() < self.presstime + click_timeout |
|
380 |
+ and math.hypot(deltax, deltay) < click_fuzz) |
|
381 |
+ |
|
382 |
+ def on_click(self, element, event): |
|
383 |
+ """Override this method in subclass to process |
|
384 |
+ click events. Note that element can be None |
|
385 |
+ (click on empty space).""" |
|
386 |
+ return False |
|
387 |
+ |
|
388 |
+ def on_area_button_release(self, area, event): |
|
389 |
+ self.drag_action.on_button_release(event) |
|
390 |
+ self.drag_action = NullAction(self) |
|
391 |
+ x, y = int(event.x), int(event.y) |
|
392 |
+ if self.is_click(event): |
|
393 |
+ el = self.get_element(x, y) |
|
394 |
+ if self.on_click(el, event): |
|
395 |
+ return True |
|
396 |
+ |
|
397 |
+ if event.button == 1: |
|
398 |
+ url = self.get_url(x, y) |
|
399 |
+ if url is not None: |
|
400 |
+ self.emit('clicked', url.url, event) |
|
401 |
+ else: |
|
402 |
+ jump = self.get_jump(x, y) |
|
403 |
+ if jump is not None: |
|
404 |
+ self.animate_to(jump.x, jump.y) |
|
405 |
+ |
|
406 |
+ return True |
|
407 |
+ |
|
408 |
+ if event.button == 1 or event.button == 2: |
|
409 |
+ return True |
|
410 |
+ return False |
|
411 |
+ |
|
412 |
+ def on_area_scroll_event(self, area, event): |
|
413 |
+ if event.direction == Gdk.ScrollDirection.UP: |
|
414 |
+ self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT, |
|
415 |
+ pos=(event.x, event.y)) |
|
416 |
+ return True |
|
417 |
+ if event.direction == Gdk.ScrollDirection.DOWN: |
|
418 |
+ self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT, |
|
419 |
+ pos=(event.x, event.y)) |
|
420 |
+ return True |
|
421 |
+ return False |
|
422 |
+ |
|
423 |
+ def on_area_motion_notify(self, area, event): |
|
424 |
+ self.drag_action.on_motion_notify(event) |
|
425 |
+ return True |
|
426 |
+ |
|
427 |
+ def on_area_size_allocate(self, area, allocation): |
|
428 |
+ if self.zoom_to_fit_on_resize: |
|
429 |
+ self.zoom_to_fit() |
|
430 |
+ |
|
431 |
+ def animate_to(self, x, y): |
|
432 |
+ self.animation = ZoomToAnimation(self, x, y) |
|
433 |
+ self.animation.start() |
|
434 |
+ |
|
435 |
+ def window2graph(self, x, y): |
|
436 |
+ rect = self.get_allocation() |
|
437 |
+ x -= 0.5*rect.width |
|
438 |
+ y -= 0.5*rect.height |
|
439 |
+ x /= self.zoom_ratio |
|
440 |
+ y /= self.zoom_ratio |
|
441 |
+ x += self.x |
|
442 |
+ y += self.y |
|
443 |
+ return x, y |
|
444 |
+ |
|
445 |
+ def get_element(self, x, y): |
|
446 |
+ x, y = self.window2graph(x, y) |
|
447 |
+ return self.graph.get_element(x, y) |
|
448 |
+ |
|
449 |
+ def get_url(self, x, y): |
|
450 |
+ x, y = self.window2graph(x, y) |
|
451 |
+ return self.graph.get_url(x, y) |
|
452 |
+ |
|
453 |
+ def get_jump(self, x, y): |
|
454 |
+ x, y = self.window2graph(x, y) |
|
455 |
+ return self.graph.get_jump(x, y) |
|
456 |
+ |
|
457 |
+ |
|
458 |
+class FindMenuToolAction(Gtk.Action): |
|
459 |
+ __gtype_name__ = "FindMenuToolAction" |
|
460 |
+ |
|
461 |
+ def do_create_tool_item(self): |
|
462 |
+ return Gtk.ToolItem() |
|
463 |
+ |
|
464 |
+ |
|
465 |
+class DotWindow(Gtk.Window): |
|
466 |
+ |
|
467 |
+ ui = ''' |
|
468 |
+ <ui> |
|
469 |
+ <toolbar name="ToolBar"> |
|
470 |
+ <toolitem action="Open"/> |
|
471 |
+ <toolitem action="Reload"/> |
|
472 |
+ <toolitem action="Print"/> |
|
473 |
+ <separator/> |
|
474 |
+ <toolitem action="ZoomIn"/> |
|
475 |
+ <toolitem action="ZoomOut"/> |
|
476 |
+ <toolitem action="ZoomFit"/> |
|
477 |
+ <toolitem action="Zoom100"/> |
|
478 |
+ <separator/> |
|
479 |
+ <toolitem name="Find" action="Find"/> |
|
480 |
+ </toolbar> |
|
481 |
+ </ui> |
|
482 |
+ ''' |
|
483 |
+ |
|
484 |
+ base_title = 'Dot Viewer' |
|
485 |
+ |
|
486 |
+ def __init__(self, widget=None, width=512, height=512): |
|
487 |
+ Gtk.Window.__init__(self) |
|
488 |
+ |
|
489 |
+ self.graph = Graph() |
|
490 |
+ |
|
491 |
+ window = self |
|
492 |
+ |
|
493 |
+ window.set_title(self.base_title) |
|
494 |
+ window.set_default_size(width, height) |
|
495 |
+ vbox = Gtk.VBox() |
|
496 |
+ window.add(vbox) |
|
497 |
+ |
|
498 |
+ self.dotwidget = widget or DotWidget() |
|
499 |
+ self.dotwidget.connect("error", lambda e, m: self.error_dialog(m)) |
|
500 |
+ |
|
501 |
+ # Create a UIManager instance |
|
502 |
+ uimanager = self.uimanager = Gtk.UIManager() |
|
503 |
+ |
|
504 |
+ # Add the accelerator group to the toplevel window |
|
505 |
+ accelgroup = uimanager.get_accel_group() |
|
506 |
+ window.add_accel_group(accelgroup) |
|
507 |
+ |
|
508 |
+ # Create an ActionGroup |
|
509 |
+ actiongroup = Gtk.ActionGroup('Actions') |
|
510 |
+ self.actiongroup = actiongroup |
|
511 |
+ |
|
512 |
+ # Create actions |
|
513 |
+ actiongroup.add_actions(( |
|
514 |
+ ('Open', Gtk.STOCK_OPEN, None, None, None, self.on_open), |
|
515 |
+ ('Reload', Gtk.STOCK_REFRESH, None, None, None, self.on_reload), |
|
516 |
+ ('Print', Gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.dotwidget.on_print), |
|
517 |
+ ('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, None, self.dotwidget.on_zoom_in), |
|
518 |
+ ('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, None, self.dotwidget.on_zoom_out), |
|
519 |
+ ('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, None, self.dotwidget.on_zoom_fit), |
|
520 |
+ ('Zoom100', Gtk.STOCK_ZOOM_100, None, None, None, self.dotwidget.on_zoom_100), |
|
521 |
+ )) |
|
522 |
+ |
|
523 |
+ find_action = FindMenuToolAction("Find", None, |
|
524 |
+ "Find a node by name", None) |
|
525 |
+ actiongroup.add_action(find_action) |
|
526 |
+ |
|
527 |
+ # Add the actiongroup to the uimanager |
|
528 |
+ uimanager.insert_action_group(actiongroup, 0) |
|
529 |
+ |
|
530 |
+ # Add a UI descrption |
|
531 |
+ uimanager.add_ui_from_string(self.ui) |
|
532 |
+ |
|
533 |
+ # Create a Toolbar |
|
534 |
+ toolbar = uimanager.get_widget('/ToolBar') |
|
535 |
+ vbox.pack_start(toolbar, False, False, 0) |
|
536 |
+ |
|
537 |
+ vbox.pack_start(self.dotwidget, True, True, 0) |
|
538 |
+ |
|
539 |
+ self.last_open_dir = "." |
|
540 |
+ |
|
541 |
+ self.set_focus(self.dotwidget) |
|
542 |
+ |
|
543 |
+ # Add Find text search |
|
544 |
+ find_toolitem = uimanager.get_widget('/ToolBar/Find') |
|
545 |
+ self.textentry = Gtk.Entry(max_length=20) |
|
546 |
+ self.textentry.set_icon_from_stock(0, Gtk.STOCK_FIND) |
|
547 |
+ find_toolitem.add(self.textentry) |
|
548 |
+ |
|
549 |
+ self.textentry.set_activates_default(True) |
|
550 |
+ self.textentry.connect ("activate", self.textentry_activate, self.textentry); |
|
551 |
+ self.textentry.connect ("changed", self.textentry_changed, self.textentry); |
|
552 |
+ |
|
553 |
+ self.show_all() |
|
554 |
+ |
|
555 |
+ def find_text(self, entry_text): |
|
556 |
+ found_items = [] |
|
557 |
+ dot_widget = self.dotwidget |
|
558 |
+ regexp = re.compile(entry_text) |
|
559 |
+ for element in dot_widget.graph.nodes + dot_widget.graph.edges: |
|
560 |
+ if element.search_text(regexp): |
|
561 |
+ found_items.append(element) |
|
562 |
+ return found_items |
|
563 |
+ |
|
564 |
+ def textentry_changed(self, widget, entry): |
|
565 |
+ entry_text = entry.get_text() |
|
566 |
+ dot_widget = self.dotwidget |
|
567 |
+ if not entry_text: |
|
568 |
+ dot_widget.set_highlight(None, search=True) |
|
569 |
+ return |
|
570 |
+ |
|
571 |
+ found_items = self.find_text(entry_text) |
|
572 |
+ dot_widget.set_highlight(found_items, search=True) |
|
573 |
+ |
|
574 |
+ def textentry_activate(self, widget, entry): |
|
575 |
+ entry_text = entry.get_text() |
|
576 |
+ dot_widget = self.dotwidget |
|
577 |
+ if not entry_text: |
|
578 |
+ dot_widget.set_highlight(None, search=True) |
|
579 |
+ return; |
|
580 |
+ |
|
581 |
+ found_items = self.find_text(entry_text) |
|
582 |
+ dot_widget.set_highlight(found_items, search=True) |
|
583 |
+ if(len(found_items) == 1): |
|
584 |
+ dot_widget.animate_to(found_items[0].x, found_items[0].y) |
|
585 |
+ |
|
586 |
+ def set_filter(self, filter): |
|
587 |
+ self.dotwidget.set_filter(filter) |
|
588 |
+ |
|
589 |
+ def set_dotcode(self, dotcode, filename=None): |
|
590 |
+ if self.dotwidget.set_dotcode(dotcode, filename): |
|
591 |
+ self.update_title(filename) |
|
592 |
+ self.dotwidget.zoom_to_fit() |
|
593 |
+ |
|
594 |
+ def set_xdotcode(self, xdotcode, filename=None): |
|
595 |
+ if self.dotwidget.set_xdotcode(xdotcode): |
|
596 |
+ self.update_title(filename) |
|
597 |
+ self.dotwidget.zoom_to_fit() |
|
598 |
+ |
|
599 |
+ def update_title(self, filename=None): |
|
600 |
+ if filename is None: |
|
601 |
+ self.set_title(self.base_title) |
|
602 |
+ else: |
|
603 |
+ self.set_title(os.path.basename(filename) + ' - ' + self.base_title) |
|
604 |
+ |
|
605 |
+ def open_file(self, filename): |
|
606 |
+ try: |
|
607 |
+ fp = open(filename, 'rt') |
|
608 |
+ self.set_dotcode(fp.read(), filename) |
|
609 |
+ fp.close() |
|
610 |
+ except IOError as ex: |
|
611 |
+ self.error_dialog(str(ex)) |
|
612 |
+ |
|
613 |
+ def on_open(self, action): |
|
614 |
+ chooser = Gtk.FileChooserDialog(parent=self, |
|
615 |
+ title="Open dot File", |
|
616 |
+ action=Gtk.FileChooserAction.OPEN, |
|
617 |
+ buttons=(Gtk.STOCK_CANCEL, |
|
618 |
+ Gtk.ResponseType.CANCEL, |
|
619 |
+ Gtk.STOCK_OPEN, |
|
620 |
+ Gtk.ResponseType.OK)) |
|
621 |
+ chooser.set_default_response(Gtk.ResponseType.OK) |
|
622 |
+ chooser.set_current_folder(self.last_open_dir) |
|
623 |
+ filter = Gtk.FileFilter() |
|
624 |
+ filter.set_name("Graphviz dot files") |
|
625 |
+ filter.add_pattern("*.dot") |
|
626 |
+ chooser.add_filter(filter) |
|
627 |
+ filter = Gtk.FileFilter() |
|
628 |
+ filter.set_name("All files") |
|
629 |
+ filter.add_pattern("*") |
|
630 |
+ chooser.add_filter(filter) |
|
631 |
+ if chooser.run() == Gtk.ResponseType.OK: |
|
632 |
+ filename = chooser.get_filename() |
|
633 |
+ self.last_open_dir = chooser.get_current_folder() |
|
634 |
+ chooser.destroy() |
|
635 |
+ self.open_file(filename) |
|
636 |
+ else: |
|
637 |
+ chooser.destroy() |
|
638 |
+ |
|
639 |
+ def on_reload(self, action): |
|
640 |
+ self.dotwidget.reload() |
|
641 |
+ |
|
642 |
+ def error_dialog(self, message): |
|
643 |
+ dlg = Gtk.MessageDialog(parent=self, |
|
644 |
+ type=Gtk.MessageType.ERROR, |
|
645 |
+ message_format=message, |
|
646 |
+ buttons=Gtk.ButtonsType.OK) |
|
647 |
+ dlg.set_title(self.base_title) |
|
648 |
+ dlg.run() |
|
649 |
+ dlg.destroy() |