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