1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2017, Data61
5# Commonwealth Scientific and Industrial Research Organisation (CSIRO)
6# ABN 41 687 119 230.
7#
8# This software may be distributed and modified according to the terms of
9# the BSD 2-Clause license. Note that NO WARRANTY is provided.
10# See "LICENSE_BSD2.txt" for details.
11#
12# @TAG(DATA61_BSD)
13
14import json, os, math
15
16from PyQt5 import QtWidgets, QtGui, QtCore, QtSvg
17
18from graphviz import *
19import pydotplus
20
21from camkes.ast import *
22from Model.AST_Model import ASTModel
23from Model import Common
24
25from Connection_Widget import ConnectionWidget, DataportWidget, ProcedureWidget, EventWidget
26from Instance_Widget import InstanceWidget
27from Save_Option_Dialog import SaveOptionDialog
28from Interface.Property import PropertyInterface
29
30class GraphWidget(QtWidgets.QGraphicsView):
31    """
32    GraphWidget - Manages the instances and connection widgets within a scene - and keeps it in sync with AST.
33    """
34
35    # --- PROPERTIES ---
36
37    @property
38    def connection_widgets(self):
39        if self._connection_widgets is None:
40            self._connection_widgets = []
41        return self._connection_widgets
42
43    @property
44    def widget_instances(self):
45        if self._widget_instances is None:
46            self._widget_instances = []
47        return self._widget_instances
48
49    @widget_instances.setter
50    def widget_instances(self, value):
51        assert isinstance(value, list)
52        self._widget_instances = value
53
54    @property
55    def context_menu(self):
56        # Setting up context (right click) menu
57        if self._context_menu is None:
58            menu = QtWidgets.QMenu()
59            proxy_menu = self.scene().addWidget(menu)
60            proxy_menu.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
61            proxy_menu.setZValue(10)
62            self._context_menu = proxy_menu
63        return self._context_menu
64
65    @property
66    def zoom_in_button(self):
67        if self._zoom_in is None:
68            self._zoom_in = QtWidgets.QPushButton("Zoom &In", self)
69            self._zoom_in.setAutoRepeat(True)
70            self._zoom_in.clicked.connect(self.zoom_in)
71            self._zoom_in.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Minus))
72            self._zoom_in.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Equal))
73            self.update_outer_ui()
74        return self._zoom_in
75
76    @property
77    def zoom_out_buttom(self):
78        if self._zoom_out is None:
79            self._zoom_out = QtWidgets.QPushButton("Zoom &Out", self)
80            self._zoom_out.setAutoRepeat(True)
81            self._zoom_out.clicked.connect(self.zoom_out)
82            self._zoom_out.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Minus))
83            self.update_outer_ui()
84        return self._zoom_out
85
86    @property
87    def save_picture_button(self):
88        if self._save_picture_button is None:
89            self._save_picture_button = QtWidgets.QPushButton("Save Image", self)
90            self._save_picture_button.setAutoRepeat(False)
91            self._save_picture_button.clicked.connect(self.save_picture)
92            self._save_picture_button.setToolTip("Save the image as a PNG or SVG")
93            self.update_outer_ui()
94        return self._save_picture_button
95
96    @property
97    def autolayout_button(self):
98        if self._autolayout_button is None:
99            self._autolayout_button = QtWidgets.QPushButton("Auto&layout", self)
100            self._autolayout_button.setAutoRepeat(False)
101            self._autolayout_button.clicked.connect(self.autolayout)
102            self._autolayout_button.setShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_L))
103            self._autolayout_button.setToolTip("Reposition instances using graphviz calculations")
104            self.update_outer_ui()
105        return self._autolayout_button
106
107    @property
108    def ast(self):
109        return self._ast
110
111    @ast.setter
112    def ast(self, value):
113        assert isinstance(value, LiftedAST)
114        self._ast = value
115
116        # When AST is set, graph is updated.
117        self.sync_model()
118
119        # Layout
120        if os.path.isfile("%s.visualCAmkES.layout" % self.get_root_location()):
121            # If layout exist, place nodes in original positions. New nodes are placed in middle
122            self.layout_from_file()
123        else:
124            # If layout doesn't exist use graphviz to place nodes in position
125            self.autolayout()
126
127    # ----- ACTIONS -----
128    @property
129    def export_action(self):
130        if self._export_action is None:
131            self._export_action = QtWidgets.QAction("Export as Image", self)
132            self._export_action.setShortcut(QtGui.QKeySequence("Ctrl+E"))
133            self._export_action.setStatusTip("Save the graph as a PNG or SVG file")
134            self._export_action.setToolTip("Save the graph as a PNG or SVG file")
135            self._export_action.triggered.connect(self.save_picture)
136        return self._export_action
137
138    @property
139    def show_components_action(self):
140        if self._show_components_action is None:
141            self._show_components_action = QtWidgets.QAction("Show all components", self)
142            self._show_components_action.setStatusTip("Set all components to not hidden")
143            self._show_components_action.setToolTip("Set all components to not hidden")
144            self._show_components_action.triggered.connect(self.show_all_components)
145        return self._show_components_action
146
147    # ---- OTHER WIDGETS ----
148    @property
149    def property_widget_dock(self):
150        return self._property_widget_dock
151
152    # --- INITIALISATION ---
153
154    def __init__(self, property_widget_dock):
155        super(GraphWidget, self).__init__()
156        self._connection_widgets = None
157        self._widget_instances = None
158        self._zoom_in = None
159        self._zoom_out = None
160        self._save_picture_button = None
161        self._export_action = None
162        self._show_components_action = None
163        self._autolayout_button = None
164        self._ast = None
165        self._color_seed = None
166        self._property_widget_dock = property_widget_dock
167
168        self._context_menu = None
169
170        # Create new scene
171        scene = QtWidgets.QGraphicsScene(self)
172        scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)  # TODO: Not sure if this is necessary
173        scene.setSceneRect(0, 0, 50, 50)  # Random size, should be given when controller renders
174        self.setScene(scene)
175
176        self.setMinimumSize(500, 500)
177
178        # Update button positions
179        self.update_outer_ui()
180
181    # --- Model Methods ---
182
183    def sync_model(self):
184        """
185        Updates view to the model representation
186        """
187
188        # Get assembly from the ast
189        ast_assembly = self.ast.assembly
190        assert isinstance(ast_assembly, Assembly)
191
192        # --- For each instance, create a node in the graph & a widget. ---
193        instance_list_copy = list(ast_assembly.instances)
194
195        widgets_to_remove = []
196
197        # Update widget's instance object counterpart
198        for widget in self.widget_instances:
199            assert isinstance(widget, InstanceWidget)
200            new_instance_object = ASTModel.find_instance(instance_list_copy, widget.name)
201            if new_instance_object is not None:
202                # If found, replace widget's local copy
203                self.sync_instance(new_instance_object, widget)
204                instance_list_copy.remove(new_instance_object)
205            else:
206                # Instance object for widget not found, probably deleted - so widget not necessary
207                widgets_to_remove.append(widget)
208
209        # Delete the widget (since it is not possible to delete the widget during iteration)
210        for widget in widgets_to_remove:
211            self.remove_instance_widget(widget)
212
213        for instance in instance_list_copy:
214            # For all new instances (instances without widget counterpart)
215            # Make a new widget
216            assert isinstance(instance, Instance)
217
218            new_widget = InstanceWidget(self.context_menu)
219            new_widget.color = self.random_color_generator()
220            self.sync_instance(instance, new_widget)
221            new_widget.widget_moved.connect(self.update_view)
222
223            # Add to internal list of widgets
224            self.widget_instances.append(new_widget)
225
226            # Add to scene
227            self.reposition_instance_widget(new_widget, 0, 0)
228
229        self.clear_connection_widgets()
230
231        # Create connection widgets for all connections in assembly
232        # Instead of syncing connections, ConnectionWidgets are considered disposable. Just delete all and remake them
233        assert isinstance(self.ast.assembly, Assembly)
234        for connection in self.ast.assembly.connections:
235
236            assert isinstance(connection, Connection)
237
238            for from_instance_end in connection.from_ends:
239                assert isinstance(from_instance_end, ConnectionEnd)
240                from_instance = from_instance_end.instance
241                assert isinstance(from_instance, Instance)
242
243                for to_instance_end in connection.to_ends:
244                    assert isinstance(to_instance_end, ConnectionEnd)
245                    to_instance = to_instance_end.instance
246                    assert isinstance(to_instance, Instance)
247
248                    new_connection_widget = self.create_connection_widget(connection,
249                                                                          from_instance.name,
250                                                                          from_instance_end.interface.name,
251                                                                          to_instance.name,
252                                                                          to_instance_end.interface.name)
253
254                    self.connection_widgets.append(new_connection_widget)
255                    self.add_connection_widget(new_connection_widget)
256
257    # -- Instances Methods --
258
259    @staticmethod
260    def sync_instance(instance, widget):
261        """
262        Sync between Instance object and InstanceWidget object
263        :param instance: the model camkes.ast.Instance to sync from
264        :param widget: the View.InstanceWidget object to be synced to
265        """
266
267        assert isinstance(instance, Instance)
268        assert isinstance(widget, InstanceWidget)
269
270        component = instance.type
271        assert isinstance(component, Component)
272
273        widget.name = instance.name
274        widget.component_type = component.name
275
276        widget.control = component.control
277        widget.hardware = component.hardware
278
279        for provide in component.provides:
280            assert isinstance(provide, Provides)
281            widget.add_provide(provide.name, provide.type.name)
282
283        for use in component.uses:
284            assert isinstance(use, Uses)
285            widget.add_use(use.name, use.type.name)
286
287        for emit in component.emits:
288            assert isinstance(emit, Emits)
289            widget.add_emit(emit.name, emit.type)
290
291        for consumes in component.consumes:
292            assert isinstance(consumes, Consumes)
293            widget.add_consume(consumes.name, consumes.type, consumes.optional)  # Optional bool
294
295        for dataport in component.dataports:
296            assert isinstance(dataport, Dataport)
297            widget.add_dataport(dataport.name, dataport.type, dataport.optional)  # Optional bool
298
299            # TODO add attributes, mutex and semaphores
300
301    def reposition_instance_widget(self, new_widget, x_pos, y_pos):
302        """
303        Add widget (or reposition) to scene.
304        :param new_widget: widget to be added (or repositioned)
305        :param x_pos: Middle of widget (x value)
306        :param y_pos: Middle of widget (y value)
307        :return:
308        """
309
310        assert isinstance(new_widget, InstanceWidget)
311
312        if new_widget not in [x for x in self.scene().items()
313                              if isinstance(x, InstanceWidget)]:
314            self.scene().addItem(new_widget)
315            new_widget.setZValue(5)  # Foreground of connections
316
317        # Position given is middle of widget. Convert to top-left corner and set as widget's position.
318        new_widget.setPos(x_pos - (new_widget.preferredSize().width() / 2),
319                          y_pos - (new_widget.preferredSize().height() / 2))
320
321    def remove_instance_widget(self, old_widget):
322        """
323        Remove widget from scene. (Effectively delete widget)
324        :param old_widget: widget to be deleted.
325        """
326
327        # Remove from vector list
328        self.widget_instances.remove(old_widget)
329
330        # Remove from scene
331        scene = self.scene()
332        assert isinstance(scene, QtWidgets.QGraphicsScene)
333        scene.removeItem(old_widget)
334
335    # -- Connection Methods --
336
337    def create_connection_widget(self, connection, from_instance, from_interface, to_instance, to_interface):
338        """
339        Create connection widget from the given model equivalents
340        :param connection: camkes.ast.Connection object to be passed to
341        :param from_instance: The camkes.ast.Instance on the From side.
342        :param from_interface: The camkes.ast.Interface associated with the From Instance
343        :param to_instance: The camkes.ast.Instance on the To side.
344        :param to_interface: The camkes.ast.Interface associated with the To Instance
345        :return: ConnectionWidget object
346        """
347
348        # Get source and destination widgets
349        source_widget = self.find_instance_widget(from_instance)
350        assert source_widget is not None
351
352        dest_widget = self.find_instance_widget(to_instance)
353        assert dest_widget is not None
354
355        # Create appropriate connection widget (based on type)
356        if connection.type.from_type == Common.Dataport:
357            new_connection_widget = DataportWidget(name=connection.name,
358                                                   con_type=connection.type.name,
359                                                   source=source_widget,
360                                                   source_type=connection.type.from_type,
361                                                   source_inf_name=from_interface,
362                                                   dest=dest_widget,
363                                                   dest_type=connection.type.to_type,
364                                                   dest_inf_name=to_interface)
365        elif connection.type.from_type == Common.Procedure:
366            new_connection_widget = ProcedureWidget(name=connection.name,
367                                                    con_type=connection.type.name,
368                                                    source=source_widget,
369                                                    source_type=connection.type.from_type,
370                                                    source_inf_name=from_interface,
371                                                    dest=dest_widget,
372                                                    dest_type=connection.type.to_type,
373                                                    dest_inf_name=to_interface)
374        elif connection.type.from_type == Common.Event:
375            new_connection_widget = EventWidget(name=connection.name,
376                                                con_type=connection.type.name,
377                                                source=source_widget,
378                                                source_type=connection.type.from_type,
379                                                source_inf_name=from_interface,
380                                                dest=dest_widget,
381                                                dest_type=connection.type.to_type,
382                                                dest_inf_name=to_interface)
383        else:
384            new_connection_widget = ConnectionWidget(name=connection.name,
385                                                     con_type=connection.type.name,
386                                                     source=source_widget,
387                                                     source_type=connection.type.from_type,
388                                                     source_inf_name=from_interface,
389                                                     dest=dest_widget,
390                                                     dest_type=connection.type.to_type,
391                                                     dest_inf_name=to_interface)
392
393        return new_connection_widget
394
395    def add_connection_widget(self, new_connection):
396        """
397        Add connection to scene
398        :param new_connection: widget to be added
399        """
400
401        assert isinstance(new_connection, ConnectionWidget)
402        self.scene().addItem(new_connection)
403        new_connection.setZValue(4)  # Behind all instance widgets (but above hidden instances and connection)
404
405    def clear_connection_widgets(self):
406        """
407        Remove all connections from scene. Effectively deleting all existing connections
408        """
409
410        scene = self.scene()
411        assert isinstance(scene, QtWidgets.QGraphicsScene)
412
413        for connection in self.connection_widgets:
414            scene.removeItem(connection)  # Remove from scene
415            connection.delete()  # Let connection do some cleaning up - (delete itself from the instance widgets)
416
417        del self.connection_widgets[:]  # Delete all connections widgets from internal list
418
419    # --- Layout Methods ---
420
421    def layout_from_file(self):
422        """
423        Open layout file from the same location as the .camkes file
424        """
425
426        # Attempt to open layout file.
427        with open("%s.visualCAmkES.layout" % self.get_root_location(), 'r') as layout_file:
428            # Load dictionaries (json format) from file.
429            json_input = json.load(layout_file)
430
431            for widget in self.widget_instances:
432                assert isinstance(widget, InstanceWidget)
433
434                # Get attributes for specific widget
435                attributes = json_input.get(widget.name)
436
437                # If widget doesn't exist in file, create an attribute with empty dictionary
438                if attributes is None:
439                    attributes = {}
440
441                position = attributes['position']
442                if position is not None:
443                    assert isinstance(position, list)
444                    # Reposition widget to new position
445                    self.reposition_instance_widget(widget, position[0], position[1])
446                else:
447                    # If position doesn't exist
448                    self.reposition_instance_widget(widget, self.geometry().x() / 2, self.geometry().y() / 2)
449
450                if attributes['hidden'] is not None:
451                    widget.hidden = attributes['hidden']
452
453        self.update_view()
454        self.save_layout_to_file()
455
456    def save_layout_to_file(self):
457        """
458        Update current view to file
459        :return:
460        """
461
462        node_positions = {}
463        for widget in self.widget_instances:
464            assert isinstance(widget, InstanceWidget)
465            attributes = {}
466
467            # Load centre of widget into widget's position attribute
468            position_array = [widget.pos().x() - (widget.preferredSize().width() / 2),
469                              widget.pos().y() - (widget.preferredSize().height() / 2)]
470            attributes['position'] = position_array
471
472            attributes['hidden'] = widget.hidden
473
474            node_positions[widget.name] = attributes
475
476        file_location =  "%s.visualCAmkES.layout" % self.get_root_location()
477
478        with open(file_location, 'w') as output:
479            json.dump(node_positions, output, indent=4)
480
481    def autolayout(self):
482        """
483        Using graphviz to layout the current graph to 'dot' layout.
484        """
485
486        # Create a empty graph
487        graph_viz = Digraph(engine='dot')
488
489        # For all instances, add a node, with it's size.
490        for widget_instance in self.widget_instances:
491            assert isinstance(widget_instance, InstanceWidget)
492
493            if widget_instance.hidden:
494                continue
495
496            size = widget_instance.preferredSize()
497            assert isinstance(size, QtCore.QSizeF)
498
499            graph_viz.node(widget_instance.name, width=str(size.width() / 72.0),
500                           height=str(size.height() / 72.0), shape="rect")
501
502        # For all (not hidden) connections), connect the source and destination widgets with minimum length of 2 inches
503        for connection in self.connection_widgets:
504            assert isinstance(connection, ConnectionWidget)
505            if not connection.hidden:
506                graph_viz.edge(connection.source_instance_widget.name, connection.dest_instance_widget.name,
507                               minlen=str(2))
508
509        # Generate / Render graph into 'dot' format
510        raw_dot_data = graph_viz.pipe('dot')
511        print "Graphviz rendering... the following is the dot file from graphviz"
512        print raw_dot_data
513
514        # Read dot format (using pydotplus)
515        dot_data = pydotplus.graph_from_dot_data(raw_dot_data)
516
517        # Get graphviz height
518        graph_attributes = dot_data.get_graph_defaults()
519
520        height = 0
521
522        for attribute_dict in graph_attributes:
523            if not isinstance(attribute_dict, dict):
524                continue
525
526            if attribute_dict['bb'] is not None:
527                rectange = Common.extract_numbers(attribute_dict['bb'])
528                height = rectange[1][1] - rectange[0][1]
529
530        # For all instances, apply new position to widgets.
531
532        for instance_widget in self.widget_instances:
533            assert isinstance(instance_widget, InstanceWidget)
534
535            if instance_widget.hidden:
536                continue
537
538            # Get instance's name
539            instance_name = instance_widget.name
540
541            # Get the node representing this instance, and get its attributes
542            node_list = dot_data.get_node(instance_name)
543
544            if len(node_list) < 1:
545                # Name may be quoted due to special characters
546                quoted_name = '"%s"' % instance_name
547                node_list = dot_data.get_node(quoted_name)
548
549            assert len(node_list) == 1  # Should only be one node
550            assert isinstance(node_list[0], pydotplus.Node)
551            node_attributes_dict = node_list[0].get_attributes()
552
553            # Extract position of the node
554            node_position_list = Common.extract_numbers(node_attributes_dict['pos'])
555            assert len(node_position_list) is 1  # Should only be one position
556            node_position = node_position_list[0]
557
558            self.reposition_instance_widget(instance_widget, x_pos=node_position[0],
559                                            y_pos=math.fabs(height - node_position[1]))
560
561        self.update_view()
562        self.save_layout_to_file()
563
564    # --- EVENTS ---
565
566    # -- UI --
567
568    def resizeEvent(self, resize_event):
569        """
570        When resizing happens, update the corner buttons to the right position.
571        :param resize_event:
572        """
573
574        assert isinstance(resize_event, QtGui.QResizeEvent)
575        super(GraphWidget, self).resizeEvent(resize_event)
576
577        self.update_outer_ui()
578
579    def update_view(self):
580        """
581        Updates the view to be position correctly. Currently resizes scene to minimum size required.
582        """
583
584        rect = self.sceneRect()
585        assert isinstance(rect, QtCore.QRectF)
586
587        smallest_x = 0
588        smallest_y = 0
589        largest_x = 0
590        largest_y = 0
591
592        # Goal is to find the top-left point and bottom-right point of the graph.
593        # To find top-left - find the InstanceWidget on the most top-left corner
594        # To find bottom-right - find the Instance widget on the most bottom-right (including widget itself)
595        for instance_widget in [x for x in self.scene().items() if isinstance(x, InstanceWidget)]:
596            assert isinstance(instance_widget, InstanceWidget)
597
598            if instance_widget.scenePos().x() < smallest_x:
599                smallest_x = instance_widget.scenePos().x()
600
601            if instance_widget.scenePos().y() < smallest_y:
602                smallest_y = instance_widget.scenePos().y()
603
604            bottom_corner = instance_widget.scenePos() + QtCore.QPointF(instance_widget.boundingRect().width(),
605                                                                        instance_widget.boundingRect().height())
606
607            if largest_x < bottom_corner.x():
608                largest_x = bottom_corner.x()
609
610            if largest_y < bottom_corner.y():
611                largest_y = bottom_corner.y()
612
613        new_rect = QtCore.QRectF(smallest_x, smallest_y, largest_x - smallest_x, largest_y - smallest_y)
614
615        self.setSceneRect(new_rect)
616
617    def update_outer_ui(self):
618        """
619        Position all the bottom-right corner buttons in the right place.
620        :return:
621        """
622
623        bottom_corner = self.geometry().bottomRight()
624
625        zoom_in_position = bottom_corner - QtCore.QPoint(self.zoom_in_button.sizeHint().width(),
626                                                         self.zoom_in_button.sizeHint().height()) \
627                           - QtCore.QPoint(20, 40)  # 40 due to some weird behaviour with File Menus
628
629        self.zoom_in_button.move(zoom_in_position)
630        self.zoom_in_button.show()
631
632        zoom_out_position = zoom_in_position - QtCore.QPoint(self.zoom_out_buttom.sizeHint().width() + 20, 0)
633
634        self.zoom_out_buttom.move(zoom_out_position)
635        self.zoom_out_buttom.show()
636
637        save_picture_position = zoom_in_position - QtCore.QPoint(self.save_picture_button.sizeHint().width() -
638                                                                 self.zoom_in_button.sizeHint().width(),
639                                                                 self.zoom_out_buttom.sizeHint().height() + 20)
640
641        self.save_picture_button.move(save_picture_position)
642        self.save_picture_button.show()
643
644        autolayout_position = save_picture_position - QtCore.QPoint(self.autolayout_button.sizeHint().width() -
645                                                                    self.save_picture_button.sizeHint().width(),
646                                                                    self.save_picture_button.sizeHint().height() + 20)
647
648        self.autolayout_button.move(autolayout_position)
649        self.autolayout_button.show()
650
651    def close_context_menu(self):
652        """
653        Close context (right-click) menu
654        """
655
656        if self._context_menu is not None:
657            self._context_menu.widget().close()
658
659    # -- Mouse events --
660    def mousePressEvent(self, mouse_event):
661        """
662        When mouse is pressed, handles the closing of context menu.
663        :param mouse_event: QMouseEvent object
664        """
665
666        assert isinstance(mouse_event, QtGui.QMouseEvent)
667
668        # Get scene position from global position
669        scene_position = self.mapToScene(mouse_event.pos())
670
671        item = self.scene().itemAt(scene_position, self.transform())
672
673        # If not pressed on context menu, close the menu.
674        if not isinstance(item, QtWidgets.QGraphicsProxyWidget):
675            self.close_context_menu()
676        elif not isinstance(item.widget(), QtWidgets.QMenu):
677            self.close_context_menu()
678
679        super(GraphWidget, self).mousePressEvent(mouse_event)
680
681    def mouseDoubleClickEvent(self, mouse_event):
682        # Get scene position from global position
683        scene_position = self.mapToScene(mouse_event.pos())
684
685        item = self.scene().itemAt(scene_position, self.transform())
686
687        if mouse_event.button() == QtCore.Qt.LeftButton:
688            # If item has property to show and edit
689
690            if isinstance(item, PropertyInterface):
691                self.property_widget_dock.setWidget(item.property_widget)
692                self.property_widget_dock.setVisible(True)
693
694    def mouseReleaseEvent(self, mouse_event):
695        """
696        When mouse is released, handles the saving of layout.
697        :param mouse_event: QMouseEvent object
698        """
699
700        super(GraphWidget, self).mouseReleaseEvent(mouse_event)
701
702        if self.ast:
703            self.save_layout_to_file()
704
705    # -- Button clicks --
706
707    def show_all_components(self):
708        """
709        Unhide all hidden components.
710        """
711
712        for widget in self.widget_instances:
713            widget.hidden = False
714
715    def zoom_in(self):
716
717        self.scale(1.1, 1.1)
718
719    def zoom_out(self):
720
721        self.scale(0.9, 0.9)
722
723    def save_picture(self):
724        """
725        Invokes the GUI routine of saving of an image
726        """
727
728        if self.ast is None:
729            return
730
731        # Ask user whether they want png or svg
732        # If png, ask what dimensions
733        save_option_dialog = SaveOptionDialog(self, self.sceneRect())
734        assert isinstance(save_option_dialog, SaveOptionDialog)
735        dialog_code = save_option_dialog.exec_()
736
737        if not dialog_code:
738            return
739
740        file_filter = ""
741
742        if save_option_dialog.picture_type == save_option_dialog.PNG:
743            file_filter = "Image (*.png)"
744        elif save_option_dialog.picture_type == save_option_dialog.SVG:
745            file_filter = "Scalable Vector Graphics (*.svg)"
746
747        filename = QtWidgets.QFileDialog.getSaveFileName(caption="Save file",
748                                                         directory=self.get_root_location(with_name=True),
749                                                         filter=file_filter)
750
751        image_location = filename[0]  # getSaveFileName returns a tuple. First index of tuple is the file name
752
753        if image_location.rfind('.') != -1:
754            image_location = image_location[:image_location.rfind('.')]
755
756        if len(image_location) <= 0:
757            # Image location is not valid
758            return
759
760        rect = self.sceneRect()
761        rect.adjust(-50, -50, 50, 50)
762
763        painter = QtGui.QPainter()
764
765        if save_option_dialog.picture_type == save_option_dialog.PNG:
766            image = QtGui.QImage(save_option_dialog.user_width,
767                                 save_option_dialog.user_height,
768                                 QtGui.QImage.Format_ARGB32)
769            image.fill(QtCore.Qt.transparent)
770
771            painter.begin(image)
772            painter.setRenderHint(QtGui.QPainter.Antialiasing)
773            self.scene().render(painter, source=rect)
774            painter.end()
775
776            image.save("%s.png" % image_location)
777
778        elif save_option_dialog.picture_type == save_option_dialog.SVG:
779            generator = QtSvg.QSvgGenerator()
780            generator.setFileName("%s.svg" % image_location)
781            generator.setSize(QtCore.QSize(rect.width(), rect.height()))
782            # generator.setViewBox(rect)
783            generator.setTitle(save_option_dialog.user_title)
784            generator.setDescription(save_option_dialog.user_description)
785
786            painter.begin(generator)
787            painter.setRenderHint(QtGui.QPainter.Antialiasing)
788            self.scene().render(painter, source=rect)
789            painter.end()
790
791        else:
792            return
793
794    # --- HELPER FUNCTIONS ---
795
796    def random_color_generator(self):
797        """
798
799        :return:
800        """
801
802        if self._color_seed is None:
803            self._color_seed = 0.9214
804
805        color = QtGui.QColor()
806        color = color.fromHslF(self._color_seed, 1, 0.78, 1)
807
808        self._color_seed = (self._color_seed + 0.618033988749895) % 1
809
810        return color
811
812    def find_instance_widget(self, name):
813        """
814
815        :param name:
816        :return:
817        """
818
819        for instance in self.widget_instances:
820            assert isinstance(instance, InstanceWidget)
821            if instance.name == name:
822                return instance
823
824        return None
825
826    def get_root_location(self, with_name=False):
827        """
828
829        :param with_name:
830        :return:
831        """
832
833        assembly = self.ast.assembly
834        assert isinstance(assembly, Assembly)
835        location = assembly.location
836        assert isinstance(location, SourceLocation)
837
838        if with_name:
839            return location.filename[:location.filename.rfind('.')]
840        else:
841            find_slash = location.filename.rfind('/')
842            if find_slash == -1:
843                find_slash = location.filename.rfind('\\')  # For windows
844            return location.filename[:find_slash + 1]
845