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 math, six 15 16from PyQt5 import QtGui, QtWidgets, QtCore 17 18from Interface.Property import PropertyInterface 19from Instance_Widget import InstanceWidget 20from Connection_Property_Widget import ConnectionPropertyWidget 21 22class ConnectionWidget(QtWidgets.QGraphicsItem, PropertyInterface): 23 """ 24 ConnectionWidget - a View represetation for the camkes.ast.Connection objects. 25 This widget is a one time use. Instead of attempting to sync this connection with the model, just make a new object. 26 """ 27 28 @property 29 def hidden(self): 30 return self._hidden 31 32 # Hidden will only work if a source and destination widget exist 33 # It will only be set false, if both source and destination is false 34 @hidden.setter 35 def hidden(self, value): 36 assert isinstance(value, bool) 37 self._hidden = value or self.source_instance_widget.hidden or \ 38 self.dest_instance_widget.hidden 39 if self._hidden: 40 self.setZValue(2) 41 else: 42 self.setZValue(4) 43 44 @property 45 def name(self): 46 return self._connection_name 47 48 @name.setter 49 def name(self, value): 50 assert isinstance(value, six.string_types) 51 self._connection_name = value 52 self.setToolTip("%s : %s" % (value, self.connection_type)) 53 54 @property 55 def connection_type(self): 56 return self._connection_type 57 58 @connection_type.setter 59 def connection_type(self, value): 60 assert isinstance(value, six.string_types) 61 self._connection_type = value 62 self.setToolTip("%s : %s" % (self.name, value) ) 63 64 @property 65 def path(self): 66 # Lazy Instantiation 67 if self._path is None: 68 self._path = QtGui.QPainterPath() 69 return self._path 70 71 def clear_path(self): 72 self._path = None 73 74 # Source information 75 @property 76 def source_instance_widget(self): 77 return self._source_instance_widget 78 79 @source_instance_widget.setter 80 def source_instance_widget(self, value): 81 assert isinstance(value, InstanceWidget) 82 self._source_instance_widget = value 83 84 @property 85 def source_connection_type(self): 86 return self._source_connection_type 87 88 @source_connection_type.setter 89 def source_connection_type(self, value): 90 assert isinstance(value, six.string_types) 91 self._source_connection_type = value 92 93 @property 94 def source_interface_name(self): 95 return self._source_interface_name 96 97 @source_interface_name.setter 98 def source_interface_name(self, value): 99 assert isinstance(value, six.string_types) 100 self._source_interface_name = value 101 102 # Destination Information 103 @property 104 def dest_instance_widget(self): 105 return self._dest_instance_widget 106 107 @dest_instance_widget.setter 108 def dest_instance_widget(self, value): 109 assert isinstance(value, InstanceWidget) 110 self._dest_instance_widget = value 111 112 @property 113 def dest_connection_type(self): 114 return self._dest_connection_type 115 116 @dest_connection_type.setter 117 def dest_connection_type(self, value): 118 assert isinstance(value, six.string_types) 119 self._dest_connection_type = value 120 121 @property 122 def dest_interface_name(self): 123 return self._dest_interface_name 124 125 @dest_interface_name.setter 126 def dest_interface_name(self, value): 127 assert isinstance(value, six.string_types) 128 self._dest_interface_name = value 129 130 @property 131 def source_pos(self): 132 return self._source_pos 133 134 @property 135 def source_angle(self): 136 return self._source_angle 137 138 def set_source_pos_angle(self, pos, angle): 139 assert isinstance(pos, QtCore.QPointF) 140 self._source_pos = pos 141 self._source_angle = angle 142 143 self.update_path() 144 145 @property 146 def dest_pos(self): 147 return self._dest_pos 148 149 @property 150 def dest_angle(self): 151 return self._dest_angle 152 153 def set_dest_pos_angle(self, pos, angle): 154 assert isinstance(pos, QtCore.QPointF) 155 self._dest_pos = pos 156 self._dest_angle = angle 157 158 self.update_path() 159 160 @PropertyInterface.property_widget.getter 161 def property_widget(self): 162 newWidget = ConnectionPropertyWidget(self) 163 return newWidget 164 165 # --- INITIALISATION --- 166 167 def __init__(self, name, con_type, source, source_type, source_inf_name, dest, dest_type, dest_inf_name): 168 super(ConnectionWidget, self).__init__() 169 170 self._connection_name = None 171 self._connection_name = name 172 self._connection_type = None 173 self.connection_type = con_type 174 175 self.setToolTip("%s : %s" % (name, con_type) ) 176 177 # Get points from attributes of the edge 178 self._source_pos = None 179 self._dest_pos = None 180 self._source_angle = None 181 self._dest_angle = None 182 183 self._path = None 184 self._hidden = False 185 186 assert isinstance(source, InstanceWidget) 187 self._source_instance_widget = source 188 189 self._source_connection_type = None 190 self.source_connection_type = source_type 191 192 self._source_interface_name = None 193 self.source_interface_name = source_inf_name 194 195 assert isinstance(dest, InstanceWidget) 196 self._dest_instance_widget = dest 197 198 self._dest_connection_type = None 199 self.dest_connection_type = dest_type 200 201 self._dest_interface_name = None 202 self.dest_interface_name = dest_inf_name 203 204 self.source_instance_widget.add_connection(self) 205 self.dest_instance_widget.add_connection(self) 206 207 # --- UI FUNCTION --- 208 209 def paint(self, q_painter, style_option, widget=None): 210 """ 211 Deals with the drawing of the connection when asked to paint. 212 Realies on self.path to be set 213 :param q_painter: QPainter to paint on 214 :param style_option: styling options - unused atm 215 :param widget: unused atm 216 """ 217 218 # assert isinstance(style_option, QtWidgets.QStyleOptionGraphicsItem) 219 # assert isinstance(widget, QtWidgets.QWidget) 220 221 assert isinstance(q_painter, QtGui.QPainter) 222 223 q_painter.setRenderHint(QtGui.QPainter.Antialiasing) 224 225 if self.source_pos is not None and self.dest_pos is not None: 226 pen = QtGui.QPen(QtGui.QColor(66, 66, 66)) 227 pen.setWidth(2) 228 229 pen_color = pen.color() 230 if self.hidden: 231 pen_color.setAlphaF(0.2) 232 else: 233 pen_color.setAlphaF(1) 234 pen.setColor(pen_color) 235 236 q_painter.setPen(pen) 237 q_painter.drawPath(self.path) 238 # q_painter.drawLine(self.source_pos, self.dest_pos) 239 240 def boundingRect(self): 241 """ 242 Calculates bounding rect from current self.path with 2.5 margin 243 :return: QRectF 244 """ 245 246 rect = self.path.boundingRect() 247 assert isinstance(rect, QtCore.QRectF) 248 rect.adjust(-2.5, -2.5, 2.5, 2.5) 249 250 return rect 251 252 def shape(self): 253 """ 254 Returns the self.path with width = 5 255 :return: QPainterPath 256 """ 257 258 stroker = QtGui.QPainterPathStroker() 259 stroker.setWidth(5) 260 return stroker.createStroke(self.path) 261 262 # -- Connector drawing -- 263 264 def update_path(self): 265 """ 266 Recreates path based on the current (which maybe newly set) source and destination points. 267 Relies on: self.source_pos and self.dest_pos to be set. 268 """ 269 270 # If there is a source and destination point to work with 271 if self.source_pos and self.dest_pos: 272 self.clear_path() 273 274 # Start at source point 275 self.path.moveTo(self.source_pos) 276 277 # For a beizer curve, there are three points that make a curve (for a cubic beizer curve). 278 # The middle point is called the control point (since it controls the curvature). 279 # There are two curves, one on either side. In the middle, a icon will be drawn if there is space. 280 281 source_control_point = self.get_control_point(self.source_pos, self.dest_pos, self.source_angle) 282 283 destination_control_point = self.get_control_point(self.dest_pos, self.source_pos, self.dest_angle) 284 285 # Find final points for both source and destionation 286 if source_control_point != destination_control_point: 287 # It doesn't make sense to compare points as < or >, because its a bit ambigious. 288 # If we consider * to be source points, and X to be destination point, the following 289 # will not occur due to the get_control_point algorithm. 290 # * - - - X - - * - - X 291 # It would normally look like: * - - - * - - X - - - X 292 293 # Find vector from source to dest 294 s_to_d = destination_control_point - source_control_point 295 296 # Get length, and then find source and destination final points. 297 # if less than 30 - no enough for icon. 298 # otherwise keep 30 pixel space for icon when finding final points 299 length = math.sqrt(s_to_d.dotProduct(s_to_d, s_to_d)) 300 if length < 30: 301 middle_vector = self.change_vector_length(s_to_d, length / 2) 302 303 source_final_point = source_control_point + middle_vector 304 destination_final_point = destination_control_point - middle_vector 305 else: 306 middle_vector = self.change_vector_length(s_to_d, length / 2 - 15) 307 308 source_final_point = source_control_point + middle_vector 309 destination_final_point = destination_control_point - middle_vector 310 311 else: 312 # Make final points the same as control points. 313 source_final_point = source_control_point 314 destination_final_point = destination_control_point 315 316 # Draw source cubic beizer curve 317 self.path.quadTo(source_control_point, source_final_point) 318 319 # Draw icon (if possible) 320 self.draw_connector_type(source_final_point, destination_final_point) 321 322 # Draw dest cubic curve 323 self.path.moveTo(destination_final_point) 324 self.path.quadTo(destination_control_point, self.dest_pos) 325 326 # Let graphicitem know that geometry has changed. 327 self.prepareGeometryChange() 328 329 def draw_connector_type(self, source_point, dest_point): 330 """ 331 Draws the icon between two points. 332 Expected to be subclassed for different connector types. 333 :param source_point: QPointF - the starting point 334 :param dest_point: QPointF - the ending point 335 :return: 336 """ 337 338 assert isinstance(source_point, QtCore.QPointF) 339 assert isinstance(dest_point, QtCore.QPointF) 340 341 self.path.moveTo(source_point) 342 self.path.lineTo(dest_point) 343 344 # --- EVENTS --- 345 346 def mousePressEvent(self, mouse_event): 347 """ 348 Deals with mouse pressed events. Does nothing atm. 349 :param mouse_event: QGraphicsSceneMouseEvent 350 """ 351 assert isinstance(mouse_event, QtWidgets.QGraphicsSceneMouseEvent) 352 super(ConnectionWidget, self).mousePressEvent(mouse_event) 353 354 # --- HELPER FUNCTIONS --- 355 356 @staticmethod 357 def change_vector_length(old_point, new_length): 358 """ 359 Helper function to change the length of a vector 360 :param old_point: The current vector 361 :param new_length: The new length of the vector 362 :return: QPointF - new vector with the new length 363 """ 364 365 assert isinstance(old_point, QtCore.QPointF) 366 old_length = math.sqrt(old_point.x() * old_point.x() + old_point.y() * old_point.y()) 367 368 if old_length == 0: 369 # It doesn't make sense to extend or shorten a zero vector 370 return old_point 371 372 new_point = QtCore.QPointF((old_point.x() * new_length) / old_length, (old_point.y() * new_length) / old_length) 373 return new_point 374 375 @staticmethod 376 def get_control_point(source_pos, dest_pos, angle): 377 """ 378 Get the middle point of the cubic beizer curve. 379 To get the source control point - pass in source point as source_pos and destination point as dest_pos 380 To get the dest control porint - pass in destination point as source_pos and source point as source_pos 381 :param source_pos: QPointF - source point 382 :param dest_pos: QPointF - destination point 383 :param angle: int - the distance that the final point has to be, from the straight line 384 :return: QPointF - the middle control point 385 """ 386 387 assert isinstance(source_pos, QtCore.QPointF) 388 assert isinstance(dest_pos, QtCore.QPointF) 389 390 # Get vector from start to end 391 s_to_d = dest_pos - source_pos 392 assert isinstance(s_to_d, QtCore.QPointF) 393 394 normal_length = 10 395 396 # If the s_to_d length is less than 20, then set normal_length to half s_to_d length 397 # This will mean that source and destination control points are the same 398 if (math.sqrt(s_to_d.dotProduct(s_to_d, s_to_d)) / 2) < normal_length: 399 normal_length = math.sqrt(s_to_d.dotProduct(s_to_d, s_to_d)) / 2 400 401 s_to_d = ConnectionWidget.change_vector_length(s_to_d, normal_length) 402 perpend_vector = QtCore.QPointF(-s_to_d.y(), s_to_d.x()) 403 perpend_vector = ConnectionWidget.change_vector_length(perpend_vector, angle) 404 405 source_control_point = source_pos + s_to_d + perpend_vector 406 407 return source_control_point 408 409 def delete(self): 410 """ 411 Before removing the connection, make sure to call this function. 412 :return: 413 """ 414 415 # TODO: Delete connection from source & destination 416 self.source_instance_widget.remove_connection(self) 417 self.dest_instance_widget.remove_connection(self) 418 419class DataportWidget(ConnectionWidget): 420 """ 421 Subclass of ConnectionWidget, which deals with the drawing of the Dataport icon 422 for the connection type. 423 """ 424 425 # Draws a rectangle 426 def draw_connector_type(self, source_point, dest_point): 427 assert isinstance(source_point, QtCore.QPointF) 428 assert isinstance(dest_point, QtCore.QPointF) 429 430 if source_point == dest_point: 431 return 432 433 s_to_d = dest_point - source_point 434 assert isinstance(s_to_d, QtCore.QPointF) 435 436 # Convert s_to_d to perpendicular vector 437 old_x = s_to_d.x() 438 s_to_d.setX(-s_to_d.y()) 439 s_to_d.setY(old_x) 440 441 new_vector = self.change_vector_length(s_to_d, 10) 442 top_left = source_point + new_vector 443 top_right = dest_point + new_vector 444 bottom_left = source_point - new_vector 445 bottom_right = dest_point - new_vector 446 447 self.path.moveTo(top_left) 448 self.path.lineTo(top_right) 449 self.path.lineTo(bottom_right) 450 self.path.lineTo(bottom_left) 451 self.path.lineTo(top_left) 452 453class ProcedureWidget(ConnectionWidget): 454 """ 455 Subclass of ConnectionWidget, which deals with the drawing of the Procedure icon 456 for the connection type. 457 """ 458 459 # Draws a circle 460 def draw_connector_type(self, source_point, dest_point): 461 assert isinstance(source_point, QtCore.QPointF) 462 assert isinstance(dest_point, QtCore.QPointF) 463 464 if source_point == dest_point: 465 return 466 467 s_to_d = dest_point - source_point 468 assert isinstance(s_to_d, QtCore.QPointF) 469 s_to_d = self.change_vector_length(s_to_d, 5) 470 471 start_point = source_point + s_to_d 472 end_point = dest_point - s_to_d 473 474 self.path.moveTo(source_point) 475 self.path.lineTo(start_point) 476 477 centre_point = self.change_vector_length(s_to_d, 14) + start_point 478 479 new_vector = QtCore.QPointF(-s_to_d.y(), s_to_d.x()) 480 new_vector = self.change_vector_length(new_vector, 14) 481 arc_start_point = centre_point + new_vector 482 483 self.path.moveTo(arc_start_point) 484 485 top_left = centre_point - QtCore.QPointF(14, 14) 486 bottom_right = centre_point + QtCore.QPointF(14, 14) 487 rect = QtCore.QRectF(top_left, bottom_right) 488 489 straight_point = QtCore.QPointF(1, 0) 490 491 start_straight_dot_product = new_vector.dotProduct(new_vector, straight_point) 492 perpend_length = math.sqrt(new_vector.x() * new_vector.x() + new_vector.y() * new_vector.y()) 493 straight_length = math.sqrt(straight_point.x() * straight_point.x() + straight_point.y() * straight_point.y()) 494 495 start_angle = math.degrees(math.acos(start_straight_dot_product / (perpend_length * straight_length))) 496 497 if new_vector.y() > 0: 498 start_angle = 360 - start_angle 499 500 self.path.arcTo(rect, start_angle, -180) 501 502 top_left = centre_point - QtCore.QPointF(6, 6) 503 bottom_right = centre_point + QtCore.QPointF(6, 6) 504 rect = QtCore.QRectF(top_left, bottom_right) 505 506 self.path.addEllipse(rect) 507 508 self.path.moveTo(end_point) 509 self.path.lineTo(dest_point) 510 511class EventWidget(ConnectionWidget): 512 """ 513 Subclass of ConnectionWidget, which deals with the drawing of the Event icon 514 for the connection type. 515 """ 516 517 # Draws a triangle 518 def draw_connector_type(self, source_point, dest_point): 519 assert isinstance(source_point, QtCore.QPointF) 520 assert isinstance(dest_point, QtCore.QPointF) 521 522 if source_point == dest_point: 523 return 524 525 s_to_d = dest_point - source_point 526 assert isinstance(s_to_d, QtCore.QPointF) 527 s_to_d = self.change_vector_length(s_to_d, 5) 528 529 start_point = source_point + s_to_d 530 end_point = dest_point - s_to_d 531 532 new_vector = QtCore.QPointF(-s_to_d.y(), s_to_d.x()) 533 new_vector = self.change_vector_length(new_vector, 10) 534 535 self.path.moveTo(source_point) 536 self.path.lineTo(start_point) 537 538 top_left = start_point + new_vector 539 bottom_left = start_point - new_vector 540 541 s_to_d = self.change_vector_length(s_to_d, 14) 542 543 first_end = start_point + s_to_d 544 545 self.path.moveTo(top_left) 546 self.path.lineTo(bottom_left) 547 self.path.lineTo(first_end) 548 self.path.lineTo(top_left) 549 550 s_to_d = self.change_vector_length(s_to_d, 6) 551 top_left = top_left + s_to_d 552 bottom_left = bottom_left + s_to_d 553 554 self.path.moveTo(top_left) 555 self.path.lineTo(end_point) 556 self.path.lineTo(bottom_left) 557 558 self.path.moveTo(end_point) 559 560 self.path.lineTo(dest_point) 561