Browse code

Keep zoom to fit on reload

Robert Cranston authored on 25/08/2022 21:22:09
Showing 1 changed files
... ...
@@ -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):
Browse code

Add tooltips to various toolbar buttons.

Moritz Meier authored on 19/07/2022 12:22:48 • José Fonseca committed on 19/07/2022 12:48:10
Showing 1 changed files
... ...
@@ -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
 
Browse code

return xdotcode anyway

Be more tolerate. Return dot output if any, even there was an error.

Costa Shulyupin authored on 11/04/2022 08:37:49 • José Fonseca committed on 11/06/2022 05:54:12
Showing 1 changed files
... ...
@@ -126,7 +126,6 @@ class DotWidget(Gtk.DrawingArea):
126 126
             sys.stderr.write(error + '\n')
127 127
         if p.returncode != 0:
128 128
             self.error_dialog(error)
129
-            return None
130 129
         return xdotcode
131 130
 
132 131
     def _set_dotcode(self, dotcode, filename=None, center=True):
Browse code

Bind "W" key to zoom_to_fit.

Arthur A. Gleckler authored on 07/03/2022 23:17:07 • José Fonseca committed on 08/03/2022 08:57:47
Showing 1 changed files
... ...
@@ -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
Browse code

Remove unnecessary max_length from text search.

With this commit it's now possible to search text of any length.

Pietro Fezzardi authored on 24/02/2022 13:23:05 • José Fonseca committed on 24/02/2022 14:17:01
Showing 1 changed files
... ...
@@ -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
 
Browse code

Set PNG to default export format and add more formats

Meier, Moritz authored on 08/10/2021 17:09:55 • José Fonseca committed on 27/11/2021 15:12:34
Showing 1 changed files
... ...
@@ -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()]
Browse code

Introduce "Export to other format" feature

Meier, Moritz authored on 08/10/2021 16:42:39 • José Fonseca committed on 27/11/2021 15:12:34
Showing 1 changed files
... ...
@@ -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()
Browse code

Support gv extensions in file chooser

The project switched to that since *.dot is taken by Microsoft Word:
https://marc.info/?l=graphviz-devel&m=129418103126092

Jan Tojnar authored on 27/11/2021 05:25:31 • José Fonseca committed on 27/11/2021 11:13:52
Showing 1 changed files
... ...
@@ -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()
Browse code

Fix WM_CLASS.

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.

Arthur A. Gleckler authored on 21/10/2021 17:13:12 • José Fonseca committed on 22/10/2021 19:50:41
Showing 1 changed files
... ...
@@ -568,6 +568,7 @@ class DotWindow(Gtk.Window):
568 568
 
569 569
         window.set_title(self.base_title)
570 570
         window.set_default_size(width, height)
571
+        window.set_wmclass("xdot", "xdot")
571 572
         vbox = Gtk.VBox()
572 573
         window.add(vbox)
573 574
 
Browse code

Handle xdot backslashes correctly.

Irrespectively of graphviz version.

Fixes https://github.com/jrfonseca/xdot.py/issues/92

Jose Fonseca authored on 28/09/2021 12:19:49
Showing 1 changed files
... ...
@@ -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
 
Browse code

Implement smooth zooming

mxmgh authored on 09/01/2021 03:16:35 • José Fonseca committed on 12/02/2021 10:41:09
Showing 1 changed files
... ...
@@ -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
 
Browse code

Pressing RET with no search text cancels focus.

That way, the user can use keyboard shortcuts again.

Arthur A. Gleckler authored on 04/09/2020 20:58:28 • José Fonseca committed on 21/11/2020 11:33:15
Showing 1 changed files
... ...
@@ -672,6 +672,7 @@ class DotWindow(Gtk.Window):
672 672
         dot_widget = self.dotwidget
673 673
         if not entry_text:
674 674
             dot_widget.set_highlight(None, search=True)
675
+            self.set_focus(self.dotwidget)
675 676
             return
676 677
 
677 678
         found_items = self.find_text(entry_text)
Browse code

Handle multiple search results

* Always pan to the first result
* Show the number of results
* Provide a button to pan to the next result

Christian Hattemer authored on 30/04/2015 16:50:26 • Jose Fonseca committed on 17/08/2020 13:06:53
Showing 1 changed files
... ...
@@ -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)
Browse code

Add toggle-toolbar feature and hide-toolbar commandline parameter.

Moritz Meier authored on 20/05/2020 21:21:59 • José Fonseca committed on 11/07/2020 11:53:07
Showing 1 changed files
... ...
@@ -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
Browse code

Ignore OSErrors when updating view

Fixes #76.

Jed Liu authored on 15/05/2020 23:15:15 • José Fonseca committed on 16/05/2020 08:35:04
Showing 1 changed files
... ...
@@ -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()
Browse code

History support

iamahuman authored on 04/12/2018 09:57:06 • Jose Fonseca committed on 25/03/2019 06:54:06
Showing 1 changed files
... ...
@@ -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)
Browse code

Faster rendering of large graphs

iamahuman authored on 23/11/2018 10:47:03 • José Fonseca committed on 01/12/2018 16:57:56
Showing 1 changed files
... ...
@@ -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
Browse code

xdot: Use theme background on DotWidget.

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.

astian authored on 03/10/2018 03:19:00 • Jose Fonseca committed on 11/10/2018 12:47:55
Showing 1 changed files
... ...
@@ -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)
Browse code

Don't recenter when we reload a file.

Fixes https://github.com/jrfonseca/xdot.py/issues/56

Benjamin Redelings authored on 22/05/2018 18:48:18 • Jose Fonseca committed on 16/06/2018 21:16:44
Showing 1 changed files
... ...
@@ -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
Browse code

Don't clobber openfilename when reloading.

Fixes https://github.com/jrfonseca/xdot.py/issues/50

Jose Fonseca authored on 11/01/2018 10:55:12
Showing 1 changed files
... ...
@@ -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
Browse code

Prevent AttributeError when processing Popen exceptions.

As found and suggested by Overmind JIANG.

Fixes https://github.com/jrfonseca/xdot.py/issues/47

Jose Fonseca authored on 18/10/2017 21:14:52
Showing 1 changed files
... ...
@@ -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)
Browse code

Ensure DOT files are read and passed as bytes.

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

Jose Fonseca authored on 06/07/2017 16:18:31
Showing 1 changed files
... ...
@@ -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:
Browse code

Make internal imports more explicit

Peter Hill authored on 02/07/2016 10:30:50 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -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):
Browse code

Fix most flake8 errors

Peter Hill authored on 02/07/2016 10:27:27 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -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)
Browse code

Cleaner splitting into separate modules

Peter Hill authored on 02/07/2016 09:45:05 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
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()